Skip to content

Commit 553520c

Browse files
authored
Merge pull request #245 from American-Institutes-for-Research/HEA-947/Add-LivelihoodStrategyType-to-LivelihoodZoneBaselineFacetedSearchView
Hea 947/add livelihood strategy type to livelihood zone baseline faceted search view
2 parents c96d141 + 58a6de5 commit 553520c

2 files changed

Lines changed: 195 additions & 32 deletions

File tree

apps/baseline/tests/test_viewsets.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222
from common.fields import translation_fields
2323
from common.tests.factories import ClassifiedProductFactory, CountryFactory
24-
from metadata.models import LivelihoodActivityScenario
24+
from metadata.models import LivelihoodActivityScenario, LivelihoodStrategyType
2525
from metadata.tests.factories import (
2626
CharacteristicGroupFactory,
2727
LivelihoodCategoryFactory,
@@ -762,6 +762,15 @@ def test_search_with_product(self):
762762
self.assertEqual(search_data["products"][0]["count"], 2) # 2 zones have this product
763763
# confirm the product value is correct
764764
self.assertEqual(search_data["products"][0]["value"], self.product1.cpc)
765+
# confirm livelihood_zone_baselines are present with correct data
766+
baselines = search_data["products"][0]["livelihood_zone_baselines"]
767+
self.assertEqual(len(baselines), 2)
768+
baseline_ids = {b["id"] for b in baselines}
769+
self.assertEqual(baseline_ids, {self.baseline1.id, self.baseline3.id})
770+
for b in baselines:
771+
self.assertIn("name", b)
772+
self.assertIn("livelihood_zone__code", b)
773+
self.assertIn("reference_year_end_date", b)
765774
# Apply the filters to the baseline
766775
baseline_url = reverse("livelihoodzonebaseline-list")
767776
response = self.client.get(
@@ -775,9 +784,11 @@ def test_search_with_product(self):
775784
self.assertTrue(any(d["name"] == self.baseline3.name for d in data))
776785
self.assertFalse(any(d["name"] == self.baseline2.name for d in data))
777786

787+
# Find the item matching characteristic1 (not relying on order)
788+
characteristic1_item = next(item for item in search_data["items"] if item["value"] == self.characteristic1.pk)
778789
response = self.client.get(
779790
baseline_url,
780-
{search_data["items"][0]["filter"]: search_data["items"][0]["value"]},
791+
{characteristic1_item["filter"]: characteristic1_item["value"]},
781792
)
782793
self.assertEqual(response.status_code, 200)
783794
self.assertEqual(len(json.loads(response.content)), 1)
@@ -804,6 +815,14 @@ def test_search_with_wealth_characterstics(self):
804815
self.assertEqual(len(data["items"]), 2)
805816
self.assertEqual(data["items"][0]["count"], 1) # 1 zone for this characteristic
806817
self.assertEqual(data["items"][1]["count"], 1) # 1 zone for this characteristic
818+
# confirm livelihood_zone_baselines are present on each item
819+
for item in data["items"]:
820+
self.assertIn("livelihood_zone_baselines", item)
821+
self.assertEqual(len(item["livelihood_zone_baselines"]), 1)
822+
self.assertIn("id", item["livelihood_zone_baselines"][0])
823+
self.assertIn("name", item["livelihood_zone_baselines"][0])
824+
self.assertIn("livelihood_zone__code", item["livelihood_zone_baselines"][0])
825+
self.assertIn("reference_year_end_date", item["livelihood_zone_baselines"][0])
807826
# Search by the second characteristic
808827
response = self.client.get(
809828
self.url,
@@ -826,6 +845,86 @@ def test_search_with_wealth_characterstics(self):
826845
self.assertEqual(response.status_code, 200)
827846
self.assertEqual(len(data["items"]), 0)
828847

848+
def test_search_with_livelihood_strategy_type(self):
849+
# Create products with "goat" in the name for partial match testing
850+
goat_product = ClassifiedProductFactory(
851+
cpc="L09901AA",
852+
description_en="Goats",
853+
common_name_en="Goat",
854+
)
855+
goat_meat_product = ClassifiedProductFactory(
856+
cpc="L09902AA",
857+
description_en="Goat Meat",
858+
common_name_en="Goat Meat",
859+
)
860+
goat_milk_product = ClassifiedProductFactory(
861+
cpc="L09903AA",
862+
description_en="Goat's Milk",
863+
common_name_en="Goat Milk",
864+
)
865+
# Create strategies linking products to baselines with specific strategy types
866+
LivelihoodStrategyFactory(
867+
product=goat_milk_product,
868+
livelihood_zone_baseline=self.baseline1,
869+
strategy_type=LivelihoodStrategyType.MILK_PRODUCTION,
870+
)
871+
LivelihoodStrategyFactory(
872+
product=goat_meat_product,
873+
livelihood_zone_baseline=self.baseline2,
874+
strategy_type=LivelihoodStrategyType.MEAT_PRODUCTION,
875+
)
876+
LivelihoodStrategyFactory(
877+
product=goat_product,
878+
livelihood_zone_baseline=self.baseline3,
879+
strategy_type=LivelihoodStrategyType.LIVESTOCK_SALE,
880+
)
881+
# Test that search "Milk" returns MilkProduction in livelihood_strategy_types facet
882+
response = self.client.get(self.url, {"search": "Milk"})
883+
self.assertEqual(response.status_code, 200)
884+
data = response.data
885+
self.assertIn("livelihood_strategy_types", data)
886+
strategy_type_results = data["livelihood_strategy_types"]
887+
milk_results = [r for r in strategy_type_results if r["value"] == "MilkProduction"]
888+
self.assertEqual(len(milk_results), 1)
889+
self.assertEqual(milk_results[0]["filter"], "strategy_type")
890+
self.assertEqual(milk_results[0]["value_label"], "Milk Production")
891+
self.assertEqual(milk_results[0]["count"], 1)
892+
# confirm livelihood_zone_baselines on strategy type result
893+
baselines = milk_results[0]["livelihood_zone_baselines"]
894+
self.assertEqual(len(baselines), 1)
895+
self.assertEqual(baselines[0]["id"], self.baseline1.id)
896+
self.assertIn("name", baselines[0])
897+
self.assertIn("livelihood_zone__code", baselines[0])
898+
self.assertIn("reference_year_end_date", baselines[0])
899+
900+
# Test that search "lait" with language=fr returns MilkProduction via French translation
901+
response = self.client.get(self.url, {"search": "lait", "language": "fr"})
902+
self.assertEqual(response.status_code, 200)
903+
data = response.data
904+
strategy_type_results = data["livelihood_strategy_types"]
905+
milk_results = [r for r in strategy_type_results if r["value"] == "MilkProduction"]
906+
self.assertEqual(len(milk_results), 1)
907+
self.assertEqual(milk_results[0]["value_label"], "Production du lait")
908+
909+
# Test that search "goat" returns multiple goat-related products
910+
response = self.client.get(self.url, {"search": "goat", "language": "en"})
911+
self.assertEqual(response.status_code, 200)
912+
data = response.data
913+
product_results = data["products"]
914+
product_cpcs = {r["value"] for r in product_results}
915+
self.assertIn(goat_product.cpc, product_cpcs)
916+
self.assertIn(goat_meat_product.cpc, product_cpcs)
917+
self.assertIn(goat_milk_product.cpc, product_cpcs)
918+
self.assertEqual(len(product_results), 3)
919+
920+
# test taht strategy_type filter to baseline list endpoint
921+
baseline_url = reverse("livelihoodzonebaseline-list")
922+
response = self.client.get(baseline_url, {"strategy_type": "MilkProduction"})
923+
self.assertEqual(response.status_code, 200)
924+
baseline_data = json.loads(response.content)
925+
self.assertEqual(len(baseline_data), 1)
926+
self.assertEqual(baseline_data[0]["name"], self.baseline1.name)
927+
829928

830929
class LivelihoodProductCategoryViewSetTestCase(APITestCase):
831930
@classmethod

apps/baseline/viewsets.py

Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
from common.fields import translation_fields
1919
from common.filters import DefaultingDateFilter, MultiFieldFilter, UpperCaseFilter
20+
from common.models import ClassifiedProduct
2021
from common.utils import make_condition_funcs
2122
from common.viewsets import AggregatingViewSet, BaseModelViewSet
22-
from metadata.models import WealthGroupCategory
23+
from metadata.models import LivelihoodStrategyType, WealthGroupCategory
2324

2425
from .models import (
2526
BaselineLivelihoodActivity,
@@ -212,6 +213,7 @@ class Meta:
212213
wealth_characteristic = CharFilter(
213214
method="filter_by_wealth_characteristic", label="Filter by Wealth Characteristic"
214215
)
216+
strategy_type = CharFilter(method="filter_by_strategy_type", label="Filter by Strategy Type")
215217
as_of_date = DefaultingDateFilter(
216218
label="As of Date",
217219
help_text="Filter baselines valid as of this date (YYYY-MM-DD format or special values like 'today').",
@@ -257,6 +259,13 @@ def filter_by_wealth_characteristic(self, queryset, name, value):
257259

258260
return queryset.filter(id__in=Subquery(matching_baselines))
259261

262+
def filter_by_strategy_type(self, queryset, name, value):
263+
"""
264+
Filter the baseline by matching livelihood strategy type
265+
"""
266+
matching_baselines = LivelihoodStrategy.objects.filter(strategy_type=value).values("livelihood_zone_baseline")
267+
return queryset.filter(id__in=matching_baselines)
268+
260269

261270
class LivelihoodZoneBaselineViewSet(BaseModelViewSet):
262271
"""
@@ -2088,6 +2097,28 @@ class LivelihoodZoneBaselineFacetedSearchView(APIView):
20882097
renderer_classes = [JSONRenderer]
20892098
permission_classes = [AllowAny]
20902099

2100+
def _get_baselines(self, baselines_qs):
2101+
# returs a list of baseline dicts with id, name, livelihood_zone__code, reference_year_end_date.
2102+
return [
2103+
{
2104+
"id": baseline.id,
2105+
"name": baseline.name,
2106+
"livelihood_zone__code": baseline.livelihood_zone.code,
2107+
"reference_year_end_date": baseline.reference_year_end_date,
2108+
}
2109+
for baseline in baselines_qs.select_related("livelihood_zone")
2110+
]
2111+
2112+
def _search_products(self, search_term):
2113+
# Search products using icontains for broader matching than the default iexact search.
2114+
q_object = Q()
2115+
for field in [*translation_fields("description"), *translation_fields("common_name")]:
2116+
q_object |= Q(**{f"{field}__icontains": search_term})
2117+
q_object |= Q(cpc__startswith=search_term)
2118+
q_object |= Q(aliases__contains=[search_term.lower()])
2119+
q_object |= Q(scientific_name__icontains=search_term)
2120+
return ClassifiedProduct.objects.filter(q_object).distinct()
2121+
20912122
def get(self, request, format=None):
20922123
"""
20932124
Return a faceted set of matching filters
@@ -2100,56 +2131,89 @@ def get(self, request, format=None):
21002131
for model_entry in MODELS_TO_SEARCH:
21012132
app_name = model_entry["app_name"]
21022133
model_name = model_entry["model_name"]
2103-
filter, filter_label, filter_category = (
2104-
model_entry["filter"]["key"],
2105-
model_entry["filter"]["label"],
2106-
model_entry["filter"]["category"],
2107-
)
2134+
filter_key = model_entry["filter"]["key"]
2135+
filter_label = model_entry["filter"]["label"]
2136+
filter_category = model_entry["filter"]["category"]
21082137
ModelClass = apps.get_model(app_name, model_name)
2109-
search_per_model = ModelClass.objects.search(search_term)
2138+
if model_name == "ClassifiedProduct":
2139+
search_per_model = self._search_products(search_term)
2140+
else:
2141+
search_per_model = ModelClass.objects.search(search_term)
21102142
results[filter_category] = []
21112143
# for activating language
21122144
with override(language):
21132145
for search_result in search_per_model:
21142146
if model_name == "ClassifiedProduct":
2115-
unique_zones = (
2116-
LivelihoodStrategy.objects.filter(product=search_result)
2117-
.values("livelihood_zone_baseline")
2118-
.distinct()
2119-
.count()
2120-
)
2147+
baselines_qs = LivelihoodZoneBaseline.objects.filter(
2148+
id__in=LivelihoodStrategy.objects.filter(product=search_result).values(
2149+
"livelihood_zone_baseline"
2150+
)
2151+
).distinct()
21212152
value_label, value = search_result.description, search_result.pk
21222153
elif model_name == "LivelihoodCategory":
2123-
unique_zones = LivelihoodZoneBaseline.objects.filter(
2154+
baselines_qs = LivelihoodZoneBaseline.objects.filter(
21242155
main_livelihood_category=search_result
2125-
).count()
2156+
)
21262157
value_label, value = search_result.description, search_result.pk
21272158
elif model_name == "LivelihoodZone":
2128-
unique_zones = LivelihoodZoneBaseline.objects.filter(livelihood_zone=search_result).count()
2159+
baselines_qs = LivelihoodZoneBaseline.objects.filter(livelihood_zone=search_result)
21292160
value_label, value = search_result.name, search_result.pk
21302161
elif model_name == "WealthCharacteristic":
2131-
unique_zones = (
2132-
WealthGroupCharacteristicValue.objects.filter(wealth_characteristic=search_result)
2133-
.values("wealth_group__livelihood_zone_baseline")
2134-
.distinct()
2135-
.count()
2136-
)
2162+
baselines_qs = LivelihoodZoneBaseline.objects.filter(
2163+
id__in=WealthGroupCharacteristicValue.objects.filter(
2164+
wealth_characteristic=search_result
2165+
).values("wealth_group__livelihood_zone_baseline")
2166+
).distinct()
21372167
value_label, value = search_result.description, search_result.pk
21382168
elif model_name == "Country":
2139-
unique_zones = (
2140-
LivelihoodZoneBaseline.objects.filter(livelihood_zone__country=search_result)
2141-
.distinct()
2142-
.count()
2143-
)
2169+
baselines_qs = LivelihoodZoneBaseline.objects.filter(
2170+
livelihood_zone__country=search_result
2171+
).distinct()
21442172
value_label, value = search_result.iso_en_name, search_result.pk
2145-
if unique_zones > 0:
2173+
baselines = self._get_baselines(baselines_qs)
2174+
if baselines:
21462175
results[filter_category].append(
21472176
{
2148-
"filter": filter,
2177+
"filter": filter_key,
21492178
"filter_label": filter_label,
21502179
"value_label": value_label,
21512180
"value": value,
2152-
"count": unique_zones,
2181+
"count": len(baselines),
2182+
"livelihood_zone_baselines": baselines,
2183+
}
2184+
)
2185+
2186+
# Search livelihood strategy types (not a model query)
2187+
results["livelihood_strategy_types"] = []
2188+
search_lower = search_term.lower()
2189+
# Get English labels outside language override for matching
2190+
english_labels = {}
2191+
with override("en"):
2192+
for strategy_type in LivelihoodStrategyType:
2193+
english_labels[strategy_type.value] = str(strategy_type.label).lower()
2194+
with override(language):
2195+
for strategy_type in LivelihoodStrategyType:
2196+
translated_label = str(strategy_type.label)
2197+
if (
2198+
search_lower in strategy_type.value.lower()
2199+
or search_lower in english_labels[strategy_type.value]
2200+
or search_lower in translated_label.lower()
2201+
):
2202+
baselines_qs = LivelihoodZoneBaseline.objects.filter(
2203+
id__in=LivelihoodStrategy.objects.filter(strategy_type=strategy_type.value).values(
2204+
"livelihood_zone_baseline"
2205+
)
2206+
).distinct()
2207+
baselines = self._get_baselines(baselines_qs)
2208+
if baselines:
2209+
results["livelihood_strategy_types"].append(
2210+
{
2211+
"filter": "strategy_type",
2212+
"filter_label": "Livelihood Strategy Type",
2213+
"value_label": translated_label,
2214+
"value": strategy_type.value,
2215+
"count": len(baselines),
2216+
"livelihood_zone_baselines": baselines,
21532217
}
21542218
)
21552219

0 commit comments

Comments
 (0)