"""
thorn.webhook.decorators
========================
Webhook decorators.
"""
from __future__ import absolute_import, unicode_literals
from itertools import chain
from six import iteritems as items, itervalues as values
from six.moves import UserDict
[docs]class webhook_model(UserDict):
"""Decorates models to send webhooks based changes to that model.
:keyword on_create: Event to dispatch whenever an instance of
this model is created (``post_save``).
:keyword on_change: Event to dispatch whenever an instance of
this model is changed (``post_save``).
:keyword on_delete: Event to dispatch whenever an instance of
this model is deleted (``post_delete``).
:keyword on_$event: Additional user defined events.,
:keyword sender_field:
Default field used as a sender for events, e.g. ``"account.user"``,
will use ``instance.account.user``.
Individual events can override the sender field user.
:keyword reverse:
A :class:`thorn.reverse.model_reverser` instance (or any callable
taking an model instance as argument), that describes how to get
the URL for an instance of this model.
Individual events can override the reverser used.
**Examples**
Simple article model, where the URL reference is retrieved
by ``reverse('article-detail', kwargs={'uuid': article.uuid})``:
.. code-block:: python
@webhook_model(
on_create=ModelEvent('article.created'),
on_change=ModelEvent('article.changed'),
on_delete=ModelEvent('article.removed'),
on_deactivate=ModelEvent(
'article.deactivate', deactivated__eq=True,
)
reverse=model_reverser('article-detail', uuid='uuid'),
)
class Article(models.Model):
uuid = models.UUIDField()
The URL may not actually exist after deletion, so maybe we want
to point the reference to something else in that special case,
like a category that can be reversed by doing
``reverse('category-detail', args=[article.category.name])``.
We can do that by having the ``on_delete`` event override
the reverser used for that event only:
.. code-block:: python
@webhook_model(
on_create=ModelEvent('article.created'),
on_change=ModelEvent('article.changed'),
on_delete=ModelEvent(
'article.removed',
reverse=model_reverser('category-detail', 'category.name'),
),
on_hipri_delete=ModelEvent(
'article.internal_delete', priority__gte=30.0,
).dispatches_on_delete(),
reverse=model_reverser('article-detail', uuid='uuid'),
)
class Article(model.Model):
uuid = models.UUIDField()
category = models.ForeignKey('category')
"""
def __init__(self,
on_create=None, on_change=None, on_delete=None,
reverse=None, sender_field=None, **kwargs):
self.reverse = reverse
self.sender_field = sender_field
self.events = {}
self.update_events(
{k: v for k, v in items(kwargs) if k.startswith('on_')},
on_create=on_create and on_create.dispatches_on_create(),
on_change=on_change and on_change.dispatches_on_change(),
on_delete=on_delete and on_delete.dispatches_on_delete(),
)
[docs] def update_events(self, events, **kwargs):
self.events.update(self.connect_events(events, **kwargs))
[docs] def connect_events(self, events, **kwargs):
return {
k: self.contribute_to_event(v)
for k, v in chain(items(events), items(kwargs))
}
[docs] def contribute_to_event(self, event):
if event:
if event.reverse is None:
event.reverse = self.reverse
event.sender_field = self.sender_field
return event
def __call__(self, model):
[event.connect_model(model) for event in values(self.events) if event]
model.webhook_events = self
return model
@property
def data(self):
# for UserDict
return self.events