Compare commits

..

19 Commits

Author SHA1 Message Date
Raphael Michel
95271c2ce3 Document signal 2026-05-05 16:04:56 +02:00
Raphael Michel
c7215f4aee Add read-only details page for subevents 2026-05-05 14:51:37 +02:00
Thomas Göttgens
0acaed41be Fix Dockerfile syntax for chmod command (#6145) 2026-05-04 11:23:44 +02:00
Raphael Michel
993acce05a Settings: Fix typo in class path to mail backend (#6144) 2026-05-04 11:22:47 +02:00
luelista
fe2132435c Fix permissions of /pretix in docker container (#6133) 2026-05-04 11:13:38 +02:00
Raphael Michel
f4fcca19a4 Orders API: Fix race condition in voucher redemption (Z#23230391) (#6067)
The old code relied on the `Voucher.redeemed` value obtained *before*
the lock was taken, not afterwards.

The change in services/orders.py is functionally pointless, but it makes
the pattern of "fill availability only after lock" clearer and might
avoid introducing similar bugs in the future.
2026-04-29 19:57:08 +02:00
Raphael Michel
24d26a9455 Badges: Add export layout for 4x3" on letter (Z#23232464) (#6128)
* Badges: Add export layout for 4x3" on letter (Z#23232464)

* Consistent naming
2026-04-29 15:31:54 +02:00
Phin Wolkwitz
589f51454e Add locations to program times (Z#23221129)
Add location for program time slots and extend .ical and PDF placeholder
2026-04-29 11:59:06 +02:00
Raphael Michel
bda27d72e7 Bump version to 2026.5.0.dev0 2026-04-28 16:48:33 +02:00
Raphael Michel
f67690bc56 Bump version to 2025.5.0.dev0 2026-04-28 16:47:51 +02:00
Raphael Michel
75c8f97080 Bump version to 2026.4.0 2026-04-28 16:47:33 +02:00
Raphael Michel
10789f097d Bump version to 2025.5.0.dev0 2026-04-28 16:39:29 +02:00
Raphael Michel
1adec102e6 Bump version to 2025.4.0 2026-04-28 16:39:24 +02:00
Raphael Michel
921fd801e5 Thumbnails: Perform color space transform before resizing (Z#23232101) (#6120) 2026-04-28 15:35:27 +02:00
Kara Engelhardt
448d2e70d5 AddOnsStep: Expand selected variants 2026-04-28 14:50:35 +02:00
Kara Engelhardt
49f692c666 AddOnsStep: Use post data as initial data if exists (Z#23232311) 2026-04-28 14:50:35 +02:00
Kara Engelhardt
2d31c62812 Allow dash character in pdf placeholders 2026-04-28 13:10:08 +02:00
Raphael Michel
08df3d828d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6295 of 6295 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2026-04-28 12:20:37 +02:00
Raphael Michel
96e10bcd71 Translations: Update German
Currently translated at 100.0% (6295 of 6295 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2026-04-28 12:20:37 +02:00
32 changed files with 995 additions and 390 deletions

View File

@@ -31,6 +31,7 @@ RUN apt-get update && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
chmod 0755 /pretix && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \
mkdir /etc/supervisord

View File

@@ -16,6 +16,7 @@ Field Type Description
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
location multi-lingual string The program time slot's location (or ``null``)
===================================== ========================== =======================================================
.. versionchanged:: TODO
@@ -54,17 +55,20 @@ Endpoints
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
"end": "2025-08-15T00:00:00Z",
"location": null
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z"
"end": "2025-08-13T22:00:00Z",
"location": null
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z"
"end": "2025-08-17T22:00:00Z",
"location": null
}
]
}
@@ -99,7 +103,8 @@ Endpoints
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z"
"end": "2025-10-27T23:00:00Z",
"location": null
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -125,7 +130,8 @@ Endpoints
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
"end": "2025-08-15T22:00:00Z",
"location": null
}
**Example response**:
@@ -139,7 +145,8 @@ Endpoints
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
"end": "2025-08-15T22:00:00Z",
"location": null
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for

View File

@@ -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, event_settings_widget, oauth_application_registered, order_position_buttons, subevent_forms,
item_formsets, order_search_filter_q, order_search_forms
item_formsets, order_search_filter_q, order_search_forms, subevent_detail_html
.. automodule:: pretix.base.signals
:no-index:

View File

@@ -19,4 +19,4 @@
# 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/>.
#
__version__ = "2026.4.0.dev0"
__version__ = "2026.5.0.dev0"

View File

@@ -191,7 +191,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end')
fields = ('start', 'end', 'location')
class ItemBundleSerializer(serializers.ModelSerializer):
@@ -222,7 +222,7 @@ class ItemBundleSerializer(serializers.ModelSerializer):
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end')
fields = ('id', 'start', 'end', 'location')
def validate(self, data):
data = super().validate(data)

View File

@@ -1416,6 +1416,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
qa = QuotaAvailability()
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
qa.compute()
v_avail = {}
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
# use further down
@@ -1445,11 +1446,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
voucher_usage[v] += 1
if voucher_usage[v] > 0:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
if v not in v_avail:
v.refresh_from_db(fields=['redeemed'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=v) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail[v] = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail[v] < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.27 on 2026-01-21 12:06
import i18nfield.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_pluggable_permissions"),
]
operations = [
migrations.AddField(
model_name="itemprogramtime",
name="location",
field=i18nfield.fields.I18nTextField(max_length=200, null=True),
)
]

View File

@@ -2306,10 +2306,17 @@ class ItemProgramTime(models.Model):
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
:param location: venue
:type location: str
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
def clean(self):
if hasattr(self, 'item') and self.item and self.item.event.has_subevents:

View File

@@ -498,9 +498,9 @@ DEFAULT_VARIABLES = OrderedDict((
) if op.valid_until else ""
}),
("program_times", {
"label": _("Program times: date and time"),
"label": _("Program times"),
"editor_sample": _(
"2017-05-31 10:00 12:00\n2017-05-31 14:00 16:00\n2017-05-31 14:00 2017-06-01 14:00"),
"2017-05-31 10:00 12:00, Room 1\n2017-05-31 14:00 16:00, Room 2\n2017-05-31 14:00 2017-06-01 14:00, Building A"),
"evaluate": lambda op, order, ev: get_program_times(op, ev)
}),
("medium_identifier", {
@@ -748,13 +748,19 @@ def get_seat(op: OrderPosition):
def get_program_times(op: OrderPosition, ev: Event):
return '\n'.join([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
) for pt in op.item.program_times.all()
])
ptstr = []
for pt in op.item.program_times.all():
ptstr.append([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
),
(', ' + ', '.join(
l.strip() for l in str(pt.location).splitlines() if l.strip())
) if str(pt.location).strip() else ''
])
return '\n'.join(''.join(l) for l in ptstr)
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
@@ -923,7 +929,7 @@ class Renderer:
# We do not use str.format like in emails so we (a) can evaluate lazily and (b) can re-implement this
# 1:1 on other platforms that render PDFs through our API (libpretixprint)
return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text)
return re.sub(r'\{([-a-zA-Z0-9:_]+)\}', replace, text)
elif o['content'].startswith('itemmeta:'):
if op.variation_id:

View File

@@ -727,8 +727,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
_check_date(event, time_machine_now_dt)
products_seen = Counter()
q_avail = Counter()
v_avail = Counter()
v_usages = Counter()
v_budget = {}
deleted_positions = set()
@@ -793,6 +791,9 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
shared_lock_objects=[event]
)
q_avail = Counter()
v_avail = Counter()
# Check maximum order size
limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
if sum(1 for cp in sorted_positions if not cp.addon_to) > limit:

