Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
82e4c331fc Log entries: Check config before bulk-scheduling 2025-10-02 14:03:31 +02:00
3 changed files with 73 additions and 19 deletions

View File

@@ -22,6 +22,7 @@
from datetime import timedelta from datetime import timedelta
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ 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): class WebHook(models.Model):
organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks') organizer = models.ForeignKey('pretixbase.Organizer', on_delete=models.CASCADE, related_name='webhooks')
enabled = models.BooleanField(default=True, verbose_name=_("Enable webhook")) 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) 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) comment = models.CharField(verbose_name=_("Comment"), max_length=255, null=True, blank=True)
objects = models.Manager.from_queryset(WebHookQuerySet)()
class Meta: class Meta:
ordering = ('id',) ordering = ('id',)

View File

@@ -27,16 +27,13 @@ from datetime import timedelta
import requests import requests
from django.db import DatabaseError, connection, transaction from django.db import DatabaseError, connection, transaction
from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy 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
from pretix.api.models import ( from pretix.api.models import WebHook, WebHookCall, WebHookCallRetry
WebHook, WebHookCall, WebHookCallRetry, WebHookEventListener,
)
from pretix.api.signals import register_webhook_events from pretix.api.signals import register_webhook_events
from pretix.base.models import LogEntry from pretix.base.models import LogEntry
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
@@ -454,20 +451,9 @@ def notify_webhooks(logentry_ids: list):
_org = logentry.organizer _org = logentry.organizer
_at = logentry.action_type _at = logentry.action_type
# All webhooks that registered for this notification webhooks = WebHook.objects.for_notification(
event_listener = WebHookEventListener.objects.filter( notification_type.action_type, logentry.organizer, logentry.event_id
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)
)
for wh in webhooks: for wh in webhooks:
send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk)) send_webhook.apply_async(args=(logentry.id, notification_type.action_type, wh.pk))

View File

@@ -38,8 +38,11 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import connections, models from django.db import connections, models
from django.db.models import Q
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pretix.api.models import WebHook
class VisibleOnlyManager(models.Manager): class VisibleOnlyManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@@ -181,12 +184,56 @@ class LogEntry(models.Model):
@classmethod @classmethod
def bulk_postprocess(cls, objects): def bulk_postprocess(cls, objects):
from pretix.api.webhooks import notify_webhooks from pretix.api.webhooks import notify_webhooks
from pretix.base.models import NotificationSetting
from ..services.notifications import notify 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: if to_notify:
notify.apply_async(args=(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: if to_wh:
notify_webhooks.apply_async(args=(to_wh,)) notify_webhooks.apply_async(args=(to_wh,))