Skip to content

Commit 22443ab

Browse files
susilnemshreeyash07
andcommitted
Email Implementation on local units (#2357)
* Email setup for editing local unit * Add email template for create and update local unit * Add email backend * Ad changes for the email template * Email for deprecate and changes on template * Changes in template and commenting out tls service * Add changes for email and create command for notifying regional and global validators * Add tags for regional and global validator * Add notify validators to values file * fix issue on deprecate api * Add api for template preview * Add USE_EMAIL_SMPT flag and last_sent_validator_type in model, Refactor email sending logic * add a comment in email_context * Add minor changes on email smtp * Add email preview api for local units * Add new views for email preview and changes on utils and email task * Changes in email preview api * Update localunit mail template * Fix issue on email preview --------- Co-authored-by: Shreeyash Shrestha <[email protected]>
1 parent 22102bc commit 22443ab

File tree

15 files changed

+378
-15
lines changed

15 files changed

+378
-15
lines changed

deploy/helm/ifrcgo-helm/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ cronjobs:
155155
schedule: '0 0 * * 0'
156156
- command: 'ingest_icrc'
157157
schedule: '0 3 * * 0'
158+
- command: 'notify_validators'
159+
schedule: '0 0 * * *'
158160

159161

160162
elasticsearch:

local_units/dev_views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.http import HttpResponse
2+
from django.template import loader
3+
from rest_framework import permissions
4+
from rest_framework.views import APIView
5+
6+
7+
class LocalUnitsEmailPreview(APIView):
8+
permission_classes = [permissions.IsAuthenticated]
9+
10+
def get(self, request):
11+
type_param = request.GET.get("type")
12+
param_types = {"new", "update", "validate", "revert", "deprecate", "regional_validator", "global_validator"}
13+
14+
if type_param not in param_types:
15+
return HttpResponse(f"Invalid type parameter. Please use one of the following values: {', '.join(param_types)}.")
16+
17+
context_mapping = {
18+
"new": {"new_local_unit": True, "validator_email": "Test Validator", "full_name": "Test User"},
19+
"update": {"update_local_unit": True, "validator_email": "Test Validator", "full_name": "Test User"},
20+
"validate": {"validate_success": True, "full_name": "Test User"},
21+
"revert": {"revert_reason": "Test Reason", "full_name": "Test User"},
22+
"deprecate": {"deprecate_local_unit": True, "deprecate_reason": "Test Deprecate Reason", "full_name": "Test User"},
23+
"regional_validator": {"is_validator_regional_admin": True, "full_name": "Regional User"},
24+
"global_validator": {"is_validator_global_admin": True, "full_name": "Global User"},
25+
}
26+
27+
context = context_mapping.get(type_param)
28+
if context is None:
29+
return HttpResponse("No context found for the email preview.")
30+
31+
template = loader.get_template("email/local_units/local_unit.html")
32+
return HttpResponse(template.render(context, request))

local_units/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
enum_register = {
44
"deprecate_reason": models.LocalUnit.DeprecateReason,
55
"validation_status": models.LocalUnitChangeRequest.Status,
6-
"validators": models.LocalUnitChangeRequest.Validator,
6+
"validators": models.Validator,
77
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from datetime import timedelta
2+
3+
from django.core.management.base import BaseCommand
4+
from django.template.loader import render_to_string
5+
from django.utils import timezone
6+
from sentry_sdk.crons import monitor
7+
8+
from local_units.models import LocalUnit, Validator
9+
from local_units.utils import (
10+
get_email_context,
11+
get_global_validators,
12+
get_region_admins,
13+
)
14+
from main.sentry import SentryMonitor
15+
from notifications.notification import send_notification
16+
17+
18+
class Command(BaseCommand):
19+
help = "Notify validators for the pending local units in different period of time"
20+
21+
@monitor(monitor_slug=SentryMonitor.NOTIFY_VALIDATORS)
22+
def handle(self, *args, **options):
23+
self.stdout.write(self.style.NOTICE("Notifying the validators..."))
24+
25+
# Regional Validators: 14 days
26+
queryset_for_regional_validators = LocalUnit.objects.filter(
27+
validated=False,
28+
is_deprecated=False,
29+
last_sent_validator_type=Validator.LOCAL,
30+
created_at__lte=timezone.now() - timedelta(days=14),
31+
)
32+
33+
# Global Validators: 28 days
34+
queryset_for_global_validators = LocalUnit.objects.filter(
35+
validated=False,
36+
is_deprecated=False,
37+
last_sent_validator_type=Validator.REGIONAL,
38+
created_at__lte=timezone.now() - timedelta(days=28),
39+
)
40+
41+
for local_unit in queryset_for_regional_validators:
42+
self.stdout.write(self.style.NOTICE(f"Notifying regional validators for local unit pk:({local_unit.id})"))
43+
email_context = get_email_context(local_unit)
44+
email_context["is_validator_regional_admin"] = True
45+
email_subject = "Action Required: Local Unit Pending Validation"
46+
email_type = "Local Unit"
47+
48+
for region_admin_validator in get_region_admins(local_unit):
49+
try:
50+
email_context["full_name"] = region_admin_validator.get_full_name()
51+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
52+
send_notification(email_subject, region_admin_validator.email, email_body, email_type)
53+
local_unit.last_sent_validator_type = Validator.REGIONAL
54+
local_unit.save(update_fields=["last_sent_validator_type"])
55+
except Exception as e:
56+
self.stdout.write(
57+
self.style.WARNING(
58+
f"Failed to notify regional validator {region_admin_validator.get_full_name()} for local unit pk:({local_unit.id}): {e}" # noqa
59+
)
60+
)
61+
continue
62+
63+
for local_unit in queryset_for_global_validators:
64+
self.stdout.write(self.style.NOTICE(f"Notifying global validators for local unit pk:({local_unit.id})"))
65+
email_context = get_email_context(local_unit)
66+
email_context["is_validator_global_admin"] = True
67+
email_subject = "Action Required: Local Unit Pending Validation"
68+
email_type = "Local Unit"
69+
70+
for global_validator in get_global_validators():
71+
try:
72+
email_context["full_name"] = global_validator.get_full_name()
73+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
74+
send_notification(email_subject, global_validator.email, email_body, email_type)
75+
local_unit.last_sent_validator_type = Validator.GLOBAL
76+
local_unit.save(update_fields=["last_sent_validator_type"])
77+
except Exception as e:
78+
self.stdout.write(
79+
self.style.WARNING(
80+
f"Failed to notify global validator {global_validator.get_full_name()} for local unit pk:({local_unit.id}): {e}" # noqa
81+
)
82+
)
83+
continue
84+
85+
self.stdout.write(self.style.SUCCESS("Successfully sent the notifications to the validators"))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 4.2.17 on 2024-12-27 14:32
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("local_units", "0018_localunit_deprecated_reason_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="localunit",
15+
name="last_sent_validator_type",
16+
field=models.IntegerField(
17+
choices=[(1, "Local"), (2, "Regional"), (3, "Global")], default=1, verbose_name="Last email sent validator type"
18+
),
19+
),
20+
]

local_units/models.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ def __str__(self):
275275
return f"{self.name} ({self.level})"
276276

277277

278+
class Validator(models.IntegerChoices):
279+
LOCAL = 1, _("Local")
280+
REGIONAL = 2, _("Regional")
281+
GLOBAL = 3, _("Global")
282+
283+
278284
@reversion.register(follow=("health",))
279285
class LocalUnit(models.Model):
280286

@@ -356,6 +362,12 @@ class DeprecateReason(models.IntegerChoices):
356362
verbose_name=_("Explain the reason why the local unit is being deleted"), blank=True, null=True
357363
)
358364

