Skip to content

Commit a42e9a0

Browse files
committed
Добавлена фильтрация по дополнительным полям программ
1 parent 66f532d commit a42e9a0

4 files changed

Lines changed: 183 additions & 1 deletion

File tree

partner_programs/serializers.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,57 @@ class Meta:
207207
"label",
208208
"field_type",
209209
"is_required",
210+
"show_filter",
210211
"help_text",
211212
"options",
212213
]
213214

214215
def get_options(self, obj):
215216
return obj.get_options_list()
217+
218+
219+
class ProgramProjectFilterRequestSerializer(serializers.Serializer):
220+
filters = serializers.DictField(
221+
child=serializers.ListField(child=serializers.CharField()),
222+
required=False,
223+
help_text="Словарь: ключ = PartnerProgramField.name, значение = список выбранных опций",
224+
)
225+
page = serializers.IntegerField(required=False, default=1, min_value=1)
226+
page_size = serializers.IntegerField(
227+
required=False, default=20, min_value=1, max_value=200
228+
)
229+
MAX_FILTERS = 3
230+
231+
def validate_filters(self, value):
232+
if not isinstance(value, dict):
233+
raise serializers.ValidationError(
234+
"Поле filters должно быть объектом (словарём ключ-значение)"
235+
)
236+
237+
if len(value) > self.MAX_FILTERS:
238+
raise serializers.ValidationError(
239+
f"Можно передать не более {self.MAX_FILTERS} фильтров."
240+
)
241+
242+
cleaned: dict = {}
243+
for key, raw_values in value.items():
244+
if not isinstance(key, str) or not key.strip():
245+
raise serializers.ValidationError(
246+
f"Ключи фильтров должны быть непустыми строками. Некорректный ключ: {key}"
247+
)
248+
249+
if isinstance(raw_values, list):
250+
normalized_values = [
251+
str(item).strip() for item in raw_values if str(item).strip() != ""
252+
]
253+
else:
254+
normalized_values = (
255+
[str(raw_values).strip()] if str(raw_values).strip() != "" else []
256+
)
257+
258+
if not normalized_values:
259+
continue
260+
261+
cleaned[key.strip()] = normalized_values
262+
263+
return cleaned

partner_programs/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
PartnerProgramRegister,
1111
PartnerProgramSetLiked,
1212
PartnerProgramSetViewed,
13+
ProgramFiltersAPIView,
14+
ProgramProjectFilterAPIView,
1315
)
1416

1517
app_name = "partner_programs"
@@ -36,4 +38,10 @@
3638
path(
3739
"<int:partnerprogram_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()
3840
),
41+
path("<int:pk>/filters/", ProgramFiltersAPIView.as_view(), name="program-filters"),
42+
path(
43+
"<int:pk>/projects/filter/",
44+
ProgramProjectFilterAPIView.as_view(),
45+
name="program-projects-filter",
46+
),
3947
]

partner_programs/utils.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import Dict, List
2+
3+
from django.db.models import Exists, OuterRef
4+
5+
from .models import (
6+
PartnerProgram,
7+
PartnerProgramField,
8+
PartnerProgramFieldValue,
9+
PartnerProgramProject,
10+
)
11+
12+
13+
def filter_program_projects_by_field_name(
14+
program: PartnerProgram, filters: Dict[str, List[str]]
15+
):
16+
"""
17+
filters: {"field_name": ["val1", "val2"], ...}
18+
Возвращает queryset PartnerProgramProject, отфильтрованный по условиям.
19+
Ключи MUST быть field.name (строки). Иначе — ошибка должна быть выброшена на уровне вьюхи.
20+
"""
21+
qs = PartnerProgramProject.objects.filter(partner_program=program)
22+
23+
if not filters:
24+
return qs.select_related("project").distinct()
25+
26+
for field_name, values in filters.items():
27+
if not isinstance(field_name, str) or not field_name.strip():
28+
raise ValueError("Не правильное имя поля")
29+
30+
field_name = field_name.strip()
31+
32+
field_obj = PartnerProgramField.objects.filter(
33+
partner_program=program, name=field_name
34+
).first()
35+
if not field_obj:
36+
raise ValueError(f"Поле {field_name} не найдено в программе с id {program.pk}")
37+
38+
subq = PartnerProgramFieldValue.objects.filter(
39+
program_project=OuterRef("pk"), field=field_obj, value_text__in=values
40+
)
41+
qs = qs.filter(Exists(subq))
42+
43+
return qs.select_related("project").distinct()

