mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df6247fb5 | ||
|
|
74ada1ca08 |
+1
-1
@@ -80,7 +80,7 @@ dependencies = [
|
||||
"Pillow==12.3.*",
|
||||
"pretix-plugin-build",
|
||||
"protobuf==7.36.*",
|
||||
"psycopg2-binary",
|
||||
"psycopg[binary,pool]",
|
||||
"pycountry",
|
||||
"pycparser==3.0",
|
||||
"pycryptodome==3.23.*",
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -889,13 +889,14 @@ class Item(LoggedModel):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
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.
|
||||
@@ -905,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
|
||||
@@ -1297,13 +1302,14 @@ class ItemVariation(models.Model):
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
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.
|
||||
@@ -1315,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
|
||||
@@ -2195,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(
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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."))
|
||||
|
||||
+19
-1
@@ -128,6 +128,23 @@ elif 'mysql' in db_backend:
|
||||
DATABASE_ADVISORY_LOCK_INDEX = config.getint('database', 'advisory_lock_index', fallback=0)
|
||||
|
||||
db_options = {}
|
||||
if 'postgresql' in db_backend:
|
||||
db_options["pool"] = {
|
||||
# https://docs.djangoproject.com/en/6.1/ref/databases/#connection-pool
|
||||
# "Django maintains a separate pool for each database alias in each process"
|
||||
# We run both celery and gunicorn in multi-process mode, so we should get away with one connection per process
|
||||
# per alias, so let's try to use a really small pool size.
|
||||
"min_size": 1,
|
||||
"max_size": 2,
|
||||
# We set low lifetime values since we want to quickly get rid of all old connections if something changes
|
||||
# on the database end, e.g. a cluster failover.
|
||||
"max_idle": 120,
|
||||
"max_lifetime": 120,
|
||||
# A check callable is set by Django automatically since we have CONN_HEALTH_CHECKS enabled.
|
||||
# In the future, we could provide different callables for default and replica databases if we want to test
|
||||
# actual writability for the default database, but it might not be necessary if a failover triggers a reconnect
|
||||
# anyways.
|
||||
}
|
||||
|
||||
postgresql_sslmode = config.get('database', 'sslmode', fallback='disable')
|
||||
USE_DATABASE_TLS = postgresql_sslmode != 'disable'
|
||||
@@ -162,7 +179,7 @@ DATABASES = {
|
||||
'PASSWORD': config.get('database', 'password', fallback=''),
|
||||
'HOST': config.get('database', 'host', fallback=''),
|
||||
'PORT': config.get('database', 'port', fallback=''),
|
||||
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
|
||||
'CONN_MAX_AGE': 0,
|
||||
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
|
||||
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
|
||||
'OPTIONS': db_options,
|
||||
@@ -382,6 +399,7 @@ if HAS_CELERY:
|
||||
if HAS_CELERY_BACKEND_TRANSPORT_OPTS:
|
||||
CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = loads(config.get('celery', 'backend_transport_options'))
|
||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
||||
CELERY_DB_REUSE_MAX = sys.maxsize # Actual reuse will be governed by psycopg3 pool features
|
||||
|
||||
if CELERY_BROKER_URL.startswith("amqp://"):
|
||||
# https://docs.celeryq.dev/en/latest/userguide/routing.html#routing-options-rabbitmq-priorities
|
||||
|
||||
@@ -81,6 +81,8 @@ CACHES = {
|
||||
DATABASE_REPLICA = 'default'
|
||||
DATABASES['default']['CONN_MAX_AGE'] = 0
|
||||
DATABASES.pop('replica', None)
|
||||
if 'pool' in DATABASES['default']['OPTIONS']:
|
||||
DATABASES['default']['OPTIONS']['pool']['max_size'] = 5
|
||||
|
||||
MIDDLEWARE.insert(0, 'pretix.testutils.middleware.DebugFlagMiddleware')
|
||||
|
||||
|
||||
@@ -293,6 +293,14 @@ def test_cartpos_cart_id_optional(token_client, organizer, event, item, quota, q
|
||||
def test_cartpos_create_subevent_validation(token_client, organizer, event, item, subevent, subevent2, quota, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'subevent': ['You need to set a subevent.']}
|
||||
|
||||
res['subevent'] = subevent2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
|
||||
@@ -2323,6 +2323,20 @@ def test_quota_create_with_subevent(token_client, organizer, event, event3, item
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [item.pk],
|
||||
"variations": [variations[0].pk],
|
||||
"subevent": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["Subevent cannot be null for event series."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
|
||||
@@ -1859,6 +1859,23 @@ def test_position_add_subevent(token_client, organizer, event, order, quota, ite
|
||||
assert op.subevent == subevent
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_subevent_required(token_client, organizer, event, order, quota, item, subevent):
|
||||
with scopes_disabled():
|
||||
assert order.positions.count() == 1
|
||||
payload = {
|
||||
'order': order.code,
|
||||
'item': item.pk,
|
||||
}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orderpositions/'.format(
|
||||
organizer.slug, event.slug,
|
||||
), format='json', data=payload
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert 'subevent' in str(resp.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_position_add_quota_empty(token_client, organizer, event, order, quota, item):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -1164,6 +1164,14 @@ def test_order_create_subevent_validation(token_client, organizer, event, item,
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'positions': [{'subevent': ['You need to set a subevent.']}]}
|
||||
|
||||
res['positions'][0]['subevent'] = subevent2.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
|
||||
@@ -638,10 +638,8 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||||
se2 = self.event.subevents.create(date_from=now(), name="SE 2")
|
||||
q0 = self.event.quotas.create(name="Q0", subevent=None, size=50)
|
||||
q1 = self.event.quotas.create(name="Q1", subevent=se1, size=50)
|
||||
q2 = self.event.quotas.create(name="Q2", subevent=se2, size=50)
|
||||
q0.items.add(self.item1)
|
||||
q1.items.add(self.item1)
|
||||
q2.items.add(self.item1)
|
||||
|
||||
@@ -661,17 +659,11 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
|
||||
OrderPosition.objects.create(order=order, item=self.item1, subevent=se2, price=2)
|
||||
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
|
||||
sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
|
||||
expires=now() + timedelta(days=3),
|
||||
total=2)
|
||||
OrderPosition.objects.create(order=order, item=self.item1, subevent=None, price=2)
|
||||
|
||||
Voucher.objects.create(item=self.item1, event=self.event, valid_until=now() + timedelta(days=5),
|
||||
block_quota=True, max_usages=6, subevent=se1)
|
||||
Voucher.objects.create(item=self.item1, event=self.event, valid_until=now() + timedelta(days=5),
|
||||
block_quota=True, max_usages=4, subevent=se2)
|
||||
# TODO how to deal with subevent=None?
|
||||
|
||||
for i in range(8):
|
||||
CartPosition.objects.create(event=self.event, item=self.item1, price=2, subevent=se1,
|
||||
@@ -681,10 +673,6 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
CartPosition.objects.create(event=self.event, item=self.item1, price=2, subevent=se2,
|
||||
expires=now() + timedelta(days=3))
|
||||
|
||||
for i in range(2):
|
||||
CartPosition.objects.create(event=self.event, item=self.item1, price=2, subevent=None,
|
||||
expires=now() + timedelta(days=3))
|
||||
|
||||
for i in range(16):
|
||||
WaitingListEntry.objects.create(
|
||||
event=self.event, item=self.item1, email='foo@bar.com', subevent=se1
|
||||
@@ -695,15 +683,11 @@ class QuotaTestCase(BaseQuotaTestCase):
|
||||
event=self.event, item=self.item1, email='foo@bar.com', subevent=se2
|
||||
)
|
||||
|
||||
for i in range(6):
|
||||
WaitingListEntry.objects.create(
|
||||
event=self.event, item=self.item1, email='foo@bar.com', subevent=None
|
||||
)
|
||||
with self.assertRaises(TypeError):
|
||||
self.item1.check_quotas()
|
||||
|
||||
self.assertEqual(self.item1.check_quotas(subevent=se1), (Quota.AVAILABILITY_OK, 50 - 5 - 6 - 8 - 16))
|
||||
self.assertEqual(self.item1.check_quotas(subevent=se2), (Quota.AVAILABILITY_OK, 50 - 2 - 4 - 5 - 13))
|
||||
self.assertEqual(self.item1.check_quotas(subevent=None), (Quota.AVAILABILITY_OK, 50 - 1 - 2 - 6))
|
||||
self.assertEqual(q0.availability(), (Quota.AVAILABILITY_OK, 50 - 1 - 2 - 6))
|
||||
self.assertEqual(q1.availability(), (Quota.AVAILABILITY_OK, 50 - 5 - 6 - 8 - 16))
|
||||
self.assertEqual(q2.availability(), (Quota.AVAILABILITY_OK, 50 - 2 - 4 - 5 - 13))
|
||||
self.event.has_subevents = False
|
||||
|
||||
@@ -2466,6 +2466,13 @@ class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.add_position(self.shirt, None, Decimal('13.00'), None)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_item_subevent_required(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.add_position(self.ticket, None, None, None)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_item_subevent_price(self):
|
||||
self.event.has_subevents = True
|
||||
|
||||
@@ -323,8 +323,6 @@ class CartTest(CartTestMixin, TestCase):
|
||||
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
|
||||
self.quota_tickets.subevent = se
|
||||
self.quota_tickets.save()
|
||||
self.quota_all.subevent = se
|
||||
self.quota_all.save()
|
||||
q = se.quotas.create(name="foo", size=None, event=self.event)
|
||||
q.items.add(self.ticket)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
|
||||
@@ -1274,8 +1274,6 @@ class WaitingListTest(EventTestMixin, SoupTest):
|
||||
self.event.save()
|
||||
with scopes_disabled():
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now(), active=False)
|
||||
self.q.subevent = se1
|
||||
self.q.save()
|
||||
response = self.client.get(
|
||||
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user