Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions alert_system/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ class LoadItemAdmin(admin.ModelAdmin):
class AlertEmailThreadAdmin(admin.ModelAdmin):
list_display = (
"user",
"parent_guid",
"parent_event_id",
"root_email_message_id",
)
search_fields = (
"parent_guid",
"parent_event_id",
"root_email_message_id",
"user__username",
)
Expand Down
7 changes: 4 additions & 3 deletions alert_system/email_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@ def send_alert_email_notification(
if not is_reply:
thread = AlertEmailThread.objects.create(
user=user,
parent_guid=load_item.parent_guid,
parent_event_id=load_item.parent_event_id,
root_email_message_id=message_id,
root_message_sent_at=timezone.now(),
)
email_log.thread = thread
email_log.save(update_fields=["thread"])
logger.info(
f"Alert Email thread created for user [{user.get_full_name()}] " f"with parent_guid [{load_item.parent_guid}]"
f"Alert Email thread created for user [{user.get_full_name()}] "
f"with parent event [{load_item.parent_event_id}]"
)

logger.info(f"Alert email sent to [{user.get_full_name()}] for LoadItem ID [{load_item.id}]")
Expand Down Expand Up @@ -127,7 +128,7 @@ def process_email_alert(load_item_id: int) -> None:
existing_threads = {
thread.user_id: thread
for thread in AlertEmailThread.objects.filter(
parent_guid=load_item.parent_guid,
parent_event_id=load_item.parent_event_id,
user_id__in=user_ids,
)
}
Expand Down
4 changes: 3 additions & 1 deletion alert_system/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@


class LoadItemFactory(factory.django.DjangoModelFactory):
guid = factory.LazyFunction(lambda: str(uuid4()))
parent_event_id = factory.LazyFunction(lambda: str(uuid4()))
event_id = factory.LazyFunction(lambda: str(uuid4()))
event_url = factory.Sequence(lambda n: f"https://test-events.com/event/{n}")

class Meta:
model = LoadItem
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.30 on 2026-05-18 08:24

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('alert_system', '0001_initial'),
]

operations = [
migrations.RemoveConstraint(
model_name='alertemailthread',
name='unique_user_guid',
),
migrations.RemoveIndex(
model_name='alertemailthread',
name='alert_syste_parent__737a31_idx',
),
migrations.RenameField(
model_name='alertemailthread',
old_name='parent_guid',
new_name='parent_event_id',
),
migrations.AddIndex(
model_name='alertemailthread',
index=models.Index(fields=['parent_event_id', 'user'], name='alert_syste_parent__7efaa4_idx'),
),
migrations.AddConstraint(
model_name='alertemailthread',
constraint=models.UniqueConstraint(fields=('parent_event_id', 'user'), name='unique_user_parent_event'),
),
]
10 changes: 5 additions & 5 deletions alert_system/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ class AlertEmailThread(models.Model):
on_delete=models.CASCADE,
related_name="alert_email_threads",
)

parent_guid = models.CharField(
# NOTE: parent_event_id field is same field form the LoadItem model.
parent_event_id = models.CharField(
help_text=_("Identifier linking related LoadItems into the same email thread."),
)

Expand All @@ -304,13 +304,13 @@ class Meta:
verbose_name = _("Email Thread")
verbose_name_plural = _("Email Threads")
ordering = ["-id"]
constraints = [models.UniqueConstraint(fields=["parent_guid", "user"], name="unique_user_guid")]
constraints = [models.UniqueConstraint(fields=["parent_event_id", "user"], name="unique_user_parent_event")]
indexes = [
models.Index(fields=["parent_guid", "user"]),
models.Index(fields=["parent_event_id", "user"]),
]

def __str__(self):
return f"Thread: {self.user.get_full_name()}-{self.parent_guid}"
return f"Thread: {self.user.get_full_name()}-{self.parent_event_id}"


class AlertEmailLog(models.Model):
Expand Down
32 changes: 16 additions & 16 deletions alert_system/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def setUp(self):
)

self.eligible_item = LoadItemFactory.create(
parent_guid=str(uuid4()),
parent_event_id=str(uuid4()),
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand Down Expand Up @@ -107,7 +107,7 @@ def test_sent_email_for_eligible_item(self, mock_send_notification):
self.assertIsNotNone(log.email_sent_at)

self.assertEqual(thread.user, self.user1)
self.assertEqual(thread.parent_guid, self.eligible_item.parent_guid)
self.assertEqual(thread.parent_event_id, self.eligible_item.parent_event_id)
self.assertEqual(thread.root_email_message_id, log.message_id)
self.assertEqual(log.thread, thread)

Expand All @@ -121,7 +121,7 @@ def test_sent_email_to_multiple_users(self, mock_send_notification):
logs = AlertEmailLog.objects.filter(item=self.eligible_item, status=AlertEmailLog.Status.SENT)
self.assertEqual(logs.count(), 2)

threads = AlertEmailThread.objects.filter(parent_guid=self.eligible_item.parent_guid)
threads = AlertEmailThread.objects.filter(parent_event_id=self.eligible_item.parent_event_id)
self.assertEqual(threads.count(), 2)

self.assertEqual(mock_send_notification.call_count, 2)
Expand Down Expand Up @@ -217,7 +217,7 @@ def test_reply_email_for_existing_thread(self, mock_send_notification):
)

