Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions projects/tests/test_project_workspace_vacancies.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def test_workspace_vacancy_reuses_legacy_short_contract(self):
"datetime_closed",
"response_count",
"date_create_time",
"required_experience",
"work_schedule",
"work_format",
"salary",
"city",
},
)
self.assertEqual(item["project"], project.pk)
Expand Down
8 changes: 7 additions & 1 deletion vacancy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class ChoicesMixin:

@classmethod
def choices(cls):
"""Return a list of tuples (value, display_name) for choices."""
Expand Down Expand Up @@ -51,3 +50,10 @@ class WorkFormat(ChoicesMixin, Enum):
REMOTE: str = "удаленная работа"
OFFICE: str = "работа в офисе"
HYBRID: str = "смешанный формат"

@classmethod
def from_display(cls, display_value):
"""Нормализует legacy-значение Angular в канонический смешанный формат."""
if display_value == "смешанная":
display_value = cls.HYBRID.value
return super().from_display(display_value)
20 changes: 20 additions & 0 deletions vacancy/migrations/0010_vacancy_city.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.11 on 2026-08-25 20:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vacancy", "0009_vacancy_specialization"),
]

operations = [
migrations.AddField(
model_name="vacancy",
name="city",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="Город"
),
),
]
7 changes: 7 additions & 0 deletions vacancy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Vacancy(models.Model):
required_experience: CharField (choice).
work_schedule: CharField (choice).
work_format: CharField (choice).
city: CharField city for office and hybrid vacancies.
project: A ForeignKey referring to the Company model.
is_active: A boolean indicating if Vacancy is active.
datetime_created: A DateTimeField indicating date of creation.
Expand Down Expand Up @@ -57,6 +58,12 @@ class Vacancy(models.Model):
null=True,
verbose_name="Формат работы",
)
city = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name="Город",
)
salary = models.IntegerField(
blank=True,
null=True,
Expand Down
36 changes: 36 additions & 0 deletions vacancy/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ def to_representation(self, instance):
return representation


class VacancyCityValidationMixin:
"""Проверяет город по итоговому формату вакансии для create, PUT и PATCH."""

CITY_REQUIRED_MESSAGE = "Для офисного или смешанного формата укажите город."

def validate(self, attrs):
attrs = super().validate(attrs)
instance = getattr(self, "instance", None)
work_format = attrs.get("work_format", getattr(instance, "work_format", None))
city = attrs.get("city", getattr(instance, "city", None))

if work_format == WorkFormat.REMOTE.name.lower():
attrs["city"] = None
elif work_format in {
WorkFormat.OFFICE.name.lower(),
WorkFormat.HYBRID.name.lower(),
}:
if not city or not city.strip():
raise serializers.ValidationError({"city": self.CITY_REQUIRED_MESSAGE})
attrs["city"] = city.strip()

return attrs


class AbstractVacancyReadOnlyFields(serializers.Serializer):
"""Общие вычисляемые поля read-only контрактов вакансии."""

Expand Down Expand Up @@ -98,6 +122,7 @@ class ProjectVacancyListSerializer(
VacancyCreationDateSerializerMixin,
serializers.ModelSerializer,
AbstractVacancyReadOnlyFields,
AbstractVacancyEnumFields,
RequiredSkillsSerializerMixin[Vacancy],
):
class Meta:
Expand All @@ -113,6 +138,11 @@ class Meta:
"datetime_closed",
"response_count",
"date_create_time",
"required_experience",
"work_schedule",
"work_format",
"salary",
"city",
]


Expand All @@ -137,6 +167,7 @@ class Meta:


class VacancyDetailSerializer(
VacancyCityValidationMixin,
VacancyCreationDateSerializerMixin,
serializers.ModelSerializer,
AbstractVacancyReadOnlyFields,
Expand Down Expand Up @@ -165,6 +196,7 @@ class Meta:
"work_schedule",
"work_format",
"salary",
"city",
]
read_only_fields = ["project"]

Expand All @@ -187,6 +219,7 @@ class Meta:
"datetime_closed",
"response_count",
"date_create_time",
"city",
]


Expand Down Expand Up @@ -220,6 +253,7 @@ class Meta:
"work_schedule",
"work_format",
"salary",
"city",
]
read_only_fields = fields

Expand Down Expand Up @@ -261,6 +295,7 @@ def validate(self, data):


class ProjectVacancyCreateListSerializer(
VacancyCityValidationMixin,
VacancyCreationDateSerializerMixin,
serializers.ModelSerializer,
AbstractVacancyReadOnlyFields,
Expand Down Expand Up @@ -321,6 +356,7 @@ class Meta:
"work_schedule",
"work_format",
"salary",
"city",
]


Expand Down
2 changes: 2 additions & 0 deletions vacancy/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def create_vacancy(
work_schedule: str | None = WorkSchedule.FULL_TIME.name.lower(),
work_format: str | None = WorkFormat.REMOTE.name.lower(),
salary: int | None = 100000,
city: str | None = None,
) -> Vacancy:
vacancy = Vacancy.objects.create(
project=project or create_project(),
Expand All @@ -107,6 +108,7 @@ def create_vacancy(
work_schedule=work_schedule,
work_format=work_format,
salary=salary,
city=city,
)
if datetime_created is not None:
Vacancy.objects.filter(pk=vacancy.pk).update(datetime_created=datetime_created)
Expand Down
Loading
Loading