diff --git a/api/drf_views.py b/api/drf_views.py index 541566c84..74a6f904c 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -262,16 +262,25 @@ def get_databank(self, request, pk): ) def get_country_figure(self, request, pk): country = self.get_object() - end_date = timezone.now() - start_date = end_date + timedelta(days=-2 * 365) - start_date = request.GET.get("start_date", start_date) - end_date = request.GET.get("end_date", end_date) - appeal_conditions = Q(atype=AppealType.APPEAL) - - all_appealhistory = AppealHistory.objects.select_related("appeal").filter(appeal__code__isnull=False) - all_appealhistory = all_appealhistory.filter(country=country) - if start_date and end_date: - all_appealhistory = all_appealhistory.filter(start_date__lte=end_date, end_date__gte=start_date) + + now = timezone.now() + start_date_from = request.GET.get("start_date_from", timezone.now() + timedelta(days=-2 * 365)) + start_date_to = request.GET.get("start_date_to", timezone.now()) + + appeal_conditions = Q(atype=AppealType.APPEAL) | Q(atype=AppealType.INTL) + + all_appealhistory = AppealHistory.objects.select_related("appeal").filter( + country=country, + appeal__code__isnull=False, + valid_from__lt=now, # TODO: Allow user to provide this? + valid_to__gt=now, # TODO: Allow user to provide this? + ) + if start_date_from and start_date_to: + all_appealhistory = all_appealhistory.filter( + start_date__gte=start_date_from, + start_date__lte=start_date_to, + ) + appeals_aggregated = all_appealhistory.annotate( appeal_with_dref=Count( Case( @@ -279,7 +288,7 @@ def get_country_figure(self, request, pk): output_field=IntegerField(), ) ), - appeal_without_dref=Count(Case(When(Q(atype=AppealType.APPEAL), then=1), output_field=IntegerField()), distinct=True), + appeal_without_dref=Count(Case(When(appeal_conditions, then=1), output_field=IntegerField())), total_population=( Case( When(appeal_conditions | Q(atype=AppealType.DREF), then=F("num_beneficiaries")), @@ -287,12 +296,24 @@ def get_country_figure(self, request, pk): ) ), amount_requested_all=( + Case( + When(appeal_conditions, then=F("amount_requested")), + output_field=IntegerField(), + ) + ), + amordref=( Case( When(appeal_conditions | Q(atype=AppealType.DREF), then=F("amount_requested")), output_field=IntegerField(), ) ), - amount_funded_all=( + amof=( + Case( + When(appeal_conditions, then=F("amount_funded")), + output_field=IntegerField(), + ) + ), + amofdref=( Case( When(appeal_conditions | Q(atype=AppealType.DREF), then=F("amount_funded")), output_field=IntegerField(), @@ -303,8 +324,10 @@ def get_country_figure(self, request, pk): active_drefs=Sum("appeal_with_dref"), active_appeals=Sum("appeal_without_dref"), target_population=Sum("total_population"), - total_amount_requested=Sum("amount_requested_all"), - total_amount_funded=Sum("amount_funded_all"), + amount_requested=Sum("amount_requested_all"), + amount_requested_dref_included=Sum("amordref"), + amount_funded=Sum("amof"), + amount_funded_dref_included=Sum("amofdref"), emergencies=Sum("emergencies_count"), ) return Response(CountryKeyFigureSerializer(appeals_aggregated).data) @@ -318,10 +341,9 @@ def get_country_figure(self, request, pk): @action(detail=True, url_path="disaster-count", pagination_class=None) def get_country_disaster_count(self, request, pk): country = self.get_object() - end_date = timezone.now() - start_date = end_date + timedelta(days=-2 * 365) - start_date = request.GET.get("start_date", start_date) - end_date = request.GET.get("end_date", end_date) + + start_date_from = request.GET.get("start_date_from", timezone.now() + timedelta(days=-2 * 365)) + start_date_to = request.GET.get("start_date_to", timezone.now()) queryset = ( Event.objects.filter( @@ -337,8 +359,12 @@ def get_country_disaster_count(self, request, pk): .order_by("countries", "dtype__name") ) - if start_date and end_date: - queryset = queryset.filter(disaster_start_date__gte=start_date, disaster_start_date__lte=end_date) + if start_date_from and start_date_to: + queryset = queryset.filter( + disaster_start_date__gte=start_date_from, + disaster_start_date__lte=start_date_to, + ) + return Response(CountryDisasterTypeCountSerializer(queryset, many=True).data) @extend_schema( @@ -349,16 +375,16 @@ def get_country_disaster_count(self, request, pk): @action(detail=True, url_path="disaster-monthly-count", pagination_class=None) def get_country_disaster_monthly_count(self, request, pk): country = self.get_object() - end_date = timezone.now() - start_date = end_date + timedelta(days=-2 * 365) - start_date = request.GET.get("start_date", start_date) - end_date = request.GET.get("end_date", end_date) + + start_date_from = request.GET.get("start_date_from", timezone.now() + timedelta(days=-2 * 365)) + start_date_to = request.GET.get("start_date_to", timezone.now()) + queryset = ( Event.objects.filter( countries__in=[country.id], dtype__isnull=False, ) - .annotate(date=TruncMonth("created_at")) + .annotate(date=TruncMonth("disaster_start_date")) .values("date", "countries", "dtype") .annotate( appeal_targeted_population=Coalesce( @@ -391,8 +417,11 @@ def get_country_disaster_monthly_count(self, request, pk): .order_by("date", "countries", "dtype__name") ) - if start_date and end_date: - queryset = queryset.filter(disaster_start_date__gte=start_date, disaster_start_date__lte=end_date) + if start_date_from and start_date_to: + queryset = queryset.filter( + disaster_start_date__gte=start_date_from, + disaster_start_date__lte=start_date_to, + ) return Response(CountryDisasterTypeMonthlySerializer(queryset, many=True).data) @@ -404,10 +433,10 @@ def get_country_disaster_monthly_count(self, request, pk): @action(detail=True, url_path="historical-disaster", pagination_class=None) def get_country_historical_disaster(self, request, pk): country = self.get_object() - end_date = timezone.now() - start_date = end_date + timedelta(days=-2 * 365) - start_date = request.GET.get("start_date", start_date) - end_date = request.GET.get("end_date", end_date) + + start_date_from = request.GET.get("start_date_from", timezone.now() + timedelta(days=-2 * 365)) + start_date_to = request.GET.get("start_date_to", timezone.now()) + dtype = request.GET.get("dtype", None) queryset = ( @@ -415,7 +444,7 @@ def get_country_historical_disaster(self, request, pk): countries__in=[country.id], dtype__isnull=False, ) - .annotate(date=TruncMonth("created_at")) + .annotate(date=TruncMonth("disaster_start_date")) .values("date", "dtype", "countries") .annotate( appeal_targeted_population=Coalesce( @@ -450,8 +479,11 @@ def get_country_historical_disaster(self, request, pk): .order_by("date", "countries", "dtype__name") ) - if start_date and end_date: - queryset = queryset.filter(disaster_start_date__gte=start_date, disaster_start_date__lte=end_date) + if start_date_from and start_date_to: + queryset = queryset.filter( + disaster_start_date__gte=start_date_from, + disaster_start_date__lte=start_date_to, + ) if dtype: queryset = queryset.filter(dtype=dtype) @@ -744,15 +776,14 @@ def get_serializer_class(self): class AppealViewset(mixins.ListModelMixin, viewsets.GenericViewSet): """Used to get Appeals from AppealHistory. Has no 'read' option, just 'list'.""" - # queryset = Appeal.objects.select_related('dtype', 'country', 'region').all() - # queryset = AppealHistory.objects.select_related('appeal__event', 'dtype', 'country', 'region').all() - queryset = AppealHistory.objects.select_related("appeal__event", "dtype", "country", "region").filter( - appeal__code__isnull=False - ) - # serializer_class = AppealSerializer + queryset = AppealHistory.objects.select_related( + "appeal__event", + "dtype", + "country", + "region", + ).filter(appeal__code__isnull=False) serializer_class = AppealHistorySerializer ordering_fields = "__all__" - # filterset_class = AppealFilter filterset_class = AppealHistoryFilter search_fields = ( "appeal__name", @@ -762,9 +793,7 @@ class AppealViewset(mixins.ListModelMixin, viewsets.GenericViewSet): def get_serializer_class(self): if is_tableau(self.request) is True: return AppealHistoryTableauSerializer - # return AppealTableauSerializer return AppealHistorySerializer - # return AppealSerializer def remove_unconfirmed_event(self, obj): if obj["needs_confirmation"]: @@ -776,9 +805,13 @@ def remove_unconfirmed_events(self, objs): # Overwrite to exclude the events which require confirmation def list(self, request, *args, **kwargs): - now = timezone.now() - date = request.GET.get("date", now) - queryset = self.filter_queryset(self.get_queryset()).filter(valid_from__lt=date, valid_to__gt=date) + date = request.GET.get("date", timezone.now()) + queryset = self.filter_queryset( + self.get_queryset().filter( + valid_from__lt=date, + valid_to__gt=date, + ) + ) page = self.paginate_queryset(queryset) if page is not None: diff --git a/api/filter_set.py b/api/filter_set.py index 81f5d70f9..397c8c2b1 100644 --- a/api/filter_set.py +++ b/api/filter_set.py @@ -204,14 +204,38 @@ class AppealFilter(filters.FilterSet): code = filters.CharFilter(field_name="code", lookup_expr="exact") status = filters.NumberFilter(field_name="status", lookup_expr="exact") id = filters.NumberFilter(field_name="id", lookup_expr="exact") + appeal_id = filters.NumberFilter( + field_name="appeal_id", lookup_expr="exact", help_text="Use this (or code) for appeal identification." + ) + district = filters.ModelMultipleChoiceFilter( + field_name="country__district", queryset=District.objects.all(), label="district", method="get_country_district" + ) + admin2 = filters.ModelMultipleChoiceFilter( + field_name="country__district__admin2", + queryset=Admin2.objects.all(), + label="admin2", + method="get_country_admin2", + ) class Meta: model = Appeal fields = { "start_date": ("exact", "gt", "gte", "lt", "lte"), "end_date": ("exact", "gt", "gte", "lt", "lte"), + "real_data_update": ("exact", "gt", "gte", "lt", "lte"), + "country__iso3": ("exact",), } + def get_country_district(self, qs, name, value): + if value: + return qs.filter(country__district=value).distinct() + return qs + + def get_country_admin2(self, qs, name, value): + if value: + return qs.filter(country__district__admin2=value).distinct() + return qs + class AppealHistoryFilter(filters.FilterSet): atype = filters.NumberFilter(field_name="atype", lookup_expr="exact") diff --git a/api/serializers.py b/api/serializers.py index 371f5370a..e950931ec 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2118,6 +2118,7 @@ class AggregateHeaderFiguresSerializer(serializers.Serializer): amount_requested = serializers.IntegerField() amount_requested_dref_included = serializers.IntegerField() amount_funded = serializers.IntegerField() + amount_funded_dref_included = serializers.IntegerField() # SearchPage Serializer @@ -2292,8 +2293,8 @@ class AggregateByDtypeSerializer(serializers.Serializer): class CountryKeyFigureInputSerializer(serializers.Serializer): - start_date = serializers.DateField(required=False) - end_date = serializers.DateField(required=False) + start_date_from = serializers.DateField(required=False) + start_date_to = serializers.DateField(required=False) dtype = serializers.IntegerField(required=False) @@ -2304,9 +2305,11 @@ class CountryKeyClimateInputSerializer(serializers.Serializer): class CountryKeyFigureSerializer(serializers.Serializer): active_drefs = serializers.IntegerField() active_appeals = serializers.IntegerField() + amount_requested = serializers.IntegerField() target_population = serializers.IntegerField() - total_amount_requested = serializers.IntegerField() - total_amount_funded = serializers.IntegerField() + amount_requested_dref_included = serializers.IntegerField() + amount_funded = serializers.IntegerField() + amount_funded_dref_included = serializers.IntegerField() emergencies = serializers.IntegerField() diff --git a/api/test_views.py b/api/test_views.py index 9d0a2227d..a4bf987ee 100644 --- a/api/test_views.py +++ b/api/test_views.py @@ -5,7 +5,6 @@ import api.models as models from api.factories.event import ( AppealFactory, - AppealHistoryFactory, AppealType, EventFactory, EventFeaturedDocumentFactory, @@ -543,58 +542,53 @@ def test_appeal_key_figure(self): ) event2 = EventFactory.create(name="test0", dtype=dtype1, num_affected=10000, countries=[country1]) event3 = EventFactory.create(name="test2", dtype=dtype2, num_affected=99999, countries=[country2]) - appeal1 = AppealFactory.create( - event=event1, dtype=dtype1, num_beneficiaries=9000, amount_requested=10000, amount_funded=1899999, code=12 - ) - appeal2 = AppealFactory.create( - event=event2, dtype=dtype2, num_beneficiaries=90023, amount_requested=100440, amount_funded=12299999, code=123 - ) - appeal3 = AppealFactory.create( - event=event3, dtype=dtype2, num_beneficiaries=91000, amount_requested=10000888, amount_funded=678888, code=1234 - ) - AppealHistoryFactory.create( - appeal=appeal1, + AppealFactory.create( + event=event1, dtype=dtype1, num_beneficiaries=9000, amount_requested=10000, amount_funded=1899999, - country=country1, - atype=AppealType.APPEAL, + code=12, start_date="2024-1-1", end_date="2024-1-1", + atype=AppealType.APPEAL, + country=country1, ) - AppealHistoryFactory.create( - appeal=appeal2, + AppealFactory.create( + event=event2, dtype=dtype2, - num_beneficiaries=1, - amount_requested=1, - amount_funded=1, - country=country1, - atype=AppealType.DREF, + num_beneficiaries=90023, + amount_requested=100440, + amount_funded=12299999, + code=123, start_date="2024-2-2", end_date="2024-2-2", + atype=AppealType.DREF, + country=country1, ) - AppealHistoryFactory.create( - appeal=appeal3, + AppealFactory.create( + event=event3, dtype=dtype2, - num_beneficiaries=1, - amount_requested=1, - amount_funded=1, - country=country1, - atype=AppealType.APPEAL, + num_beneficiaries=91000, + amount_requested=10000888, + amount_funded=678888, + code=1234, start_date="2024-3-3", end_date="2024-3-3", + atype=AppealType.APPEAL, + country=country1, ) - AppealHistoryFactory.create( - appeal=appeal3, + AppealFactory.create( + event=event3, dtype=dtype2, - num_beneficiaries=1, - amount_requested=1, - amount_funded=1, - country=country1, - atype=AppealType.APPEAL, + num_beneficiaries=91000, + amount_requested=10000888, + amount_funded=678888, + code=12345, start_date="2024-4-4", end_date="2024-4-4", + atype=AppealType.APPEAL, + country=country1, ) url = f"/api/v2/country/{country1.id}/figure/" self.client.force_authenticate(self.user) diff --git a/api/views.py b/api/views.py index 6ea078360..25bda58d6 100644 --- a/api/views.py +++ b/api/views.py @@ -609,7 +609,7 @@ def get(self, request): now = timezone.now() date = request.GET.get("date", now) appeal_conditions = ( - (Q(atype=AppealType.APPEAL) | Q(atype=AppealType.INTL)) & Q(end_date__gt=date) & Q(start_date__lt=date) + (Q(atype=AppealType.APPEAL) | Q(atype=AppealType.INTL)) & Q(end_date__gte=date) & Q(start_date__lte=date) ) all_appealhistory = AppealHistory.objects.select_related("appeal").filter( @@ -622,12 +622,11 @@ def get(self, request): all_appealhistory = all_appealhistory.filter(country__id=country) if region: all_appealhistory = all_appealhistory.filter(country__region__id=region) - appeals_aggregated = all_appealhistory.annotate( # Active Appeals with DREF type actd=Count( Case( - When(Q(atype=AppealType.DREF) & Q(end_date__gt=date) & Q(start_date__lt=date), then=1), + When(Q(atype=AppealType.DREF) & Q(end_date__gte=date) & Q(start_date__lte=date), then=1), output_field=IntegerField(), ) ), @@ -638,17 +637,20 @@ def get(self, request): # Active Appeals' target population tarp=Sum( Case( - When(Q(end_date__gt=date) & Q(start_date__lt=date), then=F("num_beneficiaries")), + When(Q(end_date__gte=date) & Q(start_date__lte=date), then=F("num_beneficiaries")), output_field=IntegerField(), ) ), # Active Appeals' requested amount, which are not DREF amor=Case(When(appeal_conditions, then=F("amount_requested")), output_field=IntegerField()), amordref=Case( - When(Q(end_date__gt=date) & Q(start_date__lt=date), then=F("amount_requested")), output_field=IntegerField() + When(Q(end_date__gte=date) & Q(start_date__lte=date), then=F("amount_requested")), output_field=IntegerField() ), # Active Appeals' funded amount, which are not DREF amof=Case(When(appeal_conditions, then=F("amount_funded")), output_field=IntegerField()), + amofdref=Case( + When(Q(end_date__gte=date) & Q(start_date__lte=date), then=F("amount_funded")), output_field=IntegerField() + ), ).aggregate( active_drefs=Sum("actd"), active_appeals=Sum("acta"), @@ -657,6 +659,7 @@ def get(self, request): amount_requested=Sum("amor"), amount_requested_dref_included=Sum("amordref"), amount_funded=Sum("amof"), + amount_funded_dref_included=Sum("amofdref"), ) return Response(AggregateHeaderFiguresSerializer(appeals_aggregated).data)