View File

@@ -574,7 +574,7 @@ class ItemCreateForm(I18nModelForm):
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
count=b.count, designated_price=b.designated_price)
for pt in self.cleaned_data['copy_from'].program_times.all():
instance.program_times.create(start=pt.start, end=pt.end)
instance.program_times.create(start=pt.start, end=pt.end, location=pt.location)
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
@@ -1354,6 +1354,10 @@ class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center, Heidelberg, Germany'
)
class Meta:
model = ItemProgramTime
@@ -1361,6 +1365,7 @@ class ItemProgramTimeForm(I18nModelForm):
fields = [
'start',
'end',
'location'
]
field_classes = {
'start': forms.SplitDateTimeField,

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.
"""
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'

View File

@@ -34,6 +34,7 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
{% bootstrap_field form.location layout="control" %}
</div>
</div>
{% endfor %}
@@ -59,6 +60,7 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
{% bootstrap_field formset.empty_form.location layout="control" %}
</div>
</div>
{% endescapescript %}

View File

@@ -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 %}
<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>
<h1>
{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}{% endblocktrans %}
{% if 'event.subevents:write' in request.eventpermset %}
<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 %}
</h1>
<div class="row">
<div class="{% if "event.orders:read" in request.eventpermset %}col-md-5{% else %}col-md-10{% endif %} col-xs-12">
<fieldset>
<legend>{% trans "General information" %}</legend>
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd>{{ subevent.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>#{{ subevent.pk }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not subevent.active %}
<span class="label label-danger">{% trans "Disabled" %}</span>
{% elif subevent.presale_has_ended %}
<span class="label label-warning">{% trans "Presale over" %}</span>
{% elif not subevent.presale_is_running %}
<span class="label label-warning">{% trans "Presale not started" %}</span>
{% else %}
<span class="label label-success">{% trans "On sale" %}</span>
{% endif %}
</dd>
<dt>{% trans "Event start time" %}</dt>
<dd>{{ subevent.date_from|date:"SHORT_DATETIME_FORMAT" }}</dd>
<dt>{% trans "Event end time" %}</dt>
<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 %}
</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>
{% 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 %}
</fieldset>
{% if subevent.comment %}
<dt>{% trans "Internal comment" %}</dt>
<dd>{{ subevent.comment|linebreaksbr }}</dd>
{% endif %}
</dl>
</fieldset>
<fieldset>
<legend>{% trans "Quotas" %}</legend>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Quota name" %}</th>
<th>{% trans "Products" %}</th>
<th>{% trans "Total capacity" %}</th>
<th>{% trans "Capacity left" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for q in quotas %}
<tr>
<td>
<strong><a
href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
{% if q.ignore_for_event_availability %}
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip"
title="{% trans "Ignore this quota when determining event availability" %}"></span>
{% endif %}
</td>
<td>
<ul>
{% for item in q.cached_items %}
{% if not item.has_variations %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endif %}
{% endfor %}
{% for v in q.variations.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
{{ v.item }} {{ v }}</a></li>
{% 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>
{% if checkinlists %}
<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 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="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 class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} /
{{ cl.position_count|default_if_none:"0" }}
</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" %}
</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 %}
</div>
</div>
</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 %}
</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>
</tbody>
</table>
</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 %}
{% 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 "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 %}
<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 %}
{% endfor %}
{% else %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No orders found.
{% endblocktrans %}
</p>
</div>
{% endif %}
</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>
{% endif %}
<div class="col-md-2 col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Date history" context "subevent" %}
</h3>
</div>
{% endif %}
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
</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>
</div>
{% 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 %}
{% 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 %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
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'^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<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(),
name='event.subevent.delete'),
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.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
@@ -52,14 +54,17 @@ from django.utils.functional import cached_property
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 +510,67 @@ 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.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'
context_object_name = 'subevent'
form_class = SubEventForm
@@ -585,7 +648,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
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

View File

@@ -5,16 +5,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:03+0000\n"
"PO-Revision-Date: 2026-03-30 11:50+0000\n"
"PO-Revision-Date: 2026-04-28 09:22+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py:87
@@ -8226,10 +8226,8 @@ msgstr ""
"2x Workshop 2"
#: pretix/base/pdf.py:383
#, fuzzy
#| msgid "List of Add-Ons"
msgid "List of Checked-In Add-Ons"
msgstr "Liste der Zusatz-Produkte"
msgstr "Liste der eingecheckten Zusatzprodukte"
#: pretix/base/pdf.py:390 pretix/control/forms/filter.py:1537
#: pretix/control/forms/filter.py:1539
@@ -9236,10 +9234,8 @@ msgid "Czech National Bank"
msgstr "Tschechische Nationalbank"
#: pretix/base/services/currencies.py:41
#, fuzzy
#| msgid "Czech National Bank"
msgid "National Bank of Poland"
msgstr "Tschechische Nationalbank"
msgstr "Nationalbank von Polen"
#: pretix/base/services/export.py:95 pretix/base/services/export.py:155
msgid ""
@@ -10330,16 +10326,12 @@ msgstr ""
"Rechnungsbetrag nicht in CZK ist."
#: pretix/base/settings.py:577 pretix/base/settings.py:586
#, fuzzy
#| msgid ""
#| "Based on Czech National Bank daily rates, whenever the invoice amount is "
#| "not in CZK."
msgid ""
"Based on National Bank of Poland daily rates, whenever the invoice amount is "
"not in PLN."
msgstr ""
"Basierend auf den Kursen der Tschechischen Nationalbank, immer wenn der "
"Rechnungsbetrag nicht in CZK ist."
"Basierend auf den Kursen der Nationalbank von Polen, immer wenn der "
"Rechnungsbetrag nicht in PLN ist."
#: pretix/base/settings.py:597
msgid "Require invoice address"
@@ -16440,10 +16432,8 @@ msgid "Allow to overbook quotas when performing this operation"
msgstr "Überbuchen von Kontingenten bei dieser Aktion erlauben"
#: pretix/control/forms/orders.py:335
#, fuzzy
#| msgid "Number of orders"
msgid "Number of products to add"
msgstr "Anzahl Bestellungen"
msgstr "Anzahl hinzuzufügender Produkt"
#: pretix/control/forms/orders.py:344
msgid "Add-on to"
@@ -16475,10 +16465,10 @@ msgstr ""
"Produktes."
#: pretix/control/forms/orders.py:441
#, fuzzy
#| msgid "You can not select the same seat multiple times."
msgid "You can not choose a seat when adding multiple products at once."
msgstr "Sie können den gleichen Sitzplatz nicht mehrfach auswählen."
msgstr ""
"Sie können keinen Sitzplatz auswählen, wenn mehrere Produkte auf einmal "
"hinzugefügt werden."
#: pretix/control/forms/orders.py:478 pretix/control/forms/orders.py:482
#: pretix/control/forms/orders.py:510 pretix/control/forms/orders.py:552
@@ -17097,7 +17087,7 @@ msgstr "Wochenendtag"
#: pretix/control/forms/subevents.py:106
msgctxt "subevent"
msgid "Skip dates that overlap with any existing date"
msgstr ""
msgstr "Überspringe Termine, die mit einem bestehenden Termin überlappen"
#: pretix/control/forms/subevents.py:109
msgctxt "subevent"
@@ -17107,6 +17097,10 @@ msgid ""
"This respects even inactive dates and works best if all dates have both a "
"start and end time."
msgstr ""
"Dies kann nützlich sein, wenn alle Termine am selben Ort stattfinden und "
"wiederholte Termine nicht in Konflikt mit existierenden Sonderterminen "
"geraten sollen. Dies berücksichtigt auch inaktive Termine und funktioniert "
"am besten, wenn alle Termine eine Start- und Endzeit haben."
#: pretix/control/forms/subevents.py:128
msgid "Keep the current values"
@@ -30276,6 +30270,8 @@ msgstr "Bitte erstelle maximal 100.000 Termine auf einmal."
#: pretix/control/views/subevents.py:966
msgid "All dates would be skipped because they conflict with existing dates."
msgstr ""
"Alle Termine würden übersprungen werden, weil sie mit existierenden Terminen "
"überlappen."
#: pretix/control/views/subevents.py:1102
#, python-brace-format

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:03+0000\n"
"PO-Revision-Date: 2026-03-30 11:50+0000\n"
"PO-Revision-Date: 2026-04-28 09:22+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8219,10 +8219,8 @@ msgstr ""
"2x Workshop 2"
#: pretix/base/pdf.py:383
#, fuzzy
#| msgid "List of Add-Ons"
msgid "List of Checked-In Add-Ons"
msgstr "Liste der Zusatz-Produkte"
msgstr "Liste der eingecheckten Zusatzprodukte"
#: pretix/base/pdf.py:390 pretix/control/forms/filter.py:1537
#: pretix/control/forms/filter.py:1539
@@ -9226,10 +9224,8 @@ msgid "Czech National Bank"
msgstr "Tschechische Nationalbank"
#: pretix/base/services/currencies.py:41
#, fuzzy
#| msgid "Czech National Bank"
msgid "National Bank of Poland"
msgstr "Tschechische Nationalbank"
msgstr "Nationalbank von Polen"
#: pretix/base/services/export.py:95 pretix/base/services/export.py:155
msgid ""
@@ -10318,16 +10314,12 @@ msgstr ""
"Rechnungsbetrag nicht in CZK ist."
#: pretix/base/settings.py:577 pretix/base/settings.py:586
#, fuzzy
#| msgid ""
#| "Based on Czech National Bank daily rates, whenever the invoice amount is "
#| "not in CZK."
msgid ""
"Based on National Bank of Poland daily rates, whenever the invoice amount is "
"not in PLN."
msgstr ""
"Basierend auf den Kursen der Tschechischen Nationalbank, immer wenn der "
"Rechnungsbetrag nicht in CZK ist."
"Basierend auf den Kursen der Nationalbank von Polen, immer wenn der "
"Rechnungsbetrag nicht in PLN ist."
#: pretix/base/settings.py:597
msgid "Require invoice address"
@@ -16417,10 +16409,8 @@ msgid "Allow to overbook quotas when performing this operation"
msgstr "Überbuchen von Kontingenten bei dieser Aktion erlauben"
#: pretix/control/forms/orders.py:335
#, fuzzy
#| msgid "Number of orders"
msgid "Number of products to add"
msgstr "Anzahl Bestellungen"
msgstr "Anzahl hinzuzufügender Produkt"
#: pretix/control/forms/orders.py:344
msgid "Add-on to"
@@ -16452,10 +16442,10 @@ msgstr ""
"Produktes."
#: pretix/control/forms/orders.py:441
#, fuzzy
#| msgid "You can not select the same seat multiple times."
msgid "You can not choose a seat when adding multiple products at once."
msgstr "Du kannst den gleichen Sitzplatz nicht mehrfach auswählen."
msgstr ""
"Du kannst keinen Sitzplatz auswählen, wenn mehrere Produkte auf einmal "
"hinzugefügt werden."
#: pretix/control/forms/orders.py:478 pretix/control/forms/orders.py:482
#: pretix/control/forms/orders.py:510 pretix/control/forms/orders.py:552
@@ -17075,7 +17065,7 @@ msgstr "Wochenendtag"
#: pretix/control/forms/subevents.py:106
msgctxt "subevent"
msgid "Skip dates that overlap with any existing date"
msgstr ""
msgstr "Überspringe Termine, die mit einem bestehenden Termin überlappen"
#: pretix/control/forms/subevents.py:109
msgctxt "subevent"
@@ -17085,6 +17075,10 @@ msgid ""
"This respects even inactive dates and works best if all dates have both a "
"start and end time."
msgstr ""
"Dies kann nützlich sein, wenn alle Termine am selben Ort stattfinden und "
"wiederholte Termine nicht in Konflikt mit existierenden Sonderterminen "
"geraten sollen. Dies berücksichtigt auch inaktive Termine und funktioniert "
"am besten, wenn alle Termine eine Start- und Endzeit haben."
#: pretix/control/forms/subevents.py:128
msgid "Keep the current values"
@@ -30232,6 +30226,8 @@ msgstr "Bitte erstelle maximal 100.000 Termine auf einmal."
#: pretix/control/views/subevents.py:966
msgid "All dates would be skipped because they conflict with existing dates."
msgstr ""
"Alle Termine würden übersprungen werden, weil sie mit existierenden Terminen "
"überlappen."
#: pretix/control/views/subevents.py:1102
#, python-brace-format

View File

@@ -57,7 +57,7 @@ from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.lib import pagesizes
from reportlab.lib.units import mm
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
from pretix.base.exporter import BaseExporter
@@ -133,6 +133,14 @@ OPTIONS = OrderedDict([
'offsets': [66.1 * mm, 29.6 * mm],
'pagesize': pagesizes.A4,
}),
('avery_4inx3in', {
'name': 'Avery 4" x 3" (74459)',
'cols': 2,
'rows': 3,
'margins': [1 * inch, .25 * inch, 1 * inch, .25 * inch],
'offsets': [4 * inch, 3 * inch],
'pagesize': pagesizes.LETTER,
}),
('avery_80x50', {
'name': 'Avery Zweckform 80 x 50 mm (L4785)',
'cols': 2,

View File

@@ -22,7 +22,7 @@
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from reportlab.lib import pagesizes
from reportlab.lib.units import mm
from reportlab.lib.units import inch, mm
def _simple_template(w, h):
@@ -261,4 +261,9 @@ TEMPLATES = {
"pagesize": (88.9 * mm, 33.87 * mm),
"layout": _simple_template(88.9 * mm, 33.87 * mm),
},
"4inx3in": {
"label": format_lazy(_("{width} x {height} inch label"), width=4, height=3),
"pagesize": (4 * inch, 3 * inch),
"layout": _simple_template(4 * inch, 3 * inch),
},
}

View File

@@ -37,6 +37,7 @@ import uuid
from collections import defaultdict
from decimal import Decimal
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core.cache import caches
@@ -69,6 +70,7 @@ from pretix.base.services.cart import (
from pretix.base.services.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons
@@ -529,6 +531,48 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
self._completed = True
return True
def _get_initial_val_price(self, current_addon_products, cartpos, item, variation):
val = None
price = None
if self.request.POST:
if variation:
field = f'cp_{cartpos.pk}_variation_{item.pk}_{variation.pk}'
else:
field = f'cp_{cartpos.pk}_item_{item.pk}'
try:
val = int(self.request.POST.get(field) or '0')
except ValueError:
pass
if val and item.free_price:
custom_price = forms.DecimalField(localize=True).to_python(self.request.POST.get(f'{field}_price') or '0')
price = get_price(
item, variation, voucher=cartpos.voucher, custom_price=custom_price, subevent=cartpos.subevent,
custom_price_is_net=self.event.settings.display_net_prices,
invoice_address=self.invoice_address,
)
else:
price = variation.suggested_price if variation else item.suggested_price
else:
current_products = current_addon_products[item.pk, variation.pk if variation else None]
val = len(current_products)
if current_products and item.free_price:
a = current_products[0]
price = TaxedPrice(
net=a.price - a.tax_value,
gross=a.price,
tax=a.tax_value,
name=a.item.tax_rule.name if a.item.tax_rule else "",
rate=a.tax_rate,
code=a.item.tax_rule.code if a.item.tax_rule else None,
)
else:
price = variation.suggested_price if variation else item.suggested_price
return val, price
@cached_property
def forms(self):
"""
@@ -587,34 +631,10 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if i.has_variations:
for v in i.available_variations:
v.initial = len(current_addon_products[i.pk, v.pk])
if v.initial and i.free_price:
a = current_addon_products[i.pk, v.pk][0]
v.initial_price = TaxedPrice(
net=a.price - a.tax_value,
gross=a.price,
tax=a.tax_value,
name=a.item.tax_rule.name if a.item.tax_rule else "",
rate=a.tax_rate,
code=a.item.tax_rule.code if a.item.tax_rule else None,
)
else:
v.initial_price = v.suggested_price
v.initial, v.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, v)
i.expand = any(v.initial for v in i.available_variations)
else:
i.initial = len(current_addon_products[i.pk, None])
if i.initial and i.free_price:
a = current_addon_products[i.pk, None][0]
i.initial_price = TaxedPrice(
net=a.price - a.tax_value,
gross=a.price,
tax=a.tax_value,
name=a.item.tax_rule.name if a.item.tax_rule else "",
rate=a.tax_rate,
code=a.item.tax_rule.code if a.item.tax_rule else None,
)
else:
i.initial_price = i.suggested_price
i.initial, i.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, None)
if items:
formsetentry['categories'].append({

View File

@@ -153,7 +153,7 @@ def get_private_icals(event, positions):
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
location = None
location = ", ".join(l.strip() for l in str(pt.location).splitlines() if l.strip())
dtstart = pt.start.astimezone(tz)
dtend = pt.end.astimezone(tz)
uid = 'pretix-{}-{}-{}-{}@{}'.format(

View File

@@ -95,17 +95,17 @@
<div class="col-md-2 col-sm-3 col-xs-6 availability-box">
{% if not event.settings.show_variations_expanded %}
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
data-label-alt="{% trans "Hide variants" %}"
aria-expanded="false" aria-controls="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations"
data-label-alt="{% if item.expand %}{% trans "Show variants" %}{% else %}{% trans "Hide variants" %}{% endif %}"
aria-expanded="{{ item.expand|yesno:"true,false" }}" aria-controls="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations"
aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
<span>{% trans "Show variants" %}</span>
<span>{% if item.expand %}{% trans "Hide variants" %}{% else %}{% trans "Show variants" %}{% endif %}</span>
</button>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}" id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations">
<div class="variations {% if not event.settings.show_variations_expanded and not item.expand %}variations-collapsed{% endif %}" id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations">
{% for var in item.available_variations %}
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation"
{% if not item.free_price %}

View File

@@ -265,7 +265,7 @@ EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
EMAIL_SUBJECT_PREFIX = '[pretix] '
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
EMAIL_TIMEOUT = 60
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]

View File

@@ -530,6 +530,7 @@ def test_item_detail_program_times(token_client, organizer, event, team, item, c
res["program_times"] = [{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
@@ -1972,32 +1973,54 @@ def program_time2(item, category):
end=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc))
@pytest.fixture
def program_time3(item, category):
return item.program_times.create(start=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 31, 0, 0, 0, tzinfo=timezone.utc),
location='Testlocation')
TEST_PROGRAM_TIMES_RES = {
0: {
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None,
},
1: {
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
"location": None,
},
2: {
"start": "2017-12-30T00:00:00Z",
"end": "2017-12-31T00:00:00Z",
"location": {"en": "Testlocation"},
}
}
@pytest.mark.django_db
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2):
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2, program_time3):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
res[1]["id"] = program_time2.pk
res[2]["id"] = program_time3.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res[0]['start'] == resp.data['results'][0]['start']
assert res[0]['end'] == resp.data['results'][0]['end']
assert res[0]['id'] == resp.data['results'][0]['id']
assert res[0] == resp.data['results'][0]
assert res[1]['start'] == resp.data['results'][1]['start']
assert res[1]['end'] == resp.data['results'][1]['end']
assert res[1]['id'] == resp.data['results'][1]['id']
assert res[1] == resp.data['results'][1]
assert res[2]['start'] == resp.data['results'][2]['start']
assert res[2]['end'] == resp.data['results'][2]['end']
assert res[2]['location'] == resp.data['results'][2]['location']
assert res[2]['id'] == resp.data['results'][2]['id']
assert res[2] == resp.data['results'][2]
@pytest.mark.django_db
@@ -2039,6 +2062,59 @@ def test_program_times_create(token_client, organizer, event, item):
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_create_location(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": {
"en": "Testlocation",
"de": "Testort"
}
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert "Testlocation" == program_time.location.localize("en")
assert "Testort" == program_time.location.localize("de")
@pytest.mark.django_db
def test_program_times_create_without_location(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z"
},
format='json'
)
assert resp.status_code == 201
assert resp.data['location'] is None
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert str(program_time.location) == ""
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
"location": None
},
format='json'
)
assert resp.status_code == 201
assert resp.data['location'] is None
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert str(program_time.location) == ""
@pytest.mark.django_db
def test_program_times_update(token_client, organizer, event, item, program_time):
resp = token_client.patch(

View File

@@ -82,7 +82,11 @@ def test_full_clone_same_organizer():
assert item1.meta_data
ItemProgramTime.objects.create(item=item1,
start=datetime.datetime(2017, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc))
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc),
location={
"en": "Testlocation",
"de": "Testort"
})
assert item1.program_times
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
hidden_if_item_available=item1)
@@ -169,6 +173,7 @@ def test_full_clone_same_organizer():
assert copied_item1.meta_data == item1.meta_data
assert copied_item1.program_times.first().start == item1.program_times.first().start
assert copied_item1.program_times.first().end == item1.program_times.first().end
assert copied_item1.program_times.first().location == item1.program_times.first().location
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type

