Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets
Submodule assets updated 1 files
+26 −37 openapi-schema.yaml
Original file line number Diff line number Diff line change
@@ -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,
),
]
21 changes: 9 additions & 12 deletions eap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
"""

Expand All @@ -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):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"),
Expand Down
58 changes: 35 additions & 23 deletions eap/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class MiniFullEAPSerializer(

class Meta:
model = FullEAP
fields = [
fields = (
"id",
"total_budget",
"readiness_budget",
Expand All @@ -232,7 +232,7 @@ class Meta:
"updated_checklist_file_details",
"created_at",
"modified_at",
]
)


class MiniEAPSerializer(serializers.ModelSerializer):
Expand All @@ -255,7 +255,6 @@ class Meta:
"status",
"status_display",
"requirement_cost",
"activated_at",
"approved_at",
"created_at",
"modified_at",
Expand Down Expand Up @@ -575,6 +574,8 @@ class Meta:
read_only_fields = [
"version",
"is_locked",
"created_by",
"modified_by",
]
exclude = ("cover_image",)

Expand Down Expand Up @@ -627,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)
Expand All @@ -635,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,
Expand Down Expand Up @@ -778,12 +781,33 @@ 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:
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 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
eap_registration: EAPRegistration | None = data.get("eap_registration", original_eap_registration)
Expand All @@ -792,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,
Expand All @@ -812,6 +833,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:
Expand Down Expand Up @@ -844,7 +868,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),
]
)

Expand Down Expand Up @@ -1065,17 +1088,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]:
Expand Down
35 changes: 3 additions & 32 deletions eap/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion eap/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading