mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Improvements around the waiting list (#2219)
* Waiting list: Support for seated events, pre-fill customer email address * Allow people to remove themselves * Update src/pretix/base/settings.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/waitinglist.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/waitinglist.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/views/waitinglist.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/presale/views/waiting.py Co-authored-by: Richard Schreiber <schreiber@rami.io> * Resolve a review note * Review notes * Fix linter issues * Fix import Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -462,6 +462,16 @@ def base_placeholders(sender, **kwargs):
|
||||
lambda waiting_list_entry, event: (waiting_list_entry.subevent or event).get_date_from_display(),
|
||||
lambda event: (event if not event.has_subevents or not event.subevents.exists() else event.subevents.first()).get_date_from_display()
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url_remove', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
event, 'presale:event.waitinglist.remove'
|
||||
) + '?voucher=' + waiting_list_entry.voucher.code,
|
||||
lambda event: build_absolute_uri(
|
||||
event,
|
||||
'presale:event.waitinglist.remove',
|
||||
) + '?voucher=68CYU2H6ZTP3WLK5',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'url', ['waiting_list_entry', 'event'],
|
||||
lambda waiting_list_entry, event: build_absolute_uri(
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
#
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import F, Q, Sum
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
@@ -114,9 +115,12 @@ class WaitingListEntry(LoggedModel):
|
||||
return '%s waits for %s' % (str(self.email), str(self.item))
|
||||
|
||||
def clean(self):
|
||||
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
|
||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||
try:
|
||||
WaitingListEntry.clean_duplicate(self.email, self.item, self.variation, self.subevent, self.pk)
|
||||
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
|
||||
WaitingListEntry.clean_subevent(self.event, self.subevent)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Invalid input')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
update_fields = kwargs.get('update_fields', [])
|
||||
@@ -147,6 +151,34 @@ class WaitingListEntry(LoggedModel):
|
||||
)
|
||||
if availability[1] is None or availability[1] < 1:
|
||||
raise WaitingListException(_('This product is currently not available.'))
|
||||
|
||||
ev = self.subevent or self.event
|
||||
if ev.seat_category_mappings.filter(product=self.item).exists():
|
||||
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
|
||||
# to use in combination with seating plans. If your event has 50 seats and a quota of 50 and
|
||||
# default settings, everything is fine and the waiting list will work as usual. However, as soon
|
||||
# as those two numbers diverge, either due to misconfiguration or due to intentional features such
|
||||
# as our COVID-19 minimum distance feature, things get ugly. Theoretically, there could be
|
||||
# significant quota available but not a single seat! The waiting list would happily send out vouchers
|
||||
# which do not work at all. Generally, we consider this a "known bug" and not fixable with the current
|
||||
# design of the waiting list and seating features.
|
||||
# However, we've put in a simple safeguard that makes sure the waiting list on its own does not screw
|
||||
# everything up. Specifically, we will not send out vouchers if the number of available seats is less
|
||||
# than the number of valid vouchers *issued through the waiting list*. Things can still go wrong due to
|
||||
# manually created vouchers, manually blocked seats or the minimum distance feature, but this reduces
|
||||
# the possible damage a bit.
|
||||
num_free_seats_for_product = ev.free_seats().filter(product=self.item).count()
|
||||
num_valid_vouchers_for_product = self.event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=self.item_id,
|
||||
subevent_id=self.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
if not free_seats:
|
||||
raise WaitingListException(_('No seat with this product is currently available.'))
|
||||
|
||||
if self.voucher:
|
||||
raise WaitingListException(_('A voucher has already been sent to this person.'))
|
||||
if '@' not in self.email:
|
||||
|
||||
@@ -22,12 +22,14 @@
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models import Exists, F, OuterRef, Q, Sum
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, User, WaitingListEntry
|
||||
from pretix.base.models import (
|
||||
Event, SeatCategoryMapping, User, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.base.services.tasks import EventTask
|
||||
from pretix.base.signals import periodic_task
|
||||
@@ -43,6 +45,19 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
|
||||
quota_cache = {}
|
||||
gone = set()
|
||||
seats_available = {}
|
||||
|
||||
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'):
|
||||
# See comment in WaitingListEntry.send_voucher() for rationale
|
||||
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count()
|
||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=m.product_id,
|
||||
subevent_id=m.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
|
||||
|
||||
qs = WaitingListEntry.objects.filter(
|
||||
event=event, voucher__isnull=True
|
||||
@@ -70,6 +85,11 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
if seats_available[wle.item_id, wle.subevent_id] < 1:
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
continue
|
||||
|
||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.quotas.filter(subevent=wle.subevent))
|
||||
@@ -91,6 +111,9 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
||||
quota_cache[q.pk][0] if quota_cache[q.pk][0] > 1 else 0,
|
||||
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
||||
)
|
||||
|
||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
||||
seats_available[wle.item_id, wle.subevent_id] -= 1
|
||||
else:
|
||||
gone.add((wle.item, wle.variation, wle.subevent))
|
||||
|
||||
|
||||
@@ -1746,6 +1746,12 @@ Please note that this link is only valid within the next {hours} hours!
|
||||
We will reassign the ticket to the next person on the list if you do not
|
||||
redeem the voucher within that timeframe.
|
||||
|
||||
If you do NOT need a ticket any more, we kindly ask you to click the
|
||||
following link to let us know. This way, we can send the ticket as quickly
|
||||
as possible to the next person on the waiting list:
|
||||
|
||||
{url_remove}
|
||||
|
||||
Best regards,
|
||||
Your {event} team"""))
|
||||
},
|
||||
|
||||
@@ -256,9 +256,22 @@
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
The waiting list currently is not compatible with some advanced features of pretix such as
|
||||
seating plans, add-on products or product bundles.
|
||||
add-on products or product bundles.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
The waiting list determines availability mainly based on quotas. If you use a seating plan and your
|
||||
number of available seats is less than the available quota, you might run into situations where
|
||||
people are sent an email from the waiting list but still are unable to book a seat.
|
||||
{% endblocktrans %}
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
Specifically, this means the waiting list is not safe to use together with the minimum distance
|
||||
feature of our seating plan module.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</div>
|
||||
{% bootstrap_field sform.waiting_list_enabled layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_auto layout="control" %}
|
||||
{% bootstrap_field sform.waiting_list_hours layout="control" %}
|
||||
|
||||
@@ -50,7 +50,7 @@ from django.views import View
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import DeleteView
|
||||
|
||||
from pretix.base.models import Item, WaitingListEntry
|
||||
from pretix.base.models import Item, Quota, WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.base.services.waitinglist import assign_automatically
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
@@ -239,8 +239,24 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
||||
if wle.variation
|
||||
else wle.item.check_quotas(count_waitinglist=False, subevent=wle.subevent, _cache=quota_cache)
|
||||
)
|
||||
if wle.availability[0] == Quota.AVAILABILITY_OK and ev.seat_category_mappings.filter(product=wle.item).exists():
|
||||
# See comment in WaitingListEntry.send_voucher() for rationale
|
||||
num_free_seats_for_product = ev.free_seats().filter(product=wle.item).count()
|
||||
num_valid_vouchers_for_product = self.request.event.vouchers.filter(
|
||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||
block_quota=True,
|
||||
item_id=wle.item_id,
|
||||
subevent=wle.subevent_id,
|
||||
waitinglistentries__isnull=False
|
||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||
wle.availability = (
|
||||
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
|
||||
min(free_seats, wle.availability[1])
|
||||
)
|
||||
|
||||
itemvar_cache[(wle.item, wle.variation, wle.subevent)] = wle.availability
|
||||
if wle.availability[0] == 100:
|
||||
if wle.availability[0] == Quota.AVAILABILITY_OK:
|
||||
any_avail = True
|
||||
|
||||
ctx['any_avail'] = any_avail
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
@@ -28,7 +29,8 @@ from pretix.base.forms.questions import (
|
||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget, guess_country,
|
||||
)
|
||||
from pretix.base.i18n import get_babel_locale, language
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.base.models import Quota, WaitingListEntry
|
||||
from pretix.presale.views.event import get_grouped_items
|
||||
|
||||
|
||||
class WaitingListForm(forms.ModelForm):
|
||||
@@ -40,8 +42,35 @@ class WaitingListForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.channel = kwargs.pop('channel')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
choices = [
|
||||
('', '')
|
||||
]
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.event, self.instance.subevent, require_seat=None
|
||||
)
|
||||
for i in items:
|
||||
if not i.allow_waitinglist:
|
||||
continue
|
||||
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
if v.cached_availability[0] == Quota.AVAILABILITY_OK:
|
||||
continue
|
||||
choices.append((f'{i.pk}-{v.pk}', f'{i.name} – {v.value}'))
|
||||
|
||||
else:
|
||||
if i.cached_availability[0] == Quota.AVAILABILITY_OK:
|
||||
continue
|
||||
choices.append((f'{i.pk}', f'{i.name}'))
|
||||
|
||||
self.fields['itemvar'] = forms.ChoiceField(
|
||||
label=_('Product'),
|
||||
choices=choices,
|
||||
)
|
||||
|
||||
event = self.event
|
||||
|
||||
if event.settings.waiting_list_names_asked:
|
||||
@@ -73,3 +102,23 @@ class WaitingListForm(forms.ModelForm):
|
||||
)
|
||||
else:
|
||||
del self.fields['phone']
|
||||
|
||||
def clean(self):
|
||||
try:
|
||||
iv = self.data.get('itemvar', '')
|
||||
if '-' in iv:
|
||||
itemid, varid = iv.split('-')
|
||||
else:
|
||||
itemid, varid = iv, None
|
||||
|
||||
self.instance.item = self.instance.event.items.get(pk=itemid)
|
||||
if varid:
|
||||
self.instance.variation = self.instance.item.variations.get(pk=varid)
|
||||
else:
|
||||
self.instance.variation = None
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(_("Invalid product selected."))
|
||||
|
||||
data = super().clean()
|
||||
return data
|
||||
|
||||
@@ -211,6 +211,30 @@
|
||||
{% eventsignal event "pretix.presale.signals.render_seating_plan" request=request %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if waitinglist_seated %}
|
||||
<aside class="front-page" aria-labelledby="waiting-list">
|
||||
<h3 id="waiting-list">{% 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>
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
{% include "pretixpresale/event/fragment_product_list.html" %}
|
||||
{% if ev.presale_is_running and display_add_to_cart %}
|
||||
<section class="front-page">
|
||||
|
||||
@@ -6,13 +6,6 @@
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_email">{% trans "Product" %}</label>
|
||||
<div class="col-md-9">
|
||||
<input class="form-control" readonly="readonly"
|
||||
value="{{ item.name }}{% if variation %} – {{ variation.value }}{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
{% if subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_email">{% trans "Event" %}</label>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n eventurl bootstrap3 %}
|
||||
{% block title %}{% trans "Waiting list" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{% trans "Remove me from the waiting list" %}</h2>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You have been selected from our waiting list to buy a ticket. If you do not need the ticket any more, please be so kind and remove your ticket from the list
|
||||
so we can pass it on to the next person waiting as quickly as possible!
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<button type="submit" class="btn btn-danger btn-lg">
|
||||
{% trans "Yes, remove my ticket" context "waitinglist" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -68,6 +68,7 @@ frame_wrapped_urls = [
|
||||
re_path(r'^(?P<subevent>[0-9]+)/seatingframe/$', pretix.presale.views.event.SeatingPlanView.as_view(),
|
||||
name='event.seatingplan'),
|
||||
re_path(r'^(?P<subevent>[0-9]+)/$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
re_path(r'^waitinglist/remove$', pretix.presale.views.waiting.WaitingRemoveView.as_view(), name='event.waitinglist.remove'),
|
||||
re_path(r'^waitinglist', pretix.presale.views.waiting.WaitingView.as_view(), name='event.waitinglist'),
|
||||
re_path(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||
]
|
||||
|
||||
@@ -180,7 +180,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
).order_by('category__position', 'category_id', 'position', 'name')
|
||||
if require_seat:
|
||||
items = items.filter(requires_seat__gt=0)
|
||||
else:
|
||||
elif require_seat is not None:
|
||||
items = items.filter(requires_seat=0)
|
||||
|
||||
if filter_items:
|
||||
@@ -427,14 +427,38 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
self.request.event.cache.set('vouchers_exist', vouchers_exist)
|
||||
context['show_vouchers'] = context['vouchers_exist'] = vouchers_exist
|
||||
|
||||
context['ev'] = self.subevent or self.request.event
|
||||
context['subevent'] = self.subevent
|
||||
|
||||
context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running
|
||||
|
||||
if not self.request.event.has_subevents or self.subevent:
|
||||
# Fetch all items
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.request.event, self.subevent,
|
||||
filter_items=self.request.GET.getlist('item'),
|
||||
filter_categories=self.request.GET.getlist('category'),
|
||||
require_seat=None,
|
||||
channel=self.request.sales_channel.identifier
|
||||
)
|
||||
|
||||
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:
|
||||
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') for item in items if not item.has_variations
|
||||
@@ -450,11 +474,8 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
context['items_by_category'] = item_group_by_category(items)
|
||||
context['display_add_to_cart'] = display_add_to_cart
|
||||
|
||||
context['ev'] = self.subevent or self.request.event
|
||||
context['subevent'] = self.subevent
|
||||
context['cart'] = self.get_cart()
|
||||
context['has_addon_choices'] = any(cp.has_addon_choices for cp in get_cart(self.request))
|
||||
context['allow_waitinglist'] = self.request.event.settings.waiting_list_enabled and context['ev'].presale_is_running
|
||||
|
||||
if self.subevent:
|
||||
context['frontpage_text'] = str(self.subevent.frontpage_text)
|
||||
|
||||
@@ -19,21 +19,24 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
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 _, pgettext_lazy
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models import Quota, SubEvent
|
||||
from pretix.base.templatetags.urlreplace import url_replace
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.views import EventViewMixin
|
||||
|
||||
from ...base.i18n import get_language_without_region
|
||||
from ...base.models import Item, ItemVariation, WaitingListEntry
|
||||
from ...base.models import Voucher, WaitingListEntry
|
||||
from ..forms.waitinglist import WaitingListForm
|
||||
from . import allow_frame_if_namespaced
|
||||
|
||||
@@ -47,17 +50,23 @@ class WaitingView(EventViewMixin, FormView):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs['instance'] = WaitingListEntry(
|
||||
item=self.item_and_variation[0], variation=self.item_and_variation[1],
|
||||
event=self.request.event, locale=get_language_without_region(),
|
||||
subevent=self.subevent
|
||||
)
|
||||
kwargs['channel'] = self.request.sales_channel.identifier
|
||||
kwargs.setdefault('initial', {})
|
||||
if 'var' in self.request.GET:
|
||||
kwargs['initial']['itemvar'] = f'{self.request.GET.get("item")}-{self.request.GET.get("var")}'
|
||||
else:
|
||||
kwargs['initial']['itemvar'] = self.request.GET.get("item")
|
||||
if getattr(self.request, 'customer', None):
|
||||
kwargs['initial']['email'] = self.request.customer.email
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['event'] = self.request.event
|
||||
ctx['subevent'] = self.subevent
|
||||
ctx['item'], ctx['variation'] = self.item_and_variation
|
||||
return ctx
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
@@ -77,20 +86,6 @@ class WaitingView(EventViewMixin, FormView):
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def item_and_variation(self):
|
||||
try:
|
||||
item = self.request.event.items.get(pk=self.request.GET.get('item'))
|
||||
if 'var' in self.request.GET:
|
||||
var = item.variations.get(pk=self.request.GET['var'])
|
||||
elif item.has_variations:
|
||||
return None
|
||||
else:
|
||||
var = None
|
||||
return item, var
|
||||
except (Item.DoesNotExist, ItemVariation.DoesNotExist, ValueError):
|
||||
return None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
|
||||
@@ -106,14 +101,6 @@ class WaitingView(EventViewMixin, FormView):
|
||||
messages.error(request, _("The presale for this event has not yet started."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
if not self.item_and_variation:
|
||||
messages.error(request, _("We could not identify the product you selected."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
if not self.item_and_variation[0].allow_waitinglist:
|
||||
messages.error(request, _("The waiting list is disabled for this product."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
self.subevent = None
|
||||
if request.event.has_subevents:
|
||||
if 'subevent' in request.GET:
|
||||
@@ -127,11 +114,11 @@ class WaitingView(EventViewMixin, FormView):
|
||||
|
||||
def form_valid(self, form):
|
||||
availability = (
|
||||
self.item_and_variation[1].check_quotas(count_waitinglist=True, subevent=self.subevent)
|
||||
if self.item_and_variation[1]
|
||||
else self.item_and_variation[0].check_quotas(count_waitinglist=True, subevent=self.subevent)
|
||||
form.instance.variation.check_quotas(count_waitinglist=True, subevent=self.subevent)
|
||||
if form.instance.variation
|
||||
else form.instance.item.check_quotas(count_waitinglist=True, subevent=self.subevent)
|
||||
)
|
||||
if availability[0] == 100:
|
||||
if availability[0] == Quota.AVAILABILITY_OK:
|
||||
messages.error(self.request, _("You cannot add yourself to the waiting list as this product is currently "
|
||||
"available."))
|
||||
return redirect(self.get_index_url())
|
||||
@@ -143,3 +130,43 @@ class WaitingView(EventViewMixin, FormView):
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_index_url()
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class WaitingRemoveView(EventViewMixin, TemplateView):
|
||||
template_name = 'pretixpresale/event/waitinglist_remove.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['event'] = self.request.event
|
||||
ctx['voucher'] = self.voucher
|
||||
return ctx
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
|
||||
try:
|
||||
self.voucher = self.request.event.vouchers.get(
|
||||
code=request.GET.get("voucher", ""),
|
||||
waitinglistentries__isnull=False,
|
||||
)
|
||||
except Voucher.DoesNotExist:
|
||||
messages.error(request, _("We could not find you on our waiting list."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
if not self.voucher.is_active():
|
||||
messages.error(request, _("Your waiting list spot is no longer valid or already used. There's nothing more to do here."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.voucher.valid_until = now() - timedelta(seconds=1)
|
||||
self.voucher.save(update_fields=['valid_until'])
|
||||
self.voucher.log_action('pretix.voucher.expired.waitinglist')
|
||||
messages.success(request, _("Thank you very much! We will assign your spot on the waiting list to someone else."))
|
||||
return redirect(self.get_index_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_index_url()
|
||||
|
||||
Reference in New Issue
Block a user