From ea04c8548654471daaed03dba46b6ffbb509569e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 6 May 2020 15:50:43 +0200 Subject: [PATCH] Various improvements to the subevent creation form (#1670) --- src/pretix/control/forms/subevents.py | 46 +++-- .../pretixcontrol/subevents/bulk.html | 80 +++++++- src/pretix/control/views/subevents.py | 189 ++++++++++-------- .../static/pretixcontrol/scss/_forms.scss | 12 ++ src/tests/control/test_events.py | 130 ++++++++++-- 5 files changed, 344 insertions(+), 113 deletions(-) diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py index 103e292326..81850ff9e6 100644 --- a/src/pretix/control/forms/subevents.py +++ b/src/pretix/control/forms/subevents.py @@ -2,6 +2,7 @@ from datetime import timedelta from urllib.parse import urlencode from django import forms +from django.core.exceptions import ValidationError from django.forms import formset_factory from django.urls import reverse from django.utils.dates import MONTHS, WEEKDAYS @@ -59,20 +60,6 @@ class SubEventForm(I18nModelForm): class SubEventBulkForm(SubEventForm): - time_from = forms.TimeField( - label=_('Event start time'), - widget=forms.TimeInput(attrs={'class': 'timepickerfield'}) - ) - time_to = forms.TimeField( - label=_('Event end time'), - widget=forms.TimeInput(attrs={'class': 'timepickerfield'}), - required=False - ) - time_admission = forms.TimeField( - label=_('Admission time'), - widget=forms.TimeInput(attrs={'class': 'timepickerfield'}), - required=False - ) rel_presale_start = RelativeDateTimeField( label=_('Start of presale'), help_text=_('Optional. No products will be sold before this date.'), @@ -376,3 +363,34 @@ RRuleFormSet = formset_factory( RRuleForm, can_order=False, can_delete=True, extra=1 ) + + +class TimeForm(forms.Form): + time_from = forms.TimeField( + label=_('Event start time'), + widget=forms.TimeInput(attrs={'class': 'timepickerfield'}), + required=True + ) + time_to = forms.TimeField( + label=_('Event end time'), + widget=forms.TimeInput(attrs={'class': 'timepickerfield'}), + required=False + ) + time_admission = forms.TimeField( + label=_('Admission time'), + widget=forms.TimeInput(attrs={'class': 'timepickerfield'}), + required=False + ) + + def clean(self): + d = super().clean() + if d.get('time_from') and d.get('time_to') and d['time_from'] > d['time_to']: + raise ValidationError({'time_to': _('The end of the event has to be later than its start.')}) + return d + + +TimeFormSet = formset_factory( + TimeForm, + min_num=1, + can_order=False, can_delete=True, extra=1, validate_min=True +) diff --git a/src/pretix/control/templates/pretixcontrol/subevents/bulk.html b/src/pretix/control/templates/pretixcontrol/subevents/bulk.html index c741083b54..59356d9e64 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/bulk.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/bulk.html @@ -261,12 +261,87 @@ +
+ {% trans "Times" context "subevent" %} + +
+
{% trans "Event start time" %}
+
+ {% trans "Event end time" %}
+ +
+
+ {% trans "Admission time" %}
+ +
+
+
+ {{ time_formset.management_form }} + {% bootstrap_formset_errors time_formset %} +
+ {% for f in time_formset %} + {% bootstrap_form_errors f %} +
+
+
+ {{ f.id }} + {% bootstrap_field f.DELETE form_group_class="" layout="inline" %} +
+
+ {% bootstrap_field f.time_from layout="inline" %} +
+
+ {% bootstrap_field f.time_to layout="inline" %} +
+
+ {% bootstrap_field f.time_admission layout="inline" %} +
+
+ +
+
+
+ {% endfor %} +
+ +

+ +

+
+
{% trans "General information" %} {% bootstrap_field form.name layout="control" %} {% bootstrap_field form.active layout="control" %} - {% bootstrap_field form.time_from layout="control" %} - {% bootstrap_field form.time_to layout="control" %}
{% bootstrap_field form.location layout="control" %}
@@ -291,7 +366,6 @@
- {% bootstrap_field form.time_admission layout="control" %} {% bootstrap_field form.frontpage_text layout="control" %} {% bootstrap_field form.is_public layout="control" %} {% if meta_forms %} diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index a6609b32c4..7a66105605 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -30,7 +30,7 @@ from pretix.control.forms.item import QuotaForm from pretix.control.forms.subevents import ( CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm, SubEventForm, SubEventItemForm, SubEventItemVariationForm, - SubEventMetaValueForm, + SubEventMetaValueForm, TimeFormSet, ) from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.signals import subevent_forms @@ -215,7 +215,7 @@ class SubEventEditorMixin(MetaDataEditorMixin): formsetclass = inlineformset_factory( SubEvent, Quota, - form=QuotaForm, formset=QuotaFormSet, + form=QuotaForm, formset=QuotaFormSet, min_num=1, validate_min=True, can_order=False, can_delete=True, extra=extra, ) if self.object: @@ -451,12 +451,17 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['event'] = self.request.event + initial = kwargs.get('initial', {}) if self.copy_from: i = modelcopy(self.copy_from) i.pk = None kwargs['instance'] = i else: kwargs['instance'] = SubEvent(event=self.request.event) + initial['location'] = self.request.event.location + initial['geo_lat'] = self.request.event.geo_lat + initial['geo_lon'] = self.request.event.geo_lon + kwargs['initial'] = initial return kwargs @transaction.atomic @@ -579,7 +584,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea form_class = SubEventBulkForm def is_valid(self, form): - return self.rrule_formset.is_valid() and super().is_valid(form) + return self.rrule_formset.is_valid() and self.time_formset.is_valid() and super().is_valid(form) def get_success_url(self) -> str: return reverse('control:event.subevents', kwargs={ @@ -594,9 +599,17 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea prefix='rruleformset' ) + @cached_property + def time_formset(self): + return TimeFormSet( + data=self.request.POST if self.request.method == "POST" else None, + prefix='timeformset' + ) + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['rrule_formset'] = self.rrule_formset + ctx['time_formset'] = self.time_formset return ctx @cached_property @@ -621,7 +634,9 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea def get_form_kwargs(self): kwargs = super().get_form_kwargs() - initial = {} + initial = { + 'active': True, + } kwargs['event'] = self.request.event tz = self.request.event.timezone if self.copy_from: @@ -643,9 +658,20 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea )) if i.presale_end else None else: kwargs['instance'] = SubEvent(event=self.request.event) + initial['location'] = self.request.event.location + initial['geo_lat'] = self.request.event.geo_lat + initial['geo_lon'] = self.request.event.geo_lon kwargs['initial'] = initial return kwargs + def get_times(self): + times = [] + for f in self.time_formset: + if f in self.time_formset.deleted_forms: + continue + times.append(f.cleaned_data) + return times + def get_rrule_set(self): s = rruleset() for f in self.rrule_formset: @@ -695,90 +721,91 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea tz = self.request.event.timezone cnt = 0 for rdate in self.get_rrule_set(): - se = copy.copy(form.instance) + for t in self.get_times(): + se = copy.copy(form.instance) - se.date_from = make_aware(datetime.combine(rdate, form.cleaned_data['time_from']), tz) - se.date_to = ( - make_aware(datetime.combine(rdate, form.cleaned_data['time_to']), tz) - if form.cleaned_data.get('time_to') - else None - ) - se.date_admission = ( - make_aware(datetime.combine(rdate, form.cleaned_data['time_admission']), tz) - if form.cleaned_data.get('time_admission') - else None - ) - se.presale_start = ( - form.cleaned_data['rel_presale_start'].datetime(se) - if form.cleaned_data.get('rel_presale_start') - else None - ) - se.presale_end = ( - form.cleaned_data['rel_presale_end'].datetime(se) - if form.cleaned_data.get('rel_presale_end') - else None - ) - se.save() - data = dict(form.cleaned_data) - for f in self.plugin_forms: - data.update({ - k: (f.cleaned_data.get(k).name - if isinstance(f.cleaned_data.get(k), File) - else f.cleaned_data.get(k)) - for k in f.cleaned_data - }) - se.log_action('pretix.subevent.added', data=data, user=self.request.user) + se.date_from = make_aware(datetime.combine(rdate, t['time_from']), tz) + se.date_to = ( + make_aware(datetime.combine(rdate, t['time_to']), tz) + if t.get('time_to') + else None + ) + se.date_admission = ( + make_aware(datetime.combine(rdate, t['time_admission']), tz) + if t.get('time_admission') + else None + ) + se.presale_start = ( + form.cleaned_data['rel_presale_start'].datetime(se) + if form.cleaned_data.get('rel_presale_start') + else None + ) + se.presale_end = ( + form.cleaned_data['rel_presale_end'].datetime(se) + if form.cleaned_data.get('rel_presale_end') + else None + ) + se.save() + data = dict(form.cleaned_data) + for f in self.plugin_forms: + data.update({ + k: (f.cleaned_data.get(k).name + if isinstance(f.cleaned_data.get(k), File) + else f.cleaned_data.get(k)) + for k in f.cleaned_data + }) + se.log_action('pretix.subevent.added', data=data, user=self.request.user) - for f in self.meta_forms: - if f.cleaned_data.get('value'): + for f in self.meta_forms: + if f.cleaned_data.get('value'): + i = copy.copy(f.instance) + i.subevent = se + i.save() + + for f in self.formset.forms: + if self.formset._should_delete_form(f): + continue + i = copy.copy(f.instance) + i.subevent = se + i.event = se.event + i.save() + selected_items = set(list(self.request.event.items.filter(id__in=[ + i.split('-')[0] for i in f.cleaned_data.get('itemvars', []) + ]))) + selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[ + i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i + ])) + i.items.add(*[_i for _i in selected_items]) + i.variations.add(*[_i for _i in selected_variations]) + + change_data = {k: f.cleaned_data.get(k) for k in f.changed_data} + change_data['id'] = i.pk + i.log_action(action='pretix.event.quota.added', user=self.request.user, data=change_data) + se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data) + + for f in self.cl_formset.forms: + if self.cl_formset._should_delete_form(f): + continue + i = copy.copy(f.instance) + i.subevent = se + i.event = se.event + i.save() + i.limit_products.add(*f.cleaned_data.get('limit_products', [])) + change_data = {k: f.cleaned_data.get(k) for k in f.changed_data} + change_data['id'] = i.pk + i.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data) + + for f in self.itemvar_forms: i = copy.copy(f.instance) i.subevent = se i.save() - for f in self.formset.forms: - if self.formset._should_delete_form(f): - continue - i = copy.copy(f.instance) - i.subevent = se - i.event = se.event - i.save() - selected_items = set(list(self.request.event.items.filter(id__in=[ - i.split('-')[0] for i in f.cleaned_data.get('itemvars', []) - ]))) - selected_variations = list(ItemVariation.objects.filter(item__event=self.request.event, id__in=[ - i.split('-')[1] for i in f.cleaned_data.get('itemvars', []) if '-' in i - ])) - i.items.add(*[_i for _i in selected_items]) - i.variations.add(*[_i for _i in selected_variations]) + for f in self.plugin_forms: + f.is_valid() + f.subevent = se + f.save() - change_data = {k: f.cleaned_data.get(k) for k in f.changed_data} - change_data['id'] = i.pk - i.log_action(action='pretix.event.quota.added', user=self.request.user, data=change_data) - se.log_action('pretix.subevent.quota.added', user=self.request.user, data=change_data) - - for f in self.cl_formset.forms: - if self.cl_formset._should_delete_form(f): - continue - i = copy.copy(f.instance) - i.subevent = se - i.event = se.event - i.save() - i.limit_products.add(*f.cleaned_data.get('limit_products', [])) - change_data = {k: f.cleaned_data.get(k) for k in f.changed_data} - change_data['id'] = i.pk - i.log_action(action='pretix.event.checkinlist.added', user=self.request.user, data=change_data) - - for f in self.itemvar_forms: - i = copy.copy(f.instance) - i.subevent = se - i.save() - - for f in self.plugin_forms: - f.is_valid() - f.subevent = se - f.save() - - cnt += 1 + cnt += 1 messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(cnt)) return redirect(reverse('control:event.subevents', kwargs={ diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index 98e43c690a..ea727771ba 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -406,6 +406,18 @@ table td > .checkbox input[type="checkbox"] { line-height: 40px; } } +#time-formset { + input { + display: block; + width: 100%; + } + .form-group { + margin: 0; + } + .row { + margin-bottom: 15px; + } +} .ticketoutput-panel .panel-heading { .checkbox { padding: 0; diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 5741a6ae4b..666e4bd5de 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -1174,10 +1174,14 @@ class SubEventsTest(SoupTest): 'rruleformset-0-end': 'count', 'rruleformset-0-count': '10', 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', 'name_0': 'Foo', 'active': 'on', - 'time_from': '13:29:31', - 'time_to': '15:29:31', 'location_0': 'Loc', 'time_admission': '', 'frontpage_text_0': '', @@ -1262,10 +1266,14 @@ class SubEventsTest(SoupTest): 'rruleformset-0-end': 'until', 'rruleformset-0-count': '10', 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', 'name_0': 'Foo', 'active': 'on', - 'time_from': '13:29:31', - 'time_to': '15:29:31', 'frontpage_text_0': '', 'rel_presale_start_0': 'unset', 'rel_presale_start_1': '', @@ -1277,10 +1285,13 @@ class SubEventsTest(SoupTest): 'rel_presale_end_2': '1', 'rel_presale_end_3': 'date_from', 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '0', + 'quotas-TOTAL_FORMS': '1', 'quotas-INITIAL_FORMS': '0', - 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '1', 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), 'checkinlist_set-TOTAL_FORMS': '0', 'checkinlist_set-INITIAL_FORMS': '0', 'checkinlist_set-MIN_NUM_FORMS': '0', @@ -1295,6 +1306,74 @@ class SubEventsTest(SoupTest): assert ses[110].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) assert ses[-1].date_from.isoformat() == "2019-04-02T11:29:31+00:00" + def test_create_bulk_daily_interval_multiple_times(self): + with scopes_disabled(): + self.event1.subevents.all().delete() + self.event1.settings.timezone = 'Europe/Berlin' + + doc = self.get_doc('/control/event/ccc/30c3/subevents/bulk_add') + assert doc.select("input[name=rruleformset-TOTAL_FORMS]") + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', { + 'rruleformset-TOTAL_FORMS': '1', + 'rruleformset-INITIAL_FORMS': '0', + 'rruleformset-MIN_NUM_FORMS': '0', + 'rruleformset-MAX_NUM_FORMS': '1000', + 'rruleformset-0-interval': '2', + 'rruleformset-0-freq': 'daily', + 'rruleformset-0-dtstart': '2018-04-03', + 'rruleformset-0-yearly_same': 'on', + 'rruleformset-0-yearly_bysetpos': '1', + 'rruleformset-0-yearly_byweekday': 'MO', + 'rruleformset-0-yearly_bymonth': '1', + 'rruleformset-0-monthly_same': 'on', + 'rruleformset-0-monthly_bysetpos': '1', + 'rruleformset-0-monthly_byweekday': 'MO', + 'rruleformset-0-end': 'until', + 'rruleformset-0-count': '10', + 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '2', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', + 'timeformset-1-time_from': '15:29:31', + 'timeformset-1-time_to': '17:29:31', + 'name_0': 'Foo', + 'active': 'on', + 'frontpage_text_0': '', + 'rel_presale_start_0': 'unset', + 'rel_presale_start_1': '', + 'rel_presale_start_2': '1', + 'rel_presale_start_3': 'date_from', + 'rel_presale_start_4': '', + 'rel_presale_end_1': '', + 'rel_presale_end_0': 'relative', + 'rel_presale_end_2': '1', + 'rel_presale_end_3': 'date_from', + 'rel_presale_end_4': '13:29:31', + 'quotas-TOTAL_FORMS': '1', + 'quotas-INITIAL_FORMS': '0', + 'quotas-MIN_NUM_FORMS': '0', + 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), + 'checkinlist_set-TOTAL_FORMS': '0', + 'checkinlist_set-INITIAL_FORMS': '0', + 'checkinlist_set-MIN_NUM_FORMS': '0', + 'checkinlist_set-MAX_NUM_FORMS': '1000', + }) + assert doc.select(".alert-success") + with scopes_disabled(): + ses = list(self.event1.subevents.order_by('date_from')) + assert len(ses) == 183 * 2 + + assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00" + assert ses[1].date_from.isoformat() == "2018-04-03T13:29:31+00:00" + assert ses[220].date_from.isoformat() == "2018-11-09T12:29:31+00:00" # DST :) + assert ses[-1].date_from.isoformat() == "2019-04-02T13:29:31+00:00" + def test_create_bulk_exclude(self): with scopes_disabled(): self.event1.subevents.all().delete() @@ -1335,10 +1414,14 @@ class SubEventsTest(SoupTest): 'rruleformset-1-count': '10', 'rruleformset-1-until': '2019-04-03', 'rruleformset-1-exclude': 'on', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', 'name_0': 'Foo', 'active': 'on', - 'time_from': '13:29:31', - 'time_to': '15:29:31', 'frontpage_text_0': '', 'rel_presale_start_0': 'unset', 'rel_presale_start_1': '', @@ -1350,10 +1433,13 @@ class SubEventsTest(SoupTest): 'rel_presale_end_2': '1', 'rel_presale_end_3': 'date_from', 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '0', + 'quotas-TOTAL_FORMS': '1', 'quotas-INITIAL_FORMS': '0', 'quotas-MIN_NUM_FORMS': '0', 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), 'checkinlist_set-TOTAL_FORMS': '0', 'checkinlist_set-INITIAL_FORMS': '0', 'checkinlist_set-MIN_NUM_FORMS': '0', @@ -1392,10 +1478,14 @@ class SubEventsTest(SoupTest): 'rruleformset-0-end': 'until', 'rruleformset-0-count': '10', 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', 'name_0': 'Foo', 'active': 'on', - 'time_from': '13:29:31', - 'time_to': '15:29:31', 'frontpage_text_0': '', 'rel_presale_start_0': 'unset', 'rel_presale_start_1': '', @@ -1407,10 +1497,13 @@ class SubEventsTest(SoupTest): 'rel_presale_end_2': '1', 'rel_presale_end_3': 'date_from', 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '0', + 'quotas-TOTAL_FORMS': '1', 'quotas-INITIAL_FORMS': '0', 'quotas-MIN_NUM_FORMS': '0', 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), 'checkinlist_set-TOTAL_FORMS': '0', 'checkinlist_set-INITIAL_FORMS': '0', 'checkinlist_set-MIN_NUM_FORMS': '0', @@ -1449,10 +1542,14 @@ class SubEventsTest(SoupTest): 'rruleformset-0-end': 'until', 'rruleformset-0-count': '10', 'rruleformset-0-until': '2019-04-03', + 'timeformset-TOTAL_FORMS': '1', + 'timeformset-INITIAL_FORMS': '0', + 'timeformset-MIN_NUM_FORMS': '1', + 'timeformset-MAX_NUM_FORMS': '1000', + 'timeformset-0-time_from': '13:29:31', + 'timeformset-0-time_to': '15:29:31', 'name_0': 'Foo', 'active': 'on', - 'time_from': '13:29:31', - 'time_to': '15:29:31', 'frontpage_text_0': '', 'rel_presale_start_0': 'unset', 'rel_presale_start_1': '', @@ -1464,10 +1561,13 @@ class SubEventsTest(SoupTest): 'rel_presale_end_2': '1', 'rel_presale_end_3': 'date_from', 'rel_presale_end_4': '13:29:31', - 'quotas-TOTAL_FORMS': '0', + 'quotas-TOTAL_FORMS': '1', 'quotas-INITIAL_FORMS': '0', 'quotas-MIN_NUM_FORMS': '0', 'quotas-MAX_NUM_FORMS': '1000', + 'quotas-0-name': 'Q1', + 'quotas-0-size': '50', + 'quotas-0-itemvars': str(self.ticket.pk), 'checkinlist_set-TOTAL_FORMS': '0', 'checkinlist_set-INITIAL_FORMS': '0', 'checkinlist_set-MIN_NUM_FORMS': '0',