@@ -125,6 +125,27 @@ def _contains_affirmative(text: str) -> bool:
125125 return any (word in normalized for word in AFFIRMATIVE_WORDS )
126126
127127
128+ def _phase_display_from_int (phase : int | None , existing_display : str | None = None ) -> str | None :
129+ """Return normalized phase display using Overview.Phase IntegerChoices.
130+
131+ Uses the IntegerChoices label, then normalizes:
132+ - "WorkPlan" -> "Workplan"
133+ - "Action And Accountability" -> "Action & accountability"
134+ """
135+ label = None
136+ try :
137+ if isinstance (phase , int ):
138+ label = Overview .Phase (phase ).label # from IntegerChoices
139+ except Exception :
140+ label = None
141+ disp = label or existing_display
142+ if disp == "Action And Accountability" :
143+ return "Action & accountability"
144+ if disp == "WorkPlan" :
145+ return "Workplan"
146+ return disp
147+
148+
128149class PERDocsFilter (filters .FilterSet ):
129150 id = filters .NumberFilter (field_name = "id" , lookup_expr = "exact" )
130151
@@ -659,11 +680,7 @@ def list(self, request, *args, **kwargs):
659680 results = data .get ("results" ) if isinstance (data , dict ) else data
660681 if results :
661682 for row in results :
662- pd = row .get ("phase_display" )
663- if pd == "Action And Accountability" :
664- row ["phase_display" ] = "Action & accountability"
665- elif pd == "WorkPlan" :
666- row ["phase_display" ] = "Workplan"
683+ row ["phase_display" ] = _phase_display_from_int (row .get ("phase" ), row .get ("phase_display" ))
667684 return Response (data )
668685
669686
@@ -734,13 +751,8 @@ def get(self, request):
734751 )
735752 items = []
736753 for ov in latest_overviews :
737- # Normalize phase display and keep raw phase if available
738- phase_display = getattr (ov , "phase_display" , None )
739- normalized_phase_display = phase_display
740- if phase_display == "Action And Accountability" :
741- normalized_phase_display = "Action & accountability"
742- elif phase_display == "WorkPlan" :
743- normalized_phase_display = "Workplan"
754+ # Compute normalized phase display from int value or existing string
755+ normalized_phase_display = _phase_display_from_int (getattr (ov , "phase" , None ), getattr (ov , "phase_display" , None ))
744756
745757 # Attach components from latest assessment tied to the overview
746758 components = []
@@ -837,10 +849,22 @@ def get(self, request):
837849 "region_id" : getattr (getattr (ov .country , "region" , None ), "id" , None ),
838850 "region_name" : getattr (getattr (ov .country , "region" , None ), "label" , None ),
839851 "latitude" : (
840- ov .country .centroid .y if getattr (ov .country , "centroid" , None ) else getattr (ov .country , "latitude" , None )
852+ round (ov .country .centroid .y , 5 )
853+ if getattr (ov .country , "centroid" , None )
854+ else (
855+ round (getattr (ov .country , "latitude" , None ), 5 )
856+ if getattr (ov .country , "latitude" , None ) is not None
857+ else None
858+ )
841859 ),
842860 "longitude" : (
843- ov .country .centroid .x if getattr (ov .country , "centroid" , None ) else getattr (ov .country , "longitude" , None )
861+ round (ov .country .centroid .x , 5 )
862+ if getattr (ov .country , "centroid" , None )
863+ else (
864+ round (getattr (ov .country , "longitude" , None ), 5 )
865+ if getattr (ov .country , "longitude" , None ) is not None
866+ else None
867+ )
844868 ),
845869 "updated_at" : getattr (ov , "updated_at" , None ),
846870 "prioritized_components" : prioritized_components ,
@@ -866,101 +890,205 @@ def get(self, request):
866890 queryset = AreaResponse .objects .prefetch_related (
867891 Prefetch (
868892 "component_response" ,
869- queryset = FormComponentResponse .objects .prefetch_related ("question_responses" ),
893+ queryset = FormComponentResponse .objects .prefetch_related ("question_responses" ).select_related (
894+ "component" ,
895+ "component__area" ,
896+ "rating" ,
897+ ),
870898 )
871899 ),
872900 )
873901 )
874902 results = []
875903 for a in assessments :
876- components = []
904+ # Collect area entries with sort keys
905+ area_entries = []
877906 for ar in a .area_responses .all ():
907+ component_entries = []
878908 for cr in ar .component_response .all ():
879- cd = getattr (cr , "component_details" , None )
880- rd = getattr (cr , "rating_details" , None )
881- # When accessed via serializer, details exist; otherwise construct minimally
882- components .append (
909+ comp = getattr (cr , "component" , None )
910+ area = getattr (comp , "area" , None ) if comp else None
911+ rating = getattr (cr , "rating" , None )
912+
913+ rating_details = (
914+ {
915+ "id" : getattr (rating , "id" , None ),
916+ "value" : getattr (rating , "value" , None ),
917+ "title" : getattr (rating , "title" , None ),
918+ }
919+ if rating is not None
920+ else None
921+ )
922+
923+ component_details = (
924+ {
925+ "id" : getattr (comp , "id" , None ),
926+ "component_num" : getattr (comp , "component_num" , None ),
927+ "area" : getattr (area , "id" , None ),
928+ "title" : getattr (comp , "title" , None )
929+ or getattr (comp , "description_en" , None )
930+ or getattr (comp , "description" , None ),
931+ "description" : getattr (comp , "description" , None ) or getattr (comp , "description_en" , None ),
932+ }
933+ if comp is not None
934+ else None
935+ )
936+
937+ component_entries .append (
883938 {
884- "component_id" : getattr (cr , "component_id" , None ),
939+ "id" : getattr (cr , "id" , None ),
940+ "component" : getattr (comp , "id" , None ),
941+ "rating" : getattr (rating , "id" , None ),
942+ "rating_details" : rating_details ,
943+ "component_details" : component_details ,
944+ "urban_considerations" : getattr (cr , "urban_considerations" , None ),
945+ "epi_considerations" : getattr (cr , "epi_considerations" , None ),
946+ "climate_environmental_considerations" : getattr (cr , "climate_environmental_considerations" , None ),
947+ "migration_considerations" : getattr (cr , "migration_considerations" , None ),
885948 "urban_considerations_simplified" : _contains_affirmative (getattr (cr , "urban_considerations" , "" )),
886949 "epi_considerations_simplified" : _contains_affirmative (getattr (cr , "epi_considerations" , "" )),
887- "migration_considerations_simplified" : _contains_affirmative (
888- getattr (cr , "migration_considerations" , "" )
889- ),
890950 "climate_environmental_considerations_simplified" : _contains_affirmative (
891951 getattr (cr , "climate_environmental_considerations" , "" )
892952 ),
893- "component_name" : (cd .title if cd else getattr (cr .component , "title" , None )),
894- "component_num" : (cd .component_num if cd else getattr (cr .component , "component_num" , None )),
895- "area_id" : (cd .area if cd else getattr (cr .component .area , "id" , None )),
896- "area_name" : (
897- AREA_NAMES .get (cd .area )
898- if (cd and isinstance (cd .area , int ))
899- else getattr (getattr (cr .component , "area" , None ), "name" , None )
953+ "migration_considerations_simplified" : _contains_affirmative (
954+ getattr (cr , "migration_considerations" , "" )
900955 ),
901- "rating_value " : ( rd . value if rd else getattr (getattr ( cr , "rating " , None ), "value" , None ) ),
902- "rating_title " : ( rd . title if rd else getattr (getattr ( cr , "rating " , None ), "title" , None ) ),
956+ "notes " : getattr (cr , "notes " , None ),
957+ "_component_num " : getattr (comp , "component_num " , 0 ),
903958 }
904959 )
960+ # Sort components by component_num
961+ component_entries .sort (key = lambda x : x .get ("_component_num" ) or 0 )
962+ # Remove sort helper keys
963+ for ce in component_entries :
964+ ce .pop ("_component_num" , None )
965+
966+ area_entries .append (
967+ {
968+ "id" : getattr (ar , "id" , None ),
969+ "component_responses" : component_entries ,
970+ "_area_num" : getattr (getattr (comp , "area" , None ), "area_num" , 0 ) if comp else 0 ,
971+ }
972+ )
973+ # Sort areas by area_num
974+ area_entries .sort (key = lambda x : x .get ("_area_num" ) or 0 )
975+ for ae in area_entries :
976+ ae .pop ("_area_num" , None )
977+
905978 results .append (
906979 {
907- "assessment_id" : a .id ,
908- "country_id" : getattr (a .overview , "country_id" , None ),
909- "country_name" : getattr (getattr (a .overview , "country" , None ), "name" , None ),
910- "components" : components ,
980+ "id" : getattr (a , "id" , None ),
981+ "area_responses" : area_entries ,
911982 }
912983 )
984+
913985 return Response ({"results" : results })
914986
915987
916988class PerDashboardDataView (views .APIView ):
917989 """Public consolidated PER dashboard data.
918990
919- Groups latest per country overview and attaches lightweight assessment entries .
991+ Aggregates by PER components (not countries) and attaches assessments .
920992 """
921993
922994 def get (self , request ):
923- latest_overviews = (
924- Overview .objects .order_by ("country_id" , "-assessment_number" , "-date_of_assessment" )
925- .distinct ("country_id" )
926- .select_related ("country" )
927- )
928- # Map assessments by country for quick attach
929- assessments_by_country = {}
930- for a in PerAssessment .objects .select_related ("overview" ):
931- cid = getattr (a .overview , "country_id" , None )
932- if cid is None :
933- continue
934- assessments_by_country .setdefault (cid , []).append (
935- {
936- "assessment_id" : a .id ,
937- "assessment_number" : getattr (a .overview , "assessment_number" , None ),
938- "date_of_assessment" : getattr (a .overview , "date_of_assessment" , None ),
939- }
940- )
941- items = []
942- for ov in latest_overviews :
943- phase_display = ov .get_phase_display () if hasattr (ov , "get_phase_display" ) else getattr (ov , "phase_display" , None )
944- if phase_display == "Action And Accountability" :
945- phase_display = "Action & accountability"
946- elif phase_display == "WorkPlan" :
947- phase_display = "Workplan"
948- items .append (
949- {
950- "country_id" : ov .country_id ,
951- "country_name" : ov .country .name if ov .country else None ,
952- "country_iso3" : getattr (ov .country , "iso3" , None ),
953- "phase" : phase_display ,
954- "assessment_number" : ov .assessment_number ,
955- "date_of_assessment" : ov .date_of_assessment ,
956- "countryAssessments" : sorted (
957- assessments_by_country .get (ov .country_id , []),
958- key = lambda x : (x ["date_of_assessment" ] or datetime .min ),
959- reverse = True ,
960- ),
961- }
995+ # Build aggregation by component across all assessments
996+ component_map = {}
997+ country_assessments : dict [str , list ] = {}
998+ # Prefetch for performance
999+ assessments = PerAssessment .objects .select_related ("overview" , "overview__country" ).prefetch_related (
1000+ Prefetch (
1001+ "area_responses" ,
1002+ queryset = AreaResponse .objects .prefetch_related (
1003+ Prefetch (
1004+ "component_response" ,
1005+ queryset = FormComponentResponse .objects .select_related ("component" , "component__area" , "rating" ),
1006+ )
1007+ ),
9621008 )
963- return Response ({"results" : items })
1009+ )
1010+
1011+ for a in assessments :
1012+ assessment_entry = {
1013+ "assessment_id" : getattr (a , "id" , None ),
1014+ "assessment_number" : getattr (a .overview , "assessment_number" , None ),
1015+ "date_of_assessment" : getattr (a .overview , "date_of_assessment" , None ),
1016+ "country_id" : getattr (a .overview , "country_id" , None ),
1017+ "country_name" : getattr (getattr (a .overview , "country" , None ), "name" , None ),
1018+ "country_iso3" : getattr (getattr (a .overview , "country" , None ), "iso3" , None ),
1019+ }
1020+ # Also prepare detailed assessment for countryAssessments with ratings
1021+ ca_components = []
1022+
1023+ for ar in a .area_responses .all ():
1024+ for cr in ar .component_response .all ():
1025+ comp = getattr (cr , "component" , None )
1026+ if comp is None :
1027+ continue
1028+ area = getattr (comp , "area" , None )
1029+ comp_id = getattr (comp , "id" , None )
1030+ if comp_id is None :
1031+ continue
1032+ # Component key aggregation
1033+ if comp_id not in component_map :
1034+ component_map [comp_id ] = {
1035+ "component_id" : comp_id ,
1036+ "component_num" : getattr (comp , "component_num" , None ),
1037+ "component_name" : getattr (comp , "title" , None )
1038+ or getattr (comp , "description_en" , None )
1039+ or getattr (comp , "description" , None ),
1040+ "area_id" : getattr (area , "id" , None ),
1041+ "area_name" : (
1042+ AREA_NAMES .get (int (getattr (area , "area_num" , 0 )))
1043+ if isinstance (getattr (area , "area_num" , None ), int )
1044+ else getattr (area , "name" , None )
1045+ ),
1046+ "assessments" : [],
1047+ }
1048+
1049+ component_map [comp_id ]["assessments" ].append (assessment_entry )
1050+
1051+ # Build component entry with rating for countryAssessments
1052+ rating = getattr (cr , "rating" , None )
1053+ ca_components .append (
1054+ {
1055+ "component_id" : comp_id ,
1056+ "component_name" : getattr (comp , "title" , None )
1057+ or getattr (comp , "description_en" , None )
1058+ or getattr (comp , "description" , None ),
1059+ "component_num" : getattr (comp , "component_num" , None ),
1060+ "area_id" : getattr (area , "id" , None ),
1061+ "area_name" : (
1062+ AREA_NAMES .get (int (getattr (area , "area_num" , 0 )))
1063+ if isinstance (getattr (area , "area_num" , None ), int )
1064+ else getattr (area , "name" , None )
1065+ ),
1066+ "rating_value" : getattr (rating , "value" , None ),
1067+ "rating_title" : getattr (rating , "title" , None ) or "" ,
1068+ }
1069+ )
1070+
1071+ # Append to countryAssessments mapping
1072+ country_name = assessment_entry ["country_name" ]
1073+ if country_name :
1074+ phase_display = _phase_display_from_int (
1075+ getattr (a .overview , "phase" , None ), getattr (a .overview , "phase_display" , None )
1076+ )
1077+ country_assessments .setdefault (country_name , []).append (
1078+ {
1079+ "assessment_number" : assessment_entry ["assessment_number" ],
1080+ "date" : assessment_entry ["date_of_assessment" ],
1081+ "components" : ca_components ,
1082+ "phase" : getattr (a .overview , "phase" , None ),
1083+ "phase_display" : phase_display ,
1084+ }
1085+ )
1086+
1087+ # Convert to list
1088+ items = list (component_map .values ())
1089+ # Optional: sort by area then component_num for stable output
1090+ items .sort (key = lambda x : ((x ["area_id" ] or 0 ), (x ["component_num" ] or 0 )))
1091+ return Response ({"assessments" : items , "countryAssessments" : country_assessments })
9641092
9651093
9661094class PerFileViewSet (mixins .ListModelMixin , mixins .CreateModelMixin , viewsets .GenericViewSet ):
0 commit comments