Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
91b406a
feat(eap): Add DevelopmentRegistration EAP model
susilnem Nov 4, 2025
5a90fa4
feat(eap): Add DevelopmentRegistrationEAP Endpoint
susilnem Nov 4, 2025
a75ab93
feat(eap): Add EAP type and status for EAP Registration
susilnem Nov 5, 2025
0335580
chore(eap): Remove disaster type and national society filters from admin
susilnem Nov 5, 2025
fde9935
chore(eap): Add eap enums in global enums
susilnem Nov 5, 2025
e47198d
feat(eap): Add Simplified EAP model
susilnem Nov 5, 2025
f1e5dae
feat(eap): Add Base Model and serializer
susilnem Nov 6, 2025
a77a9d4
feat(eap): Add simplified model, operational, actions
susilnem Nov 6, 2025
764e373
feat(eap): Add test cases for eap registration and simplified
susilnem Nov 7, 2025
ae02635
feat(eap): Add Simplified Admin, FilterSet, Status update endpoints
susilnem Nov 8, 2025
e17859e
feat(eap): Add validations, multiple file upload
susilnem Nov 11, 2025
2b0016c
feat(eap): Add status transition validations and permissions
susilnem Nov 12, 2025
7ad279f
feat(eap): Add status transition, timeline and validated budget file
susilnem Nov 13, 2025
7fd17a1
feat(eap): Upload review checklist and active-eap endpoint
susilnem Nov 14, 2025
8f8ee27
feat(eap): Add snapshot feature on simplified eap
susilnem Nov 19, 2025
fdb8726
feat(eap): Add snapshot feature and validation checks on status update
susilnem Nov 20, 2025
406724d
feat(eap): add simplified eap to global pdf export
sudip-khanal Nov 19, 2025
21aa7e2
feat(eap): Add validation on operation timeframe and time_value
susilnem Nov 25, 2025
d13e259
feat(eap): add full eap model
sudip-khanal Nov 20, 2025
0706a8c
feat(eap): Update changes on Full EAP
susilnem Nov 21, 2025
002f43b
feat(eap): update schema on updating eap file instance
susilnem Nov 25, 2025
1a9ba24
chore(eap): Update filters on eap and update migration file
susilnem Nov 24, 2025
39ce665
fix(eap): Update test cases for simplified eap generate pdf
susilnem Nov 26, 2025
41f6728
feat(full_eap): Add snapshot feature and update on active EAPs
susilnem Nov 24, 2025
583f6b9
feat(full-eap): Add test cases for full-eap
susilnem Nov 25, 2025
1821d24
feat(eap): Add full eap export pdf
susilnem Nov 26, 2025
d4a9c65
feat(eap): Update full eap fields and add new fields
susilnem Nov 26, 2025
2fb2bfc
feat(eap): add test cases for full eap, snapshot, active-eap
susilnem Nov 27, 2025
362c17e
Merge pull request #2595 from IFRCGo/feat/add-full-eap-model
susilnem Dec 3, 2025
0c7d6d8
feat(full-eap): Add new fields on full eap
susilnem Dec 5, 2025
6db57d3
feat(full-eap): Add new status and update on status transition
susilnem Dec 10, 2025
1ccb096
feat(full-eap): Add new field forecast table file
susilnem Dec 10, 2025
3c4c8f2
chore(eap): Update on active eaps endpoint
susilnem Dec 11, 2025
7104a7c
feat(eap): Add multiple validation checks for files
susilnem Dec 12, 2025
3e37af1
chore(assest): Update asset commit head
susilnem Dec 4, 2025
336cb6a
fix(eap-export): Update Export url for EAP
susilnem Dec 4, 2025
5f3e2c6
feat(eap): Add diff and version tracking for pdf export
susilnem Dec 5, 2025
8050054
feat(eap): Update on Export url for eaps
susilnem Dec 12, 2025
f892aaf
fix(eap): typing issue on eap actiona and source information
susilnem Dec 12, 2025
d965a26
fix(eap): Replace update checklist file to EAPFile
susilnem Dec 15, 2025
f7dffef
Merge pull request #2605 from IFRCGo/feature/add-new-field-full-eap
susilnem Dec 12, 2025
17729ee
Merge pull request #2606 from IFRCGo/fix/export-url-eap
susilnem Dec 15, 2025
a572696
fix(eap): Update export url on eap
susilnem Dec 15, 2025
57b22c7
chore(fulleap): Remove fields from fulleap model (#2614)
susilnem Dec 19, 2025
0d07d4e
chore(eap-registration): Update fields on eap registration
susilnem Dec 19, 2025
17fe2e5
feat(eap): Add diff file and summary file for eap
susilnem Jan 5, 2026
e7491af
EAP: Add api to download template files (#2619)
sudip-khanal Dec 29, 2025
05ff56e
refactor(export): Decoupling pdf export of playwright
susilnem Jan 5, 2026
cbbf8c3
feat(eap): Add export file generation and retrigger action on adminpanel
susilnem Jan 6, 2026
3097fd3
feat(eap): Add previous_id feature on snapshot creation
susilnem Jan 7, 2026
c52f948
Merge pull request #2623 from IFRCGo/feature/eap-export-pdf-generation
susilnem Jan 8, 2026
309fffe
EAP: email notification setup (#2624)
sudip-khanal Jan 9, 2026
37634a4
fix(eap): Update default values for email environment variables from …
sudip-khanal Jan 12, 2026
3a2c0c2
chore(eap): Update typings on registration and eaps (#2626)
susilnem Jan 9, 2026
4f1c6ca
fix(eap): update validation for full eap
frozenhelium Jan 14, 2026
da61eb8
Merge pull request #2627 from IFRCGo/fix/eap-email-env
susilnem Jan 13, 2026
7f44dfe
feat(admin2): add filter for multiple ids
frozenhelium Jan 16, 2026
e71f65b
Merge pull request #2630 from IFRCGo/fix/update-full-eap-form-validation
susilnem Jan 14, 2026
9ede2e6
fix(eap): Squash migrations and cleanup
susilnem Jan 14, 2026
30e2f9b
chore(eap): update global file export url, test cases
susilnem Jan 16, 2026
9432a6f
Merge pull request #2634 from IFRCGo/feat/admin2-filter-with-multiple-id
susilnem Jan 16, 2026
cdec93e
feat(admin2): add filtering by code on admin2 endpoint
susilnem Jan 19, 2026
70c475b
Merge pull request #2635 from IFRCGo/fix/cleanup-squash-migrations
susilnem Jan 16, 2026
be7b3cb
Merge pull request #2636 from IFRCGo/feature/add-filter-admin
susilnem Jan 20, 2026
4f4b133
chore(migration): merge migrations for api apps
susilnem Jan 23, 2026
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
1 change: 1 addition & 0 deletions api/filter_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class Meta:
model = Admin2
fields = {
"id": ("exact", "in"),
"code": ("exact", "in"),
"admin1": ("exact", "in"),
"admin1__country": ("exact", "in"),
"admin1__country__iso3": ("exact", "in"),
Expand Down
29 changes: 29 additions & 0 deletions api/migrations/0227_alter_export_export_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.26 on 2026-01-14 08:51

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0226_nsdinitiativescategory_and_more"),
]

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",
),
),
]
14 changes: 14 additions & 0 deletions api/migrations/0228_merge_20260123_0806.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.2.26 on 2026-01-23 08:06

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0227_alter_eventseveritylevelhistory_options'),
('api', '0227_alter_export_export_type'),
]

