11from django .contrib .auth import get_user_model
2- from django .db .models import QuerySet , Count , OuterRef , Subquery , IntegerField
2+ from django .db import transaction
3+ from django .db .models import Count , Prefetch , Q , QuerySet
34
45from rest_framework import generics , status
6+ from rest_framework .exceptions import ValidationError
57from rest_framework .response import Response
68
79from django_filters import rest_framework as filters
810
9- from partner_programs .models import PartnerProgramUserProfile
11+ from partner_programs .models import PartnerProgram , PartnerProgramProject
12+ from partner_programs .serializers import ProgramProjectFilterRequestSerializer
13+ from partner_programs .utils import filter_program_projects_by_field_name
1014from projects .models import Project
1115from projects .filters import ProjectFilter
12- from project_rates .models import ProjectScore
16+ from project_rates .models import Criteria , ProjectScore
1317from project_rates .pagination import RateProjectsPagination
1418from project_rates .serializers import (
1519 ProjectScoreCreateSerializer ,
@@ -27,7 +31,7 @@ class RateProject(generics.CreateAPIView):
2731 serializer_class = ProjectScoreCreateSerializer
2832 permission_classes = [IsExpertPost ]
2933
30- def get_needed_data (self ) -> tuple [dict , list [int ], str ]:
34+ def get_needed_data (self ) -> tuple [dict , list [int ], PartnerProgram ]:
3135 data = self .request .data
3236 user_id = self .request .user .id
3337 project_id = self .kwargs .get ("project_id" )
@@ -36,34 +40,68 @@ def get_needed_data(self) -> tuple[dict, list[int], str]:
3640 criterion ["criterion_id" ] for criterion in data
3741 ] # is needed for validation later
3842
39- expert = Expert .objects .get (
40- user__id = user_id , programs__criterias__id = criteria_to_get [ 0 ]
43+ criteria_qs = Criteria .objects .filter ( id__in = criteria_to_get ). select_related (
44+ "partner_program"
4145 )
46+ partner_program_ids = (
47+ criteria_qs .values_list ("partner_program_id" , flat = True ).distinct ()
48+ )
49+ if not criteria_qs .exists ():
50+ raise ValueError ("Criteria not found" )
51+ if partner_program_ids .count () != 1 :
52+ raise ValueError ("All criteria must belong to the same program" )
53+ program = criteria_qs .first ().partner_program
54+
55+ Expert .objects .get (user__id = user_id , programs = program )
4256
4357 for criterion in data :
4458 criterion ["user" ] = user_id
4559 criterion ["project" ] = project_id
4660 criterion ["criteria" ] = criterion .pop ("criterion_id" )
4761
48- return data , criteria_to_get , expert .programs .all ().first ().name
62+ if not PartnerProgramProject .objects .filter (
63+ partner_program = program , project_id = project_id
64+ ).exists ():
65+ raise ValueError ("Project is not linked to the program" )
66+
67+ return data , criteria_to_get , program
4968
5069 def create (self , request , * args , ** kwargs ) -> Response :
5170 try :
52- data , criteria_to_get , program_name = self .get_needed_data ()
71+ data , criteria_to_get , program = self .get_needed_data ()
72+ project_id = data [0 ]["project" ]
73+ user_id = request .user .id
5374
5475 serializer = ProjectScoreCreateSerializer (
5576 data = data , criteria_to_get = criteria_to_get , many = True
5677 )
5778 serializer .is_valid (raise_exception = True )
5879
59- ProjectScore .objects .bulk_create (
60- [ProjectScore (** item ) for item in serializer .validated_data ],
61- update_conflicts = True ,
62- update_fields = ["value" ],
63- unique_fields = ["criteria" , "user" , "project" ],
80+ scores_qs = ProjectScore .objects .filter (
81+ project_id = project_id , criteria__partner_program = program
6482 )
83+ user_has_scores = scores_qs .filter (user_id = user_id ).exists ()
84+
85+ if program .max_project_rates :
86+ distinct_raters = scores_qs .values ("user_id" ).distinct ().count ()
87+ if not user_has_scores and distinct_raters >= program .max_project_rates :
88+ return Response (
89+ {
90+ "error" : "max project rates reached for this program" ,
91+ "max_project_rates" : program .max_project_rates ,
92+ },
93+ status = status .HTTP_400_BAD_REQUEST ,
94+ )
95+
96+ with transaction .atomic ():
97+ ProjectScore .objects .bulk_create (
98+ [ProjectScore (** item ) for item in serializer .validated_data ],
99+ update_conflicts = True ,
100+ update_fields = ["value" ],
101+ unique_fields = ["criteria" , "user" , "project" ],
102+ )
65103
66- project = Project .objects .select_related ("leader" ).get (id = data [ 0 ][ "project" ] )
104+ project = Project .objects .select_related ("leader" ).get (id = project_id )
67105
68106 send_email .delay (
69107 ProjectRatedParams (
@@ -72,7 +110,7 @@ def create(self, request, *args, **kwargs) -> Response:
72110 project_name = project .name ,
73111 project_id = project .id ,
74112 schema_id = 2 ,
75- program_name = program_name ,
113+ program_name = program . name ,
76114 )
77115 )
78116
@@ -82,6 +120,8 @@ def create(self, request, *args, **kwargs) -> Response:
82120 {"error" : "you have no permission to rate this program" },
83121 status = status .HTTP_403_FORBIDDEN ,
84122 )
123+ except ValueError as e :
124+ return Response ({"error" : str (e )}, status = status .HTTP_400_BAD_REQUEST )
85125 except Exception as e :
86126 return Response ({"error" : str (e )}, status = status .HTTP_400_BAD_REQUEST )
87127
@@ -93,18 +133,59 @@ class ProjectListForRate(generics.ListAPIView):
93133 filterset_class = ProjectFilter
94134 pagination_class = RateProjectsPagination
95135
136+ def _get_program (self ) -> PartnerProgram :
137+ return PartnerProgram .objects .get (pk = self .kwargs .get ("program_id" ))
138+
139+ def _get_filters (self ) -> dict :
140+ """
141+ Accept filters from JSON body to mirror /partner_programs/<id>/projects/filter/:
142+ {"filters": {"case": ["Кейс 1"]}}
143+ """
144+ data = getattr (self .request , "data" , None )
145+ body_filters = (
146+ data .get ("filters" ) if isinstance (data , dict ) and data .get ("filters" ) else {}
147+ )
148+ return body_filters if isinstance (body_filters , dict ) else {}
149+
96150 def get_queryset (self ) -> QuerySet [Project ]:
97- projects_ids = PartnerProgramUserProfile .objects .filter (
98- project__isnull = False , partner_program__id = self .kwargs .get ("program_id" )
99- ).values_list ("project__id" , flat = True )
100- # `Count` the easiest way to check for rate exist (0 -> does not exist).
101- scored_expert_subquery = ProjectScore .objects .filter (
102- project = OuterRef ("pk" )
103- ).values ("user_id" )[:1 ]
104-
105- return Project .objects .filter (draft = False , id__in = projects_ids ).annotate (
106- scored = Count ("scores" ),
107- scored_expert_id = Subquery (
108- scored_expert_subquery , output_field = IntegerField ()
109- ),
151+ program = self ._get_program ()
152+
153+ filters_serializer = ProgramProjectFilterRequestSerializer (
154+ data = {"filters" : self ._get_filters ()}
110155 )
156+ filters_serializer .is_valid (raise_exception = True )
157+ field_filters = filters_serializer .validated_data .get ("filters" , {})
158+
159+ try :
160+ program_projects_qs = filter_program_projects_by_field_name (
161+ program , field_filters
162+ )
163+ except ValueError as e :
164+ raise ValidationError ({"filters" : str (e )})
165+
166+ project_ids = program_projects_qs .values_list ("project_id" , flat = True )
167+
168+ scores_prefetch = Prefetch (
169+ "scores" ,
170+ queryset = ProjectScore .objects .filter (
171+ criteria__partner_program = program
172+ ).select_related ("user" ),
173+ to_attr = "_program_scores" ,
174+ )
175+
176+ return (
177+ Project .objects .filter (draft = False , id__in = project_ids )
178+ .annotate (
179+ rated_count = Count (
180+ "scores__user" ,
181+ filter = Q (scores__criteria__partner_program = program ),
182+ distinct = True ,
183+ )
184+ )
185+ .prefetch_related (scores_prefetch )
186+ )
187+
188+ def get_serializer_context (self ):
189+ context = super ().get_serializer_context ()
190+ context ["program_max_rates" ] = self ._get_program ().max_project_rates
191+ return context
0 commit comments