Compare commits

..
Author SHA1 Message Date
Raphael Michel a883f709d0 Mail: Remove redundant SQL queries
On my local test event, this saved 75 queries on sending an email due to
an N+1 query problem in the metadata querying.
2026-02-16 13:40:24 +01:00
25 changed files with 84 additions and 183 deletions
+2 -2
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,7 +110,7 @@ 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==7.0.*", "isort==7.0.*",
+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
View File
@@ -198,7 +198,6 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
'notification_id': logentry.pk, 'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id, 'issuer_id': logentry.organizer_id,
'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,
} }
-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
@@ -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,
} }
) )
+12
View File
@@ -409,6 +409,18 @@ def mail_send_task(self, **kwargs) -> bool:
outgoing_mail.inflight_since = now() outgoing_mail.inflight_since = now()
outgoing_mail.save(update_fields=["status", "inflight_since"]) outgoing_mail.save(update_fields=["status", "inflight_since"])
# Performance optimization, saves database queries later on if we resolve the known relationships
if outgoing_mail.event_id:
assert outgoing_mail.event.organizer_id == outgoing_mail.organizer.pk
outgoing_mail.event.organizer = outgoing_mail.organizer
if outgoing_mail.order_id:
assert outgoing_mail.order.event_id == outgoing_mail.event_id
outgoing_mail.order.event = outgoing_mail.event
outgoing_mail.order.organizer = outgoing_mail.organizer
if outgoing_mail.orderposition_id:
assert outgoing_mail.orderposition.order_id == outgoing_mail.order_id
outgoing_mail.orderposition.order = outgoing_mail.order
headers = dict(outgoing_mail.headers) headers = dict(outgoing_mail.headers)
headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid)) headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid))
email = CustomEmail( email = CustomEmail(
+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
+7
View File
@@ -24,6 +24,7 @@ import logging
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from django.db.models import Prefetch, prefetch_related_objects
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
@@ -35,6 +36,7 @@ from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import ( from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber, LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
) )
from pretix.base.models import EventMetaValue
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized
from pretix.base.signals import ( from pretix.base.signals import (
@@ -752,6 +754,11 @@ def base_placeholders(sender, **kwargs):
name_scheme['sample'][f] name_scheme['sample'][f]
)) ))
prefetch_related_objects(
[sender],
Prefetch('meta_values', queryset=EventMetaValue.objects.select_related("property"), to_attr="meta_values_cached")
)
prefetch_related_objects([sender.organizer], Prefetch('meta_properties'))
for k, v in sender.meta_data.items(): for k, v in sender.meta_data.items():
ph.append(MarkdownTextPlaceholder( ph.append(MarkdownTextPlaceholder(
'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k], 'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k],
+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>
@@ -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" %}
+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(
+9 -10
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:
@@ -631,8 +631,7 @@ 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(request, _('Your emergency codes have been newly generated. Remember to store them in a safe ' messages.success(request, _('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 ' 'place in case you lose access to your devices.'))
'again here.\n\nYour emergency codes:\n- ' + '\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'))
+3 -27
View File
@@ -34,10 +34,7 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
if not is_secure: if not is_secure:
# https://www.chromestatus.com/feature/5633521622188032 # https://www.chromestatus.com/feature/5633521622188032
return return
if should_send_same_site_none(request.headers.get('User-Agent', '')):
useragent = request.headers.get('User-Agent', '')
if should_send_same_site_none(useragent):
# Chromium is rolling out SameSite=Lax as a default # Chromium is rolling out SameSite=Lax as a default
# https://www.chromestatus.com/feature/5088147346030592 # https://www.chromestatus.com/feature/5088147346030592
# This however breaks all pretix-in-an-iframe things, such as the pretix Widget. # This however breaks all pretix-in-an-iframe things, such as the pretix Widget.
@@ -47,29 +44,8 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
# This will only work on secure cookies as well # This will only work on secure cookies as well
# https://www.chromestatus.com/feature/5633521622188032 # https://www.chromestatus.com/feature/5633521622188032
response.cookies[key]['secure'] = is_secure response.cookies[key]['secure'] = is_secure
# CHIPS
if can_send_partitioned_cookie(useragent): response.cookies[key]['Partitioned'] = True
# CHIPS
response.cookies[key]['Partitioned'] = True
def can_send_partitioned_cookie(useragent):
# Safari currently exhibits a bug where Partitioned cookies (CHIPS) are not
# sent back to the originating site after multi-hop cross-site redirects,
# breaking SSO login flows in pretix.
#
# Partitioned cookies were initially introduced in Safari 18.4, removed
# again in 18.5 due to a bug, and reintroduced in Safari 26.2, where the
# current issue is present.
#
# Once the Safari issue is fixed, this check should be refined to be
# conditional on the affected versions only.
#
# WebKit issues:
#
# - https://bugs.webkit.org/show_bug.cgi?id=292975
# - https://bugs.webkit.org/show_bug.cgi?id=306194
return not is_safari(useragent)
# Based on https://www.chromium.org/updates/same-site/incompatible-clients # Based on https://www.chromium.org/updates/same-site/incompatible-clients
@@ -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>
+15 -21
View File
@@ -786,29 +786,23 @@ class PaypalMethod(BasePaymentProvider):
else: else:
pp_captured_order = response.result pp_captured_order = response.result
for purchaseunit in pp_captured_order.purchase_units:
for capture in purchaseunit.payments.captures:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
if capture.status != 'COMPLETED':
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment.info = json.dumps(pp_captured_order.dict())
payment.state = OrderPayment.PAYMENT_STATE_PENDING
payment.save()
return
payment.refresh_from_db() payment.refresh_from_db()
any_captures = False
all_captures_completed = True
for purchaseunit in pp_captured_order.purchase_units:
for capture in purchaseunit.payments.captures:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
if capture.status != 'COMPLETED':
all_captures_completed = False
else:
any_captures = True
if not (any_captures and all_captures_completed):
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment.info = json.dumps(pp_captured_order.dict())
payment.state = OrderPayment.PAYMENT_STATE_PENDING
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())
logger.error('Invalid state: %s' % repr(pp_captured_order.dict())) logger.error('Invalid state: %s' % repr(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
] ]
@@ -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 -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
-24
View File
@@ -1177,30 +1177,6 @@ def test_store_failed(token_client, organizer, clist, event, order):
assert resp.status_code == 400 assert resp.status_code == 400
@pytest.mark.django_db
def test_store_failed_after_success(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
p.all_checkins.create(
type=Checkin.TYPE_ENTRY,
nonce='foobar',
successful=True,
list=clist,
raw_barcode=p.secret
)
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
organizer.slug, event.slug, clist.pk,
), {
'raw_barcode': p.secret,
'nonce': 'foobar',
'position': p.pk,
'error_reason': 'unpaid'
}, format='json')
assert resp.status_code == 201
with scopes_disabled():
assert Checkin.all.filter(position=p).count() == 2
@pytest.mark.django_db @pytest.mark.django_db
def test_redeem_unknown(token_client, organizer, clist, event, order): def test_redeem_unknown(token_client, organizer, clist, event, order):
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True}) resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
+1 -1
View File
@@ -170,7 +170,7 @@ def test_price_mode_validation(event, item, user):
import_vouchers.apply( import_vouchers.apply(
args=(event.pk, inputfile_factory().id, settings, 'en', user.pk) args=(event.pk, inputfile_factory().id, settings, 'en', user.pk)
).get() ).get()
assert 'It is pointless to set a value without a price effect.' in str(excinfo.value) assert 'It is pointless to set a value without a price mode.' in str(excinfo.value)
settings['price_mode'] = 'static:percent' settings['price_mode'] = 'static:percent'
import_vouchers.apply( import_vouchers.apply(
+1 -5
View File
@@ -339,17 +339,13 @@ class UserSettings2FATest(SoupTest):
def test_gen_emergency(self): def test_gen_emergency(self):
self.client.get('/control/settings/2fa/') self.client.get('/control/settings/2fa/')
assert not StaticDevice.objects.filter(user=self.user, name='emergency').exists()
self.client.post('/control/settings/2fa/regenemergency')
d = StaticDevice.objects.get(user=self.user, name='emergency') d = StaticDevice.objects.get(user=self.user, name='emergency')
assert d.token_set.count() == 10 assert d.token_set.count() == 10
old_tokens = set(t.token for t in d.token_set.all()) old_tokens = set(t.token for t in d.token_set.all())
self.client.post('/control/settings/2fa/regenemergency') self.client.post('/control/settings/2fa/regenemergency')
new_tokens = set(t.token for t in d.token_set.all())
d = StaticDevice.objects.get(user=self.user, name='emergency') d = StaticDevice.objects.get(user=self.user, name='emergency')
assert d.token_set.count() == 10 assert d.token_set.count() == 10
new_tokens = set(t.token for t in d.token_set.all())
assert old_tokens != new_tokens assert old_tokens != new_tokens
def test_delete_u2f(self): def test_delete_u2f(self):