Skip to content

Commit cf793de

Browse files
committed
PER data-fetcher functionality to backend side, v1.3
1 parent 4f6a14a commit cf793de

File tree

1 file changed

+206
-78
lines changed

1 file changed

+206
-78
lines changed

per/drf_views.py

Lines changed: 206 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
128149
class 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

916988
class 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

9661094
class PerFileViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):

0 commit comments

Comments
 (0)