Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel dd3821983f Remove back link from 404 error page (#23222967)
I've kept it for 400/403/500/csrffail for now, because they also have a
"try again" link. Yes, both things have browser buttons, but they make
it a *little* clearer to technical users what one could to next, and
especially on csrffail, "step back" is always possible and possibly actually
helpful.
2026-02-17 09:31:24 +01:00
91 changed files with 120612 additions and 140857 deletions
+3 -3
View File
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.53.*",
"sentry-sdk==2.52.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -110,10 +110,10 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.34.*",
"fakeredis==2.33.*",
"flake8==7.3.*",
"freezegun",
"isort==8.0.*",
"isort==7.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=0.24",
+1 -5
View File
@@ -188,15 +188,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(
nonce=serializer.validated_data['nonce'],
successful=False
).first()
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
successful=False
).first()
if prev:
# Ignore because nonce is already handled
+3 -15
View File
@@ -259,14 +259,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(
self.request.data,
{
'id': inst.pk,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
data=merge_dicts(self.request.data, {'id': inst.pk, 'acceptor_id': self.request.organizer.id})
)
@transaction.atomic()
@@ -297,11 +290,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
action='pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={
'value': diff,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
data={'value': diff, 'acceptor_id': self.request.organizer.id}
)
return inst
@@ -331,8 +320,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
data={
'value': value,
'text': text,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
'acceptor_id': self.request.organizer.id
}
)
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 {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -198,9 +197,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
@@ -475,7 +472,7 @@ def register_default_webhook_events(sender, **kwargs):
),
ParametrizedGiftcardTransactionWebhookEvent(
'pretix.giftcards.transaction.*',
_('Gift card used in transaction'),
_('Gift card used in transcation'),
)
)
-2
View File
@@ -651,7 +651,6 @@ class OrderListExporter(MultiSheetListExporter):
pgettext('address', 'State'),
_('Voucher'),
_('Voucher budget usage'),
_('Voucher tag'),
_('Pseudonymization ID'),
_('Ticket secret'),
_('Seat ID'),
@@ -770,7 +769,6 @@ class OrderListExporter(MultiSheetListExporter):
op.state_for_address or '',
op.voucher.code if op.voucher else '',
op.voucher_budget_use if op.voucher_budget_use else '',
op.voucher.tag if op.voucher else '',
op.pseudonymization_id,
op.secret,
]
+3 -3
View File
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price effect')
verbose_name = gettext_lazy('Price mode')
default_value = None
initial = 'static:none'
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
elif value in reverse:
return reverse[value]
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())
))
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
def clean(self, value, previous_values):
value = super().clean(value, previous_values)
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
def assign(self, value, obj: Voucher, **kwargs):
+1 -1
View File
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
)
)
price_mode = models.CharField(
verbose_name=_("Price effect"),
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
+1 -3
View File
@@ -1650,8 +1650,7 @@ class GiftCardPayment(BasePaymentProvider):
action='pretix.giftcards.transaction.payment',
data={
'value': trans.value,
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug
'acceptor_id': self.event.organizer.id
}
)
except PaymentException as e:
@@ -1683,7 +1682,6 @@ class GiftCardPayment(BasePaymentProvider):
data={
'value': refund.amount,
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug,
'text': refund.comment,
}
)
+4 -13
View File
@@ -389,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
# mail_send_task(self, *, outgoing_mail)
with scopes_disabled():
mail_send(**kwargs)
return False
return
else:
raise ValueError("Unknown arguments")
@@ -443,24 +443,15 @@ def mail_send_task(self, **kwargs) -> bool:
content = ct.file.read()
args.append((name, content, ct.type))
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
# why (probably some race condition with ticket cache invalidation?), so retry later.
try:
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
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)
self.retry(max_retries=5, countdown=60)
except MaxRetriesExceededError:
# Well then, something is really wrong, let's send it without attachment before we
# 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
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
+3 -8
View File
@@ -253,8 +253,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
auth=auth,
data={
'value': position.price,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
'acceptor_id': order.event.organizer.id
}
)
break
@@ -564,7 +563,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
data={
'value': -position.price,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
@@ -2459,8 +2457,7 @@ class OrderChangeManager:
auth=self.auth,
data={
'value': -position.price,
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
'acceptor_id': self.order.event.organizer.id
}
)
@@ -2486,8 +2483,7 @@ class OrderChangeManager:
auth=self.auth,
data={
'value': -opa.position.price,
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
'acceptor_id': self.order.event.organizer.id
}
)
@@ -3457,7 +3453,6 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
data={
'value': trans.value,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
any_giftcards = True
+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
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
from phonenumber_field.formfields import PhoneNumberField
from pretix.base.forms import I18nModelForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
)
from pretix.base.models import WaitingListEntry
from pretix.control.forms.widgets import Select2
class WaitingListEntryEditForm(I18nModelForm):
itemvar = forms.ChoiceField(
error_messages={
'invalid_choice': _("Select a valid choice.")
}
)
class WaitingListEntryTransferForm(I18nModelForm):
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)
if self.event.has_subevents:
@@ -72,73 +45,12 @@ class WaitingListEntryEditForm(I18nModelForm):
}
)
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:
model = WaitingListEntry
fields = [
'email',
'name_parts',
'phone',
'subevent',
]
field_classes = {
'subevent': SafeModelChoiceField,
'email': forms.EmailField,
'phone': PhoneNumberField,
}
widgets = {
'phone': WrappedPhoneNumberPrefixWidget,
}
@@ -19,14 +19,6 @@
</ul>
<br>
{% 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 %}
{% bootstrap_form form %}
<div class="form-group buttons">
@@ -144,23 +144,14 @@
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed %}
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 %}
{% 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." %}
</p>
{% if static_tokens_device %}
<p>
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% endif %}
<p>{% trans "Unused tokens:" %}</p>
<ul>
{% for t in static_tokens %}
<li><code>{{ t.token }}</code></li>
{% endfor %}
</ul>
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% 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>
{% endfor %}
</select>
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
@@ -268,13 +267,13 @@
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
{% if request.event.has_subevents %}
<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 "Transfer to other date" context "subevent" %}"
data-toggle="tooltip">
<i class="fa fa-calendar" aria-hidden="true"></i>
</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>
{% else %}
<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/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'),
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
name='event.orders.waitinglist.edit'),
re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
name='event.orders.waitinglist.transfer'),
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/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))
else:
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['can_register'] = settings.PRETIX_REGISTRATION
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
+2 -4
View File
@@ -1850,8 +1850,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
data={
'value': value,
'text': request.POST.get('text'),
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
'acceptor_id': self.request.organizer.id
},
user=self.request.user,
)
@@ -1914,8 +1913,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
user=self.request.user,
data={
'value': form.cleaned_data['value'],
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
'acceptor_id': self.request.organizer.id
}
)
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.urls import reverse
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.html import format_html
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -87,9 +85,8 @@ logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin:
max_time = 900
max_time = 3600
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
@@ -292,13 +289,16 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
ctx = super().get_context_data()
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:
ctx['static_tokens_device'] = StaticDevice.objects.filter(
ctx['static_tokens'] = StaticDevice.objects.filter(
user=self.request.user, name='emergency'
).first()
).first().token_set.all()
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'] = []
for dt in REAL_DEVICE_TYPES:
@@ -630,14 +630,8 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
])
self.request.user.update_session_token()
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 '
'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())
)
)
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.'))
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.services.waitinglist import assign_automatically
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.views import PaginationMixin
@@ -138,17 +138,6 @@ class WaitingListQuerySetMixin:
elif force_filtered and '__ALL' not in self.request_data:
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
@@ -249,7 +238,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
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 = {}
quota_cache = {}
@@ -401,20 +390,25 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
})
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/edit.html'
template_name = 'pretixcontrol/waitinglist/transfer.html'
permission = 'can_change_orders'
form_class = WaitingListEntryEditForm
form_class = WaitingListEntryTransferForm
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:
return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True)
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('The waitinglist entry has been transferred.'))
if form.has_changed():
messages.success(self.request, _('The waitinglist entry has been changed.'))
self.object.log_action(
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
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
OK
Online-Banking
Online-Banking-Nutzer
Onlinebanking
Onlinebanking-Zugangsdaten
Open
@@ -540,8 +539,6 @@ WeChat-Zahlung
Weiterleitungs-URIs
Weiterleitungs-URL
Weiterleitungs-URLs
WERO
WERO-App
WhatsApp
Widget
Widget-Code
File diff suppressed because it is too large Load Diff
@@ -265,7 +265,6 @@ Objekt-IDs
Offline-Scan
OK
Online-Banking
Online-Banking-Nutzer
Onlinebanking
Onlinebanking-Zugangsdaten
Open
@@ -540,8 +539,6 @@ WeChat-Zahlung
Weiterleitungs-URIs
Weiterleitungs-URL
Weiterleitungs-URLs
WERO
WERO-App
WhatsApp
Widget
Widget-Code
+2185 -2433
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-20 13:01+0000\n"
"POT-Creation-Date: 2026-01-26 13:20+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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
+4 -4
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"PO-Revision-Date: 2026-02-12 20:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
"Language: ja\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:62
@@ -256,7 +256,7 @@ msgstr "承認保留中"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Redeemed"
msgstr "引き換え済み"
msgstr "使用済"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
msgid "Cancel"
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"
"Report-Msgid-Bugs-To: \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"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
"nl/>\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:62
@@ -674,7 +674,7 @@ msgstr "Zoekopdracht"
#: pretix/static/pretixcontrol/js/ui/main.js:461
msgid "All"
msgstr "Allemaal"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:462
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
Wechat
WeChat
WERO
WhatsApp
whitespace
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 "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
{% if settings.bank_details_type == "sepa" %}
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd>
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
{% endif %}
</dl>
{% if details %}
@@ -38,4 +38,4 @@
{% if payment_qr_codes %}
{% include "pretixpresale/event/payment_qr_codes.html" %}
{% endif %}
</div>
</div>
+9 -16
View File
@@ -786,12 +786,7 @@ class PaypalMethod(BasePaymentProvider):
else:
pp_captured_order = response.result
payment.refresh_from_db()
any_captures = False
all_captures_completed = True
for purchaseunit in pp_captured_order.purchase_units:
if hasattr(purchaseunit, 'payments'):
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)
@@ -799,16 +794,14 @@ class PaypalMethod(BasePaymentProvider):
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
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()
if pp_captured_order.status != 'COMPLETED':
payment.fail(info=pp_captured_order.dict())
-22
View File
@@ -118,7 +118,6 @@ logger = logging.getLogger('pretix.plugins.stripe')
# - UPI: ✗
# - Netbanking: ✗
# - TWINT: ✓
# - Wero: ✓ (No settings UI yet)
#
# Bank transfers
# - ACH Bank Transfer: ✗
@@ -510,15 +509,6 @@ class StripeSettingsHolder(BasePaymentProvider):
'before they work properly.'),
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
)
if not self.settings.connect_client_id or self.settings.secret_key:
@@ -1956,15 +1946,3 @@ class StripeMobilePay(StripeRedirectMethod):
"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,
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
StripeWeChatPay, StripeWero,
StripeWeChatPay,
)
return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
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_client_secret">{{ payment_intent_client_secret }}</script>
{% 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 %}
{% if payment_intent_redirect_action_handling %}
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
-24
View File
@@ -1177,30 +1177,6 @@ def test_store_failed(token_client, organizer, clist, event, order):
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
def test_redeem_unknown(token_client, organizer, clist, event, order):
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(
args=(event.pk, inputfile_factory().id, settings, 'en', user.pk)
).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'
import_vouchers.apply(
+1 -5
View File
@@ -339,17 +339,13 @@ class UserSettings2FATest(SoupTest):
def test_gen_emergency(self):
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')
assert d.token_set.count() == 10
old_tokens = set(t.token for t in d.token_set.all())
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')
assert d.token_set.count() == 10
new_tokens = set(t.token for t in d.token_set.all())
assert old_tokens != new_tokens
def test_delete_u2f(self):
+23 -171
View File
@@ -19,6 +19,19 @@
# 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/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Daniel
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from datetime import timedelta
import pytest
@@ -26,8 +39,7 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher,
WaitingListEntry,
Event, Item, Organizer, Quota, Team, User, Voucher, WaitingListEntry,
)
from pretix.control.views.dashboards import waitinglist_widgets
@@ -40,12 +52,11 @@ def env():
date_from=now(), plugins='pretix.plugins.banktransfer,tests.testdummy'
)
event.settings.set('ticketoutput_testdummy__enabled', True)
event.settings.set('waiting_list_names_asked', False)
event.settings.set('waiting_list_names_required', False)
event.settings.set('waiting_list_phones_asked', False)
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
item1 = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True, allow_waitinglist=True)
item2 = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23,
admission=True)
item2 = Item.objects.create(event=event, name="Ticket", default_price=23,
admission=True)
for i in range(5):
WaitingListEntry.objects.create(
@@ -67,16 +78,7 @@ def env():
t = Team.objects.create(organizer=o, can_view_orders=True, can_change_orders=True)
t.members.add(user)
t.limit_events.add(event)
wle = WaitingListEntry.objects.filter(item=item1).first()
variation = ItemVariation.objects.create(item=item1)
return {
"event": event,
"item1": item1,
"wle": wle,
"variation": variation,
}
return event, user, o, item1
@pytest.mark.django_db
@@ -120,7 +122,7 @@ def test_list(client, env):
assert 'foo0@bar.com' not in response.content.decode()
assert 'valid@example.org' not in response.content.decode()
response = client.get('/control/event/dummy/dummy/waitinglist/?item=%d' % env['item1'].pk)
response = client.get('/control/event/dummy/dummy/waitinglist/?item=%d' % env[3].pk)
assert 'item2@example.org' not in response.content.decode()
assert 'foo0@bar.com' in response.content.decode()
@@ -189,161 +191,11 @@ def test_delete_bulk(client, env):
WaitingListEntry.objects.get(id=wle.id)
@pytest.mark.django_db
def test_edit_settings(client, env):
event = env['event']
wle = env['wle']
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id)
assert ['email', 'itemvar'] == list(response.context_data['form'].fields.keys())
event.settings.set('waiting_list_names_asked', True)
response = client.get('/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id)
assert 'name_parts' in list(response.context_data['form'].fields.keys())
event.settings.set('waiting_list_names_required', True)
response = client.get('/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id)
assert response.context_data['form'].fields['name_parts'].required is True
event.settings.set('waiting_list_phones_asked', True)
response = client.get('/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id)
assert 'phone' in list(response.context_data['form'].fields.keys())
@pytest.mark.django_db
def test_edit_itemvariation(client, env):
item = env['item1']
variation = env['variation']
wle = env['wle']
client.login(email='dummy@dummy.dummy', password='dummy')
itemvar = f"{item.pk}-{variation.pk}"
client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": itemvar
}
)
wle.refresh_from_db()
assert wle.variation == variation
@pytest.mark.django_db
def test_edit_validations_only_valid_item(client, env):
item = env['item1']
wle = env['wle']
client.login(email='dummy@dummy.dummy', password='dummy')
itemvar = f"{item.pk + 10000}"
response = client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": itemvar
}
)
assert response.context_data['form'].errors['itemvar'] == ["Select a valid choice."]
@pytest.mark.django_db
def test_edit_validations_only_valid_variation(client, env):
item = env['item1']
wle = env['wle']
variation = env['variation']
client.login(email='dummy@dummy.dummy', password='dummy')
itemvar = f"{item.pk}-{variation.pk + 1}"
response = client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": itemvar
}
)
assert response.context_data['form'].errors['itemvar'] == ["Select a valid choice."]
@pytest.mark.django_db
def test_edit_validations_inactive_item(client, env):
item = env['item1']
wle = env['wle']
item.active = False
item.save()
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": f"{item.pk}"
}
)
assert response.context_data['form'].errors['itemvar'] == ["The selected product is not active."]
@pytest.mark.django_db
def test_edit_validations_inactive_variation(client, env):
item = env['item1']
wle = env['wle']
variation = env['variation']
wle.variation = variation
wle.save()
variation.active = False
variation.save()
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": f"{item.pk}-{variation.pk}"
}
)
assert response.context_data['form'].errors['itemvar'] == ["The selected product is not active."]
@pytest.mark.django_db
def test_edit_voucher_send_out(client, env):
event = env['event']
item = env['item1']
wle = env['wle']
quota = Quota.objects.create(event=event, size=100)
quota.items.add(item)
client.login(email='dummy@dummy.dummy', password='dummy')
with scopes_disabled():
wle.send_voucher()
response = client.post(
'/control/event/dummy/dummy/waitinglist/%s/edit' % wle.id,
data={
"email": f"1_{wle.email}",
"itemvar": item.pk
},
follow=True
)
assert response.status_code == 404
@pytest.mark.django_db
def test_dashboard(client, env):
with scopes_disabled():
quota = Quota.objects.create(name="Test", size=2, event=env['event'])
quota.items.add(env['item1'])
w = waitinglist_widgets(env['event'])
quota = Quota.objects.create(name="Test", size=2, event=env[0])
quota.items.add(env[3])
w = waitinglist_widgets(env[0])
assert '1' in w[0]['content']
assert '5' in w[1]['content']