Skip to content

Commit d619f08

Browse files
committed
feat(eap): add new endpoint for revise and update eap workflow
1 parent bdd3229 commit d619f08

9 files changed

Lines changed: 351 additions & 233 deletions

File tree

eap/factories.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ class Meta:
138138

139139
approach = fuzzy.FuzzyChoice(EnablingApproach.Approach)
140140
budget_per_approach = fuzzy.FuzzyInteger(1000, 1000000)
141-
ap_code = fuzzy.FuzzyInteger(100, 999)
142141

143142
@factory.post_generation
144143
def readiness_activities(self, create, extracted, **kwargs):
@@ -175,7 +174,6 @@ class Meta:
175174
sector = fuzzy.FuzzyChoice(PlannedOperation.Sector)
176175
people_targeted = fuzzy.FuzzyInteger(100, 100000)
177176
budget_per_sector = fuzzy.FuzzyInteger(1000, 1000000)
178-
ap_code = fuzzy.FuzzyInteger(100, 999)
179177

180178
@factory.post_generation
181179
def readiness_activities(self, create, extracted, **kwargs):
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 4.2.28 on 2026-04-03 09:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0228_merge_20260123_0806'),
10+
('eap', '0007_remove_eapregistration_activated_at_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name='enablingapproach',
16+
name='ap_code',
17+
),
18+
migrations.RemoveField(
19+
model_name='plannedoperation',
20+
name='ap_code',
21+
),
22+
migrations.AddField(
23+
model_name='fulleap',
24+
name='partners',
25+
field=models.ManyToManyField(blank=True, help_text='Select any partner NS involved in the EAP development.', related_name='+', to='api.country', verbose_name='Partners'),
26+
),
27+
migrations.AddField(
28+
model_name='simplifiedeap',
29+
name='partners',
30+
field=models.ManyToManyField(blank=True, help_text='Select any partner NS involved in the EAP development.', related_name='+', to='api.country', verbose_name='Partners'),
31+
),
32+
migrations.AlterField(
33+
model_name='plannedoperation',
34+
name='sector',
35+
field=models.IntegerField(choices=[(101, 'Shelter, Settlement and Housing'), (102, 'Livelihoods'), (103, 'Protection, Gender and Inclusion'), (104, 'Health and Care'), (105, 'Risk Reduction, Climate Adaptation and Recovery'), (106, 'Multipurpose Cash'), (107, 'Water, Sanitation And Hygiene'), (109, 'Education'), (110, 'Migration'), (111, 'Environment Sustainability'), (112, 'Community Engagement And Accountability')], verbose_name='sector'),
36+
),
37+
]

eap/models.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -386,27 +386,24 @@ class Sector(models.IntegerChoices):
386386
RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = 105, _("Risk Reduction, Climate Adaptation and Recovery")
387387
MULTIPURPOSE_CASH = 106, _("Multipurpose Cash")
388388
WATER_SANITATION_AND_HYGIENE = 107, _("Water, Sanitation And Hygiene")
389-
WASH = 108, _("WASH")
390389
EDUCATION = 109, _("Education")
391390
MIGRATION = 110, _("Migration")
392391
ENVIRONMENT_SUSTAINABILITY = 111, _("Environment Sustainability")
393392
COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = 112, _("Community Engagement And Accountability")
394393

