diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py
index fbca154f31..19db3f309d 100644
--- a/src/pretix/control/forms/filter.py
+++ b/src/pretix/control/forms/filter.py
@@ -1,4 +1,4 @@
-from datetime import datetime, time
+from datetime import datetime, time, timedelta
from decimal import Decimal
from urllib.parse import urlencode
@@ -766,10 +766,15 @@ class SubEventFilterForm(FilterForm):
),
required=False
)
- date = forms.DateField(
- label=_('Date'),
+ date_from = forms.DateField(
+ label=_('Date from'),
required=False,
- widget=DatePickerWidget
+ widget=DatePickerWidget,
+ )
+ date_until = forms.DateField(
+ label=_('Date until'),
+ required=False,
+ widget=DatePickerWidget,
)
weekday = forms.ChoiceField(
label=_('Weekday'),
@@ -796,7 +801,8 @@ class SubEventFilterForm(FilterForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.fields['date'].widget = DatePickerWidget()
+ self.fields['date_from'].widget = DatePickerWidget()
+ self.fields['date_until'].widget = DatePickerWidget()
def filter_qs(self, qs):
fdata = self.cleaned_data
@@ -838,19 +844,21 @@ class SubEventFilterForm(FilterForm):
Q(name__icontains=i18ncomp(query)) | Q(location__icontains=query)
)
- if fdata.get('date'):
- date_start = make_aware(datetime.combine(
- fdata.get('date'),
+ if fdata.get('date_until'):
+ date_end = make_aware(datetime.combine(
+ fdata.get('date_until') + timedelta(days=1),
time(hour=0, minute=0, second=0, microsecond=0)
), get_current_timezone())
- date_end = make_aware(datetime.combine(
- fdata.get('date'),
- time(hour=23, minute=59, second=59, microsecond=999999)
- ), get_current_timezone())
qs = qs.filter(
- Q(date_to__isnull=True, date_from__gte=date_start, date_from__lte=date_end) |
- Q(date_to__isnull=False, date_from__lte=date_end, date_to__gte=date_start)
+ Q(date_to__isnull=True, date_from__lt=date_end) |
+ Q(date_to__isnull=False, date_to__lt=date_end)
)
+ if fdata.get('date_from'):
+ date_start = make_aware(datetime.combine(
+ fdata.get('date_from'),
+ time(hour=0, minute=0, second=0, microsecond=0)
+ ), get_current_timezone())
+ qs = qs.filter(date_from__gte=date_start)
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
diff --git a/src/pretix/control/forms/renderers.py b/src/pretix/control/forms/renderers.py
index 7b7edc2272..41a55bdbe4 100644
--- a/src/pretix/control/forms/renderers.py
+++ b/src/pretix/control/forms/renderers.py
@@ -1,4 +1,4 @@
-from bootstrap3.renderers import FieldRenderer
+from bootstrap3.renderers import FieldRenderer, InlineFieldRenderer
from bootstrap3.text import text_value
from django.forms import CheckboxInput
from django.forms.utils import flatatt
@@ -58,3 +58,40 @@ class ControlFieldRenderer(FieldRenderer):
optional=not required and not isinstance(self.widget, CheckboxInput)
) + html
return html
+
+
+class BulkEditMixin:
+
+ def __init__(self, *args, **kwargs):
+ kwargs['layout'] = self.layout
+ super().__init__(*args, **kwargs)
+
+ def wrap_field(self, html):
+ field_class = self.get_field_class()
+ name = '{}{}'.format(self.field.form.prefix, self.field.name)
+ checked = self.field.form.data and name in self.field.form.data.getlist('_bulk')
+ html = (
+ '
'
+ '
'
+ '
'
+ '{html}'
+ '
'
+ '
'
+ ).format(
+ klass=field_class or '',
+ name=name,
+ label=pgettext('form_bulk', 'change'),
+ checked='checked' if checked else '',
+ html=html
+ )
+ return html
+
+
+class BulkEditFieldRenderer(BulkEditMixin, FieldRenderer):
+ layout = 'horizontal'
+
+
+class InlineBulkEditFieldRenderer(BulkEditMixin, InlineFieldRenderer):
+ layout = 'inline'
diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py
index 1bbcb7ac44..d06439202b 100644
--- a/src/pretix/control/forms/subevents.py
+++ b/src/pretix/control/forms/subevents.py
@@ -1,8 +1,9 @@
-from datetime import timedelta
+from datetime import datetime, timedelta
from urllib.parse import urlencode
from django import forms
from django.forms import formset_factory
+from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.dates import MONTHS, WEEKDAYS
from django.utils.functional import cached_property
@@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
+from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
@@ -88,6 +90,142 @@ class SubEventBulkForm(SubEventForm):
del self.fields['date_admission']
+class NullBooleanSelect(forms.NullBooleanSelect):
+ def __init__(self, attrs=None):
+ choices = (
+ ('unknown', _('Keep the current values')),
+ ('true', _('Yes')),
+ ('false', _('No')),
+ )
+ super(forms.NullBooleanSelect, self).__init__(attrs, choices)
+
+
+class SubEventBulkEditForm(I18nModelForm):
+ def __init__(self, *args, **kwargs):
+ self.mixed_values = kwargs.pop('mixed_values')
+ self.queryset = kwargs.pop('queryset')
+ super().__init__(*args, **kwargs)
+ self.fields['location'].widget.attrs['rows'] = '3'
+
+ for k in ('name', 'location', 'frontpage_text'):
+ # i18n fields
+ if k in self.mixed_values:
+ self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
+ else:
+ self.fields[k].widget.attrs['placeholder'] = ''
+ self.fields[k].one_required = False
+
+ for k in ('geo_lat', 'geo_lon'):
+ # scalar fields
+ if k in self.mixed_values:
+ self.fields[k].widget.attrs['placeholder'] = '[{}]'.format(_('Selection contains various values'))
+ else:
+ self.fields[k].widget.attrs['placeholder'] = ''
+ self.fields[k].widget.is_required = False
+ self.fields[k].required = False
+
+ for k in ('date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end'):
+ self.fields[k + '_day'] = forms.DateField(
+ label=self._meta.model._meta.get_field(k).verbose_name,
+ help_text=self._meta.model._meta.get_field(k).help_text,
+ widget=DatePickerWidget(),
+ required=False,
+ )
+ self.fields[k + '_time'] = forms.TimeField(
+ label=self._meta.model._meta.get_field(k).verbose_name,
+ help_text=self._meta.model._meta.get_field(k).help_text,
+ widget=TimePickerWidget(),
+ required=False,
+ )
+
+ class Meta:
+ model = SubEvent
+ localized_fields = '__all__'
+ fields = [
+ 'name',
+ 'location',
+ 'frontpage_text',
+ 'geo_lat',
+ 'geo_lon',
+ 'is_public',
+ 'active',
+ ]
+ field_classes = {
+ }
+ widgets = {
+ }
+
+ def save(self, commit=True):
+ objs = list(self.queryset)
+ fields = set()
+
+ check_map = {
+ 'geo_lat': '__geo',
+ 'geo_lon': '__geo',
+ }
+ for k in self.fields:
+ cb_val = self.prefix + check_map.get(k, k)
+ if cb_val not in self.data.getlist('_bulk'):
+ continue
+
+ if k.endswith('_day'):
+ for obj in objs:
+ oldval = getattr(obj, k.replace('_day', ''))
+ cval = self.cleaned_data[k]
+ if cval is None:
+ newval = None
+ if not self._meta.model._meta.get_field(k.replace('_day', '')).null:
+ continue
+ elif oldval:
+ oldval = oldval.astimezone(self.event.timezone)
+ newval = oldval.replace(
+ year=cval.year,
+ month=cval.month,
+ day=cval.day,
+ )
+ else:
+ # If there is no previous date/time set, we'll just set to midnight
+ # If the user also selected a time, this will be overridden anyways
+ newval = datetime(
+ year=cval.year,
+ month=cval.month,
+ day=cval.day,
+ tzinfo=self.event.timezone
+ )
+ setattr(obj, k.replace('_day', ''), newval)
+ fields.add(k.replace('_day', ''))
+ elif k.endswith('_time'):
+ for obj in objs:
+ # If there is no previous date/time set and only a time is changed not the
+ # date, we instead use the date of the event
+ oldval = getattr(obj, k.replace('_time', '')) or obj.date_from
+ cval = self.cleaned_data[k]
+ if cval is None:
+ continue
+ oldval = oldval.astimezone(self.event.timezone)
+ newval = oldval.replace(
+ hour=cval.hour,
+ minute=cval.minute,
+ second=cval.second,
+ )
+ setattr(obj, k.replace('_time', ''), newval)
+ fields.add(k.replace('_time', ''))
+ else:
+ fields.add(k)
+ for obj in objs:
+ setattr(obj, k, self.cleaned_data[k])
+
+ if fields:
+ SubEvent.objects.bulk_update(objs, fields, 200)
+
+ def full_clean(self):
+ if len(self.data) == 0:
+ # form wasn't submitted
+ self._errors = ErrorDict()
+ return
+ super().full_clean()
+
+
class SubEventItemOrVariationFormMixin:
def __init__(self, *args, **kwargs):
self.item = kwargs.pop('item')
@@ -162,7 +300,7 @@ class SubEventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
self.default = kwargs.pop('default', None)
- self.disabled = kwargs.pop('disabled')
+ self.disabled = kwargs.pop('disabled', False)
super().__init__(*args, **kwargs)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
diff --git a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html
index 8ae010e9e8..1d384b7408 100644
--- a/src/pretix/control/templates/pretixcontrol/order/refund_choose.html
+++ b/src/pretix/control/templates/pretixcontrol/order/refund_choose.html
@@ -15,7 +15,8 @@
{% endblocktrans %}
-