Bulk creation for event series dates (#848)

* copy-from things

* Some frontend

* rrule UI

* .

* Fixes

* UI improvements

* First test

* Tests
This commit is contained in:
Raphael Michel
2018-04-03 18:21:27 +02:00
committed by GitHub
parent 8564f93706
commit 7939503a11
15 changed files with 3820 additions and 21 deletions

View File

@@ -82,7 +82,6 @@ class RelativeDateWrapper:
new_date = new_date.astimezone(tz)
newoffset = new_date.utcoffset()
new_date += oldoffset - newoffset
return new_date
def to_string(self) -> str:
@@ -154,6 +153,11 @@ class RelativeDateTimeField(forms.MultiValueField):
('absolute', _('Fixed date:')),
('relative', _('Relative date:')),
]
if kwargs.get('limit_choices'):
limit = kwargs.pop('limit_choices')
choices = [(k, v) for k, v in BASE_CHOICES if k in limit]
else:
choices = BASE_CHOICES
if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set')))
fields = (
@@ -168,7 +172,7 @@ class RelativeDateTimeField(forms.MultiValueField):
required=False
),
forms.ChoiceField(
choices=BASE_CHOICES,
choices=choices,
required=False
),
forms.TimeField(
@@ -176,7 +180,7 @@ class RelativeDateTimeField(forms.MultiValueField):
),
)
if 'widget' not in kwargs:
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=BASE_CHOICES)
kwargs['widget'] = RelativeDateTimeWidget(status_choices=status_choices, base_choices=choices)
kwargs.pop('max_length', 0)
kwargs.pop('empty_value', 0)
super().__init__(

View File

@@ -9,7 +9,9 @@ from django.utils.translation import get_language
from pretix.base.models.auth import StaffSession
from pretix.base.settings import GlobalSettingsObject
from ..helpers.i18n import get_javascript_format, get_moment_locale
from ..helpers.i18n import (
get_javascript_format, get_javascript_output_format, get_moment_locale,
)
from .signals import html_head, nav_event, nav_global, nav_topbar
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@@ -82,6 +84,7 @@ def contextprocessor(request):
ctx['js_datetime_format'] = get_javascript_format('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format('DATE_INPUT_FORMATS')
ctx['js_long_date_format'] = get_javascript_output_format('DATE_FORMAT')
ctx['js_time_format'] = get_javascript_format('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
ctx['select2locale'] = get_language()[:2]

View File

@@ -1,10 +1,16 @@
from datetime import timedelta
from django import forms
from django.forms import formset_factory
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimePickerWidget
from pretix.helpers.money import change_decimal_field
@@ -46,6 +52,44 @@ 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.'),
required=False,
limit_choices=('date_from', 'date_to'),
)
rel_presale_end = RelativeDateTimeField(
label=_('End of presale'),
help_text=_('Optional. No products will be sold after this date. If you do not set this value, the presale '
'will end after the end date of your event.'),
required=False,
limit_choices=('date_from', 'date_to'),
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['rows'] = '3'
del self.fields['date_from']
del self.fields['date_to']
del self.fields['date_admission']
class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
@@ -97,6 +141,7 @@ class QuotaFormSet(I18nInlineFormSet):
kwargs['locales'] = self.locales
kwargs['event'] = self.event
kwargs['items'] = self.items
kwargs['items'] = self.items
return super()._construct_form(i, **kwargs)
@property
@@ -155,3 +200,175 @@ class CheckinListFormSet(I18nInlineFormSet):
)
self.add_fields(form, None)
return form
class RRuleForm(forms.Form):
# TODO: calendar.setfirstweekday
exclude = forms.BooleanField(
label=_('Exclude these dates instead of adding them.'),
required=False
)
freq = forms.ChoiceField(
choices=[
('yearly', _('year(s)')),
('monthly', _('month(s)')),
('weekly', _('week(s)')),
('daily', _('day(s)')),
]
)
interval = forms.IntegerField(
label=_('Interval'),
initial=1
)
dtstart = forms.DateField(
label=_('Start date'),
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
initial=lambda: now().date()
)
end = forms.ChoiceField(
choices=[
('count', ''),
('until', ''),
],
initial='count',
widget=forms.RadioSelect
)
count = forms.IntegerField(
label=_('Number of repititions'),
initial=10
)
until = forms.DateField(
widget=forms.DateInput(
attrs={
'class': 'datepickerfield',
'required': 'required'
}
),
label=_('Last date'),
required=True,
initial=lambda: now() + timedelta(days=365)
)
yearly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
yearly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
yearly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
yearly_bymonth = forms.ChoiceField(
choices=[
('1', _('January')),
('2', _('February')),
('3', _('March')),
('4', _('April')),
('5', _('May')),
('6', _('June')),
('7', _('July')),
('8', _('August')),
('9', _('September')),
('10', _('October')),
('11', _('November')),
('12', _('December')),
],
required=False
)
monthly_same = forms.ChoiceField(
choices=[
('on', ''),
('off', ''),
],
initial='on',
widget=forms.RadioSelect
)
monthly_bysetpos = forms.ChoiceField(
choices=[
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
)
monthly_byweekday = forms.ChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
('MO,TU,WE,TH,FR,SA,SU', _('Day')),
('MO,TU,WE,TH,FR', _('Weekday')),
('SA,SU', _('Weekend day')),
],
required=False
)
weekly_byweekday = forms.MultipleChoiceField(
choices=[
('MO', _('Monday')),
('TU', _('Tuesday')),
('WE', _('Wednesday')),
('TH', _('Thursday')),
('FR', _('Friday')),
('SA', _('Saturday')),
('SU', _('Sunday')),
],
required=False,
widget=forms.CheckboxSelectMultiple
)
def parse_weekdays(self, value):
m = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if ',' in value:
return [m.get(a) for a in value.split(',')]
else:
return m.get(value)
RRuleFormSet = formset_factory(
RRuleForm,
can_order=False, can_delete=True, extra=1
)

View File

@@ -30,6 +30,7 @@
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
@@ -52,7 +53,7 @@
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
</head>
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}">
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}" data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}" data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}">
<div id="wrapper">
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
<div class="navbar-header">

