Skip to content

Commit 52cb83d

Browse files
committed
fix(eap): update contents on email notifications
- Update export and diff file generation logic - Add validation check for time value - Update on utils email context - update test cases for notifications - add ns contact title on email and dev preview
1 parent ce3564d commit 52cb83d

15 files changed

Lines changed: 165 additions & 152 deletions

assets

eap/dev_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def get(self, request):
5656
"disaster_type": "Flood",
5757
"total_budget": "250,000 CHF",
5858
"ns_contact_name": "Test Ns Contact name",
59+
"ns_contact_title": "Programme Manager",
5960
"ns_contact_email": "test.Ns@gmail.com",
6061
"ns_contact_phone": "+977-9800000000",
6162
},

eap/serializers.py

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import typing
22
from datetime import timedelta
33

4-
from celery import group
4+
from celery import chain, group
55
from django.conf import settings
66
from django.contrib.auth.models import User
77
from django.db import transaction
@@ -357,7 +357,7 @@ class OperationActivitySerializer(
357357
)
358358
timeframe_display = serializers.CharField(source="get_timeframe_display", read_only=True)
359359
time_value = serializers.ListField(
360-
child=serializers.IntegerField(),
360+
child=serializers.IntegerField(required=True),
361361
required=True,
362362
)
363363

