Skip to content

Commit 05524db

Browse files
Merge pull request #2590 from IFRCGo/project/eap-workflow
Project: EAP Workflow
2 parents 1ad6162 + b21c351 commit 05524db

43 files changed

Lines changed: 11747 additions & 217 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/filter_set.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class Meta:
162162
model = Admin2
163163
fields = {
164164
"id": ("exact", "in"),
165+
"code": ("exact", "in"),
165166
"admin1": ("exact", "in"),
166167
"admin1__country": ("exact", "in"),
167168
"admin1__country__iso3": ("exact", "in"),
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 4.2.30 on 2026-04-27 06:11
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("api", "0230_alter_districtgeoms_district"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="export",
14+
name="export_type",
15+
field=models.CharField(
16+
choices=[
17+
("dref-applications", "DREF Application"),
18+
("dref-operational-updates", "DREF Operational Update"),
19+
("dref-final-reports", "DREF Final Report"),
20+
("old-dref-final-reports", "Old DREF Final Report"),
21+
("per", "Per"),
22+
("simplified", "Simplified EAP"),
23+
("full", "Full EAP"),
24+
],
25+
max_length=255,
26+
verbose_name="Export Type",
27+
),
28+
),
29+
]

api/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3333,6 +3333,8 @@ class ExportType(models.TextChoices):
33333333
FINAL_REPORT = "dref-final-reports", _("DREF Final Report")
33343334
OLD_FINAL_REPORT = "old-dref-final-reports", _("Old DREF Final Report")
33353335
PER = "per", _("Per")
3336+
SIMPLIFIED_EAP = "simplified", _("Simplified EAP")
3337+
FULL_EAP = "full", _("Full EAP")
33363338

33373339
export_id = models.IntegerField(verbose_name=_("Export Id"))
33383340
export_type = models.CharField(verbose_name=_("Export Type"), max_length=255, choices=ExportType.choices)

api/playwright.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import json
2+
import pathlib
3+
import tempfile
4+
import time
5+
6+
from django.conf import settings
7+
from django.core.files.base import ContentFile
8+
from playwright.sync_api import sync_playwright
9+
10+
from .utils import DebugPlaywright
11+
12+
footer_template = """
13+
<div class="footer" style="width: 100%;font-size: 8px;color: #FEFEFE; bottom: 10px; position: absolute;">
14+
<div style="float: left; margin-top: 10px; margin-left: 40px;">
15+
Page <span class="pageNumber"></span> / <span class="totalPages"></span>
16+
</div>
17+
<div style="float: right; margin-right: 40px;">
18+
<svg
19+
xmlns="http://www.w3.org/2000/svg"
20+
viewBox="0 0 89.652 89.654"
21+
height="48"
22+
width="48"
23+
>
24+
<path
25+
d="M50.284 18.637a5.14 5.14 0 00-5.136-5.135 5.139 5.139 0 00-5.135 5.135 5.141 5.141 0 005.135 5.138 5.146 5.146 0 005.136-5.138M28.416 63.032a5.143 5.143 0 00-5.138 5.138 5.14 5.14 0 005.138 5.133 5.14 5.14 0 005.136-5.133 5.143 5.143 0 00-5.136-5.138M45.151 34.057a7.021 7.021 0 00-7.02 7.025 7.02 7.02 0 0014.04 0 7.021 7.021 0 00-7.02-7.025M61.883 63.032a5.143 5.143 0 00-5.135 5.138 5.138 5.138 0 005.135 5.133 5.14 5.14 0 005.136-5.133 5.143 5.143 0 00-5.136-5.138"
26+
class="st1"
27+
fill="#F5333F"
28+
/>
29+
<path
30+
d="M61.883 75.769c-4.19 0-7.601-3.41-7.601-7.602 0-2.32 1.05-4.4 2.696-5.794L49.726 50.26a10.205 10.205 0 01-4.575 1.085c-1.648 0-3.196-.397-4.577-1.085l-7.252 12.113a7.571 7.571 0 012.693 5.794c0 4.191-3.408 7.602-7.599 7.602-4.19 0-7.601-3.41-7.601-7.602 0-4.19 3.41-7.601 7.601-7.601.984 0 1.926.196 2.791.54l7.303-12.2a10.236 10.236 0 01-3.63-7.827c0-5.254 3.947-9.58 9.038-10.189v-4.762c-3.606-.59-6.368-3.72-6.368-7.49 0-4.192 3.41-7.602 7.601-7.602s7.599 3.41 7.599 7.601c0 3.77-2.762 6.9-6.366 7.49v4.763c5.093.611 9.038 4.935 9.038 10.19a10.23 10.23 0 01-3.633 7.826l7.306 12.2a7.544 7.544 0 012.791-.54c4.191 0 7.599 3.41 7.599 7.601s-3.41 7.602-7.602 7.602m-49.286-34.65c0-5.485 3.44-10.057 9.194-10.057 4.194 0 7.715 2.236 8.226 6.562h-3.281c-.32-2.524-2.524-3.818-4.945-3.818-4.117 0-5.834 3.627-5.834 7.313s1.717 7.313 5.834 7.313c3.44.056 5.32-2.016 5.376-5.268h-5.106v-2.556h8.173v10.11h-2.151l-.51-2.257c-1.803 2.043-3.44 2.715-5.78 2.715-5.754 0-9.196-4.57-9.196-10.057M44.826 0C20.07 0 0 20.069 0 44.828c0 24.755 20.071 44.826 44.826 44.826 24.757 0 44.826-20.071 44.826-44.826C89.652 20.068 69.582 0 44.826 0"
31+
class="st1"
32+
fill="#F5333F"
33+
/>
34+
</svg>
35+
</div>
36+
</div>
37+
""" # noqa
38+
39+
40+
def build_storage_state(tmp_dir, user, token, language="en"):
41+
temp_file = pathlib.Path(tmp_dir, "storage_state.json")
42+
temp_file.touch()
43+
44+
state = {
45+
"origins": [
46+
{
47+
"origin": settings.GO_WEB_INTERNAL_URL + "/",
48+
"localStorage": [
49+
{
50+
"name": "user",
51+
"value": json.dumps(
52+
{
53+
"id": user.id,
54+
"username": user.username,
55+
"firstName": user.first_name,
56+
"lastName": user.last_name,
57+
"token": token.key,
58+
}
59+
),
60+
},
61+
{"name": "language", "value": json.dumps(language)},
62+
],
63+
}
64+
]
65+
}
66+
with open(temp_file, "w") as f:
67+
json.dump(state, f)
68+
return temp_file
69+
70+
71+
def render_pdf_from_url(
72+
*,
73+
url: str,
74+
user,
75+
token,
76+
language: str = "en",
77+
timeout: int = 300_000,
78+
):
79+
"""
80+
Renders a URL to PDF using Playwright.
81+
Returns a Django ContentFile.
82+
"""
83+
with tempfile.TemporaryDirectory() as tmp_dir:
84+
storage_state = build_storage_state(
85+
tmp_dir=tmp_dir,
86+
user=user,
87+
token=token,
88+
language=language,
89+
)
90+
91+
with sync_playwright() as playwright:
92+
browser = playwright.chromium.connect(settings.PLAYWRIGHT_SERVER_URL)
93+
94+
try:
95+
context = browser.new_context(storage_state=storage_state)
96+
page = context.new_page()
97+
98+
if settings.DEBUG_PLAYWRIGHT:
99+
DebugPlaywright.debug(page)
100+
101+
page.goto(url, timeout=timeout)
102+
time.sleep(5)
103+
# NOTE: Use wait_for_load_state instead of sleep?
104+
# page.wait_for_load_state("networkidle", timeout=timeout)
105+
page.wait_for_selector(
106+
"#pdf-preview-ready",
107+
state="attached",
108+
timeout=timeout,
109+
)
110+
111+
pdf_bytes = page.pdf(
112+
display_header_footer=True,
113+
prefer_css_page_size=True,
114+
print_background=True,
115+
footer_template=footer_template,
116+
header_template="<p></p>",
117+
)
118+
finally:
119+
browser.close()
120+
121+
return ContentFile(pdf_bytes)

