'.format(self.action_type)
@@ -121,31 +135,39 @@ class NotificationType:
"""
raise NotImplementedError() # NOQA
+ @property
+ def is_event_level(self) -> bool:
+ """
+ Return `True` if this notification type can be configured per event (the default).
+ """
+ return True
+
def build_notification(self, logentry: LogEntry) -> Notification:
"""
This is the main function that you should override. It is supposed to turn a log entry
object into a notification object that can then be rendered e.g. into an email.
"""
return Notification(
- logentry.event,
- logentry.display()
+ event=logentry.event,
+ title=logentry.display(),
+ organizer=logentry.organizer,
)
-def get_all_notification_types(event=None):
+def get_all_notification_types(event_or_organizer=None):
global _ALL_TYPES
- if event is None and _ALL_TYPES:
+ if event_or_organizer is None and _ALL_TYPES:
return _ALL_TYPES
types = OrderedDict()
- for recv, ret in register_notification_types.send(event):
+ for recv, ret in register_notification_types.send(event_or_organizer):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.action_type] = r
else:
types[ret.action_type] = ret
- if event is None:
+ if event_or_organizer is None:
_ALL_TYPES = types
return types
@@ -181,6 +203,7 @@ class ParametrizedOrderNotificationType(NotificationType):
n = Notification(
event=logentry.event,
+ organizer=logentry.organizer,
title=self._title.format(order=order, event=logentry.event),
url=order_url
)
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 4dc51c0b4..e6a345246 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -1527,42 +1527,37 @@ class GiftCardPayment(BasePaymentProvider):
})
def refund_control_render(self, request, refund) -> str:
- from .models import GiftCard
-
if 'gift_card' in refund.info_data:
- gc = GiftCard.objects.get(pk=refund.info_data.get('gift_card'))
template = get_template('pretixcontrol/giftcards/payment.html')
-
ctx = {
'request': request,
'event': self.event,
- 'gc': gc,
}
+ try:
+ gc = self.event.organizer.accepted_gift_cards.get(pk=refund.info_data.get('gift_card'))
+ ctx['gc'] = gc
+ except GiftCard.DoesNotExist:
+ pass
return template.render(ctx)
def payment_control_render(self, request, payment) -> str:
- from .models import GiftCard
+ template = get_template('pretixcontrol/giftcards/payment.html')
+ ctx = {
+ 'request': request,
+ 'event': self.event,
+ **({'error': payment.info_data[
+ 'error']} if 'error' in payment.info_data else {}),
+ **({'gift_card_secret': payment.info_data[
+ 'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
+ }
- if any(key in payment.info_data for key in ('gift_card', 'error')):
- template = get_template('pretixcontrol/giftcards/payment.html')
- ctx = {
- 'request': request,
- 'event': self.event,
- **({'error': payment.info_data[
- 'error']} if 'error' in payment.info_data else {}),
- **({'gift_card_secret': payment.info_data[
- 'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
- }
+ try:
+ ctx['gc'] = self.event.organizer.accepted_gift_cards.get(pk=payment.info_data.get('gift_card'))
+ except GiftCard.DoesNotExist:
+ pass
- try:
- gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
- ctx = {
- 'gc': gc,
- }
- except GiftCard.DoesNotExist:
- pass
- finally:
- return template.render(ctx)
+ if any(key in ctx for key in ('gc', 'gift_card_secret', 'error')):
+ return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
d = payment.info_data
@@ -1572,10 +1567,9 @@ class GiftCardPayment(BasePaymentProvider):
d = refund.info_data
return d.get('gift_card_secret', d.get('gift_card_code', self.public_name))
- def api_payment_details(self, payment: OrderPayment):
- from .models import GiftCard
+ def api_payment_details(self, payment: OrderPayment | OrderRefund):
try:
- gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
+ gc = self.event.organizer.accepted_gift_cards.get(pk=payment.info_data.get('gift_card'))
except GiftCard.DoesNotExist:
return {
**({'error': payment.info_data[
@@ -1698,7 +1692,19 @@ class GiftCardPayment(BasePaymentProvider):
@transaction.atomic()
def execute_refund(self, refund: OrderRefund):
from .models import GiftCard
- gc = GiftCard.objects.get(pk=refund.info_data.get('gift_card') or refund.payment.info_data.get('gift_card'))
+ try:
+ gc = self.event.organizer.accepted_gift_cards.get(
+ pk=refund.info_data.get('gift_card') or refund.payment.info_data.get('gift_card'),
+ currency=self.event.currency
+ )
+ except GiftCard.DoesNotExist:
+ raise PaymentException(_("Gift card not found."))
+
+ if gc.testmode != refund.order.testmode:
+ raise PaymentException(_("Only test gift card can be used in test mode and vice versa."))
+ if gc.expires and gc.expires < time_machine_now():
+ raise PaymentException(_("This gift card is no longer valid."))
+
trans = gc.transactions.create(
value=refund.amount,
order=refund.order,
diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py
index 376518087..a3d421769 100644
--- a/src/pretix/base/services/mail.py
+++ b/src/pretix/base/services/mail.py
@@ -316,6 +316,7 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
body_html = None
m = OutgoingMail.objects.create(
+ guid=guid,
organizer=organizer,
event=event,
order=order,
diff --git a/src/pretix/base/services/notifications.py b/src/pretix/base/services/notifications.py
index 950e111c6..20af9a252 100644
--- a/src/pretix/base/services/notifications.py
+++ b/src/pretix/base/services/notifications.py
@@ -34,7 +34,9 @@ from pretix.base.models import (
from pretix.base.notifications import Notification, get_all_notification_types
from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
-from pretix.base.signals import notification
+from pretix.base.signals import (
+ event_notification_sent, organizer_notification_sent,
+)
from pretix.celery_app import app
from pretix.helpers.celery import get_task_priority
from pretix.helpers.urls import mainreverse_absolute
@@ -47,40 +49,47 @@ def notify(logentry_ids: list):
logentry_ids = [logentry_ids]
qs = LogEntry.all.select_related(
- 'event', 'event__organizer'
+ 'event', 'event__organizer', 'organizer'
).order_by(
- 'action_type', 'event_id',
+ 'action_type', 'event_id', 'organizer_id',
).filter(id__in=logentry_ids)
- _event, _at, notify_specific, notify_global = None, None, None, None
+ _event, _organizer, _at, notify_specific, notify_global = None, None, None, None, None
for logentry in qs:
- if not logentry.event:
- break # Ignore, we only have event-related notifications right now
+ if not logentry.event and not logentry.organizer:
+ break # Ignore, we only have event- or organizer-related notifications right now
notification_type = logentry.notification_type
if not notification_type:
break # No suitable plugin
- if _event != logentry.event or _at != logentry.action_type or notify_global is None:
+ if _event != logentry.event or _organizer != logentry.organizer or _at != logentry.action_type or notify_global is None:
_event = logentry.event
+ _organizer = logentry.organizer
_at = logentry.action_type
+
+ event_or_organizer = logentry.event or logentry.organizer
# All users that have the permission to get the notification
- users = logentry.event.get_users_with_permission(
+ users = event_or_organizer.get_users_with_permission(
notification_type.required_permission
).filter(notifications_send=True, is_active=True)
+
if logentry.user:
users = users.exclude(pk=logentry.user.pk)
# Get all notification settings, both specific to this event as well as global
- notify_specific = {
- (ns.user, ns.method): ns.enabled
- for ns in NotificationSetting.objects.filter(
- event=logentry.event,
- action_type=notification_type.action_type,
- user__pk__in=users.values_list('pk', flat=True)
- )
- }
+ if logentry.event:
+ notify_specific = {
+ (ns.user, ns.method): ns.enabled
+ for ns in NotificationSetting.objects.filter(
+ event=logentry.event,
+ action_type=notification_type.action_type,
+ user__pk__in=users.values_list('pk', flat=True)
+ )
+ }
+ else:
+ notify_specific = {}
notify_global = {
(ns.user, ns.method): ns.enabled
for ns in NotificationSetting.objects.filter(
@@ -106,7 +115,10 @@ def notify(logentry_ids: list):
priority=get_task_priority("notifications", logentry.organizer_id),
)
- notification.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
+ if logentry.event:
+ event_notification_sent.send(logentry.event, logentry_id=logentry.id, notification_type=notification_type.action_type)
+ elif logentry.organizer:
+ organizer_notification_sent.send(logentry.organizer, logentry_id=logentry.id, notification_type=notification_type.action_type)
@app.task(base=ProfiledTask, acks_late=True, max_retries=9, default_retry_delay=900)
@@ -158,13 +170,20 @@ def send_notification_mail(notification: Notification, user: User):
body_plain = tpl_plain.render(ctx)
guid = uuid.uuid4()
+ settings_holder = notification.event or notification.organizer
+ prefix = settings_holder.settings.mail_prefix
+ if not prefix:
+ if notification.event:
+ prefix = notification.event.slug.upper()
+ elif notification.organizer:
+ prefix = notification.organizer.name
m = OutgoingMail.objects.create(
guid=guid,
user=user,
to=[user.email],
subject='[{}] {}: {}'.format(
settings.PRETIX_INSTANCE_NAME,
- notification.event.settings.mail_prefix or notification.event.slug.upper(),
+ prefix,
notification.title
),
body_plain=body_plain,
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 890fa0640..9c6c0e4cc 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -359,7 +359,8 @@ def mark_order_refunded(order, user=None, auth=None, api_token=None):
def mark_order_expired(order, user=None, auth=None):
"""
- Mark this order as expired. This sets the payment status and returns the order object.
+ Mark this order as expired. This sets the order status, generates an invoice cancellation
+ and returns the order object.
:param order: The order to change
:param user: The user that performed the change
"""
@@ -969,7 +970,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
gift_cards = [] # for backwards compatibility
for p in payment_requests:
if p['provider'] == 'giftcard':
- gift_cards.append(GiftCard.objects.get(pk=p['info_data']['gift_card']))
+ gift_cards.extend(event.organizer.accepted_gift_cards.filter(pk=p['info_data']['gift_card']))
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total, payment_requests=payment_requests,
meta_info=meta_info, positions=positions, gift_cards=gift_cards):
@@ -2474,7 +2475,11 @@ class OrderChangeManager:
self.order.log_action('pretix.event.order.changed.feevalue', user=self.user, auth=self.auth, data={
'fee': fee.pk,
'old_price': fee.value,
- 'new_price': op.value.gross
+ 'new_price': op.value.gross,
+ 'old_tax_rate': fee.tax_rate,
+ 'new_tax_rate': op.value.rate,
+ 'old_tax_value': fee.tax_value,
+ 'new_tax_value': op.value.tax,
})
fee.value = op.value.gross
fee._calculate_tax()
@@ -2484,9 +2489,13 @@ class OrderChangeManager:
self.order.log_action('pretix.event.order.changed.price', user=self.user, auth=self.auth, data={
'position': position.pk,
'positionid': position.positionid,
- 'old_price': position.price,
'addon_to': position.addon_to_id,
- 'new_price': op.price.gross
+ 'old_price': position.price,
+ 'new_price': op.price.gross,
+ 'old_tax_rate': position.tax_rate,
+ 'new_tax_rate': op.price.rate,
+ 'old_tax_value': position.tax_value,
+ 'new_tax_value': op.price.tax,
})
position.price = op.price.gross
position.price_includes_rounding_correction = Decimal("0.00")
@@ -2501,25 +2510,43 @@ class OrderChangeManager:
elif isinstance(op, self.TaxRuleOperation):
if isinstance(op.position, OrderPosition):
position = position_cache.setdefault(op.position.pk, op.position)
- self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
+ log = {
'position': position.pk,
'positionid': position.positionid,
'addon_to': position.addon_to_id,
'old_taxrule': position.tax_rule.pk if position.tax_rule else None,
- 'new_taxrule': op.tax_rule.pk
- })
+ 'new_taxrule': op.tax_rule.pk,
+ 'old_price': position.price,
+ 'old_tax_rate': position.tax_rate,
+ 'old_tax_value': position.tax_value,
+ }
position._calculate_tax(op.tax_rule)
position.save()
+ log.update({
+ 'new_price': position.price,
+ 'new_tax_rate': position.tax_rate,
+ 'new_tax_value': position.tax_value,
+ })
+ self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data=log)
elif isinstance(op.position, OrderFee):
fee = fee_cache.setdefault(op.position.pk, op.position)
- self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data={
+ log = {
'fee': fee.pk,
'fee_type': fee.fee_type,
'old_taxrule': fee.tax_rule.pk if fee.tax_rule else None,
- 'new_taxrule': op.tax_rule.pk
- })
+ 'new_taxrule': op.tax_rule.pk,
+ 'old_price': fee.value,
+ 'old_tax_rate': fee.tax_rate,
+ 'old_tax_value': fee.tax_value,
+ }
fee._calculate_tax(op.tax_rule)
fee.save()
+ log.update({
+ 'new_price': fee.value,
+ 'new_tax_rate': fee.tax_rate,
+ 'new_tax_value': fee.tax_value,
+ })
+ self.order.log_action('pretix.event.order.changed.tax_rule', user=self.user, auth=self.auth, data=log)
elif isinstance(op, self.CancelFeeOperation):
fee = fee_cache.setdefault(op.fee.pk, op.fee)
self.order.log_action('pretix.event.order.changed.cancelfee', user=self.user, auth=self.auth, data={
diff --git a/src/pretix/base/services/placeholders.py b/src/pretix/base/services/placeholders.py
index 4d1e2b54d..6e03d0ff6 100644
--- a/src/pretix/base/services/placeholders.py
+++ b/src/pretix/base/services/placeholders.py
@@ -752,6 +752,11 @@ def base_placeholders(sender, **kwargs):
lambda position_or_address: concatenation_for_salutation(get_best_name(position_or_address, parts=True)),
lambda event: concatenation_for_salutation(name_scheme['sample']),
))
+ ph.append(SimpleFunctionalTextPlaceholder(
+ "attendee_name_for_salutation", ["position"],
+ lambda position: concatenation_for_salutation(position.attendee_name_parts),
+ lambda event: concatenation_for_salutation(name_scheme['sample']),
+ ))
for f, l, w in name_scheme['fields']:
if f == 'full_name':
diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py
index a9d1c3f6e..798b1bb17 100644
--- a/src/pretix/base/signals.py
+++ b/src/pretix/base/signals.py
@@ -605,15 +605,14 @@ subclass of pretix.base.ticketoutput.BaseTicketOutput
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
-register_notification_types = EventPluginSignal()
+register_notification_types = GlobalSignal()
"""
This signal is sent out to get all known notification types. Receivers should return an
instance of a subclass of pretix.base.notifications.NotificationType or a list of such
instances.
-As with all event-plugin signals, the ``sender`` keyword argument will contain the event,
-however for this signal, the ``sender`` **may also be None** to allow creating the general
-notification settings!
+When called for actually sending notifications, ``sender`` will be the event or organizer,
+depending on context.
"""
register_event_permission_groups = GlobalSignal()
@@ -628,7 +627,7 @@ This signal is sent out to get all known permissions. Receivers should return an
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
"""
-notification = EventPluginSignal()
+event_notification_sent = EventPluginSignal()
"""
Arguments: ``logentry_id``, ``notification_type``
@@ -637,6 +636,15 @@ This signal is sent out when a notification is sent.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
+organizer_notification_sent = OrganizerPluginSignal()
+"""
+Arguments: ``logentry_id``, ``notification_type``
+
+This signal is sent out when a notification is sent.
+
+As with all organizer-plugin signals, the ``sender`` keyword argument will contain the organizer.
+"""
+
register_sales_channel_types = GlobalSignal()
"""
This signal is sent out to get all known sales channels types. Receivers should return an
diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py
index e87e54f3e..bcb4b019c 100644
--- a/src/pretix/control/forms/__init__.py
+++ b/src/pretix/control/forms/__init__.py
@@ -172,7 +172,7 @@ class CachedFileInput(forms.ClearableFileInput):
from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
- v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
+ v = CachedFile.objects.filter(id=data[name + '-cachedfile'], web_download=True).first()
if not v.allowed_for_session(self.request):
v = None
return v
diff --git a/src/pretix/control/forms/modelimport.py b/src/pretix/control/forms/modelimport.py
index 656f16b4f..ff8a127f6 100644
--- a/src/pretix/control/forms/modelimport.py
+++ b/src/pretix/control/forms/modelimport.py
@@ -66,7 +66,7 @@ class ProcessForm(forms.Form):
widget=forms.Select(
attrs={'data-static': 'true'}
),
- help_text=conditional_escape(c.help_text),
+ help_text=conditional_escape(c.help_text or ""),
)
def get_columns(self):
diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py
index 188bdab03..ff6cfcdf2 100644
--- a/src/pretix/control/forms/organizer.py
+++ b/src/pretix/control/forms/organizer.py
@@ -38,7 +38,6 @@ from urllib.parse import urlparse
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
-from django.db.models import Q
from django.forms import formset_factory, inlineformset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
@@ -70,9 +69,9 @@ from pretix.base.forms.widgets import (
SplitDateTimePickerWidget, format_placeholders_help_text,
)
from pretix.base.models import (
- Customer, Device, Event, EventMetaProperty, Gate, GiftCard,
- GiftCardAcceptance, Membership, MembershipType, OrderPosition, Organizer,
- ReusableMedium, SalesChannel, Team,
+ Customer, Device, Event, EventMetaProperty, Gate, GiftCard, Membership,
+ MembershipType, OrderPosition, Organizer, ReusableMedium, SalesChannel,
+ Team,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink, TeamQuerySet
@@ -364,7 +363,7 @@ class TeamForm(forms.ModelForm):
for opt in pg.options
],
label=pg.label,
- help_text=conditional_escape(pg.help_text),
+ help_text=conditional_escape(pg.help_text or ""),
initial=initial,
widget=forms.RadioSelect,
)
@@ -389,7 +388,7 @@ class TeamForm(forms.ModelForm):
for opt in pg.options
],
label=pg.label,
- help_text=conditional_escape(pg.help_text),
+ help_text=conditional_escape(pg.help_text or ""),
initial=initial,
widget=forms.RadioSelect,
)
@@ -894,15 +893,7 @@ class GiftCardCreateForm(forms.ModelForm):
def clean_secret(self):
s = self.cleaned_data['secret']
- if GiftCard.objects.filter(
- secret__iexact=s
- ).filter(
- Q(issuer=self.organizer) |
- Q(issuer__in=GiftCardAcceptance.objects.filter(
- acceptor=self.organizer,
- active=True,
- ).values_list('issuer', flat=True))
- ).exists():
+ if self.organizer.accepted_gift_cards.filter(secret__iexact=s).exists():
raise ValidationError(
_('A gift card with the same secret already exists in your or an affiliated organizer account.')
)
@@ -1198,7 +1189,7 @@ OrganizerFooterLinkFormset = inlineformset_factory(
class SSOProviderForm(I18nModelForm):
config_oidc_base_url = forms.URLField(
- label=pgettext_lazy('sso_oidc', 'Base URL'),
+ label=pgettext_lazy('sso_oidc', 'Base URL / issuer'),
required=False,
)
config_oidc_client_id = forms.CharField(
@@ -1295,6 +1286,14 @@ class SSOProviderForm(I18nModelForm):
class SSOClientForm(I18nModelForm):
+ disp_base_url = forms.CharField(
+ label=_('Base URL / issuer'),
+ disabled=True,
+ )
+ disp_client_id = forms.CharField(
+ label=_('Client ID'),
+ disabled=True,
+ )
regenerate_client_secret = forms.BooleanField(
label=_('Invalidate old client secret and generate a new one'),
required=False,
@@ -1302,7 +1301,8 @@ class SSOClientForm(I18nModelForm):
class Meta:
model = CustomerSSOClient
- fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
+ fields = ['is_active', 'name', 'disp_base_url', 'disp_client_id', 'regenerate_client_secret',
+ 'client_type', 'authorization_grant_type', 'redirect_uris',
'allowed_scopes', 'require_pkce']
widgets = {
'authorization_grant_type': forms.RadioSelect,
@@ -1317,13 +1317,15 @@ class SSOClientForm(I18nModelForm):
help_text=self.fields['allowed_scopes'].help_text,
required=self.fields['allowed_scopes'].required,
initial=self.fields['allowed_scopes'].initial,
- choices=CustomerSSOClient.SCOPE_CHOICES,
+ choices=[(val, format_html('{} – {}', val, label)) for val, label in CustomerSSOClient.SCOPE_CHOICES],
widget=forms.CheckboxSelectMultiple
)
+ organizer = kwargs['event']
+ self.initial['disp_base_url'] = eventreverse_absolute(organizer, 'presale:organizer.index', {}).strip('/')
if self.instance and self.instance.pk:
- self.fields['client_id'].disabled = True
+ self.initial['disp_client_id'] = self.instance.client_id
else:
- del self.fields['client_id']
+ self.initial['disp_client_id'] = _('(will be generated)')
del self.fields['regenerate_client_secret']
diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py
index 4b3dac134..c28d36c35 100644
--- a/src/pretix/control/forms/subevents.py
+++ b/src/pretix/control/forms/subevents.py
@@ -153,7 +153,7 @@ class SubEventBulkEditForm(I18nModelForm):
self.fields[k].widget.is_required = False
self.fields[k].required = False
- for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'):
+ for k in ('date_from', 'date_to'):
self.fields[k + '_day'] = forms.DateField(
label=self._meta.model._meta.get_field(k).verbose_name,
help_text=self._meta.model._meta.get_field(k).help_text,
@@ -167,6 +167,14 @@ class SubEventBulkEditForm(I18nModelForm):
required=False,
)
+ for k in ('date_admission', 'presale_start', 'presale_end'):
+ self.fields[k + '_rel'] = RelativeDateTimeField(
+ label=self._meta.model._meta.get_field(k).verbose_name,
+ help_text=self._meta.model._meta.get_field(k).help_text,
+ required=False,
+ limit_choices=('event__date_from',),
+ )
+
class Meta:
model = SubEvent
localized_fields = '__all__'
@@ -247,6 +255,18 @@ class SubEventBulkEditForm(I18nModelForm):
)
setattr(obj, k.replace('_time', ''), newval)
fields.add(k.replace('_time', ''))
+ elif k.endswith('_rel'):
+ target_key = k[:-4]
+ for obj in objs:
+ cval = self.cleaned_data[k]
+ if cval is None:
+ newval = None
+ if not self._meta.model._meta.get_field(target_key).null:
+ continue
+ else:
+ newval = cval.datetime(obj)
+ setattr(obj, target_key, newval)
+ fields.add(target_key)
else:
fields.add(k)
for obj in objs:
diff --git a/src/pretix/control/templates/pretixcontrol/giftcards/payment.html b/src/pretix/control/templates/pretixcontrol/giftcards/payment.html
index bdde92e04..58db07390 100644
--- a/src/pretix/control/templates/pretixcontrol/giftcards/payment.html
+++ b/src/pretix/control/templates/pretixcontrol/giftcards/payment.html
@@ -1,22 +1,24 @@
{% load i18n %}
- - {% trans "Gift card code" %}
- -
- {% if gc %}
-
- {{ gc.secret }}
-
- {% if gc.issuer.slug != request.organizer %}
-
-
- {{ gc.issuer }}
-
+ {% if gc or gift_card_secret %}
+ - {% trans "Gift card code" %}
+ -
+ {% if gc %}
+
+ {{ gc.secret }}
+
+ {% if gc.issuer.slug != request.organizer %}
+
+
+ {{ gc.issuer }}
+
+ {% endif %}
+ {% elif gift_card_secret %}
+ {{ gift_card_secret }}
{% endif %}
- {% elif gift_card_secret %}
- {{ gift_card_secret }}
- {% endif %}
-
+
+ {% endif %}
{% if gc %}
- {% trans "Issuer" %}
- {{ gc.issuer }}
@@ -25,4 +27,4 @@
- {% trans "Error" %}
- {{ error }}
{% endif %}
-
+
\ No newline at end of file
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
index e1265ced3..aaebd0859 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/ssoclient_edit.html
@@ -1,12 +1,29 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
+{% block title %}
+ {% if client %}
+ {% trans "SSO client:" %} {{ client.name }}
+ {% else %}
+ {% trans "Create credentials for a new SSO client" %}
+ {% endif %}
+{% endblock %}
{% block inner %}
{% if client %}
{% trans "SSO client:" %} {{ client.name }}
{% else %}
- {% trans "Create a new SSO client" %}
+ {% trans "Create credentials for a new SSO client" %}
+
+ {% blocktrans trimmed %}
+ After submitting this form, client credentials for your SSO client will be generated.
+ {% endblocktrans %}
+
{% endif %}
+
+ {% blocktrans trimmed with url="https://docs.pretix.eu/guides/customer-accounts/#using-pretix-as-an-sso-provider" %}
+ For more information, refer to our documentation on Using pretix as an SSO provider with external SSO clients.
+ {% endblocktrans %}
+