Skip to content

Commit 7734573

Browse files
committed
Initial working draft of aggregating serializer and viewset, see #HEA-547
1 parent e8d8c36 commit 7734573

File tree

4 files changed

+388
-2
lines changed

4 files changed

+388
-2
lines changed

apps/baseline/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ class LivelihoodActivity(common_models.Model):
10991099
quantity_sold = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Sold/Exchanged"))
11001100
quantity_other_uses = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Other Uses"))
11011101
# Can normally be calculated / validated as `quantity_produced + quantity_purchased - quantity_sold - quantity_other_uses` # NOQA: E501
1102-
# but there are exceptions, such as MilkProduction, where there is also an amount used for ButterProduction
1102+
# but there are exceptions, such as MilkProduction, where there is also an amount used for ButterProduction, is this captured quantity_other_uses? # NOQA: E501
11031103
quantity_consumed = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Consumed"))
11041104

11051105
price = models.FloatField(blank=True, null=True, verbose_name=_("Price"), help_text=_("Price per unit"))
@@ -1110,7 +1110,7 @@ class LivelihoodActivity(common_models.Model):
11101110
# of external goods or services.
11111111
expenditure = models.FloatField(blank=True, null=True, help_text=_("Expenditure"))
11121112

1113-
# Can normally be calculated / validated as `quantity_consumed` * `kcals_per_unit`
1113+
# Can normally be calculated / validated as `quantity_consumed` * `livelihoodstrategy__product__kcals_per_unit`
11141114
kcals_consumed = models.PositiveIntegerField(
11151115
blank=True,
11161116
null=True,

apps/baseline/serializers.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from django.db.models import Sum
2+
from django.utils import translation
3+
from rest_framework import fields as rest_framework_fields
14
from rest_framework import serializers
25
from rest_framework_gis.serializers import GeoFeatureModelSerializer
36

47
from common.fields import translation_fields
8+
from metadata.models import LivelihoodStrategyType
59

610
from .models import (
711
BaselineLivelihoodActivity,
@@ -1466,3 +1470,175 @@ def get_strategy_label(self, obj):
14661470

14671471
def get_wealth_group_label(self, obj):
14681472
return str(obj.wealth_group)
1473+
1474+
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)
1479+
1480+
def to_representation(self, obj):
1481+
return self.parent.get_field(obj, self.field_name)
1482+
1483+
1484+
class LivelihoodZoneBaselineReportSerializer(serializers.ModelSerializer):
1485+
class Meta:
1486+
model = LivelihoodZoneBaseline
1487+
fields = (
1488+
"id",
1489+
"name",
1490+
"description",
1491+
"source_organization",
1492+
"source_organization_name",
1493+
"livelihood_zone",
1494+
"livelihood_zone_name",
1495+
"country_pk",
1496+
"country_iso_en_name",
1497+
"main_livelihood_category",
1498+
"bss",
1499+
"currency",
1500+
"reference_year_start_date",
1501+
"reference_year_end_date",
1502+
"valid_from_date",
1503+
"valid_to_date", # to display "is latest" / "is historic" in the UI for each ref yr
1504+
"population_source",
1505+
"population_estimate",
1506+
"livelihoodzone_pk",
1507+
"livelihood_strategy_pk",
1508+
"strategy_type",
1509+
"livelihood_activity_pk",
1510+
"wealth_group_category_code",
1511+
"population_estimate",
1512+
"slice_sum_kcals_consumed",
1513+
"sum_kcals_consumed",
1514+
"kcals_consumed_percent",
1515+
"product_cpc",
1516+
"product_common_name",
1517+
)
1518+
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.
1525+
aggregates = {
1526+
"kcals_consumed": Sum,
1527+
}
1528+
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)
1533+
slice_fields = {
1534+
"product": "livelihood_strategies__product__cpc__istartswith",
1535+
"strategy_type": "livelihood_strategies__strategy_type__iexact",
1536+
}
1537+
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+
1612+
@staticmethod
1613+
def field_to_database_path(field_name):
1614+
language_code = translation.get_language()
1615+
return {
1616+
"livelihoodzone_pk": "pk",
1617+
"name": f"name_{language_code}",
1618+
"description": f"description_{language_code}",
1619+
"valid_to_date": "valid_to_date",
1620+
"livelihood_strategy_pk": "livelihood_strategies__pk",
1621+
"livelihood_activity_pk": "livelihood_strategies__livelihoodactivity__pk",
1622+
"wealth_group_category_code": "livelihood_strategies__livelihoodactivity__wealth_group__wealth_group_category__code", # NOQA: E501
1623+
"kcals_consumed": "livelihood_strategies__livelihoodactivity__kcals_consumed",
1624+
"livelihood_zone_name": f"livelihood_zone__name_{language_code}",
1625+
"source_organization_pk": "source_organization__pk",
1626+
"source_organization_name": "source_organization__name",
1627+
"country_pk": "livelihood_zone__country__pk",
1628+
"country_iso_en_name": "livelihood_zone__country__iso_en_name",
1629+
"product_cpc": "livelihood_strategies__product",
1630+
"strategy_type": "livelihood_strategies__strategy_type",
1631+
"product_common_name": f"livelihood_strategies__product__common_name_{language_code}",
1632+
}.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

Comments
 (0)