Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
712463b259 Fix shredder test 2025-08-19 12:45:15 +02:00
Raphael Michel
78ad4f4979 Update src/pretix/api/webhooks.py
Co-authored-by: luelista <weller@rami.io>
2025-08-19 12:43:34 +02:00
Raphael Michel
8e106d2f85 Webhooks: Add vouchers (Z#23203072)
This also requires more consistent usage of webhook types to avoid
vouchers not being known to the external system.
2025-08-08 12:19:07 +02:00
7 changed files with 59 additions and 12 deletions

View File

@@ -60,6 +60,9 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.added`` * ``pretix.event.added``
* ``pretix.event.changed`` * ``pretix.event.changed``
* ``pretix.event.deleted`` * ``pretix.event.deleted``
* ``pretix.voucher.added``
* ``pretix.voucher.changed``
* ``pretix.voucher.deleted``
* ``pretix.subevent.added`` * ``pretix.subevent.added``
* ``pretix.subevent.changed`` * ``pretix.subevent.changed``
* ``pretix.subevent.deleted`` * ``pretix.subevent.deleted``

View File

@@ -78,6 +78,13 @@ class WebhookEvent:
""" """
raise NotImplementedError() # NOQA raise NotImplementedError() # NOQA
@property
def help_text(self) -> str:
"""
A human-readable description
"""
return ""
def get_all_webhook_events(): def get_all_webhook_events():
global _ALL_EVENTS global _ALL_EVENTS
@@ -97,9 +104,10 @@ def get_all_webhook_events():
class ParametrizedWebhookEvent(WebhookEvent): class ParametrizedWebhookEvent(WebhookEvent):
def __init__(self, action_type, verbose_name): def __init__(self, action_type, verbose_name, help_text=""):
self._action_type = action_type self._action_type = action_type
self._verbose_name = verbose_name self._verbose_name = verbose_name
self._help_text = help_text
super().__init__() super().__init__()
@property @property
@@ -110,6 +118,10 @@ class ParametrizedWebhookEvent(WebhookEvent):
def verbose_name(self): def verbose_name(self):
return self._verbose_name return self._verbose_name
@property
def help_text(self):
return self._help_text
class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent): class ParametrizedOrderWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry): def build_payload(self, logentry: LogEntry):
@@ -161,6 +173,19 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
} }
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
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,
'voucher': logentry.object_id,
'action': logentry.action_type,
}
class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent): class ParametrizedSubEventWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry): def build_payload(self, logentry: LogEntry):
@@ -346,8 +371,9 @@ def register_default_webhook_events(sender, **kwargs):
), ),
ParametrizedItemWebhookEvent( ParametrizedItemWebhookEvent(
'pretix.event.item.*', 'pretix.event.item.*',
_('Product changed (including product added or deleted and including changes to nested objects like ' _('Product changed'),
'variations or bundles)'), _('This includes product added or deleted and changes to nested objects like '
'variations or bundles.'),
), ),
ParametrizedEventWebhookEvent( ParametrizedEventWebhookEvent(
'pretix.event.live.activated', 'pretix.event.live.activated',
@@ -381,6 +407,19 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.event.orders.waitinglist.voucher_assigned', 'pretix.event.orders.waitinglist.voucher_assigned',
_('Waiting list entry received voucher'), _('Waiting list entry received voucher'),
), ),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.added',
_('Voucher added'),
),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.changed',
_('Voucher changed'),
_('Only includes explicit changes to the voucher, not e.g. an increase of the number of redemptions.')
),
ParametrizedVoucherWebhookEvent(
'pretix.voucher.deleted',
_('Voucher deleted'),
),
ParametrizedCustomerWebhookEvent( ParametrizedCustomerWebhookEvent(
'pretix.customer.created', 'pretix.customer.created',
_('Customer account created'), _('Customer account created'),

View File

@@ -207,16 +207,19 @@ class WaitingListEntry(LoggedModel):
block_quota=True, block_quota=True,
subevent=self.subevent, subevent=self.subevent,
) )
v.log_action('pretix.voucher.added.waitinglist', { v.log_action('pretix.voucher.added', {
'item': self.item.pk, 'item': self.item.pk,
'variation': self.variation.pk if self.variation else None, 'variation': self.variation.pk if self.variation else None,
'tag': 'waiting-list', 'tag': 'waiting-list',
'block_quota': True, 'block_quota': True,
'valid_until': v.valid_until.isoformat(), 'valid_until': v.valid_until.isoformat(),
'max_usages': 1, 'max_usages': 1,
'subevent': self.subevent.pk if self.subevent else None,
'source': 'waitinglist',
}, user=user, auth=auth)
v.log_action('pretix.voucher.added.waitinglist', {
'email': self.email, 'email': self.email,
'waitinglistentry': self.pk, 'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, auth=auth) }, user=user, auth=auth)
self.voucher = v self.voucher = v
self.save() self.save()

View File

@@ -43,7 +43,7 @@ from django.forms import formset_factory, inlineformset_factory
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.html import conditional_escape from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField from django_scopes.forms import SafeModelChoiceField
@@ -695,7 +695,9 @@ class WebHookForm(forms.ModelForm):
self.fields['events'].choices = [ self.fields['events'].choices = [
( (
a.action_type, a.action_type,
mark_safe('{} <code>{}</code>'.format(a.verbose_name, a.action_type)) format_html('{} <code>{}</code><br><span class="text-muted">{}</span>', a.verbose_name, a.action_type, a.help_text)
if a.help_text else
format_html('{} <code>{}</code>', a.verbose_name, a.action_type)
) for a in get_all_webhook_events().values() ) for a in get_all_webhook_events().values()
] ]
if self.instance and self.instance.pk: if self.instance and self.instance.pk:

View File

@@ -563,11 +563,11 @@ class CoreOrderLogEntryType(OrderLogEntryType):
@log_entry_types.new_from_dict({ @log_entry_types.new_from_dict({
'pretix.voucher.added': _('The voucher has been created.'), 'pretix.voucher.added': _('The voucher has been created.'),
'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'), 'pretix.voucher.sent': _('The voucher has been sent to {recipient}.'),
'pretix.voucher.added.waitinglist': _('The voucher has been created and sent to a person on the waiting list.'),
'pretix.voucher.expired.waitinglist': _( 'pretix.voucher.expired.waitinglist': _(
'The voucher has been set to expire because the recipient removed themselves from the waiting list.'), 'The voucher has been set to expire because the recipient removed themselves from the waiting list.'),
'pretix.voucher.changed': _('The voucher has been changed.'), 'pretix.voucher.changed': _('The voucher has been changed.'),
'pretix.voucher.deleted': _('The voucher has been deleted.'), 'pretix.voucher.deleted': _('The voucher has been deleted.'),
'pretix.voucher.added.waitinglist': _('The voucher has been sent to {email} through the waiting list.'),
}) })
class CoreVoucherLogEntryType(VoucherLogEntryType): class CoreVoucherLogEntryType(VoucherLogEntryType):
pass pass

View File

@@ -380,7 +380,7 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
messages.success(self.request, mark_safe(_('The new voucher has been created: {code}').format( messages.success(self.request, mark_safe(_('The new voucher has been created: {code}').format(
code=format_html('<a href="{url}">{code}</a>', url=url, code=self.object.code) code=format_html('<a href="{url}">{code}</a>', url=url, code=self.object.code)
))) )))
form.instance.log_action('pretix.voucher.added', data=dict(form.cleaned_data), user=self.request.user) form.instance.log_action('pretix.voucher.added', data={**dict(form.cleaned_data), "source": "control"}, user=self.request.user)
return ret return ret
@transaction.atomic @transaction.atomic
@@ -475,7 +475,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
data['bulk'] = True data['bulk'] = True
del data['codes'] del data['codes']
log_entries.append( log_entries.append(
v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False) v.log_action('pretix.voucher.added', data={**data, "source": "control_bulk"}, user=self.request.user, save=False)
) )
LogEntry.bulk_create_and_postprocess(log_entries) LogEntry.bulk_create_and_postprocess(log_entries)
form.post_bulk_save(batch_vouchers) form.post_bulk_save(batch_vouchers)

View File

@@ -140,7 +140,7 @@ def test_waitinglist_shredder(event, item):
) )
wle.send_voucher() wle.send_voucher()
assert '@' in wle.voucher.comment assert '@' in wle.voucher.comment
assert '@' in wle.voucher.all_logentries().last().data assert '@' in wle.voucher.all_logentries().get(action_type="pretix.voucher.added.waitinglist").data
s = WaitingListShredder(event) s = WaitingListShredder(event)
f = list(s.generate_files()) f = list(s.generate_files())
assert json.loads(f[0][2]) == [ assert json.loads(f[0][2]) == [
@@ -166,7 +166,7 @@ def test_waitinglist_shredder(event, item):
assert '@' not in wle.email assert '@' not in wle.email
assert '+49' not in str(wle.phone) assert '+49' not in str(wle.phone)
assert '@' not in wle.voucher.comment assert '@' not in wle.voucher.comment
assert '@' not in wle.voucher.all_logentries().last().data assert '@' not in wle.voucher.all_logentries().get(action_type="pretix.voucher.added.waitinglist").data
@pytest.mark.django_db @pytest.mark.django_db