395-
# NOTE: AP Codes are read only in EAP forms and passed through ENUMS for now.
396-
# They are not stored in the database but are derived from the sector field. Make sure to keep them in sync.
397-
class APCode(models.TextChoices):
398-
SHELTER_SETTLEMENT_AND_HOUSING = "AP101, AP103, AP104", _("AP101, AP103, AP104")
399-
LIVELIHOODS = "AP007", _("AP007")
400-
PROTECTION_GENDER_AND_INCLUSION = "AP114, AP116, AP117", _("AP114, AP116, AP117")
401-
HEALTH_AND_CARE = "AP107, AP108, AP109", _("AP107, AP108, AP109")
402-
RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = "AP101, AP103, AP104, AP105, AP106", _("AP101, AP103, AP104, AP105, AP106")
403-
MULTIPURPOSE_CASH = "AP081", _("AP081")
404-
WATER_SANITATION_AND_HYGIENE = "AP110, AP111", _("AP110, AP111")
405-
WASH = "AP110, AP111", _("AP110, AP111")
406-
EDUCATION = "AP115", _("AP115")
407-
MIGRATION = "AP112, AP113", _("AP112, AP113")
408-
ENVIRONMENT_SUSTAINABILITY = "AP102", _("AP102")
409-
COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = "AP129", _("AP129")
394+
# NOTE: Use integer values directly in APCode rather than referencing `Sector` directly
395+
class APCode(models.IntegerChoices):
396+
SHELTER_SETTLEMENT_AND_HOUSING = 101, _("AP101, AP103, AP104")
397+
LIVELIHOODS = 102, _("AP007")
398+
PROTECTION_GENDER_AND_INCLUSION = 103, _("AP114, AP116, AP117")
399+
HEALTH_AND_CARE = 104, _("AP107, AP108, AP109")
400+
RISK_REDUCTION_CLIMATE_ADAPTATION_AND_RECOVERY = 105, _("AP101, AP103, AP104, AP105, AP106")
401+
MULTIPURPOSE_CASH = 106, _("AP081")
402+
WATER_SANITATION_AND_HYGIENE = 107, _("AP110, AP111")
403+
EDUCATION = 109, _("AP115")
404+
MIGRATION = 110, _("AP112, AP113")
405+
ENVIRONMENT_SUSTAINABILITY = 111, _("AP102")
406+
COMMUNITY_ENGAGEMENT_AND_ACCOUNTABILITY = 112, _("AP129")
410407

411408
sector = models.IntegerField(choices=Sector.choices, verbose_name=_("sector"))
412409
people_targeted = models.IntegerField(verbose_name=_("People Targeted"))
@@ -456,14 +453,15 @@ class Approach(models.IntegerChoices):
456453

457454
# NOTE: AP Codes are read only in EAP forms and passed through ENUMS for now.
458455
# They are not stored in the database but are derived from the Approach field. Make sure to keep them in sync.
459-
class APCode(models.TextChoices):
460-
SECRETARIAT_SERVICES = "AP122", _("AP122")
461-
NATIONAL_SOCIETY_STRENGTHENING = "AP124, AP125, AP126", _("AP124, AP125, AP126")
462-
PARTNERSHIP_AND_COORDINATION = "AP049, AP118, AP119, AP120, AP121, AP127, AP128", _("AP049, AP118, AP119, AP120, AP121, AP127, AP128")
456+
class APCode(models.IntegerChoices):
457+
SECRETARIAT_SERVICES = 10, _("AP122")
458+
NATIONAL_SOCIETY_STRENGTHENING = 20, _("AP124, AP125, AP126")
459+
PARTNERSHIP_AND_COORDINATION = 30, _(
460+
"AP049, AP118, AP119, AP120, AP121, AP127, AP128"
461+
)
463462

464463
approach = models.IntegerField(choices=Approach.choices, verbose_name=_("Approach"))
465464
budget_per_approach = models.IntegerField(verbose_name=_("Budget per approach (CHF)"))
466-
ap_code = models.IntegerField(verbose_name=_("AP Code"))
467465
previous_id = models.PositiveIntegerField(verbose_name=_("Previous ID"), null=True, blank=True)
468466

469467
indicators = models.ManyToManyField(
@@ -821,6 +819,14 @@ class CommonEAPFields(models.Model):
821819
related_name="+",
822820
)
823821

822+
partners = models.ManyToManyField(
823+
Country,
824+
verbose_name=_("Partners"),
825+
help_text=_("Select any partner NS involved in the EAP development."),
826+
related_name="+",
827+
blank=True,
828+
)
829+
824830
people_targeted = models.IntegerField(
825831
verbose_name=_("People Targeted."),
826832
blank=True,
@@ -1285,6 +1291,7 @@ def generate_snapshot(self):
12851291
},
12861292
exclude_clone_m2m_fields={
12871293
"admin2",
1294+
"partners",
12881295
"cover_image",
12891296
"hazard_impact_images",
12901297
"risk_selected_protocols_images",
@@ -1831,6 +1838,7 @@ def generate_snapshot(self):
18311838
},
18321839
exclude_clone_m2m_fields={
18331840
"admin2",
1841+
"partners",
18341842
"cover_image",
18351843
# Files
18361844
"hazard_selection_images",

eap/serializers.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ def get_fields(self):
497497
fields["budget_file"] = serializers.PrimaryKeyRelatedField(
498498
queryset=EAPFile.objects.all(), required=False, allow_null=True
499499
)
500+
fields["partners_details"] = MiniCountrySerializer(source="partners", many=True, read_only=True)
500501
fields["budget_file_details"] = EAPFileSerializer(source="budget_file", read_only=True)
501502
fields["updated_checklist_file_details"] = EAPFileSerializer(source="updated_checklist_file", read_only=True)
502503
return fields
@@ -821,8 +822,8 @@ def validate(self, data: dict[str, typing.Any]) -> dict[str, typing.Any]:
821822
EAPRegistration.Status.NS_ADDRESSING_COMMENTS,
822823
]:
823824
raise serializers.ValidationError(
824-
gettext("Cannot update while EAP Application is in %s."),
825-
EAPRegistration.Status(eap_registration.get_status_enum).label,
825+
gettext("Cannot update while EAP Application is in %s.")
826+
% EAPRegistration.Status(eap_registration.get_status_enum).label
826827
)
827828