365+
last_sent_validator_type = models.IntegerField(
366+
choices=Validator.choices,
367+
verbose_name=_("Last email sent validator type"),
368+
default=Validator.LOCAL,
369+
)
370+
359371
def __str__(self):
360372
branch_name = self.local_branch_name or self.english_branch_name
361373
return f"{branch_name} ({self.country.name})"
@@ -377,11 +389,6 @@ class Status(models.IntegerChoices):
377389
APPROVED = 2, _("Approved")
378390
REVERT = 3, _("Revert")
379391

380-
class Validator(models.IntegerChoices):
381-
LOCAL = 1, _("Local")
382-
REGIONAL = 2, _("Regional")
383-
GLOBAL = 3, _("Global")
384-
385392
local_unit = models.ForeignKey(
386393
LocalUnit, on_delete=models.CASCADE, verbose_name=_("Local Unit"), related_name="local_unit_change_request"
387394
)

local_units/tasks.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from celery import shared_task
2+
from django.template.loader import render_to_string
3+
4+
from local_units.models import LocalUnit, LocalUnitChangeRequest
5+
from notifications.notification import send_notification
6+
7+
from .utils import get_email_context, get_local_admins
8+
9+
10+
@shared_task
11+
def send_local_unit_email(local_unit_id: int, new: bool = True):
12+
if not local_unit_id:
13+
return None
14+
15+
instance = LocalUnit.objects.get(id=local_unit_id)
16+
users = get_local_admins(instance)
17+
email_context = get_email_context(instance)
18+
email_context["new_local_unit"] = True
19+
email_subject = "Action Required: New Local Unit Pending Validation"
20+
email_type = "New Local Unit"
21+
# NOTE: Update case for the local unit
22+
if not new:
23+
email_context.pop("new_local_unit")
24+
email_context["update_local_unit"] = True
25+
email_subject = "Action Required: Local Unit Pending Validation"
26+
email_type = "Update Local Unit"
27+
28+
for user in users:
29+
# NOTE: Adding the validator email to the context
30+
email_context["validator_email"] = user.email
31+
email_context["full_name"] = user.get_full_name()
32+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
33+
send_notification(email_subject, user.email, email_body, email_type)
34+
return email_context
35+
36+
37+
@shared_task
38+
def send_validate_success_email(local_unit_id: int, message: str = ""):
39+
if not local_unit_id:
40+
return None
41+
42+
instance = LocalUnit.objects.get(id=local_unit_id)
43+
user = instance.created_by
44+
email_context = get_email_context(instance)
45+
email_context["full_name"] = user.get_full_name()
46+
email_context["validate_success"] = True
47+
email_subject = "Your Local Unit Addition Request: Approved"
48+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
49+
email_type = f"{message} Local Unit"
50+
51+
send_notification(email_subject, user.email, email_body, email_type)
52+
return email_context
53+
54+
55+
@shared_task
56+
def send_revert_email(local_unit_id: int, change_request_id: int):
57+
if not local_unit_id:
58+
return None
59+
60+
instance = LocalUnit.objects.get(id=local_unit_id)
61+
change_request_instance = LocalUnitChangeRequest.objects.get(id=change_request_id)
62+
user = instance.created_by
63+
email_context = get_email_context(instance)
64+
email_context["full_name"] = user.get_full_name()
65+
email_context["revert_reason"] = change_request_instance.rejected_reason
66+
email_subject = "Your Local Unit Addition Request: Reverted"
67+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
68+
email_type = "Revert Local Unit"
69+
70+
send_notification(email_subject, user.email, email_body, email_type)
71+
return email_context
72+
73+
74+
@shared_task
75+
def send_deprecate_email(local_unit_id: int):
76+
if not local_unit_id:
77+
return None
78+
79+
instance = LocalUnit.objects.get(id=local_unit_id)
80+
user = instance.created_by
81+
email_context = get_email_context(instance)
82+
email_context["full_name"] = user.get_full_name()
83+
email_context["deprecate_local_unit"] = True
84+
email_context["deprecate_reason"] = instance.deprecated_reason_overview
85+
email_subject = "Your Local Unit Addition Request: Deprecated"
86+
email_body = render_to_string("email/local_units/local_unit.html", email_context)
87+
email_type = "Deprecate Local Unit"
88+
89+
send_notification(email_subject, user.email, email_body, email_type)
90+
return email_context

