# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # from datetime import datetime from urllib.parse import urlencode from django import forms from django.core.exceptions import ValidationError from django.forms import formset_factory from django.forms.utils import ErrorDict from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ 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, SubEventItemVariation from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper from pretix.base.templatetags.money import money_filter from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget from pretix.control.forms.rrule import RRuleForm from pretix.helpers.money import change_decimal_field class SubEventForm(I18nModelForm): def __init__(self, *args, **kwargs): self.event = kwargs['event'] instance = kwargs.get('instance') if instance and not instance.name: kwargs['initial'].setdefault('name', self.event.name) kwargs['initial'].setdefault('location', self.event.location) kwargs['initial'].setdefault('geo_lat', self.event.geo_lat) kwargs['initial'].setdefault('geo_lon', self.event.geo_lon) super().__init__(*args, **kwargs) self.fields['location'].widget.attrs['rows'] = '3' class Meta: model = SubEvent localized_fields = '__all__' fields = [ 'name', 'active', 'is_public', 'date_from', 'date_to', 'date_admission', 'presale_start', 'presale_end', 'location', 'frontpage_text', 'geo_lat', 'geo_lon', ] field_classes = { 'date_from': SplitDateTimeField, 'date_to': SplitDateTimeField, 'date_admission': SplitDateTimeField, 'presale_start': SplitDateTimeField, 'presale_end': SplitDateTimeField, } widgets = { 'date_from': SplitDateTimePickerWidget(), 'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}), 'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}), 'presale_start': SplitDateTimePickerWidget(), 'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}), } class SubEventBulkForm(SubEventForm): 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 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') self.variation = kwargs.pop('variation', None) super().__init__(*args, **kwargs) change_decimal_field(self.fields['price'], self.item.event.currency) class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True) self.fields['price'].label = str(self.item) class Meta: model = SubEventItem fields = ['price', 'disabled', 'available_from', 'available_until'] widgets = { 'available_from': SplitDateTimePickerWidget(), 'available_until': SplitDateTimePickerWidget(), 'price': forms.TextInput } field_classes = { 'available_from': SplitDateTimeField, 'available_until': SplitDateTimeField, } def clean(self): d = super().clean() if d.get('available_from') and d.get('available_until'): if d.get('available_from') > d.get('available_until'): raise ValidationError(_('The end of availability should be after the start of availability.')) return d class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True) self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value) class Meta: model = SubEventItemVariation fields = ['price', 'disabled', 'available_from', 'available_until'] widgets = { 'available_from': SplitDateTimePickerWidget(), 'available_until': SplitDateTimePickerWidget(), 'price': forms.TextInput } field_classes = { 'available_from': SplitDateTimeField, 'available_until': SplitDateTimeField, } def clean(self): d = super().clean() if d.get('available_from') and d.get('available_until'): if d.get('available_from') > d.get('available_until'): raise ValidationError(_('The end of availability should be after the start of availability.')) return d class BulkSubEventItemForm(SubEventItemForm): rel_available_from = RelativeDateTimeField( label=_('Available from'), required=False, limit_choices=('date_from', 'date_to'), ) rel_available_until = RelativeDateTimeField( label=_('Available until'), required=False, limit_choices=('date_from', 'date_to'), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) del self.fields['available_from'] del self.fields['available_until'] if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial: self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from) if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial: self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until) class BulkSubEventItemVariationForm(SubEventItemVariationForm): rel_available_from = RelativeDateTimeField( label=_('Available from'), required=False, limit_choices=('date_from', 'date_to'), ) rel_available_until = RelativeDateTimeField( label=_('Available_until'), required=False, limit_choices=('date_from', 'date_to'), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) del self.fields['available_from'] del self.fields['available_until'] if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial: self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from) if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial: self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until) class QuotaFormSet(I18nInlineFormSet): def __init__(self, *args, **kwargs): self.event = kwargs.pop('event', None) self.locales = self.event.settings.get('locales') super().__init__(*args, **kwargs) @cached_property def items(self): return self.event.items.prefetch_related('variations').all() def _construct_form(self, i, **kwargs): kwargs['locales'] = self.locales kwargs['event'] = self.event kwargs['items'] = self.items kwargs['items'] = self.items return super()._construct_form(i, **kwargs) @property def empty_form(self): form = self.form( auto_id=self.auto_id, prefix=self.add_prefix('__prefix__'), empty_permitted=True, use_required_attribute=False, locales=self.locales, event=self.event, items=self.items ) self.add_fields(form, None) return form 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', False) super().__init__(*args, **kwargs) if self.property.allowed_values: self.fields['value'] = forms.ChoiceField( label=self.property.name, choices=[ ('', _('Default ({value})').format(value=self.default or self.property.default) if self.default or self.property.default else ''), ] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()], ) else: self.fields['value'].label = self.property.name self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default self.fields['value'].widget.attrs['data-typeahead-url'] = ( reverse('control:events.meta.typeahead') + '?' + urlencode({ 'property': self.property.name, 'organizer': self.property.organizer.slug, }) ) self.fields['value'].required = False if self.disabled: self.fields['value'].widget.attrs['readonly'] = 'readonly' def clean_slug(self): if self.disabled: return self.instance.value if self.instance else None return self.cleaned_data['slug'] class Meta: model = SubEventMetaValue fields = ['value'] widgets = { 'value': forms.TextInput } class CheckinListFormSet(I18nInlineFormSet): def __init__(self, *args, **kwargs): self.event = kwargs.pop('event', None) self.locales = self.event.settings.get('locales') super().__init__(*args, **kwargs) @cached_property def items(self): return self.event.items.prefetch_related('variations').all() def _construct_form(self, i, **kwargs): kwargs['event'] = self.event return super()._construct_form(i, **kwargs) @property def empty_form(self): form = self.form( auto_id=self.auto_id, prefix=self.add_prefix('__prefix__'), empty_permitted=True, use_required_attribute=False, event=self.event, ) self.add_fields(form, None) return form class RRuleFormSetForm(RRuleForm): exclude = forms.BooleanField( label=_('Exclude these dates instead of adding them.'), required=False ) RRuleFormSet = formset_factory( RRuleFormSetForm, 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', 'autocomplete': 'off'}), required=True ) time_to = forms.TimeField( label=_('Event end time'), widget=forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}), required=False ) time_admission = forms.TimeField( label=_('Admission time'), widget=forms.TimeInput(attrs={'class': 'timepickerfield', 'autocomplete': 'off'}), required=False ) TimeFormSet = formset_factory( TimeForm, min_num=1, can_order=False, can_delete=True, extra=1, validate_min=True )