Add read-only details page for subevents

This commit is contained in:
Raphael Michel
2026-05-05 14:47:12 +02:00
parent 0acaed41be
commit c7215f4aee
8 changed files with 666 additions and 281 deletions

View File

@@ -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. 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() organizer_edit_tabs = DeprecatedSignal()
""" """
Arguments: 'organizer', 'request' Arguments: 'organizer', 'request'

View File

@@ -4,275 +4,298 @@
{% load formset_tags %} {% load formset_tags %}
{% load eventsignal %} {% load eventsignal %}
{% load static %} {% 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 %} {% block content %}
{% if not subevent.pk %} <h1>
<h1>{% trans "Create date" context "subevent" %}</h1> {% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}{% endblocktrans %}
{% else %} {% if 'event.subevents:write' in request.eventpermset %}
<h1>{% trans "Date" context "subevent" %}</h1> <a href="{% url "control:event.subevent.edit" event=request.event.slug organizer=request.event.organizer.slug subevent=subevent.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit" %}
</a>
{% endif %} {% endif %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data"> </h1>
{% csrf_token %}
{% bootstrap_form_errors form %}
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
{% endfor %}
<div class="row"> <div class="row">
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}"> <div class="{% if "event.orders:read" in request.eventpermset %}col-md-5{% else %}col-md-10{% endif %} col-xs-12">
<fieldset> <fieldset>
<legend>{% trans "General information" %}</legend> <legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %} <dl class="dl-horizontal">
{% bootstrap_field form.active layout="control" %} <dt>{% trans "Name" %}</dt>
{% bootstrap_field form.date_from layout="control" %} <dd>{{ subevent.name }}</dd>
{% bootstrap_field form.date_to layout="control" %} <dt>{% trans "ID" %}</dt>
{% include "pretixcontrol/event/fragment_geodata.html" %} <dd>#{{ subevent.pk }}</dd>
{% bootstrap_field form.date_admission layout="control" %} <dt>{% trans "Status" %}</dt>
{% bootstrap_field form.frontpage_text layout="control" %} <dd>
{% bootstrap_field form.is_public layout="control" %} {% if not subevent.active %}
{% bootstrap_field form.comment layout="control" %} <span class="label label-danger">{% trans "Disabled" %}</span>
{% if meta_forms %} {% elif subevent.presale_has_ended %}
<div class="form-group metadata-group"> <span class="label label-warning">{% trans "Presale over" %}</span>
<label class="col-md-3 control-label">{% trans "Meta data" %}</label> {% elif not subevent.presale_is_running %}
<div class="col-md-9"> <span class="label label-warning">{% trans "Presale not started" %}</span>
{% for form in meta_forms %} {% else %}
<div class="row"> <span class="label label-success">{% trans "On sale" %}</span>
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %} {% endif %}
</fieldset> </dd>
<fieldset> <dt>{% trans "Event start time" %}</dt>
<legend>{% trans "Timeline" %}</legend> <dd>{{ subevent.date_from|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% bootstrap_field form.presale_start layout="control" %} <dt>{% trans "Event end time" %}</dt>
{% bootstrap_field form.presale_end layout="control" %} <dd>{{ subevent.date_to|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% if subevent.date_admission %}
<dt>{% trans "Admission time" %}</dt>
<dd>{{ subevent.date_admission|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if subevent.presale_start %}
<dt>{% trans "Start of presale" %}</dt>
<dd>{{ subevent.presale_start|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if subevent.presale_end %}
<dt>{% trans "End of presale" %}</dt>
<dd>{{ subevent.presale_end|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if subevent.location %}
<dt>{% trans "Location" %}</dt>
<dd>{{ subevent.location|linebreaksbr }}</dd>
{% endif %}
<dt>{% trans "Show in lists" %}</dt>
<dd>{{ subevent.is_public|yesno }}</dd>
{% for k, v in subevent.meta_data.items %}
<dt>{{ k }}</dt>
<dd>{{ v }}</dd>
{% endfor %}
{% if subevent.comment %}
<dt>{% trans "Internal comment" %}</dt>
<dd>{{ subevent.comment|linebreaksbr }}</dd>
{% endif %}
</dl>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Quotas" %}</legend> <legend>{% trans "Quotas" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}"> <div class="table-responsive">
{{ formset.management_form }} <table class="table table-hover table-quotas">
{% bootstrap_formset_errors formset %} <thead>
<div data-formset-body> <tr>
{% for form in formset %} <th>{% trans "Quota name" %}</th>
<div class="panel panel-default" data-formset-form> <th>{% trans "Products" %}</th>
<div class="sr-only"> <th>{% trans "Total capacity" %}</th>
{{ form.id }} <th>{% trans "Capacity left" %}</th>
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %} <th class="action-col-2"></th>
</div> </tr>
<div class="panel-heading"> </thead>
<h4 class="panel-title"> <tbody>
<div class="row"> {% for q in quotas %}
<div class="col-md-10"> <tr>
{% bootstrap_field form.name layout='inline' form_group_class="" %} <td>
</div> <strong><a
<div class="col-md-2 text-right flip"> href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
<button type="button" class="btn btn-danger" data-formset-delete-button> {% if q.ignore_for_event_availability %}
<i class="fa fa-trash"></i></button> <span class="fa fa-eye-slash text-muted" data-toggle="tooltip"
</div> title="{% trans "Ignore this quota when determining event availability" %}"></span>
</div> {% endif %}
</h4> </td>
</div> <td>
<div class="panel-body form-horizontal"> <ul>
{% bootstrap_form_errors form %} {% for item in q.cached_items %}
{% bootstrap_field form.size layout="control" %} {% if not item.has_variations %}
{% bootstrap_field form.itemvars layout="control" %} <li>
{% bootstrap_field form.release_after_exit layout="control" %} <a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
{% bootstrap_field form.ignore_for_event_availability layout="control" %} </li>
</div> {% endif %}
</div>
{% endfor %} {% endfor %}
</div> {% for v in q.variations.all %}
<script type="form-template" data-formset-empty-form> <li>
{% escapescript %} <a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
<div class="panel panel-default" data-formset-form> {{ v.item }} {{ v }}</a></li>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.size layout="control" %}
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
</p>
</fieldset>
<fieldset>
<legend>{% trans "Product settings" %}</legend>
<p class="text-muted">
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
</label>
<div class="col-md-4">
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
</div>
</div>
</div>
{% endfor %} {% endfor %}
</ul>
</td>
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
<td class="text-right flip">
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</fieldset> </fieldset>
{% if checkinlists %}
<fieldset> <fieldset>
<legend>{% trans "Check-in lists" %}</legend> <legend>{% trans "Check-in lists" %}</legend>
<p class="help-block"> <div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Name" %}</th>
{% if "event.orders:read" in request.eventpermset %}
<th>{% trans "Checked in" %}</th>
{% endif %}
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for cl in checkinlists %}
<tr>
<td>
<strong><a
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
</td>
{% if "event.orders:read" in request.eventpermset %}
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} /
{{ cl.position_count|default_if_none:"0" }}
</div>
</div>
</td>
{% endif %}
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in cl.limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td class="text-right flip">
{% if "event.orders:read" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
{% endif %}
{% if "event.settings.general:write" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}"
data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>
{% endif %}
{% eventsignal request.event "pretix.control.signals.subevent_detail_html" quota=quota %}
</div>
{% if "event.orders:read" in request.eventpermset %}
<div class="col-md-5 col-xs-12">
<fieldset>
<legend>
{% trans "Orders" %}
<span class="badge">
{{ order_count }}
</span>
</legend>
{% if order_count %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-orders">
<thead>
<tr>
<th>{% trans "Order code" %}</th>
<th>{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
{% for o in orders %}
<tr>
<td>
<strong>
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
{{ o.code }}
</a>
</strong>
<br>
{% if o.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if o.status == "p" and o.pcnt == 0 %}
{# Everything related to this subevent is canceled #}
<span class="label label-danger">
<span class="fa fa-times"></span>
{% trans "partially canceled" %}
</span>
{% else %}
{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}
{% endif %}
</td>
<td>
{% if "." in o.sales_channel.icon %}
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
{% else %}
<span class="fa fa-fw fa-{{ o.sales_channel.icon }} text-muted"
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
{% endif %}
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if o.email %}
<br>{% icon "envelope-o fa-fw text-muted" %}
{{ o.email|default_if_none:"" }}
{% endif %}
{% if o.invoice_address.name %}
<br>{% icon "user fa-fw text-muted" %} {{ o.invoice_address.name }}
{% endif %}
<br>{% icon "ticket text-muted fa-fw" %} {{ o.pcnt }}
{% if o.comment %}
<br>
<span class="text-muted">
{{ o.comment|linebreaksbr }}
</span>
{% endif %}
{% if o.custom_followup_due %}
<br>
<span class="label label-danger">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
TODO {{ date }}{% endblocktrans %}</span>
{% elif o.custom_followup_at %}
<br>
<span class="label label-default">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
TODO {{ date }}{% endblocktrans %}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if order_count > 10 %}
<p class="text-center">
<a href="{% url "control:event.orders" organizer=request.organizer.slug event=request.event.slug %}?subevent={{ subevent.pk }}"
class="btn btn-default">
{% trans "View all" %}
</a>
</p>
{% endif %}
{% else %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
You can choose to either add one or more check-in lists for every date in your series individually, No orders found.
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 %} {% endblocktrans %}
</p> </p>
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
{{ cl_formset.management_form }}
{% bootstrap_formset_errors cl_formset %}
<div data-formset-body>
{% for form in cl_formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div> </div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% 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 %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ cl_formset.empty_form.id }}
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
{% if cl_formset.empty_form.gates %}
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
{% endif %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
</button>
</p>
</fieldset>
{% for f in plugin_forms %}
{% if f.title %}
<fieldset>
<legend>{{ f.title }}</legend>
{% if f.template %}
{% include f.template with form=f %}
{% else %}
{% bootstrap_form f layout="control" %}
{% endif %} {% endif %}
</fieldset> </fieldset>
{% endif %}
{% endfor %}
<fieldset>
<legend>{% trans "Additional settings" %}</legend>
{% 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 %}
</fieldset>
</div> </div>
{% if subevent.pk %} {% endif %}
<div class="col-xs-12 col-lg-2"> <div class="col-md-2 col-xs-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">
@@ -282,12 +305,5 @@
{% include "pretixcontrol/includes/logs.html" with obj=subevent %} {% include "pretixcontrol/includes/logs.html" with obj=subevent %}
</div> </div>
</div> </div>
{% endif %}
</div> </div>
<div class="form-group submit-group submit-group-sticky">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,293 @@
{% 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 %}
<h1>{% trans "Create date" context "subevent" %}</h1>
{% else %}
<h1>{% trans "Date" context "subevent" %}</h1>
{% endif %}
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
{% endfor %}
<div class="row">
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}">
<fieldset>
<legend>{% trans "General information" %}</legend>
{% 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 %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for form in meta_forms %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="control" %}
{% bootstrap_field form.presale_end layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Quotas" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% 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" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.size layout="control" %}
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
</p>
</fieldset>
<fieldset>
<legend>{% trans "Product settings" %}</legend>
<p class="text-muted">
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
{% bootstrap_form_errors f %}
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
</label>
<div class="col-md-4">
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
</div>
</div>
</div>
{% endfor %}
</fieldset>
<fieldset>
<legend>{% trans "Check-in lists" %}</legend>
<p class="help-block">
{% 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 %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
{{ cl_formset.management_form }}
{% bootstrap_formset_errors cl_formset %}
<div data-formset-body>
{% for form in cl_formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% 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 %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ cl_formset.empty_form.id }}
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 text-right flip">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
{% if cl_formset.empty_form.gates %}
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
{% endif %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
</button>
</p>
</fieldset>
{% for f in plugin_forms %}
{% if f.title %}
<fieldset>
<legend>{{ f.title }}</legend>
{% if f.template %}
{% include f.template with form=f %}
{% else %}
{% bootstrap_form f layout="control" %}
{% endif %}
</fieldset>
{% endif %}
{% endfor %}
<fieldset>
<legend>{% trans "Additional settings" %}</legend>
{% 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 %}
</fieldset>
</div>
{% if subevent.pk %}
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Date history" context "subevent" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
</div>
</div>
{% endif %}
</div>
<div class="form-group submit-group submit-group-sticky">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -182,7 +182,7 @@
{% endif %} {% endif %}
{% if "event.subevents:write" in request.eventpermset %} {% if "event.subevents:write" in request.eventpermset %}
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a> <a href="{% url "control:event.subevent.edit" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}"> <div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" <button type="button" class="btn btn-default btn-sm dropdown-toggle"
data-toggle="dropdown"> data-toggle="dropdown">

View File

@@ -308,7 +308,8 @@ urlpatterns = [
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'), re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
re_path(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'), 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/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'), re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventDetail.as_view(), name='event.subevent'),
re_path(r'^subevents/(?P<subevent>\d+)/edit$', subevents.SubEventUpdate.as_view(), name='event.subevent.edit'),
re_path(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(), re_path(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
name='event.subevent.delete'), name='event.subevent.delete'),
re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),

View File

@@ -41,7 +41,9 @@ from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.db import transaction 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.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
@@ -52,14 +54,17 @@ from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.views import View 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.checkin import CheckinList
from pretix.base.models.event import SubEvent, SubEventMetaValue from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import ( 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.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services import tickets from pretix.base.services import tickets
from pretix.base.services.quotas import QuotaAvailability from pretix.base.services.quotas import QuotaAvailability
@@ -505,9 +510,67 @@ class SubEventEditorMixin(MetaDataEditorMixin):
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms) ) 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 model = SubEvent
template_name = 'pretixcontrol/subevents/detail.html' 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.all.filter(
subevent=self.object,
)
)
).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' permission = 'event.subevents:write'
context_object_name = 'subevent' context_object_name = 'subevent'
form_class = SubEventForm form_class = SubEventForm
@@ -585,7 +648,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView): class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
model = SubEvent model = SubEvent
template_name = 'pretixcontrol/subevents/detail.html' template_name = 'pretixcontrol/subevents/edit.html'
permission = 'event.subevents:write' permission = 'event.subevents:write'
context_object_name = 'subevent' context_object_name = 'subevent'
form_class = SubEventForm form_class = SubEventForm