828829
# NOTE: Cannot update locked Full EAP
@@ -950,27 +951,26 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t
950951
% EAPRegistration.Status(new_status).label
951952
)
952953

953-
# latest EAP
954954
if self.instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP:
955-
snapshot_instance = self.instance.latest_simplified_eap.generate_snapshot()
956-
self.instance.latest_simplified_eap = snapshot_instance
957-
snapshot_instance.review_checklist_file = review_checklist_file
958-
snapshot_instance.save(update_fields=["review_checklist_file"])
959-
self.instance.save(update_fields=["latest_simplified_eap"])
955+
self.instance.latest_simplified_eap.review_checklist_file = review_checklist_file
956+
self.instance.latest_simplified_eap.save(update_fields=["review_checklist_file"])
960957
else:
961-
snapshot_instance = self.instance.latest_full_eap.generate_snapshot()
962-
self.instance.latest_full_eap = snapshot_instance
963-
snapshot_instance.review_checklist_file = review_checklist_file
964-
snapshot_instance.save(update_fields=["review_checklist_file"])
965-
self.instance.save(update_fields=["latest_full_eap"])
958+
self.instance.latest_full_eap.review_checklist_file = review_checklist_file
959+
self.instance.latest_full_eap.save(update_fields=["review_checklist_file"])
966960

967961
# NOTE: Clearing validated budget file, if changes to NS Addressing Comments.
968962
if (current_status, new_status) == (
969963
EAPRegistration.Status.TECHNICALLY_VALIDATED,
970964
EAPRegistration.Status.NS_ADDRESSING_COMMENTS,
971965
):
972966
self.instance.validated_budget_file = None
973-
self.instance.save(update_fields=["validated_budget_file"])
967+
self.instance.technically_validated_at = None
968+
self.instance.save(
969+
update_fields=[
970+
"validated_budget_file",
971+
"technically_validated_at",
972+
]
973+
)
974974

975975
elif (current_status, new_status) == (
976976
EAPRegistration.Status.UNDER_REVIEW,
@@ -1130,31 +1130,30 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any
11301130
]:
11311131
"""
11321132
NOTE:
1133-
At the transition (UNDER_REVIEW -> NS_ADDRESSING_COMMENTS), the EAP snapshot
1134-
is generated inside `_validate_status()` BEFORE we reach this `update()` method.
1135-
1133+
At the transition (UNDER_REVIEW -> NS_ADDRESSING_COMMENTS),
1134+
after revise of eap, new snapshot will be created with incremented version.
11361135
That snapshot operation:
11371136
- Locks the reviewed EAP (previous version)
11381137
- Creates a new snapshot (incremented version)
11391138
- Updates latest_simplified_eap or latest_full_eap to the new version
11401139
11411140
Email logic based on eap_count:
1142-
- If eap_count == 2 (i.e., first snapshot already exists and this is the first IFRC feedback cycle)
1141+
- If eap_count == 1 (i.e., first eap has been created), indicating the first feedback cycle:
11431142
- Send the first feedback email
1144-
- Else (eap_count > 2), indicating subsequent feedback cycles:
1143+
- Else (eap_count > 1), indicating subsequent feedback cycles:
11451144
- Send the resubmitted feedback email
11461145
11471146
Therefore:
1148-
- version == 2 always corresponds to the first IFRC feedback cycle
1149-
- Any later versions (>= 3) correspond to resubmitted cycles
1150-
- Also when the IFRC resubmits after technical validation, it will be version >= 3
1147+
- version == 1 always corresponds to the first IFRC feedback cycle
1148+
- Any later versions (>1) correspond to resubmitted cycles
1149+
- Also when the IFRC resubmits after technical validation, it will be version > 2
11511150
11521151
Deadline update rules:
11531152
- First IFRC feedback cycle: deadline is set to 90 days from the current date.
11541153
- Subsequent feedback or resubmission cycles: deadline is set to 30 days from the current date.
11551154
"""
11561155

