diff --git a/api/migrations/0229_alter_export_export_type.py b/api/migrations/0229_alter_export_export_type.py new file mode 100644 index 000000000..1e89038e5 --- /dev/null +++ b/api/migrations/0229_alter_export_export_type.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.26 on 2025-12-04 09:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0228_alter_export_export_type"), + ] + + operations = [ + migrations.AlterField( + model_name="export", + name="export_type", + field=models.CharField( + choices=[ + ("dref-applications", "DREF Application"), + ("dref-operational-updates", "DREF Operational Update"), + ("dref-final-reports", "DREF Final Report"), + ("old-dref-final-reports", "Old DREF Final Report"), + ("per", "Per"), + ("simplified", "Simplified EAP"), + ("full", "Full EAP"), + ], + max_length=255, + verbose_name="Export Type", + ), + ), + ] diff --git a/api/models.py b/api/models.py index e3621622e..9e6acc911 100644 --- a/api/models.py +++ b/api/models.py @@ -2560,8 +2560,8 @@ class ExportType(models.TextChoices): FINAL_REPORT = "dref-final-reports", _("DREF Final Report") OLD_FINAL_REPORT = "old-dref-final-reports", _("Old DREF Final Report") PER = "per", _("Per") - SIMPLIFIED_EAP = "simplified-eap", _("Simplified EAP") - FULL_EAP = "full-eap", _("Full EAP") + SIMPLIFIED_EAP = "simplified", _("Simplified EAP") + FULL_EAP = "full", _("Full EAP") export_id = models.IntegerField(verbose_name=_("Export Id")) export_type = models.CharField(verbose_name=_("Export Type"), max_length=255, choices=ExportType.choices) diff --git a/api/serializers.py b/api/serializers.py index 80fa58b0f..4d8496b05 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -15,7 +15,7 @@ from api.utils import CountryValidator, RegionValidator from deployments.models import EmergencyProject, Personnel, PersonnelDeployment from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate -from eap.models import FullEAP, SimplifiedEAP +from eap.models import EAPRegistration, FullEAP, SimplifiedEAP from lang.models import String from lang.serializers import ModelSerializer from local_units.models import DelegationOffice @@ -2544,6 +2544,11 @@ class ExportSerializer(serializers.ModelSerializer): status_display = serializers.CharField(source="get_status_display", read_only=True) # NOTE: is_pga is used to determine if the export contains PGA or not is_pga = serializers.BooleanField(default=False, required=False, write_only=True) + # NOTE: diff is used to determine if the export is requested for diff view or not + # Currently only used for EAP exports + diff = serializers.BooleanField(default=False, required=False, write_only=True) + # NOTE: Version of a EAP export being requested, only applicable for full and simplified EAP exports + version = serializers.IntegerField(required=False, write_only=True) class Meta: model = Export @@ -2559,6 +2564,7 @@ def create(self, validated_data): export_id = validated_data.get("export_id") export_type = validated_data.get("export_type") country_id = validated_data.get("per_country") + version = validated_data.pop("version", None) if export_type == Export.ExportType.DREF: title = Dref.objects.filter(id=export_id).first().title elif export_type == Export.ExportType.OPS_UPDATE: @@ -2569,12 +2575,42 @@ def create(self, validated_data): overview = Overview.objects.filter(id=export_id).first() title = f"{overview.country.name}-preparedness-{overview.get_phase_display()}" elif export_type == Export.ExportType.SIMPLIFIED_EAP: - simplified_eap = SimplifiedEAP.objects.filter(id=export_id).first() + if version: + simplified_eap = SimplifiedEAP.objects.filter( + eap_registration=export_id, + version=version, + ).first() + if not simplified_eap: + raise serializers.ValidationError("No Simplified EAP found for the given EAP Registration ID and version") + else: + eap_registration = EAPRegistration.objects.filter(id=export_id).first() + if not eap_registration: + raise serializers.ValidationError("No EAP Registration found for the given ID") + + simplified_eap = eap_registration.latest_simplified_eap + if not simplified_eap: + serializers.ValidationError("No Simplified EAP found for the given EAP Registration ID") + title = ( f"{simplified_eap.eap_registration.national_society.name}-{simplified_eap.eap_registration.disaster_type.name}" ) elif export_type == Export.ExportType.FULL_EAP: - full_eap = FullEAP.objects.filter(id=export_id).first() + if version: + full_eap = FullEAP.objects.filter( + eap_registration=export_id, + version=version, + ).first() + if not full_eap: + raise serializers.ValidationError("No Full EAP found for the given EAP Registration ID and version") + else: + eap_registration = EAPRegistration.objects.filter(id=export_id).first() + if not eap_registration: + raise serializers.ValidationError("No EAP Registration found for the given ID") + + full_eap = eap_registration.latest_full_eap + if not full_eap: + serializers.ValidationError("No Full EAP found for the given EAP Registration ID") + title = f"{full_eap.eap_registration.national_society.name}-{full_eap.eap_registration.disaster_type.name}" else: title = "Export" @@ -2582,6 +2618,19 @@ def create(self, validated_data): if export_type == Export.ExportType.PER: validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/countries/{country_id}/{export_type}/{export_id}/export/" + + elif export_type in [ + Export.ExportType.SIMPLIFIED_EAP, + Export.ExportType.FULL_EAP, + ]: + validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/eap/{export_id}/{export_type}/export/" + # NOTE: EAP exports with diff view only for EAPs exports + if version: + validated_data["url"] += f"?version={version}" + diff = validated_data.pop("diff") + if diff: + validated_data["url"] += "&diff=true" if version else "?diff=true" + else: validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/{export_type}/{export_id}/export/" diff --git a/assets b/assets index 585e4c8ee..31f596014 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 585e4c8eea8c255aab7830437b82b20c85d94cce +Subproject commit 31f5960143b09b0d9cfd4e729760d2517783758e diff --git a/eap/serializers.py b/eap/serializers.py index bf863204f..f18672f04 100644 --- a/eap/serializers.py +++ b/eap/serializers.py @@ -98,6 +98,8 @@ class Meta: "version", "is_locked", "updated_checklist_file", + "created_at", + "modified_at", ] @@ -118,6 +120,8 @@ class Meta: "version", "is_locked", "updated_checklist_file", + "created_at", + "modified_at", ] @@ -143,6 +147,8 @@ class Meta: "requirement_cost", "activated_at", "approved_at", + "created_at", + "modified_at", ] diff --git a/eap/test_views.py b/eap/test_views.py index 4ea1befc5..5abedca49 100644 --- a/eap/test_views.py +++ b/eap/test_views.py @@ -1614,8 +1614,11 @@ def setUp(self): self.partner2 = CountryFactory.create(name="partner2", iso3="AAA", iso="AA") self.user = UserFactory.create() + self.url = "/api/v2/pdf-export/" - self.eap_registration = EAPRegistrationFactory.create( + @mock.patch("api.serializers.generate_url.delay") + def test_simplified_eap_export(self, mock_generate_url): + eap_registration = EAPRegistrationFactory.create( eap_type=EAPType.SIMPLIFIED_EAP, country=self.country, national_society=self.national_society, @@ -1624,13 +1627,8 @@ def setUp(self): created_by=self.user, modified_by=self.user, ) - - self.url = "/api/v2/pdf-export/" - - @mock.patch("api.serializers.generate_url.delay") - def test_simplified_eap_export(self, mock_generate_url): - self.simplified_eap = SimplifiedEAPFactory.create( - eap_registration=self.eap_registration, + simplified_eap = SimplifiedEAPFactory.create( + eap_registration=eap_registration, created_by=self.user, modified_by=self.user, national_society_contact_title="NS Title Example", @@ -1639,9 +1637,12 @@ def test_simplified_eap_export(self, mock_generate_url): modified_by=self.user, ), ) + eap_registration.latest_simplified_eap = simplified_eap + eap_registration.save() + data = { "export_type": Export.ExportType.SIMPLIFIED_EAP, - "export_id": self.simplified_eap.id, + "export_id": eap_registration.id, "is_pga": False, } @@ -1652,7 +1653,7 @@ def test_simplified_eap_export(self, mock_generate_url): self.assert_201(response) self.assertIsNotNone(response.data["id"], response.data) - expected_url = f"{settings.GO_WEB_INTERNAL_URL}/{Export.ExportType.SIMPLIFIED_EAP}/{self.simplified_eap.id}/export/" + expected_url = f"{settings.GO_WEB_INTERNAL_URL}/eap/{eap_registration.id}/{Export.ExportType.SIMPLIFIED_EAP}/export/" self.assertEqual(response.data["url"], expected_url) self.assertEqual(response.data["status"], Export.ExportStatus.PENDING) @@ -1666,10 +1667,41 @@ def test_simplified_eap_export(self, mock_generate_url): django_get_language(), ) + # Test Export Snapshot + + # create a new snapshot + simplfied_eap_snapshot = simplified_eap.generate_snapshot() + assert simplfied_eap_snapshot.version == 2, "Snapshot version should be 2" + + data = { + "export_type": Export.ExportType.SIMPLIFIED_EAP, + "export_id": eap_registration.id, + "version": 2, + } + + with self.capture_on_commit_callbacks(execute=True): + response = self.client.post(self.url, data, format="json") + self.assert_201(response) + self.assertIsNotNone(response.data["id"], response.data) + + expected_url = ( + f"{settings.GO_WEB_INTERNAL_URL}/eap/{eap_registration.id}/{Export.ExportType.SIMPLIFIED_EAP}/export/?version=2" + ) + self.assertEqual(response.data["url"], expected_url) + @mock.patch("api.serializers.generate_url.delay") def test_full_eap_export(self, mock_generate_url): - self.full_eap = FullEAPFactory.create( - eap_registration=self.eap_registration, + eap_registration = EAPRegistrationFactory.create( + eap_type=EAPType.FULL_EAP, + country=self.country, + national_society=self.national_society, + disaster_type=self.disaster_type, + created_by=self.user, + modified_by=self.user, + ) + + full_eap = FullEAPFactory.create( + eap_registration=eap_registration, created_by=self.user, modified_by=self.user, budget_file=EAPFileFactory._create_file( @@ -1677,9 +1709,13 @@ def test_full_eap_export(self, mock_generate_url): modified_by=self.user, ), ) + + eap_registration.latest_full_eap = full_eap + eap_registration.save() + data = { "export_type": Export.ExportType.FULL_EAP, - "export_id": self.full_eap.id, + "export_id": eap_registration.id, "is_pga": False, } @@ -1689,7 +1725,7 @@ def test_full_eap_export(self, mock_generate_url): response = self.client.post(self.url, data, format="json") self.assert_201(response) self.assertIsNotNone(response.data["id"], response.data) - expected_url = f"{settings.GO_WEB_INTERNAL_URL}/{Export.ExportType.FULL_EAP}/{self.full_eap.id}/export/" + expected_url = f"{settings.GO_WEB_INTERNAL_URL}/eap/{eap_registration.id}/{Export.ExportType.FULL_EAP}/export/" self.assertEqual(response.data["url"], expected_url) self.assertEqual(response.data["status"], Export.ExportStatus.PENDING) @@ -1703,6 +1739,59 @@ def test_full_eap_export(self, mock_generate_url): django_get_language(), ) + @mock.patch("api.serializers.generate_url.delay") + def test_diff_export_eap(self, mock_generate_url): + eap_registration = EAPRegistrationFactory.create( + eap_type=EAPType.SIMPLIFIED_EAP, + country=self.country, + national_society=self.national_society, + disaster_type=self.disaster_type, + created_by=self.user, + modified_by=self.user, + ) + + simplified_eap = SimplifiedEAPFactory.create( + eap_registration=eap_registration, + created_by=self.user, + modified_by=self.user, + budget_file=EAPFileFactory._create_file( + created_by=self.user, + modified_by=self.user, + ), + ) + + eap_registration.latest_simplified_eap = simplified_eap + eap_registration.save() + + self.authenticate(self.user) + data = { + "export_type": Export.ExportType.SIMPLIFIED_EAP, + "export_id": eap_registration.id, + "diff": True, + } + + self.authenticate(self.user) + + with self.capture_on_commit_callbacks(execute=True): + response = self.client.post(self.url, data, format="json") + self.assert_201(response) + self.assertIsNotNone(response.data["id"], response.data) + + expected_url = ( + f"{settings.GO_WEB_INTERNAL_URL}/eap/{eap_registration.id}/{Export.ExportType.SIMPLIFIED_EAP}/export/?diff=true" + ) + self.assertEqual(response.data["url"], expected_url) + + self.assertEqual(mock_generate_url.called, True) + title = f"{self.national_society.name}-{self.disaster_type.name}" + mock_generate_url.assert_called_once_with( + expected_url, + response.data["id"], + self.user.id, + title, + django_get_language(), + ) + class EAPFullTestCase(APITestCase): def setUp(self): diff --git a/eap/views.py b/eap/views.py index c3fccf92e..6ca759a11 100644 --- a/eap/views.py +++ b/eap/views.py @@ -104,7 +104,6 @@ def get_queryset(self) -> QuerySet[EAPRegistration]: "partners", "simplified_eap", ) - .order_by("id") ) @action(