forked from CGM_Public/pretix_original
- [x] Data model - [x] UI - [x] Fire hooks - [x] Unit tests - [x] Display logs - [x] API to modify hooks - [x] Documentation - [x] More hooks!
This commit is contained in:
@@ -5,5 +5,8 @@ class PretixApiConfig(AppConfig):
|
||||
name = 'pretix.api'
|
||||
label = 'pretixapi'
|
||||
|
||||
def ready(self):
|
||||
from . import signals, webhooks # noqa
|
||||
|
||||
|
||||
default_app_config = 'pretix.api.PretixApiConfig'
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Generated by Django 2.1.1 on 2018-11-07 10:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0102_auto_20181017_0024'),
|
||||
('pretixapi', '0002_auto_20180604_1120'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WebHook',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='Enable webhook')),
|
||||
('target_url', models.URLField(verbose_name='Target URL')),
|
||||
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Organizer')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WebHookCall',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||
('target_url', models.URLField()),
|
||||
('is_retry', models.BooleanField(default=False)),
|
||||
('execution_time', models.FloatField(null=True)),
|
||||
('return_code', models.PositiveIntegerField(default=0)),
|
||||
('payload', models.TextField()),
|
||||
('response_body', models.TextField()),
|
||||
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WebHookEventListener',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action_type', models.CharField(max_length=255)),
|
||||
('webhook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixapi.WebHook')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhookcall',
|
||||
name='success',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='all_events',
|
||||
field=models.BooleanField(default=True, verbose_name='All events (including newly created ones)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='pretixbase.Organizer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhookcall',
|
||||
name='webhook',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='pretixapi.WebHook'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhookeventlistener',
|
||||
name='webhook',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='listeners', to='pretixapi.WebHook'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhookcall',
|
||||
name='action_type',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -68,3 +68,41 @@ class OAuthRefreshToken(AbstractRefreshToken):
|
||||
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
|
||||
related_name="refresh_token"
|
||||
)
|
||||
|
||||
|
||||
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"))
|
||||
target_url = models.URLField(verbose_name=_("Target URL"))
|
||||
all_events = models.BooleanField(default=True, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('pretixbase.Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
@property
|
||||
def action_types(self):
|
||||
return [
|
||||
l.action_type for l in self.listeners.all()
|
||||
]
|
||||
|
||||
|
||||
class WebHookEventListener(models.Model):
|
||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='listeners')
|
||||
action_type = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
ordering = ("action_type",)
|
||||
|
||||
|
||||
class WebHookCall(models.Model):
|
||||
webhook = models.ForeignKey('WebHook', on_delete=models.CASCADE, related_name='calls')
|
||||
datetime = models.DateTimeField(auto_now_add=True)
|
||||
target_url = models.URLField()
|
||||
action_type = models.CharField(max_length=255)
|
||||
is_retry = models.BooleanField(default=False)
|
||||
execution_time = models.FloatField(null=True)
|
||||
return_code = models.PositiveIntegerField(default=0)
|
||||
success = models.BooleanField(default=False)
|
||||
payload = models.TextField()
|
||||
response_body = models.TextField()
|
||||
|
||||
class Meta:
|
||||
ordering = ("-datetime",)
|
||||
|
||||
71
src/pretix/api/serializers/webhooks.py
Normal file
71
src/pretix/api/serializers/webhooks.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.webhooks import get_all_webhook_events
|
||||
from pretix.base.models import Event
|
||||
|
||||
|
||||
class EventRelatedField(serializers.SlugRelatedField):
|
||||
def get_queryset(self):
|
||||
return self.context['organizer'].events.all()
|
||||
|
||||
|
||||
class ActionTypesField(serializers.Field):
|
||||
def to_representation(self, instance: WebHook):
|
||||
return instance.action_types
|
||||
|
||||
def to_internal_value(self, data):
|
||||
types = get_all_webhook_events()
|
||||
for d in data:
|
||||
if d not in types:
|
||||
raise ValidationError('Invalid action type "%s".' % d)
|
||||
return {'action_types': data}
|
||||
|
||||
|
||||
class WebHookSerializer(I18nAwareModelSerializer):
|
||||
limit_events = EventRelatedField(
|
||||
slug_field='slug',
|
||||
queryset=Event.objects.none(),
|
||||
many=True
|
||||
)
|
||||
action_types = ActionTypesField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = WebHook
|
||||
fields = ('id', 'enabled', 'target_url', 'all_events', 'limit_events', 'action_types')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
for event in full_data.get('limit_events'):
|
||||
if self.context['organizer'] != event.organizer:
|
||||
raise ValidationError('One or more events do not belong to this organizer.')
|
||||
|
||||
if full_data.get('limit_events') and full_data.get('all_events'):
|
||||
raise ValidationError('You can set either limit_events or all_events.')
|
||||
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
action_types = validated_data.pop('action_types')
|
||||
inst = super().create(validated_data)
|
||||
for l in action_types:
|
||||
inst.listeners.create(action_type=l)
|
||||
return inst
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
action_types = validated_data.pop('action_types', None)
|
||||
instance = super().update(instance, validated_data)
|
||||
if action_types is not None:
|
||||
current_listeners = set(instance.listeners.values_list('action_type', flat=True))
|
||||
new_listeners = set(action_types)
|
||||
for l in current_listeners - new_listeners:
|
||||
instance.listeners.filter(action_type=l).delete()
|
||||
for l in new_listeners - current_listeners:
|
||||
instance.listeners.create(action_type=l)
|
||||
return instance
|
||||
21
src/pretix/api/signals.py
Normal file
21
src/pretix/api/signals.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.api.models import WebHookCall
|
||||
from pretix.base.signals import periodic_task
|
||||
|
||||
register_webhook_events = Signal(
|
||||
providing_args=[]
|
||||
)
|
||||
"""
|
||||
This signal is sent out to get all known webhook events. Receivers should return an
|
||||
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
|
||||
instances.
|
||||
"""
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
def cleanup_webhook_logs(sender, **kwargs):
|
||||
WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete()
|
||||
@@ -8,7 +8,7 @@ from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, item, oauth, order, organizer, voucher,
|
||||
waitinglist,
|
||||
waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -17,6 +17,7 @@ router.register(r'organizers', organizer.OrganizerViewSet)
|
||||
orga_router = routers.DefaultRouter()
|
||||
orga_router.register(r'events', event.EventViewSet)
|
||||
orga_router.register(r'subevents', event.SubEventViewSet)
|
||||
orga_router.register(r'webhooks', webhooks.WebHookViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
event_router.register(r'subevents', event.SubEventViewSet)
|
||||
|
||||
@@ -244,7 +244,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ignore_unpaid=ignore_unpaid,
|
||||
nonce=nonce,
|
||||
datetime=dt,
|
||||
questions_supported=self.request.data.get('questions_supported', True)
|
||||
questions_supported=self.request.data.get('questions_supported', True),
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
except RequiredQuestionsError as e:
|
||||
return Response({
|
||||
|
||||
49
src/pretix/api/views/webhooks.py
Normal file
49
src/pretix/api/views/webhooks.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.api.serializers.webhooks import WebHookSerializer
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
class WebHookViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = WebHookSerializer
|
||||
queryset = WebHook.objects.none()
|
||||
permission = 'can_change_organizer_settings'
|
||||
write_permission = 'can_change_organizer_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.webhooks.prefetch_related('listeners')
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.webhook.created',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(organizer=self.request.organizer)
|
||||
self.request.organizer.log_action(
|
||||
'pretix.webhook.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
self.request.organizer.log_action(
|
||||
'pretix.webhook.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk, 'enabled': False}
|
||||
)
|
||||
instance.enabled = False
|
||||
instance.save(update_fields=['enabled'])
|
||||
252
src/pretix/api/webhooks.py
Normal file
252
src/pretix/api/webhooks.py
Normal file
@@ -0,0 +1,252 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
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 ugettext_lazy as _
|
||||
from requests import RequestException
|
||||
|
||||
from pretix.api.models import WebHook, WebHookCall, WebHookEventListener
|
||||
from pretix.api.signals import register_webhook_events
|
||||
from pretix.base.models import LogEntry
|
||||
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
|
||||
from pretix.celery_app import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_EVENTS = None
|
||||
|
||||
|
||||
class WebhookEvent:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return '<WebhookEvent: {}>'.format(self.action_type)
|
||||
|
||||
@property
|
||||
def action_type(self) -> str:
|
||||
"""
|
||||
The action_type string that this notification handles, for example
|
||||
``"pretix.event.order.paid"``. Only one notification type should be registered
|
||||
per action type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name of this notification type.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def build_payload(self, logentry: LogEntry) -> dict:
|
||||
"""
|
||||
This is the main function that you should override. It is supposed to turn a log entry
|
||||
object into a dictionary that can be used as the webhook payload.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
|
||||
def get_all_webhook_events():
|
||||
global _ALL_EVENTS
|
||||
|
||||
if _ALL_EVENTS:
|
||||
return _ALL_EVENTS
|
||||
|
||||
types = OrderedDict()
|
||||
for recv, ret in register_webhook_events.send(None):
|
||||
if isinstance(ret, (list, tuple)):
|
||||
for r in ret:
|
||||
types[r.action_type] = r
|
||||
else:
|
||||
types[ret.action_type] = ret
|
||||
_ALL_EVENTS = types
|
||||
return types
|
||||
|
||||
|
||||
class ParametrizedOrderWebhookEvent(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):
|
||||
order = logentry.content_object
|
||||
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'organizer': order.event.organizer.slug,
|
||||
'event': order.event.slug,
|
||||
'code': order.code,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
|
||||
|
||||
class ParametrizedOrderPositionWebhookEvent(ParametrizedOrderWebhookEvent):
|
||||
|
||||
def build_payload(self, logentry: LogEntry):
|
||||
d = super().build_payload(logentry)
|
||||
d['orderposition_id'] = logentry.parsed_data.get('position')
|
||||
d['orderposition_positionid'] = logentry.parsed_data.get('positionid')
|
||||
d['checkin_list'] = logentry.parsed_data.get('list')
|
||||
d['first_checkin'] = logentry.parsed_data.get('first_checkin')
|
||||
|
||||
|
||||
@receiver(register_webhook_events, dispatch_uid="base_register_default_webhook_events")
|
||||
def register_default_webhook_events(sender, **kwargs):
|
||||
return (
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.placed',
|
||||
_('New order placed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.paid',
|
||||
_('Order marked as paid'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.canceled',
|
||||
_('Order canceled'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.expired',
|
||||
_('Order expired'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.modified',
|
||||
_('Order information changed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.contact.changed',
|
||||
_('Order contact address changed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.changed.*',
|
||||
_('Order changed'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.refund.created.externally',
|
||||
_('External refund of payment'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.refunded',
|
||||
_('Order refunded'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.approved',
|
||||
_('Order approved'),
|
||||
),
|
||||
ParametrizedOrderWebhookEvent(
|
||||
'pretix.event.order.denied',
|
||||
_('Order denied'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
'pretix.event.checkin',
|
||||
_('Ticket checked in'),
|
||||
),
|
||||
ParametrizedOrderPositionWebhookEvent(
|
||||
'pretix.event.checkin.reverted',
|
||||
_('Ticket check-in reverted'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@app.task(base=TransactionAwareTask)
|
||||
def notify_webhooks(logentry_id: int):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
|
||||
if not logentry.organizer:
|
||||
return # We need to know the organizer
|
||||
|
||||
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:
|
||||
return # Ignore, no webhooks for this event 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)
|
||||
)
|
||||
|
||||
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)
|
||||
def send_webhook(self, logentry_id: int, action_type: str, webhook_id: int):
|
||||
# 9 retries with 2**(2*x) timing is roughly 72 hours
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
webhook = WebHook.objects.get(id=webhook_id)
|
||||
|
||||
types = get_all_webhook_events()
|
||||
event_type = types.get(action_type)
|
||||
if not event_type or not webhook.enabled:
|
||||
return # Ignore, e.g. plugin not installed
|
||||
|
||||
payload = event_type.build_payload(logentry)
|
||||
t = time.time()
|
||||
|
||||
try:
|
||||
try:
|
||||
resp = requests.post(
|
||||
webhook.target_url,
|
||||
json=payload,
|
||||
allow_redirects=False
|
||||
)
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=resp.status_code,
|
||||
payload=json.dumps(payload),
|
||||
response_body=resp.text[:1024 * 1024],
|
||||
success=200 <= resp.status_code <= 299
|
||||
)
|
||||
if resp.status_code == 410:
|
||||
webhook.enabled = False
|
||||
webhook.save()
|
||||
elif resp.status_code > 299:
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except RequestException as e:
|
||||
WebHookCall.objects.create(
|
||||
webhook=webhook,
|
||||
action_type=logentry.action_type,
|
||||
target_url=webhook.target_url,
|
||||
is_retry=self.request.retries > 0,
|
||||
execution_time=time.time() - t,
|
||||
return_code=0,
|
||||
payload=json.dumps(payload),
|
||||
response_body=str(e)[:1024 * 1024]
|
||||
)
|
||||
raise self.retry(countdown=2 ** (self.request.retries * 2))
|
||||
except MaxRetriesExceededError:
|
||||
pass
|
||||
@@ -53,6 +53,7 @@ class LoggingMixin:
|
||||
from .organizer import TeamAPIToken
|
||||
from ..notifications import get_all_notification_types
|
||||
from ..services.notifications import notify
|
||||
from pretix.api.webhooks import get_all_webhook_events, notify_webhooks
|
||||
|
||||
event = None
|
||||
if isinstance(self, Event):
|
||||
@@ -80,8 +81,21 @@ class LoggingMixin:
|
||||
if save:
|
||||
logentry.save()
|
||||
|
||||
if action in get_all_notification_types():
|
||||
no_types = get_all_notification_types()
|
||||
wh_types = get_all_webhook_events()
|
||||
|
||||
no_type = None
|
||||
wh_type = None
|
||||
typepath = logentry.action_type
|
||||
while (not no_type or not wh_types) and '.' in typepath:
|
||||
wh_type = wh_type or wh_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
|
||||
no_type = no_type or no_types.get(typepath + ('.*' if typepath != logentry.action_type else ''))
|
||||
typepath = typepath.rsplit('.', 1)[0]
|
||||
|
||||
if no_type:
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
if wh_type:
|
||||
notify_webhooks.apply_async(args=(logentry.pk,))
|
||||
return logentry
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,16 @@ class LogEntry(models.Model):
|
||||
return response
|
||||
return self.action_type
|
||||
|
||||
@cached_property
|
||||
def organizer(self):
|
||||
if self.event:
|
||||
return self.event.organizer
|
||||
elif hasattr(self.content_object, 'event'):
|
||||
return self.content_object.event.organizer
|
||||
elif hasattr(self.content_object, 'organizer'):
|
||||
return self.content_object.organizer
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
|
||||
|
||||
@@ -225,7 +225,7 @@ def register_default_notification_types(sender, **kwargs):
|
||||
),
|
||||
ParametrizedOrderNotificationType(
|
||||
sender,
|
||||
'pretix.event.order.changed',
|
||||
'pretix.event.order.changed.*',
|
||||
_('Order changed'),
|
||||
_('Order {order.code} has been changed.')
|
||||
),
|
||||
|
||||
@@ -59,7 +59,8 @@ def _save_answers(op, answers, given_answers):
|
||||
|
||||
@transaction.atomic
|
||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True):
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None):
|
||||
"""
|
||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||
not valid at this time.
|
||||
@@ -133,7 +134,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'forced': op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'list': clist.pk
|
||||
})
|
||||
}, user=user, auth=auth)
|
||||
else:
|
||||
if not force:
|
||||
raise CheckInError(
|
||||
@@ -147,4 +148,4 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'forced': force,
|
||||
'datetime': dt,
|
||||
'list': clist.pk
|
||||
})
|
||||
}, user=user, auth=auth)
|
||||
|
||||
@@ -17,9 +17,15 @@ def notify(logentry_id: int):
|
||||
if not logentry.event:
|
||||
return # Ignore, we only have event-related notifications right now
|
||||
types = get_all_notification_types(logentry.event)
|
||||
notification_type = types.get(logentry.action_type)
|
||||
|
||||
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:
|
||||
return # Ignore, e.g. plugin not active for this event
|
||||
return # No suitable plugin
|
||||
|
||||
# All users that have the permission to get the notification
|
||||
users = logentry.event.get_users_with_permission(
|
||||
@@ -33,7 +39,7 @@ def notify(logentry_id: int):
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event=logentry.event,
|
||||
action_type=logentry.action_type,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
@@ -41,7 +47,7 @@ def notify(logentry_id: int):
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event__isnull=True,
|
||||
action_type=logentry.action_type,
|
||||
action_type=notification_type.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
}
|
||||
@@ -49,20 +55,20 @@ def notify(logentry_id: int):
|
||||
for um, enabled in notify_specific.items():
|
||||
user, method = um
|
||||
if enabled:
|
||||
send_notification.apply_async(args=(logentry_id, user.pk, method))
|
||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
||||
|
||||
for um, enabled in notify_global.items():
|
||||
user, method = um
|
||||
if enabled and um not in notify_specific:
|
||||
send_notification.apply_async(args=(logentry_id, user.pk, method))
|
||||
send_notification.apply_async(args=(logentry_id, notification_type.action_type, user.pk, method))
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask)
|
||||
def send_notification(logentry_id: int, user_id: int, method: str):
|
||||
def send_notification(logentry_id: int, action_type: str, user_id: int, method: str):
|
||||
logentry = LogEntry.all.get(id=logentry_id)
|
||||
user = User.objects.get(id=user_id)
|
||||
types = get_all_notification_types(logentry.event)
|
||||
notification_type = types.get(logentry.action_type)
|
||||
notification_type = types.get(action_type)
|
||||
if not notification_type:
|
||||
return # Ignore, e.g. plugin not active for this event
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.api.webhooks import get_all_webhook_events
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.models import Device, Organizer, Team
|
||||
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
|
||||
@@ -222,3 +225,32 @@ class OrganizerDisplaySettingsForm(SettingsForm):
|
||||
self.fields['primary_font'].choices += [
|
||||
(a, a) for a in get_fonts()
|
||||
]
|
||||
|
||||
|
||||
class WebHookForm(forms.ModelForm):
|
||||
events = forms.MultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
label=pgettext_lazy('webhooks', 'Event types')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_events'].queryset = organizer.events.all()
|
||||
self.fields['events'].choices = [
|
||||
(
|
||||
a.action_type,
|
||||
mark_safe('{} – <code>{}</code>'.format(a.verbose_name, a.action_type))
|
||||
) for a in get_all_webhook_events().values()
|
||||
]
|
||||
if self.instance:
|
||||
self.fields['events'].initial = list(self.instance.listeners.values_list('action_type', flat=True))
|
||||
|
||||
class Meta:
|
||||
model = WebHook
|
||||
fields = ['target_url', 'enabled', 'all_events', 'limit_events']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -123,13 +123,13 @@ def _display_checkin(event, logentry):
|
||||
|
||||
if data.get('first'):
|
||||
if show_dt:
|
||||
return _('Position #{posid} has been scanned at {datetime} for list "{list}".').format(
|
||||
return _('Position #{posid} has been checked in at {datetime} for list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list
|
||||
)
|
||||
else:
|
||||
return _('Position #{posid} has been scanned for list "{list}".').format(
|
||||
return _('Position #{posid} has been checked in for list "{list}".').format(
|
||||
posid=data.get('positionid'),
|
||||
list=checkin_list
|
||||
)
|
||||
@@ -321,6 +321,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
return _display_checkin(sender, logentry)
|
||||
|
||||
if logentry.action_type == 'pretix.control.views.checkin':
|
||||
# deprecated
|
||||
dt = dateutil.parser.parse(data.get('datetime'))
|
||||
tz = pytz.timezone(sender.settings.timezone)
|
||||
dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
|
||||
@@ -344,7 +345,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
list=checkin_list
|
||||
)
|
||||
|
||||
if logentry.action_type == 'pretix.control.views.checkin.reverted':
|
||||
if logentry.action_type in ('pretix.control.views.checkin.reverted', 'pretix.event.checkin.reverted'):
|
||||
if 'list' in data:
|
||||
try:
|
||||
checkin_list = sender.checkin_lists.get(pk=data.get('list')).name
|
||||
|
||||
@@ -46,6 +46,13 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'can_change_organizer_settings' in request.orgapermset %}
|
||||
<li {% if "organizer.webhook" in url_name %}class="active"{% endif %}>
|
||||
<a href="{% url "control:organizer.webhooks" organizer=organizer.slug %}">
|
||||
{% trans "Webhooks" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for nav in nav_organizer %}
|
||||
<li {% if nav.active %}class="active"{% endif %}>
|
||||
<a href="{{ nav.url }}">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if webhook %}
|
||||
<legend>{% trans "Modify webhook" %}</legend>
|
||||
{% else %}
|
||||
<legend>{% trans "Create a new webhook" %}</legend>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.target_url layout="control" %}
|
||||
{% bootstrap_field form.enabled layout="control" %}
|
||||
{% bootstrap_field form.events layout="control" %}
|
||||
{% bootstrap_field form.all_events layout="control" %}
|
||||
{% bootstrap_field form.limit_events layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<legend>{% blocktrans with url=webhook.target_url %}Logs for webhook {{ url }}{% endblocktrans %}</legend>
|
||||
<p>
|
||||
{% trans "This page shows all calls to your webhook in the past 30 days." %}
|
||||
</p>
|
||||
{% for c in calls %}
|
||||
<details class="panel panel-default">
|
||||
<summary class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
{% if c.is_retry %}
|
||||
<span class="fa fa-repeat fa-fw" data-toggle="tooltip" title="{% trans "This webhook was retried since it previously failed." %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-clock-o fa-fw"></span>
|
||||
{% endif %}
|
||||
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
<span class="fa fa-tag fa-fw"></span>
|
||||
{{ c.action_type }}
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-2 col-xs-4">
|
||||
<span class="fa fa-hourglass fa-fw"></span>
|
||||
{{ c.execution_time|floatformat:2 }}s
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-8 text-right">
|
||||
{% if c.success %}
|
||||
<span class="label label-success">
|
||||
<span class="fa fa-check-circle fa-fw"></span>
|
||||
{{ c.return_code }}
|
||||
</span>
|
||||
{% else %}
|
||||
{% if c.return_code %}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-warning fa-fw"></span>
|
||||
{{ c.return_code }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-warning fa-fw"></span>
|
||||
{% trans "Failed" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div id="{{ c.pk }}">
|
||||
|
||||
<div class="panel-body">
|
||||
<strong>{% trans "Request URL" %}</strong>
|
||||
<pre><code>POST {{ c.target_url }}</code></pre>
|
||||
<strong>{% trans "Request POST body" %}</strong>
|
||||
<pre><code>{{ c.payload }}</code></pre>
|
||||
<strong>{% trans "Response body" %}</strong>
|
||||
<pre><code>{{ c.response_body }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{% empty %}
|
||||
<div class="alert-info">{% trans "This webhook did not receive any events in the last 30 days." %}</div>
|
||||
{% endfor %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,78 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<legend>
|
||||
{% trans "Webhooks" %}
|
||||
</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
This menu allows you to create webhooks to connect pretix to other online services.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if webhooks|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any webhooks yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.webhook.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create webhook" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:organizer.webhook.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create webhook" %}</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Target URL" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for w in webhooks %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if not w.enabled %}<del>{% endif %}
|
||||
{{ w.target_url }}
|
||||
{% if not w.enabled %}</del>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if w.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for e in w.limit_events.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
||||
{{ e }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.webhook.edit" organizer=request.organizer.slug webhook=w.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Edit" %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url "control:organizer.webhook.logs" organizer=request.organizer.slug webhook=w.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Logs" %}">
|
||||
<i class="fa fa-list"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -71,6 +71,13 @@ urlpatterns = [
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
|
||||
name='organizer.display'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/webhooks$', organizer.WebHookListView.as_view(), name='organizer.webhooks'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/webhook/add$', organizer.WebHookCreateView.as_view(),
|
||||
name='organizer.webhook.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/webhook/(?P<webhook>[^/]+)/edit$', organizer.WebHookUpdateView.as_view(),
|
||||
name='organizer.webhook.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/webhook/(?P<webhook>[^/]+)/logs$', organizer.WebHookLogsView.as_view(),
|
||||
name='organizer.webhook.logs'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
||||
name='organizer.device.add'),
|
||||
|
||||
@@ -94,10 +94,11 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
for op in positions:
|
||||
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
|
||||
Checkin.objects.filter(position=op, list=self.list).delete()
|
||||
op.order.log_action('pretix.control.views.checkin.reverted', data={
|
||||
op.order.log_action('pretix.event.checkin.reverted', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'list': self.list.pk
|
||||
'list': self.list.pk,
|
||||
'web': True
|
||||
}, user=request.user)
|
||||
|
||||
messages.success(request, _('The selected check-ins have been reverted.'))
|
||||
@@ -108,12 +109,14 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={
|
||||
'datetime': now(),
|
||||
})
|
||||
op.order.log_action('pretix.control.views.checkin', data={
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'first': created,
|
||||
'forced': False,
|
||||
'datetime': now(),
|
||||
'list': self.list.pk
|
||||
'list': self.list.pk,
|
||||
'web': True
|
||||
}, user=request.user)
|
||||
|
||||
messages.success(request, _('The selected tickets have been marked as checked in.'))
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.views.generic import (
|
||||
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
|
||||
)
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
|
||||
from pretix.base.models.event import EventMetaProperty
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
@@ -25,13 +26,14 @@ from pretix.control.forms.filter import OrganizerFilterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
DeviceForm, EventMetaPropertyForm, OrganizerDeleteForm,
|
||||
OrganizerDisplaySettingsForm, OrganizerForm, OrganizerSettingsForm,
|
||||
OrganizerUpdateForm, TeamForm,
|
||||
OrganizerUpdateForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
|
||||
)
|
||||
from pretix.control.signals import nav_organizer
|
||||
from pretix.control.views import PaginationMixin
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
from pretix.presale.style import regenerate_organizer_css
|
||||
|
||||
@@ -761,3 +763,110 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
|
||||
return redirect(reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
|
||||
|
||||
class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = WebHook
|
||||
template_name = 'pretixcontrol/organizers/webhooks.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'webhooks'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.webhooks.prefetch_related('limit_events')
|
||||
|
||||
|
||||
class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = WebHook
|
||||
template_name = 'pretixcontrol/organizers/webhook_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = WebHookForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.webhooks', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
self.request.organizer.log_action('pretix.webhook.created', user=self.request.user, data=merge_dicts({
|
||||
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
}, {'id': form.instance.pk}))
|
||||
new_listeners = set(form.cleaned_data['events'])
|
||||
for l in new_listeners:
|
||||
self.object.listeners.create(action_type=l)
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = WebHook
|
||||
template_name = 'pretixcontrol/organizers/webhook_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'webhook'
|
||||
form_class = WebHookForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(WebHook, organizer=self.request.organizer, pk=self.kwargs.get('webhook'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.webhooks', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.request.organizer.log_action('pretix.webhook.changed', user=self.request.user, data=merge_dicts({
|
||||
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
}, {'id': form.instance.pk}))
|
||||
|
||||
current_listeners = set(self.object.listeners.values_list('action_type', flat=True))
|
||||
new_listeners = set(form.cleaned_data['events'])
|
||||
for l in current_listeners - new_listeners:
|
||||
self.object.listeners.filter(action_type=l).delete()
|
||||
for l in new_listeners - current_listeners:
|
||||
self.object.listeners.create(action_type=l)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = WebHook
|
||||
template_name = 'pretixcontrol/organizers/webhook_logs.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'calls'
|
||||
paginate_by = 50
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['webhook'] = self.webhook
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def webhook(self):
|
||||
return get_object_or_404(
|
||||
WebHook, organizer=self.request.organizer, pk=self.kwargs.get('webhook')
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.webhook.calls.order_by('-datetime')
|
||||
|
||||
@@ -76,16 +76,16 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
return p
|
||||
|
||||
def generate(self, p: OrderPosition, override_layout=None, override_background=None):
|
||||
raise NotImplemented
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_layout_settings_key(self):
|
||||
raise NotImplemented
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_background_settings_key(self):
|
||||
raise NotImplemented
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_default_background(self):
|
||||
raise NotImplemented
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_current_background(self):
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user