Skip to content

Commit 83c622b

Browse files
committed
Added vacancy filters + new salary field
1 parent 8813dd1 commit 83c622b

5 files changed

Lines changed: 233 additions & 17 deletions

File tree

vacancy/constants.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,48 @@ class ChoicesMixin:
66
@classmethod
77
def choices(cls):
88
"""Return a list of tuples (value, display_name) for choices."""
9-
return [(item.value, item.value) for item in cls]
9+
return [(item.name.lower(), item.value) for item in cls]
10+
11+
@classmethod
12+
def from_display(cls, display_value):
13+
"""Convert display value to Enum name."""
14+
for item in cls:
15+
if item.value == display_value:
16+
return item.name.lower()
17+
elif display_value is None:
18+
return None
19+
raise ValueError(f"Invalid display value: {display_value}")
20+
21+
@classmethod
22+
def to_display(cls, name_value):
23+
"""Convert Enum name to display value."""
24+
for item in cls:
25+
if item.name.lower() == name_value:
26+
return item.value
27+
elif name_value is None:
28+
return None
29+
raise ValueError(f"Invalid name value: {name_value}")
1030

1131

1232
class WorkExperience(ChoicesMixin, Enum):
1333

14-
NO_EXPERIENCE: str = "Без опыта"
15-
UP_TO_A_YEAR: str = "До 1 года"
16-
FROM_ONE_TO_THREE_YEARS: str = "От 1 года до 3 лет"
17-
FROM_THREE_YEARS: str = "От 3 лет и более"
34+
NO_EXPERIENCE: str = "без опыта"
35+
UP_TO_A_YEAR: str = "до 1 года"
36+
FROM_ONE_TO_THREE_YEARS: str = "от 1 года до 3 лет"
37+
FROM_THREE_YEARS: str = "от 3 лет и более"
1838

1939

2040
class WorkSchedule(ChoicesMixin, Enum):
2141

22-
FULL_TIME: str = "Полный рабочий день"
23-
SHIFT_WORK: str = "Сменный график"
24-
FLEXIBLE_SCHEDULE: str = "Гибкий график"
25-
PART_TIME: str = "Частичная занятость"
26-
INTERNSHIP: str = "Стажировка"
42+
FULL_TIME: str = "полный рабочий день"
43+
SHIFT_WORK: str = "сменный график"
44+
FLEXIBLE_SCHEDULE: str = "гибкий график"
45+
PART_TIME: str = "частичная занятость"
46+
INTERNSHIP: str = "стажировка"
2747

2848

2949
class WorkFormat(ChoicesMixin, Enum):
3050

31-
REMOTE: str = "Удаленная работа"
32-
OFFICE: str = "Работа в офисе"
33-
HYBRID: str = "Смешанная"
51+
REMOTE: str = "удаленная работа"
52+
OFFICE: str = "работа в офисе"
53+
HYBRID: str = "смешанный формат"

vacancy/filters.py

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from django.db.models import QuerySet, Q, F
2+
13
from django_filters import rest_framework as filters
24

35
from vacancy.models import Vacancy
4-
from django.db.models import QuerySet
6+
from vacancy.constants import (
7+
WorkExperience,
8+
WorkSchedule,
9+
WorkFormat,
10+
)
511

612

