mirror of
https://github.com/pretix/pretix.git
synced 2026-09-22 17:44:41 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f65466875 | ||
|
|
6a181506e9 | ||
|
|
84df699503 |
@@ -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.'
|
||||
)
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
|
||||
@@ -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.'
|
||||
)
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 '',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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."))
|
||||
|
||||
Reference in New Issue
Block a user