Skip to content

Commit

Permalink
Hot fix PR to add expenditure and income to the aggregating API witho…
Browse files Browse the repository at this point in the history
…ut changing the API parameter and field names for the demo, see #HEA-659
  • Loading branch information
chrispreee committed Jan 24, 2025
1 parent da40467 commit 0623ab3
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 24 deletions.
30 changes: 27 additions & 3 deletions apps/baseline/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,11 +1509,17 @@ class Meta:
"livelihood_activity_pk",
"wealth_group_category_code",
"population_estimate",
"product_cpc",
"product_common_name",
"slice_sum_kcals_consumed",
"sum_kcals_consumed",
"kcals_consumed_percent",
"product_cpc",
"product_common_name",
"sum_income",
"slice_sum_income",
"income_percent",
"sum_expenditure",
"slice_sum_expenditure",
"expenditure_percent",
)

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

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

livelihood_zone_name = DictQuerySetField("livelihood_zone_name")
Expand Down Expand Up @@ -1565,6 +1578,14 @@ class Meta:
sum_kcals_consumed = DictQuerySetField("sum_kcals_consumed")
kcals_consumed_percent = DictQuerySetField("kcals_consumed_percent")

slice_sum_income = DictQuerySetField("slice_sum_income")
sum_income = DictQuerySetField("sum_income")
income_percent = DictQuerySetField("income_percent")

slice_sum_expenditure = DictQuerySetField("slice_sum_expenditure")
sum_expenditure = DictQuerySetField("sum_expenditure")
expenditure_percent = DictQuerySetField("expenditure_percent")

def get_fields(self):
"""
User can specify fields= parameter to specify a field list, comma-delimited.
Expand Down Expand Up @@ -1621,12 +1642,15 @@ def field_to_database_path(field_name):
"livelihood_activity_pk": "livelihood_strategies__livelihoodactivity__pk",
"wealth_group_category_code": "livelihood_strategies__livelihoodactivity__wealth_group__wealth_group_category__code", # NOQA: E501
"kcals_consumed": "livelihood_strategies__livelihoodactivity__kcals_consumed",
"income": "livelihood_strategies__livelihoodactivity__income",
"expenditure": "livelihood_strategies__livelihoodactivity__expenditure",
"percentage_kcals": "livelihood_strategies__livelihoodactivity__percentage_kcals",
"livelihood_zone_name": f"livelihood_zone__name_{language_code}",
"source_organization_pk": "source_organization__pk",
"source_organization_name": "source_organization__name",
"country_pk": "livelihood_zone__country__pk",
"country_iso_en_name": "livelihood_zone__country__iso_en_name",
"product_cpc": "livelihood_strategies__product",
"product_cpc": "livelihood_strategies__product__cpc",
"strategy_type": "livelihood_strategies__strategy_type",
"product_common_name": f"livelihood_strategies__product__common_name_{language_code}",
}.get(field_name, field_name)
Expand Down
31 changes: 10 additions & 21 deletions apps/baseline/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.apps import apps
from django.conf import settings
from django.db import models
from django.db.models import F, OuterRef, Q, Subquery
from django.db.models import F, FloatField, Q, Subquery
from django.db.models.functions import Coalesce, NullIf
from django.utils.translation import override
from django_filters import rest_framework as filters
Expand Down Expand Up @@ -1770,24 +1770,9 @@ def global_aggregates(self):
"""
global_aggregates = {}
for field_name, aggregate in self.serializer_class.aggregates.items():
subquery = LivelihoodZoneBaseline.objects.all()

# The FilterSet applies the global filters, such as Wealth Group Category.
# We also need to apply these to the subquery that gets the kcal totals per LZB (eg, the kcal_percent
# denominator), to restrict the 100% value by, for example, wealth group.
subquery = self.filter_queryset(subquery)

# Join to outer query
subquery = subquery.filter(pk=OuterRef("pk"))

# Annotate with the aggregate expression, eg, sum_kcals_consumed
aggregate_field_name = self.serializer_class.aggregate_field_name(field_name, aggregate)
subquery = subquery.annotate(
**{aggregate_field_name: aggregate(self.serializer_class.field_to_database_path(field_name))}
).values(aggregate_field_name)[:1]

global_aggregates[aggregate_field_name] = Subquery(subquery)

field_path = self.serializer_class.field_to_database_path(field_name)
global_aggregates[aggregate_field_name] = aggregate(field_path, default=0, output_field=FloatField())
return global_aggregates

def get_slice_aggregates(self):
Expand All @@ -1803,7 +1788,9 @@ def get_slice_aggregates(self):
# Annotate the queryset with the aggregate, eg, slice_sum_kcals_consumed, applying the slice filters.
# This is then divided by, eg, sum_kcals_consumed for the percentage of the slice.
field_path = self.serializer_class.field_to_database_path(field_name)
slice_aggregates[aggregate_field_name] = aggregate(field_path, filter=slice_filter, default=0)
slice_aggregates[aggregate_field_name] = aggregate(
field_path, filter=slice_filter, default=0, output_field=FloatField()
)
return slice_aggregates

def get_slice_filters(self):
Expand All @@ -1825,8 +1812,10 @@ def get_calculations_on_aggregates(self):
for field_name, aggregate in self.serializer_class.aggregates.items():
slice_total = F(self.serializer_class.slice_aggregate_field_name(field_name, aggregate))
overall_total = F(self.serializer_class.aggregate_field_name(field_name, aggregate))
expr = slice_total * 100 / NullIf(overall_total, 0) # Protects against divide by zero
expr = Coalesce(expr, 0) # Zero if no LivActivities found for prod/strategy slice
# Protect against divide by zero (divide by null returns null without error)
expr = slice_total * 100 / NullIf(overall_total, 0)
# Zero if no LivActivities found for prod/strategy slice, rather than null:
expr = Coalesce(expr, 0, output_field=FloatField())
slice_percent_field_name = self.serializer_class.slice_percent_field_name(field_name, aggregate)
calcs_on_aggregates[slice_percent_field_name] = expr
return calcs_on_aggregates
Expand Down

0 comments on commit 0623ab3

Please sign in to comment.