mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Subevents: Bulk editor (#1918)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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 = (
|
||||
'<div class="{klass} bulk-edit-field-group">'
|
||||
'<label class="field-toggle">'
|
||||
'<input type="checkbox" name="_bulk" value="{name}" {checked}> {label}'
|
||||
'</label>'
|
||||
'<div class="field-content">'
|
||||
'{html}'
|
||||
'</div>'
|
||||
'</div>'
|
||||
).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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user