initial_item = LoadItemFactory.create(
parent_guid=str(uuid4()),
parent_event_id=str(uuid4()),
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand All @@ -231,7 +231,7 @@ def test_reply_email_for_existing_thread(self, mock_send_notification):

thread = AlertEmailThreadFactory.create(
user=user,
parent_guid=initial_item.parent_guid,
parent_event_id=initial_item.parent_event_id,
root_email_message_id=str(uuid4()),
root_message_sent_at=timezone.now(),
)
Expand All @@ -247,7 +247,7 @@ def test_reply_email_for_existing_thread(self, mock_send_notification):
)

update_item = LoadItemFactory.create(
parent_guid=initial_item.parent_guid,
parent_event_id=initial_item.parent_event_id,
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand All @@ -267,17 +267,17 @@ def test_reply_email_for_existing_thread(self, mock_send_notification):

mock_send_notification.assert_called_once()

threads = AlertEmailThread.objects.filter(parent_guid=initial_item.parent_guid)
threads = AlertEmailThread.objects.filter(parent_event_id=initial_item.parent_event_id)
self.assertEqual(threads.count(), 1)

@mock.patch("alert_system.email_processing.send_notification")
def test_reply_email_to_multiple_users(self, mock_send_notification):

parent_guid = str(uuid4())
parent_event_id = str(uuid4())

# Create initial item
initial_item = LoadItemFactory.create(
parent_guid=parent_guid,
parent_event_id=parent_event_id,
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand All @@ -292,14 +292,14 @@ def test_reply_email_to_multiple_users(self, mock_send_notification):
# Create threads for both users
thread1 = AlertEmailThreadFactory.create(
user=self.user1,
parent_guid=parent_guid,
parent_event_id=parent_event_id,
root_email_message_id="message-id-1",
root_message_sent_at=timezone.now(),
)

thread2 = AlertEmailThreadFactory.create(
user=self.user2,
parent_guid=parent_guid,
parent_event_id=parent_event_id,
root_email_message_id="message-id-2",
root_message_sent_at=timezone.now(),
)
Expand All @@ -325,7 +325,7 @@ def test_reply_email_to_multiple_users(self, mock_send_notification):
)

related_item = LoadItemFactory.create(
parent_guid=parent_guid,
parent_event_id=parent_event_id,
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand All @@ -350,7 +350,7 @@ def test_reply_email_to_multiple_users(self, mock_send_notification):
@mock.patch("alert_system.email_processing.send_notification")
def test_duplicate_reply(self, mock_send_notification):

parent_guid = str(uuid4())
parent_event_id = str(uuid4())

user = UserFactory.create()
country = CountryFactory.create(
Expand All @@ -366,7 +366,7 @@ def test_duplicate_reply(self, mock_send_notification):
)

LoadItemFactory.create(
parent_guid=parent_guid,
parent_event_id=parent_event_id,
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand All @@ -380,13 +380,13 @@ def test_duplicate_reply(self, mock_send_notification):

thread = AlertEmailThreadFactory.create(
user=user,
parent_guid=parent_guid,
parent_event_id=parent_event_id,
root_email_message_id="root-123",
root_message_sent_at=timezone.now(),
)

update_item = LoadItemFactory.create(
parent_guid=parent_guid,
parent_event_id=parent_event_id,
connector=self.connector,
item_eligible=True,
is_past_event=False,
Expand Down
16 changes: 15 additions & 1 deletion alert_system/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@
logger = logging.getLogger(__name__)


def get_latest_episode_load_item(load_item: LoadItem) -> LoadItem:
"""
Given a load_item, return the sibling LoadItem with the highest
episode_number for the same parent_event_id.
Falls back to the original load_item if no siblings exist.
"""
latest = LoadItem.objects.filter(parent_event_id=load_item.parent_event_id).order_by("-episode_number").first()
return latest or load_item


def get_alert_email_context(load_item: LoadItem, user: User):

country_names = []

if load_item.country_codes:
country_names = list(Country.objects.filter(iso3__in=load_item.country_codes).values_list("name", flat=True))

# Fetch related_montandon_events from the latest episode
latest_episode_item = get_latest_episode_load_item(load_item)

email_context = {
"user_name": user.get_full_name(),
"event_title": load_item.event_title,
Expand All @@ -26,7 +40,7 @@ def get_alert_email_context(load_item: LoadItem, user: User):
"total_buildings_exposed": load_item.total_buildings_exposed,
"hazard_types": load_item.connector.dtype,
"related_go_events": load_item.related_go_events.all(),
"related_montandon_events": load_item.related_montandon_events.filter(item_eligible=True).order_by(
"related_montandon_events": latest_episode_item.related_montandon_events.filter(item_eligible=True).order_by(
"-total_people_exposed"
),
"frontend_url": settings.GO_WEB_URL,
Expand Down
Loading