From b8c42a376a7a4e66ec8081251c3cc9c056fcbed3 Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Mon, 16 Mar 2026 14:46:51 +0545 Subject: [PATCH 1/2] feat(eap): add new fields and remove status activated - add new fields lead_timeframe_unit - change pending pfa to approved(pending pfa) --- assets | 2 +- ...e_eapregistration_activated_at_and_more.py | 33 +++++++++++++++++++ eap/models.py | 21 +++++------- eap/serializers.py | 33 +++++++++++-------- eap/test_views.py | 33 ++----------------- eap/views.py | 2 +- 6 files changed, 66 insertions(+), 58 deletions(-) create mode 100644 eap/migrations/0007_remove_eapregistration_activated_at_and_more.py diff --git a/assets b/assets index d685cc197..ddeb2616e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d685cc19773c32242fc67787fafc8043172f1308 +Subproject commit ddeb2616ec1fa1c30834217e6b2a3b69b3ee0227 diff --git a/eap/migrations/0007_remove_eapregistration_activated_at_and_more.py b/eap/migrations/0007_remove_eapregistration_activated_at_and_more.py new file mode 100644 index 000000000..56ca12787 --- /dev/null +++ b/eap/migrations/0007_remove_eapregistration_activated_at_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.29 on 2026-03-16 09:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('eap', '0006_fulleap_meal_source_of_information_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='eapregistration', + name='activated_at', + ), + migrations.AddField( + model_name='fulleap', + name='lead_timeframe_unit', + field=models.IntegerField(blank=True, choices=[(10, 'Years'), (20, 'Months'), (30, 'Days'), (40, 'Hours')], null=True, verbose_name='Lead Timeframe Unit'), + ), + migrations.AlterField( + model_name='eapregistration', + name='status', + field=models.IntegerField(choices=[(10, 'Under Development'), (20, 'Under Review'), (30, 'NS Addressing Comments'), (40, 'Technically Validated'), (50, 'Approved(Pending PFA)'), (60, 'Approved')], default=10, help_text='Select the current status of the EAP development process.', verbose_name='EAP Status'), + ), + migrations.AlterField( + model_name='enablingapproach', + name='ap_code', + field=models.IntegerField(default=1, verbose_name='AP Code'), + preserve_default=False, + ), + ] diff --git a/eap/models.py b/eap/models.py index 699a54db6..1f376d9fc 100644 --- a/eap/models.py +++ b/eap/models.py @@ -441,7 +441,7 @@ class Approach(models.IntegerChoices): approach = models.IntegerField(choices=Approach.choices, verbose_name=_("Approach")) budget_per_approach = models.IntegerField(verbose_name=_("Budget per approach (CHF)")) - ap_code = models.IntegerField(verbose_name=_("AP Code"), null=True, blank=True) + ap_code = models.IntegerField(verbose_name=_("AP Code")) previous_id = models.PositiveIntegerField(verbose_name=_("Previous ID"), null=True, blank=True) indicators = models.ManyToManyField( @@ -553,7 +553,7 @@ class EAPStatus(models.IntegerChoices): IFRC can change status to NS_ADDRESSING_COMMENTS or PENDING_PFA. """ - PENDING_PFA = 50, _("Pending PFA") + PENDING_PFA = 50, _("Approved(Pending PFA)") """EAP is in the process of signing the PFA between IFRC and NS. """ @@ -562,9 +562,6 @@ class EAPStatus(models.IntegerChoices): Cannot be changed back to previous statuses. """ - ACTIVATED = 70, _("Activated") - """EAP has been activated""" - # BASE MODEL FOR EAP class EAPRegistration(EAPBaseModel): @@ -722,12 +719,6 @@ class EAPRegistration(EAPBaseModel): verbose_name=_("pending pfa at"), help_text=_("Timestamp when the EAP was marked as pending PFA."), ) - activated_at = models.DateTimeField( - null=True, - blank=True, - verbose_name=_("activated at"), - help_text=_("Timestamp when the EAP was activated."), - ) # EAP submission deadline deadline = models.DateField( @@ -1449,13 +1440,19 @@ class FullEAP(EAPBaseModel, CommonEAPFields): ) # NOTE: In days - # TODO(susilnem): add unit for lead time lead_time = models.IntegerField( verbose_name=_("Lead Time"), null=True, blank=True, ) + lead_timeframe_unit = models.IntegerField( + choices=TimeFrame.choices, + verbose_name=_("Lead Timeframe Unit"), + null=True, + blank=True, + ) + trigger_statement_source_of_information = models.ManyToManyField( SourceInformation, verbose_name=_("Trigger Statement Source of Forecast"), diff --git a/eap/serializers.py b/eap/serializers.py index 53352d415..a85365780 100644 --- a/eap/serializers.py +++ b/eap/serializers.py @@ -255,7 +255,6 @@ class Meta: "status", "status_display", "requirement_cost", - "activated_at", "approved_at", "created_at", "modified_at", @@ -784,6 +783,23 @@ class Meta: ) exclude = ("cover_image",) + def _validate_timeframe(self, data: dict[str, typing.Any]) -> None: + lead_unit = data.get("lead_timeframe_unit") + lead_time_value = data.get("lead_time") + + if lead_time_value is not None and lead_unit is None: + raise serializers.ValidationError( + { + "lead_timeframe_unit": gettext("lead timeframe and unit must both be provided."), + } + ) + + if lead_unit is not None and lead_time_value is not None: + if lead_unit != TimeFrame.DAYS: + raise serializers.ValidationError( + {"lead_timeframe_unit": gettext("lead timeframe unit must be Days for Full EAP.")} + ) + def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: original_eap_registration = getattr(self.instance, "eap_registration", None) if self.instance else None eap_registration: EAPRegistration | None = data.get("eap_registration", original_eap_registration) @@ -812,6 +828,9 @@ def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: if eap_type and eap_type != EAPType.FULL_EAP: raise serializers.ValidationError("Cannot create Full EAP for non-full EAP registration.") + # Validate timeframe fields + self._validate_timeframe(data) + # Validate all image fields in one place for field in self.IMAGE_FIELDS: if field in data: @@ -844,7 +863,6 @@ def create(self, validated_data: dict[str, typing.Any]): (EAPRegistration.Status.TECHNICALLY_VALIDATED, EAPRegistration.Status.NS_ADDRESSING_COMMENTS), (EAPRegistration.Status.TECHNICALLY_VALIDATED, EAPRegistration.Status.PENDING_PFA), (EAPRegistration.Status.PENDING_PFA, EAPRegistration.Status.APPROVED), - (EAPRegistration.Status.APPROVED, EAPRegistration.Status.ACTIVATED), ] ) @@ -1065,17 +1083,6 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t ] ) - elif (current_status, new_status) == ( - EAPRegistration.Status.APPROVED, - EAPRegistration.Status.ACTIVATED, - ): - # Update timestamp - self.instance.activated_at = timezone.now() - self.instance.save( - update_fields=[ - "activated_at", - ] - ) return validated_data def validate(self, validated_data: dict[str, typing.Any]) -> dict[str, typing.Any]: diff --git a/eap/test_views.py b/eap/test_views.py index 14554ba4b..6815585de 100644 --- a/eap/test_views.py +++ b/eap/test_views.py @@ -260,7 +260,7 @@ def test_active_eaps(self): partners=[self.partner2.id], created_by=self.country_admin, modified_by=self.country_admin, - status=EAPStatus.ACTIVATED, + status=EAPStatus.APPROVED, eap_type=EAPType.SIMPLIFIED_EAP, ) EAPRegistrationFactory.create( @@ -1719,36 +1719,6 @@ def test_status_transition(self): response = self.client.patch(url, update_data, format="json") self.assertEqual(response.status_code, 400) - # NOTE: Transition to ACTIVATED - # APPROVED -> ACTIVATED - data = { - "status": EAPStatus.ACTIVATED, - } - - # LOGIN as country admin user - # FAILS: As only ifrc admins or superuser can - self.authenticate(self.country_admin) - response = self.client.post(self.url, data, format="json") - self.assertEqual(response.status_code, 400) - - # LOGIN as IFRC admin user - # SUCCESS: As only ifrc admins or superuser can - self.assertIsNone(self.eap_registration.activated_at) - self.authenticate(self.ifrc_admin_user) - response = self.client.post(self.url, data, format="json") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data["status"], EAPStatus.ACTIVATED) - # Check is the activated timeline is added - self.eap_registration.refresh_from_db() - self.assertIsNotNone(self.eap_registration.activated_at) - - # Check as if NS user cannot update after ACTIVATED - # FAILS As simplified EAP is in ACTIVATED, cannot updated - self.authenticate(self.country_admin) - url = f"/api/v2/simplified-eap/{simplified_eap.id}/" - response = self.client.patch(url, update_data, format="json") - self.assertEqual(response.status_code, 400) - @mock.patch("eap.serializers.generate_export_eap_pdf") @mock.patch("eap.serializers.group") @mock.patch("eap.serializers.send_new_eap_submission_email") @@ -2427,6 +2397,7 @@ def test_create_full_eap(self): "total_budget": 10000, "objective": "FUll eap objective", "lead_time": 5, + "lead_timeframe_unit": TimeFrame.DAYS, "expected_submission_time": "2024-12-31", "readiness_budget": 3000, "pre_positioning_budget": 4000, diff --git a/eap/views.py b/eap/views.py index a4ee97326..94740d1a1 100644 --- a/eap/views.py +++ b/eap/views.py @@ -64,7 +64,7 @@ def get_queryset(self) -> QuerySet[EAPRegistration]: return ( super() .get_queryset() - .filter(status__in=[EAPStatus.APPROVED, EAPStatus.ACTIVATED]) + .filter(status=EAPStatus.APPROVED) .select_related("disaster_type", "country") .annotate( requirement_cost=Case( From 77a7bd932612a0e37e79805ef5367537b583cc77 Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Wed, 25 Mar 2026 16:01:58 +0545 Subject: [PATCH 2/2] chore(eap): update eap validation checks and error messages --- assets | 2 +- eap/serializers.py | 35 ++++++++++++++++++++--------------- eap/test_views.py | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/assets b/assets index ddeb2616e..0ad5d584b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ddeb2616ec1fa1c30834217e6b2a3b69b3ee0227 +Subproject commit 0ad5d584b48cb85a72166a708b17bedb8f782e5e diff --git a/eap/serializers.py b/eap/serializers.py index a85365780..edcbd0273 100644 --- a/eap/serializers.py +++ b/eap/serializers.py @@ -218,7 +218,7 @@ class MiniFullEAPSerializer( class Meta: model = FullEAP - fields = [ + fields = ( "id", "total_budget", "readiness_budget", @@ -232,7 +232,7 @@ class Meta: "updated_checklist_file_details", "created_at", "modified_at", - ] + ) class MiniEAPSerializer(serializers.ModelSerializer): @@ -574,6 +574,8 @@ class Meta: read_only_fields = [ "version", "is_locked", + "created_by", + "modified_by", ] exclude = ("cover_image",) @@ -626,6 +628,11 @@ def _validate_timeframe(self, data: dict[str, typing.Any]) -> None: {"operational_timeframe": gettext("operational timeframe value is not valid for Months unit.")} ) + def validate_eap_registration(self, eap_registration: EAPRegistration) -> EAPRegistration: + if not self.instance and eap_registration.has_eap_application: + raise serializers.ValidationError("EAP for this registration has already been created.") + return eap_registration + def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: original_eap_registration = getattr(self.instance, "eap_registration", None) if self.instance else None eap_registration: EAPRegistration | None = data.get("eap_registration", original_eap_registration) @@ -634,9 +641,6 @@ def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: if self.instance and original_eap_registration != eap_registration: raise serializers.ValidationError("EAP Registration cannot be changed for existing EAP.") - if not self.instance and eap_registration.has_eap_application: - raise serializers.ValidationError("Simplified EAP for this EAP registration already exists.") - if self.instance and eap_registration.get_status_enum not in [ EAPRegistration.Status.UNDER_DEVELOPMENT, EAPRegistration.Status.NS_ADDRESSING_COMMENTS, @@ -777,10 +781,12 @@ class FullEAPSerializer( class Meta: model = FullEAP - read_only_fields = ( + read_only_fields = [ + "version", + "is_locked", "created_by", "modified_by", - ) + ] exclude = ("cover_image",) def _validate_timeframe(self, data: dict[str, typing.Any]) -> None: @@ -794,11 +800,13 @@ def _validate_timeframe(self, data: dict[str, typing.Any]) -> None: } ) - if lead_unit is not None and lead_time_value is not None: - if lead_unit != TimeFrame.DAYS: - raise serializers.ValidationError( - {"lead_timeframe_unit": gettext("lead timeframe unit must be Days for Full EAP.")} - ) + if lead_unit is not None and lead_time_value is not None and lead_unit != TimeFrame.DAYS: + raise serializers.ValidationError({"lead_timeframe_unit": gettext("lead timeframe unit must be Days for Full EAP.")}) + + def validate_eap_registration(self, eap_registration: EAPRegistration) -> EAPRegistration: + if not self.instance and eap_registration.has_eap_application: + raise serializers.ValidationError("EAP for this registration has already been created.") + return eap_registration def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: original_eap_registration = getattr(self.instance, "eap_registration", None) if self.instance else None @@ -808,9 +816,6 @@ def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]: if self.instance and original_eap_registration != eap_registration: raise serializers.ValidationError("EAP Registration cannot be changed for existing EAP.") - if not self.instance and eap_registration.has_eap_application: - raise serializers.ValidationError("Full EAP for this EAP registration already exists.") - if self.instance and eap_registration.get_status_enum not in [ EAPRegistration.Status.UNDER_DEVELOPMENT, EAPRegistration.Status.NS_ADDRESSING_COMMENTS, diff --git a/eap/test_views.py b/eap/test_views.py index 6815585de..c1185984e 100644 --- a/eap/test_views.py +++ b/eap/test_views.py @@ -716,7 +716,7 @@ def test_create_simplified_eap(self): # Cannot create Simplified EAP for the same EAP Registration again response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 400, response.data) def test_update_simplified_eap(self): eap_registration = EAPRegistrationFactory.create(