diff --git a/doc/api/resources/webhooks.rst b/doc/api/resources/webhooks.rst index 1257a9e5c..4f69ddcc7 100644 --- a/doc/api/resources/webhooks.rst +++ b/doc/api/resources/webhooks.rst @@ -31,8 +31,10 @@ action_types list of strings A list of actio The following values for ``action_types`` are valid with pretix core: * ``pretix.event.order.placed`` + * ``pretix.event.order.placed.require_approval`` * ``pretix.event.order.paid`` * ``pretix.event.order.canceled`` + * ``pretix.event.order.reactivated`` * ``pretix.event.order.expired`` * ``pretix.event.order.modified`` * ``pretix.event.order.contact.changed`` diff --git a/src/pretix/api/webhooks.py b/src/pretix/api/webhooks.py index ee553b11c..dd6d33335 100644 --- a/src/pretix/api/webhooks.py +++ b/src/pretix/api/webhooks.py @@ -7,7 +7,7 @@ import requests from celery.exceptions import MaxRetriesExceededError from django.db.models import Exists, OuterRef, Q 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 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): def build_payload(self, logentry: LogEntry): @@ -169,44 +230,69 @@ def register_default_webhook_events(sender, **kwargs): 'pretix.event.checkin.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) -def notify_webhooks(logentry_id: int): - logentry = LogEntry.all.select_related('event', 'event__organizer').get(id=logentry_id) +def notify_webhooks(logentry_ids: list): + 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: - return # We need to know the organizer + notification_type = logentry.webhook_type - types = get_all_webhook_events() - notification_type = None - 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: + break # Ignore, no webhooks for this event type - if not notification_type: - return # Ignore, no webhooks for this event type + if _org != logentry.organizer or _at != logentry.action_type or webhooks is None: + _org = logentry.organizer + _at = logentry.action_type - # All webhooks that registered for this notification - event_listener = WebHookEventListener.objects.filter( - webhook=OuterRef('pk'), - action_type=notification_type.action_type - ) + # All webhooks that registered for this notification + event_listener = WebHookEventListener.objects.filter( + webhook=OuterRef('pk'), + 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( - 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) - ) - - for wh in webhooks: - send_webhook.apply_async(args=(logentry_id, notification_type.action_type, wh.pk)) + 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) diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 4ca262124..69df3fc55 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -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.deactivated': _('The test mode has been disabled.'), '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.deleted': _('An answer option has been removed from the question.'), 'pretix.event.question.option.changed': _('An answer option has been changed.'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index cc3b3e82f..9b550d5de 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -937,6 +937,7 @@ class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixi data={ 'event_id': self.request.event.pk, 'name': str(self.request.event.name), + 'slug': self.request.event.slug, 'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True)) } ) diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py index 6785e83f3..c8d0781b3 100644 --- a/src/pretix/control/views/main.py +++ b/src/pretix/control/views/main.py @@ -236,6 +236,10 @@ class EventWizard(SafeSessionWizardView): event.has_subevents = foundation_data['has_subevents'] event.testmode = True 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 basics_data["team"] is not None: