Skip to content
Open
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
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ not released yet
----------------

* Fix documentation
* Allow rendering to work without saving the notification into DB

0.2.2 (2020-02-11)
------------------
Expand Down
55 changes: 38 additions & 17 deletions example/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pynotify.dispatchers import BaseDispatcher
from pynotify.handlers import BaseHandler
from pynotify.helpers import signal_map
from pynotify.models import AdminNotificationTemplate
from pynotify.models import AdminNotificationTemplate, Notification


# MOCK OBJECTS ------------------------------------------------------------------------------------
Expand Down Expand Up @@ -54,14 +54,20 @@ def get_related_objects(self):
def get_extra_data(self):
return {'some_value': 123}

def _can_handle(self):
return super()._can_handle() and self.signal_kwargs.get('can_handle', True)

def _can_create_notification(self, recipient):
return recipient.username != 'James'
return super()._can_create_notification(recipient) and self.signal_kwargs.get('can_create', True)

def _can_dispatch_notification(self, notification, dispatcher):
return notification.recipient.username != 'John'
def _can_save_notification(self, notification):
return super()._can_save_notification(notification) and self.signal_kwargs.get('can_save', True)

def _can_handle(self):
return super()._can_handle() and self.signal_kwargs.get('can_handle', True)
def _can_dispatch_notification(self, notification, dispatcher):
return (
super()._can_dispatch_notification(notification, dispatcher)
and self.signal_kwargs.get('can_dispatch', True)
)

class Meta:
signal = test_signal_data
Expand All @@ -87,7 +93,6 @@ class HandlerTestCase(TestCase):
def setUp(self):
self.user1 = User.objects.create_user('Jack')
self.user2 = User.objects.create_user('John')
self.user3 = User.objects.create_user('James')
self.template = AdminNotificationTemplate.objects.create(title='Hello slug!', slug='test_slug')

def test_handler_should_be_automatically_registered(self):
Expand Down Expand Up @@ -123,7 +128,10 @@ class Meta:

def test_handler_should_create_notification_using_template_data(self):
users = [self.user1, self.user2]
test_signal_data.send(sender=None, recipients=[self.user1, self.user2, self.user3])
test_signal_data.send(sender=None, recipients=[self.user1, self.user2])

self.assertEqual(Notification.objects.all().count(), 2)
self.assertEqual(len(MockDispatcher.dispatched_notifications), 2)

for user in users:
notification = user.notifications.get()
Expand All @@ -134,23 +142,36 @@ def test_handler_should_create_notification_using_template_data(self):
self.assertEqual(notification.get_extra_data(), {'some_value': 123})
self.assertEqual(related_object.name, 'first_recipient')
self.assertEqual(related_object.content_object, self.user1)
if user.username == 'Jack':
self.assertIn(notification, MockDispatcher.dispatched_notifications)

self.assertEqual(len(MockDispatcher.dispatched_notifications), 1)
self.assertIn(notification, MockDispatcher.dispatched_notifications)

# Repeated notification should use the same template
test_signal_data.send(sender=None, recipients=[self.user1])
notifications = self.user1.notifications.all()
self.assertEqual(notifications[0].template, notifications[1].template)

# Test _can_handle() method is used
self.user1.notifications.all().delete()
test_signal_data.send(sender=None, recipients=[self.user1], can_handle=False)
self.assertEqual(self.user1.notifications.count(), 0)

def test_handler_should_create_notification_using_template_slug(self):
test_signal_slug.send(sender=MockSender, recipients=[self.user1])
notification = self.user1.notifications.get()
self.assertEqual(notification.template.admin_template, self.template)
self.assertEqual(notification.title, 'Hello slug!')

def test_can_handle_method_should_be_used(self):
test_signal_data.send(sender=None, recipients=[self.user1], can_handle=False)
self.assertEqual(self.user1.notifications.count(), 0)
self.assertEqual(len(MockDispatcher.dispatched_notifications), 0)

def test_can_create_notification_method_should_be_used(self):
test_signal_data.send(sender=None, recipients=[self.user1], can_create=False)
self.assertEqual(self.user1.notifications.count(), 0)
self.assertEqual(len(MockDispatcher.dispatched_notifications), 0)

def test_can_save_notification_method_should_be_used(self):
test_signal_data.send(sender=None, recipients=[self.user1], can_save=False)
self.assertEqual(self.user1.notifications.count(), 0)
self.assertEqual(len(MockDispatcher.dispatched_notifications), 1)
self.assertIsNone(MockDispatcher.dispatched_notifications[0].pk)

def test_can_dispatch_notification_method_should_be_used(self):
test_signal_data.send(sender=None, recipients=[self.user1], can_dispatch=False)
self.assertEqual(self.user1.notifications.count(), 1)
self.assertEqual(len(MockDispatcher.dispatched_notifications), 0)
90 changes: 70 additions & 20 deletions example/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,26 +103,28 @@ def setUp(self):
self.author = User.objects.create_user('John')
self.article = Article.objects.create(title='The Old Witch', author=self.author)
self.random_user = User.objects.create_user('Mr.Random')
self.random_user2 = User.objects.create_user('Mr.Random 2')

self.template = NotificationTemplate.objects.create(
title='{{article}}',
text='{{author}} created a new article named {{article}}.',
trigger_action='{{article.get_absolute_url}}',
)

self.notification = Notification.objects.create(
self.notification = Notification(
recipient=self.recipient,
template=self.template,
related_objects={
'article': self.article,
'author': self.article.author,
'random_user': self.random_user,
},
extra_data={
'some_value': 123,
'decimal_value': Decimal('1.55'),
}
)
self.notification.set_local_related_objects({
'article': self.article,
'author': self.article.author,
'random_user': self.random_user,
})
self.notification.set_extra_data({
'some_value': 123,
'decimal_value': Decimal('1.55'),
})
self.notification.save()

def test_generated_fields_should_use_template_for_rendering(self):
self.assertEqual(self.notification.title, 'The Old Witch')
Expand All @@ -148,11 +150,27 @@ def test_extra_data_should_be_dictionary(self):
self.notification.set_extra_data(1000)

def test_related_objects_and_extra_data_should_not_contain_same_keys(self):
self.notification.set_local_related_objects({'random_user': self.random_user})
with self.assertRaises(ValueError):
self.notification.context
with self.assertRaises(ValueError):
self.notification.save()

self.notification.set_local_related_objects({'some_value': self.random_user})
with self.assertRaises(ValueError):
self.notification.context
with self.assertRaises(ValueError):
self.notification.save()

self.notification.set_local_related_objects({})
self.notification.set_extra_data({'article': 123})
with self.assertRaises(ValueError):
self.notification.context
with self.assertRaises(ValueError):
self.notification.save()

def test_context_should_contain_related_objects_as_proxies_and_extra_data(self):
self.notification.set_local_related_objects({'local_user': self.random_user2})
ctx = self.notification.context

self.assertTrue(isinstance(ctx['article'], SecureRelatedObject))
Expand All @@ -164,6 +182,9 @@ def test_context_should_contain_related_objects_as_proxies_and_extra_data(self):
self.assertTrue(isinstance(ctx['random_user'], SecureRelatedObject))
self.assertEqual(ctx['random_user']._object, self.random_user)

self.assertTrue(isinstance(ctx['local_user'], SecureRelatedObject))
self.assertEqual(ctx['local_user']._object, self.random_user2)

self.assertEqual(ctx['some_value'], 123)
self.assertEqual(ctx['decimal_value'], '1.55')

Expand All @@ -186,26 +207,55 @@ def test_creating_notification_should_not_be_possible_with_related_objects_in_in
['abc', 'abc'],
{'abc': 'abc'},
)
notification = Notification.objects.create(recipient=self.recipient, template=self.template)
for related_objects in INVALID_RELATED_OBJECTS:
with self.assertRaises(TypeError):
Notification.objects.create(
recipient=self.recipient,
template=self.template,
related_objects=related_objects
)
notification.set_local_related_objects(related_objects)
notification.save()
notification.set_local_related_objects({})

def test_creating_notification_should_allow_list_of_related_objects(self):
notification = Notification.objects.create(
recipient=self.recipient,
template=self.template,
related_objects=[self.random_user],
)
notification = Notification(recipient=self.recipient, template=self.template)
notification.set_local_related_objects([self.random_user])
notification.save()

self.assertEqual(notification.related_objects.count(), 1)
related_object = notification.related_objects.get()
self.assertEqual(related_object.name, None)
self.assertEqual(related_object.content_object, self.random_user)
self.assertEqual(notification.context, {})

def test_saving_notification_should_save_list_of_local_related_objects_into_db(self):
notification = Notification(recipient=self.recipient, template=self.template)

notification.set_local_related_objects([self.random_user2])
self.assertEqual(len(notification._local_related_objects_list), 1)
self.assertEqual(notification._local_related_objects_list[0], self.random_user2)
self.assertEqual(notification.related_objects.count(), 0)

notification.save()
self.assertEqual(len(notification._local_related_objects_list), 0)
self.assertEqual(notification.related_objects.count(), 1)

