Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
628a885
feat(eap): Add DevelopmentRegistration EAP model
susilnem Nov 4, 2025
8ec9f7a
feat(eap): Add DevelopmentRegistrationEAP Endpoint
susilnem Nov 4, 2025
209ea62
feat(eap): Add EAP type and status for EAP Registration
susilnem Nov 5, 2025
97f49b5
chore(eap): Remove disaster type and national society filters from admin
susilnem Nov 5, 2025
fbe8bc6
chore(eap): Add eap enums in global enums
susilnem Nov 5, 2025
9644e07
feat(eap): Add Simplified EAP model
susilnem Nov 5, 2025
eb7e55f
feat(eap): Add Base Model and serializer
susilnem Nov 6, 2025
315e5aa
feat(eap): Add simplified model, operational, actions
susilnem Nov 6, 2025
ce481e3
feat(eap): Add test cases for eap registration and simplified
susilnem Nov 7, 2025
718e21b
feat(eap): Add Simplified Admin, FilterSet, Status update endpoints
susilnem Nov 8, 2025
95fde31
feat(eap): Add validations, multiple file upload
susilnem Nov 11, 2025
6e82751
feat(eap): Add status transition validations and permissions
susilnem Nov 12, 2025
0a794ef
feat(eap): Add status transition, timeline and validated budget file
susilnem Nov 13, 2025
3c824d8
feat(eap): Upload review checklist and active-eap endpoint
susilnem Nov 14, 2025
9a43765
feat(eap): Add snapshot feature on simplified eap
susilnem Nov 19, 2025
e0fc5e7
feat(eap): Add snapshot feature and validation checks on status update
susilnem Nov 20, 2025
05e8871
feat(eap): add simplified eap to global pdf export
sudip-khanal Nov 19, 2025
bd974f6
feat(eap): Add validation on operation timeframe and time_value
susilnem Nov 25, 2025
63dcd5c
feat(eap): update schema on updating eap file instance
susilnem Nov 25, 2025
8e4c24d
feat(eap): add full eap model
sudip-khanal Nov 20, 2025
d9c41c8
fix(eap): Update test cases for simplified eap generate pdf
susilnem Nov 26, 2025
55daae1
feat(eap): Update changes on Full EAP
susilnem Nov 21, 2025
d738eeb
chore(eap): Update filters on eap and update migration file
susilnem Nov 24, 2025
e92427d
feat(full_eap): Add snapshot feature and update on active EAPs
susilnem Nov 24, 2025
8457270
feat(full-eap): Add test cases for full-eap
susilnem Nov 25, 2025
d0a8153
feat(eap): Add full eap export pdf
susilnem Nov 26, 2025
0377b09
feat(eap): Update full eap fields and add new fields
susilnem Nov 26, 2025
3a892b6
feat(eap): add test cases for full eap, snapshot, active-eap
susilnem Nov 27, 2025
79f129c
Merge pull request #2595 from IFRCGo/feat/add-full-eap-model
susilnem Dec 3, 2025
d90747e
chore(assest): Update asset commit head
susilnem Dec 4, 2025
881a6e2
feat(full-eap): Add new fields on full eap
susilnem Dec 5, 2025
f24c27a
feat(full-eap): Add new status and update on status transition
susilnem Dec 10, 2025
6fdb51e
feat(full-eap): Add new field forecast table file
susilnem Dec 10, 2025
e1c9808
chore(eap): Update on active eaps endpoint
susilnem Dec 11, 2025
b5bbe5e
feat(eap): Add multiple validation checks for files
susilnem Dec 12, 2025
b3f0034
fix(eap): typing issue on eap actiona and source information
susilnem Dec 12, 2025
174316a
fix(eap-export): Update Export url for EAP
susilnem Dec 4, 2025
9469205
feat(eap): Add diff and version tracking for pdf export
susilnem Dec 5, 2025
4b48094
feat(eap): Update on Export url for eaps
susilnem Dec 12, 2025
dba4c7b
fix(eap): Replace update checklist file to EAPFile
susilnem Dec 15, 2025
563c855
Merge pull request #2605 from IFRCGo/feature/add-new-field-full-eap
susilnem Dec 12, 2025
411b63b
Merge pull request #2606 from IFRCGo/fix/export-url-eap
susilnem Dec 15, 2025
e14b273
fix(eap): Update export url on eap
susilnem Dec 15, 2025
3d4c252
chore(fulleap): Remove fields from fulleap model (#2614)
susilnem Dec 19, 2025
87b0c39
feat(eap): Add diff file and summary file for eap
susilnem Jan 5, 2026
2e9afa2
chore(eap-registration): Update fields on eap registration
susilnem Dec 19, 2025
afe5d83
refactor(export): Decoupling pdf export of playwright
susilnem Jan 5, 2026
9a5b87b
EAP: Add api to download template files (#2619)
sudip-khanal Dec 29, 2025
d498b0f
feat(eap): Add export file generation and retrigger action on adminpanel
susilnem Jan 6, 2026
da9c569
feat(eap): Add previous_id feature on snapshot creation
susilnem Jan 7, 2026
fd7f8db
Merge pull request #2623 from IFRCGo/feature/eap-export-pdf-generation
susilnem Jan 8, 2026
a7ecd26
EAP: email notification setup (#2624)
sudip-khanal Jan 9, 2026
7d13e49
fix(eap): Update default values for email environment variables from …
sudip-khanal Jan 12, 2026
6a560a4
chore(eap): Update typings on registration and eaps (#2626)
susilnem Jan 9, 2026
83a2f57
fix(eap): update validation for full eap
frozenhelium Jan 14, 2026
968ce14
Merge pull request #2627 from IFRCGo/fix/eap-email-env
susilnem Jan 13, 2026
0a68a67
feat(admin2): add filter for multiple ids
frozenhelium Jan 16, 2026
83359af
Merge pull request #2630 from IFRCGo/fix/update-full-eap-form-validation
susilnem Jan 14, 2026
9651d00
fix(eap): Squash migrations and cleanup
susilnem Jan 14, 2026
ba605be
Merge pull request #2634 from IFRCGo/feat/admin2-filter-with-multiple-id
susilnem Jan 16, 2026
80cfde7
chore(eap): update global file export url, test cases
susilnem Jan 16, 2026
733ca8e
feat(admin2): add filtering by code on admin2 endpoint
susilnem Jan 19, 2026
d7b0430
Merge pull request #2635 from IFRCGo/fix/cleanup-squash-migrations
susilnem Jan 16, 2026
95cef89
Merge pull request #2636 from IFRCGo/feature/add-filter-admin
susilnem Jan 20, 2026
2ae7923
chore(migration): merge migrations for api apps
susilnem Jan 23, 2026
ba7698a
feat(eap): add diff PDF to pending-PFA email attachment
sudip-khanal Feb 4, 2026
9f258be
feat(eap): validation checks only on status transition
susilnem Feb 4, 2026
2b99205
feat(eap): add share eap functionality
susilnem Jan 28, 2026
9915892
Merge pull request #2647 from IFRCGo/fix/email-attachment
susilnem Feb 9, 2026
4fe370c
feat(eap): Remove required fields and add feedbacks
susilnem Feb 27, 2026
d8ae80d
Merge pull request #2643 from IFRCGo/feature/eap-share-functionality
susilnem Feb 13, 2026
fd44474
feat(eap): add new fields and remove status activated
susilnem Mar 16, 2026
ffade8f
chore(eap): update eap validation checks and error messages
susilnem Mar 25, 2026
c482c7f
feat(eap): Add APCode enums for sectors
susilnem Apr 3, 2026
00d1080
feat(eap): add new endpoint for revise and update eap workflow
susilnem Apr 3, 2026
bfa6827
feat(eap): add eap options endpoint
susilnem Apr 6, 2026
7f27627
Merge pull request #2646 from IFRCGo/feat/eap-create-update-validation
susilnem Feb 27, 2026
5415c65
feat(eap): update revise workflow with locked feature
susilnem Apr 8, 2026
5a46448
feat(full-eap): add additional attachments for full eap
susilnem Apr 15, 2026
30699f8
fix(eap): update contents on email notifications
susilnem Apr 23, 2026
1d7fc31
Merge pull request #2689 from IFRCGo/feature/eap-feedbacks
susilnem Apr 16, 2026
25aab17
Merge pull request #2722 from IFRCGo/fix/eap-issues
sudip-khanal Apr 23, 2026
34cfe4f
fix(email): Skip for empty email on notification
susilnem Apr 23, 2026
b21c351
chore(asset): rebase, squash and update asset pointer
susilnem Apr 27, 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-27 06:11

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