View File

@@ -0,0 +1,442 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% load captureas %}
{% load eventsignal %}
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
{% block content %}
<h1>{% trans "Create multiple dates" context "subevent" %}</h1>
<form action="" method="post" class="form-horizontal" id="subevent-bulk-create-form">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% for f in itemvar_forms %}
{% bootstrap_form_errors f %}
{% endfor %}
<div class="row">
<div class="col-md-8">
<fieldset>
<legend>{% trans "Dates" context "subevent" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ rrule_formset.prefix }}"
id="rrule-formset">
{{ rrule_formset.management_form }}
{% bootstrap_formset_errors rrule_formset %}
<div data-formset-body>
{% for f in rrule_formset %}
{% bootstrap_form_errors f %}
<div data-formset-form>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% trans "Repetition rule" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger btn-xs"
data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-inline rrule-form">
<div class="sr-only">
{{ f.id }}
{% bootstrap_field f.DELETE form_group_class="" layout="inline" %}
</div>
{% captureas ffield_freq %}
{% bootstrap_field f.freq layout="inline" %}
{% endcaptureas %}
{% captureas ffield_interval %}
{% bootstrap_field f.interval layout="inline" %}
{% endcaptureas %}
{% captureas ffield_dtstart %}
{% bootstrap_field f.dtstart layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bysetpos %}
{% bootstrap_field f.yearly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_byweekday %}
{% bootstrap_field f.yearly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bymonth %}
{% bootstrap_field f.yearly_bymonth layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_bysetpos %}
{% bootstrap_field f.monthly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_byweekday %}
{% bootstrap_field f.monthly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_count %}
{% bootstrap_field f.count layout="inline" %}
{% endcaptureas %}
{% captureas ffield_until %}
{% bootstrap_field f.until layout="inline" %}
{% endcaptureas %}
{% blocktrans trimmed with freq=ffield_freq interval=ffield_interval start=ffield_dtstart %}
Repeat every {{ interval }} {{ freq }}, starting at {{ start }}.
{% endblocktrans %}<br>
<div class="repeat-yearly">
<div class="radio">
<label>
{{ f.yearly_same.0 }}
{% trans "At the same date every year" %}
</label><br>
<label>
{{ f.yearly_same.1 }}
{% blocktrans trimmed with setpos=ffield_yearly_bysetpos weekday=ffield_yearly_byweekday month=ffield_yearly_bymonth %}
On the {{ setpos }} {{ weekday }} of {{ month }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-monthly">
<div class="radio">
<label>
{{ f.monthly_same.0 }}
{% trans "At the same date every month" %}
</label><br>
<label>
{{ f.monthly_same.1 }}
{% blocktrans trimmed with setpos=ffield_monthly_bysetpos weekday=ffield_monthly_byweekday %}
On the {{ setpos }} {{ weekday }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-weekly">
{% bootstrap_field f.weekly_byweekday layout="inline" %}
</div>
<div class="repeat-until">
<div class="radio">
<label>
{{ f.end.0 }}
{% blocktrans trimmed with count=ffield_count %}
Repeat for {{ count }} times
{% endblocktrans %}
</label><br>
<label>
{{ f.end.1 }}
{% blocktrans trimmed with until=ffield_until %}
Repeat until {{ until }}
{% endblocktrans %}<br>
</label>
</div>
</div>
{% bootstrap_field f.exclude layout="inline" %}
</div>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div data-formset-form>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<div class="row">
<div class="col-md-10">
{% trans "Repetition rule" %}
</div>
<div class="col-md-2 text-right">
<button type="button" class="btn btn-danger btn-xs"
data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</h4>
</div>
<div class="panel-body form-inline rrule-form">
<div class="sr-only">
{{ rrule_formset.empty_form.id }}
{% bootstrap_field rrule_formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
{% captureas ffield_freq %}
{% bootstrap_field rrule_formset.empty_form.freq layout="inline" %}
{% endcaptureas %}
{% captureas ffield_interval %}
{% bootstrap_field rrule_formset.empty_form.interval layout="inline" %}
{% endcaptureas %}
{% captureas ffield_dtstart %}
{% bootstrap_field rrule_formset.empty_form.dtstart layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bysetpos %}
{% bootstrap_field rrule_formset.empty_form.yearly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_byweekday %}
{% bootstrap_field rrule_formset.empty_form.yearly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_yearly_bymonth %}
{% bootstrap_field rrule_formset.empty_form.yearly_bymonth layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_bysetpos %}
{% bootstrap_field rrule_formset.empty_form.monthly_bysetpos layout="inline" %}
{% endcaptureas %}
{% captureas ffield_monthly_byweekday %}
{% bootstrap_field rrule_formset.empty_form.monthly_byweekday layout="inline" %}
{% endcaptureas %}
{% captureas ffield_count %}
{% bootstrap_field rrule_formset.empty_form.count layout="inline" %}
{% endcaptureas %}
{% captureas ffield_until %}
{% bootstrap_field rrule_formset.empty_form.until layout="inline" %}
{% endcaptureas %}
{% blocktrans trimmed with freq=ffield_freq interval=ffield_interval start=ffield_dtstart %}
Repeat every {{ interval }} {{ freq }}, starting at {{ start }}.
{% endblocktrans %}<br>
<div class="repeat-yearly">
<div class="radio">
<label>
{{ rrule_formset.empty_form.yearly_same.0 }}
{% trans "At the same date every year" %}
</label><br>
<label>
{{ rrule_formset.empty_form.yearly_same.1 }}
{% blocktrans trimmed with setpos=ffield_yearly_bysetpos weekday=ffield_yearly_byweekday month=ffield_yearly_bymonth %}
On the {{ setpos }} {{ weekday }} of {{ month }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-monthly">
<div class="radio">
<label>
{{ rrule_formset.empty_form.monthly_same.0 }}
{% trans "At the same date every month" %}
</label><br>
<label>
{{ rrule_formset.empty_form.monthly_same.1 }}
{% blocktrans trimmed with setpos=ffield_monthly_bysetpos weekday=ffield_monthly_byweekday %}
On the {{ setpos }} {{ weekday }}
{% endblocktrans %}<br>
</label>
</div>
</div>
<div class="repeat-weekly">
{% bootstrap_field rrule_formset.empty_form.weekly_byweekday layout="inline" %}
</div>
<div class="repeat-until">
<div class="radio">
<label>
{{ rrule_formset.empty_form.end.0 }}
{% blocktrans trimmed with count=ffield_count %}
Repeat for {{ count }} times
{% endblocktrans %}
</label><br>
<label>
{{ rrule_formset.empty_form.end.1 }}
{% blocktrans trimmed with until=ffield_until %}
Repeat until {{ until }}
{% endblocktrans %}<br>
</label>
</div>
</div>
{% bootstrap_field rrule_formset.empty_form.exclude layout="inline" %}
</div>
</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 rule" %}</button>
</p>
</div>
</fieldset>
</div>
<div class="col-md-4">
<fieldset>
<legend>{% trans "Preview" context "subevent" %}</legend>
<ul id="rrule-preview">
</ul>
</fieldset>
</div>
</div>
<fieldset>
<legend>{% trans "General information" %}</legend>
{% 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" %}
{% bootstrap_field form.time_admission layout="control" %}
{% bootstrap_field form.frontpage_text 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" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.rel_presale_start layout="control" %}
{% bootstrap_field form.rel_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">
<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" %}
</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">
<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" %}
</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 "Item prices" %}</legend>
{% for f in itemvar_forms %}
{% bootstrap_field f.price addon_after=request.event.currency layout="control" %}
{% endfor %}
</fieldset>
<fieldset>
<legend>{% trans "Check-in lists" %}</legend>
<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">
<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" %}
</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">
<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" %}
</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>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -38,6 +38,9 @@
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i>
{% trans "Create a new date" context "subevent" %}</a>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i>
{% trans "Create many new dates" context "subevent" %}</a>
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
@@ -97,7 +100,24 @@
</td>
<td class="text-right">
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}" class="btn btn-default btn-sm"><i class="fa fa-copy"></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">
<span class="fa fa-copy"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for a new date" context "subevent" %}
</a>
</li>
<li>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for many new dates" context "subevent" %}
</a>
</li>
</ul>
</div>
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>

View File

@@ -103,6 +103,7 @@ urlpatterns = [
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
name='event.subevent.delete'),
url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),
url(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'),
url(r'^items/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'),
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),

View File

@@ -1,5 +1,7 @@
import copy
from datetime import datetime
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule, rruleset
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import transaction
@@ -7,19 +9,25 @@ from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum
from django.db.models.functions import Coalesce
from django.forms import inlineformset_factory
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.functional import cached_property
from django.utils.timezone import make_aware
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from pretix.base.models.checkin import CheckinList
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
from pretix.base.models.items import (
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
)
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.control.forms.checkin import CheckinListForm
from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import (
CheckinListFormSet, QuotaFormSet, SubEventForm, SubEventItemForm,
SubEventItemVariationForm, SubEventMetaValueForm,
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkForm,
SubEventForm, SubEventItemForm, SubEventItemVariationForm,
SubEventMetaValueForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import PaginationMixin
@@ -92,7 +100,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
def get(self, request, *args, **kwargs):
if self.get_object().orderposition_set.count() > 0:
messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
'placed.'))
'placed.'))
return HttpResponseRedirect(self.get_success_url())
return super().get(request, *args, **kwargs)
@@ -103,7 +111,7 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
if self.object.orderposition_set.count() > 0:
messages.error(request, pgettext_lazy('subevent', 'A date can not be deleted if orders already have been '
'placed.'))
'placed.'))
return HttpResponseRedirect(self.get_success_url())
elif not self.object.allow_delete(): # checking if this is the last date in the event series
messages.error(request, pgettext_lazy('subevent', 'The last date of an event series can not be deleted.'))
@@ -142,7 +150,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
extra = 0
kwargs = {}
if self.copy_from:
if self.copy_from and self.request.method != "POST":
kwargs['initial'] = [
{
'name': cl.name,
@@ -152,7 +160,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
]
extra = len(kwargs['initial'])
elif not self.object:
elif not self.object and self.request.method != "POST":
kwargs['initial'] = [
{
'name': '',
@@ -179,7 +187,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
extra = 0
kwargs = {}
if self.copy_from:
if self.copy_from and self.request.method != "POST":
kwargs['initial'] = [
{
'size': q.size,
@@ -199,9 +207,11 @@ class SubEventEditorMixin(MetaDataEditorMixin):
if self.object:
kwargs['queryset'] = self.object.quotas.prefetch_related('items', 'variations')
return formsetclass(self.request.POST if self.request.method == "POST" else None,
instance=self.object,
event=self.request.event, **kwargs)
return formsetclass(
self.request.POST if self.request.method == "POST" else None,
instance=self.object,
event=self.request.event, **kwargs
)
def save_cl_formset(self, obj):
for form in self.cl_formset.initial_forms:
@@ -285,7 +295,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
@cached_property
def copy_from(self):
if self.request.GET.get("copy_from") and not getattr(self, 'object'):
if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
try:
return self.request.event.subevents.get(pk=self.request.GET.get("copy_from"))
except SubEvent.DoesNotExist:
@@ -428,6 +438,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
form.instance.event = self.request.event
messages.success(self.request, pgettext_lazy('subevent', 'The new date has been created.'))
ret = super().form_valid(form)
self.object = form.instance
form.instance.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
self.save_formset(form.instance)
@@ -435,6 +446,239 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
for f in self.itemvar_forms:
f.instance.subevent = form.instance
f.save()
self.object = form.instance
for f in self.meta_forms:
f.instance.subevent = form.instance
self.save_meta()
return ret
@cached_property
def meta_forms(self):
def clone(o):
o = copy.copy(o)
o.pk = None
return o
if self.copy_from:
val_instances = {
v.property_id: clone(v) for v in self.copy_from.meta_values.all()
}
else:
val_instances = {}
formlist = []
for p in self.request.organizer.meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
model = SubEvent
template_name = 'pretixcontrol/subevents/bulk.html'
permission = 'can_change_settings'
context_object_name = 'subevent'
form_class = SubEventBulkForm
def is_valid(self, form):
return self.rrule_formset.is_valid() and super().is_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
@cached_property
def rrule_formset(self):
return RRuleFormSet(
data=self.request.POST if self.request.method == "POST" else None,
prefix='rruleformset'
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['rrule_formset'] = self.rrule_formset
return ctx
@cached_property
def meta_forms(self):
def clone(o):
o = copy.copy(o)
o.pk = None
return o
if self.copy_from:
val_instances = {
v.property_id: clone(v) for v in self.copy_from.meta_values.all()
}
else:
val_instances = {}
formlist = []
for p in self.request.organizer.meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
initial = {}
kwargs['event'] = self.request.event
tz = self.request.event.timezone
if self.copy_from:
i = copy.copy(self.copy_from)
i.pk = None
kwargs['instance'] = i
initial['time_from'] = i.date_from.astimezone(tz).time()
initial['time_to'] = i.date_to.astimezone(tz).time() if i.date_to else None
initial['time_admission'] = i.date_admission.astimezone(tz).time() if i.date_admission else None
initial['rel_presale_start'] = RelativeDateWrapper(RelativeDate(
days_before=(i.date_from.astimezone(tz).date() - i.presale_start.astimezone(tz).date()).days,
base_date_name='date_from',
time=i.presale_start.astimezone(tz).time()
)) if i.presale_start else None
initial['rel_presale_end'] = RelativeDateWrapper(RelativeDate(
days_before=(i.date_from.astimezone(tz).date() - i.presale_end.astimezone(tz).date()).days,
base_date_name='date_from',
time=i.presale_end.astimezone(tz).time()
)) if i.presale_start else None
else:
kwargs['instance'] = SubEvent(event=self.request.event)
kwargs['initial'] = initial
return kwargs
def get_rrule_set(self):
s = rruleset()
for f in self.rrule_formset:
if f in self.rrule_formset.deleted_forms:
continue
rule_kwargs = {}
rule_kwargs['dtstart'] = f.cleaned_data['dtstart']
rule_kwargs['interval'] = f.cleaned_data['interval']
if f.cleaned_data['freq'] == 'yearly':
freq = YEARLY
if f.cleaned_data['yearly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['yearly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['yearly_byweekday'])
rule_kwargs['bymonth'] = int(f.cleaned_data['yearly_bymonth'])
elif f.cleaned_data['freq'] == 'monthly':
freq = MONTHLY
if f.cleaned_data['monthly_same'] == "off":
rule_kwargs['bysetpos'] = int(f.cleaned_data['monthly_bysetpos'])
rule_kwargs['byweekday'] = f.parse_weekdays(f.cleaned_data['monthly_byweekday'])
elif f.cleaned_data['freq'] == 'weekly':
freq = WEEKLY
if f.cleaned_data['weekly_byweekday']:
rule_kwargs['byweekday'] = [f.parse_weekdays(a) for a in f.cleaned_data['weekly_byweekday']]
elif f.cleaned_data['freq'] == 'daily':
freq = DAILY
if f.cleaned_data['end'] == 'count':
rule_kwargs['count'] = f.cleaned_data['count']
else:
rule_kwargs['until'] = f.cleaned_data['until']
if f.cleaned_data['exclude']:
s.exrule(rrule(freq, **rule_kwargs))
else:
s.rrule(rrule(freq, **rule_kwargs))
return s
@transaction.atomic
def form_valid(self, form):
tz = self.request.event.timezone
cnt = 0
for rdate in self.get_rrule_set():
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()
se.log_action('pretix.subevent.added', data=dict(form.cleaned_data), user=self.request.user)
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()
cnt += 1
messages.success(self.request, pgettext_lazy('subevent', '{} new dates have been created.').format(cnt))
return redirect(reverse('control:event.subevents', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}))
def post(self, request, *args, **kwargs):
form = self.get_form()
self.object = SubEvent(event=self.request.event)
if self.is_valid(form):
return self.form_valid(form)
else:
return self.form_invalid(form)

View File

@@ -34,6 +34,48 @@ date_conversion_to_moment = {
'%X': ''
}
out_date_conversion_to_moment = {
'a': 'a',
'A': 'A',
'b': 'MMM',
'c': 'YYYY-MM-DDTHH:mm:ss.SSSSSSZ',
'd': 'DD',
'e': 'zz',
'E': 'MMMM',
'f': 'h:mm',
'F': 'MMMM',
'g': 'h',
'G': 'H',
'h': 'hh',
'H': 'HH',
'i': 'mm',
'I': '',
'j': 'D',
'l': 'dddd',
'L': '',
'm': 'MM',
'M': 'MMM',
'n': 'M',
'N': 'MMM', # fuzzy
'o': 'GGGG',
'O': 'ZZ',
'P': 'h:mm a',
'r': 'ddd, D MMM YYYY HH:mm:ss Z',
's': 'ss',
'S': 'Do', # fuzzy
't': '',
'T': 'z',
'u': 'SSSSSS',
'U': 'X',
'w': 'd',
'W': 'W',
'y': 'YY',
'Y': 'YYYY',
'z': 'DDD',
'Z': ''
}
moment_locales = {
'af', 'az', 'bs', 'de-at', 'en-gb', 'et', 'fr-ch', 'hi', 'it', 'ko', 'me', 'ms-my', 'pa-in', 'se', 'sr', 'th',
'tzm-latn', 'zh-hk', 'ar', 'be', 'ca', 'de', 'en-ie', 'eu', 'fr', 'hr', 'ja', 'ky', 'mi', 'my', 'pl', 'si', 'ss',
@@ -45,10 +87,23 @@ moment_locales = {
}
toJavascript_re = re.compile(r'(?<!\w)(' + '|'.join(date_conversion_to_moment.keys()) + r')\b')
toJavascriptOut_re = re.compile(r'(?<!\w)(' + '|'.join(out_date_conversion_to_moment.keys()) + r')\b')
def get_javascript_output_format(format_name):
f = get_format(format_name)
if not isinstance(f, str):
f = f[0]
return toJavascriptOut_re.sub(
lambda x: out_date_conversion_to_moment[x.group()],
f
)
def get_javascript_format(format_name):
f = get_format(format_name)[0]
f = get_format(format_name)
if not isinstance(f, str):
f = f[0]
return toJavascript_re.sub(
lambda x: date_conversion_to_moment[x.group()],
f

View File

@@ -0,0 +1,25 @@
from django import template
register = template.Library()
@register.tag(name='captureas')
def do_captureas(parser, token):
try:
tag_name, args = token.contents.split(None, 1)
except ValueError:
raise template.TemplateSyntaxError("'captureas' node requires a variable name.")
nodelist = parser.parse(('endcaptureas',))
parser.delete_first_token()
return CaptureasNode(nodelist, args)
class CaptureasNode(template.Node):
def __init__(self, nodelist, varname):
self.nodelist = nodelist
self.varname = varname
def render(self, context):
output = self.nodelist.render(context)
context[self.varname] = output
return ''

View File

@@ -1,9 +1,146 @@
/*globals $, Morris, gettext*/
/*globals $, Morris, gettext, RRule, RRuleSet*/
$(function () {
if (!$("div[data-formset-prefix=checkinlist_set]").length) {
return;
}
function parse_weekday(wd) {
map = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if (wd.indexOf(",") > 0) {
var wds = [];
$.each(wd.split(","), function (k, v) {
wds.push(map[v]);
});
return wds;
} else {
return map[wd];
}
}
function rrule_preview() {
var ruleset = new RRuleSet();
$(".rrule-form").each(function () {
if ($(this).find("input[name$=DELETE]").prop("checked")) {
return;
}
var rule_args = {};
var $form = $(this);
var freq = $form.find("select[name*=freq]").val();
if (!$form.find("input[name*=dtstart]").data("DateTimePicker")) {
// uninitialized
return;
}
var dtstart = $form.find("input[name*=dtstart]").data("DateTimePicker").date();
dtstart = dtstart.add(dtstart.utcOffset(), 'm').add(12, 'h').utcOffset(0);
rule_args.dtstart = dtstart.toDate();
rule_args.interval = parseInt($form.find("input[name*=interval]").val()) || 1;
if (freq === 'yearly') {
rule_args.freq = RRule.YEARLY;
var same = $form.find("input[name*=yearly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=yearly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=yearly_byweekday]").val());
rule_args.bymonth = parseInt($form.find("select[name*=yearly_bymonth]").val());
}
} else if (freq === 'monthly') {
rule_args.freq = RRule.MONTHLY;
var same = $form.find("input[name*=monthly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=monthly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=monthly_byweekday]").val());
}
} else if (freq === 'weekly') {
rule_args.freq = RRule.WEEKLY;
var days = [];
$form.find("input[name*=weekly_byweekday]:checked").each(function () {
days.push(parse_weekday($(this).val()));
});
if (days.length !== 0) {
rule_args.byweekday = days;
}
} else if (freq === 'daily') {
rule_args.freq = RRule.DAILY;
}
var end = $form.find("input[name*=end]:checked").val();
if (end === "count") {
rule_args.count = parseInt($form.find("input[name*=count]").val()) || 1;
} else {
var date = $form.find("input[name*=until]").data("DateTimePicker").date();
if (date !== null) {
rule_args.until = date.toDate();
}
}
if ($form.find("input[name*=exclude]").prop("checked")) {
ruleset.exrule(new RRule(rule_args));
$form.closest(".panel").addClass("panel-danger").removeClass("panel-default");
} else {
ruleset.rrule(new RRule(rule_args));
$form.closest(".panel").addClass("panel-default").removeClass("panel-danger");
}
});
var all_dates = ruleset.all();
var format = $("body").attr("data-longdateformat") + " (dddd)";
$("#rrule-preview").html("");
if (all_dates.length > 20) {
$("#rrule-preview").html("");
all_dates.slice(0, 10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
$("#rrule-preview").append($("<li>").text(ngettext(
"(one more date)",
"({num} more dates)",
all_dates.length - 20
).replace(/\{num\}/g, all_dates.length - 20)));
all_dates.slice(-10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
} else {
all_dates.forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
}
}
function rrule_form_toggles($form) {
var freq = $form.find("select[name*=freq]").val();
$form.find(".repeat-yearly").toggle(freq === "yearly");
$form.find(".repeat-monthly").toggle(freq === "monthly");
$form.find(".repeat-weekly").toggle(freq === "weekly");
}
function rrule_bind_form($form) {
$form.find("select[name*=freq]").change(function () {
rrule_form_toggles($form);
});
rrule_form_toggles($form);
}
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
rrule_preview();
});
rrule_preview();
$(".rrule-form").each(function () { rrule_bind_form($(this)); });
$("#rrule-formset").on("formAdded", "div", function (event) { rrule_bind_form($(event.target)); });
var $namef = $("input[id^=id_name]").first();
var lastValue = $namef.val();
$namef.change(function () {

View File

@@ -296,3 +296,44 @@ table td > .checkbox input[type="checkbox"] {
width: 100px;
display: inline;
}
.form-horizontal [data-formset] .rrule-form .form-group {
margin: 0;
width: auto;
}
.rrule-form {
.form-control {
display: inline;
}
input[type=number] {
width: 100px;
}
.repeat-yearly, .repeat-monthly, .repeat-weekly {
line-height: 35px;
padding: 0 0 15px 0;
margin: 10px 0;
border-top: 1px solid $panel-default-border;
border-bottom: 1px solid $panel-default-border;
input[type="radio"], input[type="checkbox"] {
top: 2px;
}
}
.repeat-weekly > div.form-group {
display: block;
line-height: 1;
padding-top: 5px;
& > div {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: space-between;
div.checkbox {
flex-grow: 1;
}
}
}
.repeat-until {
line-height: 40px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -852,6 +852,327 @@ class SubEventsTest(SoupTest):
assert doc.select(".alert-danger")
assert self.event1.subevents.filter(pk=self.subevent1.pk).exists()
def test_create_bulk(self):
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': '1',
'rruleformset-0-freq': 'yearly',
'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': 'count',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'name_0': 'Foo',
'active': 'on',
'time_from': '13:29:31',
'time_to': '15:29:31',
'location_0': 'Loc',
'time_admission': '',
'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-id': '',
'quotas-0-name': 'Bar',
'quotas-0-size': '12',
'quotas-0-itemvars': str(self.ticket.pk),
'item-%d-price' % self.ticket.pk: '16',
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-id': '',
'checkinlist_set-0-name': 'Foo',
'checkinlist_set-0-limit_products': str(self.ticket.pk),
})
assert doc.select(".alert-success")
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 10
assert str(ses[0].name) == "Foo"
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[0].date_to.isoformat() == "2018-04-03T13:29:31+00:00"
assert not ses[0].presale_start
assert ses[0].presale_end.isoformat() == "2018-04-02T11:29:31+00:00"
assert ses[0].quotas.count() == 1
assert list(ses[0].quotas.first().items.all()) == [self.ticket]
assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16
assert ses[0].checkinlist_set.count() == 1
assert str(ses[1].name) == "Foo"
assert ses[1].date_from.isoformat() == "2019-04-03T11:29:31+00:00"
assert ses[1].date_to.isoformat() == "2019-04-03T13:29:31+00:00"
assert not ses[1].presale_start
assert ses[1].presale_end.isoformat() == "2019-04-02T11:29:31+00:00"
assert ses[1].quotas.count() == 1
assert list(ses[1].quotas.first().items.all()) == [self.ticket]
assert SubEventItem.objects.get(subevent=ses[0], item=self.ticket).price == 16
assert ses[1].checkinlist_set.count() == 1
assert ses[-1].date_from.isoformat() == "2027-04-03T11:29:31+00:00"
def test_create_bulk_daily_interval(self):
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',
'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': '',
'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': '0',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'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")
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 183
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
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_exclude(self):
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': '2',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-interval': '1',
'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',
'rruleformset-1-interval': '1',
'rruleformset-1-freq': 'weekly',
'rruleformset-1-dtstart': '2018-04-03',
'rruleformset-1-yearly_same': 'on',
'rruleformset-1-yearly_bysetpos': '1',
'rruleformset-1-yearly_byweekday': 'MO',
'rruleformset-1-yearly_bymonth': '1',
'rruleformset-1-monthly_same': 'on',
'rruleformset-1-monthly_bysetpos': '1',
'rruleformset-1-monthly_byweekday': 'MO',
'rruleformset-1-weekly_byweekday': 'MO',
'rruleformset-1-end': 'until',
'rruleformset-1-count': '10',
'rruleformset-1-until': '2019-04-03',
'rruleformset-1-exclude': 'on',
'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': '',
'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': '0',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'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")
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 314
assert ses[0].date_from.isoformat() == "2018-04-03T11:29:31+00:00"
assert ses[5].date_from.isoformat() == "2018-04-08T11:29:31+00:00"
assert ses[6].date_from.isoformat() == "2018-04-10T11:29:31+00:00"
def test_create_bulk_monthly_interval(self):
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
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': '1',
'rruleformset-0-freq': 'monthly',
'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': 'off',
'rruleformset-0-monthly_bysetpos': '-1',
'rruleformset-0-monthly_byweekday': 'MO,TU,WE,TH,FR',
'rruleformset-0-weekly_byweekday': 'TH',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'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': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '0',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'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")
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 12
assert ses[0].date_from.isoformat() == "2018-04-30T11:29:31+00:00"
assert ses[1].date_from.isoformat() == "2018-05-31T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-29T12:29:31+00:00"
def test_create_bulk_weekly_interval(self):
self.event1.subevents.all().delete()
self.event1.settings.timezone = 'Europe/Berlin'
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': '1',
'rruleformset-0-freq': 'weekly',
'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,TU,WE,TH,FR',
'rruleformset-0-weekly_byweekday': 'TH',
'rruleformset-0-end': 'until',
'rruleformset-0-count': '10',
'rruleformset-0-until': '2019-04-03',
'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': '',
'rel_presale_start_2': '1',
'rel_presale_start_3': 'date_from',
'rel_presale_start_4': '',
'rel_presale_end_0': 'unset',
'rel_presale_end_1': '',
'rel_presale_end_2': '1',
'rel_presale_end_3': 'date_from',
'rel_presale_end_4': '13:29:31',
'quotas-TOTAL_FORMS': '0',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'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")
ses = list(self.event1.subevents.order_by('date_from'))
assert len(ses) == 52
assert ses[0].date_from.isoformat() == "2018-04-05T11:29:31+00:00"
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
class EventDeletionTest(SoupTest):
def setUp(self):