Skip to content

Commit b62754d

Browse files
committed
feat(eap): add email recipient from admin panel
- used on email notification - add test cases
1 parent 7516b59 commit b62754d

8 files changed

Lines changed: 297 additions & 63 deletions

File tree

eap/admin.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
EAPFile,
66
EAPRegistration,
77
EAPType,
8+
EmailRecipient,
89
FullEAP,
910
KeyActor,
1011
SimplifiedEAP,
@@ -21,6 +22,26 @@ class EAPFileAdmin(admin.ModelAdmin):
2122
)
2223

2324

25+
@admin.register(EmailRecipient)
26+
class EmailRecipientAdmin(admin.ModelAdmin):
27+
list_select_related = True
28+
search_fields = ("email",)
29+
list_display = (
30+
"type",
31+
"title",
32+
"region",
33+
"email",
34+
)
35+
list_filter = (
36+
"type",
37+
"region",
38+
)
39+
autocomplete_fields = ("region",)
40+
41+
def has_module_permission(self, request):
42+
return request.user.is_superuser
43+
44+
2445
@admin.register(EAPRegistration)
2546
class DevelopmentRegistrationEAPAdmin(admin.ModelAdmin):
2647
list_select_related = True

eap/factories.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
EAPRegistration,
1111
EAPStatus,
1212
EAPType,
13+
EmailRecipient,
1314
EnablingApproach,
1415
FullEAP,
1516
KeyActor,
@@ -238,3 +239,12 @@ def key_actors(self, create, extracted, **kwargs):
238239
if extracted:
239240
for actor in extracted:
240241
self.key_actors.add(actor)
242+
243+
244+
class EmailRecipientFactory(factory.django.DjangoModelFactory):
245+
class Meta:
246+
model = EmailRecipient
247+
248+
type = fuzzy.FuzzyChoice(EAPType)
249+
email = factory.LazyAttribute(lambda obj: f"{obj.type.lower()}@example.com")
250+
title = fuzzy.FuzzyText(length=10, prefix="Title-")
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Generated by Django 4.2.30 on 2026-04-30 11:21
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("api", "0231_alter_export_export_type"),
10+
("eap", "0003_eapaction_eapcontact_eapfile_eapimpact_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="EmailRecipient",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("title", models.CharField(max_length=255, verbose_name="Title")),
27+
(
28+
"type",
29+
models.IntegerField(
30+
choices=[
31+
(10, "DREF Anticipatory Pillar"),
32+
(20, "DREF AA Global Team"),
33+
(30, "Regional Coordinator"),
34+
],
35+
verbose_name="Email Type",
36+
),
37+
),
38+
("email", models.EmailField(max_length=254)),
39+
(
40+
"region",
41+
models.ForeignKey(
42+
blank=True,
43+
help_text="Only assign region for Regional Coordinator email type.",
44+
null=True,
45+
on_delete=django.db.models.deletion.CASCADE,
46+
to="api.region",
47+
),
48+
),
49+
],
50+
options={
51+
"verbose_name": "Email Recipient",
52+
"verbose_name_plural": "Email Recipients",
53+
},
54+
),
55+
migrations.AddConstraint(
56+
model_name="emailrecipient",
57+
constraint=models.UniqueConstraint(
58+
condition=models.Q(("region__isnull", True)),
59+
fields=("type", "email"),
60+
name="unique_email_type_no_region",
61+
violation_error_message="Email must be unique for the given type when no region is assigned.",
62+
),
63+
),
64+
migrations.AddConstraint(
65+
model_name="emailrecipient",
66+
constraint=models.UniqueConstraint(
67+
condition=models.Q(("region__isnull", False)),
68+
fields=("type", "region", "email"),
69+
name="unique_email_type_region",
70+
violation_error_message="Email must be unique for the given type and region.",
71+
),
72+
),
73+
]

eap/models.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from django.conf import settings
22
from django.contrib.postgres.fields import ArrayField
3+
from django.core.exceptions import ValidationError
34
from django.db import models, transaction
5+
from django.db.models import Q
46
from django.utils import timezone
57
from django.utils.translation import gettext_lazy as _
68

7-
from api.models import Admin2, Country, DisasterType, District
9+
from api.models import Admin2, Country, DisasterType, District, Region
810
from main.fields import SecureFileField
911

1012

@@ -248,6 +250,64 @@ def __str__(self):
248250
return f"{self.name}"
249251

250252

253+
# NOTE: Managed through admin panel for now, used for email notification
254+
class EmailRecipient(models.Model):
255+
class EmailType(models.IntegerChoices):
256+
DREF_ANTICIPATORY = 10, "DREF Anticipatory Pillar"
257+
DREF_AA_GLOBAL_TEAM = 20, "DREF AA Global Team"
258+
REGIONAL_COORDINATOR = 30, "Regional Coordinator"
259+
260+
title = models.CharField(max_length=255, verbose_name=_("Title"))
261+
type = models.IntegerField(choices=EmailType.choices, verbose_name=_("Email Type"))
262+
region = models.ForeignKey(
263+
Region,
264+
null=True,
265+
blank=True,
266+
on_delete=models.CASCADE,
267+
help_text=_("Only assign region for Regional Coordinator email type."),
268+
)
269+
email = models.EmailField()
270+
271+
# TYPING
272+
id: int
273+
region_id: int | None
274+
275+
class Meta:
276+
verbose_name = _("Email Recipient")
277+
verbose_name_plural = _("Email Recipients")
278+
constraints = [
279+
models.UniqueConstraint(
280+
fields=["type", "email"],
281+
condition=Q(region__isnull=True),
282+
name="unique_email_type_no_region",
283+
violation_error_message=_("Email must be unique for the given type when no region is assigned."),
284+
),
285+
models.UniqueConstraint(
286+
fields=["type", "region", "email"],
287+
condition=Q(region__isnull=False),
288+
name="unique_email_type_region",
289+
violation_error_message=_("Email must be unique for the given type and region."),
290+
),
291+
]
292+
293+
def __str__(self):
294+
return f"{self.get_type_display()} - {self.email}"
295+
296+
def clean(self):
297+
if (
298+
self.type
299+
in [
300+
self.EmailType.DREF_ANTICIPATORY,
301+
self.EmailType.DREF_AA_GLOBAL_TEAM,
302+
]
303+
and self.region is not None
304+
):
305+
raise ValidationError(f"{self.get_type_display()} should not have a region assigned.")
306+
307+
if self.type == self.EmailType.REGIONAL_COORDINATOR and self.region is None:
308+
raise ValidationError("Regional coordinator must have a region.")
309+
310+
251311
class TimeFrame(models.IntegerChoices):
252312
YEARS = 10, _("Years")
253313
MONTHS = 20, _("Months")

0 commit comments

Comments
 (0)