View File

@@ -137,6 +137,7 @@ event_urls = [
"subevents/select2", "subevents/select2",
"subevents/add", "subevents/add",
"subevents/2/delete", "subevents/2/delete",
"subevents/2/edit",
"subevents/2/", "subevents/2/",
"quotas/", "quotas/",
"quotas/2/delete", "quotas/2/delete",
@@ -360,8 +361,9 @@ event_permission_urls = [
("event.items:write", "discounts/reorder", 400, HTTP_POST), ("event.items:write", "discounts/reorder", 400, HTTP_POST),
("event.items:write", "discounts/add", 200, HTTP_GET), ("event.items:write", "discounts/add", 200, HTTP_GET),
(None, "subevents/", 200, HTTP_GET), (None, "subevents/", 200, HTTP_GET),
("event.subevents:write", "subevents/2/", 404, HTTP_GET), (None, "subevents/2/", 404, HTTP_GET),
("event.subevents:write", "subevents/2/", 404, HTTP_POST), ("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/2/delete", 404, HTTP_GET),
("event.subevents:write", "subevents/add", 200, HTTP_GET), ("event.subevents:write", "subevents/add", 200, HTTP_GET),
("event.subevents:write", "subevents/bulk_add", 200, HTTP_GET), ("event.subevents:write", "subevents/bulk_add", 200, HTTP_GET),

View File

@@ -110,9 +110,9 @@ class SubEventsTest(SoupTest):
assert se.checkinlist_set.count() == 1 assert se.checkinlist_set.count() == 1
def test_modify(self): 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]") 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', 'name_0': 'SE2',
'active': 'on', 'active': 'on',
'date_from_0': '2017-07-01', 'date_from_0': '2017-07-01',