Skip to content

Commit f1d58da

Browse files
feat: send doc event emails via celery (#7680)
* feat: notify_event_to_subscribers_task * fix: avoid circular import, handle error * fix: don't queue task in test mode * fix: don't even send mail in test mode * test: separately test signal * fix: if/else error * test: better naming * test: test the new task * test: better test name * test: refactor notify email test * fix: save, not update * test: restore template coverage
1 parent 877e842 commit f1d58da

File tree

3 files changed

+119
-16
lines changed

3 files changed

+119
-16
lines changed

ietf/community/models.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44

5+
from django.conf import settings
56
from django.db import models
67
from django.db.models import signals
78
from django.urls import reverse as urlreverse
@@ -11,6 +12,9 @@
1112
from ietf.person.models import Person, Email
1213
from ietf.utils.models import ForeignKey
1314

15+
from .tasks import notify_event_to_subscribers_task
16+
17+
1418
class CommunityList(models.Model):
1519
person = ForeignKey(Person, blank=True, null=True)
1620
group = ForeignKey(Group, blank=True, null=True)
@@ -106,8 +110,11 @@ def notify_events(sender, instance, **kwargs):
106110
if getattr(instance, "skip_community_list_notification", False):
107111
return
108112

109-
from ietf.community.utils import notify_event_to_subscribers
110-
notify_event_to_subscribers(instance)
113+
# kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to
114+
# start a Celery task during tests. To prevent this, don't queue a celery task if we're running
115+
# tests.
116+
if settings.SERVER_MODE != "test":
117+
notify_event_to_subscribers_task.delay(event_id=instance.pk)
111118

112119

113120
signals.post_save.connect(notify_events)

ietf/community/tasks.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
from celery import shared_task
3+
4+
from ietf.doc.models import DocEvent
5+
from ietf.utils.log import log
6+
7+
8+
@shared_task
9+
def notify_event_to_subscribers_task(event_id):
10+
from .utils import notify_event_to_subscribers
11+
event = DocEvent.objects.filter(pk=event_id).first()
12+
if event is None:
13+
log(f"Unable to send subscriber notifications because DocEvent {event_id} was not found")
14+
else:
15+
notify_event_to_subscribers(event)

ietf/community/tests.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,26 @@
22
# -*- coding: utf-8 -*-
33

44

5+
import mock
56
from pyquery import PyQuery
67

8+
from django.test.utils import override_settings
79
from django.urls import reverse as urlreverse
810

911
import debug # pyflakes:ignore
1012

1113
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
1214
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
13-
from ietf.community.utils import reset_name_contains_index_for_rule
15+
from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers
16+
from ietf.community.tasks import notify_event_to_subscribers_task
1417
import ietf.community.views
1518
from ietf.group.models import Group
1619
from ietf.group.utils import setup_default_community_list_for_group
1720
from ietf.doc.models import State
1821
from ietf.doc.utils import add_state_change_event
1922
from ietf.person.models import Person, Email, Alias
2023
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
21-
from ietf.utils.mail import outbox
22-
from ietf.doc.factories import WgDraftFactory
24+
from ietf.doc.factories import DocEventFactory, WgDraftFactory
2325
from ietf.group.factories import GroupFactory, RoleFactory
2426
from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory
2527

@@ -423,26 +425,105 @@ def test_subscription_for_group(self):
423425
r = self.client.get(url)
424426
self.assertEqual(r.status_code, 200)
425427

426-
def test_notification(self):
428+
@mock.patch("ietf.community.models.notify_event_to_subscribers_task")
429+
def test_notification_signal_receiver(self, mock_notify_task):
430+
"""Saving a DocEvent should notify subscribers
431+
432+
This implicitly tests that notify_events is hooked up to the post_save signal.
433+
"""
434+
# Arbitrary model that's not a DocEvent
435+
p = PersonFactory()
436+
mock_notify_task.reset_mock() # clear any calls that resulted from the factories
437+
# be careful overriding SERVER_MODE - we do it here because the method
438+
# under test does not make this call when in "test" mode
439+
with override_settings(SERVER_MODE="not-test"):
440+
p.save()
441+
self.assertFalse(mock_notify_task.delay.called)
442+
443+
d = DocEventFactory()
444+
mock_notify_task.reset_mock() # clear any calls that resulted from the factories
445+
# be careful overriding SERVER_MODE - we do it here because the method
446+
# under test does not make this call when in "test" mode
447+
with override_settings(SERVER_MODE="not-test"):
448+
d.save()
449+
self.assertEqual(mock_notify_task.delay.call_count, 1)
450+
self.assertEqual(mock_notify_task.delay.call_args, mock.call(event_id = d.pk))
451+
452+
mock_notify_task.reset_mock()
453+
d.skip_community_list_notification = True
454+
# be careful overriding SERVER_MODE - we do it here because the method
455+
# under test does not make this call when in "test" mode
456+
with override_settings(SERVER_MODE="not-test"):
457+
d.save()
458+
self.assertFalse(mock_notify_task.delay.called)
459+
460+
del(d.skip_community_list_notification)
461+
d.doc.type_id="rfc" # not "draft"
462+
d.doc.save()
463+
# be careful overriding SERVER_MODE - we do it here because the method
464+
# under test does not make this call when in "test" mode
465+
with override_settings(SERVER_MODE="not-test"):
466+
d.save()
467+
self.assertFalse(mock_notify_task.delay.called)
468+
469+
@mock.patch("ietf.utils.mail.send_mail_text")
470+
def test_notify_event_to_subscribers(self, mock_send_mail_text):
427471
person = PersonFactory(user__username='plain')
428472
draft = WgDraftFactory()
429473

430474
clist = CommunityList.objects.create(person=person)
431475
if not draft in clist.added_docs.all():
432476
clist.added_docs.add(draft)
433477

434-
EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
478+
sub_to_significant = EmailSubscription.objects.create(
479+
community_list=clist,
480+
email=Email.objects.filter(person__user__username="plain").first(),
481+
notify_on="significant",
482+
)
483+
sub_to_all = EmailSubscription.objects.create(
484+
community_list=clist,
485+
email=Email.objects.filter(person__user__username="plain").first(),
486+
notify_on="all",
487+
)
435488

436-
mailbox_before = len(outbox)
437489
active_state = State.objects.get(type="draft", slug="active")
438490
system = Person.objects.get(name="(System)")
439-
add_state_change_event(draft, system, None, active_state)
440-
self.assertEqual(len(outbox), mailbox_before)
491+
event = add_state_change_event(draft, system, None, active_state)
492+
notify_event_to_subscribers(event)
493+
self.assertEqual(mock_send_mail_text.call_count, 1)
494+
address = mock_send_mail_text.call_args[0][1]
495+
subject = mock_send_mail_text.call_args[0][3]
496+
content = mock_send_mail_text.call_args[0][4]
497+
self.assertEqual(address, sub_to_all.email.address)
498+
self.assertIn(draft.name, subject)
499+
self.assertIn(clist.long_name(), content)
441500

442-
mailbox_before = len(outbox)
443501
rfc_state = State.objects.get(type="draft", slug="rfc")
444-
add_state_change_event(draft, system, active_state, rfc_state)
445-
self.assertEqual(len(outbox), mailbox_before + 1)
446-
self.assertTrue(draft.name in outbox[-1]["Subject"])
447-
448-
502+
event = add_state_change_event(draft, system, active_state, rfc_state)
503+
mock_send_mail_text.reset_mock()
504+
notify_event_to_subscribers(event)
505+
self.assertEqual(mock_send_mail_text.call_count, 2)
506+
addresses = [call_args[0][1] for call_args in mock_send_mail_text.call_args_list]
507+
subjects = {call_args[0][3] for call_args in mock_send_mail_text.call_args_list}
508+
contents = {call_args[0][4] for call_args in mock_send_mail_text.call_args_list}
509+
self.assertCountEqual(
510+
addresses,
511+
[sub_to_significant.email.address, sub_to_all.email.address],
512+
)
513+
self.assertEqual(len(subjects), 1)
514+
self.assertIn(draft.name, subjects.pop())
515+
self.assertEqual(len(contents), 1)
516+
self.assertIn(clist.long_name(), contents.pop())
517+
518+
@mock.patch("ietf.community.utils.notify_event_to_subscribers")
519+
def test_notify_event_to_subscribers_task(self, mock_notify):
520+
d = DocEventFactory()
521+
notify_event_to_subscribers_task(event_id=d.pk)
522+
self.assertEqual(mock_notify.call_count, 1)
523+
self.assertEqual(mock_notify.call_args, mock.call(d))
524+
mock_notify.reset_mock()
525+
526+
d.delete()
527+
notify_event_to_subscribers_task(event_id=d.pk)
528+
self.assertFalse(mock_notify.called)
529+

0 commit comments

Comments
 (0)