Compare commits

..
Author SHA1 Message Date
Richard Schreiber e009498386 Move request.GET.items to ctx 2026-02-12 07:22:35 +01:00
107 changed files with 121196 additions and 177577 deletions
+3 -3
View File
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.53.*", "sentry-sdk==2.52.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
@@ -110,10 +110,10 @@ dev = [
"aiohttp==3.13.*", "aiohttp==3.13.*",
"coverage", "coverage",
"coveralls", "coveralls",
"fakeredis==2.34.*", "fakeredis==2.33.*",
"flake8==7.3.*", "flake8==7.3.*",
"freezegun", "freezegun",
"isort==8.0.*", "isort==7.0.*",
"pep8-naming==0.15.*", "pep8-naming==0.15.*",
"potypo", "potypo",
"pytest-asyncio>=0.24", "pytest-asyncio>=0.24",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.3.0.dev0" __version__ = "2026.2.0.dev0"
+1 -5
View File
@@ -188,15 +188,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
clist = self.get_object() clist = self.get_object()
if serializer.validated_data.get('nonce'): if serializer.validated_data.get('nonce'):
if kwargs.get('position'): if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter( prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
nonce=serializer.validated_data['nonce'],
successful=False
).first()
else: else:
prev = clist.checkins.filter( prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'], nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'], raw_barcode=serializer.validated_data['raw_barcode'],
successful=False
).first() ).first()
if prev: if prev:
# Ignore because nonce is already handled # Ignore because nonce is already handled
+3 -15
View File
@@ -259,14 +259,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual', action='pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data=merge_dicts( data=merge_dicts(self.request.data, {'id': inst.pk, 'acceptor_id': self.request.organizer.id})
self.request.data,
{
'id': inst.pk,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
) )
@transaction.atomic() @transaction.atomic()
@@ -297,11 +290,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual', action='pretix.giftcards.transaction.manual',
user=self.request.user, user=self.request.user,
auth=self.request.auth, auth=self.request.auth,
data={ data={'value': diff, 'acceptor_id': self.request.organizer.id}
'value': diff,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
) )
return inst return inst
@@ -331,8 +320,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
data={ data={
'value': value, 'value': value,
'text': text, 'text': text,
'acceptor_id': self.request.organizer.id, 'acceptor_id': self.request.organizer.id
'acceptor_slug': self.request.organizer.slug
} }
) )
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK) return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
+1 -4
View File
@@ -183,7 +183,6 @@ class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
return { return {
'notification_id': logentry.pk, 'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id, 'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk, 'giftcard': giftcard.pk,
'action': logentry.action_type, 'action': logentry.action_type,
} }
@@ -198,9 +197,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
return { return {
'notification_id': logentry.pk, 'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id, 'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'), 'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk, 'giftcard': giftcard.pk,
'action': logentry.action_type, 'action': logentry.action_type,
} }
@@ -475,7 +472,7 @@ def register_default_webhook_events(sender, **kwargs):
), ),
ParametrizedGiftcardTransactionWebhookEvent( ParametrizedGiftcardTransactionWebhookEvent(
'pretix.giftcards.transaction.*', 'pretix.giftcards.transaction.*',
_('Gift card used in transaction'), _('Gift card used in transcation'),
) )
) )
+2 -5
View File
@@ -216,10 +216,7 @@ class OutboundSyncProvider:
try: try:
mapped_objects = self.sync_order(sq.order) mapped_objects = self.sync_order(sq.order)
actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list] if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()):
should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken)
logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry)
if should_write_logentry:
sq.order.log_action("pretix.event.order.data_sync.success", { sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier, "provider": self.identifier,
"objects": { "objects": {
@@ -240,7 +237,7 @@ class OutboundSyncProvider:
sq.set_sync_error("exceeded", e.messages, e.full_message) sq.set_sync_error("exceeded", e.messages, e.full_message)
else: else:
logger.info( logger.info(
f"Could not sync order {sq.order.code} to {sq.sync_provider} " f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})", f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True, exc_info=True,
) )
+7 -9
View File
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback, DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback, markdown_compile_email, truelink_callback,
) )
from pretix.helpers.format import FormattedString, SafeFormatter, format_map from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext get_available_placeholders, PlaceholderContext
@@ -141,7 +141,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
return markdown_compile_email(plaintext, context=context) return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str: def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
apply_format_map = not isinstance(plain_body, FormattedString)
body_md = self.compile_markdown(plain_body, context) body_md = self.compile_markdown(plain_body, context)
if context: if context:
linker = bleach.Linker( linker = bleach.Linker(
@@ -150,13 +149,12 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback], callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True parse_email=True
) )
if apply_format_map: body_md = format_map(
body_md = format_map( body_md,
body_md, context=context,
context=context, mode=SafeFormatter.MODE_RICH_TO_HTML,
mode=SafeFormatter.MODE_RICH_TO_HTML, linkifier=linker
linkifier=linker )
)
htmlctx = { htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME, 'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
-2
View File
@@ -651,7 +651,6 @@ class OrderListExporter(MultiSheetListExporter):
pgettext('address', 'State'), pgettext('address', 'State'),
_('Voucher'), _('Voucher'),
_('Voucher budget usage'), _('Voucher budget usage'),
_('Voucher tag'),
_('Pseudonymization ID'), _('Pseudonymization ID'),
_('Ticket secret'), _('Ticket secret'),
_('Seat ID'), _('Seat ID'),
@@ -770,7 +769,6 @@ class OrderListExporter(MultiSheetListExporter):
op.state_for_address or '', op.state_for_address or '',
op.voucher.code if op.voucher else '', op.voucher.code if op.voucher else '',
op.voucher_budget_use if op.voucher_budget_use else '', op.voucher_budget_use if op.voucher_budget_use else '',
op.voucher.tag if op.voucher else '',
op.pseudonymization_id, op.pseudonymization_id,
op.secret, op.secret,
] ]
+3 -3
View File
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
class PriceModeColumn(ImportColumn): class PriceModeColumn(ImportColumn):
identifier = 'price_mode' identifier = 'price_mode'
verbose_name = gettext_lazy('Price effect') verbose_name = gettext_lazy('Price mode')
default_value = None default_value = None
initial = 'static:none' initial = 'static:none'
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
elif value in reverse: elif value in reverse:
return reverse[value] return reverse[value]
else: else:
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format( raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
value=value, options=', '.join(d.keys()) value=value, options=', '.join(d.keys())
)) ))
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
def clean(self, value, previous_values): def clean(self, value, previous_values):
value = super().clean(value, previous_values) value = super().clean(value, previous_values)
if value and previous_values.get("price_mode") == "none": if value and previous_values.get("price_mode") == "none":
raise ValidationError(_("It is pointless to set a value without a price effect.")) raise ValidationError(_("It is pointless to set a value without a price mode."))
return value return value
def assign(self, value, obj: Voucher, **kwargs): def assign(self, value, obj: Voucher, **kwargs):
+1 -1
View File
@@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model):
def set_sync_error(self, failure_mode, messages, full_message): def set_sync_error(self, failure_mode, messages, full_message):
logger.exception( logger.exception(
f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})" f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
) )
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", { self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider, "provider": self.sync_provider,
+3 -5
View File
@@ -87,7 +87,7 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map from ...helpers.format import format_map
from ...helpers.names import build_name from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var from ...testutils.middleware import debugflags_var
from ._transactions import ( from ._transactions import (
@@ -1178,8 +1178,7 @@ class Order(LockModel, LoggedModel):
recipient = position.attendee_email recipient = position.attendee_email
email_content = render_mail(template, context) email_content = render_mail(template, context)
if not isinstance(subject, FormattedString): subject = format_map(subject, context)
subject = format_map(subject, context)
mail( mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender, self.event, self.locale, self, headers=headers, sender=sender,
@@ -2908,8 +2907,7 @@ class OrderPosition(AbstractPosition):
with language(self.order.locale, self.order.event.settings.region): with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email recipient = self.attendee_email
email_content = render_mail(template, context) email_content = render_mail(template, context)
if not isinstance(subject, FormattedString): subject = format_map(subject, context)
subject = format_map(subject, context)
mail( mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender, self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
+1 -1
View File
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
) )
) )
price_mode = models.CharField( price_mode = models.CharField(
verbose_name=_("Price effect"), verbose_name=_("Price mode"),
max_length=100, max_length=100,
choices=PRICE_MODES, choices=PRICE_MODES,
default='none' default='none'
+1 -3
View File
@@ -1650,8 +1650,7 @@ class GiftCardPayment(BasePaymentProvider):
action='pretix.giftcards.transaction.payment', action='pretix.giftcards.transaction.payment',
data={ data={
'value': trans.value, 'value': trans.value,
'acceptor_id': self.event.organizer.id, 'acceptor_id': self.event.organizer.id
'acceptor_slug': self.event.organizer.slug
} }
) )
except PaymentException as e: except PaymentException as e:
@@ -1683,7 +1682,6 @@ class GiftCardPayment(BasePaymentProvider):
data={ data={
'value': refund.amount, 'value': refund.amount,
'acceptor_id': self.event.organizer.id, 'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug,
'text': refund.comment, 'text': refund.comment,
} }
) )
+32 -54
View File
@@ -81,9 +81,7 @@ from pretix.base.signals import (
) )
from pretix.celery_app import app from pretix.celery_app import app
from pretix.helpers import OF_SELF from pretix.helpers import OF_SELF
from pretix.helpers.format import ( from pretix.helpers.format import SafeFormatter, format_map
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
)
from pretix.helpers.hierarkey import clean_filename from pretix.helpers.hierarkey import clean_filename
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.ical import get_private_icals from pretix.presale.ical import get_private_icals
@@ -220,9 +218,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if email == INVALID_ADDRESS: if email == INVALID_ADDRESS:
return return
if isinstance(template, FormattedString):
raise TypeError("Cannot pass an already formatted body template")
if no_order_links and not plain_text_only: if no_order_links and not plain_text_only:
raise ValueError('If you set no_order_links, you also need to set plain_text_only.') raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
@@ -256,28 +251,23 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
if event and attach_tickets and not event.settings.mail_attach_tickets: if event and attach_tickets and not event.settings.mail_attach_tickets:
attach_tickets = False attach_tickets = False
with language(locale), override(timezone): with language(locale):
if isinstance(context, dict) and order: if isinstance(context, dict) and order:
_autoextend_context(context, order) _autoextend_context(context, order)
# Build raw content # Build raw content
content_plain = render_mail(template, context, placeholder_mode=None) body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
if settings_holder: if settings_holder:
signature = str(settings_holder.settings.get('mail_text_signature')) signature = str(settings_holder.settings.get('mail_text_signature'))
else: else:
signature = "" signature = ""
# Build full plain-text body # Build full plain-text body
if not isinstance(content_plain, FormattedString):
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
else:
body_plain = content_plain
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links) body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
# Build subject # Build subject
if not isinstance(subject, FormattedString): subject = str(subject).format_map(TolerantDict(context))
subject = format_map(subject, context)
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900] subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
if settings_holder: if settings_holder:
subject = prefix_subject(settings_holder, subject) subject = prefix_subject(settings_holder, subject)
@@ -296,24 +286,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
else: else:
renderer = ClassicMailRenderer(None, organizer) renderer = ClassicMailRenderer(None, organizer)
try: with override(timezone):
if 'context' in inspect.signature(renderer.render).parameters: content_plain = render_mail(template, context, placeholder_mode=None)
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context) try:
elif 'position' in inspect.signature(renderer.render).parameters: if 'context' in inspect.signature(renderer.render).parameters:
# Backwards compatibility body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
warnings.warn('Email renderer called without context argument because context argument is not ' elif 'position' in inspect.signature(renderer.render).parameters:
'supported.', # Backwards compatibility
DeprecationWarning) warnings.warn('Email renderer called without context argument because context argument is not '
body_html = renderer.render(content_plain, signature, raw_subject, order, position) 'supported.',
else: DeprecationWarning)
# Backwards compatibility body_html = renderer.render(content_plain, signature, raw_subject, order, position)
warnings.warn('Email renderer called without position argument because position argument is not ' else:
'supported.', # Backwards compatibility
DeprecationWarning) warnings.warn('Email renderer called without position argument because position argument is not '
body_html = renderer.render(content_plain, signature, raw_subject, order) 'supported.',
except: DeprecationWarning)
logger.exception('Could not render HTML body') body_html = renderer.render(content_plain, signature, raw_subject, order)
body_html = None except:
logger.exception('Could not render HTML body')
body_html = None
m = OutgoingMail.objects.create( m = OutgoingMail.objects.create(
organizer=organizer, organizer=organizer,
@@ -389,7 +381,7 @@ def mail_send_task(self, **kwargs) -> bool:
# mail_send_task(self, *, outgoing_mail) # mail_send_task(self, *, outgoing_mail)
with scopes_disabled(): with scopes_disabled():
mail_send(**kwargs) mail_send(**kwargs)
return False return
else: else:
raise ValueError("Unknown arguments") raise ValueError("Unknown arguments")
@@ -443,24 +435,15 @@ def mail_send_task(self, **kwargs) -> bool:
content = ct.file.read() content = ct.file.read()
args.append((name, content, ct.type)) args.append((name, content, ct.type))
attach_size += len(content) attach_size += len(content)
except Exception as e: except Exception:
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out # This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
# why (probably some race condition with ticket cache invalidation?), so retry later. # why (probably some race condition with ticket cache invalidation?), so retry later.
try: try:
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry') self.retry(max_retries=5, countdown=60)
retry_after = 60
outgoing_mail.error = "Tickets not ready"
outgoing_mail.error_detail = str(e)
outgoing_mail.sent = now()
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
"actual_attachments"])
self.retry(max_retries=5, countdown=retry_after)
except MaxRetriesExceededError: except MaxRetriesExceededError:
# Well then, something is really wrong, let's send it without attachment before we # Well then, something is really wrong, let's send it without attachment before we
# don't send at all # don't send at all
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment') logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
pass pass
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024: if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
@@ -807,12 +790,7 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
body = format_map(body, context, mode=placeholder_mode) body = format_map(body, context, mode=placeholder_mode)
else: else:
tpl = get_template(template) tpl = get_template(template)
context = { body = tpl.render(context)
# Known bug, should behave differently for plain and HTML but we'll fix after security release
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
for k, v in context.items()
}
body = FormattedString(tpl.render(context))
return body return body
@@ -958,7 +936,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += "\r\n\r\n-- \r\n" body_plain += "\r\n\r\n-- \r\n"
if signature: if signature:
signature = format_map(signature, {"event": event.name if event else ''}) signature = signature.format(event=event.name if event else '')
body_plain += signature body_plain += signature
body_plain += "\r\n\r\n-- \r\n" body_plain += "\r\n\r\n-- \r\n"
@@ -970,7 +948,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
body_plain += _( body_plain += _(
"You can view your order details at the following URL:\n{orderurl}." "You can view your order details at the following URL:\n{orderurl}."
).replace("\n", "\r\n").format( ).replace("\n", "\r\n").format(
orderurl=build_absolute_uri( event=event.name, orderurl=build_absolute_uri(
order.event, 'presale:event.order.position', kwargs={ order.event, 'presale:event.order.position', kwargs={
'order': order.code, 'order': order.code,
'secret': position.web_secret, 'secret': position.web_secret,
+3 -8
View File
@@ -253,8 +253,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
auth=auth, auth=auth,
data={ data={
'value': position.price, 'value': position.price,
'acceptor_id': order.event.organizer.id, 'acceptor_id': order.event.organizer.id
'acceptor_slug': order.event.organizer.slug
} }
) )
break break
@@ -564,7 +563,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
data={ data={
'value': -position.price, 'value': -position.price,
'acceptor_id': order.event.organizer.id, 'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
} }
) )
@@ -2459,8 +2457,7 @@ class OrderChangeManager:
auth=self.auth, auth=self.auth,
data={ data={
'value': -position.price, 'value': -position.price,
'acceptor_id': self.order.event.organizer.id, 'acceptor_id': self.order.event.organizer.id
'acceptor_slug': self.order.event.organizer.slug
} }
) )
@@ -2486,8 +2483,7 @@ class OrderChangeManager:
auth=self.auth, auth=self.auth,
data={ data={
'value': -opa.position.price, 'value': -opa.position.price,
'acceptor_id': self.order.event.organizer.id, 'acceptor_id': self.order.event.organizer.id
'acceptor_slug': self.order.event.organizer.slug
} }
) )
@@ -3457,7 +3453,6 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
data={ data={
'value': trans.value, 'value': trans.value,
'acceptor_id': order.event.organizer.id, 'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
} }
) )
any_giftcards = True any_giftcards = True
+3
View File
@@ -8,6 +8,9 @@
<h1>{% trans "Not found" %}</h1> <h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p> <p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p> <p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %} {% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post"> <form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p> <p>
+1 -89
View File
@@ -19,44 +19,17 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from django import forms
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField from django_scopes.forms import SafeModelChoiceField
from phonenumber_field.formfields import PhoneNumberField
from pretix.base.forms import I18nModelForm from pretix.base.forms import I18nModelForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
)
from pretix.base.models import WaitingListEntry from pretix.base.models import WaitingListEntry
from pretix.control.forms.widgets import Select2 from pretix.control.forms.widgets import Select2
class WaitingListEntryEditForm(I18nModelForm): class WaitingListEntryTransferForm(I18nModelForm):
itemvar = forms.ChoiceField(
error_messages={
'invalid_choice': _("Select a valid choice.")
}
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.get('instance', None)
initial = kwargs.get('initial', {})
choices = []
if self.instance and self.instance.pk and 'itemvar' not in initial:
if self.instance.variation is not None:
initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}'
if self.instance.variation.active is False:
choices.append((initial['itemvar'], str(self.instance.variation)))
else:
initial['itemvar'] = self.instance.item.pk
if self.instance.item.active is False:
choices.append((initial['itemvar'], str(self.instance)))
kwargs['initial'] = initial
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.event.has_subevents: if self.event.has_subevents:
@@ -72,73 +45,12 @@ class WaitingListEntryEditForm(I18nModelForm):
} }
) )
self.fields['subevent'].widget.choices = self.fields['subevent'].choices self.fields['subevent'].widget.choices = self.fields['subevent'].choices
else:
del self.fields['subevent']
if self.event.settings.waiting_list_names_asked:
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=self.event.settings.waiting_list_names_required,
scheme=self.event.organizer.settings.name_scheme,
titles=self.event.organizer.settings.name_scheme_titles,
label=_('Name'),
)
else:
del self.fields['name_parts']
if not self.event.settings.waiting_list_phones_asked:
del self.fields['phone']
items = self.event.items.filter(active=True).prefetch_related(
'variations'
)
for item in items:
if len(item.variations.all()) > 0:
for variation in item.variations.all():
if variation.active:
choices.append(
('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation)))
)
else:
choices.append(('{}'.format(item.pk), str(item)))
self.fields['itemvar'].label = _("Product")
self.fields['itemvar'].help_text = _("Only includes active products.")
self.fields['itemvar'].required = True
self.fields['itemvar'].choices = choices
def clean(self):
cleaned_data = super().clean()
if self.instance.voucher is not None:
raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.'))
itemvar = cleaned_data.get('itemvar')
if itemvar:
self.instance.item = self.event.items.get(pk=itemvar.split('-')[0])
if '-' in itemvar:
self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1])
if ((self.instance.item and not self.instance.item.active) or
(self.instance.variation and not self.instance.variation.active)):
self.add_error('itemvar', _('The selected product is not active.'))
return cleaned_data
class Meta: class Meta:
model = WaitingListEntry model = WaitingListEntry
fields = [ fields = [
'email',
'name_parts',
'phone',
'subevent', 'subevent',
] ]
field_classes = { field_classes = {
'subevent': SafeModelChoiceField, 'subevent': SafeModelChoiceField,
'email': forms.EmailField,
'phone': PhoneNumberField,
}
widgets = {
'phone': WrappedPhoneNumberPrefixWidget,
} }
@@ -19,14 +19,6 @@
</ul> </ul>
<br> <br>
{% endif %} {% endif %}
{% if possible_cookie_problem %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please
check if your browser is set to block cookies, or delete all existing cookies and retry.
{% endblocktrans %}
</div>
{% endif %}
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
<div class="form-group buttons"> <div class="form-group buttons">
@@ -144,23 +144,14 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{% blocktrans trimmed %} {% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
If you lose access to your devices, you can use one of your emergency tokens to log in.
We recommend to store them in a safe place, e.g. printed out or in a password manager.
Every token can be used at most once.
{% endblocktrans %}
</p> </p>
{% if static_tokens_device %} <p>{% trans "Unused tokens:" %}</p>
<p> <ul>
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %} {% for t in static_tokens %}
You generated your emergency tokens on {{ generation_date_time }}. <li><code>{{ t.token }}</code></li>
{% endblocktrans %} {% endfor %}
</p> </ul>
{% else %}
<p>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% endif %}
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default"> <a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span> <span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %} {% trans "Generate new emergency tokens" %}
@@ -1,33 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Edit entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Edit entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
{% bootstrap_field form.email layout="control" %}
{% if form.name_parts %}
{% bootstrap_field form.name_parts layout="control" %}
{% endif %}
{% if form.phone %}
{% bootstrap_field form.phone layout="control" %}
{% endif %}
{% bootstrap_field form.itemvar layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -124,7 +124,6 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
{% if request.event.has_subevents %} {% if request.event.has_subevents %}
<select name="subevent" class="form-control"> <select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option> <option value="">{% trans "All dates" context "subevent" %}</option>
@@ -268,13 +267,13 @@
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}"> data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span> <span class="fa fa-thumbs-down"></span>
</button> </button>
{% if request.event.has_subevents %}
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}" <a href="{% url "control:event.orders.waitinglist.transfer" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}" class="btn btn-default btn-sm" title="{% trans "Transfer to other date" context "subevent" %}"
data-toggle="tooltip"> data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i> <i class="fa fa-calendar" aria-hidden="true"></i>
</a> </a>
{% endif %}
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a> <a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %} {% else %}
<button class="btn btn-default btn-sm disabled"> <button class="btn btn-default btn-sm disabled">
@@ -0,0 +1,23 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Transfer entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Transfer entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans trimmed context "subevent" %}
Please select the date to which the following waiting list entry should be
transferred: <strong>{{ entry }}</strong>?
{% endblocktrans %}</p>
{% bootstrap_field form.subevent layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Transfer" %}
</button>
</div>
</form>
{% endblock %}
+2 -2
View File
@@ -480,8 +480,8 @@ urlpatterns = [
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'), re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(), re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'), name='event.orders.waitinglist.delete'),
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(), re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
name='event.orders.waitinglist.edit'), name='event.orders.waitinglist.transfer'),
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'), re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'), re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'), re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
-2
View File
@@ -149,8 +149,6 @@ def login(request):
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False)) return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
else: else:
form = LoginForm(backend=backend, request=request) form = LoginForm(backend=backend, request=request)
# Detect redirection loop (usually means cookie not accepted)
ctx['possible_cookie_problem'] = request.path in request.headers.get("Referer", "")
ctx['form'] = form ctx['form'] = form
ctx['can_register'] = settings.PRETIX_REGISTRATION ctx['can_register'] = settings.PRETIX_REGISTRATION
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
+4 -8
View File
@@ -870,15 +870,11 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
) )
except ValueError: except ValueError:
msgs[self.supported_locale[idx]] = format_html( msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
'<div class="alert alert-danger">{}</div>', PlaceholderValidator.error_message)
PlaceholderValidator.error_message
)
except KeyError as e: except KeyError as e:
msgs[self.supported_locale[idx]] = format_html( msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
'<div class="alert alert-danger">{}</div>', _('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
)
return JsonResponse({ return JsonResponse({
'item': preview_item, 'item': preview_item,
+2 -4
View File
@@ -1850,8 +1850,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
data={ data={
'value': value, 'value': value,
'text': request.POST.get('text'), 'text': request.POST.get('text'),
'acceptor_id': self.request.organizer.id, 'acceptor_id': self.request.organizer.id
'acceptor_slug': self.request.organizer.slug
}, },
user=self.request.user, user=self.request.user,
) )
@@ -1914,8 +1913,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
user=self.request.user, user=self.request.user,
data={ data={
'value': form.cleaned_data['value'], 'value': form.cleaned_data['value'],
'acceptor_id': self.request.organizer.id, 'acceptor_id': self.request.organizer.id
'acceptor_slug': self.request.organizer.slug
} }
) )
return redirect(reverse( return redirect(reverse(
+10 -16
View File
@@ -49,14 +49,12 @@ from django.db import transaction
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
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.decorators import method_decorator
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
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 _
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -87,9 +85,8 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin: class RecentAuthenticationRequiredMixin:
max_time = 900 max_time = 3600
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0) tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time: if tdelta > self.max_time:
@@ -292,13 +289,16 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
ctx = super().get_context_data() ctx = super().get_context_data()
try: try:
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency') ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
except StaticDevice.MultipleObjectsReturned: except StaticDevice.MultipleObjectsReturned:
ctx['static_tokens_device'] = StaticDevice.objects.filter( ctx['static_tokens'] = StaticDevice.objects.filter(
user=self.request.user, name='emergency' user=self.request.user, name='emergency'
).first() ).first().token_set.all()
except StaticDevice.DoesNotExist: except StaticDevice.DoesNotExist:
ctx['static_tokens_device'] = None d = StaticDevice.objects.create(user=self.request.user, name='emergency')
for i in range(10):
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
ctx['static_tokens'] = d.token_set.all()
ctx['devices'] = [] ctx['devices'] = []
for dt in REAL_DEVICE_TYPES: for dt in REAL_DEVICE_TYPES:
@@ -630,14 +630,8 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
]) ])
self.request.user.update_session_token() self.request.user.update_session_token()
update_session_auth_hash(self.request, self.request.user) update_session_auth_hash(self.request, self.request.user)
messages.success( messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
request, 'place in case you lose access to your devices.'))
_('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices. You will not be able to view them '
'again here.\n\nYour emergency codes:\n{tokens}').format(
tokens='- ' + '\n- '.join(t.token for t in d.token_set.all())
)
)
return redirect(reverse('control:user.settings.2fa')) return redirect(reverse('control:user.settings.2fa'))
+11 -17
View File
@@ -53,7 +53,7 @@ from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import assign_automatically from pretix.base.services.waitinglist import assign_automatically
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.waitinglist import WaitingListEntryEditForm from pretix.control.forms.waitinglist import WaitingListEntryTransferForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
@@ -138,17 +138,6 @@ class WaitingListQuerySetMixin:
elif force_filtered and '__ALL' not in self.request_data: elif force_filtered and '__ALL' not in self.request_data:
qs = qs.none() qs = qs.none()
if self.request_data.get("search", "") != "":
s = self.request_data.get("search", "")
search_q = Q(email__icontains=s)
if self.request.event.settings.waiting_list_names_asked:
search_q = search_q | Q(name_cached__icontains=s)
if self.request.event.settings.waiting_list_phones_asked:
search_q = search_q | Q(phone__icontains=s)
qs = qs.filter(search_q)
return qs return qs
@@ -249,7 +238,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['items'] = Item.objects.filter(event=self.request.event) ctx['items'] = Item.objects.filter(event=self.request.event)
ctx['filtered'] = any(param in self.request.GET for param in ("status", "item", "search")) ctx['filtered'] = ("status" in self.request.GET or "item" in self.request.GET)
itemvar_cache = {} itemvar_cache = {}
quota_cache = {} quota_cache = {}
@@ -401,20 +390,25 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
}) })
class EntryEdit(EventPermissionRequiredMixin, UpdateView): class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
model = WaitingListEntry model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/edit.html' template_name = 'pretixcontrol/waitinglist/transfer.html'
permission = 'can_change_orders' permission = 'can_change_orders'
form_class = WaitingListEntryEditForm form_class = WaitingListEntryTransferForm
context_object_name = 'entry' context_object_name = 'entry'
def dispatch(self, request, *args, **kwargs):
if not self.request.event.has_subevents:
raise Http404(_("This is not an event series."))
return super().dispatch(request, *args, **kwargs)
def get_object(self, queryset=None) -> WaitingListEntry: def get_object(self, queryset=None) -> WaitingListEntry:
return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True) return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True)
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('The waitinglist entry has been transferred.'))
if form.has_changed(): if form.has_changed():
messages.success(self.request, _('The waitinglist entry has been changed.'))
self.object.log_action( self.object.log_action(
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={ 'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data k: form.cleaned_data.get(k) for k in form.changed_data
+2 -25
View File
@@ -22,7 +22,6 @@
import logging import logging
from string import Formatter from string import Formatter
from django.core.exceptions import SuspiciousOperation
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,17 +37,6 @@ class PlainHtmlAlternativeString:
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')" return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
class FormattedString(str):
"""
A str subclass that has been specifically marked as "already formatted" for email rendering
purposes to avoid duplicate formatting.
"""
__slots__ = ()
def __str__(self):
return self
class SafeFormatter(Formatter): class SafeFormatter(Formatter):
""" """
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
@@ -89,19 +77,8 @@ class SafeFormatter(Formatter):
# Ignore format_spec # Ignore format_spec
return super().format_field(self._prepare_value(value), '') return super().format_field(self._prepare_value(value), '')
def convert_field(self, value, conversion):
# Ignore any conversions
if conversion is None:
return value
else:
return str(value)
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
if isinstance(template, FormattedString):
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
if not isinstance(template, str): if not isinstance(template, str):
template = str(template) template = str(template)
return FormattedString( return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-3
View File
@@ -265,7 +265,6 @@ Objekt-IDs
Offline-Scan Offline-Scan
OK OK
Online-Banking Online-Banking
Online-Banking-Nutzer
Onlinebanking Onlinebanking
Onlinebanking-Zugangsdaten Onlinebanking-Zugangsdaten
Open Open
@@ -540,8 +539,6 @@ WeChat-Zahlung
Weiterleitungs-URIs Weiterleitungs-URIs
Weiterleitungs-URL Weiterleitungs-URL
Weiterleitungs-URLs Weiterleitungs-URLs
WERO
WERO-App
WhatsApp WhatsApp
Widget Widget
Widget-Code Widget-Code
File diff suppressed because it is too large Load Diff
@@ -265,7 +265,6 @@ Objekt-IDs
Offline-Scan Offline-Scan
OK OK
Online-Banking Online-Banking
Online-Banking-Nutzer
Onlinebanking Onlinebanking
Onlinebanking-Zugangsdaten Onlinebanking-Zugangsdaten
Open Open
@@ -540,8 +539,6 @@ WeChat-Zahlung
Weiterleitungs-URIs Weiterleitungs-URIs
Weiterleitungs-URL Weiterleitungs-URL
Weiterleitungs-URLs Weiterleitungs-URLs
WERO
WERO-App
WhatsApp WhatsApp
Widget Widget
Widget-Code Widget-Code
+2262 -2556
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n" "POT-Creation-Date: 2026-01-26 13:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n" "PO-Revision-Date: 2026-02-06 07:41+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n" "Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n" "js/ja/>\n"
"Language: ja\n" "Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -256,7 +256,7 @@ msgstr "承認保留中"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed" msgid "Redeemed"
msgstr "引き換え済み" msgstr "使用済"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49 #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel" msgid "Cancel"
@@ -456,11 +456,11 @@ msgstr "="
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Product" msgid "Product"
msgstr "品" msgstr "品"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "Product variation" msgid "Product variation"
msgstr "品バリエーション" msgstr "品バリエーション"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107 #: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
msgid "Gate" msgid "Gate"
@@ -726,8 +726,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still " "The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available." "complete your order as long as theyre available."
msgstr "" msgstr ""
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了" "カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
"できます。" "とができます。"
#: pretix/static/pretixpresale/js/ui/cart.js:49 #: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired" msgid "Cart expired"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n" "POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-19 22:00+0000\n" "PO-Revision-Date: 2026-02-05 23:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n" "Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/" "Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n" "nl/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16\n" "X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62 #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -674,7 +674,7 @@ msgstr "Zoekopdracht"
#: pretix/static/pretixcontrol/js/ui/main.js:461 #: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All" msgid "All"
msgstr "Allemaal" msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:462 #: pretix/static/pretixcontrol/js/ui/main.js:462
msgid "None" msgid "None"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -187,7 +187,6 @@ webhooks
webserver webserver
Wechat Wechat
WeChat WeChat
WERO
WhatsApp WhatsApp
whitespace whitespace
xlsx xlsx
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -21,10 +21,10 @@
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd> <dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd> <dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
{% if settings.bank_details_type == "sepa" %} {% if settings.bank_details_type == "sepa" %}
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd> <dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd> <dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd> <dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd> <dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
{% endif %} {% endif %}
</dl> </dl>
{% if details %} {% if details %}
@@ -38,4 +38,4 @@
{% if payment_qr_codes %} {% if payment_qr_codes %}
{% include "pretixpresale/event/payment_qr_codes.html" %} {% include "pretixpresale/event/payment_qr_codes.html" %}
{% endif %} {% endif %}
</div> </div>
+9 -16
View File
@@ -786,12 +786,7 @@ class PaypalMethod(BasePaymentProvider):
else: else:
pp_captured_order = response.result pp_captured_order = response.result
payment.refresh_from_db() for purchaseunit in pp_captured_order.purchase_units:
any_captures = False
all_captures_completed = True
for purchaseunit in pp_captured_order.purchase_units:
if hasattr(purchaseunit, 'payments'):
for capture in purchaseunit.payments.captures: for capture in purchaseunit.payments.captures:
try: try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id) ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
@@ -799,16 +794,14 @@ class PaypalMethod(BasePaymentProvider):
pass pass
if capture.status != 'COMPLETED': if capture.status != 'COMPLETED':
all_captures_completed = False messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
else: 'soon as the payment completed.'))
any_captures = True payment.info = json.dumps(pp_captured_order.dict())
if not (any_captures and all_captures_completed): payment.state = OrderPayment.PAYMENT_STATE_PENDING
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as ' payment.save()
'soon as the payment completed.')) return
payment.info = json.dumps(pp_captured_order.dict())
payment.state = OrderPayment.PAYMENT_STATE_PENDING payment.refresh_from_db()
payment.save()
return
if pp_captured_order.status != 'COMPLETED': if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict()) payment.fail(info=pp_captured_order.dict())
-22
View File
@@ -118,7 +118,6 @@ logger = logging.getLogger('pretix.plugins.stripe')
# - UPI: ✗ # - UPI: ✗
# - Netbanking: ✗ # - Netbanking: ✗
# - TWINT: ✓ # - TWINT: ✓
# - Wero: ✓ (No settings UI yet)
# #
# Bank transfers # Bank transfers
# - ACH Bank Transfer: ✗ # - ACH Bank Transfer: ✗
@@ -510,15 +509,6 @@ class StripeSettingsHolder(BasePaymentProvider):
'before they work properly.'), 'before they work properly.'),
required=False, required=False,
)), )),
# Disabled for now, since still in closed Beta and only available to dedicated boarded accounts.
# ('method_wero',
# forms.BooleanField(
# label=_('Wero'),
# disabled=self.event.currency not in 'EUR',
# help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account '
# 'before they work properly.'),
# required=False,
# )),
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings ] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
) )
if not self.settings.connect_client_id or self.settings.secret_key: if not self.settings.connect_client_id or self.settings.secret_key:
@@ -1956,15 +1946,3 @@ class StripeMobilePay(StripeRedirectMethod):
"type": "mobilepay", "type": "mobilepay",
}, },
} }
class StripeWero(StripeRedirectMethod):
identifier = 'stripe_wero'
verbose_name = _('WERO via Stripe')
public_name = 'WERO'
method = 'wero'
confirmation_method = 'automatic'
explanation = _(
'This payment method is available to European online banking users, whose banking institutions support WERO '
'either through their native banking apps or through the WERO wallet app. Please have you app ready.'
)
+2 -2
View File
@@ -49,14 +49,14 @@ def register_payment_provider(sender, **kwargs):
StripeMultibanco, StripePayByBank, StripePayPal, StripePromptPay, StripeMultibanco, StripePayByBank, StripePayPal, StripePromptPay,
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit, StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint, StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
StripeWeChatPay, StripeWero, StripeWeChatPay,
) )
return [ return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact, StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay, StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish, StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
StripeTwint, StripeMobilePay, StripeWero StripeTwint, StripeMobilePay
] ]
@@ -9,7 +9,7 @@
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script> <script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script> <script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
{% if payment_intent_next_action_redirect_url %} {% if payment_intent_next_action_redirect_url %}
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script> <script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url }}</script>
{% endif %} {% endif %}
{% if payment_intent_redirect_action_handling %} {% if payment_intent_redirect_action_handling %}
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script> <script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
@@ -1,14 +1,14 @@
{% extends "pretixpresale/event/base.html" %} {% extends "pretixpresale/event/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{% trans "Resend order link" %}{% endblock %} {% block title %}{% trans "Resend order links" %}{% endblock %}
{% block custom_header %} {% block custom_header %}
{{ block.super }} {{ block.super }}
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2> <h2>
{% trans "Resend order link" %} {% trans "Resend order links" %}
</h2> </h2>
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
+1
View File
@@ -393,6 +393,7 @@ class TokenView(View):
if grant.code_challenge_method == "S256": if grant.code_challenge_method == "S256":
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=") expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
print(grant.code_challenge, expected_challenge)
if expected_challenge != grant.code_challenge: if expected_challenge != grant.code_challenge:
return JsonResponse({ return JsonResponse({
"error": "invalid_grant", "error": "invalid_grant",
+1 -4
View File
@@ -1510,10 +1510,7 @@ class OrderChangeMixin:
'max_count': iao.max_count, 'max_count': iao.max_count,
'iao': iao, 'iao': iao,
'items': [i for i in items if not i.require_voucher], 'items': [i for i in items if not i.require_voucher],
'items_missing': { 'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
k: v for k, v in current_addon_products_missing.items()
if v and k[0].category_id == iao.addon_category_id
},
}) })
return positions return positions
-4
View File
@@ -211,10 +211,6 @@ USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fal
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False) REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
if REQUEST_ID_HEADER in config.cp.BOOLEAN_STATES:
raise ImproperlyConfigured(
"request_id_header should be set to a header name, not a boolean value."
)
if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False): if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@@ -252,7 +252,7 @@ Vue.component('availbox', {
variation: Object variation: Object
}, },
mounted: function() { mounted: function() {
if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) { if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
this.$refs.quantity.value = 1; this.$refs.quantity.value = 1;
if (this.order_max === 1) { if (this.order_max === 1) {
this.$refs.quantity.checked = true; this.$refs.quantity.checked = true;

Some files were not shown because too many files have changed in this diff Show More