From a42e9a015dc060d6d0f7ee66d9ca0823f3e1f355 Mon Sep 17 00:00:00 2001 From: Toksi86 Date: Tue, 12 Aug 2025 13:58:43 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8F=D0=BC=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- partner_programs/serializers.py | 48 +++++++++++++++++++ partner_programs/urls.py | 8 ++++ partner_programs/utils.py | 43 +++++++++++++++++ partner_programs/views.py | 85 ++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 partner_programs/utils.py diff --git a/partner_programs/serializers.py b/partner_programs/serializers.py index eb62514b..43e2806f 100644 --- a/partner_programs/serializers.py +++ b/partner_programs/serializers.py @@ -207,9 +207,57 @@ class Meta: "label", "field_type", "is_required", + "show_filter", "help_text", "options", ] def get_options(self, obj): return obj.get_options_list() + + +class ProgramProjectFilterRequestSerializer(serializers.Serializer): + filters = serializers.DictField( + child=serializers.ListField(child=serializers.CharField()), + required=False, + help_text="Словарь: ключ = PartnerProgramField.name, значение = список выбранных опций", + ) + page = serializers.IntegerField(required=False, default=1, min_value=1) + page_size = serializers.IntegerField( + required=False, default=20, min_value=1, max_value=200 + ) + MAX_FILTERS = 3 + + def validate_filters(self, value): + if not isinstance(value, dict): + raise serializers.ValidationError( + "Поле filters должно быть объектом (словарём ключ-значение)" + ) + + if len(value) > self.MAX_FILTERS: + raise serializers.ValidationError( + f"Можно передать не более {self.MAX_FILTERS} фильтров." + ) + + cleaned: dict = {} + for key, raw_values in value.items(): + if not isinstance(key, str) or not key.strip(): + raise serializers.ValidationError( + f"Ключи фильтров должны быть непустыми строками. Некорректный ключ: {key}" + ) + + if isinstance(raw_values, list): + normalized_values = [ + str(item).strip() for item in raw_values if str(item).strip() != "" + ] + else: + normalized_values = ( + [str(raw_values).strip()] if str(raw_values).strip() != "" else [] + ) + + if not normalized_values: + continue + + cleaned[key.strip()] = normalized_values + + return cleaned diff --git a/partner_programs/urls.py b/partner_programs/urls.py index 8918aadd..3a361024 100644 --- a/partner_programs/urls.py +++ b/partner_programs/urls.py @@ -10,6 +10,8 @@ PartnerProgramRegister, PartnerProgramSetLiked, PartnerProgramSetViewed, + ProgramFiltersAPIView, + ProgramProjectFilterAPIView, ) app_name = "partner_programs" @@ -36,4 +38,10 @@ path( "/news//set_liked/", NewsDetailSetLiked.as_view() ), + path("/filters/", ProgramFiltersAPIView.as_view(), name="program-filters"), + path( + "/projects/filter/", + ProgramProjectFilterAPIView.as_view(), + name="program-projects-filter", + ), ] diff --git a/partner_programs/utils.py b/partner_programs/utils.py new file mode 100644 index 00000000..a6288e9b --- /dev/null +++ b/partner_programs/utils.py @@ -0,0 +1,43 @@ +from typing import Dict, List + +from django.db.models import Exists, OuterRef + +from .models import ( + PartnerProgram, + PartnerProgramField, + PartnerProgramFieldValue, + PartnerProgramProject, +) + + +def filter_program_projects_by_field_name( + program: PartnerProgram, filters: Dict[str, List[str]] +): + """ + filters: {"field_name": ["val1", "val2"], ...} + Возвращает queryset PartnerProgramProject, отфильтрованный по условиям. + Ключи MUST быть field.name (строки). Иначе — ошибка должна быть выброшена на уровне вьюхи. + """ + qs = PartnerProgramProject.objects.filter(partner_program=program) + + if not filters: + return qs.select_related("project").distinct() + + for field_name, values in filters.items(): + if not isinstance(field_name, str) or not field_name.strip(): + raise ValueError("Не правильное имя поля") + + field_name = field_name.strip() + + field_obj = PartnerProgramField.objects.filter( + partner_program=program, name=field_name + ).first() + if not field_obj: + raise ValueError(f"Поле {field_name} не найдено в программе с id {program.pk}") + + subq = PartnerProgramFieldValue.objects.filter( + program_project=OuterRef("pk"), field=field_obj, value_text__in=values + ) + qs = qs.filter(Exists(subq)) + + return qs.select_related("project").distinct() diff --git a/partner_programs/views.py b/partner_programs/views.py index 93939be3..993a1c6d 100644 --- a/partner_programs/views.py +++ b/partner_programs/views.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.db import IntegrityError, transaction +from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.timezone import now from drf_yasg import openapi @@ -16,6 +17,7 @@ from partner_programs.helpers import date_to_iso from partner_programs.models import ( PartnerProgram, + PartnerProgramField, PartnerProgramFieldValue, PartnerProgramProject, PartnerProgramUserProfile, @@ -24,14 +26,20 @@ from partner_programs.permissions import IsProjectLeader from partner_programs.serializers import ( PartnerProgramDataSchemaSerializer, + PartnerProgramFieldSerializer, PartnerProgramForMemberSerializer, PartnerProgramForUnregisteredUserSerializer, PartnerProgramListSerializer, PartnerProgramNewUserSerializer, PartnerProgramUserSerializer, + ProgramProjectFilterRequestSerializer, ) +from partner_programs.utils import filter_program_projects_by_field_name from projects.models import Project -from projects.serializers import PartnerProgramFieldValueUpdateSerializer +from projects.serializers import ( + PartnerProgramFieldValueUpdateSerializer, + ProjectListSerializer, +) from vacancy.mapping import ( MessageTypeEnum, UserProgramRegisterParams, @@ -341,3 +349,78 @@ def post(self, request, pk, *args, **kwargs): {"detail": "Проект успешно сдан на проверку."}, status=status.HTTP_200_OK, ) + + +class ProgramFiltersAPIView(APIView): + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, pk): + program = get_object_or_404(PartnerProgram, pk=pk) + fields = PartnerProgramField.objects.filter( + partner_program=program, show_filter=True + ) + serializer = PartnerProgramFieldSerializer(fields, many=True) + return Response(serializer.data) + + +class ProgramProjectFilterAPIView(GenericAPIView): + serializer_class = ProgramProjectFilterRequestSerializer + permission_classes = [permissions.IsAuthenticated] + pagination_class = PartnerProgramPagination + + def post(self, request, pk): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + program = get_object_or_404(PartnerProgram, pk=pk) + filters = data.get("filters", {}) + + field_names = list(filters.keys()) + field_qs = PartnerProgramField.objects.filter( + partner_program=program, name__in=field_names + ) + field_by_name = {f.name: f for f in field_qs} + + missing = [name for name in field_names if name not in field_by_name] + if missing: + return Response( + {"detail": f"Поля не найденные в программе: {missing}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + for field_name, values in filters.items(): + field_obj = field_by_name[field_name] + if not field_obj.show_filter: + return Response( + { + "detail": f"Поле '{field_name}' недоступно для фильтрации (show_filter=False)." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + opts = field_obj.get_options_list() + if opts: + invalid_values = [val for val in values if val not in opts] + if invalid_values: + return Response( + { + "detail": f"Неверные значения для поля '{field_name}'.", + "invalid": invalid_values, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": f"Поле '{field_name}' не имеет вариантов (options)."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + qs = filter_program_projects_by_field_name(program, filters) + + paginator = self.pagination_class() + page = paginator.paginate_queryset(qs, request, view=self) + projects = [pp.project for pp in page] + serializer_out = ProjectListSerializer( + projects, many=True, context={"request": request} + ) + return paginator.get_paginated_response(serializer_out.data)