diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 5c75a4185f..109476ea5b 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -72,7 +72,7 @@ Dashboards """""""""" .. automodule:: pretix.control.signals - :members: event_dashboard_widgets, user_dashboard_widgets + :members: event_dashboard_widgets, user_dashboard_widgets, event_dashboard_top Ticket designs """""""""""""" diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 22cbdbde07..1324609c0d 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -6,7 +6,7 @@ from typing import List, Optional from celery.exceptions import MaxRetriesExceededError from django.core.exceptions import ValidationError from django.db import DatabaseError, transaction -from django.db.models import Count, Exists, OuterRef, Q +from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value from django.dispatch import receiver from django.utils.timezone import make_aware, now from django.utils.translation import gettext as _, pgettext_lazy @@ -147,6 +147,8 @@ class CartManager: ).select_related('item', 'subevent') def _is_seated(self, item, subevent): + if not self.event.settings.seating_choice: + return False if (item, subevent) not in self._seated_cache: self._seated_cache[item, subevent] = item.seat_category_mappings.filter(subevent=subevent).exists() return self._seated_cache[item, subevent] @@ -328,15 +330,18 @@ class CartManager: raise e def extend_expired_positions(self): + requires_seat = Exists( + SeatCategoryMapping.objects.filter( + Q(product=OuterRef('item')) + & (Q(subevent=OuterRef('subevent')) if self.event.has_subevents else Q(subevent__isnull=True)) + ) + ) + if not self.event.settings.seating_choice: + requires_seat = Value(0, output_field=IntegerField()) expired = self.positions.filter(expires__lte=self.now_dt).select_related( 'item', 'variation', 'voucher', 'addon_to', 'addon_to__item' ).annotate( - requires_seat=Exists( - SeatCategoryMapping.objects.filter( - Q(product=OuterRef('item')) - & (Q(subevent=OuterRef('subevent')) if self.event.has_subevents else Q(subevent__isnull=True)) - ) - ) + requires_seat=requires_seat ).prefetch_related( 'item__quotas', 'variation__quotas', @@ -349,7 +354,7 @@ class CartManager: if cp.pk in removed_positions or (cp.addon_to_id and cp.addon_to_id in removed_positions): continue - cp.item.requires_seat = cp.requires_seat + cp.item.requires_seat = self.event.settings.seating_choice and cp.requires_seat if cp.is_bundled: bundle = cp.addon_to.item.bundles.filter(bundled_item=cp.item, bundled_variation=cp.variation).first() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 7763d914ef..ad5ef1fade 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -9,7 +9,9 @@ from celery.exceptions import MaxRetriesExceededError from django.conf import settings from django.core.cache import cache from django.db import transaction -from django.db.models import Exists, F, Max, Min, OuterRef, Q, Sum +from django.db.models import ( + Exists, F, IntegerField, Max, Min, OuterRef, Q, Sum, Value, +) from django.db.models.functions import Coalesce, Greatest from django.db.transaction import get_connection from django.dispatch import receiver @@ -890,13 +892,16 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str], except InvoiceAddress.DoesNotExist: pass - positions = CartPosition.objects.annotate( - requires_seat=Exists( - SeatCategoryMapping.objects.filter( - Q(product=OuterRef('item')) - & (Q(subevent=OuterRef('subevent')) if event.has_subevents else Q(subevent__isnull=True)) - ) + requires_seat = Exists( + SeatCategoryMapping.objects.filter( + Q(product=OuterRef('item')) + & (Q(subevent=OuterRef('subevent')) if event.has_subevents else Q(subevent__isnull=True)) ) + ) + if not event.settings.seating_choice: + requires_seat = Value(0, output_field=IntegerField()) + positions = CartPosition.objects.annotate( + requires_seat=requires_seat ).filter( id__in=position_ids, event=event ) @@ -1377,7 +1382,7 @@ class OrderChangeManager: raise OrderError(self.error_messages['subevent_required']) seated = item.seat_category_mappings.filter(subevent=subevent).exists() - if seated and not seat: + if seated and not seat and self.event.settings.seating_choice: raise OrderError(self.error_messages['seat_required']) elif not seated and seat: raise OrderError(self.error_messages['seat_forbidden']) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index ba55e62deb..6276af88b5 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1744,6 +1744,18 @@ Your {event} team""")) 'default': settings.ENTROPY['giftcard_secret'], 'type': int }, + 'seating_choice': { + 'default': 'True', + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Customers can choose their own seats"), + help_text=_("If disabled, you will need to manually assign seats in the backend. Note that this can mean " + "people will not know their seat after their purchase and it might not be written on their " + "ticket."), + ), + 'type': bool, + }, 'seating_minimal_distance': { 'default': '0', 'type': float diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 3e8281bba7..609c83b7a7 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -399,7 +399,10 @@ class OrderPositionChangeForm(forms.Form): self.fields['tax_rule'].queryset = instance.event.tax_rules.all() self.fields['tax_rule'].label_from_instance = self.taxrule_label_from_instance - if not instance.seat: + if not instance.seat and not ( + not instance.event.settings.seating_choice and + instance.item.seat_category_mappings.filter(subevent=instance.subevent).exists() + ): del self.fields['seat'] choices = [ diff --git a/src/pretix/control/signals.py b/src/pretix/control/signals.py index aa2f90065f..1c16d4ec85 100644 --- a/src/pretix/control/signals.py +++ b/src/pretix/control/signals.py @@ -92,6 +92,17 @@ This is no ``EventPluginSignal``, so you do not get the event in the ``sender`` and you may get the signal regardless of whether your plugin is active. """ +event_dashboard_top = EventPluginSignal( + providing_args=['request'] +) +""" +This signal is sent out to include custom HTML in the top part of the the event dashboard. +Receivers should return HTML. + +As with all plugin signals, the ``sender`` keyword argument will contain the event. +An additional keyword argument ``subevent`` *can* contain a sub-event. +""" + event_dashboard_widgets = EventPluginSignal( providing_args=[] ) diff --git a/src/pretix/control/templates/pretixcontrol/event/index.html b/src/pretix/control/templates/pretixcontrol/event/index.html index 737c5103bb..45ef51318b 100644 --- a/src/pretix/control/templates/pretixcontrol/event/index.html +++ b/src/pretix/control/templates/pretixcontrol/event/index.html @@ -3,6 +3,7 @@ {% load eventurl %} {% load bootstrap3 %} {% load static %} +{% load eventsignal %} {% block title %}{{ request.event.name }}{% endblock %} {% block content %}