Compare commits

..
Author SHA1 Message Date
Raphael Michel 8f65466875 Reset throttle 2026-09-21 14:27:53 +02:00
Raphael Michel 6a181506e9 Add reset button 2026-09-18 17:32:00 +02:00
Raphael Michel 84df699503 User details: Show list of 2FA devices 2026-09-18 16:32:27 +02:00
31 changed files with 302 additions and 187 deletions
+5 -1
View File
@@ -77,7 +77,11 @@ class BaseCartPositionCreateSerializer(I18nAwareModelSerializer):
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if subevent and subevent.event != self.context['event']:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
+5 -1
View File
@@ -1094,7 +1094,11 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if subevent and subevent.event != self.context['event']:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
+2 -6
View File
@@ -275,15 +275,11 @@ class OrderPositionChangeSerializer(serializers.ModelSerializer):
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if self.instance.subevent_id and not subevent:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if not self.instance.subevent_id and subevent:
raise ValidationError(
'You cannot set a subevent if none was set previously.'
)
if subevent and subevent.event != self.context['event']:
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
-1
View File
@@ -207,7 +207,6 @@ class OrderListExporter(MultiSheetListExporter):
if form_data.get('event_date_range'):
dt_start, dt_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['event_date_range'], self.timezone)
# Subevent-less positions in a series will never be found when this filter is set but that seems like a valid way to do this
if dt_start:
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
+4 -6
View File
@@ -1202,9 +1202,8 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
return field
def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
validate_address,
)
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
d = super().clean()
@@ -1445,9 +1444,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on'
def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time
validate_address,
)
from pretix.base.addressvalidation import \
validate_address # local import to prevent impact on startup time
data = self.cleaned_data
+11 -3
View File
@@ -896,7 +896,7 @@ class Item(LoggedModel):
return check_quotas
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
include_bundled=False, fail_on_no_quotas=False):
include_bundled=False, trust_parameters=False, fail_on_no_quotas=False):
"""
This method is used to determine whether this Item is currently available
for sale.
@@ -906,11 +906,15 @@ class Item(LoggedModel):
to no quotas being checked at all, this method will return
unlimited availability.
:param include_bundled: Also take availability of bundled items into consideration.
:param trust_parameters: Disable checking of the subevent parameter and disable checking if
any variations exist (performance optimization).
:returns: any of the return codes of :py:meth:`Quota.availability()`.
:raises ValueError: if you call this on an item which has variations associated with it.
Please use the method on the ItemVariation object you are interested in.
"""
if not trust_parameters and not subevent and self.event.has_subevents:
raise TypeError('You need to supply a subevent.')
check_quotas = self._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
quotacounter = Counter()
res = Quota.AVAILABILITY_OK, None
@@ -1305,7 +1309,7 @@ class ItemVariation(models.Model):
return check_quotas
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None,
include_bundled=False, fail_on_no_quotas=False) -> Tuple[int, int]:
include_bundled=False, trust_parameters=False, fail_on_no_quotas=False) -> Tuple[int, int]:
"""
This method is used to determine whether this ItemVariation is currently
available for sale in terms of quotas.
@@ -1317,6 +1321,8 @@ class ItemVariation(models.Model):
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
:returns: any of the return codes of :py:meth:`Quota.availability()`.
"""
if not trust_parameters and not subevent and self.item.event.has_subevents: # NOQA
raise TypeError('You need to supply a subevent.')
check_quotas = self._get_quotas(ignored_quotas=ignored_quotas, subevent=subevent)
quotacounter = Counter()
res = Quota.AVAILABILITY_OK, None
@@ -2197,7 +2203,9 @@ class Quota(LoggedModel):
@staticmethod
def clean_subevent(event, subevent):
if event.has_subevents:
if subevent and event != subevent.event:
if not subevent:
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != subevent.event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if subevent:
+9 -9
View File
@@ -656,8 +656,8 @@ class Order(LockModel, LoggedModel):
terms = [
until.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
] or [until.datetime(self.event)] # use event settings only if there is no subevent in the cart
return min(terms)
]
return min(terms) if terms else None
else:
return until.datetime(self.event)
@@ -672,8 +672,8 @@ class Order(LockModel, LoggedModel):
terms = [
until.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
] or [until.datetime(self.event)] # use event settings only if there is no subevent in the cart
return min(terms)
]
return min(terms) if terms else None
else:
return until.datetime(self.event)
@@ -890,8 +890,8 @@ class Order(LockModel, LoggedModel):
dates = [
modify_deadline.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
] or [modify_deadline.datetime(self.event)] # use event settings only if there is no subevent in the cart
return min(dates)
]
return min(dates) if dates else None
elif modify_deadline:
return modify_deadline.datetime(self.event)
return None
@@ -953,8 +953,8 @@ class Order(LockModel, LoggedModel):
dates = [
dl_date.datetime(se)
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
] or [dl_date.datetime(self.event)] # use event settings only if there is no subevent in the cart
dl_date = min(dates)
]
dl_date = min(dates) if dates else None
else:
dl_date = dl_date.datetime(self.event)
return dl_date
@@ -983,7 +983,7 @@ class Order(LockModel, LoggedModel):
terms = [
term_last.datetime(se).date()
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
] or [term_last.datetime(self.event)] # use event settings only if there is no subevent in the cart
]
if terms:
term_last = min(terms)
else:
+3 -1
View File
@@ -300,7 +300,9 @@ class WaitingListEntry(LoggedModel):
@staticmethod
def clean_subevent(event, subevent):
if event.has_subevents:
if subevent and event != subevent.event:
if not subevent:
raise ValidationError(_('Subevent cannot be null for event series.'))
if event != subevent.event:
raise ValidationError(_('The subevent does not belong to this event.'))
else:
if subevent:
+4 -5
View File
@@ -637,7 +637,6 @@ class BasePaymentProvider:
def _absolute_availability_date(self, rel_date, cart_id=None, order=None, aggregate_fn=min):
if not rel_date:
return None
if self.event.has_subevents and cart_id:
dates = [
rel_date.datetime(se).date()
@@ -646,16 +645,16 @@ class BasePaymentProvider:
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
] or [rel_date.datetime(self.event).date()] # Use event dates only on carts with no subevents
return aggregate_fn(dates)
]
return aggregate_fn(dates) if dates else None
elif self.event.has_subevents and order:
dates = [
rel_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
] or [rel_date.datetime(self.event).date()] # Use event dates only on carts with no subevents
return aggregate_fn(dates)
]
return aggregate_fn(dates) if dates else None
elif self.event.has_subevents:
raise NotImplementedError('Payment provider is not subevent-ready.')
else:
+3 -1
View File
@@ -793,7 +793,9 @@ class CartManager:
operations = []
for i in items:
if self.event.has_subevents and i.get('subevent'):
if self.event.has_subevents:
if not i.get('subevent') or int(i.get('subevent')) not in self._subevents_cache:
raise CartError(error_messages['subevent_required'])
subevent = self._subevents_cache[int(i.get('subevent'))]
else:
subevent = None
+2 -2
View File
@@ -58,9 +58,9 @@ class CrossSellingService:
result = (
(DummyCategory(category, subevent),
self._prepare_items(subevent, items_qs, discount_info),
f'subevent_{subevent.pk}_' if subevent else '')
f'subevent_{subevent.pk}_')
for subevent in subevents
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk if subevent else 0)
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
)
else:
result = (
+3 -2
View File
@@ -289,7 +289,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
answ.to_string_i18n()
)
if invoice.event.has_subevents and p.subevent_id:
if invoice.event.has_subevents:
desc += "<br />" + pgettext("subevent", "Date: {}").format(p.subevent)
if invoice.event.settings.invoice_event_location and location and len(locations) > 1:
@@ -423,7 +423,8 @@ def _service_period_for_position(invoice, position, invoice_dt):
period_start = position.subevent.date_from
period_end = position.subevent.date_to
else:
# Does not make sense to use the parent event date here
# Currently impossible case, but might not be in the future and never makes
# sense to use the event date here
period_start = invoice_dt
period_end = invoice_dt
elif invoice.event.settings.invoice_period == "auto_no_event":
+4 -2
View File
@@ -53,7 +53,7 @@ from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
Sum, Value,
)
from django.db.models.functions import Cast, Coalesce, Greatest
from django.db.models.functions import Cast, Greatest
from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
@@ -1548,7 +1548,7 @@ def send_download_reminders(sender, **kwargs):
if event.has_subevents:
qs = qs.annotate(
first_date=Coalesce(Min('all_positions__subevent__date_from'), Value(event.date_from))
first_date=Min('all_positions__subevent__date_from')
).filter(
Q(first_date__gte=today)
)
@@ -1954,6 +1954,8 @@ class OrderChangeManager:
is_bundled = True
else:
raise OrderError(self.error_messages['addon_invalid'])
if self.order.event.has_subevents and not subevent:
raise OrderError(self.error_messages['subevent_required'])
seated = item.seat_category_mappings.filter(subevent=subevent).exists()
if seated and not seat and self.event.settings.seating_choice:
+1
View File
@@ -342,6 +342,7 @@ class QuotaForm(I18nModelForm):
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']
+2 -1
View File
@@ -362,7 +362,7 @@ class OrderPositionAddForm(forms.Form):
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
label=pgettext_lazy('subevent', 'Date'),
required=False,
required=True,
empty_label=None
)
@@ -416,6 +416,7 @@ class OrderPositionAddForm(forms.Form):
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
self.fields['subevent'].required = True
else:
del self.fields['subevent']
change_decimal_field(self.fields['price'], order.event.currency)
+1 -1
View File
@@ -59,7 +59,7 @@ class WaitingListEntryEditForm(I18nModelForm):
super().__init__(*args, **kwargs)
if self.event.has_subevents and self.instance.subevent_id:
if self.event.has_subevents:
self.fields['subevent'].required = True
self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2(
+1
View File
@@ -774,6 +774,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.resetdrift': _('Drift and throttle values for two-factor devices have been reset.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
@@ -150,11 +150,7 @@
</td>
{% if request.event.has_subevents %}
<td>
{% if q.subevent %}
{{ q.subevent.name }} {{ q.subevent.get_date_range_display_with_times }}
{% else %}
{% endif %}
{{ q.subevent.name }} {{ q.subevent.get_date_range_display_with_times }}
</td>
{% endif %}
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
@@ -113,7 +113,7 @@
{% endif %}
</div>
<div class="col-sm-5">
{{ position.subevent|default_if_none:"" }}
{{ position.subevent }}
</div>
<div class="col-sm-4">
{% bootstrap_field position.form.subevent layout='inline' %}
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "User" %}{% endblock %}
{% block content %}
<h1>{% trans "User" %} {{ user.email }}</h1>
@@ -59,8 +60,83 @@
{% bootstrap_field form.is_verified layout='control' %}
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<button class="btn btn-default btn-xs pull-right" type="submit" form="resetdriftthrottle">
{% trans "Reset drift and throttle" %}
</button>
<h3 class="panel-title">
{% trans "Available two-factor authentication methods" %}
</h3>
</div>
<table class="panel-body table table-hover">
{% for d in devices %}
<tr>
<td>
{% if d.devicetype == 'totp' %}
TOTP
{% elif d.devicetype == 'u2f' %}
U2F
{% elif d.devicetype == 'webauthn' %}
WebAuthn
{% elif d.devicetype == 'emergency' %}
{% trans "Emergency tokens" %}
{% endif %}
{% if d.confirmed %}
{% icon "check" %}
{% else %}
{% icon "warning" %}
{% endif %}
</td>
<td>
{{ d.name }}
</td>
<td>
{% if d.throttling_failure_timestamp %}
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
1 failed attempt since {{ date }}
{% plural %}
{{ cnt }} failed attempts since {{ date }}
{% endblocktrans %}
<br>
{% endif %}
{% if d.throttling_enabled and not d.verify_is_allowed.0 %}
<strong>
{% blocktrans trimmed with date=d.verify_is_allowed.1.locked_until|date:"SHORT_DATETIME_FORMAT" %}
Currently locked until {{ date }}
{% endblocktrans %}
</strong>
<br>
{% endif %}
{% if d.devicetype == 'totp' %}
<small>
<code>step = {{ d.step }},
t0 = {{ d.t0 }},
digits = {{ d.digits }},
tolerance = {{ d.tolerance }},
drift = {{ d.drift }},
last_t = {{ d.last_t }}</code>
</small>
{% elif d.devicetype == 'u2f' %}
<small>
<code>sign_count = {{ d.sign_count }}</code>
</small>
{% elif d.devicetype == 'emergency' %}
<small>
<code>token_count = {{ d.token_set.count }}</code>
</small>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Team memberships" %}</legend>
@@ -102,4 +178,8 @@
</div>
</div>
</div>
<form action="{% url "control:users.resetdriftthrottle" id=user.pk %}" id="resetdriftthrottle" method="post">
{% csrf_token %}
</form>
{% endblock %}
@@ -203,11 +203,7 @@
</td>
{% if request.event.has_subevents %}
<td>
{% if v.subevent %}
{{ v.subevent.name }} {{ v.subevent.get_date_range_display_with_times }}
{% else %}
{% endif %}
{{ v.subevent.name }} {{ v.subevent.get_date_range_display_with_times }}
</td>
{% endif %}
<td class="text-right flip">
@@ -209,13 +209,7 @@
{% endif %}
</td>
{% if request.event.has_subevents %}
<td>
{% if e.subevent %}
{{ e.subevent }}
{% else %}
{% endif %}
</td>
<td>{{ e.subevent }}</td>
{% endif %}
<td>
{{ e.created|date:"SHORT_DATETIME_FORMAT" }}
+1
View File
@@ -78,6 +78,7 @@ urlpatterns = [
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
re_path(r'^users/(?P<id>\d+)/resetdriftthrottle$', users.Reset2FADriftThrottleView.as_view(), name='users.resetdriftthrottle'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
+39 -2
View File
@@ -41,15 +41,18 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import ListView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from hijack import signals
from pretix.base.auth import get_auth_backends
from pretix.base.models import User
from pretix.base.models import U2FDevice, User, WebAuthnDevice
from pretix.control.forms.filter import UserFilterForm
from pretix.control.forms.users import UserEditForm
from pretix.control.permissions import AdministratorPermissionRequiredMixin
from pretix.control.views import CreateView, UpdateView
from pretix.control.views.user import RecentAuthenticationRequiredMixin
from pretix.control.views.user import (
REAL_DEVICE_TYPES, RecentAuthenticationRequiredMixin,
)
def get_used_backend(request):
@@ -107,6 +110,21 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
ctx['backend'] = (
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
)
ctx['devices'] = []
for dt in [*REAL_DEVICE_TYPES, StaticDevice]:
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
for obj in objs:
if dt == TOTPDevice:
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
elif dt == WebAuthnDevice:
obj.devicetype = 'webauthn'
elif dt == StaticDevice:
obj.devicetype = 'emergency'
ctx['devices'] += objs
return ctx
def get_success_url(self):
@@ -183,6 +201,25 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
return reverse('control:users.edit', kwargs=self.kwargs)
class Reset2FADriftThrottleView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
self.object.totpdevice_set.update(drift=0, throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.staticdevice_set.update(throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
messages.success(request, _(
'The drift values for TOTP devices have been reset.'
))
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('control:users.edit', kwargs=self.kwargs)
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = "pretixcontrol/users/anonymize.html"
+1 -1
View File
@@ -358,7 +358,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
str(w.priority)
]
if self.request.event.has_subevents:
row.append(str(w.subevent) if w.subevent else '')
row.append(str(w.subevent))
writer.writerow(row)
r = HttpResponse(output.getvalue().encode("utf-8"), content_type='text/csv')
+13 -23
View File
@@ -418,7 +418,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
str(op.item) + (" " + str(op.variation.value) if op.variation else ""),
money_filter(op.price, self.event.currency),
)
if self.event.has_subevents and op.subevent and not cl.subevent:
if self.event.has_subevents and not cl.subevent:
item += '\n{} ({})'.format(
op.subevent.name,
date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
@@ -612,19 +612,14 @@ class CSVCheckinList(CheckInListMixin, ListExporter):
row.append(op.attendee_email or (op.addon_to.attendee_email if op.addon_to else '') or op.order.email or '')
row.append(str(op.order.phone) if op.order.phone else '')
if self.event.has_subevents:
if op.subevent:
row.append(str(op.subevent.name))
row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT'))
if op.subevent.date_to:
row.append(
date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
)
else:
row.append('')
row.append(str(op.subevent.name))
row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT'))
if op.subevent.date_to:
row.append(
date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
)
else:
row.append('')
row.append('')
row.append('')
acache = {}
if op.addon_to:
for a in op.addon_to.answers.all():
@@ -727,19 +722,14 @@ class CSVCheckinCodeList(CheckInListMixin, ListExporter):
_('Yes') if op.order.status == Order.STATUS_PAID else _('No'),
]
if self.event.has_subevents:
if op.subevent:
row.append(str(op.subevent.name))
row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT'))
if op.subevent.date_to:
row.append(
date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
)
else:
row.append('')
row.append(str(op.subevent.name))
row.append(date_format(op.subevent.date_from.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT'))
if op.subevent.date_to:
row.append(
date_format(op.subevent.date_to.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
)
else:
row.append('')
row.append('')
row.append('')
row += [
date_format(op.valid_from, 'SHORT_DATETIME_FORMAT') if op.valid_from else '',
+1 -1
View File
@@ -55,7 +55,7 @@ class ScheduledMail(models.Model):
id = models.BigAutoField(primary_key=True)
rule = models.ForeignKey("Rule", on_delete=models.CASCADE)
subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE) # must be set in a series, other case unsupported
subevent = models.ForeignKey(SubEvent, null=True, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
last_computed = models.DateTimeField(auto_now_add=True)
@@ -185,68 +185,69 @@
</div>
{% eventsignal event "pretix.presale.signals.front_page_top" request=request subevent=subevent %}
{% endif %}
{% endif %}
{% if ev.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask
data-asynctask-headline="{% trans "We're now trying to reserve this for you!" %}"
data-asynctask-text="{% blocktrans with time=event.settings.reservation_time %}Once the items are in your cart, you will have {{ time }} minutes to complete your purchase.{% endblocktrans %}"
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ cart_redirect|urlencode }}&next_error={{ request.path|urlencode }}">
{% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
{% if ev.seating_plan_id and event.settings.seating_choice %}
{% if event.has_subevents %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request subevent=subevent %}
{% else %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request %}
{% if ev.presale_is_running or event.settings.show_items_outside_presale_period %}
<form method="post" data-asynctask
data-asynctask-headline="{% trans "We're now trying to reserve this for you!" %}"
data-asynctask-text="{% blocktrans with time=event.settings.reservation_time %}Once the items are in your cart, you will have {{ time }} minutes to complete your purchase.{% endblocktrans %}"
action="{% eventurl request.event "presale:event.cart.add" cart_namespace=cart_namespace %}?next={{ cart_redirect|urlencode }}&next_error={{ request.path|urlencode }}">
{% csrf_token %}
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
{% if ev.seating_plan_id and event.settings.seating_choice %}
{% if event.has_subevents %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request subevent=subevent %}
{% else %}
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request %}
{% endif %}
{% endif %}
{% endif %}
{% if waitinglist_seated %}
<aside class="front-page" aria-labelledby="waiting-list">
<h3 id="waiting-list" class="sr-only">{% trans "Waiting list" %}</h3>
<div class="row">
<div class="col-md-8 col-sm-6 col-xs-12">
<p>
{% blocktrans trimmed %}
Some of the categories in the seating plan above are currently sold out. If you want, you can add yourself to the
waiting list. We will then notify if seats are available again.
{% endblocktrans %}
</p>
{% if waitinglist_seated %}
<aside class="front-page" aria-labelledby="waiting-list">
<h3 id="waiting-list" class="sr-only">{% trans "Waiting list" %}</h3>
<div class="row">
<div class="col-md-8 col-sm-6 col-xs-12">
<p>
{% blocktrans trimmed %}
Some of the categories in the seating plan above are currently sold out. If you want, you can add yourself to the
waiting list. We will then notify if seats are available again.
{% endblocktrans %}
</p>
</div>
<div class="col-md-4 col-sm-6 col-xs-12">
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}{% if subevent %}?subevent={{ subevent.pk }}{% endif %}" class="btn btn-default btn-block">
<span class="fa fa-plus-circle" aria-hidden="true"></span>
{% trans "Join waiting list" %}
</a>
</div>
<div class="clearfix"></div>
</div>
<div class="col-md-4 col-sm-6 col-xs-12">
<a href="{% eventurl event "presale:event.waitinglist" cart_namespace=cart_namespace|default_if_none:"" %}{% if subevent %}?subevent={{ subevent.pk }}{% endif %}" class="btn btn-default btn-block">
<span class="fa fa-plus-circle" aria-hidden="true"></span>
{% trans "Join waiting list" %}
</a>
</div>
<div class="clearfix"></div>
</div>
</aside>
{% endif %}
</aside>
{% endif %}
<h2 class="sr-only">{% trans "Products" %}</h2>
{% include "pretixpresale/event/fragment_product_list.html" %}
{% if ev.presale_is_running and display_add_to_cart %}
<div class="front-page">
<div class="row">
<div class="col-md-4 col-md-offset-8 col-xs-12">
<button class="btn btn-block btn-primary btn-lg" type="submit" id="btn-add-to-cart">
{% if request.event.settings.redirect_to_checkout_directly %}
{% if allfree %}
<i class="fa fa-check" aria-hidden="true"></i> {% trans "Register" context "free_tickets" %}
<h2 class="sr-only">{% trans "Products" %}</h2>
{% include "pretixpresale/event/fragment_product_list.html" %}
{% if ev.presale_is_running and display_add_to_cart %}
<div class="front-page">
<div class="row">
<div class="col-md-4 col-md-offset-8 col-xs-12">
<button class="btn btn-block btn-primary btn-lg" type="submit" id="btn-add-to-cart">
{% if request.event.settings.redirect_to_checkout_directly %}
{% if allfree %}
<i class="fa fa-check" aria-hidden="true"></i> {% trans "Register" context "free_tickets" %}
{% else %}
<i class="fa fa-shopping-cart" aria-hidden="true"></i> {% trans "Proceed with checkout" %}
{% endif %}
{% else %}
<i class="fa fa-shopping-cart" aria-hidden="true"></i> {% trans "Proceed with checkout" %}
<i class="fa fa-shopping-cart" aria-hidden="true"></i> {% trans "Add to cart" %}
{% endif %}
{% else %}
<i class="fa fa-shopping-cart" aria-hidden="true"></i> {% trans "Add to cart" %}
{% endif %}
</button>
</button>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div>
</div>
{% endif %}
</form>
{% endif %}
</form>
{% endif %}
{% endif %}
{% if show_vouchers %}
<aside class="front-page" aria-labelledby="redeem-a-voucher">
+1 -1
View File
@@ -814,7 +814,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
if hasattr(self, 'voucher') and self.voucher.subevent:
self.subevent = self.voucher.subevent
if not err and not self.subevent: # TODO
if not err and not self.subevent:
return redirect_to_url(
eventreverse(
self.request.event, 'presale:event.index',
+43 -42
View File
@@ -194,54 +194,55 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
context['allow_waitinglist'] = context['ev'].waiting_list_active and context['ev'].presale_is_running
# Fetch all items
items, display_add_to_cart = prepare_item_list_for_shop(
self.request.event,
subevent=self.subevent,
filter_items=self.request.GET.getlist('item'),
filter_categories=self.request.GET.getlist('category'),
require_seat=None,
channel=self.request.sales_channel,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None
),
)
if not self.request.event.has_subevents or self.subevent:
# Fetch all items
items, display_add_to_cart = prepare_item_list_for_shop(
self.request.event,
subevent=self.subevent,
filter_items=self.request.GET.getlist('item'),
filter_categories=self.request.GET.getlist('category'),
require_seat=None,
channel=self.request.sales_channel,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None
),
)
context['waitinglist_seated'] = False
if context['allow_waitinglist']:
for i in items:
if not i.allow_waitinglist or not i.requires_seat:
continue
context['waitinglist_seated'] = False
if context['allow_waitinglist']:
for i in items:
if not i.allow_waitinglist or not i.requires_seat:
continue
if i.has_variations:
for v in i.available_variations:
if v.cached_availability[0] != Quota.AVAILABILITY_OK:
if i.has_variations:
for v in i.available_variations:
if v.cached_availability[0] != Quota.AVAILABILITY_OK:
context['waitinglist_seated'] = True
break
else:
if i.cached_availability[0] != Quota.AVAILABILITY_OK:
context['waitinglist_seated'] = True
break
else:
if i.cached_availability[0] != Quota.AVAILABILITY_OK:
context['waitinglist_seated'] = True
break
items = [i for i in items if not i.requires_seat]
context['itemnum'] = len(items)
context['allfree'] = all(
item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons
for item in items if not item.has_variations
) and all(
all(
var.display_price.gross == Decimal('0.00')
for var in item.available_variations
) and not item.mandatory_priced_addons
for item in items if item.has_variations
)
items = [i for i in items if not i.requires_seat]
context['itemnum'] = len(items)
context['allfree'] = all(
item.display_price.gross == Decimal('0.00') and not item.mandatory_priced_addons
for item in items if not item.has_variations
) and all(
all(
var.display_price.gross == Decimal('0.00')
for var in item.available_variations
) and not item.mandatory_priced_addons
for item in items if item.has_variations
)
# Regroup those by category
context['items_by_category'] = item_group_by_category(items)
context['display_add_to_cart'] = display_add_to_cart
# Regroup those by category
context['items_by_category'] = item_group_by_category(items)
context['display_add_to_cart'] = display_add_to_cart
context['cart'] = self.get_cart()
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
+3 -2
View File
@@ -29,7 +29,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views.generic import FormView, TemplateView
from pretix.base.models import Quota, SubEvent
@@ -158,7 +158,8 @@ class WaitingView(EventViewMixin, FormView):
except ValueError:
raise Http404()
else:
self.subevent = None
messages.error(request, pgettext_lazy('subevent', "You need to select a date."))
return redirect(self.get_index_url())
if not (self.subevent or self.request.event).waiting_list_active:
messages.error(request, _("Waiting lists are disabled for this event."))