Add webhooks for changes to events and subevents

This commit is contained in:
Raphael Michel
2020-11-06 11:46:54 +01:00
parent eeb3c1a960
commit df83682d55
5 changed files with 124 additions and 31 deletions

View File

@@ -31,8 +31,10 @@ action_types list of strings A list of actio
The following values for ``action_types`` are valid with pretix core: The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.order.placed`` * ``pretix.event.order.placed``
* ``pretix.event.order.placed.require_approval``
* ``pretix.event.order.paid`` * ``pretix.event.order.paid``
* ``pretix.event.order.canceled`` * ``pretix.event.order.canceled``
* ``pretix.event.order.reactivated``
* ``pretix.event.order.expired`` * ``pretix.event.order.expired``
* ``pretix.event.order.modified`` * ``pretix.event.order.modified``
* ``pretix.event.order.contact.changed`` * ``pretix.event.order.contact.changed``

View File

@@ -7,7 +7,7 @@ import requests
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import scope, scopes_disabled from django_scopes import scope, scopes_disabled
from requests import RequestException from requests import RequestException
@@ -97,6 +97,67 @@ class ParametrizedOrderWebhookEvent(WebhookEvent):
} }
class ParametrizedEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
if logentry.action_type == 'pretix.event.deleted':
organizer = logentry.content_object
return {
'notification_id': logentry.pk,
'organizer': organizer.slug,
'event': logentry.parsed_data.get('slug'),
'action': logentry.action_type,
}
event = logentry.content_object
if not event:
return None
return {
'notification_id': logentry.pk,
'organizer': event.organizer.slug,
'event': event.slug,
'action': logentry.action_type,
}
class ParametrizedSubEventWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name):
self._action_type = action_type
self._verbose_name = verbose_name
super().__init__()
@property
def action_type(self):
return self._action_type
@property
def verbose_name(self):
return self._verbose_name
def build_payload(self, logentry: LogEntry):
# do not use content_object, this is also called in deletion
return {
'notification_id': logentry.pk,
'organizer': logentry.event.organizer.slug,
'event': logentry.event.slug,
'subevent': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent): class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
def build_payload(self, logentry: LogEntry): def build_payload(self, logentry: LogEntry):
@@ -169,44 +230,69 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.checkin.reverted', 'pretix.event.checkin.reverted',
_('Ticket check-in reverted'), _('Ticket check-in reverted'),
), ),
ParametrizedEventWebhookEvent(
'pretix.event.added',
_('Event created'),
),
ParametrizedEventWebhookEvent(
'pretix.event.changed',
_('Event details changed'),
),
ParametrizedEventWebhookEvent(
'pretix.event.deleted',
_('Event details changed'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.added',
pgettext_lazy('subevent', 'Event series date added'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.changed',
pgettext_lazy('subevent', 'Event series date changed'),
),
ParametrizedSubEventWebhookEvent(
'pretix.subevent.deleted',
pgettext_lazy('subevent', 'Event series date deleted'),
),
) )
@app.task(base=TransactionAwareTask, acks_late=True) @app.task(base=TransactionAwareTask, acks_late=True)
def notify_webhooks(logentry_id: int): def notify_webhooks(logentry_ids: list):
logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id) if not isinstance(logentry_ids, list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related('event', 'event__organizer').filter(id__in=logentry_ids)
_org, _at, webhooks = None, None, None
for logentry in qs:
if not logentry.organizer:
break # We need to know the organizer
if not logentry.organizer: notification_type = logentry.webhook_type
return # We need to know the organizer
types = get_all_webhook_events() if not notification_type:
notification_type = None break # Ignore, no webhooks for this event type
typepath = logentry.action_type
while not notification_type and '.' in typepath:
notification_type = types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
typepath = typepath.rsplit('.', 1)[0]
if not notification_type: if _org != logentry.organizer or _at != logentry.action_type or webhooks is None:
return # Ignore, no webhooks for this event type _org = logentry.organizer
_at = logentry.action_type
# All webhooks that registered for this notification # All webhooks that registered for this notification
event_listener = WebHookEventListener.objects.filter( event_listener = WebHookEventListener.objects.filter(
webhook=OuterRef('pk'), webhook=OuterRef('pk'),
action_type=notification_type.action_type action_type=notification_type.action_type
) )
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter(
organizer=logentry.organizer,
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter( for wh in webhooks:
organizer=logentry.organizer, send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))
has_el=True,
enabled=True
)
if logentry.event_id:
webhooks = webhooks.filter(
Q(all_events=True) | Q(limit_events__pk=logentry.event_id)
)
for wh in webhooks:
send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk))
@app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True) @app.task(base=ProfiledTask, bind=True, max_retries=9, acks_late=True)

View File

@@ -397,7 +397,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.testmode.activated': _('The shop has been taken into test mode.'), 'pretix.event.testmode.activated': _('The shop has been taken into test mode.'),
'pretix.event.testmode.deactivated': _('The test mode has been disabled.'), 'pretix.event.testmode.deactivated': _('The test mode has been disabled.'),
'pretix.event.added': _('The event has been created.'), 'pretix.event.added': _('The event has been created.'),
'pretix.event.changed': _('The event settings have been changed.'), 'pretix.event.changed': _('The event details have been changed.'),
'pretix.event.question.option.added': _('An answer option has been added to the question.'), 'pretix.event.question.option.added': _('An answer option has been added to the question.'),
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'), 'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
'pretix.event.question.option.changed': _('An answer option has been changed.'), 'pretix.event.question.option.changed': _('An answer option has been changed.'),

View File

@@ -937,6 +937,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi
data={ data={
'event_id': self.request.event.pk, 'event_id': self.request.event.pk,
'name': str(self.request.event.name), 'name': str(self.request.event.name),
'slug': self.request.event.slug,
'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True)) 'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True))
} }
) )

View File

@@ -236,6 +236,10 @@ class EventWizard(SafeSessionWizardView):
event.has_subevents = foundation_data['has_subevents'] event.has_subevents = foundation_data['has_subevents']
event.testmode = True event.testmode = True
form_dict['basics'].save() form_dict['basics'].save()
event.log_action(
'pretix.event.added',
user=self.request.user,
)
if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer): if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer):
if basics_data["team"] is not None: if basics_data["team"] is not None: