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
This commit is contained in:
Jennifer Richards 2024-07-12 12:10:46 -03:00 committed by GitHub
parent 877e8429b1
commit f1d58da877
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 119 additions and 16 deletions

View file

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.db import models
from django.db.models import signals
from django.urls import reverse as urlreverse
@ -11,6 +12,9 @@ from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.utils.models import ForeignKey
from .tasks import notify_event_to_subscribers_task
class CommunityList(models.Model):
person = ForeignKey(Person, blank=True, null=True)
group = ForeignKey(Group, blank=True, null=True)
@ -106,8 +110,11 @@ def notify_events(sender, instance, **kwargs):
if getattr(instance, "skip_community_list_notification", False):
return
from ietf.community.utils import notify_event_to_subscribers
notify_event_to_subscribers(instance)
# kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to
# start a Celery task during tests. To prevent this, don't queue a celery task if we're running
# tests.
if settings.SERVER_MODE != "test":
notify_event_to_subscribers_task.delay(event_id=instance.pk)
signals.post_save.connect(notify_events)

15
ietf/community/tasks.py Normal file
View file

@ -0,0 +1,15 @@
# Copyright The IETF Trust 2024, All Rights Reserved
from celery import shared_task
from ietf.doc.models import DocEvent
from ietf.utils.log import log
@shared_task
def notify_event_to_subscribers_task(event_id):
from .utils import notify_event_to_subscribers
event = DocEvent.objects.filter(pk=event_id).first()
if event is None:
log(f"Unable to send subscriber notifications because DocEvent {event_id} was not found")
else:
notify_event_to_subscribers(event)

View file

@ -2,15 +2,18 @@
# -*- coding: utf-8 -*-
import mock
from pyquery import PyQuery
from django.test.utils import override_settings
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers
from ietf.community.tasks import notify_event_to_subscribers_task
import ietf.community.views
from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group
@ -18,8 +21,7 @@ from ietf.doc.models import State
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person, Email, Alias
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.mail import outbox
from ietf.doc.factories import WgDraftFactory
from ietf.doc.factories import DocEventFactory, WgDraftFactory
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory
@ -423,7 +425,49 @@ class CommunityListTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_notification(self):
@mock.patch("ietf.community.models.notify_event_to_subscribers_task")
def test_notification_signal_receiver(self, mock_notify_task):
"""Saving a DocEvent should notify subscribers
This implicitly tests that notify_events is hooked up to the post_save signal.
"""
# Arbitrary model that's not a DocEvent
p = PersonFactory()
mock_notify_task.reset_mock() # clear any calls that resulted from the factories
# be careful overriding SERVER_MODE - we do it here because the method
# under test does not make this call when in "test" mode
with override_settings(SERVER_MODE="not-test"):
p.save()
self.assertFalse(mock_notify_task.delay.called)
d = DocEventFactory()
mock_notify_task.reset_mock() # clear any calls that resulted from the factories
# be careful overriding SERVER_MODE - we do it here because the method
# under test does not make this call when in "test" mode
with override_settings(SERVER_MODE="not-test"):
d.save()
self.assertEqual(mock_notify_task.delay.call_count, 1)
self.assertEqual(mock_notify_task.delay.call_args, mock.call(event_id = d.pk))
mock_notify_task.reset_mock()
d.skip_community_list_notification = True
# be careful overriding SERVER_MODE - we do it here because the method
# under test does not make this call when in "test" mode
with override_settings(SERVER_MODE="not-test"):
d.save()
self.assertFalse(mock_notify_task.delay.called)
del(d.skip_community_list_notification)
d.doc.type_id="rfc" # not "draft"
d.doc.save()
# be careful overriding SERVER_MODE - we do it here because the method
# under test does not make this call when in "test" mode
with override_settings(SERVER_MODE="not-test"):
d.save()
self.assertFalse(mock_notify_task.delay.called)
@mock.patch("ietf.utils.mail.send_mail_text")
def test_notify_event_to_subscribers(self, mock_send_mail_text):
person = PersonFactory(user__username='plain')
draft = WgDraftFactory()
@ -431,18 +475,55 @@ class CommunityListTests(TestCase):
if not draft in clist.added_docs.all():
clist.added_docs.add(draft)
EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
sub_to_significant = EmailSubscription.objects.create(
community_list=clist,
email=Email.objects.filter(person__user__username="plain").first(),
notify_on="significant",
)
sub_to_all = EmailSubscription.objects.create(
community_list=clist,
email=Email.objects.filter(person__user__username="plain").first(),
notify_on="all",
)
mailbox_before = len(outbox)
active_state = State.objects.get(type="draft", slug="active")
system = Person.objects.get(name="(System)")
add_state_change_event(draft, system, None, active_state)
self.assertEqual(len(outbox), mailbox_before)
event = add_state_change_event(draft, system, None, active_state)
notify_event_to_subscribers(event)
self.assertEqual(mock_send_mail_text.call_count, 1)
address = mock_send_mail_text.call_args[0][1]
subject = mock_send_mail_text.call_args[0][3]
content = mock_send_mail_text.call_args[0][4]
self.assertEqual(address, sub_to_all.email.address)
self.assertIn(draft.name, subject)
self.assertIn(clist.long_name(), content)
mailbox_before = len(outbox)
rfc_state = State.objects.get(type="draft", slug="rfc")
add_state_change_event(draft, system, active_state, rfc_state)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue(draft.name in outbox[-1]["Subject"])
event = add_state_change_event(draft, system, active_state, rfc_state)
mock_send_mail_text.reset_mock()
notify_event_to_subscribers(event)
self.assertEqual(mock_send_mail_text.call_count, 2)
addresses = [call_args[0][1] for call_args in mock_send_mail_text.call_args_list]
subjects = {call_args[0][3] for call_args in mock_send_mail_text.call_args_list}
contents = {call_args[0][4] for call_args in mock_send_mail_text.call_args_list}
self.assertCountEqual(
addresses,
[sub_to_significant.email.address, sub_to_all.email.address],
)
self.assertEqual(len(subjects), 1)
self.assertIn(draft.name, subjects.pop())
self.assertEqual(len(contents), 1)
self.assertIn(clist.long_name(), contents.pop())
@mock.patch("ietf.community.utils.notify_event_to_subscribers")
def test_notify_event_to_subscribers_task(self, mock_notify):
d = DocEventFactory()
notify_event_to_subscribers_task(event_id=d.pk)
self.assertEqual(mock_notify.call_count, 1)
self.assertEqual(mock_notify.call_args, mock.call(d))
mock_notify.reset_mock()
d.delete()
notify_event_to_subscribers_task(event_id=d.pk)
self.assertFalse(mock_notify.called)