1- from django .db .models import Sum
1+ from django .db .models import F , FloatField , Sum
22from django .utils import translation
3- from rest_framework import fields as rest_framework_fields
43from rest_framework import serializers
54from rest_framework_gis .serializers import GeoFeatureModelSerializer
65
76from common .fields import translation_fields
8- from metadata . models import LivelihoodStrategyType
7+ from common . serializers import AggregatingSerializer
98
109from .models import (
1110 BaselineLivelihoodActivity ,
@@ -1472,28 +1471,78 @@ def get_wealth_group_label(self, obj):
14721471 return str (obj .wealth_group )
14731472
14741473
1475- class DictQuerySetField (rest_framework_fields .SerializerMethodField ):
1476- def __init__ (self , field_name = None , ** kwargs ):
1477- self .field_name = field_name
1478- super ().__init__ (** kwargs )
1474+ class LivelihoodZoneBaselineReportSerializer (AggregatingSerializer ):
1475+ """
1476+ There are two ‘levels’ of filter needed on this endpoint. The standard ones which are already on the LZB endpoint
1477+ filter the LZBs that are returned (eg, population range and wealth group). Let’s call them ‘global’ filters.
1478+ Everything needs filtering by wealth group or population, if those filters are active.
14791479
1480- def to_representation ( self , obj ):
1481- return self . parent . get_field ( obj , self . field_name )
1480+ The data ‘slice’ strategy type and product filters do not remove LZBs from the results by themselves; they
1481+ only exclude values from the calculated slice statistics.
14821482
1483+ If a user selects Sorghum, that filters the kcals income for our slice. The kcals income for the slice is then
1484+ divided by the kcals income on the global set for the kcals income percent.
1485+
1486+ The global filters are identical to those already on the LZB endpoint (and will always be - it is sharing the
1487+ code). These are applied to the LZB, row and slice totals.
1488+
1489+ The slice filters are:
1490+
1491+ - slice_by_product (for multiple, repeat the parameter, eg, slice_by_product=R0&slice_by_product=B01). These
1492+ match any CPC code that starts with the value. (The client needs to convert the selected product to CPC.)
1493+
1494+ - slice_by_strategy_type - you can specify multiple, and you need to pass the code not the label (which could be
1495+ translated). (These are case-insensitive but otherwise must be an exact match.)
1496+
1497+ The slice is defined by matching any of the products, AND any of the strategy types (as opposed to OR).
1498+
1499+ Translated fields, eg, name, description, are rendered in the currently selected locale if possible. (Except
1500+ Country, which has different translations following ISO.) This can be selected in the UI or set using eg,
1501+ &language=pt which overrides the UI selection.
1502+
1503+ You select the fields you want using the &fields= parameter in the usual way. If you omit the fields parameter all
1504+ fields are returned. These are currently the same field list as the normal LZB endpoint, plus the aggregations,
1505+ called slice_sum_kcals_consumed, sum_kcals_consumed, kcals_consumed_percent, plus product CPC and product common
1506+ name translated. If you omit a field, the statistics for that field will be aggregated together.
1507+
1508+ The ordering code is also shared with the normal LZB endpoint, which uses the standard
1509+ &ordering= parameter. If none are specified, the results are sorted by the aggregations descending, ie,
1510+ biggest percentage first.
1511+
1512+ The strategy type codes are:
1513+ MilkProduction
1514+ ButterProduction
1515+ MeatProduction
1516+ LivestockSale
1517+ CropProduction
1518+ FoodPurchase
1519+ PaymentInKind
1520+ ReliefGiftOther
1521+ Hunting
1522+ Fishing
1523+ WildFoodGathering
1524+ OtherCashIncome
1525+ OtherPurchase
1526+
1527+ The product hierarchy can be retrieved from the classified product endpoint /api/classifiedproduct/.
1528+
1529+ You can then filter by any of the calculated fields. To do so, prefix the field name with min_ or max_.
1530+ """
14831531
1484- class LivelihoodZoneBaselineReportSerializer (serializers .ModelSerializer ):
14851532 class Meta :
14861533 model = LivelihoodZoneBaseline
14871534 fields = (
1488- "id" ,
1489- "name" ,
1490- "description" ,
14911535 "source_organization" ,
14921536 "source_organization_name" ,
1493- "livelihood_zone" ,
1494- "livelihood_zone_name" ,
14951537 "country_pk" ,
14961538 "country_iso_en_name" ,
1539+ "livelihoodzone_pk" ,
1540+ "livelihood_zone" ,
1541+ "livelihood_zone_name" ,
1542+ "id" ,
1543+ "name" ,
1544+ "description" ,
1545+ "wealth_group_category_code" ,
14971546 "main_livelihood_category" ,
14981547 "bss" ,
14991548 "currency" ,
@@ -1503,112 +1552,33 @@ class Meta:
15031552 "valid_to_date" , # to display "is latest" / "is historic" in the UI for each ref yr
15041553 "population_source" ,
15051554 "population_estimate" ,
1506- "livelihoodzone_pk" ,
1507- "livelihood_strategy_pk" ,
15081555 "strategy_type" ,
1556+ "livelihood_strategy_pk" ,
15091557 "livelihood_activity_pk" ,
1510- "wealth_group_category_code" ,
1511- "population_estimate" ,
1512- "slice_sum_kcals_consumed" ,
1513- "sum_kcals_consumed" ,
1514- "kcals_consumed_percent" ,
15151558 "product_cpc" ,
15161559 "product_common_name" ,
15171560 )
15181561
1519- # For each of these aggregates the following calculation columns are added:
1520- # (a) Total at the LZB level (filtered by population, wealth group, etc), eg, sum_kcals_consumed.
1521- # (b) Total for the selected product/strategy type slice, eg, slice_sum_kcals_consumed.
1522- # (c) The percentage the slice represents of the whole, eg, kcals_consumed_percent.
1523- # Filters are automatically created, eg, min_kcals_consumed_percent and max_kcals_consumed_percent.
1524- # If no ordering is specified by the FilterSet, the results are ordered by percent descending in the order here.
15251562 aggregates = {
15261563 "kcals_consumed" : Sum ,
1564+ "income" : Sum ,
1565+ "expenditure" : Sum ,
1566+ "percentage_kcals" : Sum ,
1567+ "kcal_income_sum" : Sum (
1568+ (
1569+ F ("livelihood_strategies__livelihoodactivity__quantity_purchased" )
1570+ + F ("livelihood_strategies__livelihoodactivity__quantity_produced" )
1571+ )
1572+ * F ("livelihood_strategies__product__kcals_per_unit" ),
1573+ output_field = FloatField (),
1574+ ),
15271575 }
15281576
1529- # For each of these pairs, a URL parameter is created "slice_{field}", eg, ?slice_product=
1530- # They can appear zero, one or multiple times in the URL, and define a sub-slice of the row-level data.
1531- # A slice includes activities with ANY of the products, AND, ANY of the strategy types.
1532- # For example: (product=R0 OR product=L0) AND (strategy_type=MilkProd OR strategy_type=CropProd)
15331577 slice_fields = {
15341578 "product" : "livelihood_strategies__product__cpc__istartswith" ,
15351579 "strategy_type" : "livelihood_strategies__strategy_type__iexact" ,
15361580 }
15371581
1538- livelihood_zone_name = DictQuerySetField ("livelihood_zone_name" )
1539- source_organization_name = DictQuerySetField ("source_organization_pk" )
1540- country_pk = DictQuerySetField ("country_pk" )
1541- country_iso_en_name = DictQuerySetField ("country_iso_en_name" )
1542- livelihoodzone_pk = DictQuerySetField ("livelihoodzone_pk" )
1543- livelihood_strategy_pk = DictQuerySetField ("livelihood_strategy_pk" )
1544- livelihood_activity_pk = DictQuerySetField ("livelihood_activity_pk" )
1545- wealth_group_category_code = DictQuerySetField ("wealth_group_category_code" )
1546- id = DictQuerySetField ("id" )
1547- name = DictQuerySetField ("name" )
1548- description = DictQuerySetField ("description" )
1549- source_organization = DictQuerySetField ("source_organization" )
1550- livelihood_zone = DictQuerySetField ("livelihood_zone" )
1551- main_livelihood_category = DictQuerySetField ("main_livelihood_category" )
1552- bss = DictQuerySetField ("bss" )
1553- currency = DictQuerySetField ("currency" )
1554- reference_year_start_date = DictQuerySetField ("reference_year_start_date" )
1555- reference_year_end_date = DictQuerySetField ("reference_year_end_date" )
1556- valid_from_date = DictQuerySetField ("valid_from_date" )
1557- valid_to_date = DictQuerySetField ("valid_to_date" )
1558- population_source = DictQuerySetField ("population_source" )
1559- population_estimate = DictQuerySetField ("population_estimate" )
1560- product_cpc = DictQuerySetField ("product_cpc" )
1561- product_common_name = DictQuerySetField ("product_common_name" )
1562- strategy_type = DictQuerySetField ("strategy_type" )
1563-
1564- slice_sum_kcals_consumed = DictQuerySetField ("slice_sum_kcals_consumed" )
1565- sum_kcals_consumed = DictQuerySetField ("sum_kcals_consumed" )
1566- kcals_consumed_percent = DictQuerySetField ("kcals_consumed_percent" )
1567-
1568- def get_fields (self ):
1569- """
1570- User can specify fields= parameter to specify a field list, comma-delimited.
1571-
1572- If the fields parameter is not passed or does not match fields, defaults to self.Meta.fields.
1573-
1574- The aggregated fields self.aggregates are added regardless of user field selection.
1575- """
1576- field_list = "request" in self .context and self .context ["request" ].query_params .get ("fields" , None )
1577- if not field_list :
1578- return super ().get_fields ()
1579-
1580- # User-provided list of fields
1581- field_names = set (field_list .split ("," ))
1582-
1583- # Add the aggregates that are always returned
1584- for field_name , aggregate in self .aggregates .items ():
1585- field_names |= {
1586- field_name ,
1587- self .aggregate_field_name (field_name , aggregate ),
1588- self .slice_aggregate_field_name (field_name , aggregate ),
1589- self .slice_percent_field_name (field_name , aggregate ),
1590- }
1591-
1592- # Add the ordering field if specified
1593- ordering = self .context ["request" ].query_params .get ("ordering" )
1594- if ordering :
1595- field_names .add (ordering )
1596-
1597- # Remove any that don't match a field as a dict
1598- return {k : v for k , v in super ().get_fields ().items () if k in field_names }
1599-
1600- def get_field (self , obj , field_name ):
1601- """
1602- Aggregated querysets are a list of dicts.
1603- This is called by AggregatedQuerysetField to get the value from the row dict.
1604- """
1605- db_field = self .field_to_database_path (field_name )
1606- value = obj .get (db_field , "" )
1607- # Get the readable, translated string from the choice key.
1608- if field_name == "strategy_type" and value :
1609- return dict (LivelihoodStrategyType .choices ).get (value , value )
1610- return value
1611-
16121582 @staticmethod
16131583 def field_to_database_path (field_name ):
16141584 language_code = translation .get_language ()
@@ -1621,24 +1591,15 @@ def field_to_database_path(field_name):
16211591 "livelihood_activity_pk" : "livelihood_strategies__livelihoodactivity__pk" ,
16221592 "wealth_group_category_code" : "livelihood_strategies__livelihoodactivity__wealth_group__wealth_group_category__code" , # NOQA: E501
16231593 "kcals_consumed" : "livelihood_strategies__livelihoodactivity__kcals_consumed" ,
1594+ "income" : "livelihood_strategies__livelihoodactivity__income" ,
1595+ "expenditure" : "livelihood_strategies__livelihoodactivity__expenditure" ,
1596+ "percentage_kcals" : "livelihood_strategies__livelihoodactivity__percentage_kcals" ,
16241597 "livelihood_zone_name" : f"livelihood_zone__name_{ language_code } " ,
16251598 "source_organization_pk" : "source_organization__pk" ,
16261599 "source_organization_name" : "source_organization__name" ,
16271600 "country_pk" : "livelihood_zone__country__pk" ,
16281601 "country_iso_en_name" : "livelihood_zone__country__iso_en_name" ,
1629- "product_cpc" : "livelihood_strategies__product " ,
1602+ "product_cpc" : "livelihood_strategies__product__cpc " ,
16301603 "strategy_type" : "livelihood_strategies__strategy_type" ,
16311604 "product_common_name" : f"livelihood_strategies__product__common_name_{ language_code } " ,
16321605 }.get (field_name , field_name )
1633-
1634- @staticmethod
1635- def aggregate_field_name (field_name , aggregate ):
1636- return f"{ aggregate .name .lower ()} _{ field_name } " # eg, sum_kcals_consumed
1637-
1638- @staticmethod
1639- def slice_aggregate_field_name (field_name , aggregate ):
1640- return f"slice_{ aggregate .name .lower ()} _{ field_name } " # eg, slice_sum_kcals_consumed
1641-
1642- @staticmethod
1643- def slice_percent_field_name (field_name , aggregate ):
1644- return f"{ field_name } _percent" # eg, kcals_consumed_percent
0 commit comments