From 0623ab3da1ddc6e332bc5cc03eca01e27aed2945 Mon Sep 17 00:00:00 2001 From: chrispreee <117157625+chrispreee@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:49:59 +0000 Subject: [PATCH] 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 --- apps/baseline/serializers.py | 30 +++++++++++++++++++++++++++--- apps/baseline/viewsets.py | 31 ++++++++++--------------------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/apps/baseline/serializers.py b/apps/baseline/serializers.py index 7b3cc74..338eed0 100644 --- a/apps/baseline/serializers.py +++ b/apps/baseline/serializers.py @@ -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: @@ -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= @@ -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") @@ -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. @@ -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) diff --git a/apps/baseline/viewsets.py b/apps/baseline/viewsets.py index 20e9d13..eccb70a 100644 --- a/apps/baseline/viewsets.py +++ b/apps/baseline/viewsets.py @@ -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 @@ -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): @@ -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): @@ -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