@@ -371,6 +371,9 @@ def validate(self, validated_data: dict[str, typing.Any]) -> dict[str, typing.An
371371
timeframe = validated_data["timeframe"]
372372
time_value = validated_data["time_value"]
373373

374+
if time_value is None or len(time_value) == 0:
375+
raise serializers.ValidationError({"time_value": gettext("time_value is required and cannot be empty.")})
376+
374377
allowed_values = ALLOWED_MAP_TIMEFRAMES_VALUE.get(timeframe, [])
375378
invalid_values = [value for value in time_value if value not in allowed_values]
376379

@@ -880,7 +883,7 @@ def create(self, validated_data: dict[str, typing.Any]):
880883
class EAPStatusSerializer(BaseEAPSerializer):
881884
status_display = serializers.CharField(source="get_status_display", read_only=True)
882885
# NOTE: Only required when changing status to NS Addressing Comments
883-
review_checklist_file = serializers.FileField(required=False)
886+
review_checklist_file = serializers.FileField(required=False, write_only=True)
884887

885888
class Meta:
886889
model = EAPRegistration
@@ -924,23 +927,9 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t
924927
if self.instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP:
925928
self.instance.latest_simplified_eap.is_locked = True
926929
self.instance.latest_simplified_eap.save(update_fields=["is_locked"])
927-
# NOTE: Generating export PDF asynchronously
928-
transaction.on_commit(
929-
lambda: generate_export_eap_pdf.delay(
930-
eap_registration_id=self.instance.id,
931-
version=self.instance.latest_simplified_eap.version,
932-
)
933-
)
934930
else:
935931
self.instance.latest_full_eap.is_locked = True
936932
self.instance.latest_full_eap.save(update_fields=["is_locked"])
937-
# NOTE: Generate export PDF asynchronously
938-
transaction.on_commit(
939-
lambda: generate_export_eap_pdf.delay(
940-
eap_registration_id=self.instance.id,
941-
version=self.instance.latest_full_eap.version,
942-
)
943-
)
944933

945934
# NOTE: IFRC Admins should be able to transition from TECHNICALLY_VALIDATED
946935
# to NS_ADDRESSING_COMMENTS to allow NS users to update their EAP changes after validated budget has been set.
@@ -1029,21 +1018,6 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t
10291018
# Lock the latest eap
10301019
self.instance.latest_simplified_eap.is_locked = True
10311020
self.instance.latest_simplified_eap.save(update_fields=["is_locked"])
1032-
1033-
# Generating PDFs asynchronously
1034-
transaction.on_commit(
1035-
lambda: group(
1036-
generate_export_eap_pdf.s(
1037-
eap_registration_id=self.instance.id,
1038-
version=self.instance.latest_simplified_eap.version,
1039-
),
1040-
generate_export_diff_pdf.s(
1041-
eap_registration_id=self.instance.id,
1042-
version=self.instance.latest_simplified_eap.version,
1043-
),
1044-
).apply_async()
1045-
)
1046-
10471021
else:
10481022
if self.instance.latest_full_eap.is_locked:
10491023
raise serializers.ValidationError(
@@ -1065,20 +1039,6 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t
10651039
self.instance.latest_full_eap.is_locked = True
10661040
self.instance.latest_full_eap.save(update_fields=["is_locked"])
10671041

1068-
# Generating PDFs asynchronously
1069-
transaction.on_commit(
1070-
lambda: group(
1071-
generate_export_eap_pdf.s(
1072-
eap_registration_id=self.instance.id,
1073-
version=self.instance.latest_full_eap.version,
1074-
),
1075-
generate_export_diff_pdf.s(
1076-
eap_registration_id=self.instance.id,
1077-
version=self.instance.latest_full_eap.version,
1078-
),
1079-
).apply_async()
1080-
)
1081-
10821042
elif (current_status, new_status) == (
10831043
EAPRegistration.Status.TECHNICALLY_VALIDATED,
10841044
EAPRegistration.Status.PENDING_PFA,
@@ -1102,10 +1062,6 @@ def _validate_status(self, validated_data: dict[str, typing.Any]) -> dict[str, t
11021062
]
11031063
)
11041064

1105-
# Generate summary eap for full eap
1106-
if self.instance.get_eap_type_enum == EAPType.FULL_EAP:
1107-
transaction.on_commit(lambda: generate_eap_summary_pdf.delay(self.instance.id))
1108-
11091065
elif (current_status, new_status) == (
11101066
EAPRegistration.Status.PENDING_PFA,
11111067
EAPRegistration.Status.APPROVED,
@@ -1152,7 +1108,18 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any
11521108
EAPRegistration.Status.UNDER_DEVELOPMENT,
11531109
EAPRegistration.Status.UNDER_REVIEW,
11541110
):
1155-
transaction.on_commit(lambda: send_new_eap_submission_email.delay(eap_registration_id))
1111+
# NOTE: Generating export pdf and sending email to IFRC at the first submission to under review.
1112+
latest_eap = (
1113+
instance.latest_simplified_eap
1114+
if instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP
1115+
else instance.latest_full_eap
1116+
)
1117+
transaction.on_commit(
1118+
lambda: chain(
1119+
generate_export_eap_pdf.s(eap_registration_id, latest_eap.version),
1120+
send_new_eap_submission_email.si(eap_registration_id),
1121+
).apply_async()
1122+
)
11561123

11571124
elif (old_status, new_status) in [
11581125
(EAPRegistration.Status.UNDER_REVIEW, EAPRegistration.Status.NS_ADDRESSING_COMMENTS),
@@ -1205,7 +1172,18 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any
12051172
EAPRegistration.Status.NS_ADDRESSING_COMMENTS,
12061173
EAPRegistration.Status.UNDER_REVIEW,
12071174
):
1208-
transaction.on_commit(lambda: send_eap_resubmission_email.delay(eap_registration_id))
1175+
# NOTE: Generating diff pdf and sending email to IFRC after NS resubmission.
1176+
latest_eap = (
1177+
instance.latest_simplified_eap
1178+
if instance.get_eap_type_enum == EAPType.SIMPLIFIED_EAP
1179+
else instance.latest_full_eap
1180+
)
1181+
transaction.on_commit(
1182+
lambda: chain(
1183+
generate_export_diff_pdf.s(eap_registration_id, latest_eap.version),
1184+
send_eap_resubmission_email.si(eap_registration_id),
1185+
).apply_async()
1186+
)
12091187

12101188
elif (old_status, new_status) == (
12111189
EAPRegistration.Status.UNDER_REVIEW,
@@ -1217,7 +1195,23 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any
12171195
EAPRegistration.Status.TECHNICALLY_VALIDATED,
12181196
EAPRegistration.Status.PENDING_PFA,
12191197
):
1220-
transaction.on_commit(lambda: send_pending_pfa_email.delay(eap_registration_id))
1198+
# NOTE: Generating diff pdf and summary pdf (for full eap) and sending email to PFA after technical validation.
1199+
is_full_eap = instance.get_eap_type_enum == EAPType.FULL_EAP
1200+
version = instance.latest_simplified_eap.version if not is_full_eap else instance.latest_full_eap.version
1201+
1202+
tasks = [
1203+
generate_export_diff_pdf.s(eap_registration_id, version),
1204+
]
1205+
1206+
if is_full_eap:
1207+
tasks.append(generate_eap_summary_pdf.s(eap_registration_id))
1208+
1209+
transaction.on_commit(
1210+
lambda: chain(
1211+
group(tasks),
1212+
send_pending_pfa_email.si(eap_registration_id),
1213+
).apply_async()
1214+
)
12211215

12221216
elif (old_status, new_status) == (
12231217
EAPRegistration.Status.PENDING_PFA,

eap/tasks.py

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,6 @@ def send_new_eap_submission_email(eap_registration_id: int):
212212
else:
213213
latest_eap = instance.latest_full_eap
214214

215-
if not latest_eap.export_file:
216-
generate_export_eap_pdf(
217-
eap_registration_id=instance.id,
218-
version=latest_eap.version,
219-
)
220215
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
221216

222217
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
@@ -253,7 +248,7 @@ def send_new_eap_submission_email(eap_registration_id: int):
253248

254249
@shared_task
255250
def send_feedback_email(eap_registration_id: int):
256-
instance = EAPRegistration.objects.filter(id=eap_registration_id).first()
251+
instance: EAPRegistration | None = EAPRegistration.objects.filter(id=eap_registration_id).first()
257252
if not instance:
258253
return None
259254

@@ -286,7 +281,7 @@ def send_feedback_email(eap_registration_id: int):
286281
email_context = get_eap_email_context(instance)
287282
email_subject = (
288283
f"[DREF {instance.get_eap_type_display()} FEEDBACK] "
289-
f"{instance.country} {instance.disaster_type} TO THE {instance.national_society}"
284+
f"{instance.country} {instance.disaster_type} TO THE {instance.national_society.society_name}"
290285
)
291286
email_body = render_to_string("email/eap/feedback_to_national_society.html", email_context)
292287
email_type = "Feedback to the National Society"
@@ -303,7 +298,6 @@ def send_feedback_email(eap_registration_id: int):
303298

304299
@shared_task
305300
def send_eap_resubmission_email(eap_registration_id: int):
306-
307301
instance = EAPRegistration.objects.filter(id=eap_registration_id).first()
308302
if not instance:
309303
return None
@@ -312,14 +306,7 @@ def send_eap_resubmission_email(eap_registration_id: int):
312306
else:
313307
latest_eap = instance.latest_full_eap
314308

315-
if not latest_eap.diff_file:
316-
generate_export_diff_pdf(
317-
eap_registration_id=instance.id,
318-
version=latest_eap.version,
319-
)
320-
321309
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
322-
323310
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)
324311

325312
recipients = [
@@ -351,13 +338,11 @@ def send_eap_resubmission_email(eap_registration_id: int):
351338
mailtype=email_type,
352339
cc_recipients=cc_recipients,
353340
)
354-
355341
return True
356342

357343

358344
@shared_task
359345
def send_feedback_email_for_resubmitted_eap(eap_registration_id: int):
360-
361346
instance = EAPRegistration.objects.filter(id=eap_registration_id).first()
362347
if not instance:
363348
return None
@@ -388,7 +373,7 @@ def send_feedback_email_for_resubmitted_eap(eap_registration_id: int):
388373
email_context = get_eap_email_context(instance)
389374
email_subject = (
390375
f"[DREF {instance.get_eap_type_display()} FEEDBACK] "
391-
f"{instance.country} {instance.disaster_type} version {latest_eap.version} TO {instance.national_society}"
376+
f"{instance.country} {instance.disaster_type} version {latest_eap.version} TO {instance.national_society.society_name}"
392377
)
393378
email_body = render_to_string("email/eap/feedback_to_revised_eap.html", email_context)
394379
email_type = "Feedback to the National Society"
@@ -458,18 +443,6 @@ def send_pending_pfa_email(eap_registration_id: int):
458443
is_full_eap = instance.get_eap_type_enum == EAPType.FULL_EAP
459444

460445
latest_eap = instance.latest_full_eap if is_full_eap else instance.latest_simplified_eap
461-
462-
if not latest_eap.diff_file:
463-
generate_export_diff_pdf(
464-
eap_registration_id=instance.id,
465-
version=latest_eap.version,
466-
)
467-
468-
if is_full_eap and not instance.summary_file:
469-
generate_eap_summary_pdf(
470-
eap_registration_id=instance.id,
471-
)
472-
473446
partner_ns_emails = list(latest_eap.partner_contacts.values_list("email", flat=True))
474447

475448
regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region)

eap/test_views.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,28 +1790,22 @@ def test_status_transition(self):
17901790
response = self.client.patch(url, update_data, format="json")
17911791
self.assertEqual(response.status_code, 400, response.data)
17921792

1793-
@mock.patch("eap.serializers.generate_export_eap_pdf")
1793+
@mock.patch("eap.serializers.chain")
17941794
@mock.patch("eap.serializers.group")
1795-
@mock.patch("eap.serializers.send_new_eap_submission_email")
17961795
@mock.patch("eap.serializers.send_feedback_email")
1797-
@mock.patch("eap.serializers.send_eap_resubmission_email")
17981796
@mock.patch("eap.serializers.send_technical_validation_email")
17991797
@mock.patch("eap.serializers.send_feedback_email_for_resubmitted_eap")
1800-
@mock.patch("eap.serializers.send_pending_pfa_email")
18011798
@mock.patch("eap.serializers.send_approved_email")
18021799
def test_status_transitions_trigger_email(
18031800
self,
18041801
send_approved_email,
1805-
send_pending_pfa_email,
18061802
send_feedback_email_for_resubmitted_eap,
18071803
send_technical_validation_email,
1808-
send_eap_resubmission_email,
18091804
send_feedback_email,
1810-
send_new_eap_submission_email,
18111805
mock_group,
1812-
generate_export_eap_pdf,
1806+
mock_chain,
18131807
):
1814-
1808+
mock_chain.return_value.apply_async = mock.Mock()
18151809
# Create permissions
18161810
management.call_command("make_permissions")
18171811

@@ -1872,13 +1866,13 @@ def test_status_transitions_trigger_email(
18721866
with self.capture_on_commit_callbacks(execute=True):
18731867
response = self.client.post(url, data, format="json")
18741868
self.assert_200(response)
1869+
1870+
self.assertTrue(mock_chain.called)
1871+
self.assertTrue(mock_chain.return_value.apply_async.called)
1872+
mock_chain.reset_mock()
1873+
18751874
eap_registration.refresh_from_db()
18761875
self.assertEqual(response.data["status"], EAPStatus.UNDER_REVIEW)
1877-
generate_export_eap_pdf.delay.assert_called_once_with(
1878-
eap_registration_id=eap_registration.id, version=simplified_eap.version
1879-
)
1880-
send_new_eap_submission_email.delay.assert_called_once_with(eap_registration.id)
1881-
send_new_eap_submission_email.delay.reset_mock()
18821876

18831877
# UNDER_REVIEW -> NS_ADDRESSING_COMMENTS
18841878
data = {
@@ -1947,12 +1941,9 @@ def test_status_transitions_trigger_email(
19471941
eap_registration.refresh_from_db()
19481942
self.assertEqual(response.data["status"], EAPStatus.UNDER_REVIEW)
19491943

1950-
# NOTE: Check that two signatures are created
1951-
mock_group.assert_called_once()
1952-
self.assertEqual(len(mock_group.call_args.args), 2)
1953-
1954-
send_eap_resubmission_email.delay.assert_called_once_with(eap_registration.id)
1955-
send_eap_resubmission_email.delay.reset_mock()
1944+
self.assertTrue(mock_chain.called)
1945+
self.assertTrue(mock_chain.return_value.apply_async.called)
1946+
mock_chain.reset_mock()
19561947

19571948
# AGAIN NOTE: Transition to NS_ADDRESSING_COMMENTS
19581949
# UNDER_REVIEW -> NS_ADDRESSING_COMMENTS
@@ -2017,9 +2008,10 @@ def test_status_transitions_trigger_email(
20172008
self.assert_200(response)
20182009
eap_registration.refresh_from_db()
20192010
self.assertEqual(response.data["status"], EAPStatus.UNDER_REVIEW)
2020-
self.assertTrue(mock_group.called)
2021-
send_eap_resubmission_email.delay.assert_called_once_with(eap_registration.id)
2022-
send_eap_resubmission_email.delay.reset_mock()
2011+
2012+
self.assertTrue(mock_chain.called)
2013+
self.assertTrue(mock_chain.return_value.apply_async.called)
2014+
mock_chain.reset_mock()
20232015

20242016
# Transition UNDER_REVIEW -> TECHNICALLY_VALIDATED
20252017
data = {"status": EAPStatus.TECHNICALLY_VALIDATED}
@@ -2096,9 +2088,10 @@ def test_status_transitions_trigger_email(
20962088
self.assert_200(response)
20972089
eap_registration.refresh_from_db()
20982090
self.assertEqual(response.data["status"], EAPStatus.UNDER_REVIEW)
2099-
self.assertTrue(mock_group.called)
2100-
send_eap_resubmission_email.delay.assert_called_once_with(eap_registration.id)
2101-
send_eap_resubmission_email.delay.reset_mock()
2091+
2092+
self.assertTrue(mock_chain.called)
2093+
self.assertTrue(mock_chain.return_value.apply_async.called)
2094+
mock_chain.reset_mock()
21022095

21032096
# Again Transition UNDER_REVIEW -> TECHNICALLY_VALIDATED
21042097
data = {"status": EAPStatus.TECHNICALLY_VALIDATED}
@@ -2132,7 +2125,16 @@ def test_status_transitions_trigger_email(
21322125
self.assert_200(response)
21332126
self.assertEqual(response.data["status"], EAPStatus.PENDING_PFA)
21342127
eap_registration.refresh_from_db()
2135-
send_pending_pfa_email.delay.assert_called_once_with(eap_registration.id)
2128+
2129+
self.assertTrue(mock_chain.called)
2130+
self.assertTrue(mock_group.called)
2131+
self.assertTrue(mock_chain.return_value.apply_async.called)
2132+
2133+
tasks = mock_group.call_args[0][0]
2134+
self.assertEqual(len(tasks), 1)
2135+
2136+
mock_chain.reset_mock()
2137+
mock_group.reset_mock()
21362138

21372139
# Transition PENDING_PFA -> APPROVED
21382140
data = {"status": EAPStatus.APPROVED}

0 commit comments

Comments
 (0)