related_object = notification.related_objects.get()
self.assertEqual(related_object.name, None)
self.assertEqual(related_object.content_object, self.random_user2)

def test_saving_notification_should_save_dictionary_of_local_related_objects_into_db(self):
notification = Notification(recipient=self.recipient, template=self.template)

notification.set_local_related_objects({'random_user2': self.random_user2})
self.assertEqual(len(notification._local_related_objects_dict), 1)
self.assertEqual(notification._local_related_objects_dict['random_user2'], self.random_user2)
self.assertEqual(notification.related_objects.count(), 0)

notification.save()
self.assertEqual(len(notification._local_related_objects_dict), 0)
self.assertEqual(notification.related_objects.count(), 1)

related_object = notification.related_objects.get()
self.assertEqual(related_object.name, 'random_user2')
self.assertEqual(related_object.content_object, self.random_user2)

def test_notification_should_have_string_representation(self):
self.assertEqual(str(self.notification), 'notification #{}'.format(self.notification.pk))
84 changes: 52 additions & 32 deletions pynotify/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class BaseHandler(metaclass=HandlerMeta):

@cached_property
def _template(self):
"""
Returns notification template that will be used for creation of notification(s).
"""
template_slug = self.get_template_slug()
if template_slug:
admin_template = AdminNotificationTemplate.objects.get(slug=template_slug)
Expand All @@ -75,50 +78,62 @@ def _template(self):

return template

def _create_notification(self, recipient):
"""
Creates notification for ``recipient``.
"""
notification = Notification(recipient=recipient, template=self._template)

extra_data = self.get_extra_data()
if extra_data:
notification.set_extra_data(extra_data)

related_objects = self.get_related_objects()
if related_objects:
notification.set_local_related_objects(related_objects)

if self._can_save_notification(notification):
notification.save()

return notification

def _init_dispatchers(self):
self.dispatchers = []
"""
Initializes dipatchers that will be used for sending of notification(s).
"""
self._dispatchers = []
dispatcher_classes = self.get_dispatcher_classes()
if dispatcher_classes:
for dispatcher_class in dispatcher_classes:
self.dispatchers.append(self._init_dispatcher(dispatcher_class))
self._dispatchers.append(self._init_dispatcher(dispatcher_class))

def _init_dispatcher(self, dispatcher_class):
return dispatcher_class()

def _can_create_notification(self, recipient):
"""
Returns ``True`` if notification can be created for ``recipient``.
Initializes a single dispatcher. Override this method if you need specific initialization procedure.
"""
return True
return dispatcher_class()

def _create_notification(self, recipient):
def _can_handle(self):
"""
Creates notification for ``recipient``.
Returns ``True`` if handler can handle creating of notification(s).
"""
if self._can_create_notification(recipient):
return Notification.objects.create(
recipient=recipient,
template=self._template,
related_objects=self.get_related_objects(),
extra_data=self.get_extra_data(),
)
return True

def _can_dispatch_notification(self, notification, dispatcher):
def _can_create_notification(self, recipient):
"""
Returns ``True`` if ``notification`` can be dispatched using ``dispatcher``.
Returns ``True`` if notification can be created for ``recipient``.
"""
return True

def _dispatch_notification(self, notification, dispatcher):
def _can_save_notification(self, notification):
"""
Dispatches ``notification`` using ``dispatcher``.
Returns ``True`` if ``notification`` can be saved into DB.
"""
if self._can_dispatch_notification(notification, dispatcher):
dispatcher.dispatch(notification)
return True

def _can_handle(self):
def _can_dispatch_notification(self, notification, dispatcher):
"""
Returns ``True`` if handler should handle creating of notification(s).
Returns ``True`` if ``notification`` can be dispatched using ``dispatcher``.
"""
return True

Expand All @@ -127,14 +142,19 @@ def handle(self, signal_kwargs):
Handles creation of notifications from ``signal_kwargs``.
"""
self.signal_kwargs = signal_kwargs
if self._can_handle():
self._init_dispatchers()
for recipient in self.get_recipients():
notification = self._create_notification(recipient)

if notification:
for dispatcher in self.dispatchers:
self._dispatch_notification(notification, dispatcher)

if not self._can_handle():
return

self._init_dispatchers()
for recipient in self.get_recipients():
if not self._can_create_notification(recipient):
continue

notification = self._create_notification(recipient)
for dispatcher in self._dispatchers:
if self._can_dispatch_notification(notification, dispatcher):
dispatcher.dispatch(notification)

def get_recipients(self):
"""
Expand Down
Loading