diff --git a/src/pretix/api/models.py b/src/pretix/api/models.py index 39787fc309..6f605166d4 100644 --- a/src/pretix/api/models.py +++ b/src/pretix/api/models.py @@ -22,6 +22,7 @@ from datetime import timedelta from django.db import models +from django.db.models import Exists, OuterRef, Q from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -111,6 +112,24 @@ class OAuthRefreshToken(AbstractRefreshToken): ) +class WebHookQuerySet(models.QuerySet): + def for_notification(self, action_type, organizer, event): + event_listener = WebHookEventListener.objects.filter( + webhook=OuterRef('pk'), + action_type=action_type + ) + webhooks = WebHook.objects.annotate(has_el=Exists(event_listener)).filter( + organizer=organizer, + has_el=True, + enabled=True + ) + if event: + webhooks = webhooks.filter( + Q(all_events=True) | Q(limit_events__pk=event.pk if not isinstance(event, int) else event) + ) + return webhooks + + class WebHook(models.Model): organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks') enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook")) @@ -119,6 +138,8 @@ class WebHook(models.Model): limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True) comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True) + objects = models.Manager.from_queryset(WebHookQuerySet)() + class Meta: ordering = ('id',) diff --git a/src/pretix/api/webhooks.py b/src/pretix/api/webhooks.py index 2dcf525e9e..a633a52757 100644 --- a/src/pretix/api/webhooks.py +++ b/src/pretix/api/webhooks.py @@ -27,16 +27,13 @@ from datetime import timedelta import requests from django.db import DatabaseError, connection, transaction -from django.db.models import Exists, OuterRef, Q from django.dispatch import receiver from django.utils.timezone import now from django.utils.translation import gettext_lazy as _, pgettext_lazy from django_scopes import scope, scopes_disabled from requests import RequestException -from pretix.api.models import ( - WebHook, WebHookCall, WebHookCallRetry, WebHookEventListener, -) +from pretix.api.models import WebHook, WebHookCall, WebHookCallRetry from pretix.api.signals import register_webhook_events from pretix.base.models import LogEntry from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask @@ -454,20 +451,9 @@ def notify_webhooks(logentry_ids: list): _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 + webhooks = WebHook.objects.for_notification( + notification_type.action_type, logentry.organizer, 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)) diff --git a/src/pretix/base/models/log.py b/src/pretix/base/models/log.py index 368bd99f10..39a65c4d50 100644 --- a/src/pretix/base/models/log.py +++ b/src/pretix/base/models/log.py @@ -38,8 +38,11 @@ import logging from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import connections, models +from django.db.models import Q from django.utils.functional import cached_property +from pretix.api.models import WebHook + class VisibleOnlyManager(models.Manager): def get_queryset(self): @@ -181,12 +184,56 @@ class LogEntry(models.Model): @classmethod def bulk_postprocess(cls, objects): from pretix.api.webhooks import notify_webhooks + from pretix.base.models import NotificationSetting from ..services.notifications import notify - to_notify = [o.id for o in objects if o.notification_type] + # Regular LogEntry.save() always kicks off notify and notify_webhooks tasks, regardless of whether a webhook + # listener exists. However, in bulk processing, it makes sense to check once and then only create the task if + # there is something to do. + _webhook_active_cache = {} + _notification_active_cache = {} + + def _is_webhook_active(logentry): + nonlocal _webhook_active_cache + + key = (logentry.action_type, logentry.organizer_id, logentry.event_id) + if key not in _webhook_active_cache: + notification_type = logentry.webhook_type + _webhook_active_cache[key] = notification_type and WebHook.objects.for_notification( + notification_type.action_type, logentry.organizer, logentry.event_id + ).exists() + return _webhook_active_cache[key] + + def _is_notification_active(logentry): + nonlocal _notification_active_cache + + key = (logentry.action_type, logentry.organizer_id, logentry.event_id) + if key not in _notification_active_cache: + notification_type = logentry.notification_type + if notification_type and logentry.event: + # We only have event-related notifications right now + users = logentry.event.get_users_with_permission( + notification_type.required_permission + ).filter(notifications_send=True, is_active=True) + + _notification_active_cache[key] = NotificationSetting.objects.filter( + # This is not technically fully correct since it's returning True if a user has the + # notification enabled on the global level and then disabled on the per-event-level, + # but it's good enough as a first check to avoid useless celery tasks for bulk actions + Q(event_id=logentry.event_id) | Q(event__isnull=True), + action_type=notification_type.action_type, + user__pk__in=users.values_list('pk', flat=True), + enabled=True, + ).exists() + + else: + _webhook_active_cache[key] = False + return _notification_active_cache[key] + + to_notify = [o.id for o in objects if _is_notification_active(o)] if to_notify: notify.apply_async(args=(to_notify,)) - to_wh = [o.id for o in objects if o.webhook_type] + to_wh = [o.id for o in objects if _is_webhook_active(o)] if to_wh: notify_webhooks.apply_async(args=(to_wh,))