diff --git a/assets b/assets index a94f24cc6..45bd2a808 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a94f24cc6463b5f6b4ae568fc355c3550b226deb +Subproject commit 45bd2a808cca8de08e7908975ff06029bf096532 diff --git a/deploy/helm/ifrcgo-helm/values.yaml b/deploy/helm/ifrcgo-helm/values.yaml index af3d3c130..a7c07185d 100644 --- a/deploy/helm/ifrcgo-helm/values.yaml +++ b/deploy/helm/ifrcgo-helm/values.yaml @@ -279,6 +279,8 @@ cronjobs: # https://github.com/jazzband/django-oauth-toolkit/blob/master/docs/management_commands.rst#cleartokens - command: 'oauth_cleartokens' schedule: '0 1 * * *' + - command: 'eap_submission_reminder' + schedule: '0 0 * * *' elasticsearch: diff --git a/eap/management/commands/eap_submission_reminder.py b/eap/management/commands/eap_submission_reminder.py new file mode 100644 index 000000000..fa0a47e67 --- /dev/null +++ b/eap/management/commands/eap_submission_reminder.py @@ -0,0 +1,34 @@ +from datetime import timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone +from sentry_sdk.crons import monitor + +from eap.models import EAPRegistration +from eap.tasks import send_deadline_reminder_email +from main.sentry import SentryMonitor + + +class Command(BaseCommand): + help = "Send EAP submission reminder emails 1 week before deadline" + + @monitor(monitor_slug=SentryMonitor.EAP_SUBMISSION_REMINDER) + def handle(self, *args, **options): + """ + Finds EAP-registrations whose submission deadline is exactly 1 week from today + and sends reminder emails for each matching registration. + """ + target_date = timezone.now().date() + timedelta(weeks=1) + queryset = EAPRegistration.objects.filter( + deadline=target_date, + ) + + if not queryset.exists(): + self.stdout.write(self.style.NOTICE("No EAP registrations found for deadline reminder.")) + return + + for instance in queryset.iterator(): + self.stdout.write(self.style.NOTICE(f"Sending deadline reminder email for EAPRegistration ID={instance.id}")) + send_deadline_reminder_email(instance.id) + + self.stdout.write(self.style.SUCCESS("Successfully sent all deadline reminder emails.")) diff --git a/eap/migrations/0014_eapregistration_deadline_and_more.py b/eap/migrations/0014_eapregistration_deadline_and_more.py new file mode 100644 index 000000000..6b2d72bbb --- /dev/null +++ b/eap/migrations/0014_eapregistration_deadline_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.26 on 2025-12-24 05:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('eap', '0013_alter_eapregistration_national_society_contact_email_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='eapregistration', + name='deadline', + field=models.DateField(blank=True, help_text='Date by which the EAP submission must be completed.', null=True, verbose_name='deadline'), + ), + migrations.AddField( + model_name='eapregistration', + name='deadline_remainder_sent_at', + field=models.DateTimeField(blank=True, help_text='Timestamp when the deadline reminder email was sent.', null=True, verbose_name='deadline reminder email sent at'), + ), + ] diff --git a/eap/models.py b/eap/models.py index 99445c636..d5fd75dcc 100644 --- a/eap/models.py +++ b/eap/models.py @@ -694,6 +694,21 @@ class EAPRegistration(EAPBaseModel): help_text=_("Timestamp when the EAP was activated."), ) + # EAP submission deadline + deadline = models.DateField( + null=True, + blank=True, + verbose_name=_("deadline"), + help_text=_("Date by which the EAP submission must be completed."), + ) + + deadline_remainder_sent_at = models.DateTimeField( + null=True, + blank=True, + verbose_name=_("deadline reminder email sent at"), + help_text=_("Timestamp when the deadline reminder email was sent."), + ) + # TYPING id: int national_society_id: int diff --git a/eap/serializers.py b/eap/serializers.py index b6d85f415..9e193928f 100644 --- a/eap/serializers.py +++ b/eap/serializers.py @@ -1,4 +1,5 @@ import typing +from datetime import timedelta from django.contrib.auth.models import User from django.db import transaction @@ -193,6 +194,7 @@ class Meta: "modified_by", "latest_simplified_eap", "latest_full_eap", + "deadline", ] def create(self, validated_data: dict[str, typing.Any]): @@ -960,11 +962,28 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any Therefore: - version == 2 always corresponds to the first IFRC feedback cycle - Any later versions (>= 3) correspond to resubmitted cycles + + Deadline update rules: + - First IFRC feedback cycle: deadline is set to 90 days from the current date. + - Subsequent feedback or resubmission cycles: deadline is set to 30 days from the current date. """ if eap_count == 2: + updated_instance.deadline = timezone.now().date() + timedelta(days=90) + updated_instance.save( + update_fields=[ + "deadline", + ] + ) transaction.on_commit(lambda: send_feedback_email.delay(eap_registration_id)) + elif eap_count > 2: + updated_instance.deadline = timezone.now().date() + timedelta(days=30) + updated_instance.save( + update_fields=[ + "deadline", + ] + ) transaction.on_commit(lambda: send_feedback_email_for_resubmitted_eap.delay(eap_registration_id)) elif (old_status, new_status) == ( @@ -976,6 +995,12 @@ def update(self, instance: EAPRegistration, validated_data: dict[str, typing.Any EAPRegistration.Status.TECHNICALLY_VALIDATED, EAPRegistration.Status.NS_ADDRESSING_COMMENTS, ): + updated_instance.deadline = timezone.now().date() + timedelta(days=30) + updated_instance.save( + update_fields=[ + "deadline", + ] + ) transaction.on_commit(lambda: send_feedback_email_for_resubmitted_eap.delay(eap_registration_id)) elif (old_status, new_status) == ( diff --git a/eap/tasks.py b/eap/tasks.py index 819ae4ddd..62384ad55 100644 --- a/eap/tasks.py +++ b/eap/tasks.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.template.loader import render_to_string +from django.utils import timezone from eap.models import EAPRegistration, EAPType, FullEAP, SimplifiedEAP from eap.utils import get_coordinator_emails_by_region, get_eap_email_context @@ -114,7 +115,7 @@ def send_feedback_email(eap_registration_id: int): ifrc_delegation_focal_point_email = latest_simplified_eap.ifrc_delegation_focal_point_email else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email ifrc_delegation_focal_point_email = latest_full_eap.ifrc_delegation_focal_point_email regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -165,7 +166,7 @@ def send_eap_resubmission_email(eap_registration_id: int): latest_version = latest_simplified_eap.version else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email latest_version = latest_full_eap.version regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -268,7 +269,7 @@ def send_technical_validation_email(eap_registration_id: int): partner_ns_email = latest_simplified_eap.partner_ns_email else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -312,7 +313,7 @@ def send_pending_pfa_email(eap_registration_id: int): partner_ns_email = latest_simplified_eap.partner_ns_email else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -357,7 +358,7 @@ def send_approved_email(eap_registration_id: int): email_context = "Simplified EAP" else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email email_context = "Full EAP" regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -402,7 +403,7 @@ def send_deadline_reminder_email(eap_registration_id: int): partner_ns_email = latest_simplified_eap.partner_ns_email else: latest_full_eap = instance.latest_full_eap - partner_ns_email = latest_full_eap.partner_ns_name + partner_ns_email = latest_full_eap.partner_ns_email regional_coordinator_emails: list[str] = get_coordinator_emails_by_region(instance.country.region) @@ -422,7 +423,7 @@ def send_deadline_reminder_email(eap_registration_id: int): ) email_context = get_eap_email_context(instance) email_subject = f"[DREF {instance.get_eap_type_display()} SUBMISSION REMINDER] {instance.country} {instance.disaster_type}" - email_body = render_to_string("email/eap/approved.html", email_context) + email_body = render_to_string("email/eap/reminder.html", email_context) email_type = "Approved EAP" send_notification( subject=email_subject, @@ -431,4 +432,7 @@ def send_deadline_reminder_email(eap_registration_id: int): mailtype=email_type, cc_recipients=cc_recipients, ) + instance.deadline_remainder_sent_at = timezone.now() + instance.save(update_fields=["deadline_remainder_sent_at"]) + return email_context diff --git a/eap/utils.py b/eap/utils.py index 83c4c1c76..a4ddc07c4 100644 --- a/eap/utils.py +++ b/eap/utils.py @@ -34,7 +34,6 @@ def get_coordinator_emails_by_region(region: Region | None) -> list[str]: # TODO @sudip-khanal: Add files to email context after implementing file sending in email notification -# also include the deadline field once it added to the model. def get_eap_email_context(instance): @@ -52,6 +51,7 @@ def get_eap_email_context(instance): "ns_contact_name": eap_registration_data["national_society_contact_name"], "ns_contact_email": eap_registration_data["national_society_contact_email"], "ns_contact_phone": eap_registration_data["national_society_contact_phone_number"], + "deadline": eap_registration_data["deadline"], "frontend_url": settings.GO_WEB_URL, # "review_checklist_file":eap_registration_data["review_checklist_file"], # "validated_budget_file":eap_registration_data["validated_budget_file"], diff --git a/main/sentry.py b/main/sentry.py index e149e4953..b74e2d6ab 100644 --- a/main/sentry.py +++ b/main/sentry.py @@ -130,6 +130,7 @@ class SentryMonitor(models.TextChoices): INGEST_ICRC = "ingest_icrc", "0 3 * * 0" # NOTIFY_VALIDATORS = "notify_validators", "0 0 * * *" # NOTE: Disable local unit email notification for now OAUTH_CLEARTOKENS = "oauth_cleartokens", "0 1 * * *" + EAP_SUBMISSION_REMINDER = "eap_submission_reminder", "0 0 * * *" @staticmethod def load_cron_data() -> typing.List[typing.Tuple[str, str]]: diff --git a/notifications/templates/email/eap/feedback_to_national_society.html b/notifications/templates/email/eap/feedback_to_national_society.html index 1e60ac8b3..393e71bf6 100644 --- a/notifications/templates/email/eap/feedback_to_national_society.html +++ b/notifications/templates/email/eap/feedback_to_national_society.html @@ -26,7 +26,7 @@
The NS has 3 months to address these comments, which means that we expect to receive the new version - of the EAP no later than Deadline (3 months). + of the EAP no later than {{ deadline }} (3 months). In case the NS has any questions about the feedback provided, we are available to organize a feedback call. Do not hesitate to contact us should you have any further questions at DREF.anticipatorypillar@ifrc.org.
diff --git a/notifications/templates/email/eap/feedback_to_revised_eap.html b/notifications/templates/email/eap/feedback_to_revised_eap.html index 0dbd98177..9bd6a614c 100644 --- a/notifications/templates/email/eap/feedback_to_revised_eap.html +++ b/notifications/templates/email/eap/feedback_to_revised_eap.html @@ -31,7 +31,7 @@The NS has 1 month to address these comments, which means that we expect to receive the new version of the EAP no later than - Deadline (1 month). + {{ deadline }} (1 month). In case the NS has any questions about the feedback provided, we are available to organize a feedback call. Do not hesitate to contact us should you have any further questions at DREF.anticipatorypillar@ifrc.org. diff --git a/notifications/templates/email/eap/reminder.html b/notifications/templates/email/eap/reminder.html index 89f2fd3b8..47d635384 100644 --- a/notifications/templates/email/eap/reminder.html +++ b/notifications/templates/email/eap/reminder.html @@ -9,7 +9,7 @@
- This is a reminder that the next version of the {{ country_name }} {{ disaster_type }} should be submitted before Deadline. + This is a reminder that the next version of the {{ country_name }} {{ disaster_type }} should be submitted before {{ deadline }}.