operations = [
]
2 changes: 2 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2564,6 +2564,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", _("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)
Expand Down
121 changes: 121 additions & 0 deletions api/playwright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import json
import pathlib
import tempfile
import time

from django.conf import settings
from django.core.files.base import ContentFile
from playwright.sync_api import sync_playwright

from .utils import DebugPlaywright

footer_template = """
<div class="footer" style="width: 100%;font-size: 8px;color: #FEFEFE; bottom: 10px; position: absolute;">
<div style="float: left; margin-top: 10px; margin-left: 40px;">
Page <span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
<div style="float: right; margin-right: 40px;">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 89.652 89.654"
height="48"
width="48"
>
<path
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"
class="st1"
fill="#F5333F"
/>
<path
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"
class="st1"
fill="#F5333F"
/>
</svg>
</div>
</div>
""" # noqa


def build_storage_state(tmp_dir, user, token, language="en"):
temp_file = pathlib.Path(tmp_dir, "storage_state.json")
temp_file.touch()

state = {
"origins": [
{
"origin": settings.GO_WEB_INTERNAL_URL + "/",
"localStorage": [
{
"name": "user",
"value": json.dumps(
{
"id": user.id,
"username": user.username,
"firstName": user.first_name,
"lastName": user.last_name,
"token": token.key,
}
),
},
{"name": "language", "value": json.dumps(language)},
],
}
]
}
with open(temp_file, "w") as f:
json.dump(state, f)
return temp_file