View File

@@ -692,7 +692,8 @@ class ItemsTest(ItemFormTest):
self.item2.program_times.create(start=datetime.datetime(2017, 12, 27, 0, 0, 0,
tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0,
tzinfo=datetime.timezone.utc))
tzinfo=datetime.timezone.utc),
location={"en": "Testlocation", "de": "Testort"})
doc = self.get_doc('/control/event/%s/%s/items/add?copy_from=%d' % (self.orga1.slug, self.event1.slug, self.item2.pk))
data = extract_form_fields(doc.select("form")[0])
@@ -723,6 +724,7 @@ class ItemsTest(ItemFormTest):
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
assert i_old.program_times.first().start == i_new.program_times.first().start
assert i_old.program_times.first().end == i_new.program_times.first().end
assert i_old.program_times.first().location == i_new.program_times.first().location
def test_add_to_existing_quota(self):
with scopes_disabled():

View File

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

View File

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

View File

@@ -99,6 +99,15 @@ class BaseCheckoutTestCase:
self.workshopquota.variations.add(self.workshop2a)
self.workshopquota.variations.add(self.workshop2b)
self.parkingcat = ItemCategory.objects.create(name="Parking", is_addon=True, event=self.event)
self.parkingquota = Quota.objects.create(event=self.event, name='Parking', size=5)
self.parking1 = Item.objects.create(event=self.event, name='Premium Parking',
category=self.parkingcat, default_price=Decimal('15.00'))
self.parking2 = Item.objects.create(event=self.event, name='Standard Parking',
category=self.parkingcat, default_price=Decimal('5.00'))
self.parkingquota.items.add(self.parking1)
self.parkingquota.items.add(self.parking2)
def _set_session(self, key, value):
session = self.client.session
session['carts'][get_cart_session_key(self.client, self.event)][key] = value
@@ -4202,6 +4211,58 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert '35.29' in response.content.decode()
assert '10.08' in response.content.decode()
def test_set_addons_invalid_initial(self):
self.event.settings.locales = ['de', 'en']
self.event.settings.locale = 'de'
with scopes_disabled():
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1)
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.parkingcat, min_count=1)
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() - timedelta(minutes=10)
)
self.workshop1.free_price = True
self.workshop1.save()
self.workshop2.free_price = True
self.workshop2.save()
ws1_val = 'cp_{}_item_{}'.format(cp1.pk, self.workshop1.pk)
ws1_price = 'cp_{}_item_{}_price'.format(cp1.pk, self.workshop1.pk)
ws2a_val = 'cp_{}_variation_{}_{}'.format(cp1.pk, self.workshop2.pk, self.workshop2a.pk)
ws2a_price = 'cp_{}_variation_{}_{}_price'.format(cp1.pk, self.workshop2.pk, self.workshop2a.pk)
p1_val = 'cp_{}_item_{}'.format(cp1.pk, self.parking1.pk)
p2_val = 'cp_{}_item_{}'.format(cp1.pk, self.parking2.pk)
response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), {
ws1_val: '1',
ws2a_val: '1',
})
assert response.status_code == 200
with scopes_disabled():
assert cp1.addons.count() == 0
doc = BeautifulSoup(response.text, 'lxml')
assert doc.find('input', {'name': ws1_val}).attrs.get('checked')
assert doc.find('input', {'name': ws2a_val}).attrs.get('checked')
assert not doc.find('input', {'name': p1_val}).attrs.get('checked')
assert not doc.find('input', {'name': p2_val}).attrs.get('checked')
response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), {
ws1_val: '1',
ws1_price: '222,22',
ws2a_val: '1',
ws2a_price: '333.33',
})
assert response.status_code == 200
with scopes_disabled():
assert cp1.addons.count() == 0
doc = BeautifulSoup(response.text, 'lxml')
assert doc.find('input', {'name': ws1_val}).attrs.get('checked')
assert doc.find('input', {'name': ws1_price}).attrs.get('value') in ['222.22', '222,22']
assert doc.find('input', {'name': ws2a_val}).attrs.get('checked')
assert doc.find('input', {'name': ws2a_price}).attrs.get('value') in ['333.33', '333,33']
assert not doc.find('input', {'name': p1_val}).attrs.get('checked')
assert not doc.find('input', {'name': p2_val}).attrs.get('checked')
def test_confirm_subevent_presale_not_yet(self):
with scopes_disabled():
self.event.has_subevents = True