Skip to content

Commit 70ab711

Browse files
refactor: separate concerns / rename notify_events (#8328)
* refactor: separate signal receiver from work * test: split test to match code structure * test: fix test * refactor: reorg signals in community app
1 parent ec67f34 commit 70ab711

File tree

5 files changed

+96
-66
lines changed

5 files changed

+96
-66
lines changed

ietf/community/apps.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
3+
from django.apps import AppConfig
4+
5+
6+
class CommunityConfig(AppConfig):
7+
name = "ietf.community"
8+
9+
def ready(self):
10+
"""Initialize the app after the registry is populated"""
11+
# implicitly connects @receiver-decorated signals
12+
from . import signals # pyflakes: ignore

ietf/community/models.py

+2-33
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
# Copyright The IETF Trust 2012-2020, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

4-
5-
from django.conf import settings
6-
from django.db import models, transaction
7-
from django.db.models import signals
4+
from django.db import models
85
from django.urls import reverse as urlreverse
96

10-
from ietf.doc.models import Document, DocEvent, State
7+
from ietf.doc.models import Document, State
118
from ietf.group.models import Group
129
from ietf.person.models import Person, Email
1310
from ietf.utils.models import ForeignKey
1411

15-
from .tasks import notify_event_to_subscribers_task
16-
1712

1813
class CommunityList(models.Model):
1914
person = ForeignKey(Person, blank=True, null=True)
@@ -98,29 +93,3 @@ class EmailSubscription(models.Model):
9893

9994
def __str__(self):
10095
return "%s to %s (%s changes)" % (self.email, self.community_list, self.notify_on)
101-
102-
103-
def notify_events(sender, instance, **kwargs):
104-
if not isinstance(instance, DocEvent):
105-
return
106-
107-
if not kwargs.get("created", False):
108-
return # only notify on creation
109-
110-
if instance.doc.type_id != 'draft':
111-
return
112-
113-
if getattr(instance, "skip_community_list_notification", False):
114-
return
115-
116-
# kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to
117-
# start a Celery task during tests. To prevent this, don't queue a celery task if we're running
118-
# tests.
119-
if settings.SERVER_MODE != "test":
120-
# Wrap in on_commit in case a transaction is open
121-
transaction.on_commit(
122-
lambda: notify_event_to_subscribers_task.delay(event_id=instance.pk)
123-
)
124-
125-
126-
signals.post_save.connect(notify_events)

ietf/community/signals.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
3+
from django.conf import settings
4+
from django.db import transaction
5+
from django.db.models.signals import post_save
6+
from django.dispatch import receiver
7+
8+
from ietf.doc.models import DocEvent
9+
from .tasks import notify_event_to_subscribers_task
10+
11+
12+
def notify_of_event(event: DocEvent):
13+
"""Send subscriber notification emails for a 'draft'-related DocEvent
14+
15+
If the event is attached to a draft of type 'doc', queues a task to send notification emails to
16+
community list subscribers. No emails will be sent when SERVER_MODE is 'test'.
17+
"""
18+
if event.doc.type_id != "draft":
19+
return
20+
21+
if getattr(event, "skip_community_list_notification", False):
22+
return
23+
24+
# kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to
25+
# start a Celery task during tests. To prevent this, don't queue a celery task if we're running
26+
# tests.
27+
if settings.SERVER_MODE != "test":
28+
# Wrap in on_commit in case a transaction is open
29+
transaction.on_commit(
30+
lambda: notify_event_to_subscribers_task.delay(event_id=event.pk)
31+
)
32+
33+
34+
# dispatch_uid ensures only a single signal receiver binding is made
35+
@receiver(post_save, dispatch_uid="notify_of_events_receiver_uid")
36+
def notify_of_events_receiver(sender, instance, **kwargs):
37+
"""Call notify_of_event after saving a new DocEvent"""
38+
if not isinstance(instance, DocEvent):
39+
return
40+
41+
if not kwargs.get("created", False):
42+
return # only notify on creation
43+
44+
notify_of_event(instance)

ietf/community/tests.py

+36-31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Copyright The IETF Trust 2016-2023, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

4-
54
import mock
65
from pyquery import PyQuery
76

@@ -11,6 +10,7 @@
1110
import debug # pyflakes:ignore
1211

1312
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
13+
from ietf.community.signals import notify_of_event
1414
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
1515
from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers
1616
from ietf.community.tasks import notify_event_to_subscribers_task
@@ -431,53 +431,58 @@ def test_subscription_for_group(self):
431431
r = self.client.get(url)
432432
self.assertEqual(r.status_code, 200)
433433

434-
# Mock out the on_commit call so we can tell whether the task was actually queued
435-
@mock.patch("ietf.submit.views.transaction.on_commit", side_effect=lambda x: x())
436-
@mock.patch("ietf.community.models.notify_event_to_subscribers_task")
437-
def test_notification_signal_receiver(self, mock_notify_task, mock_on_commit):
438-
"""Saving a DocEvent should notify subscribers
434+
@mock.patch("ietf.community.signals.notify_of_event")
435+
def test_notification_signal_receiver(self, mock_notify_of_event):
436+
"""Saving a newly created DocEvent should notify subscribers
439437
440-
This implicitly tests that notify_events is hooked up to the post_save signal.
438+
This implicitly tests that notify_of_event_receiver is hooked up to the post_save signal.
441439
"""
442440
# Arbitrary model that's not a DocEvent
443-
person = PersonFactory()
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-
person.save()
449-
self.assertFalse(mock_notify_task.delay.called)
450-
441+
person = PersonFactory.build() # builds but does not save...
442+
mock_notify_of_event.reset_mock() # clear any calls that resulted from the factories
443+
person.save()
444+
self.assertFalse(mock_notify_of_event.called)
445+
451446
# build a DocEvent that is not yet persisted
452447
doc = DocumentFactory()
453-
d = DocEventFactory.build(by=person, doc=doc)
454-
# mock_notify_task.reset_mock() # clear any calls that resulted from the factories
448+
event = DocEventFactory.build(by=person, doc=doc) # builds but does not save...
449+
mock_notify_of_event.reset_mock() # clear any calls that resulted from the factories
450+
event.save()
451+
self.assertEqual(mock_notify_of_event.call_count, 1, "notify_task should be run on creation of DocEvent")
452+
self.assertEqual(mock_notify_of_event.call_args, mock.call(event))
453+
454+
# save the existing DocEvent and see that no notification is sent
455+
mock_notify_of_event.reset_mock()
456+
event.save()
457+
self.assertFalse(mock_notify_of_event.called, "notify_task should not be run save of on existing DocEvent")
458+
459+
# Mock out the on_commit call so we can tell whether the task was actually queued
460+
@mock.patch("ietf.submit.views.transaction.on_commit", side_effect=lambda x: x())
461+
@mock.patch("ietf.community.signals.notify_event_to_subscribers_task")
462+
def test_notify_of_event(self, mock_notify_task, mock_on_commit):
463+
"""The community notification task should be called as intended"""
464+
person = PersonFactory() # builds but does not save...
465+
doc = DocumentFactory()
466+
event = DocEventFactory(by=person, doc=doc)
455467
# be careful overriding SERVER_MODE - we do it here because the method
456468
# under test does not make this call when in "test" mode
457469
with override_settings(SERVER_MODE="not-test"):
458-
d.save()
459-
self.assertEqual(mock_notify_task.delay.call_count, 1, "notify_task should be run on creation of DocEvent")
460-
self.assertEqual(mock_notify_task.delay.call_args, mock.call(event_id = d.pk))
461-
462-
mock_notify_task.reset_mock()
463-
with override_settings(SERVER_MODE="not-test"):
464-
d.save()
465-
self.assertFalse(mock_notify_task.delay.called, "notify_task should not be run save of on existing DocEvent")
466-
470+
notify_of_event(event)
471+
self.assertTrue(mock_notify_task.delay.called, "notify_task should run for a DocEvent on a draft")
467472
mock_notify_task.reset_mock()
468-
d = DocEventFactory.build(by=person, doc=doc)
469-
d.skip_community_list_notification = True
473+
474+
event.skip_community_list_notification = True
470475
# be careful overriding SERVER_MODE - we do it here because the method
471476
# under test does not make this call when in "test" mode
472477
with override_settings(SERVER_MODE="not-test"):
473-
d.save()
478+
notify_of_event(event)
474479
self.assertFalse(mock_notify_task.delay.called, "notify_task should not run when skip_community_list_notification is set")
475480

476-
d = DocEventFactory.build(by=person, doc=DocumentFactory(type_id="rfc"))
481+
event = DocEventFactory.build(by=person, doc=DocumentFactory(type_id="rfc"))
477482
# be careful overriding SERVER_MODE - we do it here because the method
478483
# under test does not make this call when in "test" mode
479484
with override_settings(SERVER_MODE="not-test"):
480-
d.save()
485+
notify_of_event(event)
481486
self.assertFalse(mock_notify_task.delay.called, "notify_task should not run on a document with type 'rfc'")
482487

483488
@mock.patch("ietf.utils.mail.send_mail_text")

ietf/utils/management/commands/loadrelated.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import debug # pyflakes:ignore
2525

26-
from ietf.community.models import notify_events
26+
from ietf.community.signals import notify_of_events_receiver
2727

2828
class Command(loaddata.Command):
2929
help = ("""
@@ -62,7 +62,7 @@ def handle(self, *args, **options):
6262
#
6363
self.serialization_formats = serializers.get_public_serializer_formats()
6464
#
65-
post_save.disconnect(notify_events)
65+
post_save.disconnect(notify_of_events_receiver())
6666
#
6767
connection = connections[self.using]
6868
self.fixture_count = 0

0 commit comments

Comments
 (0)