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:
Raphael Michel
2021-09-27 20:48:02 +02:00
committed by GitHub
parent a9a4cf6fca
commit 9f2ffc3276
15 changed files with 359 additions and 62 deletions

View File

@@ -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

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -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()