1157-
if eap_count == 2:
1156+
if eap_count == 1:
11581157
updated_instance.deadline = timezone.now().date() + timedelta(days=90)
11591158
updated_instance.save(
11601159
update_fields=[
@@ -1163,7 +1162,7 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any
11631162
)
11641163
transaction.on_commit(lambda: send_feedback_email.delay(eap_registration_id))
11651164

1166-
elif eap_count > 2:
1165+
elif eap_count > 1:
11671166
updated_instance.deadline = timezone.now().date() + timedelta(days=30)
11681167
updated_instance.save(
11691168
update_fields=[

eap/tasks.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,7 @@ def send_new_eap_submission_email(eap_registration_id: int):
217217
eap_registration_id=instance.id,
218218
version=latest_eap.version,
219219
)
220-
partner_contacts = latest_eap.partner_contacts
221-
partner_ns_emails = list(partner_contacts.values_list("email", flat=True))
220+
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
222221

223222
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
224223

@@ -265,8 +264,7 @@ def send_feedback_email(eap_registration_id: int):
265264

266265
ifrc_delegation_focal_point_email = latest_eap.ifrc_delegation_focal_point_email
267266

268-
partner_contacts = latest_eap.partner_contacts
269-
partner_ns_emails = list(partner_contacts.values_list("email", flat=True))
267+
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
270268

271269
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
272270

@@ -314,16 +312,13 @@ def send_eap_resubmission_email(eap_registration_id: int):
314312
else:
315313
latest_eap = instance.latest_full_eap
316314

317-
latest_version = latest_eap.version
318-
319315
if not latest_eap.diff_file:
320316
generate_export_diff_pdf(
321317
eap_registration_id=instance.id,
322318
version=latest_eap.version,
323319
)
324320

325-
partner_contacts = latest_eap.partner_contacts
326-
partner_ns_emails = list(partner_contacts.values_list("email", flat=True))
321+
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
327322

328323
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
329324

@@ -345,7 +340,7 @@ def send_eap_resubmission_email(eap_registration_id: int):
345340
email_context = get_eap_email_context(instance)
346341
email_subject = (
347342
f"[DREF {instance.get_eap_type_display()} FOR REVIEW] "
348-
f"{instance.country} {instance.disaster_type} version {latest_version} TO THE IFRC-DREF"
343+
f"{instance.country} {instance.disaster_type} version {latest_eap.version} TO THE IFRC-DREF"
349344
)
350345
email_body = render_to_string("email/eap/re-submission.html", email_context)
351346
email_type = "Feedback to the National Society"
@@ -369,26 +364,10 @@ def send_feedback_email_for_resubmitted_eap(eap_registration_id: int):
369364

370365
if instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP:
371366
latest_eap = instance.latest_simplified_eap
372-
eap_model = SimplifiedEAP
373367
else:
374368
latest_eap = instance.latest_full_eap
375-
eap_model = FullEAP
376-
377-
latest_version = latest_eap.version
378369

379-
partner_contacts = latest_eap.partner_contacts
380-
partner_ns_emails = list(partner_contacts.values_list("email", flat=True))
381-
382-
previous_eap = (
383-
eap_model.objects.filter(
384-
eap_registration=instance,
385-
version__lt=latest_version,
386-
)
387-
.order_by("-version")
388-
.first()
389-
)
390-
391-
previous_version = previous_eap.version if previous_eap else None
370+
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
392371

393372
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
394373

@@ -409,7 +388,7 @@ def send_feedback_email_for_resubmitted_eap(eap_registration_id: int):
409388
email_context = get_eap_email_context(instance)
410389
email_subject = (
411390
f"[DREF {instance.get_eap_type_display()} FEEDBACK] "
412-
f"{instance.country} {instance.disaster_type} version {previous_version} TO {instance.national_society}"
391+
f"{instance.country} {instance.disaster_type} version {latest_eap.version} TO {instance.national_society}"
413392
)
414393
email_body = render_to_string("email/eap/feedback_to_revised_eap.html", email_context)
415394
email_type = "Feedback to the National Society"
@@ -491,8 +470,7 @@ def send_pending_pfa_email(eap_registration_id: int):
491470
eap_registration_id=instance.id,
492471
)
493472

494-
partner_contacts = latest_eap.partner_contacts
495-
partner_ns_emails = list(partner_contacts.values_list("email", flat=True))
473+
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
496474

497475
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
498476

0 commit comments

Comments
 (0)