partner_programs/views.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.auth import get_user_model
22
from django.db import IntegrityError, transaction
3+
from django.shortcuts import get_object_or_404
34
from django.utils import timezone
45
from django.utils.timezone import now
56
from drf_yasg import openapi
@@ -16,6 +17,7 @@
1617
from partner_programs.helpers import date_to_iso
1718
from partner_programs.models import (
1819
PartnerProgram,
20+
PartnerProgramField,
1921
PartnerProgramFieldValue,
2022
PartnerProgramProject,
2123
PartnerProgramUserProfile,
@@ -24,14 +26,20 @@
2426
from partner_programs.permissions import IsProjectLeader
2527
from partner_programs.serializers import (
2628
PartnerProgramDataSchemaSerializer,
29+
PartnerProgramFieldSerializer,
2730
PartnerProgramForMemberSerializer,
2831
PartnerProgramForUnregisteredUserSerializer,
2932
PartnerProgramListSerializer,
3033
PartnerProgramNewUserSerializer,
3134
PartnerProgramUserSerializer,
35+
ProgramProjectFilterRequestSerializer,
3236
)
37+
from partner_programs.utils import filter_program_projects_by_field_name
3338
from projects.models import Project
34-
from projects.serializers import PartnerProgramFieldValueUpdateSerializer
39+
from projects.serializers import (
40+
PartnerProgramFieldValueUpdateSerializer,
41+
ProjectListSerializer,
42+
)
3543
from vacancy.mapping import (
3644
MessageTypeEnum,
3745
UserProgramRegisterParams,
@@ -341,3 +349,78 @@ def post(self, request, pk, *args, **kwargs):
341349
{"detail": "Проект успешно сдан на проверку."},
342350
status=status.HTTP_200_OK,
343351
)
352+
353+
354+
class ProgramFiltersAPIView(APIView):
355+
permission_classes = [permissions.IsAuthenticated]
356+
357+
def get(self, request, pk):
358+
program = get_object_or_404(PartnerProgram, pk=pk)
359+
fields = PartnerProgramField.objects.filter(
360+
partner_program=program, show_filter=True
361+
)
362+
serializer = PartnerProgramFieldSerializer(fields, many=True)
363+
return Response(serializer.data)
364+
365+
366+
class ProgramProjectFilterAPIView(GenericAPIView):
367+
serializer_class = ProgramProjectFilterRequestSerializer
368+
permission_classes = [permissions.IsAuthenticated]
369+
pagination_class = PartnerProgramPagination
370+
371+
def post(self, request, pk):
372+
serializer = self.get_serializer(data=request.data)
373+
serializer.is_valid(raise_exception=True)
374+
data = serializer.validated_data
375+
376+
program = get_object_or_404(PartnerProgram, pk=pk)
377+
filters = data.get("filters", {})
378+
379+
field_names = list(filters.keys())
380+
field_qs = PartnerProgramField.objects.filter(
381+
partner_program=program, name__in=field_names
382+
)
383+
field_by_name = {f.name: f for f in field_qs}
384+
385+
missing = [name for name in field_names if name not in field_by_name]
386+
if missing:
387+
return Response(
388+
{"detail": f"Поля не найденные в программе: {missing}"},
389+
status=status.HTTP_400_BAD_REQUEST,
390+
)
391+
392+
for field_name, values in filters.items():
393+
field_obj = field_by_name[field_name]
394+
if not field_obj.show_filter:
395+
return Response(
396+
{
397+
"detail": f"Поле '{field_name}' недоступно для фильтрации (show_filter=False)."
398+
},
399+
status=status.HTTP_400_BAD_REQUEST,
400+
)
401+
opts = field_obj.get_options_list()
402+
if opts:
403+
invalid_values = [val for val in values if val not in opts]
404+
if invalid_values:
405+
return Response(
406+
{
407+
"detail": f"Неверные значения для поля '{field_name}'.",
408+
"invalid": invalid_values,
409+
},
410+
status=status.HTTP_400_BAD_REQUEST,
411+
)
412+
else:
413+
return Response(
414+
{"detail": f"Поле '{field_name}' не имеет вариантов (options)."},
415+
status=status.HTTP_400_BAD_REQUEST,
416+
)
417+
418+
qs = filter_program_projects_by_field_name(program, filters)
419+
420+
paginator = self.pagination_class()
421+
page = paginator.paginate_queryset(qs, request, view=self)
422+
projects = [pp.project for pp in page]
423+
serializer_out = ProjectListSerializer(
424+
projects, many=True, context={"request": request}
425+
)
426+
return paginator.get_paginated_response(serializer_out.data)

0 commit comments

Comments
 (0)