713
def project_id_filter(queryset, name, value) -> QuerySet:
@@ -18,12 +24,20 @@ class VacancyFilter(filters.FilterSet):
1824
Adds filtering to DRF list retrieve views
1925
2026
Parameters to filter by:
21-
project_id (int), is_active (default to True if not set otherwise) (boolean)
27+
project_id (int),
28+
is_active (boolean) (default to True if not set otherwise),
29+
required_experience (multiple choise)
30+
work_schedule (multiple choise)
31+
work_format (multiple choise)
32+
salary_min (int)
33+
salary_max (int)
2234
2335
Examples:
2436
?project_id=1 equals to .filter(project_id=1)
2537
(no params passed) equals to .filter(is_active=True)
2638
?is_active=false equals to .filter(is_active=False)
39+
?work_schedule=full_time&work_schedule=part_time equals to .filter(required_experience__in=value)
40+
?salary_min=100&salary_max=150 equals to .filter(salary__range=(100, 150))
2741
"""
2842

2943
def __init__(self, *args, **kwargs):
@@ -33,9 +47,76 @@ def __init__(self, *args, **kwargs):
3347
self.data = dict(self.data)
3448
self.data["is_active"] = True
3549

36-
is_active = filters.BooleanFilter(field_name="is_active")
50+
def filter_by_experience(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]:
51+
return (
52+
queryset
53+
.filter(Q(required_experience__in=value) | Q(required_experience=None))
54+
.order_by(F("required_experience").asc(nulls_last=True))
55+
)
56+
57+
def filter_by_schedule(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]:
58+
return (
59+
queryset
60+
.filter(Q(work_schedule__in=value) | Q(work_schedule=None))
61+
.order_by(F("work_schedule").asc(nulls_last=True))
62+
)
63+
64+
def filter_by_format(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]:
65+
return (
66+
queryset
67+
.filter(Q(work_format__in=value) | Q(work_format=None))
68+
.order_by(F("work_format").asc(nulls_last=True))
69+
)
70+
71+
def filter_by_salary_min(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]:
72+
try:
73+
min_salary = int(value[0])
74+
return (
75+
queryset
76+
.filter(Q(salary__gte=min_salary) | Q(salary=None))
77+
.order_by(F("salary").asc(nulls_last=True))
78+
)
79+
except ValueError:
80+
return queryset
81+
82+
def filter_by_salary_max(self, queryset: QuerySet[Vacancy], name, value: list[str]) -> QuerySet[Vacancy]:
83+
try:
84+
max_salary = int(value[0])
85+
return (
86+
queryset
87+
.filter(Q(salary__lte=max_salary) | Q(salary=None))
88+
.order_by(F("salary").asc(nulls_last=True))
89+
)
90+
except ValueError:
91+
return queryset
92+
3793
project_id = filters.Filter(method=project_id_filter)
94+
is_active = filters.BooleanFilter(field_name="is_active")
95+
96+
required_experience = filters.MultipleChoiceFilter(
97+
method="filter_by_experience",
98+
choices=WorkExperience.choices(),
99+
)
100+
work_schedule = filters.MultipleChoiceFilter(
101+
method="filter_by_schedule",
102+
choices=WorkSchedule.choices(),
103+
)
104+
work_format = filters.MultipleChoiceFilter(
105+
method="filter_by_format",
106+
choices=WorkFormat.choices(),
107+
)
108+
109+
salary_min = filters.Filter(method="filter_by_salary_min")
110+
salary_max = filters.Filter(method="filter_by_salary_max")
38111

39112
class Meta:
40113
model = Vacancy
41-
fields = ("project_id", "is_active")
114+
fields = (
115+
"project_id",
116+
"is_active",
117+
"required_experience",
118+
"work_schedule",
119+
"work_format",
120+
"salary_min",
121+
"salary_max",
122+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Generated by Django 4.2.11 on 2024-12-04 10:56
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("vacancy", "0007_vacancy_required_experience_vacancy_work_format_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="vacancy",
16+
name="salary",
17+
field=models.IntegerField(
18+
blank=True,
19+
null=True,
20+
validators=[django.core.validators.MinValueValidator(0)],
21+
verbose_name="Зарплата",
22+
),
23+
),
24+
migrations.AlterField(
25+
model_name="vacancy",
26+
name="required_experience",
27+
field=models.CharField(
28+
blank=True,
29+
choices=[
30+
("no_experience", "без опыта"),
31+
("up_to_a_year", "до 1 года"),
32+
("from_one_to_three_years", "от 1 года до 3 лет"),
33+
("from_three_years", "от 3 лет и более"),
34+
],
35+
max_length=50,
36+
null=True,
37+
verbose_name="Требуемый опыт",
38+
),
39+
),
40+
migrations.AlterField(
41+
model_name="vacancy",
42+
name="work_format",
43+
field=models.CharField(
44+
blank=True,
45+
choices=[
46+
("remote", "удаленная работа"),
47+
("office", "работа в офисе"),
48+
("hybrid", "смешанный формат"),
49+
],
50+
max_length=50,
51+
null=True,
52+
verbose_name="Формат работы",
53+
),
54+
),
55+
migrations.AlterField(
56+
model_name="vacancy",
57+
name="work_schedule",
58+
field=models.CharField(
59+
blank=True,
60+
choices=[
61+
("full_time", "полный рабочий день"),
62+
("shift_work", "сменный график"),
63+
("flexible_schedule", "гибкий график"),
64+
("part_time", "частичная занятость"),
65+
("internship", "стажировка"),
66+
],
67+
max_length=50,
68+
null=True,
69+
verbose_name="График работы",
70+
),
71+
),
72+
]

vacancy/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.contenttypes.fields import GenericRelation
22
from django.db import models
3+
from django.core.validators import MinValueValidator
34
from django.utils import timezone
45

56
from files.models import UserFile
@@ -53,6 +54,12 @@ class Vacancy(models.Model):
5354
null=True,
5455
verbose_name="Формат работы",
5556
)
57+
salary = models.IntegerField(
58+
blank=True,
59+
null=True,
60+
validators=[MinValueValidator(0)],
61+
verbose_name="Зарплата",
62+
)
5663
project = models.ForeignKey(
5764
Project,
5865
on_delete=models.CASCADE,

vacancy/serializers.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from projects.models import Project
1111
from projects.validators import validate_project
1212
from users.serializers import UserDetailSerializer
13+
from vacancy.constants import WorkExperience, WorkSchedule, WorkFormat
1314
from vacancy.models import Vacancy, VacancyResponse
1415

1516
User = get_user_model()
@@ -25,6 +26,37 @@ class RequiredSkillsWriteSerializerMixin(RequiredSkillsSerializerMixin):
2526
)
2627

2728

29+
class AbstractVacancyEnumFields(serializers.Serializer):
30+
required_experience = serializers.CharField(allow_null=True)
31+
work_schedule = serializers.CharField(allow_null=True)
32+
work_format = serializers.CharField(allow_null=True)
33+
34+
def validate_required_experience(self, value):
35+
try:
36+
return WorkExperience.from_display(value)
37+
except ValueError as e:
38+
raise serializers.ValidationError(str(e))
39+
40+
def validate_work_schedule(self, value):
41+
try:
42+
return WorkSchedule.from_display(value)
43+
except ValueError as e:
44+
raise serializers.ValidationError(str(e))
45+
46+
def validate_work_format(self, value):
47+
try:
48+
return WorkFormat.from_display(value)
49+
except ValueError as e:
50+
raise serializers.ValidationError(str(e))
51+
52+
def to_representation(self, instance):
53+
representation = super().to_representation(instance)
54+
representation["required_experience"] = WorkExperience.to_display(instance.required_experience)
55+
representation["work_schedule"] = WorkSchedule.to_display(instance.work_schedule)
56+
representation["work_format"] = WorkFormat.to_display(instance.work_format)
57+
return representation
58+
59+
2860
class AbstractVacancyReadOnlyFields(serializers.Serializer):
2961
"""Abstract read-only fields for Vacancy."""
3062

@@ -70,6 +102,7 @@ class Meta:
70102
class VacancyDetailSerializer(
71103
serializers.ModelSerializer,
72104
AbstractVacancyReadOnlyFields,
105+
AbstractVacancyEnumFields,
73106
RequiredSkillsWriteSerializerMixin[Vacancy],
74107
):
75108
project = ProjectForVacancySerializer(many=False, read_only=True)
@@ -91,6 +124,7 @@ class Meta:
91124
"required_experience",
92125
"work_schedule",
93126
"work_format",
127+
"salary",
94128
]
95129
read_only_fields = ["project"]
96130

@@ -155,6 +189,7 @@ def validate(self, data):
155189
class ProjectVacancyCreateListSerializer(
156190
serializers.ModelSerializer,
157191
AbstractVacancyReadOnlyFields,
192+
AbstractVacancyEnumFields,
158193
RequiredSkillsWriteSerializerMixin[Vacancy],
159194
):
160195

@@ -208,6 +243,7 @@ class Meta:
208243
"required_experience",
209244
"work_schedule",
210245
"work_format",
246+
"salary",
211247
]
212248

213249

0 commit comments

Comments
 (0)