|
| 1 | +from django.db.models import Sum |
| 2 | +from django.utils import translation |
| 3 | +from rest_framework import fields as rest_framework_fields |
1 | 4 | from rest_framework import serializers |
2 | 5 | from rest_framework_gis.serializers import GeoFeatureModelSerializer |
3 | 6 |
|
4 | 7 | from common.fields import translation_fields |
| 8 | +from metadata.models import LivelihoodStrategyType |
5 | 9 |
|
6 | 10 | from .models import ( |
7 | 11 | BaselineLivelihoodActivity, |
@@ -1466,3 +1470,175 @@ def get_strategy_label(self, obj): |
1466 | 1470 |
|
1467 | 1471 | def get_wealth_group_label(self, obj): |
1468 | 1472 | 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