Add support for reserved seating (#1228)

* Initial work on seating

* Add seat guids

* Add product_list_top

* CartAdd: Ignore item when a seat is passed

* Cart display

* product_list_top → render_seating_plan

* Render seating plan in voucher redemption

* Fix failing tests

* Add tests for extending cart positions with seats

* Add subevent_forms to docs

* Update schema, migrations

* Dealing with expired orders

* steps to order change

* Change order positions

* Allow to add seats

* tests for ocm

* Fix things after rebase

* Seating plans API

* Add more tests for cart behaviour

* Widget support

* Adjust widget tests

* Re-enable CSP

* Update schema

* Api: position.seat

* Add guid to word list

* API: (sub)event.seating_plan

* Vali fixes

* Fix api

* Fix reference in test

* Fix test for real
This commit is contained in:
Raphael Michel
2019-06-25 11:00:03 +02:00
committed by GitHub
parent f79d17cb6a
commit 93089d87e3
77 changed files with 3689 additions and 164 deletions

View File

@@ -59,7 +59,7 @@ class CartMixin:
cartpos = queryset.order_by(
'item__category__position', 'item__category_id', 'item__position', 'item__name', 'variation__value'
).select_related(
'item', 'variation', 'addon_to', 'subevent', 'subevent__event', 'subevent__event__organizer'
'item', 'variation', 'addon_to', 'subevent', 'subevent__event', 'subevent__event__organizer', 'seat'
).prefetch_related(
*prefetch
)
@@ -103,13 +103,13 @@ class CartMixin:
)
addon_penalty = 1 if pos.addon_to else 0
if downloads or pos.pk in has_addons or pos.addon_to:
return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0)
return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0), pos.seat_id
if answers and (has_attendee_data or pos.item.questions.all()):
return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0)
return i, addon_penalty, pos.pk, 0, 0, 0, 0, (pos.subevent_id or 0), pos.seat_id
return (
0, addon_penalty, 0, pos.item_id, pos.variation_id, pos.price, (pos.voucher_id or 0),
(pos.subevent_id or 0)
(pos.subevent_id or 0), pos.seat_id
)
positions = []

View File

@@ -90,10 +90,26 @@ class CartActionMixin:
if value.strip() == '' or '_' not in key:
return
if not key.startswith('item_') and not key.startswith('variation_'):
if not key.startswith('item_') and not key.startswith('variation_') and not key.startswith('seat_'):
return
parts = key.split("_")
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
if key.startswith('seat_'):
try:
return {
'item': int(parts[1]),
'variation': int(parts[2]) if len(parts) > 2 else None,
'count': 1,
'seat': value,
'price': price,
'voucher': voucher,
'subevent': self.request.POST.get("subevent")
}
except ValueError:
raise CartError(_('Please enter numbers only.'))
try:
amount = int(value)
except ValueError:
@@ -103,7 +119,6 @@ class CartActionMixin:
elif amount == 0:
return
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
if key.startswith('item_'):
try:
return {
@@ -131,8 +146,7 @@ class CartActionMixin:
def _items_from_post_data(self):
"""
Parses the POST data and returns a list of tuples in the
form (item id, variation id or None, number)
Parses the POST data and returns a list of dictionaries
"""
# Compatibility patch that makes the frontend code a lot easier

View File

@@ -7,7 +7,7 @@ from importlib import import_module
import pytz
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Prefetch
from django.db.models import Count, Exists, OuterRef, Prefetch
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
@@ -17,7 +17,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from pretix.base.models import ItemVariation, Quota
from pretix.base.models import ItemVariation, Quota, SeatCategoryMapping
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemBundle
from pretix.multidomain.urlreverse import eventreverse
@@ -49,7 +49,7 @@ def item_group_by_category(items):
)
def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0):
items = event.items.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).select_related(
'category', 'tax_rule', # for re-grouping
).prefetch_related(
@@ -81,10 +81,20 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
).distinct()),
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations')
has_variations=Count('variations'),
requires_seat=Exists(
SeatCategoryMapping.objects.filter(
product_id=OuterRef('pk'),
subevent=subevent
)
)
).filter(
quotac__gt=0
quotac__gt=0,
).order_by('category__position', 'category_id', 'position', 'name')
if require_seat:
items = items.filter(requires_seat__gt=0)
else:
items = items.filter(requires_seat=0)
display_add_to_cart = False
external_quota_cache = event.cache.get('item_quota_cache')
quota_cache = external_quota_cache or {}
@@ -342,6 +352,52 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
return context
@method_decorator(allow_frame_if_namespaced, 'dispatch')
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
class SeatingPlanView(EventViewMixin, TemplateView):
template_name = "pretixpresale/event/seatingplan.html"
def get(self, request, *args, **kwargs):
from pretix.presale.views.cart import get_or_create_cart_id
self.subevent = None
if request.GET.get('src', '') == 'widget' and 'take_cart_id' in request.GET:
# User has clicked "Open in a new tab" link in widget
get_or_create_cart_id(request)
return redirect(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs))
elif request.GET.get('iframe', '') == '1' and 'take_cart_id' in request.GET:
# Widget just opened, a cart already exists. Let's to a stupid redirect to check if cookies are disabled
get_or_create_cart_id(request)
return redirect(eventreverse(request.event, 'presale:event.seatingplan', kwargs=kwargs) + '?require_cookie=true&cart_id={}'.format(
request.GET.get('take_cart_id')
))
elif request.GET.get('iframe', '') == '1' and len(self.request.GET.get('widget_data', '{}')) > 3:
# We've been passed data from a widget, we need to create a cart session to store it.
get_or_create_cart_id(request)
if request.event.has_subevents:
if 'subevent' in kwargs:
self.subevent = request.event.subevents.using(settings.DATABASE_REPLICA).filter(pk=kwargs['subevent'], active=True).first()
if not self.subevent or not self.subevent.seating_plan:
raise Http404()
return super().get(request, *args, **kwargs)
else:
raise Http404()
else:
if 'subevent' in kwargs or not request.event.seating_plan:
raise Http404()
else:
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cart_redirect'] = eventreverse(self.request.event, 'presale:event.checkout.start',
kwargs={'cart_namespace': kwargs.get('cart_namespace') or ''})
if context['cart_redirect'].startswith('https:'):
context['cart_redirect'] = '/' + context['cart_redirect'].split('/', 3)[3]
return context
class EventIcalDownload(EventViewMixin, View):
def get(self, request, *args, **kwargs):
if not self.request.event:

View File

@@ -515,6 +515,8 @@ class WidgetAPIProductList(EventListMixin, View):
data['display_add_to_cart'] = False
data['itemnum'] = 0
data['has_seating_plan'] = ev.seating_plan is not None
vouchers_exist = self.request.event.get_cache().get('vouchers_exist')
if vouchers_exist is None:
vouchers_exist = self.request.event.vouchers.exists()