Skip to content

Commit a9b2573

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

9 files changed

Lines changed: 298 additions & 63 deletions

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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 4.2.30 on 2026-04-28 06:35
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0231_alter_export_export_type'),
11+
('eap', '0003_eapaction_eapcontact_eapfile_eapimpact_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='EmailRecipient',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('type', models.IntegerField(choices=[(10, 'DREF Anticipatory Pillar'), (20, 'DREF AA Global Team'), (30, 'Regional Coordinator')], verbose_name='Email Type')),
20+
('email', models.EmailField(max_length=254)),
21+
('region', models.ForeignKey(blank=True, help_text='Only assign region for Regional Coordinator email type.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.region')),
22+
],
23+
options={
24+
'verbose_name': 'Email Recipient',
25+
'verbose_name_plural': 'Email Recipients',
26+
},
27+
),
28+
migrations.AddConstraint(
29+
model_name='emailrecipient',
30+
constraint=models.UniqueConstraint(fields=('type', 'region', 'email'), name='unique_eap_email_per_type_region', violation_error_message='This email is already assigned.'),
31+
),
32+
]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Generated by Django 4.2.30 on 2026-04-30 10:33
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("eap", "0004_emailrecipient_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveConstraint(
13+
model_name="emailrecipient",
14+
name="unique_eap_email_per_type_region",
15+
),
16+
migrations.AddField(
17+
model_name="emailrecipient",
18+
name="title",
19+
field=models.CharField(
20+
default="test", max_length=255, verbose_name="Title"
21+
),
22+
preserve_default=False,
23+
),
24+
migrations.AddConstraint(
25+
model_name="emailrecipient",
26+
constraint=models.UniqueConstraint(
27+
condition=models.Q(("region__isnull", True)),
28+
fields=("type", "email"),
29+
name="unique_email_type_no_region",
30+
violation_error_message="Email must be unique for the given type when no region is assigned.",
31+
),
32+
),
33+
migrations.AddConstraint(
34+
model_name="emailrecipient",
35+
constraint=models.UniqueConstraint(
36+
condition=models.Q(("region__isnull", False)),
37+
fields=("type", "region", "email"),
38+
name="unique_email_type_region",
39+
violation_error_message="Email must be unique for the given type and region.",
40+
),
41+
),
42+
]

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)