diff --git a/project_rates/constants.py b/project_rates/constants.py index 3d9ae97b..c47e83e3 100644 --- a/project_rates/constants.py +++ b/project_rates/constants.py @@ -1,6 +1,26 @@ +from typing import Literal +from dataclasses import dataclass + +from rest_framework.generics import ListAPIView + +from users.models import CustomUser + +NumericTypes: list[str] = ["int", "float"] + +ValidatableTypesNames = Literal[*NumericTypes, "bool", "str"] + VERBOSE_TYPES = ( ("str", "Текст"), ("int", "Целочисленное число"), ("float", "Число с плавающей точкой"), ("bool", "Да или нет"), ) + + +@dataclass +class RatesRequestData: + program_id: int + user: CustomUser + view: ListAPIView + scored: bool | None = None + project_id: int | None = None diff --git a/project_rates/models.py b/project_rates/models.py index 3616e422..f2743779 100644 --- a/project_rates/models.py +++ b/project_rates/models.py @@ -1,10 +1,10 @@ from django.contrib.auth import get_user_model from django.db import models +from .constants import VERBOSE_TYPES from partner_programs.models import PartnerProgram from projects.models import Project -from .constants import VERBOSE_TYPES -from .validators import ProjectScoreValidate +from .validators import ProjectScoreValidator User = get_user_model() @@ -80,12 +80,13 @@ def __str__(self): return f"ProjectScore<{self.id}> - {self.criteria.name}" def save(self, *args, **kwargs): - ProjectScoreValidate( - criteria_type=self.criteria.type, - value=self.value, - criteria_min_value=self.criteria.min_value, - criteria_max_value=self.criteria.max_value, - ) + data_to_validate = { + "criteria_type": self.criteria.type, + "value": self.value, + "criteria_min_value": self.criteria.min_value, + "criteria_max_value": self.criteria.max_value, + } + ProjectScoreValidator.validate(**data_to_validate) super().save(*args, **kwargs) class Meta: diff --git a/project_rates/serializers.py b/project_rates/serializers.py index d07f20c1..f124c528 100644 --- a/project_rates/serializers.py +++ b/project_rates/serializers.py @@ -3,13 +3,34 @@ from core.services import get_views_count from .models import Criteria, ProjectScore from projects.models import Project -from .validators import ProjectScoreValidate +from .validators import ProjectScoreValidator class ProjectScoreCreateSerializer(serializers.ModelSerializer): + def __init__(self, *args, **kwargs): + self.criteria_to_get = kwargs.pop("criteria_to_get", None) + super(ProjectScoreCreateSerializer, self).__init__(*args, **kwargs) + class Meta: model = ProjectScore fields = ["criteria", "user", "project", "value"] + validators = [] + + def get_queryset(self): + return self.Meta.model.objects.filter( + criteria__id__in=self.criteria_to_get + ).select_related("criteria", "project", "user") + + def validate(self, data): + criteria = data["criteria"] + data_to_validate = { + "criteria_type": criteria.type, + "value": data.get("value"), + "criteria_min_value": criteria.min_value, + "criteria_max_value": criteria.max_value, + } + ProjectScoreValidator.validate(**data_to_validate) + return data class CriteriaSerializer(serializers.ModelSerializer): @@ -75,7 +96,7 @@ def serialize_data_func(criteria_to_get: list, data: dict): for criterion in data: needed_criteria = criteria.get(int(criterion["criterion_id"])) - ProjectScoreValidate( + ProjectScoreValidator( criteria_type=needed_criteria.type, value=criterion["value"], criteria_min_value=needed_criteria.min_value, diff --git a/project_rates/services.py b/project_rates/services.py new file mode 100644 index 00000000..b7ee7b18 --- /dev/null +++ b/project_rates/services.py @@ -0,0 +1,70 @@ +from django.db.models import Count, Q, QuerySet + +from project_rates.models import Criteria, ProjectScore +from project_rates.serializers import ( + CriteriaSerializer, + ProjectScoreSerializer, + ProjectScoreGetSerializer, +) +from projects.models import Project + + +def get_querysets(RatesRequestData) -> dict[str, QuerySet]: + program_id = RatesRequestData.program_id + project_id = RatesRequestData.project_id + user = RatesRequestData.user + + criterias = Criteria.objects.prefetch_related("partner_program").filter( + partner_program_id=program_id + ) + scores = ProjectScore.objects.prefetch_related("criteria").filter( + criteria__in=criterias.values_list("id", flat=True), user=user + ) + + if project_id: + projects = [Project.objects.get(id=project_id)] + else: + projects = Project.objects.filter( + partner_program_profiles__partner_program_id=program_id + ).distinct() + + if RatesRequestData.scored: + criterias_quantity = len(criterias) + projects = projects.annotate( + user_scores_count=Count("scores", filter=Q(scores__user=user)) + ).filter(user_scores_count=criterias_quantity) + + if not project_id: + projects = RatesRequestData.view.paginate_queryset(projects) + + return { + "criterias_queryset": criterias, + "scores_queryset": scores, + "projects_queryset": projects, + } + + +def serialize_project_criterias(querysets: dict[str, QuerySet]) -> list[dict]: + criteria_serializer = CriteriaSerializer(querysets["criterias_queryset"], many=True) + scores_serializer = ProjectScoreSerializer(querysets["scores_queryset"], many=True) + + projects_serializer = ProjectScoreGetSerializer( + querysets["projects_queryset"], + context={ + "data_criterias": criteria_serializer.data, + "data_scores": scores_serializer.data, + }, + many=True, + ) + return projects_serializer.data + + +def count_scored_criterias(project_data: dict): + filled_values = sum( + 1 + for criteria in project_data["criterias"] + if criteria["name"] == "Комментарий" or criteria.get("value") + ) + + if filled_values == len(project_data["criterias"]): + project_data["is_scored"] = True diff --git a/project_rates/urls.py b/project_rates/urls.py index 8b74b862..47889ba7 100644 --- a/project_rates/urls.py +++ b/project_rates/urls.py @@ -4,12 +4,10 @@ RateProject, RateProjects, RateProjectsDetails, - ScoredProjects, ) urlpatterns = [ path("rate/", RateProject.as_view()), path("", RateProjects.as_view()), - path("scored/", ScoredProjects.as_view()), path("details", RateProjectsDetails.as_view()), ] diff --git a/project_rates/validators.py b/project_rates/validators.py index 9ac6800b..c8c9d871 100644 --- a/project_rates/validators.py +++ b/project_rates/validators.py @@ -1,32 +1,38 @@ -class ProjectScoreValidate: - def __init__(self, **kwargs): - self.criteria_type = kwargs.get("criteria_type") - self.value = kwargs.get("value") - self.criteria_min_value = kwargs.get("criteria_min_value") - self.criteria_max_value = kwargs.get("criteria_max_value") +from project_rates.constants import ValidatableTypesNames, NumericTypes - self._validate_data_type() - self._validate_numeric_limits() - def _validate_data_type(self): - if self.criteria_type in ["float", "int"]: +class ProjectScoreValidator: + @classmethod + def validate(cls, **kwargs): + criteria_type: ValidatableTypesNames = kwargs.get("criteria_type") + value: str = kwargs.get("value") + criteria_min_value: float | None = kwargs.get("criteria_min_value") + criteria_max_value: float | None = kwargs.get("criteria_max_value") + + cls._validate_data_type(criteria_type, value) + if criteria_type in NumericTypes: + cls._validate_numeric_limits( + criteria_min_value, criteria_max_value, float(value) + ) + + @staticmethod + def _validate_data_type(criteria_type: str, value: str): + if criteria_type in NumericTypes: try: - float(self.value) + float(value) except ValueError: raise ValueError("Введённое значение не соответствует формату!") except TypeError: raise TypeError("Вы не ввели никакие данные!") - elif (self.criteria_type == "bool") and (self.value not in ["True", "False"]): + elif (criteria_type == "bool") and (value not in ["True", "False"]): raise TypeError("Введённое значение не соответствует формату!") - def _validate_numeric_limits(self): - if self.criteria_type in ["int", "float"]: - if self.criteria_min_value is not None and self.criteria_min_value > float( - self.value - ): - raise ValueError("Оценка этого критерия принизила допустимые значения!") - elif self.criteria_max_value is not None and self.criteria_max_value < float( - self.value - ): - raise ValueError("Оценка этого критерия превысила допустимые значения!") + @staticmethod + def _validate_numeric_limits( + min_value: float | None, max_value: float | None, value: float + ): + if min_value is not None and min_value > value: + raise ValueError("Оценка этого критерия ниже допустимого значения!") + elif max_value is not None and max_value < value: + raise ValueError("Оценка этого критерия превысила допустимое значение!") diff --git a/project_rates/views.py b/project_rates/views.py index 41fe5b69..00ea77d2 100644 --- a/project_rates/views.py +++ b/project_rates/views.py @@ -1,19 +1,20 @@ from django.contrib.auth import get_user_model -from django.db.models import Count, Q +from django.db.models import QuerySet from rest_framework import generics, status from rest_framework.response import Response -from core.services import get_views_count -from projects.models import Project -from project_rates.models import Criteria, ProjectScore +from project_rates.constants import RatesRequestData +from project_rates.services import ( + get_querysets, + serialize_project_criterias, + count_scored_criterias, +) +from project_rates.models import ProjectScore from project_rates.pagination import RateProjectsPagination from project_rates.serializers import ( ProjectScoreCreateSerializer, - CriteriaSerializer, - ProjectScoreSerializer, ProjectScoreGetSerializer, - serialize_data_func, ) from users.permissions import IsExpert, IsExpertPost @@ -24,193 +25,94 @@ class RateProject(generics.CreateAPIView): serializer_class = ProjectScoreCreateSerializer permission_classes = [IsExpertPost] - def create(self, request, *args, **kwargs): - # try: + def get_needed_data(self) -> tuple[dict, list[int]]: data = self.request.data - - user = self.request.user.id + user_id = self.request.user.id project_id = self.kwargs.get("project_id") - criteria_to_get = [] + criteria_to_get = [ + criterion["criterion_id"] for criterion in data + ] # is needed for validation later for criterion in data: - criterion["user_id"] = user - criterion["project_id"] = project_id - criteria_to_get.append(criterion["criterion_id"]) - - serialize_data_func(criteria_to_get, data) - ProjectScore.objects.bulk_create( - [ProjectScore(**score) for score in data], - update_conflicts=True, - update_fields=["value"], - unique_fields=["criteria", "user", "project"], - ) + criterion["user"] = user_id + criterion["project"] = project_id + criterion["criteria"] = criterion.pop("criterion_id") - return Response({"success": True}, status=status.HTTP_201_CREATED) + return data, criteria_to_get + def create(self, request, *args, **kwargs) -> Response: + try: + data, criteria_to_get = self.get_needed_data() -class RateProjects(generics.ListAPIView): - serializer_class = ProjectScoreGetSerializer - permission_classes = [IsExpert] - pagination_class = RateProjectsPagination - - def get(self, request, *args, **kwargs): - user = self.request.user - program_id = self.kwargs.get("program_id") - - criterias = Criteria.objects.prefetch_related("partner_program").filter( - partner_program_id=program_id - ) - scores = ProjectScore.objects.prefetch_related("criteria").filter( - criteria__in=criterias.values_list("id", flat=True), user=user - ) - unpaginated_projects = Project.objects.filter( - partner_program_profiles__partner_program_id=program_id - ).distinct() - - projects = self.paginate_queryset(unpaginated_projects) - - criteria_serializer = CriteriaSerializer(data=criterias, many=True) - scores_serializer = ProjectScoreSerializer(data=scores, many=True) - - criteria_serializer.is_valid() - scores_serializer.is_valid() - - projects_serializer = self.get_serializer( - data=projects, - context={ - "data_criterias": criteria_serializer.data, - "data_scores": scores_serializer.data, - }, - many=True, - ) - - projects_serializer.is_valid() + serializer = ProjectScoreCreateSerializer( + data=data, criteria_to_get=criteria_to_get, many=True + ) + serializer.is_valid(raise_exception=True) - for project in projects_serializer.data: - filled_values = 0 - for criteria in project["criterias"]: - if criteria["name"] == "Комментарий" or criteria.get("value", None): - filled_values += 1 + ProjectScore.objects.bulk_create( + [ProjectScore(**item) for item in serializer.validated_data], + update_conflicts=True, + update_fields=["value"], + unique_fields=["criteria", "user", "project"], + ) - if filled_values == len(project["criterias"]): - project["is_scored"] = True + return Response({"success": True}, status=status.HTTP_201_CREATED) - return self.get_paginated_response(projects_serializer.data) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) -class ScoredProjects(generics.ListAPIView): +class RateProjects(generics.ListAPIView): serializer_class = ProjectScoreGetSerializer permission_classes = [IsExpert] pagination_class = RateProjectsPagination - def get(self, request, *args, **kwargs): - user = self.request.user - program_id = self.kwargs.get("program_id") + def get_request_data(self) -> RatesRequestData: + scored = True if self.request.query_params.get("scored") == "true" else False - criterias = Criteria.objects.prefetch_related("partner_program").filter( - partner_program_id=program_id + return RatesRequestData( + program_id=self.kwargs.get("program_id"), + user=self.request.user, + view=self, + scored=scored, ) - quantity_criterias = criterias.count() - scores = ProjectScore.objects.prefetch_related("criteria").filter( - criteria__in=criterias.values_list("id", flat=True), user=user - ) - unpaginated_projects = ( - Project.objects.filter( - partner_program_profiles__partner_program_id=program_id - ) - .annotate(user_scores_count=Count("scores", filter=Q(scores__user=user))) - .filter(user_scores_count=quantity_criterias) - .distinct() - ) + def get_querysets_dict(self) -> dict[str, QuerySet]: + return get_querysets(self.get_request_data()) - projects = self.paginate_queryset(unpaginated_projects) + def serialize_querysets(self) -> list[dict]: + return serialize_project_criterias(self.get_querysets_dict()) - criteria_serializer = CriteriaSerializer(data=criterias, many=True) - scores_serializer = ProjectScoreSerializer(data=scores, many=True) + def get(self, request, *args, **kwargs): + serialized_data = self.serialize_querysets() - criteria_serializer.is_valid() - scores_serializer.is_valid() + if self.request.query_params.get("scored") == "true": + [project.update({"is_scored": True}) for project in serialized_data] + else: + [count_scored_criterias(project) for project in serialized_data] - projects_serializer = self.get_serializer( - data=projects, - context={ - "data_criterias": criteria_serializer.data, - "data_scores": scores_serializer.data, - }, - many=True, - ) + return self.get_paginated_response(serialized_data) - projects_serializer.is_valid() - for project in projects_serializer.data: - project["is_scored"] = True +class RateProjectsDetails(RateProjects): + permission_classes = [IsExpertPost] # потом решить проблему с этим - return self.get_paginated_response(projects_serializer.data) + def get_request_data(self) -> RatesRequestData: + request_data = super().get_request_data() + project_id = self.request.query_params.get("project_id") + program_id = self.request.query_params.get("program_id") -class RateProjectsDetails(generics.ListAPIView): - serializer_class = ProjectScoreGetSerializer - permission_classes = [IsExpert] + request_data.project_id = int(project_id) if project_id else None + request_data.program_id = int(program_id) if program_id else None + return request_data def get(self, request, *args, **kwargs): - user = self.request.user - project_id = self.request.query_params.get("project_id") + try: + serialized_data = self.serialize_querysets()[0] - criterias = Criteria.objects.prefetch_related("partner_program").filter( - partner_program_id=int(self.request.query_params.get("program_id")) - ) - project = Project.objects.filter(id=int(project_id)).first() - scores = ProjectScore.objects.prefetch_related("criteria").filter( - criteria__in=criterias.values_list("id", flat=True), - user=user, - project=project, - ) + count_scored_criterias(serialized_data) - criterias_data = [] - for criteria in criterias: - criteria_data = { - "id": criteria.id, - "name": criteria.name, - "description": criteria.description, - "type": criteria.type, - "min_value": criteria.min_value, - "max_value": criteria.max_value, - } - criterias_data.append(criteria_data) - - project_scores_data = [] - for project_score in scores: - project_score_data = { - "criteria_id": project_score.criteria.id, - "value": project_score.value, - } - project_scores_data.append(project_score_data) - - for score in project_scores_data: - for criteria in criterias_data: - if criteria["id"] == score["criteria_id"]: - criteria["value"] = score["value"] - - response = { - "id": project.id, - "name": project.name, - "leader": project.leader.id, - "description": project.description, - "image_address": project.image_address, - "presentation_address": project.presentation_address, - "industry": project.industry.id, - "region": project.region, - "criterias": criterias_data, - "views_count": get_views_count(project), - } - - filled_values = 0 - for criteria in response["criterias"]: - if criteria.get("value", None): - filled_values += 1 - - if filled_values == len(response["criterias"]): - response["is_scored"] = True - - return Response(response, status=200) + return Response(serialized_data, status=status.HTTP_200_OK) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)