diff --git a/project_rates/admin.py b/project_rates/admin.py index c5ee8df2..1ecb1455 100644 --- a/project_rates/admin.py +++ b/project_rates/admin.py @@ -5,22 +5,26 @@ # Register your models here. @admin.register(Criteria) class CriteriaAdmin(admin.ModelAdmin): - list_display = ( - "id", - "name", - "type", - # добавить проект / программу - ) + list_display = ("id", "name", "type", "get_program_name") list_display_links = ( "id", "name", ) + def get_program_name(self, obj): + return obj.partner_program.name + @admin.register(ProjectScore) class ProjectScoreAdmin(admin.ModelAdmin): - list_display = ( - "id", - # добавить проект / программу - ) + list_display = ("id", "get_user_name", "get_project_name", "get_criteria_name") list_display_links = ("id",) + + def get_user_name(self, obj): + return f"{obj.user.last_name} {obj.user.first_name}" + + def get_criteria_name(self, obj): + return obj.criteria.name + + def get_project_name(self, obj): + return obj.project.name diff --git a/project_rates/pagination.py b/project_rates/pagination.py new file mode 100644 index 00000000..91a85ab6 --- /dev/null +++ b/project_rates/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import LimitOffsetPagination + + +class RateProjectsPagination(LimitOffsetPagination): + default_limit = 10 + limit_query_param = "limit" + offset_query_param = "offset" diff --git a/rate_projects/models.py b/rate_projects/models.py new file mode 100644 index 00000000..2f72b7a6 --- /dev/null +++ b/rate_projects/models.py @@ -0,0 +1,94 @@ +from django.contrib.auth import get_user_model +from django.db import models + +from partner_programs.models import PartnerProgram +from projects.models import Project +from .constants import VERBOSE_NAME_TYPES +from .validators import ProjectScoreValidate + +User = get_user_model() + + +class Criteria(models.Model): + """ + Criteria model + + Attributes: + name: A CharField name of the criteria + description: A TextField description of criteria + type: A CharField choice between "str", "int", "bool" and "float" + min_value: Optional FloatField for numeric values + max_value: Optional FloatField for numeric values + partner_program: A ForeignKey connection to PartnerProgram model + + """ + + name = models.CharField(verbose_name="Название", max_length=50) + description = models.TextField(verbose_name="Описание", null=True, blank=True) + type = models.CharField(verbose_name="Тип", max_length=8, choices=VERBOSE_NAME_TYPES) + + min_value = models.FloatField( + verbose_name="Минимально допустимое числовое значение", + help_text="(если есть)", + null=True, + blank=True, + ) + max_value = models.FloatField( + verbose_name="Максимально допустимое числовое значение", + help_text="(если есть)", + null=True, + blank=True, + ) + partner_program = models.ForeignKey( + PartnerProgram, + on_delete=models.CASCADE, + related_name="criterias", + ) + + def __str__(self): + return f"Criteria<{self.id}> - {self.name} {self.partner_program.name}" + + class Meta: + verbose_name = "Критерий оценки проекта" + verbose_name_plural = "Критерии оценки проектов" + + +class ProjectScore(models.Model): + """ + ProjectScore model + + Attributes: + criteria: A ForeignKey connection to Criteria model + user: A ForeignKey connection to User model + + value: CharField for value + + + """ + + criteria = models.ForeignKey( + Criteria, on_delete=models.CASCADE, related_name="scores" + ) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="scores") + project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="scores") + + value = models.CharField( + verbose_name="Значение", max_length=50, null=True, blank=True + ) + + 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, + ) + super().save(*args, **kwargs) + + class Meta: + verbose_name = "Оценка проекта" + verbose_name_plural = "Оценки проектов" + unique_together = ("criteria", "user", "project") diff --git a/rate_projects/serializers.py b/rate_projects/serializers.py new file mode 100644 index 00000000..1d16d717 --- /dev/null +++ b/rate_projects/serializers.py @@ -0,0 +1,51 @@ +from rest_framework import serializers +from .models import Criteria, ProjectScore +from projects.models import Project + + +class ProjectScoreCreateSerializer(serializers.ModelSerializer): + class Meta: + model = ProjectScore + fields = ["criteria", "user", "project", "value"] + + +class CriteriaSerializer(serializers.ModelSerializer): + class Meta: + model = Criteria + exclude = ["partner_program"] + + +class ProjectScoreSerializer(serializers.ModelSerializer): + class Meta: + model = ProjectScore + fields = ["criteria_id", "project_id", "value"] + + +class ProjectScoreGetSerializer(serializers.ModelSerializer): + criterias = serializers.SerializerMethodField() + + class Meta: + model = Project + fields = [ + "id", + "name", + "leader", + "description", + "image_address", + "industry", + "criterias", + ] + + def get_criterias(self, obj): + criterias = [] + for criteria in self.context["data_criterias"]: + copied_criteria = criteria.copy() + for score in self.context["data_scores"]: + if ( + criteria["id"] == score["criteria_id"] + and obj.id == score["project_id"] + ): + copied_criteria["value"] = score["value"] + + criterias.append(copied_criteria) + return criterias diff --git a/rate_projects/urls.py b/rate_projects/urls.py new file mode 100644 index 00000000..4f9f208e --- /dev/null +++ b/rate_projects/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from rate_projects.views import RateProject, RateProjects + +urlpatterns = [ + path("rate/", RateProject.as_view()), + path("", RateProjects.as_view()), +] diff --git a/rate_projects/validators.py b/rate_projects/validators.py new file mode 100644 index 00000000..9ac6800b --- /dev/null +++ b/rate_projects/validators.py @@ -0,0 +1,32 @@ +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") + + self._validate_data_type() + self._validate_numeric_limits() + + def _validate_data_type(self): + if self.criteria_type in ["float", "int"]: + try: + float(self.value) + except ValueError: + raise ValueError("Введённое значение не соответствует формату!") + except TypeError: + raise TypeError("Вы не ввели никакие данные!") + + elif (self.criteria_type == "bool") and (self.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("Оценка этого критерия превысила допустимые значения!") diff --git a/rate_projects/views.py b/rate_projects/views.py new file mode 100644 index 00000000..e7c6fa39 --- /dev/null +++ b/rate_projects/views.py @@ -0,0 +1,78 @@ +from django.contrib.auth import get_user_model + +from rest_framework import generics, status +from rest_framework.response import Response + +from projects.models import Project +from rate_projects.models import Criteria, ProjectScore +from rate_projects.pagination import RateProjectsPagination +from rate_projects.serializers import ( + ProjectScoreCreateSerializer, + CriteriaSerializer, + ProjectScoreSerializer, + ProjectScoreGetSerializer, +) +from users.permissions import IsExpert + +User = get_user_model() + + +class RateProject(generics.CreateAPIView): + serializer_class = ProjectScoreCreateSerializer + permission_classes = [IsExpert] + + def create(self, request, *args, **kwargs): + try: + data = self.request.data + data["user"] = self.request.user.id + + serializer = self.get_serializer(data=data) + if serializer.is_valid(): + serializer.save() + self.perform_create(serializer) + return Response({"success": True}, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +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() + + return self.get_paginated_response(projects_serializer.data)