local_units/test_views.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
LocalUnitLevel,
2323
LocalUnitType,
2424
PrimaryHCC,
25+
Validator,
2526
VisibilityChoices,
2627
)
2728

@@ -677,7 +678,7 @@ def test_validate_local_unit(self):
677678
local_unit_request = LocalUnitChangeRequest.objects.filter(
678679
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
679680
).last()
680-
self.assertEqual(local_unit_request.current_validator, LocalUnitChangeRequest.Validator.GLOBAL)
681+
self.assertEqual(local_unit_request.current_validator, Validator.GLOBAL)
681682

682683
# Testing For the local unit admin/Local validator
683684
self.authenticate(self.local_unit_admin_user)
@@ -688,7 +689,7 @@ def test_validate_local_unit(self):
688689
local_unit_request = LocalUnitChangeRequest.objects.filter(
689690
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
690691
).last()
691-
self.assertEqual(local_unit_request.current_validator, LocalUnitChangeRequest.Validator.LOCAL)
692+
self.assertEqual(local_unit_request.current_validator, Validator.LOCAL)
692693

693694
# Testing For the regional validator
694695
self.authenticate(self.regional_validator_user)
@@ -699,7 +700,7 @@ def test_validate_local_unit(self):
699700
local_unit_request = LocalUnitChangeRequest.objects.filter(
700701
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
701702
).last()
702-
self.assertEqual(local_unit_request.current_validator, LocalUnitChangeRequest.Validator.REGIONAL)
703+
self.assertEqual(local_unit_request.current_validator, Validator.REGIONAL)
703704

