From 748054de56b731483c696d8dd91b34971bb12116 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 4 Jun 2026 11:19:04 +0200 Subject: [PATCH] Add read-only details page for subevents (#6151) * Add read-only details page for subevents * Document signal * Review notes * Fix incorrect subquery --- doc/development/api/general.rst | 2 +- src/pretix/control/signals.py | 10 + .../pretixcontrol/subevents/detail.html | 554 +++++++++--------- .../pretixcontrol/subevents/edit.html | 296 ++++++++++ .../pretixcontrol/subevents/index.html | 6 +- src/pretix/control/urls.py | 3 +- src/pretix/control/views/subevents.py | 89 ++- src/tests/control/test_permissions.py | 6 +- src/tests/control/test_subevents.py | 4 +- 9 files changed, 684 insertions(+), 286 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/subevents/edit.html diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 18f446a5a..37d6ed224 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -65,7 +65,7 @@ Backend .. automodule:: pretix.control.signals :members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info, order_approve_info, event_settings_widget, oauth_application_registered, - order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms + order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms, subevent_detail_html .. automodule:: pretix.base.signals :no-index: diff --git a/src/pretix/control/signals.py b/src/pretix/control/signals.py index 5ce67198f..2a20f685e 100644 --- a/src/pretix/control/signals.py +++ b/src/pretix/control/signals.py @@ -213,6 +213,16 @@ quota as argument in the ``quota`` keyword argument. As with all event plugin signals, the ``sender`` keyword argument will contain the event. """ +subevent_detail_html = EventPluginSignal() +""" +Arguments: 'subevent' + +This signal allows you to append HTML to a SubEvent's detail view. You receive the +subevent as argument in the ``subevent`` keyword argument. + +As with all event plugin signals, the ``sender`` keyword argument will contain the event. +""" + organizer_edit_tabs = DeprecatedSignal() """ Arguments: 'organizer', 'request' diff --git a/src/pretix/control/templates/pretixcontrol/subevents/detail.html b/src/pretix/control/templates/pretixcontrol/subevents/detail.html index 5bce7afbb..a346730cd 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/detail.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/detail.html @@ -4,290 +4,306 @@ {% load formset_tags %} {% load eventsignal %} {% load static %} -{% block title %}{% trans "Date" context "subevent" %}{% endblock %} +{% load money %} +{% load icon %} +{% block title %}{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }} +{% endblocktrans %}{% endblock %} {% block content %} - {% if not subevent.pk %} -

{% trans "Create date" context "subevent" %}

- {% else %} -

{% trans "Date" context "subevent" %}

- {% endif %} -
- {% csrf_token %} - {% bootstrap_form_errors form %} - {% for f in itemvar_forms %} - {% bootstrap_form_errors f %} - {% endfor %} -
-
-
- {% trans "General information" %} - {% bootstrap_field form.name layout="control" %} - {% bootstrap_field form.active layout="control" %} - {% bootstrap_field form.date_from layout="control" %} - {% bootstrap_field form.date_to layout="control" %} - {% include "pretixcontrol/event/fragment_geodata.html" %} - {% bootstrap_field form.date_admission layout="control" %} - {% bootstrap_field form.frontpage_text layout="control" %} - {% bootstrap_field form.is_public layout="control" %} - {% bootstrap_field form.comment layout="control" %} - {% if meta_forms %} - +

+ {% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}{% endblocktrans %} + {% if 'event.subevents:write' in request.eventpermset %} + + + {% trans "Edit" %} + + {% endif %} +

+
+
+
+ {% trans "General information" %} +
+
{% trans "Name" %}
+
{{ subevent.name }}
+
{% trans "ID" %}
+
#{{ subevent.pk }}
+
{% trans "Status" %}
+
+ {% if not subevent.active %} + {% trans "Disabled" %} + {% elif subevent.presale_has_ended %} + {% trans "Presale over" %} + {% elif not subevent.presale_is_running %} + {% trans "Presale not started" %} + {% else %} + {% trans "On sale" %} + {% endif %} +
+
{% trans "Event start time" %}
+
{{ subevent.date_from|date:"SHORT_DATETIME_FORMAT" }}
+
{% trans "Event end time" %}
+
{{ subevent.date_to|date:"SHORT_DATETIME_FORMAT" }}
+ {% if subevent.date_admission %} +
{% trans "Admission time" %}
+
{{ subevent.date_admission|date:"SHORT_DATETIME_FORMAT" }}
{% endif %} -
-
- {% trans "Timeline" %} - {% bootstrap_field form.presale_start layout="control" %} - {% bootstrap_field form.presale_end layout="control" %} -
-
- {% trans "Quotas" %} -
- {{ formset.management_form }} - {% bootstrap_formset_errors formset %} -
- {% for form in formset %} -
-
- {{ form.id }} - {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} -
-
-

-
-
- {% bootstrap_field form.name layout='inline' form_group_class="" %} -
-
- -
-
-

-
-
- {% bootstrap_form_errors form %} - {% bootstrap_field form.size layout="control" %} - {% bootstrap_field form.itemvars layout="control" %} - {% bootstrap_field form.release_after_exit layout="control" %} - {% bootstrap_field form.ignore_for_event_availability layout="control" %} -
-
- {% endfor %} -
- -

- -

-
-
- {% trans "Product settings" %} -

- {% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %} -

- {% for f in itemvar_forms %} -
- {% bootstrap_form_errors f %} -
- -
-
- {% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %} -
-
-
- {% bootstrap_field f.disabled layout="inline" form_group_class="" %} -
-
-
-
- - {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}
- {% bootstrap_field f.available_from form_group_class="foo" layout="inline" %} -
-
- - {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}
- {% bootstrap_field f.available_until form_group_class="" layout="inline" %} -
-
-
+ {% if subevent.presale_start %} +
{% trans "Start of presale" %}
+
{{ subevent.presale_start|date:"SHORT_DATETIME_FORMAT" }}
+ {% endif %} + {% if subevent.presale_end %} +
{% trans "End of presale" %}
+
{{ subevent.presale_end|date:"SHORT_DATETIME_FORMAT" }}
+ {% endif %} + {% if subevent.location %} +
{% trans "Location" %}
+
{{ subevent.location|linebreaksbr }}
+ {% endif %} +
{% trans "Show in lists" %}
+
{{ subevent.is_public|yesno }}
+ {% for k, v in subevent.meta_data.items %} +
{{ k }}
+
{{ v }}
{% endfor %} -
+ {% if subevent.comment %} +
{% trans "Internal comment" %}
+
{{ subevent.comment|linebreaksbr }}
+ {% endif %} + +
+
+ {% trans "Quotas" %} +
+ + + + + + + + + + + + {% for q in quotas %} + + + + + + + + {% endfor %} + +
{% trans "Quota name" %}{% trans "Products" %}{% trans "Total capacity" %}{% trans "Capacity left" %}
+ {{ q.name }} + {% if q.ignore_for_event_availability %} + + {% endif %} + +
    + {% for item in q.cached_items %} + {% if not item.has_variations %} +
  • + {{ item }} +
  • + {% endif %} + {% endfor %} + {% for v in q.variations.all %} +
  • + + {{ v.item }} – {{ v }}
  • + {% endfor %} +
+
{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %} + {% if 'event.items:write' in request.eventpermset %} + + {% endif %} +
+
+
+ {% if checkinlists %}
{% trans "Check-in lists" %} -

- {% blocktrans trimmed %} - You can choose to either add one or more check-in lists for every date in your series individually, - or use just one check-in list for all your dates and limit admission through check-in rules. Which - approach is better depends on multiple factors, such as the number of dates in your series. For a - series with one or less event date per day, individual lists are usually more helpful. If you - use dates to represent many time slots on the same day, or even overlapping time slots, working with - just one large check-in list will be easier. - {% endblocktrans %} -

-
- {{ cl_formset.management_form }} - {% bootstrap_formset_errors cl_formset %} -
- {% for form in cl_formset %} -
-
- {{ form.id }} - {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} -
-
-

-
-
- {% bootstrap_field form.name layout='inline' form_group_class="" %} +
+ + + + + {% if "event.orders:read" in request.eventpermset %} + + {% endif %} + + + + + + {% for cl in checkinlists %} + + + {% if "event.orders:read" in request.eventpermset %} + + {% endif %} + + + {% endfor %} - - -

- -

+ +
{% trans "Name" %}{% trans "Checked in" %}{% trans "Products" %}
+ {{ cl.name }} + +
+
+
+
-
- +
+ {{ cl.checkin_count|default_if_none:"0" }} / + {{ cl.position_count|default_if_none:"0" }}
- -
-
- {% bootstrap_form_errors form %} - {% bootstrap_field form.include_pending layout="control" %} - {% bootstrap_field form.all_products layout="control" %} - {% bootstrap_field form.limit_products layout="control" %} - {% bootstrap_field form.allow_entry_after_exit layout="control" %} - {% if form.gates %} - {% bootstrap_field form.gates layout="control" %} +
+ {% if cl.all_products %} + {% trans "All" %} + {% else %} +
    + {% for item in cl.limit_products.all %} +
  • + {{ item }} +
  • + {% endfor %} +
{% endif %} - - +
+ {% if "event.orders:read" in request.eventpermset %} + + {% endif %} + {% if "event.settings.general:write" in request.eventpermset %} + + + + {% endif %} +

- {% for f in plugin_forms %} - {% if f.title %} -
- {{ f.title }} - {% if f.template %} - {% include f.template with form=f %} - {% else %} - {% bootstrap_form f layout="control" %} - {% endif %} -
- {% endif %} - {% endfor %} + {% endif %} + {% eventsignal request.event "pretix.control.signals.subevent_detail_html" quota=quota %} +
+ {% if "event.orders:read" in request.eventpermset %} +
- {% trans "Additional settings" %} - {% for f in plugin_forms %} - {% if not f.title %} - {% if f.template %} - {% include f.template with form=f %} - {% else %} - {% bootstrap_form f layout="control" %} - {% endif %} + + {% trans "Orders" %} + + {{ order_count }} + + + {% if order_count %} +
+ + + + + + + + + {% for o in orders %} + + + + + {% endfor %} + +
{% trans "Order code" %}{% trans "Details" %}
+ + + {{ o.code }} + + +
+ {% if o.testmode %} + {% trans "TEST MODE" %} + {% endif %} + {% if o.status == "p" and o.pcnt == 0 %} + {# Everything related to this subevent is canceled #} + + + {% trans "partially canceled" %} + + {% else %} + {% include "pretixcontrol/orders/fragment_order_status.html" with order=o %} + {% endif %} +
+ {% if "." in o.sales_channel.icon %} + + {% else %} + + {% endif %} + {{ o.datetime|date:"SHORT_DATETIME_FORMAT" }} + {% if o.email %} +
{% icon "envelope-o fa-fw text-muted" %} + {{ o.email|default_if_none:"" }} + {% endif %} + {% if o.invoice_address.name %} +
{% icon "user fa-fw text-muted" %} {{ o.invoice_address.name }} + {% endif %} +
{% icon "ticket text-muted fa-fw" %} {{ o.pcnt }} + {% if o.comment %} +
+ + {{ o.comment|linebreaksbr }} + + {% endif %} + {% if o.custom_followup_due %} +
+ {% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %} + TODO {{ date }}{% endblocktrans %} + {% elif o.custom_followup_at %} +
+ {% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %} + TODO {{ date }}{% endblocktrans %} + {% endif %} +
+
+ {% if order_count > 10 %} +

+ + {% trans "View all" %} + +

{% endif %} - {% endfor %} + {% else %} +
+

+ {% blocktrans trimmed %} + No orders found. + {% endblocktrans %} +

+
+ {% endif %}
- {% if subevent.pk %} -
-
-
-

- {% trans "Date history" context "subevent" %} -

-
- {% include "pretixcontrol/includes/logs.html" with obj=subevent %} -
+ {% endif %} +
+
+
+

+ {% trans "Date history" context "subevent" %} +

- {% endif %} + {% include "pretixcontrol/includes/logs.html" with obj=subevent %} +
-
- -
- +
{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/edit.html b/src/pretix/control/templates/pretixcontrol/subevents/edit.html new file mode 100644 index 000000000..eccbe5425 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/subevents/edit.html @@ -0,0 +1,296 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load formset_tags %} +{% load eventsignal %} +{% load static %} +{% block title %}{% trans "Date" context "subevent" %}{% endblock %} +{% block content %} + {% if not subevent.pk %} +

{% trans "Create date" context "subevent" %}

+ {% else %} +

{% trans "Date" context "subevent" %}

+ {% endif %} +
+ {% csrf_token %} + {% bootstrap_form_errors form %} + {% for f in itemvar_forms %} + {% bootstrap_form_errors f %} + {% endfor %} +
+
+
+ {% trans "General information" %} + {% bootstrap_field form.name layout="control" %} + {% bootstrap_field form.active layout="control" %} + {% bootstrap_field form.date_from layout="control" %} + {% bootstrap_field form.date_to layout="control" %} + {% include "pretixcontrol/event/fragment_geodata.html" %} + {% bootstrap_field form.date_admission layout="control" %} + {% bootstrap_field form.frontpage_text layout="control" %} + {% bootstrap_field form.is_public layout="control" %} + {% bootstrap_field form.comment layout="control" %} + {% if meta_forms %} + + {% endif %} +
+
+ {% trans "Timeline" %} + {% bootstrap_field form.presale_start layout="control" %} + {% bootstrap_field form.presale_end layout="control" %} +
+
+ {% trans "Quotas" %} +
+ {{ formset.management_form }} + {% bootstrap_formset_errors formset %} +
+ {% for form in formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+

+
+
+ {% bootstrap_field form.name layout='inline' form_group_class="" %} +
+
+ +
+
+

+
+
+ {% bootstrap_form_errors form %} + {% bootstrap_field form.size layout="control" %} + {% bootstrap_field form.itemvars layout="control" %} + {% bootstrap_field form.release_after_exit layout="control" %} + {% bootstrap_field form.ignore_for_event_availability layout="control" %} +
+
+ {% endfor %} +
+ +

+ +

+
+
+ {% trans "Product settings" %} +

+ {% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %} +

+ {% for f in itemvar_forms %} +
+ {% bootstrap_form_errors f %} +
+ +
+
+ {% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %} +
+
+
+ {% bootstrap_field f.disabled layout="inline" form_group_class="" %} +
+
+
+
+ + {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}
+ {% bootstrap_field f.available_from form_group_class="foo" layout="inline" %} +
+
+ + {% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}
+ {% bootstrap_field f.available_until form_group_class="" layout="inline" %} +
+
+
+ {% endfor %} +
+
+ {% trans "Check-in lists" %} +

+ {% blocktrans trimmed %} + You can choose to either add one or more check-in lists for every date in your series individually, + or use just one check-in list for all your dates and limit admission through check-in rules. Which + approach is better depends on multiple factors, such as the number of dates in your series. For a + series with one or less event date per day, individual lists are usually more helpful. If you + use dates to represent many time slots on the same day, or even overlapping time slots, working with + just one large check-in list will be easier. + {% endblocktrans %} +

+
+ {{ cl_formset.management_form }} + {% bootstrap_formset_errors cl_formset %} +
+ {% for form in cl_formset %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+

+
+
+ {% bootstrap_field form.name layout='inline' form_group_class="" %} +
+
+ +
+
+

+
+
+ {% bootstrap_form_errors form %} + {% bootstrap_field form.include_pending layout="control" %} + {% bootstrap_field form.all_products layout="control" %} + {% bootstrap_field form.limit_products layout="control" %} + {% bootstrap_field form.allow_entry_after_exit layout="control" %} + {% if form.gates %} + {% bootstrap_field form.gates layout="control" %} + {% endif %} +
+
+ {% endfor %} +
+ +

+ +

+
+ {% for f in plugin_forms %} + {% if f.title %} +
+ {{ f.title }} + {% if f.template %} + {% include f.template with form=f %} + {% else %} + {% bootstrap_form f layout="control" %} + {% endif %} +
+ {% endif %} + {% endfor %} +
+ {% trans "Additional settings" %} + {% for f in plugin_forms %} + {% if not f.title %} + {% if f.template %} + {% include f.template with form=f %} + {% else %} + {% bootstrap_form f layout="control" %} + {% endif %} + {% endif %} + {% endfor %} +
+
+ {% if subevent.pk %} +
+
+
+

+ {% trans "Date history" context "subevent" %} +

+
+ {% include "pretixcontrol/includes/logs.html" with obj=subevent %} +
+
+ {% endif %} +
+
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/index.html b/src/pretix/control/templates/pretixcontrol/subevents/index.html index 52ac83fdf..60c235dcb 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/index.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/index.html @@ -133,7 +133,7 @@ {% endif %} - + {{ s.name }}
#{{ s.pk }} @@ -182,7 +182,7 @@ {% endif %} {% if "event.subevents:write" in request.eventpermset %} - +
- + {% endif %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 1d9576126..31075eb77 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -308,7 +308,8 @@ urlpatterns = [ re_path(r'^pdf/editor/(?P[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'), re_path(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'), re_path(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'), - re_path(r'^subevents/(?P\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'), + re_path(r'^subevents/(?P\d+)/$', subevents.SubEventDetail.as_view(), name='event.subevent'), + re_path(r'^subevents/(?P\d+)/edit$', subevents.SubEventUpdate.as_view(), name='event.subevent.edit'), re_path(r'^subevents/(?P\d+)/delete$', subevents.SubEventDelete.as_view(), name='event.subevent.delete'), re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index c06be5261..8a91e1aa8 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -41,7 +41,9 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.core.files import File from django.db import transaction -from django.db.models import Count, F, Prefetch, ProtectedError +from django.db.models import ( + Count, Exists, F, OuterRef, Prefetch, ProtectedError, Subquery, +) from django.db.models.functions import Coalesce, TruncDate, TruncTime from django.forms import inlineformset_factory from django.http import Http404, HttpResponse, HttpResponseRedirect @@ -49,17 +51,21 @@ from django.shortcuts import redirect, render from django.urls import reverse from django.utils.formats import get_format from django.utils.functional import cached_property +from django.utils.http import url_has_allowed_host_and_scheme from django.utils.timezone import make_aware, now from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.views import View -from django.views.generic import CreateView, FormView, ListView, UpdateView +from django.views.generic import ( + CreateView, DetailView, FormView, ListView, UpdateView, +) -from pretix.base.models import CartPosition, LogEntry +from pretix.base.models import CartPosition, LogEntry, OrderPosition from pretix.base.models.checkin import CheckinList from pretix.base.models.event import SubEvent, SubEventMetaValue from pretix.base.models.items import ( - ItemVariation, Quota, SubEventItem, SubEventItemVariation, + Item, ItemVariation, Quota, SubEventItem, SubEventItemVariation, ) +from pretix.base.models.orders import CancellationRequest from pretix.base.reldate import RelativeDate, RelativeDateWrapper from pretix.base.services import tickets from pretix.base.services.quotas import QuotaAvailability @@ -505,9 +511,68 @@ class SubEventEditorMixin(MetaDataEditorMixin): ) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms) -class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView): +class SubEventDetail(EventPermissionRequiredMixin, DetailView): model = SubEvent template_name = 'pretixcontrol/subevents/detail.html' + permission = None + context_object_name = 'subevent' + + def get_object(self, queryset=None) -> SubEvent: + try: + return self.request.event.subevents.get( + id=self.kwargs['subevent'] + ) + except SubEvent.DoesNotExist: + raise Http404(pgettext_lazy("subevent", "The requested date does not exist.")) + + def get_context_data(self, **kwargs): + oqs = self.request.event.orders.filter( + Exists( + OrderPosition.objects.filter( + subevent=self.object, + order_id=OuterRef("id"), + ) + ) + ).annotate( + pcnt=Subquery( + OrderPosition.objects.filter( + subevent=self.object, + ).values("subevent").annotate(c=Count("*")).values("c") + ), + has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))), + ).select_related("invoice_address").prefetch_related("sales_channel") + ctx = { + "quotas": self.object.quotas.prefetch_related( + Prefetch( + "items", + queryset=Item.objects.annotate( + has_variations=Exists(ItemVariation.objects.filter(item=OuterRef("pk"))) + ), + to_attr="cached_items" + ), + "variations", + "variations__item", + ).order_by("name", "pk"), + "checkinlists": self.object.checkinlist_set.prefetch_related("limit_products"), + "orders": oqs[:11], + "order_count": oqs.count(), + } + + qa = QuotaAvailability() + qa.queue(*ctx["quotas"]) + qa.compute() + for quota in ctx["quotas"]: + quota.cached_avail = qa.results[quota] + + return super().get_context_data( + **kwargs, + **ctx, + ) + + +class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView): + model = SubEvent + template_name = 'pretixcontrol/subevents/edit.html' permission = 'event.subevents:write' context_object_name = 'subevent' form_class = SubEventForm @@ -573,20 +638,28 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi return HttpResponseRedirect(self.get_success_url()) def get_success_url(self) -> str: - return reverse('control:event.subevents', kwargs={ + if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None): + return self.request.GET.get("next") + return reverse('control:event.subevent', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, - }) + ('?' + self.request.GET.get('returnto') if 'returnto' in self.request.GET else '') + 'subevent': self.object.pk, + }) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['event'] = self.request.event return kwargs + def get_context_data(self, **kwargs): + return super().get_context_data( + next_url=self.get_success_url() + ) + class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView): model = SubEvent - template_name = 'pretixcontrol/subevents/detail.html' + template_name = 'pretixcontrol/subevents/edit.html' permission = 'event.subevents:write' context_object_name = 'subevent' form_class = SubEventForm diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index aaff20d9e..83f68da87 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -137,6 +137,7 @@ event_urls = [ "subevents/select2", "subevents/add", "subevents/2/delete", + "subevents/2/edit", "subevents/2/", "quotas/", "quotas/2/delete", @@ -360,8 +361,9 @@ event_permission_urls = [ ("event.items:write", "discounts/reorder", 400, HTTP_POST), ("event.items:write", "discounts/add", 200, HTTP_GET), (None, "subevents/", 200, HTTP_GET), - ("event.subevents:write", "subevents/2/", 404, HTTP_GET), - ("event.subevents:write", "subevents/2/", 404, HTTP_POST), + (None, "subevents/2/", 404, HTTP_GET), + ("event.subevents:write", "subevents/2/edit", 404, HTTP_GET), + ("event.subevents:write", "subevents/2/edit", 404, HTTP_POST), ("event.subevents:write", "subevents/2/delete", 404, HTTP_GET), ("event.subevents:write", "subevents/add", 200, HTTP_GET), ("event.subevents:write", "subevents/bulk_add", 200, HTTP_GET), diff --git a/src/tests/control/test_subevents.py b/src/tests/control/test_subevents.py index 5e58e0ebf..1089e9891 100644 --- a/src/tests/control/test_subevents.py +++ b/src/tests/control/test_subevents.py @@ -110,9 +110,9 @@ class SubEventsTest(SoupTest): assert se.checkinlist_set.count() == 1 def test_modify(self): - doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk) + doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/edit' % self.subevent1.pk) assert doc.select("input[name=quotas-TOTAL_FORMS]") - doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk, { + doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/edit' % self.subevent1.pk, { 'name_0': 'SE2', 'active': 'on', 'date_from_0': '2017-07-01',