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
48 changes: 48 additions & 0 deletions partner_programs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions partner_programs/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
PartnerProgramRegister,
PartnerProgramSetLiked,
PartnerProgramSetViewed,
ProgramFiltersAPIView,
ProgramProjectFilterAPIView,
)

app_name = "partner_programs"
Expand All @@ -36,4 +38,10 @@
path(
"<int:partnerprogram_pk>/news/<int:pk>/set_liked/", NewsDetailSetLiked.as_view()
),
path("<int:pk>/filters/", ProgramFiltersAPIView.as_view(), name="program-filters"),
path(
"<int:pk>/projects/filter/",
ProgramProjectFilterAPIView.as_view(),
name="program-projects-filter",
),
]
43 changes: 43 additions & 0 deletions partner_programs/utils.py
Original file line number Diff line number Diff line change
@@ -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()
85 changes: 84 additions & 1 deletion partner_programs/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,6 +17,7 @@
from partner_programs.helpers import date_to_iso
from partner_programs.models import (
PartnerProgram,
PartnerProgramField,
PartnerProgramFieldValue,
PartnerProgramProject,
PartnerProgramUserProfile,
Expand All @@ -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,
Expand Down Expand Up @@ -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)