def render_pdf_from_url(
*,
url: str,
user,
token,
language: str = "en",
timeout: int = 300_000,
):
"""
Renders a URL to PDF using Playwright.
Returns a Django ContentFile.
"""
with tempfile.TemporaryDirectory() as tmp_dir:
storage_state = build_storage_state(
tmp_dir=tmp_dir,
user=user,
token=token,
language=language,
)

with sync_playwright() as playwright:
browser = playwright.chromium.connect(settings.PLAYWRIGHT_SERVER_URL)

try:
context = browser.new_context(storage_state=storage_state)
page = context.new_page()

if settings.DEBUG_PLAYWRIGHT:
DebugPlaywright.debug(page)

page.goto(url, timeout=timeout)
time.sleep(5)
# NOTE: Use wait_for_load_state instead of sleep?
# page.wait_for_load_state("networkidle", timeout=timeout)
page.wait_for_selector(
"#pdf-preview-ready",
state="attached",
timeout=timeout,
)

pdf_bytes = page.pdf(
display_header_footer=True,
prefer_css_page_size=True,
print_background=True,
footer_template=footer_template,
header_template="<p></p>",
)
finally:
browser.close()

return ContentFile(pdf_bytes)
76 changes: 70 additions & 6 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
from rest_framework import serializers

# from api.utils import pdf_exporter
from api.tasks import generate_url
from api.utils import CountryValidator, RegionValidator
from api.tasks import generate_export_pdf
from api.utils import CountryValidator, RegionValidator, generate_eap_export_url
from deployments.models import EmergencyProject, Personnel, PersonnelDeployment
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
from eap.models import EAPRegistration, FullEAP, SimplifiedEAP
from lang.models import String
from lang.serializers import ModelSerializer
from local_units.models import DelegationOffice
Expand Down Expand Up @@ -371,12 +372,14 @@ class Admin2Serializer(GeoSerializerMixin, ModelSerializer):
bbox = serializers.SerializerMethodField()
centroid = serializers.SerializerMethodField()
district_id = serializers.IntegerField(source="admin1.id", read_only=True)
district_name = serializers.CharField(source="admin1.name", read_only=True)

class Meta:
model = Admin2
fields = (
"id",
"district_id",
"district_name",
"name",
"code",
"bbox",
Expand All @@ -387,10 +390,11 @@ class Meta:

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

class Meta:
model = Admin2
fields = ("id", "name", "code", "district_id")
fields = ("id", "name", "code", "district_id", "district_name")


class MiniDistrictSerializer(ModelSerializer):
Expand Down Expand Up @@ -2545,6 +2549,13 @@ 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, help_text="Only applicable for EAP exports")
# NOTE: Version of a EAP export being requested, only applicable for full and simplified EAP exports
version = serializers.IntegerField(required=False, write_only=True, help_text="Only applicable for EAP exports")
# NOTE: Only for FUll eap export
summary = serializers.BooleanField(default=False, required=False, write_only=True, help_text="Only applicable for FUll EAP")

class Meta:
model = Export
Expand All @@ -2556,10 +2567,12 @@ def validate_pdf_file(self, pdf_file):
return pdf_file

def create(self, validated_data):
language = django_get_language()
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)
diff = validated_data.pop("diff", False)
summary = validated_data.pop("summary", False)
if export_type == Export.ExportType.DREF:
title = Dref.objects.filter(id=export_id).first().title
elif export_type == Export.ExportType.OPS_UPDATE:
Expand All @@ -2569,17 +2582,67 @@ def create(self, validated_data):
elif export_type == Export.ExportType.PER:
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:
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:
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"
user = self.context["request"].user

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"] = generate_eap_export_url(
registration_id=export_id,
version=version,
diff=diff,
summary=summary,
)

else:
validated_data["url"] = f"{settings.GO_WEB_INTERNAL_URL}/{export_type}/{export_id}/export/"

# Adding is_pga to the url
is_pga = validated_data.pop("is_pga")
is_pga = validated_data.pop("is_pga", False)
if is_pga:
validated_data["url"] += "?is_pga=true"
validated_data["requested_by"] = user
Expand All @@ -2589,7 +2652,8 @@ def create(self, validated_data):
export.requested_at = timezone.now()
export.save(update_fields=["status", "requested_at"])

transaction.on_commit(lambda: generate_url.delay(export.url, export.id, user.id, title, language))
language = django_get_language()
transaction.on_commit(lambda: generate_export_pdf.delay(export.id, title, language))
return export

def update(self, instance, validated_data):
Expand Down
Loading
Loading