704705
# Testing for Root User/Global validator
705706
self.authenticate(self.root_user)
@@ -710,4 +711,4 @@ def test_validate_local_unit(self):
710711
local_unit_request = LocalUnitChangeRequest.objects.filter(
711712
local_unit=local_unit_id, status=LocalUnitChangeRequest.Status.APPROVED
712713
).last()
713-
self.assertEqual(local_unit_request.current_validator, LocalUnitChangeRequest.Validator.GLOBAL)
714+
self.assertEqual(local_unit_request.current_validator, Validator.GLOBAL)

local_units/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.conf import settings
2+
from django.contrib.auth import get_user_model
3+
4+
User = get_user_model()
5+
6+
7+
def get_email_context(instance):
8+
from local_units.serializers import PrivateLocalUnitSerializer
9+
10+
# NOTE: Passing through serializer, might need more info in the future
11+
local_unit_data = PrivateLocalUnitSerializer(instance).data
12+
email_context = {
13+
"id": local_unit_data["id"],
14+
"frontend_url": settings.FRONTEND_URL,
15+
}
16+
return email_context
17+
18+
19+
def get_local_admins(instance):
20+
"""
21+
Get the user with the country level admin permission for the country of the instance
22+
"""
23+
country_admins = User.objects.filter(groups__permissions__codename=f"country_admin_{instance.country_id}")
24+
return country_admins
25+
26+
27+
def get_region_admins(instance):
28+
"""
29+
Get the user with the region level admin permission for the region of the instance
30+
"""
31+
region_admins = User.objects.filter(groups__permissions__codename=f"region_admin_{instance.country.region_id}")
32+
return region_admins
33+
34+
35+
def get_global_validators():
36+
"""
37+
Get the user with the global validator permission
38+
"""
39+
global_validators = User.objects.filter(groups__permissions__codename="local_unit_global_validator")
40+
return global_validators

0 commit comments

Comments
 (0)