11from django .apps import apps
22from django .conf import settings
33from django .db import models
4- from django .db .models import F , OuterRef , Q , Subquery
4+ from django .db .models import F , FloatField , Q , Subquery
55from django .db .models .functions import Coalesce , NullIf
66from django .utils .translation import override
77from django_filters import rest_framework as filters
@@ -1628,6 +1628,115 @@ class CopingStrategyViewSet(BaseModelViewSet):
16281628 ]
16291629
16301630
1631+ MODELS_TO_SEARCH = [
1632+ {
1633+ "app_name" : "common" ,
1634+ "model_name" : "ClassifiedProduct" ,
1635+ "filter" : {"key" : "product" , "label" : "Product" , "category" : "products" },
1636+ },
1637+ {
1638+ "app_name" : "metadata" ,
1639+ "model_name" : "LivelihoodCategory" ,
1640+ "filter" : {"key" : "main_livelihood_category" , "label" : "Main Livelihood Category" , "category" : "zone_types" },
1641+ },
1642+ {
1643+ "app_name" : "baseline" ,
1644+ "model_name" : "LivelihoodZone" ,
1645+ "filter" : {"key" : "livelihood_zone" , "label" : "Livelihood zone" , "category" : "zones" },
1646+ },
1647+ {
1648+ "app_name" : "metadata" ,
1649+ "model_name" : "WealthCharacteristic" ,
1650+ "filter" : {"key" : "wealth_characteristic" , "label" : "Items" , "category" : "items" },
1651+ },
1652+ {
1653+ "app_name" : "common" ,
1654+ "model_name" : "Country" ,
1655+ "filter" : {"key" : "country" , "label" : "Country" , "category" : "countries" },
1656+ },
1657+ ]
1658+
1659+
1660+ class LivelihoodZoneBaselineFacetedSearchView (APIView ):
1661+ """
1662+ Performs a faceted search to find Livelihood Zone Baselines using a specified search term.
1663+
1664+ The search applies to multiple related models, filtering results based on the configured
1665+ criteria for each model. For each matching result, it calculates the number of unique
1666+ livelihood zones associated with the filter and includes relevant metadata in the response.
1667+ """
1668+
1669+ renderer_classes = [JSONRenderer ]
1670+ permission_classes = [AllowAny ]
1671+
1672+ def get (self , request , format = None ):
1673+ """
1674+ Return a faceted set of matching filters
1675+ """
1676+ results = {}
1677+ search_term = request .query_params .get (settings .REST_FRAMEWORK ["SEARCH_PARAM" ], "" )
1678+ language = request .query_params .get ("language" , "en" )
1679+
1680+ if search_term :
1681+ for model_entry in MODELS_TO_SEARCH :
1682+ app_name = model_entry ["app_name" ]
1683+ model_name = model_entry ["model_name" ]
1684+ filter , filter_label , filter_category = (
1685+ model_entry ["filter" ]["key" ],
1686+ model_entry ["filter" ]["label" ],
1687+ model_entry ["filter" ]["category" ],
1688+ )
1689+ ModelClass = apps .get_model (app_name , model_name )
1690+ search_per_model = ModelClass .objects .search (search_term )
1691+ results [filter_category ] = []
1692+ # for activating language
1693+ with override (language ):
1694+ for search_result in search_per_model :
1695+ if model_name == "ClassifiedProduct" :
1696+ unique_zones = (
1697+ LivelihoodStrategy .objects .filter (product = search_result )
1698+ .values ("livelihood_zone_baseline" )
1699+ .distinct ()
1700+ .count ()
1701+ )
1702+ value_label , value = search_result .description , search_result .pk
1703+ elif model_name == "LivelihoodCategory" :
1704+ unique_zones = LivelihoodZoneBaseline .objects .filter (
1705+ main_livelihood_category = search_result
1706+ ).count ()
1707+ value_label , value = search_result .description , search_result .pk
1708+ elif model_name == "LivelihoodZone" :
1709+ unique_zones = LivelihoodZoneBaseline .objects .filter (livelihood_zone = search_result ).count ()
1710+ value_label , value = search_result .name , search_result .pk
1711+ elif model_name == "WealthCharacteristic" :
1712+ unique_zones = (
1713+ WealthGroupCharacteristicValue .objects .filter (wealth_characteristic = search_result )
1714+ .values ("wealth_group__livelihood_zone_baseline" )
1715+ .distinct ()
1716+ .count ()
1717+ )
1718+ value_label , value = search_result .description , search_result .pk
1719+ elif model_name == "Country" :
1720+ unique_zones = (
1721+ LivelihoodZoneBaseline .objects .filter (livelihood_zone__country = search_result )
1722+ .distinct ()
1723+ .count ()
1724+ )
1725+ value_label , value = search_result .iso_en_name , search_result .pk
1726+ if unique_zones > 0 :
1727+ results [filter_category ].append (
1728+ {
1729+ "filter" : filter ,
1730+ "filter_label" : filter_label ,
1731+ "value_label" : value_label ,
1732+ "value" : value ,
1733+ "count" : unique_zones ,
1734+ }
1735+ )
1736+
1737+ return Response (results )
1738+
1739+
16311740class LivelihoodZoneBaselineReportViewSet (ModelViewSet ):
16321741 """
16331742 There are two ‘levels’ of filter needed on this endpoint. The standard ones which are already on the LZB endpoint
@@ -1770,24 +1879,9 @@ def global_aggregates(self):
17701879 """
17711880 global_aggregates = {}
17721881 for field_name , aggregate in self .serializer_class .aggregates .items ():
1773- subquery = LivelihoodZoneBaseline .objects .all ()
1774-
1775- # The FilterSet applies the global filters, such as Wealth Group Category.
1776- # We also need to apply these to the subquery that gets the kcal totals per LZB (eg, the kcal_percent
1777- # denominator), to restrict the 100% value by, for example, wealth group.
1778- subquery = self .filter_queryset (subquery )
1779-
1780- # Join to outer query
1781- subquery = subquery .filter (pk = OuterRef ("pk" ))
1782-
1783- # Annotate with the aggregate expression, eg, sum_kcals_consumed
17841882 aggregate_field_name = self .serializer_class .aggregate_field_name (field_name , aggregate )
1785- subquery = subquery .annotate (
1786- ** {aggregate_field_name : aggregate (self .serializer_class .field_to_database_path (field_name ))}
1787- ).values (aggregate_field_name )[:1 ]
1788-
1789- global_aggregates [aggregate_field_name ] = Subquery (subquery )
1790-
1883+ field_path = self .serializer_class .field_to_database_path (field_name )
1884+ global_aggregates [aggregate_field_name ] = aggregate (field_path , default = 0 , output_field = FloatField ())
17911885 return global_aggregates
17921886
17931887 def get_slice_aggregates (self ):
@@ -1803,7 +1897,9 @@ def get_slice_aggregates(self):
18031897 # Annotate the queryset with the aggregate, eg, slice_sum_kcals_consumed, applying the slice filters.
18041898 # This is then divided by, eg, sum_kcals_consumed for the percentage of the slice.
18051899 field_path = self .serializer_class .field_to_database_path (field_name )
1806- slice_aggregates [aggregate_field_name ] = aggregate (field_path , filter = slice_filter , default = 0 )
1900+ slice_aggregates [aggregate_field_name ] = aggregate (
1901+ field_path , filter = slice_filter , default = 0 , output_field = FloatField ()
1902+ )
18071903 return slice_aggregates
18081904
18091905 def get_slice_filters (self ):
@@ -1825,117 +1921,10 @@ def get_calculations_on_aggregates(self):
18251921 for field_name , aggregate in self .serializer_class .aggregates .items ():
18261922 slice_total = F (self .serializer_class .slice_aggregate_field_name (field_name , aggregate ))
18271923 overall_total = F (self .serializer_class .aggregate_field_name (field_name , aggregate ))
1828- expr = slice_total * 100 / NullIf (overall_total , 0 ) # Protects against divide by zero
1829- expr = Coalesce (expr , 0 ) # Zero if no LivActivities found for prod/strategy slice
1924+ # Protect against divide by zero (divide by null returns null without error)
1925+ expr = slice_total * 100 / NullIf (overall_total , 0 )
1926+ # Zero if no LivActivities found for prod/strategy slice, rather than null:
1927+ expr = Coalesce (expr , 0 , output_field = FloatField ())
18301928 slice_percent_field_name = self .serializer_class .slice_percent_field_name (field_name , aggregate )
18311929 calcs_on_aggregates [slice_percent_field_name ] = expr
18321930 return calcs_on_aggregates
1833-
1834-
1835- MODELS_TO_SEARCH = [
1836- {
1837- "app_name" : "common" ,
1838- "model_name" : "ClassifiedProduct" ,
1839- "filter" : {"key" : "product" , "label" : "Product" , "category" : "products" },
1840- },
1841- {
1842- "app_name" : "metadata" ,
1843- "model_name" : "LivelihoodCategory" ,
1844- "filter" : {"key" : "main_livelihood_category" , "label" : "Main Livelihood Category" , "category" : "zone_types" },
1845- },
1846- {
1847- "app_name" : "baseline" ,
1848- "model_name" : "LivelihoodZone" ,
1849- "filter" : {"key" : "livelihood_zone" , "label" : "Livelihood zone" , "category" : "zones" },
1850- },
1851- {
1852- "app_name" : "metadata" ,
1853- "model_name" : "WealthCharacteristic" ,
1854- "filter" : {"key" : "wealth_characteristic" , "label" : "Items" , "category" : "items" },
1855- },
1856- {
1857- "app_name" : "common" ,
1858- "model_name" : "Country" ,
1859- "filter" : {"key" : "country" , "label" : "Country" , "category" : "countries" },
1860- },
1861- ]
1862-
1863-
1864- class LivelihoodZoneBaselineFacetedSearchView (APIView ):
1865- """
1866- Performs a faceted search to find Livelihood Zone Baselines using a specified search term.
1867-
1868- The search applies to multiple related models, filtering results based on the configured
1869- criteria for each model. For each matching result, it calculates the number of unique
1870- livelihood zones associated with the filter and includes relevant metadata in the response.
1871- """
1872-
1873- renderer_classes = [JSONRenderer ]
1874- permission_classes = [AllowAny ]
1875-
1876- def get (self , request , format = None ):
1877- """
1878- Return a faceted set of matching filters
1879- """
1880- results = {}
1881- search_term = request .query_params .get (settings .REST_FRAMEWORK ["SEARCH_PARAM" ], "" )
1882- language = request .query_params .get ("language" , "en" )
1883-
1884- if search_term :
1885- for model_entry in MODELS_TO_SEARCH :
1886- app_name = model_entry ["app_name" ]
1887- model_name = model_entry ["model_name" ]
1888- filter , filter_label , filter_category = (
1889- model_entry ["filter" ]["key" ],
1890- model_entry ["filter" ]["label" ],
1891- model_entry ["filter" ]["category" ],
1892- )
1893- ModelClass = apps .get_model (app_name , model_name )
1894- search_per_model = ModelClass .objects .search (search_term )
1895- results [filter_category ] = []
1896- # for activating language
1897- with override (language ):
1898- for search_result in search_per_model :
1899- if model_name == "ClassifiedProduct" :
1900- unique_zones = (
1901- LivelihoodStrategy .objects .filter (product = search_result )
1902- .values ("livelihood_zone_baseline" )
1903- .distinct ()
1904- .count ()
1905- )
1906- value_label , value = search_result .description , search_result .pk
1907- elif model_name == "LivelihoodCategory" :
1908- unique_zones = LivelihoodZoneBaseline .objects .filter (
1909- main_livelihood_category = search_result
1910- ).count ()
1911- value_label , value = search_result .description , search_result .pk
1912- elif model_name == "LivelihoodZone" :
1913- unique_zones = LivelihoodZoneBaseline .objects .filter (livelihood_zone = search_result ).count ()
1914- value_label , value = search_result .name , search_result .pk
1915- elif model_name == "WealthCharacteristic" :
1916- unique_zones = (
1917- WealthGroupCharacteristicValue .objects .filter (wealth_characteristic = search_result )
1918- .values ("wealth_group__livelihood_zone_baseline" )
1919- .distinct ()
1920- .count ()
1921- )
1922- value_label , value = search_result .description , search_result .pk
1923- elif model_name == "Country" :
1924- unique_zones = (
1925- LivelihoodZoneBaseline .objects .filter (livelihood_zone__country = search_result )
1926- .distinct ()
1927- .count ()
1928- )
1929- value_label , value = search_result .iso_en_name , search_result .pk
1930- if unique_zones > 0 :
1931- results [filter_category ].append (
1932- {
1933- "filter" : filter ,
1934- "filter_label" : filter_label ,
1935- "value_label" : value_label ,
1936- "value" : value ,
1937- "count" : unique_zones ,
1938- }
1939- )
1940-
1941- return Response (results )
0 commit comments