Skip to content

Commit 0623ab3

Browse files
committed
Hot fix PR to add expenditure and income to the aggregating API without changing the API parameter and field names for the demo, see #HEA-659
1 parent da40467 commit 0623ab3

File tree

2 files changed

+37
-24
lines changed

2 files changed

+37
-24
lines changed

apps/baseline/serializers.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,11 +1509,17 @@ class Meta:
15091509
"livelihood_activity_pk",
15101510
"wealth_group_category_code",
15111511
"population_estimate",
1512+
"product_cpc",
1513+
"product_common_name",
15121514
"slice_sum_kcals_consumed",
15131515
"sum_kcals_consumed",
15141516
"kcals_consumed_percent",
1515-
"product_cpc",
1516-
"product_common_name",
1517+
"sum_income",
1518+
"slice_sum_income",
1519+
"income_percent",
1520+
"sum_expenditure",
1521+
"slice_sum_expenditure",
1522+
"expenditure_percent",
15171523
)
15181524

15191525
# For each of these aggregates the following calculation columns are added:
@@ -1524,6 +1530,8 @@ class Meta:
15241530
# If no ordering is specified by the FilterSet, the results are ordered by percent descending in the order here.
15251531
aggregates = {
15261532
"kcals_consumed": Sum,
1533+
"income": Sum,
1534+
"expenditure": Sum,
15271535
}
15281536

15291537
# For each of these pairs, a URL parameter is created "slice_{field}", eg, ?slice_product=
@@ -1532,7 +1540,12 @@ class Meta:
15321540
# For example: (product=R0 OR product=L0) AND (strategy_type=MilkProd OR strategy_type=CropProd)
15331541
slice_fields = {
15341542
"product": "livelihood_strategies__product__cpc__istartswith",
1543+
# this parameter must be set to one of values (not labels) from LivelihoodStrategyType, eg, MilkProduction
15351544
"strategy_type": "livelihood_strategies__strategy_type__iexact",
1545+
# TODO: Support filter expressions on the right here, so we can slice on, for example, a
1546+
# WealthGroupCharacteristicValue where WealthGroupCharacteristic is some hard-coded value,
1547+
# eg, the slice on WGCV where WGC=PhoneOwnership, or on WGCV > 3 where WGC=HouseholdSize, eg:
1548+
# {"phone_ownership": lambda val: Q(wgcv__path=val, wgc__path__code="PhoneOwnership")}
15361549
}
15371550

15381551
livelihood_zone_name = DictQuerySetField("livelihood_zone_name")
@@ -1565,6 +1578,14 @@ class Meta:
15651578
sum_kcals_consumed = DictQuerySetField("sum_kcals_consumed")
15661579
kcals_consumed_percent = DictQuerySetField("kcals_consumed_percent")
15671580

1581+
slice_sum_income = DictQuerySetField("slice_sum_income")
1582+
sum_income = DictQuerySetField("sum_income")
1583+
income_percent = DictQuerySetField("income_percent")
1584+
1585+
slice_sum_expenditure = DictQuerySetField("slice_sum_expenditure")
1586+
sum_expenditure = DictQuerySetField("sum_expenditure")
1587+
expenditure_percent = DictQuerySetField("expenditure_percent")
1588+
15681589
def get_fields(self):
15691590
"""
15701591
User can specify fields= parameter to specify a field list, comma-delimited.
@@ -1621,12 +1642,15 @@ def field_to_database_path(field_name):
16211642
"livelihood_activity_pk": "livelihood_strategies__livelihoodactivity__pk",
16221643
"wealth_group_category_code": "livelihood_strategies__livelihoodactivity__wealth_group__wealth_group_category__code", # NOQA: E501
16231644
"kcals_consumed": "livelihood_strategies__livelihoodactivity__kcals_consumed",
1645+
"income": "livelihood_strategies__livelihoodactivity__income",
1646+
"expenditure": "livelihood_strategies__livelihoodactivity__expenditure",
1647+
"percentage_kcals": "livelihood_strategies__livelihoodactivity__percentage_kcals",
16241648
"livelihood_zone_name": f"livelihood_zone__name_{language_code}",
16251649
"source_organization_pk": "source_organization__pk",
16261650
"source_organization_name": "source_organization__name",
16271651
"country_pk": "livelihood_zone__country__pk",
16281652
"country_iso_en_name": "livelihood_zone__country__iso_en_name",
1629-
"product_cpc": "livelihood_strategies__product",
1653+
"product_cpc": "livelihood_strategies__product__cpc",
16301654
"strategy_type": "livelihood_strategies__strategy_type",
16311655
"product_common_name": f"livelihood_strategies__product__common_name_{language_code}",
16321656
}.get(field_name, field_name)

apps/baseline/viewsets.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.apps import apps
22
from django.conf import settings
33
from django.db import models
4-
from django.db.models import F, OuterRef, Q, Subquery
4+
from django.db.models import F, FloatField, Q, Subquery
55
from django.db.models.functions import Coalesce, NullIf
66
from django.utils.translation import override
77
from django_filters import rest_framework as filters
@@ -1770,24 +1770,9 @@ def global_aggregates(self):
17701770
"""
17711771
global_aggregates = {}
17721772
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
17841773
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-
1774+
field_path = self.serializer_class.field_to_database_path(field_name)
1775+
global_aggregates[aggregate_field_name] = aggregate(field_path, default=0, output_field=FloatField())
17911776
return global_aggregates
17921777

17931778
def get_slice_aggregates(self):
@@ -1803,7 +1788,9 @@ def get_slice_aggregates(self):
18031788
# Annotate the queryset with the aggregate, eg, slice_sum_kcals_consumed, applying the slice filters.
18041789
# This is then divided by, eg, sum_kcals_consumed for the percentage of the slice.
18051790
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)
1791+
slice_aggregates[aggregate_field_name] = aggregate(
1792+
field_path, filter=slice_filter, default=0, output_field=FloatField()
1793+
)
18071794
return slice_aggregates
18081795

18091796
def get_slice_filters(self):
@@ -1825,8 +1812,10 @@ def get_calculations_on_aggregates(self):
18251812
for field_name, aggregate in self.serializer_class.aggregates.items():
18261813
slice_total = F(self.serializer_class.slice_aggregate_field_name(field_name, aggregate))
18271814
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
1815+
# Protect against divide by zero (divide by null returns null without error)
1816+
expr = slice_total * 100 / NullIf(overall_total, 0)
1817+
# Zero if no LivActivities found for prod/strategy slice, rather than null:
1818+
expr = Coalesce(expr, 0, output_field=FloatField())
18301819
slice_percent_field_name = self.serializer_class.slice_percent_field_name(field_name, aggregate)
18311820
calcs_on_aggregates[slice_percent_field_name] = expr
18321821
return calcs_on_aggregates

0 commit comments

Comments
 (0)