api/serializers.py

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from rest_framework import serializers
1212

1313
# from api.utils import pdf_exporter
14-
from api.tasks import generate_url
15-
from api.utils import CountryValidator, RegionValidator
14+
from api.tasks import generate_export_pdf
15+
from api.utils import CountryValidator, RegionValidator, generate_eap_export_url
1616
from deployments.models import EmergencyProject, Personnel, PersonnelDeployment
1717
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
18+
from eap.models import EAPRegistration, FullEAP, SimplifiedEAP
1819
from lang.models import String
1920
from lang.serializers import ModelSerializer
2021
from local_units.models import DelegationOffice
@@ -371,12 +372,14 @@ class Admin2Serializer(GeoSerializerMixin, ModelSerializer):
371372
bbox = serializers.SerializerMethodField()
372373
centroid = serializers.SerializerMethodField()
373374
district_id = serializers.IntegerField(source="admin1.id", read_only=True)
375+
district_name = serializers.CharField(source="admin1.name", read_only=True)
374376

375377
class Meta:
376378
model = Admin2
377379
fields = (
378380
"id",
379381
"district_id",
382+
"district_name",
380383
"name",
381384
"code",
382385
"bbox",
@@ -387,10 +390,11 @@ class Meta:
387390

388391
class MiniAdmin2Serializer(ModelSerializer):
389392
district_id = serializers.IntegerField(source="admin1.id", read_only=True)
393+
district_name = serializers.CharField(source="admin1.name", read_only=True)
390394

391395
class Meta:
392396
model = Admin2
393-
fields = ("id", "name", "code", "district_id")
397+
fields = ("id", "name", "code", "district_id", "district_name")
394398

395399

396400
class MiniDistrictSerializer(ModelSerializer):
@@ -2545,6 +2549,13 @@ class ExportSerializer(serializers.ModelSerializer):
25452549
status_display = serializers.CharField(source="get_status_display", read_only=True)
25462550
# NOTE: is_pga is used to determine if the export contains PGA or not
25472551
is_pga = serializers.BooleanField(default=False, required=False, write_only=True)
2552+
# NOTE: diff is used to determine if the export is requested for diff view or not
2553+
# Currently only used for EAP exports
2554+
diff = serializers.BooleanField(default=False, required=False, write_only=True, help_text="Only applicable for EAP exports")
2555+
# NOTE: Version of a EAP export being requested, only applicable for full and simplified EAP exports
2556+
version = serializers.IntegerField(required=False, write_only=True, help_text="Only applicable for EAP exports")
2557+
# NOTE: Only for FUll eap export
2558+
summary = serializers.BooleanField(default=False, required=False, write_only=True, help_text="Only applicable for FUll EAP")
25482559

25492560
class Meta:
25502561
model = Export
@@ -2556,10 +2567,12 @@ def validate_pdf_file(self, pdf_file):
25562567
return pdf_file
25572568

25582569
def create(self, validated_data):
2559-
language = django_get_language()
25602570
export_id = validated_data.get("export_id")
25612571
export_type = validated_data.get("export_type")
25622572
country_id = validated_data.get("per_country")
2573+
version = validated_data.pop("version", None)
2574+
diff = validated_data.pop("diff", False)
2575+
summary = validated_data.pop("summary", False)
25632576
if export_type == Export.ExportType.DREF:
25642577
title = Dref.objects.filter(id=export_id).first().title
25652578
elif export_type == Export.ExportType.OPS_UPDATE:
@@ -2569,17 +2582,67 @@ def create(self, validated_data):
25692582
elif export_type == Export.ExportType.PER:
25702583
overview = Overview.objects.filter(id=export_id).first()
25712584
title = f"{overview.country.name}-preparedness-{overview.get_phase_display()}"
2585+
elif export_type == Export.ExportType.SIMPLIFIED_EAP:
2586+
if version:
2587+
simplified_eap = SimplifiedEAP.objects.filter(
2588+
eap_registration=export_id,
2589+
version=version,
2590+
).first()
2591+
if not simplified_eap:
2592+
raise serializers.ValidationError("No Simplified EAP found for the given EAP Registration ID and version")
2593+
else:
2594+
eap_registration = EAPRegistration.objects.filter(id=export_id).first()
2595+
if not eap_registration:
2596+
raise serializers.ValidationError("No EAP Registration found for the given ID")
2597+
2598+
simplified_eap = eap_registration.latest_simplified_eap
2599+
if not simplified_eap:
2600+
serializers.ValidationError("No Simplified EAP found for the given EAP Registration ID")
2601+
2602+
title = (
2603+
f"{simplified_eap.eap_registration.national_society.name}-{simplified_eap.eap_registration.disaster_type.name}"
2604+
)
2605+
elif export_type == Export.ExportType.FULL_EAP:
2606+
if version:
2607+
full_eap = FullEAP.objects.filter(
2608+
eap_registration=export_id,
2609+
version=version,
2610+
).first()
2611+
if not full_eap:
2612+
raise serializers.ValidationError("No Full EAP found for the given EAP Registration ID and version")
2613+
else:
2614+
eap_registration = EAPRegistration.objects.filter(id=export_id).first()
2615+
if not eap_registration:
2616+
raise serializers.ValidationError("No EAP Registration found for the given ID")
2617+
2618+
full_eap = eap_registration.latest_full_eap
2619+
if not full_eap:
2620+
serializers.ValidationError("No Full EAP found for the given EAP Registration ID")
2621+
2622+
title = f"{full_eap.eap_registration.national_society.name}-{full_eap.eap_registration.disaster_type.name}"
25722623
else:
25732624
title = "Export"
25742625
user = self.context["request"].user
25752626

25762627
if export_type == Export.ExportType.PER:
25772628
validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/countries/{country_id}/{export_type}/{export_id}/export/"
2629+
2630+
elif export_type in [
2631+
Export.ExportType.SIMPLIFIED_EAP,
2632+
Export.ExportType.FULL_EAP,
2633+
]:
2634+
validated_data["url"] = generate_eap_export_url(
2635+
registration_id=export_id,
2636+
version=version,
2637+
diff=diff,
2638+
summary=summary,
2639+
)
2640+
25782641
else:
25792642
validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/{export_type}/{export_id}/export/"
25802643

25812644
# Adding is_pga to the url
2582-
is_pga = validated_data.pop("is_pga")
2645+
is_pga = validated_data.pop("is_pga", False)
25832646
if is_pga:
25842647
validated_data["url"] += "?is_pga=true"
25852648
validated_data["requested_by"] = user
@@ -2589,7 +2652,8 @@ def create(self, validated_data):
25892652
export.requested_at = timezone.now()
25902653
export.save(update_fields=["status", "requested_at"])
25912654

2592-
transaction.on_commit(lambda: generate_url.delay(export.url, export.id, user.id, title, language))
2655+
language = django_get_language()
2656+
transaction.on_commit(lambda: generate_export_pdf.delay(export.id, title, language))
25932657
return export
25942658

25952659
def update(self, instance, validated_data):

0 commit comments

Comments
 (0)