From 89231d1d6437203fce797828c24a3601e09e73a1 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Wed, 22 Apr 2026 22:03:31 +0545 Subject: [PATCH 1/4] feat(event): make event source enum - Add Enum for event sources. - Add source field in place of auto generated source field on event model. - Migrate existing event records to enum type. - Update related serializers and tests. --- api/admin.py | 45 ++------------ api/drf_views.py | 34 ++++++++++- api/enums.py | 1 + api/factories/event.py | 2 +- api/filter_set.py | 7 +-- api/management/commands/create_events.py | 3 +- api/management/commands/index_and_notify.py | 2 +- api/management/commands/ingest_gdacs.py | 3 +- api/management/commands/ingest_mdb.py | 3 +- api/management/commands/ingest_who.py | 4 +- ...vent_auto_generated_source_event_source.py | 58 +++++++++++++++++++ api/models.py | 30 +++++++++- api/serializers.py | 5 +- deployments/tests.py | 2 + 14 files changed, 137 insertions(+), 62 deletions(-) create mode 100644 api/migrations/0231_remove_event_auto_generated_source_event_source.py diff --git a/api/admin.py b/api/admin.py index defad87f3..6b5fc585c 100644 --- a/api/admin.py +++ b/api/admin.py @@ -26,7 +26,6 @@ import api.models as models from api.admin_classes import RegionRestrictedAdmin -from api.event_sources import SOURCES from api.management.commands.index_and_notify import Command as Notify from lang.admin import TranslationAdmin, TranslationInlineModelAdmin from notifications.models import RecordType, SubscriptionType @@ -158,38 +157,6 @@ def queryset(self, request, queryset): return queryset.filter(is_featured=False) -class EventSourceFilter(admin.SimpleListFilter): - title = _("source") - parameter_name = "event_source" - - def lookups(self, request, model_admin): - return ( - ("input", _("Manual input")), - ("gdacs", _("GDACs scraper")), - ("who", _("WHO scraper")), - ("report_ingest", _("Field report ingest")), - ("report_admin", _("Field report admin")), - ("appeal_admin", _("Appeals admin")), - ("unknown", _("Unknown automated")), - ) - - def queryset(self, request, queryset): - if self.value() == "input": - return queryset.filter(auto_generated=False) - elif self.value() == "gdacs": - return queryset.filter(auto_generated_source=SOURCES["gdacs"]) - elif self.value() == "who": - return queryset.filter(auto_generated_source__startswith="www.who.int") - elif self.value() == "report_ingest": - return queryset.filter(auto_generated_source=SOURCES["report_ingest"]) - elif self.value() == "report_admin": - return queryset.filter(auto_generated_source=SOURCES["report_admin"]) - elif self.value() == "appeal_admin": - return queryset.filter(auto_generated_source=SOURCES["appeal_admin"]) - elif self.value() == "unknown": - return queryset.filter(auto_generated=True).filter(auto_generated_source__isnull=True) - - class DisasterTypeAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdmin): search_fields = ("name",) @@ -246,9 +213,9 @@ def level_updated_at(self, obj): "cc_status", "glide", "auto_generated", - "auto_generated_source", + "source", ) - list_filter = [IsFeaturedFilter, EventSourceFilter] + list_filter = [IsFeaturedFilter, "source"] actions = ["create_field_reports"] search_fields = ( "name", @@ -369,7 +336,7 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No self.readonly_fields = ( "appeals", "field_reports", - "auto_generated_source", + "source", "parent_event", "created_at", "updated_at", @@ -378,7 +345,7 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No self.readonly_fields = ( "appeals", "field_reports", - "auto_generated_source", + "source", "created_at", "updated_at", ) @@ -651,7 +618,7 @@ def create_events(self, request, queryset): dtype=getattr(report, "dtype"), disaster_start_date=getattr(report, "created_at"), auto_generated=True, - auto_generated_source=SOURCES["report_admin"], + source=models.Event.EventSource.REPORT_ADMIN, ) if getattr(report, "countries").exists(): for country in report.countries.all(): @@ -760,7 +727,7 @@ def create_events(self, request, queryset): dtype=getattr(appeal, "dtype"), disaster_start_date=getattr(appeal, "start_date"), auto_generated=True, - auto_generated_source=SOURCES["appeal_admin"], + source=models.Event.EventSource.APPEAL_ADMIN, ) if appeal.country is not None: event.countries.add(appeal.country) diff --git a/api/drf_views.py b/api/drf_views.py index 34b7fe918..75479fbb0 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -751,7 +751,7 @@ def get_queryset(self, *args, **kwargs): if self.action == "response_activity_events": return ( qset.filter(parent_event__isnull=True) - .filter(Q(auto_generated=False) | Q(auto_generated_source="New field report")) + .filter(Q(auto_generated=False) | Q(source=Event.EventSource.NEW_REPORT)) .select_related("dtype") ) return ( @@ -1344,7 +1344,7 @@ class SupportedActivityViewset(viewsets.ReadOnlyModelViewSet): # summary=report.description or "", # disaster_start_date=report.start_date, # auto_generated=True, -# auto_generated_source=SOURCES["new_report"], +# source=Event.EventSource.NEW_REPORT, # visibility=report.visibility, # **{TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME: django_get_language()}, # ) @@ -1542,3 +1542,33 @@ class CountrySupportingPartnerViewSet(viewsets.ModelViewSet): def get_queryset(self): return CountrySupportingPartner.objects.select_related("country") + + +class EmergencyViewset(viewsets.ReadOnlyModelViewSet): + queryset = Event.objects.all() + lookup_field = "id" + serializer_class = DetailEmergencySerializer + filterset_class = EventFilter + + def get_queryset(self): + return ( + super() + .get_queryset() + .select_related( + "dtype", + "parent_event", + ) + .prefetch_related( + "regions", + "countries", + "countries_for_preview", + Prefetch("key_figures", queryset=KeyFigure.objects.all()), + Prefetch("contacts", queryset=EventContact.objects.all()), + ) + .annotate( + latest_field_report_id=Subquery( + FieldReport.objects.filter(event=OuterRef("pk")).order_by("-created_at").values("id")[:1] + ), + appeal_id=Subquery(Appeal.objects.filter(event=OuterRef("pk")).order_by("-created_at").values("id")[:1]), + ) + ) diff --git a/api/enums.py b/api/enums.py index 83e81850e..115855b4d 100644 --- a/api/enums.py +++ b/api/enums.py @@ -20,4 +20,5 @@ "action_category": models.ActionCategory, "profile_org_types": models.Profile.OrgTypes, "supporting_type": models.CountrySupportingPartner.SupportingPartnerType, + "event_source": models.Event.EventSource, } diff --git a/api/factories/event.py b/api/factories/event.py index f3656a2e6..465433b0d 100644 --- a/api/factories/event.py +++ b/api/factories/event.py @@ -72,7 +72,7 @@ def regions(self, create, extracted, **kwargs): previous_update = fuzzy.FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=pytz.utc)) auto_generated = fuzzy.FuzzyChoice([True, False]) - auto_generated_source = fuzzy.FuzzyText(length=50) + source = fuzzy.FuzzyChoice(Event.EventSource.values) is_featured = fuzzy.FuzzyChoice([True, False]) is_featured_region = fuzzy.FuzzyChoice([True, False]) diff --git a/api/filter_set.py b/api/filter_set.py index 05b696afb..a9a98c7b0 100644 --- a/api/filter_set.py +++ b/api/filter_set.py @@ -2,7 +2,6 @@ from django.contrib.auth.models import User from django.db import models -from api.event_sources import SOURCES from api.models import ( Admin2, Appeal, @@ -182,10 +181,8 @@ class EventFilter(filters.FilterSet): countries__in = ListFilter(field_name="countries__id") regions__in = ListFilter(field_name="regions__id") id = filters.NumberFilter(field_name="id", lookup_expr="exact") - auto_generated_source = filters.ChoiceFilter( - label="Auto generated source choices", - choices=[(v, v) for v in SOURCES.values()], - ) + source = filters.ChoiceFilter(choices=Event.EventSource.choices, lookup_expr="exact") + is_subscribed = filters.BooleanFilter(label="is_subscribed", method="get_is_subcribed_event") class Meta: diff --git a/api/management/commands/create_events.py b/api/management/commands/create_events.py index 7078b772f..df6bef81d 100644 --- a/api/management/commands/create_events.py +++ b/api/management/commands/create_events.py @@ -1,6 +1,5 @@ from django.core.management.base import BaseCommand -from api.event_sources import SOURCES from api.models import Appeal, Event @@ -18,7 +17,7 @@ def handle(self, *args, **options): "dtype": appeal.dtype, "disaster_start_date": appeal.start_date, "auto_generated": True, - "auto_generated_source": SOURCES["appeal_admin"], + "source": Event.EventSource.APPEAL_ADMIN, } event = Event.objects.create(**fields) if appeal.country is not None: diff --git a/api/management/commands/index_and_notify.py b/api/management/commands/index_and_notify.py index 1635077db..434964774 100644 --- a/api/management/commands/index_and_notify.py +++ b/api/management/commands/index_and_notify.py @@ -1035,7 +1035,7 @@ def handle(self, *args, **options): condR = Q(real_data_update__gte=time_diff) # instead of modified at cond2 = ~Q(previous_update__gte=time_diff_1_day) # negate (~) no previous_update in the last day, so send once a day condF = Q( - auto_generated_source="New field report" + source=Event.EventSource.NEW_REPORT ) # exclude those events that were generated from field reports, to avoid 2x notif. condE = Q(status=CronJobStatus.ERRONEOUS) diff --git a/api/management/commands/ingest_gdacs.py b/api/management/commands/ingest_gdacs.py index c356d1cfc..5a99466d2 100644 --- a/api/management/commands/ingest_gdacs.py +++ b/api/management/commands/ingest_gdacs.py @@ -3,7 +3,6 @@ from dateutil.parser import parse from django.core.management.base import BaseCommand -from api.event_sources import SOURCES from api.logger import logger from api.models import Country, CronJob, CronJobStatus, DisasterType, Event, GDACSEvent @@ -104,7 +103,7 @@ def handle(self, *args, **options): "summary": data["description"], "disaster_start_date": data["publication_date"], "auto_generated": True, - "auto_generated_source": SOURCES["gdacs"], + "source": Event.EventSource.GDACS, "ifrc_severity_level": data["alert_level"], } event = Event.objects.create(**fields) diff --git a/api/management/commands/ingest_mdb.py b/api/management/commands/ingest_mdb.py index c153ef882..6cfa7c57b 100644 --- a/api/management/commands/ingest_mdb.py +++ b/api/management/commands/ingest_mdb.py @@ -13,7 +13,6 @@ from django.core.management.base import BaseCommand from django.utils import timezone -from api.event_sources import SOURCES from api.fixtures.dtype_map import PK_MAP from api.logger import logger from api.models import ( @@ -249,7 +248,7 @@ def handle(self, *args, **options): "dtype": report_dtype, "disaster_start_date": datetime.utcnow().replace(tzinfo=timezone.utc), "auto_generated": True, - "auto_generated_source": SOURCES["report_ingest"], + "source": Event.EventSource.REPORT_INGEST, } event = Event(**event_record) event.save() diff --git a/api/management/commands/ingest_who.py b/api/management/commands/ingest_who.py index bdc0704c8..b04aa6e01 100644 --- a/api/management/commands/ingest_who.py +++ b/api/management/commands/ingest_who.py @@ -11,7 +11,7 @@ class Command(BaseCommand): help = "Add new event (=emergency) entries from WHO API" def handle(self, *args, **options): - guids = [e.auto_generated_source for e in Event.objects.filter(auto_generated_source__startswith="www.who.int")] + guids = [e.source for e in Event.objects.filter(source=Event.EventSource.WHO)] logger.info("Querying WHO RSS feed for new emergency data") # get latest @@ -131,7 +131,7 @@ def handle(self, *args, **options): "summary": summary, "disaster_start_date": date, "auto_generated": True, - "auto_generated_source": data["guid"], + "source": Event.EventSource.WHO, "ifrc_severity_level": alert_level, } # TODO: fields['name'] sometimes exceeds 100 maxlength, so will need some altering if this will be used diff --git a/api/migrations/0231_remove_event_auto_generated_source_event_source.py b/api/migrations/0231_remove_event_auto_generated_source_event_source.py new file mode 100644 index 000000000..da4c95c34 --- /dev/null +++ b/api/migrations/0231_remove_event_auto_generated_source_event_source.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.29 on 2026-04-29 05:48 + +from django.db import migrations, models + + +def migrate_sources(apps, schema_editor): + """ + Populate the source field for Event records: + - Non auto generated events are mapped to MANUAL_INPUT. + - Events that are auto generated and have auto_generated_source are mapped to the corresponding enum value. + - 0 is temporary placeholder for unmapped/unknown auto-generated sources that will be reclassified into proper EventSource via `migrate_event_source` command. + """ + + Model = apps.get_model("api", "Event") + for obj in Model.objects.iterator(): + if not obj.auto_generated: + value = 100 + + else: + src = (obj.auto_generated_source or "").lower() + if "gdacs" in src: + value = 110 + elif "who.int" in src: + value = 120 + elif "field report dmis ingest" in src: + value = 130 + elif "field report admin" in src: + value = 140 + elif "appeal" in src: + value = 150 + elif "new field report" in src: + value = 160 + else: + value = 0 + obj.source = value + obj.save(update_fields=["source"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0230_alter_districtgeoms_district'), + ] + + operations = [ + + migrations.AddField( + model_name='event', + name='source', + field=models.IntegerField(choices=[(100, 'Manual input'), (110, 'GDACs scraper'), (120, 'WHO scraper'), (130, 'Field report DMIS ingest'), (140, 'Field report admin'), (150, 'Appeal admin'), (160, 'New field report'), (170, 'DREF')], default=100, verbose_name='Event source'), + ), + migrations.RunPython(migrate_sources,reverse_code=migrations.RunPython.noop), + + migrations.RemoveField( + model_name='event', + name='auto_generated_source', + ), + ] diff --git a/api/models.py b/api/models.py index 93292aa20..26663ae79 100644 --- a/api/models.py +++ b/api/models.py @@ -767,6 +767,32 @@ def snippet_image_path(instance, filename): class Event(models.Model): """A disaster, which could cover multiple countries""" + class EventSource(models.IntegerChoices): + + Manual_Input = 100, _("Manual input") + """MANUAL_INPUT: Event data manually entered by a user through the event administration interface.""" + + GDACS = 110, _("GDACs scraper") + """GDACS: Event data automatically ingested from the GDACS scraper.""" + + WHO = 120, _("WHO scraper") + """WHO: Event data automatically ingested from the (WHO) scraper.""" + + REPORT_INGEST = 130, _("Field report DMIS ingest") + """REPORT_INGEST: Event data imported through the DMIS field report.""" + + REPORT_ADMIN = 140, _("Field report admin") + """REPORT_ADMIN: Event data created or modified via the field report administration interface.""" + + APPEAL_ADMIN = 150, _("Appeal admin") + """APPEAL_ADMIN: Event data created or managed through the appeal administration interface.""" + + NEW_REPORT = 160, _("New field report") + """NEW_REPORT: Event data originating from newly created field reports.""" + + DREF = 170, _("DREF") + """DREF: Event originating records.""" + name = models.CharField(verbose_name=_("name"), max_length=256) dtype = models.ForeignKey(DisasterType, verbose_name=_("disaster type"), null=True, on_delete=models.SET_NULL) disaster_start_date = models.DateTimeField(verbose_name=_("disaster start date")) @@ -825,9 +851,7 @@ class Event(models.Model): previous_update = models.DateTimeField(verbose_name=_("previous update"), null=True, blank=True) auto_generated = models.BooleanField(verbose_name=_("auto generated"), default=False, editable=False) - auto_generated_source = models.CharField( - verbose_name=_("auto generated source"), max_length=50, null=True, blank=True, editable=False - ) + source = models.IntegerField(choices=EventSource.choices, default=EventSource.Manual_Input, verbose_name=_("Event source")) # Meant to give the organization a way of highlighting certain, important events. is_featured = models.BooleanField(default=False, verbose_name=_("is featured on home page")) diff --git a/api/serializers.py b/api/serializers.py index 8a6b08e91..e67834527 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -26,7 +26,6 @@ from per.models import Overview from utils.file_check import validate_file_type -from .event_sources import SOURCES from .models import ( Action, ActionsTaken, @@ -1065,7 +1064,7 @@ class Meta: "name", "slug", "dtype", - "auto_generated_source", + "source", "emergency_response_contact_email", "countries_for_preview", ) @@ -2197,7 +2196,7 @@ def create_event(self, report): summary=report.description or "", disaster_start_date=report.start_date, auto_generated=True, - auto_generated_source=SOURCES["new_report"], + source=Event.EventSources.NEW_REPORT, visibility=report.visibility, **{TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME: django_get_language()}, ) diff --git a/deployments/tests.py b/deployments/tests.py index 667bf8dc5..a341db85b 100644 --- a/deployments/tests.py +++ b/deployments/tests.py @@ -266,6 +266,8 @@ def test_project_update(self): new_country = country.CountryFactory( name="country-2", society_name="society-name-2", + iso="Te", + iso3="frj", ) new_district = district.DistrictFactory( country=new_country, From a40688d7938f65e60f565e5b388fd0ac5463fad2 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Fri, 24 Apr 2026 15:41:03 +0545 Subject: [PATCH 2/4] feat(api): add new api endpoint for emergency - Add new emergency api for. - Attach latest field report. - Add test case for emergency endpoint. --- api/drf_views.py | 21 +++++++-- api/factories/event.py | 7 +++ api/serializers.py | 52 +++++++++++++++++++++- api/test_views.py | 98 +++++++++++++++++++++++++++++++++++++++++- assets | 2 +- main/urls.py | 3 ++ 6 files changed, 177 insertions(+), 6 deletions(-) diff --git a/api/drf_views.py b/api/drf_views.py index 75479fbb0..8fe5823f1 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -89,6 +89,7 @@ Export, ExternalPartner, FieldReport, + KeyFigure, MainContact, Profile, Region, @@ -122,6 +123,7 @@ CountrySupportingPartnerSerializer, CountryTableauSerializer, DeploymentsByEventSerializer, + DetailEmergencySerializer, DetailEventSerializer, DisasterTypeSerializer, DistrictSerializer, @@ -1544,11 +1546,12 @@ def get_queryset(self): return CountrySupportingPartner.objects.select_related("country") -class EmergencyViewset(viewsets.ReadOnlyModelViewSet): +class EmergencyViewset(ReadOnlyVisibilityViewset): queryset = Event.objects.all() lookup_field = "id" serializer_class = DetailEmergencySerializer filterset_class = EventFilter + visibility_model_class = Event def get_queryset(self): return ( @@ -1566,9 +1569,21 @@ def get_queryset(self): Prefetch("contacts", queryset=EventContact.objects.all()), ) .annotate( + first_field_report_id=Subquery( + FieldReport.objects.filter(event=OuterRef("pk")) + .order_by( + "fr_num", + "updated_at", + ) + .values("id")[:1] + ), latest_field_report_id=Subquery( - FieldReport.objects.filter(event=OuterRef("pk")).order_by("-created_at").values("id")[:1] + FieldReport.objects.filter(event=OuterRef("pk")) + .order_by( + "-fr_num", + "-updated_at", + ) + .values("id")[:1] ), - appeal_id=Subquery(Appeal.objects.filter(event=OuterRef("pk")).order_by("-created_at").values("id")[:1]), ) ) diff --git a/api/factories/event.py b/api/factories/event.py index 465433b0d..78588a825 100644 --- a/api/factories/event.py +++ b/api/factories/event.py @@ -12,6 +12,7 @@ AppealHistory, AppealType, Event, + EventContact, EventFeaturedDocument, EventLink, ) @@ -133,3 +134,9 @@ class AppealHistoryFactory(factory.django.DjangoModelFactory): class Meta: model = AppealHistory + + +class EventContactFactory(factory.django.DjangoModelFactory): + + class Meta: + model = EventContact diff --git a/api/serializers.py b/api/serializers.py index e67834527..2e649a10e 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2196,7 +2196,7 @@ def create_event(self, report): summary=report.description or "", disaster_start_date=report.start_date, auto_generated=True, - source=Event.EventSources.NEW_REPORT, + source=Event.EventSource.NEW_REPORT, visibility=report.visibility, **{TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME: django_get_language()}, ) @@ -2601,3 +2601,53 @@ class CountrySupportingPartnerSerializer(serializers.ModelSerializer): class Meta: model = CountrySupportingPartner fields = "__all__" + + +class DetailEmergencySerializer(serializers.ModelSerializer): + contacts = EventContactSerializer(many=True, read_only=True) + key_figures = KeyFigureSerializer(many=True, read_only=True) + countries = MiniCountrySerializer(many=True, read_only=True) + ifrc_severity_level_display = serializers.CharField(source="get_ifrc_severity_level_display", read_only=True) + visibility_display = serializers.CharField(source="get_visibility_display", read_only=True) + source_display = serializers.CharField(source="get_source_display", read_only=True) + # NOTE: Populated from Queryset using Annotate + first_field_report_id = serializers.IntegerField(read_only=True) + latest_field_report_id = serializers.IntegerField(read_only=True) + + class Meta: + model = Event + fields = ( + "name", + "dtype", + "countries", + "summary", + "disaster_start_date", + "auto_generated", + "source", + "source_display", + "key_figures", + "is_featured", + "is_featured_region", + "hide_attached_field_reports", + "hide_field_report_map", + "id", + "slug", + "ifrc_severity_level", + "ifrc_severity_level_display", + "ifrc_severity_level_update_date", + "parent_event", + "emergency_response_contact_email", + "visibility", + "visibility_display", + "contacts", + "num_injured", + "num_dead", + "num_missing", + "num_affected", + "num_displaced", + "created_at", + "updated_at", + "previous_update", + "first_field_report_id", + "latest_field_report_id", + ) diff --git a/api/test_views.py b/api/test_views.py index 9d695410b..c0efad0ef 100644 --- a/api/test_views.py +++ b/api/test_views.py @@ -1,6 +1,6 @@ import re import uuid -from datetime import timedelta +from datetime import datetime, timedelta from unittest.mock import patch from django.contrib.auth.models import User @@ -10,9 +10,11 @@ from django.utils import timezone import api.models as models +from api.factories.disaster_type import DisasterTypeFactory from api.factories.event import ( AppealFactory, AppealType, + EventContactFactory, EventFactory, EventFeaturedDocumentFactory, EventLinkFactory, @@ -1042,3 +1044,97 @@ def test_ifrc_user_sees_all(self): ] ), ) + + +class EmergencyViewTestCase(APITestCase): + def setUp(self): + super().setUp() + self.disaster_type = DisasterTypeFactory.create( + name="Flood", + ) + + self.event1 = EventFactory.create( + dtype=self.disaster_type, + source=models.Event.EventSource.GDACS, + slug="test1", + parent_event=None, + ) + self.contact = EventContactFactory.create( + event=self.event1, + ) + + self.field_report1 = FieldReportFactory.create( + event=self.event1, + created_at=timezone.make_aware(datetime(2024, 1, 1)), + updated_at=timezone.make_aware(datetime(2026, 1, 1)), + fr_num=50, + ) + + self.field_report2 = FieldReportFactory.create( + event=self.event1, + created_at=timezone.make_aware(datetime(2024, 1, 1)), + updated_at=timezone.make_aware(datetime(2025, 1, 1)), + fr_num=20, + ) + + self.event2 = EventFactory.create( + dtype=self.disaster_type, + source=models.Event.EventSource.WHO, + slug="test2", + parent_event=None, + ) + + self.event3 = EventFactory.create( + dtype=self.disaster_type, + source=models.Event.EventSource.APPEAL_ADMIN, + slug="test3", + parent_event=None, + ) + self.appeal2 = AppealFactory.create( + event=self.event3, + dtype=self.disaster_type, + num_beneficiaries=9000, + amount_requested=10000, + amount_funded=1899999, + ) + + self.url = "/api/v2/emergency/" + + def test_get_emergency_list(self): + response = self.client.get(self.url) + self.assert_200(response) + self.assertEqual(response.data["count"], 2) + + def test_retrive_emergency_detail(self): + url = f"/api/v2/emergency/{self.event1.id}/" + response = self.client.get(url) + self.assert_200(response) + self.assertEqual(response.data["id"], self.event1.id) + self.assertEqual(response.data["slug"], self.event1.slug) + self.assertEqual(response.data["name"], self.event1.name) + self.assertEqual(response.data["source"], models.Event.EventSource.GDACS) + + # first field report id check + self.assertEqual(response.data["first_field_report_id"], self.field_report2.id) + # latest check field report + self.assertEqual(response.data["latest_field_report_id"], self.field_report1.id) + + # Filter Tests + def test_filter_by_source(self): + url = f"{self.url}?source=120" + response = self.client.get(url) + self.assert_200(response) + self.assertEqual(response.data["count"], 1) + self.assertEqual(response.data["results"][0]["source"], models.Event.EventSource.WHO) + + def test_filter_by_appeal_source(self): + url = f"{self.url}?source=150" + response = self.client.get(url) + self.assert_200(response) + self.assertEqual(response.data["count"], 1) + self.assertEqual(response.data["results"][0]["source"], models.Event.EventSource.APPEAL_ADMIN) + + def test_filter_by_source_no_match(self): + url = f"{self.url}?source=500" + response = self.client.get(url) + self.assert_400(response) diff --git a/assets b/assets index ab85eb4e7..de7d052a5 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ab85eb4e797b854f141ee18a02559ed82ec91f93 +Subproject commit de7d052a5fb75baa7a410e5a8cece5e52f043835 diff --git a/main/urls.py b/main/urls.py index d9d6cc676..d9209e53d 100644 --- a/main/urls.py +++ b/main/urls.py @@ -88,6 +88,9 @@ router.register(r"admin2", api_views.Admin2Viewset, basename="admin2") router.register(r"district", api_views.DistrictViewset, basename="district") router.register(r"district_rmd", api_views.DistrictRMDViewset, basename="district_rmd") + +router.register(r"emergency", api_views.EmergencyViewset, basename="emergency") + router.register(r"domainwhitelist", registration_views.DomainWhitelistViewset) router.register(r"eru", deployment_views.ERUViewset, basename="eru") router.register( From d1ede0646c18bff802d7b87db201979e766e877c Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Tue, 28 Apr 2026 10:17:40 +0545 Subject: [PATCH 3/4] feat(api): add related appeal id on emergency api endpoint --- api/drf_views.py | 1 + api/serializers.py | 2 ++ api/test_views.py | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/drf_views.py b/api/drf_views.py index 8fe5823f1..41d3221a6 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -1585,5 +1585,6 @@ def get_queryset(self): ) .values("id")[:1] ), + appeal_id=Subquery(Appeal.objects.filter(event=OuterRef("pk")).order_by("-created_at").values("id")[:1]), ) ) diff --git a/api/serializers.py b/api/serializers.py index 2e649a10e..3eae63645 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2613,6 +2613,7 @@ class DetailEmergencySerializer(serializers.ModelSerializer): # NOTE: Populated from Queryset using Annotate first_field_report_id = serializers.IntegerField(read_only=True) latest_field_report_id = serializers.IntegerField(read_only=True) + appeal_id = serializers.IntegerField(read_only=True) class Meta: model = Event @@ -2650,4 +2651,5 @@ class Meta: "previous_update", "first_field_report_id", "latest_field_report_id", + "appeal_id", ) diff --git a/api/test_views.py b/api/test_views.py index c0efad0ef..47966e966 100644 --- a/api/test_views.py +++ b/api/test_views.py @@ -1103,7 +1103,7 @@ def setUp(self): def test_get_emergency_list(self): response = self.client.get(self.url) self.assert_200(response) - self.assertEqual(response.data["count"], 2) + self.assertEqual(response.data["count"], 3) def test_retrive_emergency_detail(self): url = f"/api/v2/emergency/{self.event1.id}/" @@ -1120,7 +1120,7 @@ def test_retrive_emergency_detail(self): self.assertEqual(response.data["latest_field_report_id"], self.field_report1.id) # Filter Tests - def test_filter_by_source(self): + def test_filter_by_who_source(self): url = f"{self.url}?source=120" response = self.client.get(url) self.assert_200(response) From efb86d85bcee4d4209f77f9de91c0bdee10a9df9 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Wed, 29 Apr 2026 16:59:21 +0545 Subject: [PATCH 4/4] feat(api): add script to update event source --- .../commands/migrate_event_source.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 api/management/commands/migrate_event_source.py diff --git a/api/management/commands/migrate_event_source.py b/api/management/commands/migrate_event_source.py new file mode 100644 index 000000000..25294e698 --- /dev/null +++ b/api/management/commands/migrate_event_source.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand + +from api.models import Event + + +class Command(BaseCommand): + help = "Update event sources based on name prefix and auto-generated status" + + def handle(self, *args, **options): + queryset = Event.objects.filter(auto_generated=True, source=0) + self.stdout.write( + self.style.NOTICE( + f"{queryset.count()} events will be assigned a source (GDACS or Manual Input) based on name prefix." + ) + ) + to_update = [] + for event in queryset.iterator(): + if event.name.startswith("GDACS"): + event.source = Event.EventSource.GDACS + else: + event.source = Event.EventSource.Manual_Input + event.auto_generated = False + to_update.append(event) + + Event.objects.bulk_update(to_update, ["source", "auto_generated"]) + self.stdout.write(self.style.SUCCESS("Event sources have been updated successfully."))