Skip to content

Commit b159ec5

Browse files
Merge pull request #17 from CivicDataLab/dev
Collaboration and Geography Enhancements
2 parents 6f46a8a + aaf509e commit b159ec5

15 files changed

+1384
-26
lines changed

api/management/commands/populate_geographies.py

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
Django management command to populate Asia Pacific geography data.
3-
Covers: India, Indonesia, Thailand at state/province level.
3+
Covers: India, Indonesia, Thailand, Philippines at state/province level.
44
55
Usage:
66
python manage.py populate_geographies
@@ -17,7 +17,9 @@
1717

1818

1919
class Command(BaseCommand):
20-
help = "Populate Asia Pacific geography data (India, Indonesia, Thailand)"
20+
help = (
21+
"Populate Asia Pacific geography data (India, Indonesia, Thailand, Philippines)"
22+
)
2123

2224
def add_arguments(self, parser: CommandParser) -> None:
2325
parser.add_argument(
@@ -64,6 +66,9 @@ def _populate_geographies(self) -> None:
6466
# Thailand and its provinces
6567
self._populate_thailand(asia_pacific)
6668

69+
# Philippines and its provinces
70+
self._populate_philippines(asia_pacific)
71+
6772
def _populate_india(self, parent: Geography) -> None:
6873
"""Populate India and its states/UTs."""
6974
india, created = Geography.objects.get_or_create(
@@ -302,3 +307,133 @@ def _populate_thailand(self, parent: Geography) -> None:
302307
self.stdout.write(
303308
f"Thai provinces: {created_count} created, {len(thai_provinces) - created_count} already existed"
304309
)
310+
311+
def _populate_philippines(self, parent: Geography) -> None:
312+
"""Populate Philippines and its provinces."""
313+
philippines, created = Geography.objects.get_or_create(
314+
name="Philippines",
315+
defaults={"code": "PH", "type": GeoTypes.COUNTRY, "parent_id": parent},
316+
)
317+
if created:
318+
self.stdout.write(f"Created country: {philippines.name}")
319+
else:
320+
self.stdout.write(f"Country already exists: {philippines.name}")
321+
322+
philippine_provinces = [
323+
# Luzon - NCR
324+
("Metro Manila", "NCR"),
325+
# Luzon - CAR
326+
("Abra", "ABR"),
327+
("Apayao", "APA"),
328+
("Benguet", "BEN"),
329+
("Ifugao", "IFU"),
330+
("Kalinga", "KAL"),
331+
("Mountain Province", "MOU"),
332+
# Luzon - Region I (Ilocos)
333+
("Ilocos Norte", "ILN"),
334+
("Ilocos Sur", "ILS"),
335+
("La Union", "LUN"),
336+
("Pangasinan", "PAN"),
337+
# Luzon - Region II (Cagayan Valley)
338+
("Batanes", "BTN"),
339+
("Cagayan", "CAG"),
340+
("Isabela", "ISA"),
341+
("Nueva Vizcaya", "NUV"),
342+
("Quirino", "QUI"),
343+
# Luzon - Region III (Central Luzon)
344+
("Aurora", "AUR"),
345+
("Bataan", "BAN"),
346+
("Bulacan", "BUL"),
347+
("Nueva Ecija", "NUE"),
348+
("Pampanga", "PAM"),
349+
("Tarlac", "TAR"),
350+
("Zambales", "ZMB"),
351+
# Luzon - Region IV-A (CALABARZON)
352+
("Batangas", "BTG"),
353+
("Cavite", "CAV"),
354+
("Laguna", "LAG"),
355+
("Quezon", "QUE"),
356+
("Rizal", "RIZ"),
357+
# Luzon - Region IV-B (MIMAROPA)
358+
("Marinduque", "MAD"),
359+
("Occidental Mindoro", "MDC"),
360+
("Oriental Mindoro", "MDR"),
361+
("Palawan", "PLW"),
362+
("Romblon", "ROM"),
363+
# Luzon - Region V (Bicol)
364+
("Albay", "ALB"),
365+
("Camarines Norte", "CAN"),
366+
("Camarines Sur", "CAS"),
367+
("Catanduanes", "CAT"),
368+
("Masbate", "MAS"),
369+
("Sorsogon", "SOR"),
370+
# Visayas - Region VI (Western Visayas)
371+
("Aklan", "AKL"),
372+
("Antique", "ANT"),
373+
("Capiz", "CAP"),
374+
("Guimaras", "GUI"),
375+
("Iloilo", "ILI"),
376+
("Negros Occidental", "NEC"),
377+
# Visayas - Region VII (Central Visayas)
378+
("Bohol", "BOH"),
379+
("Cebu", "CEB"),
380+
("Negros Oriental", "NER"),
381+
("Siquijor", "SIG"),
382+
# Visayas - Region VIII (Eastern Visayas)
383+
("Biliran", "BIL"),
384+
("Eastern Samar", "EAS"),
385+
("Leyte", "LEY"),
386+
("Northern Samar", "NSA"),
387+
("Samar", "WSA"),
388+
("Southern Leyte", "SLE"),
389+
# Mindanao - Region IX (Zamboanga Peninsula)
390+
("Zamboanga del Norte", "ZAN"),
391+
("Zamboanga del Sur", "ZAS"),
392+
("Zamboanga Sibugay", "ZSI"),
393+
# Mindanao - Region X (Northern Mindanao)
394+
("Bukidnon", "BUK"),
395+
("Camiguin", "CAM"),
396+
("Lanao del Norte", "LAN"),
397+
("Misamis Occidental", "MSC"),
398+
("Misamis Oriental", "MSR"),
399+
# Mindanao - Region XI (Davao)
400+
("Davao de Oro", "COM"),
401+
("Davao del Norte", "DAV"),
402+
("Davao del Sur", "DAS"),
403+
("Davao Occidental", "DAO"),
404+
("Davao Oriental", "DAO"),
405+
# Mindanao - Region XII (SOCCSKSARGEN)
406+
("Cotabato", "NCO"),
407+
("Sarangani", "SAR"),
408+
("South Cotabato", "SCO"),
409+
("Sultan Kudarat", "SUK"),
410+
# Mindanao - Region XIII (Caraga)
411+
("Agusan del Norte", "AGN"),
412+
("Agusan del Sur", "AGS"),
413+
("Dinagat Islands", "DIN"),
414+
("Surigao del Norte", "SUN"),
415+
("Surigao del Sur", "SUR"),
416+
# Mindanao - BARMM
417+
("Basilan", "BAS"),
418+
("Lanao del Sur", "LAS"),
419+
("Maguindanao", "MAG"),
420+
("Sulu", "SLU"),
421+
("Tawi-Tawi", "TAW"),
422+
]
423+
424+
created_count = 0
425+
for province_name, province_code in philippine_provinces:
426+
_, created = Geography.objects.get_or_create(
427+
name=province_name,
428+
defaults={
429+
"code": province_code,
430+
"type": GeoTypes.STATE,
431+
"parent_id": philippines,
432+
},
433+
)
434+
if created:
435+
created_count += 1
436+
437+
self.stdout.write(
438+
f"Philippine provinces: {created_count} created, {len(philippine_provinces) - created_count} already existed"
439+
)

api/models/Geography.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import List, Optional
22

33
from django.db import models
44

@@ -19,6 +19,49 @@ class Geography(models.Model):
1919
def __str__(self) -> str:
2020
return f"{self.name} ({self.type})"
2121

22+
def get_all_descendant_names(self) -> List[str]:
23+
"""
24+
Get all descendant geography names including self.
25+
This is used for hierarchical filtering - when a parent geography is selected,
26+
all child geographies should also be included in the filter.
27+
28+
Returns:
29+
List of geography names including self and all descendants
30+
"""
31+
descendants = [self.name]
32+
children = Geography.objects.filter(parent_id=self.id)
33+
34+
for child in children:
35+
descendants.extend(child.get_all_descendant_names()) # type: ignore[attr-defined]
36+
37+
return descendants
38+
39+
@classmethod
40+
def get_geography_names_with_descendants(
41+
cls, geography_names: List[str]
42+
) -> List[str]:
43+
"""
44+
Given a list of geography names, return all names including their descendants.
45+
This is a helper method for filtering that expands parent geographies to include children.
46+
47+
Args:
48+
geography_names: List of geography names to expand
49+
50+
Returns:
51+
List of geography names including all descendants
52+
"""
53+
all_names = set()
54+
55+
for name in geography_names:
56+
try:
57+
geography = cls.objects.get(name=name)
58+
all_names.update(geography.get_all_descendant_names())
59+
except cls.DoesNotExist:
60+
# If geography doesn't exist, just add the name as-is
61+
all_names.add(name)
62+
63+
return list(all_names)
64+
2265
class Meta:
2366
db_table = "geography"
2467
verbose_name_plural = "geographies"

api/models/SDG.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ class SDG(models.Model):
1313
code = models.CharField(
1414
max_length=10, unique=True, null=False, blank=False
1515
) # e.g., "SDG1", "SDG2"
16+
number = models.IntegerField(
17+
null=True, blank=True
18+
) # Numeric value for proper ordering (1, 2, 3... 17)
1619
description = models.TextField(null=True, blank=True)
1720
slug = models.SlugField(max_length=100, null=True, blank=False, unique=True)
1821

@@ -28,4 +31,4 @@ class Meta:
2831
db_table = "sdg"
2932
verbose_name = "SDG"
3033
verbose_name_plural = "SDGs"
31-
ordering = ["code"]
34+
ordering = ["number"]

api/types/type_collaborative.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def metadata(self) -> Optional[List["TypeCollaborativeMetadata"]]:
187187
def contributors(self) -> Optional[List["TypeUser"]]:
188188
"""Get contributors associated with this collaborative."""
189189
try:
190-
queryset = self.contributors.all() # type: ignore
190+
queryset = self.contributors.all().order_by("first_name") # type: ignore
191191
if not queryset.exists():
192192
return []
193193
return TypeUser.from_django_list(queryset)
@@ -215,10 +215,14 @@ def organization_relationships(
215215
def supporting_organizations(self) -> Optional[List["TypeOrganization"]]:
216216
"""Get supporting organizations for this collaborative."""
217217
try:
218-
relationships = CollaborativeOrganizationRelationship.objects.filter(
219-
collaborative=self, # type: ignore
220-
relationship_type=OrganizationRelationshipType.SUPPORTER,
221-
).select_related("organization")
218+
relationships = (
219+
CollaborativeOrganizationRelationship.objects.filter(
220+
collaborative=self, # type: ignore
221+
relationship_type=OrganizationRelationshipType.SUPPORTER,
222+
)
223+
.select_related("organization")
224+
.order_by("organization__name")
225+
)
222226

223227
if not relationships.exists():
224228
return []
@@ -232,10 +236,14 @@ def supporting_organizations(self) -> Optional[List["TypeOrganization"]]:
232236
def partner_organizations(self) -> Optional[List["TypeOrganization"]]:
233237
"""Get partner organizations for this collaborative."""
234238
try:
235-
relationships = CollaborativeOrganizationRelationship.objects.filter(
236-
collaborative=self, # type: ignore
237-
relationship_type=OrganizationRelationshipType.PARTNER,
238-
).select_related("organization")
239+
relationships = (
240+
CollaborativeOrganizationRelationship.objects.filter(
241+
collaborative=self, # type: ignore
242+
relationship_type=OrganizationRelationshipType.PARTNER,
243+
)
244+
.select_related("organization")
245+
.order_by("organization__name")
246+
)
239247

240248
if not relationships.exists():
241249
return []

api/types/type_sdg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class SDGFilter:
2121
class SDGOrder:
2222
"""Order class for SDG model."""
2323

24+
number: auto
2425
code: auto
2526
name: auto
2627

api/views/paginated_elastic_view.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,21 @@ def get(self, request: HttpRequest) -> Response:
9191
serializer = self.serializer_class(response, many=True)
9292
aggregations: Dict[str, Any] = response.aggregations.to_dict()
9393

94-
metadata_aggregations = aggregations["metadata"]["filtered_metadata"][
95-
"composite_agg"
96-
]["buckets"]
97-
aggregations.pop("metadata")
98-
if "catalogs" in aggregations:
99-
aggregations.pop("catalogs")
100-
for agg in metadata_aggregations:
101-
label: str = agg["key"]["metadata_label"]
94+
if "metadata" in aggregations:
95+
96+
metadata_aggregations = aggregations["metadata"]["filtered_metadata"][
97+
"composite_agg"
98+
]["buckets"]
99+
aggregations.pop("metadata")
100+
for agg in metadata_aggregations:
101+
label: str = agg["key"]["metadata_label"]
102102
value: str = agg["key"].get("metadata_value", "")
103103
if label not in aggregations:
104104
aggregations[label] = {}
105105
aggregations[label][value] = agg["doc_count"]
106106

107+
if "catalogs" in aggregations:
108+
aggregations.pop("catalogs")
107109
# Handle sectors aggregation (now comes as "sectors.raw")
108110
if "sectors.raw" in aggregations:
109111
sectors_agg = aggregations["sectors.raw"]["buckets"]
@@ -139,6 +141,20 @@ def get(self, request: HttpRequest) -> Response:
139141
for agg in formats_agg:
140142
aggregations["formats"][agg["key"]] = agg["doc_count"]
141143

144+
# Handle geographies aggregation (now comes as "geographies.raw")
145+
if "geographies.raw" in aggregations:
146+
geographies_agg = aggregations["geographies.raw"]["buckets"]
147+
aggregations.pop("geographies.raw")
148+
aggregations["geographies"] = {}
149+
for agg in geographies_agg:
150+
aggregations["geographies"][agg["key"]] = agg["doc_count"]
151+
elif "geographies" in aggregations:
152+
geographies_agg = aggregations["geographies"]["buckets"]
153+
aggregations.pop("geographies")
154+
aggregations["geographies"] = {}
155+
for agg in geographies_agg:
156+
aggregations["geographies"][agg["key"]] = agg["doc_count"]
157+
142158
if "status" in aggregations:
143159
status_agg = aggregations["status"]["buckets"]
144160
aggregations.pop("status")

api/views/search_dataset.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from rest_framework import serializers
1010
from rest_framework.permissions import AllowAny
1111

12-
from api.models import Dataset, DatasetMetadata, Metadata
12+
from api.models import Dataset, DatasetMetadata, Geography, Metadata
1313
from api.utils.telemetry_utils import trace_method, track_metrics
1414
from api.views.paginated_elastic_view import PaginatedElasticSearchAPIView
1515
from search.documents import DatasetDocument
@@ -260,6 +260,13 @@ def add_filters(self, filters: Dict[str, str], search: Search) -> Search:
260260
raw_filter = filter + ".raw"
261261
if raw_filter in self.aggregations:
262262
filter_values = filters[filter].split(",")
263+
264+
# For geographies, expand to include all descendant geographies
265+
if filter == "geographies":
266+
filter_values = Geography.get_geography_names_with_descendants(
267+
filter_values
268+
)
269+
263270
search = search.filter("terms", **{raw_filter: filter_values})
264271
else:
265272
search = search.filter("term", **{filter: filters[filter]})

api/views/search_usecase.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from rest_framework import serializers
1010
from rest_framework.permissions import AllowAny
1111

12-
from api.models import Metadata, UseCase, UseCaseMetadata
12+
from api.models import Geography, Metadata, UseCase, UseCaseMetadata
1313
from api.utils.telemetry_utils import trace_method, track_metrics
1414
from api.views.paginated_elastic_view import PaginatedElasticSearchAPIView
1515
from search.documents import UseCaseDocument
@@ -311,6 +311,13 @@ def add_filters(self, filters: Dict[str, str], search: Search) -> Search:
311311
raw_filter = filter + ".raw"
312312
if raw_filter in self.aggregations:
313313
filter_values = filters[filter].split(",")
314+
315+
# For geographies, expand to include all descendant geographies
316+
if filter == "geographies":
317+
filter_values = Geography.get_geography_names_with_descendants(
318+
filter_values
319+
)
320+
314321
search = search.filter("terms", **{raw_filter: filter_values})
315322
else:
316323
search = search.filter("term", **{filter: filters[filter]})

0 commit comments

Comments
 (0)