Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
3546efa
feat(eap): Add DevelopmentRegistration EAP model
susilnem Nov 4, 2025
13dfd5f
feat(eap): Add DevelopmentRegistrationEAP Endpoint
susilnem Nov 4, 2025
748b2ca
feat(eap): Add EAP type and status for EAP Registration
susilnem Nov 5, 2025
e19a60b
chore(eap): Remove disaster type and national society filters from admin
susilnem Nov 5, 2025
9472ca5
chore(eap): Add eap enums in global enums
susilnem Nov 5, 2025
28ef776
feat(eap): Add Simplified EAP model
susilnem Nov 5, 2025
86de800
feat(eap): Add Base Model and serializer
susilnem Nov 6, 2025
31768ea
feat(eap): Add simplified model, operational, actions
susilnem Nov 6, 2025
5655b4f
feat(eap): Add test cases for eap registration and simplified
susilnem Nov 7, 2025
40b34ad
feat(eap): Add Simplified Admin, FilterSet, Status update endpoints
susilnem Nov 8, 2025
aeaeb69
feat(eap): Add validations, multiple file upload
susilnem Nov 11, 2025
cb66e21
feat(eap): Add status transition validations and permissions
susilnem Nov 12, 2025
47170b8
feat(eap): Add status transition, timeline and validated budget file
susilnem Nov 13, 2025
c1afbac
feat(eap): Upload review checklist and active-eap endpoint
susilnem Nov 14, 2025
5e83c6a
feat(eap): Add snapshot feature on simplified eap
susilnem Nov 19, 2025
6fe8244
feat(eap): Add snapshot feature and validation checks on status update
susilnem Nov 20, 2025
63bbc59
feat(eap): add simplified eap to global pdf export
sudip-khanal Nov 19, 2025
aa6bc92
feat(eap): Add validation on operation timeframe and time_value
susilnem Nov 25, 2025
dffa6c7
feat(eap): update schema on updating eap file instance
susilnem Nov 25, 2025
1553f35
feat(eap): add full eap model
sudip-khanal Nov 20, 2025
0246766
fix(eap): Update test cases for simplified eap generate pdf
susilnem Nov 26, 2025
94447a2
feat(eap): Update changes on Full EAP
susilnem Nov 21, 2025
3a48919
chore(eap): Update filters on eap and update migration file
susilnem Nov 24, 2025
18bffa0
feat(full_eap): Add snapshot feature and update on active EAPs
susilnem Nov 24, 2025
5d3ab2b
feat(full-eap): Add test cases for full-eap
susilnem Nov 25, 2025
4b74480
feat(eap): Add full eap export pdf
susilnem Nov 26, 2025
424d4b0
feat(eap): Update full eap fields and add new fields
susilnem Nov 26, 2025
9958b96
feat(eap): add test cases for full eap, snapshot, active-eap
susilnem Nov 27, 2025
9c9fc09
Merge pull request #2595 from IFRCGo/feat/add-full-eap-model
susilnem Dec 3, 2025
745575e
chore(assest): Update asset commit head
susilnem Dec 4, 2025
1dc5e87
feat(full-eap): Add new fields on full eap
susilnem Dec 5, 2025
bb0b7f5
feat(full-eap): Add new status and update on status transition
susilnem Dec 10, 2025
1458670
feat(full-eap): Add new field forecast table file
susilnem Dec 10, 2025
03bdc56
chore(eap): Update on active eaps endpoint
susilnem Dec 11, 2025
f9718a5
feat(eap): Add multiple validation checks for files
susilnem Dec 12, 2025
98fffe0
fix(eap): typing issue on eap actiona and source information
susilnem Dec 12, 2025
a9d64c0
Merge pull request #2605 from IFRCGo/feature/add-new-field-full-eap
susilnem Dec 12, 2025
452535c
fix(eap-export): Update Export url for EAP
susilnem Dec 4, 2025
8fdb051
feat(eap): Add diff and version tracking for pdf export
susilnem Dec 5, 2025
c2453c1
feat(eap): Update on Export url for eaps
susilnem Dec 12, 2025
d711ee7
fix(eap): Replace update checklist file to EAPFile
susilnem Dec 15, 2025
0d5c710
Merge pull request #2606 from IFRCGo/fix/export-url-eap
susilnem Dec 15, 2025
c9b98a8
fix(eap): Update export url on eap
susilnem Dec 15, 2025
0b630f1
chore(fulleap): Remove fields from fulleap model (#2614)
susilnem Dec 19, 2025
d4a9264
chore(eap-registration): Update fields on eap registration
susilnem Dec 19, 2025
a313fc2
EAP: Add api to download template files (#2619)
sudip-khanal Dec 29, 2025
3e59f93
feat(eap): Add diff file and summary file for eap
susilnem Jan 5, 2026
2010282
refactor(export): Decoupling pdf export of playwright
susilnem Jan 5, 2026
b60c0b7
feat(eap): Add export file generation and retrigger action on adminpanel
susilnem Jan 6, 2026
7a2d04c
feat(eap): Add previous_id feature on snapshot creation
susilnem Jan 7, 2026
1f1424b
Merge pull request #2623 from IFRCGo/feature/eap-export-pdf-generation
susilnem Jan 8, 2026
ba6e8d3
EAP: email notification setup (#2624)
sudip-khanal Jan 9, 2026
97478ee
fix(eap): Update default values for email environment variables from …
sudip-khanal Jan 12, 2026
db1b45e
chore(eap): Update typings on registration and eaps (#2626)
susilnem Jan 9, 2026
6efbd5d
fix(eap): update validation for full eap
frozenhelium Jan 14, 2026
071fdcf
Merge pull request #2627 from IFRCGo/fix/eap-email-env
susilnem Jan 13, 2026
8f0ba7b
feat(admin2): add filter for multiple ids
frozenhelium Jan 16, 2026
5784e14
Merge pull request #2630 from IFRCGo/fix/update-full-eap-form-validation
susilnem Jan 14, 2026
aa19d77
fix(eap): Squash migrations and cleanup
susilnem Jan 14, 2026
9cea572
Merge pull request #2634 from IFRCGo/feat/admin2-filter-with-multiple-id
susilnem Jan 16, 2026
88fb354
chore(eap): update global file export url, test cases
susilnem Jan 16, 2026
5681011
feat(admin2): add filtering by code on admin2 endpoint
susilnem Jan 19, 2026
a5fb6bd
Merge pull request #2635 from IFRCGo/fix/cleanup-squash-migrations
susilnem Jan 16, 2026
7d3cfb7
Merge pull request #2636 from IFRCGo/feature/add-filter-admin
susilnem Jan 20, 2026
49bf987
chore(migration): merge migrations for api apps
susilnem Jan 23, 2026
5d744a9
feat(eap): add diff PDF to pending-PFA email attachment
sudip-khanal Feb 4, 2026
0ad7275
feat(eap): validation checks only on status transition
susilnem Feb 4, 2026
025ecb2
feat(eap): add share eap functionality
susilnem Jan 28, 2026
cb12465
Merge pull request #2647 from IFRCGo/fix/email-attachment
susilnem Feb 9, 2026
c45c8d3
feat(eap): Remove required fields and add feedbacks
susilnem Feb 27, 2026
1a8cf7d
Merge pull request #2643 from IFRCGo/feature/eap-share-functionality
susilnem Feb 13, 2026
884408b
feat(eap): add new fields and remove status activated
susilnem Mar 16, 2026
a86b79e
chore(eap): update eap validation checks and error messages
susilnem Mar 25, 2026
bdd3229
feat(eap): Add APCode enums for sectors
susilnem Apr 3, 2026
d619f08
feat(eap): add new endpoint for revise and update eap workflow
susilnem Apr 3, 2026
a32239a
feat(eap): add eap options endpoint
susilnem Apr 6, 2026
4d3a293
Merge pull request #2646 from IFRCGo/feat/eap-create-update-validation
susilnem Feb 27, 2026
e82d4b3
feat(eap): update revise workflow with locked feature
susilnem Apr 8, 2026
b2022af
feat(full-eap): add additional attachments for full eap
susilnem Apr 15, 2026
ce3564d
Merge pull request #2689 from IFRCGo/feature/eap-feedbacks
susilnem Apr 16, 2026
52cb83d
fix(eap): update contents on email notifications
susilnem Apr 23, 2026
111774e
Merge pull request #2722 from IFRCGo/fix/eap-issues
sudip-khanal Apr 23, 2026
686b257
fix(email): Skip for empty email on notification
susilnem Apr 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/0231_alter_export_export_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.30 on 2026-04-16 08:35

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0230_alter_districtgeoms_district"),
]

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",
),
),
]
2 changes: 2 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3333,6 +3333,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