Skip to content

Commit 9e11f0b

Browse files
authored
Merge pull request #290 from American-Institutes-for-Research/HEA-1086/improve_livelihood_activity_admin
Hea 1086/improve livelihood activity admin
2 parents d32d220 + e4e3fa5 commit 9e11f0b

4 files changed

Lines changed: 186 additions & 12 deletions

File tree

apps/baseline/admin.py

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from copy import deepcopy
23

34
from binary_database_files.models import File
@@ -330,6 +331,7 @@ class LivelihoodStrategyAdmin(admin.ModelAdmin):
330331
*translation_fields("livelihood_zone_baseline__livelihood_zone__name__icontains"),
331332
*translation_fields("product__common_name__icontains"),
332333
*translation_fields("season__name__icontains"),
334+
"livelihood_zone_baseline__livelihood_zone__country__iso_en_ro_name",
333335
)
334336

335337
list_filter = (
@@ -338,6 +340,22 @@ class LivelihoodStrategyAdmin(admin.ModelAdmin):
338340
("livelihood_zone_baseline__livelihood_zone__country", admin.RelatedOnlyFieldListFilter),
339341
)
340342

343+
def get_search_results(self, request, queryset, search_term):
344+
# Allow natural key format "BF01: 2011-10-31" by stripping the colon separator.
345+
normalized = search_term.replace(":", "")
346+
date_match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", normalized)
347+
year_match = re.search(r"\b(\d{4})\b", normalized)
348+
# Remove date/year from the term so text fields are searched without them.
349+
text_term = re.sub(r"\b\d{4}(?:-\d{2}-\d{2})?\b", "", normalized).strip()
350+
queryset, use_distinct = super().get_search_results(request, queryset, text_term)
351+
if date_match:
352+
queryset = queryset.filter(livelihood_zone_baseline__reference_year_end_date=date_match.group(1))
353+
elif year_match:
354+
queryset = queryset.filter(
355+
livelihood_zone_baseline__reference_year_end_date__year=int(year_match.group(1))
356+
)
357+
return queryset, use_distinct
358+
341359
def get_queryset(self, request):
342360
return (
343361
super()
@@ -504,6 +522,8 @@ class LivelihoodActivityAdmin(admin.ModelAdmin):
504522
list_filter = (
505523
"strategy_type",
506524
"scenario",
525+
("livelihood_zone_baseline", admin.RelatedOnlyFieldListFilter),
526+
("wealth_group__wealth_group_category", admin.RelatedOnlyFieldListFilter),
507527
("livelihood_strategy__product", admin.RelatedOnlyFieldListFilter),
508528
("livelihood_strategy__season", admin.RelatedOnlyFieldListFilter),
509529
("livelihood_zone_baseline__livelihood_zone__country", admin.RelatedOnlyFieldListFilter),
@@ -517,15 +537,32 @@ class LivelihoodActivityAdmin(admin.ModelAdmin):
517537
"livelihood_strategy__additional_identifier__icontains",
518538
"livelihood_zone_baseline__livelihood_zone__code",
519539
"livelihood_zone_baseline__livelihood_zone__alternate_code",
540+
*translation_fields("livelihood_zone_baseline__livelihood_zone__name"),
520541
"livelihood_strategy__product__cpc__iexact",
521542
"livelihood_strategy__product__aliases__icontains",
522543
"livelihood_strategy__season__aliases__icontains",
523544
"livelihood_strategy__additional_identifier",
524545
*translation_fields("livelihood_strategy__product__common_name__icontains"),
525546
*translation_fields("livelihood_strategy__product__description__icontains"),
526547
*translation_fields("livelihood_strategy__season__name__icontains"),
548+
"livelihood_zone_baseline__livelihood_zone__country__iso_en_ro_name",
527549
)
528550

551+
def get_search_results(self, request, queryset, search_term):
552+
# Allow natural key format "BF01: 2011-10-31" by stripping the colon separator.
553+
normalized = search_term.replace(":", "")
554+
date_match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", normalized)
555+
year_match = re.search(r"\b(\d{4})\b", normalized)
556+
text_term = re.sub(r"\b\d{4}(?:-\d{2}-\d{2})?\b", "", normalized).strip()
557+
queryset, use_distinct = super().get_search_results(request, queryset, text_term)
558+
if date_match:
559+
queryset = queryset.filter(livelihood_zone_baseline__reference_year_end_date=date_match.group(1))
560+
elif year_match:
561+
queryset = queryset.filter(
562+
livelihood_zone_baseline__reference_year_end_date__year=int(year_match.group(1))
563+
)
564+
return queryset, use_distinct
565+
529566
def get_object(self, request, object_id, from_field=None):
530567
obj = super().get_object(request, object_id, from_field)
531568
if obj is None:
@@ -600,6 +637,7 @@ def get_country_name(self, obj):
600637
None,
601638
{
602639
"fields": [
640+
"wealth_group",
603641
"livelihood_strategy",
604642
"scenario",
605643
"extra",
@@ -1065,19 +1103,39 @@ class SeasonalActivityAdmin(admin.ModelAdmin):
10651103
"is_key",
10661104
)
10671105
search_fields = (
1068-
"seasonal_activity_type",
1069-
"season",
1070-
"product",
1071-
"additional_identifier",
1106+
"livelihood_zone_baseline__livelihood_zone__code",
1107+
"livelihood_zone_baseline__livelihood_zone__alternate_code",
1108+
"seasonal_activity_type__code__icontains",
1109+
*translation_fields("seasonal_activity_type__name__icontains"),
1110+
*translation_fields("season__name__icontains"),
1111+
"season__aliases__icontains",
1112+
*translation_fields("product__common_name__icontains"),
1113+
"product__cpc__iexact",
1114+
"additional_identifier__icontains",
10721115
)
10731116
list_filter = (
10741117
"livelihood_zone_baseline__livelihood_zone",
10751118
"seasonal_activity_type",
1076-
"season",
1077-
"product",
1119+
("season", admin.RelatedOnlyFieldListFilter),
1120+
("product", admin.RelatedOnlyFieldListFilter),
10781121
"is_key",
10791122
)
10801123

1124+
def get_search_results(self, request, queryset, search_term):
1125+
# Allow natural key format "BF01: 2011-10-31" by stripping the colon separator.
1126+
normalized = search_term.replace(":", "")
1127+
date_match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", normalized)
1128+
year_match = re.search(r"\b(\d{4})\b", normalized)
1129+
text_term = re.sub(r"\b\d{4}(?:-\d{2}-\d{2})?\b", "", normalized).strip()
1130+
queryset, use_distinct = super().get_search_results(request, queryset, text_term)
1131+
if date_match:
1132+
queryset = queryset.filter(livelihood_zone_baseline__reference_year_end_date=date_match.group(1))
1133+
elif year_match:
1134+
queryset = queryset.filter(
1135+
livelihood_zone_baseline__reference_year_end_date__year=int(year_match.group(1))
1136+
)
1137+
return queryset, use_distinct
1138+
10811139

10821140
class SeasonalActivityOccurrenceAdmin(admin.ModelAdmin):
10831141
list_display = (
@@ -1088,10 +1146,15 @@ class SeasonalActivityOccurrenceAdmin(admin.ModelAdmin):
10881146
"end_month",
10891147
)
10901148
search_fields = (
1091-
"seasonal_activity__seasonal_activity_type",
1092-
"seasonal_activity__season",
1093-
"seasonal_activity__product",
1094-
"seasonal_activity__additional_identifier",
1149+
"seasonal_activity__livelihood_zone_baseline__livelihood_zone__code",
1150+
"seasonal_activity__livelihood_zone_baseline__livelihood_zone__alternate_code",
1151+
"seasonal_activity__seasonal_activity_type__code__icontains",
1152+
*translation_fields("seasonal_activity__seasonal_activity_type__name__icontains"),
1153+
*translation_fields("seasonal_activity__season__name__icontains"),
1154+
"seasonal_activity__season__aliases__icontains",
1155+
*translation_fields("seasonal_activity__product__common_name__icontains"),
1156+
"seasonal_activity__product__cpc__iexact",
1157+
"seasonal_activity__additional_identifier__icontains",
10951158
)
10961159
list_filter = (
10971160
"seasonal_activity__seasonal_activity_type",
@@ -1100,6 +1163,23 @@ class SeasonalActivityOccurrenceAdmin(admin.ModelAdmin):
11001163
)
11011164
ordering = ["start"]
11021165

1166+
def get_search_results(self, request, queryset, search_term):
1167+
# Allow natural key format "BF01: 2011-10-31" by stripping the colon separator.
1168+
normalized = search_term.replace(":", "")
1169+
date_match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", normalized)
1170+
year_match = re.search(r"\b(\d{4})\b", normalized)
1171+
text_term = re.sub(r"\b\d{4}(?:-\d{2}-\d{2})?\b", "", normalized).strip()
1172+
queryset, use_distinct = super().get_search_results(request, queryset, text_term)
1173+
if date_match:
1174+
queryset = queryset.filter(
1175+
seasonal_activity__livelihood_zone_baseline__reference_year_end_date=date_match.group(1)
1176+
)
1177+
elif year_match:
1178+
queryset = queryset.filter(
1179+
seasonal_activity__livelihood_zone_baseline__reference_year_end_date__year=int(year_match.group(1))
1180+
)
1181+
return queryset, use_distinct
1182+
11031183
@admin.display(boolean=True, description="Key seasonal activity")
11041184
def seasonal_activity_is_key(self, obj):
11051185
return obj.seasonal_activity.is_key

apps/baseline/tests/test_admin.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,22 @@ def test_livelihoodstrategy_search_fields(self):
262262
self.assertContains(response, self.strategy1.product.cpc)
263263
self.assertNotContains(response, self.strategy2.product.cpc)
264264

265+
def test_livelihoodstrategy_search_by_natural_key(self):
266+
zone_code = self.strategy1.livelihood_zone_baseline.livelihood_zone.code
267+
ref_end_date = self.strategy1.livelihood_zone_baseline.reference_year_end_date.isoformat()
268+
response = self.client.get(self.url, {"q": f"{zone_code}: {ref_end_date}"})
269+
self.assertEqual(response.status_code, 200)
270+
self.assertContains(response, self.strategy1.product.cpc)
271+
self.assertNotContains(response, self.strategy2.product.cpc)
272+
273+
def test_livelihoodstrategy_search_by_code_and_year(self):
274+
zone_code = self.strategy1.livelihood_zone_baseline.livelihood_zone.code
275+
ref_end_date = self.strategy1.livelihood_zone_baseline.reference_year_end_date
276+
response = self.client.get(self.url, {"q": f"{zone_code} {ref_end_date.year}"})
277+
self.assertEqual(response.status_code, 200)
278+
self.assertContains(response, self.strategy1.product.cpc)
279+
self.assertNotContains(response, self.strategy2.product.cpc)
280+
265281
def test_livelihoodstrategy_list_filter(self):
266282
response = self.client.get(
267283
self.url,
@@ -707,6 +723,32 @@ def test_search(self):
707723
self.assertIn(self.livelihood_strategy1.strategy_type, result_list_str)
708724
self.assertNotIn(self.livelihood_strategy2.strategy_type, result_list_str)
709725

726+
def test_search_by_baseline_natural_key(self):
727+
zone_code = self.livelihood_zone_baseline1.livelihood_zone.code
728+
ref_end_date = self.livelihood_zone_baseline1.reference_year_end_date.isoformat()
729+
natural_key = f"{zone_code}: {ref_end_date}"
730+
url = reverse("admin:baseline_livelihoodactivity_changelist") + f"?q={natural_key}"
731+
response = self.client.get(url)
732+
self.assertEqual(response.status_code, 200)
733+
soup = BeautifulSoup(response.content, "html.parser")
734+
result_list = soup.find(id="result_list")
735+
result_list_str = str(result_list)
736+
self.assertIn(f'value="{self.activity1.pk}"', result_list_str)
737+
self.assertNotIn(f'value="{self.activity2.pk}"', result_list_str)
738+
739+
def test_search_by_baseline_code_and_year(self):
740+
zone_code = self.livelihood_zone_baseline1.livelihood_zone.code
741+
ref_end_date = self.livelihood_zone_baseline1.reference_year_end_date
742+
search_term = f"{zone_code} {ref_end_date.year}"
743+
url = reverse("admin:baseline_livelihoodactivity_changelist") + f"?q={search_term}"
744+
response = self.client.get(url)
745+
self.assertEqual(response.status_code, 200)
746+
soup = BeautifulSoup(response.content, "html.parser")
747+
result_list = soup.find(id="result_list")
748+
result_list_str = str(result_list)
749+
self.assertIn(f'value="{self.activity1.pk}"', result_list_str)
750+
self.assertNotIn(f'value="{self.activity2.pk}"', result_list_str)
751+
710752
def test_get_product_common_name(self):
711753
modeladmin = LivelihoodActivityAdmin(LivelihoodActivity, self.site)
712754
self.assertEqual(
@@ -738,6 +780,8 @@ def test_filters(self):
738780
filters = {
739781
"strategy_type": self.livelihood_strategy1.strategy_type,
740782
"scenario": self.activity3.scenario,
783+
"livelihood_zone_baseline__id__exact": self.livelihood_zone_baseline1.pk,
784+
"wealth_group__wealth_group_category": self.activity1.wealth_group.wealth_group_category.pk,
741785
"livelihood_strategy__product__cpc": self.livelihood_strategy1.product.cpc,
742786
"livelihood_strategy__season__id__exact": self.livelihood_strategy2.season.pk,
743787
"livelihood_zone_baseline__livelihood_zone__country": country.iso3166a2,

apps/baseline/tests/test_viewsets.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,16 @@ def test_html(self):
21112111
df = pd.read_html(content)[0].fillna("")
21122112
self.assertEqual(len(df), self.num_records + 1)
21132113

2114+
def test_search_by_zone_code_and_year(self):
2115+
baseline = LivelihoodZoneBaselineFactory()
2116+
strategy = LivelihoodStrategyFactory(livelihood_zone_baseline=baseline)
2117+
zone_code = baseline.livelihood_zone.code
2118+
ref_year = baseline.reference_year_end_date.year
2119+
response = self.client.get(self.url, {"search": f"{zone_code} {ref_year}"})
2120+
self.assertEqual(response.status_code, 200)
2121+
ids = [r["id"] for r in response.json()]
2122+
self.assertIn(strategy.pk, ids)
2123+
21142124
def test_filter_by_country(self):
21152125
country = CountryFactory(
21162126
iso3166a2="AA",
@@ -2361,6 +2371,16 @@ def test_html(self):
23612371
df = pd.read_html(content)[0].fillna("")
23622372
self.assertEqual(len(df), self.num_records + 1)
23632373

2374+
def test_search_by_zone_code_and_year(self):
2375+
baseline = LivelihoodZoneBaselineFactory()
2376+
activity = LivelihoodActivityFactory(livelihood_zone_baseline=baseline)
2377+
zone_code = baseline.livelihood_zone.code
2378+
ref_year = baseline.reference_year_end_date.year
2379+
response = self.client.get(self.url, {"search": f"{zone_code} {ref_year}"})
2380+
self.assertEqual(response.status_code, 200)
2381+
ids = [r["id"] for r in response.json()]
2382+
self.assertIn(activity.pk, ids)
2383+
23642384
def test_filter_by_country(self):
23652385
country = CountryFactory(
23662386
iso3166a2="AA",

apps/baseline/viewsets.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from django.apps import apps
66
from django.conf import settings
77
from django.db import models
8-
from django.db.models import Expression, F, Q, Subquery, TextField, Value
9-
from django.db.models.functions import Coalesce, NullIf
8+
from django.db.models import CharField, Expression, F, Q, Subquery, TextField, Value
9+
from django.db.models.functions import Cast, Coalesce, ExtractYear, NullIf
1010
from django.utils import translation
1111
from django.utils.decorators import method_decorator
1212
from django.utils.translation import override
@@ -980,8 +980,23 @@ class LivelihoodStrategyViewSet(BaseModelViewSet):
980980
search_fields = [
981981
"additional_identifier",
982982
"strategy_type",
983+
"livelihood_zone_baseline__livelihood_zone__code",
984+
"livelihood_zone_baseline__livelihood_zone__alternate_code",
985+
"reference_year",
983986
]
984987

988+
def get_queryset(self):
989+
return (
990+
super()
991+
.get_queryset()
992+
.annotate(
993+
reference_year=Cast(
994+
ExtractYear("livelihood_zone_baseline__reference_year_end_date"),
995+
output_field=CharField(),
996+
)
997+
)
998+
)
999+
9851000

9861001
LIVELIHOOD_ACTIVITY_ORDER_BY = ["sort_key"]
9871002

@@ -1087,8 +1102,23 @@ class LivelihoodActivityViewSet(BaseModelViewSet):
10871102
search_fields = [
10881103
"scenario",
10891104
"strategy_type",
1105+
"livelihood_zone_baseline__livelihood_zone__code",
1106+
"livelihood_zone_baseline__livelihood_zone__alternate_code",
1107+
"reference_year",
10901108
]
10911109

1110+
def get_queryset(self):
1111+
return (
1112+
super()
1113+
.get_queryset()
1114+
.annotate(
1115+
reference_year=Cast(
1116+
ExtractYear("livelihood_zone_baseline__reference_year_end_date"),
1117+
output_field=CharField(),
1118+
)
1119+
)
1120+
)
1121+
10921122

10931123
class BaselineLivelihoodActivityFilterSet(LivelihoodActivityFilterSet):
10941124
class Meta(LivelihoodActivityFilterSet.Meta):

0 commit comments

Comments
 (0)