From e076f4bbd9b6e10c6c6d361b0dc6c6dd2ea49778 Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Mon, 8 Dec 2025 16:20:20 +0100 Subject: [PATCH] factor out subevent.py --- src/pretix/base/subevent.py | 188 ++++++++++++++++++++++++++++++ src/pretix/control/forms/item.py | 190 +------------------------------ src/pretix/control/views/item.py | 2 +- 3 files changed, 195 insertions(+), 185 deletions(-) create mode 100644 src/pretix/base/subevent.py diff --git a/src/pretix/base/subevent.py b/src/pretix/base/subevent.py new file mode 100644 index 000000000..d71bed185 --- /dev/null +++ b/src/pretix/base/subevent.py @@ -0,0 +1,188 @@ +from ast import literal_eval +from collections import namedtuple +from datetime import datetime +from typing import Union + +from django import forms +from django.core.exceptions import ValidationError +from django.urls import reverse +from django.utils.translation import gettext_lazy as _, pgettext_lazy + +from pretix.base.forms.widgets import SplitDateTimePickerWidget +from pretix.base.models import Event +from pretix.control.forms import SplitDateTimeField +from pretix.control.forms.widgets import Select2 + +SubEventSelection = namedtuple( + typename='SubEventSelection', + field_names=['selection', 'subevents', 'start', 'end', ], + defaults=['subevent', None, None, None], +) + + +subeventselectionparts = namedtuple( + typename='subeventselectionparts', + field_names=['selection', 'subevents', 'start', 'end'] +) + + +class SubEventSelectionWrapper: + def __init__(self, data: Union[None, SubEventSelection]): + self.data = data + + def get_queryset(self, event: Event): + if self.data.selection == 'subevent': + if self.data.subevents is None: + return event.subevents.all() + else: + return event.subevents.filter(pk=self.data.subevents) + elif self.data.selection == 'timerange': + if self.data.start and self.data.end: + return event.subevents.filter(date_from__lte=self.data.start, + date_from__gte=self.data.end) + elif self.data.start: + return event.subevents.filter(date_from__gte=self.data.start) + elif self.data.end: + return event.subevents.filter(date_from__lte=self.data.end) + return event.subevents.all() + + def to_string(self) -> str: + if self.data: + if self.data.selection == 'subevent': + return 'SUBEVENT/pk/{}'.format(self.data.subevents.pk) + elif self.data.selection == 'timerange': + if self.data.start and self.data.end: + return 'SUBEVENT/range/{}/{}'.format(self.data.start.isoformat(), self.data.end.isoformat()) + elif self.data.start: + return 'SUBEVENT/from/{}'.format(self.data.start) + elif self.data.end: + return 'SUBEVENT/to/{}'.format(self.data.end) + return 'SUBEVENT' + + @classmethod + def from_string(cls, input: str): + data = SubEventSelection(selection='subevent') + + if input.startswith('SUBEVENT'): + parts = input.split('/') + if len(parts) == 1: + data = SubEventSelection(selection='subevent') + elif parts[1] == 'pk': + data = SubEventSelection( + selection='subevent', + subevents=literal_eval(parts[2]) + ) + elif parts[1] == 'range': + data = SubEventSelection( + selection="timerange", + start=datetime.fromisoformat(parts[2]), + end=datetime.fromisoformat(parts[3]), + ) + elif parts[1] == 'from': + data = SubEventSelection( + selection="timerange", + start=datetime.fromisoformat(parts[2]), + ) + elif parts[1] == 'to': + data = SubEventSelection( + selection="timerange", + end=datetime.fromisoformat(parts[3]), + ) + return SubEventSelectionWrapper( + data=data + ) + + +class SubeventSelectionWidget(forms.MultiWidget): + template_name = 'pretixcontrol/forms/widgets/subeventselection.html' + parts = SubEventSelection + + def __init__(self, event: Event, status_choices, subevent_choices, *args, **kwargs): + widgets = subeventselectionparts( + selection=forms.RadioSelect( + choices=status_choices, + + ), + subevents=Select2( + attrs={ + 'class': 'simple-subevent-choice', + 'data-model-select2': 'event', + 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ + 'event': event.slug, + 'organizer': event.organizer.slug, + }), + 'data-placeholder': pgettext_lazy('subevent', 'All dates') + }, + ), + start=SplitDateTimePickerWidget(), + end=SplitDateTimePickerWidget(), + + ) + widgets.subevents.choices = subevent_choices + super().__init__(widgets=widgets, *args, **kwargs) + + def decompress(self, value): + + if isinstance(value, str): + value = SubEventSelectionWrapper.from_string(value) + if isinstance(value, subeventselectionparts): + return value + + return subeventselectionparts(selection='subevent', start=None, end=None, subevents=None) + + +class SubeventSelectionField(forms.MultiValueField): + widget = SubeventSelectionWidget + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + + choices = [ + ("subevent", _("Subevent")), + ("timerange", _("Timerange")) + ] + + fields = SubEventSelection( + selection=forms.ChoiceField( + choices=choices, + required=True, + ), + subevents=forms.ModelChoiceField( + required=False, + queryset=self.event.subevents, + empty_label=pgettext_lazy('subevent', 'All dates') + ), + start=SplitDateTimeField( + required=False, + ), + end=SplitDateTimeField( + required=False, + ), + ) + + kwargs['widget'] = SubeventSelectionWidget( + event=self.event, + status_choices=choices, + subevent_choices=fields.subevents.widget.choices, + ) + + super().__init__( + fields=fields, require_all_fields=False, *args, **kwargs + ) + + def compress(self, data_list): + if not data_list: + return None + return SubEventSelectionWrapper(data=SubEventSelection(*data_list)).to_string() + + def clean(self, value): + data = subeventselectionparts(*value) + + if data.selection == "timerange": + if (data.start != ["", ""] and data.end != ["", ""]) and data.end < data.start: + raise ValidationError(_("The end date must be after the start date.")) + + if (data.start == ["", ""]) and (data.end == ["", ""]): + raise ValidationError(_('At least one of start and end must be specified.')) + + return super().clean(value) diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 7eccb55da..568052c7b 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -33,12 +33,8 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. import copy -import datetime import os -from ast import literal_eval -from collections import namedtuple from decimal import Decimal -from typing import Union from urllib.parse import urlencode from django import forms @@ -52,9 +48,7 @@ from django.utils.functional import cached_property from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.timezone import now -from django.utils.translation import ( - gettext as __, gettext_lazy as _, pgettext_lazy, -) +from django.utils.translation import gettext as __, gettext_lazy as _ from django_scopes.forms import ( SafeModelChoiceField, SafeModelMultipleChoiceField, ) @@ -63,11 +57,14 @@ from i18nfield.forms import I18nFormField, I18nTextarea from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm from pretix.base.forms.widgets import DatePickerWidget from pretix.base.models import ( - Event, Item, ItemCategory, ItemProgramTime, ItemVariation, Order, - OrderPosition, Question, QuestionOption, Quota + Item, ItemCategory, ItemProgramTime, ItemVariation, Order, OrderPosition, + Question, QuestionOption, Quota, ) from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue from pretix.base.signals import item_copy_data +from pretix.base.subevent import ( + SubeventSelectionField, SubEventSelectionWrapper, +) from pretix.control.forms import ( ButtonGroupRadioSelect, ExtFileField, ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple, SplitDateTimeField, @@ -279,181 +276,6 @@ class QuestionOptionForm(I18nModelForm): ] -SubEventSelection = namedtuple( - typename='SubEventSelection', - field_names=['selection', 'subevents', 'start', 'end', ], - defaults=['subevent', None, None, None], -) - - -subeventselectionparts = namedtuple( - typename='subeventselectionparts', - field_names=['selection', 'subevents', 'start', 'end'] -) - - -class SubEventSelectionWrapper: - def __init__(self, data: Union[None, SubEventSelection]): - self.data = data - - def get_queryset(self, event: Event): - if self.data.selection == 'subevent': - if self.data.subevents is None: - return event.subevents.all() - else: - return event.subevents.filter(pk=self.data.subevents) - elif self.data.selection == 'timerange': - if self.data.start and self.data.end: - return event.subevents.filter(date_from__lte=self.data.start, - date_from__gte=self.data.end) - elif self.data.start: - return event.subevents.filter(date_from__gte=self.data.start) - elif self.data.end: - return event.subevents.filter(date_from__lte=self.data.end) - return event.subevents.all() - - def to_string(self) -> str: - if self.data: - if self.data.selection == 'subevent': - return 'SUBEVENT/pk/{}'.format(self.data.subevents.pk) - elif self.data.selection == 'timerange': - if self.data.start and self.data.end: - return 'SUBEVENT/range/{}/{}'.format(self.data.start.isoformat(), self.data.end.isoformat()) - elif self.data.start: - return 'SUBEVENT/from/{}'.format(self.data.start) - elif self.data.end: - return 'SUBEVENT/to/{}'.format(self.data.end) - return 'SUBEVENT' - - @classmethod - def from_string(cls, input: str): - data = SubEventSelection(selection='subevent') - - if input.startswith('SUBEVENT'): - parts = input.split('/') - if len(parts) == 1: - data = SubEventSelection(selection='subevent') - elif parts[1] == 'pk': - data = SubEventSelection( - selection='subevent', - subevents=literal_eval(parts[2]) - ) - elif parts[1] == 'range': - data = SubEventSelection( - selection="timerange", - start=datetime.datetime.fromisoformat(parts[2]), - end=datetime.datetime.fromisoformat(parts[3]), - ) - elif parts[1] == 'from': - data = SubEventSelection( - selection="timerange", - start=datetime.datetime.fromisoformat(parts[2]), - ) - elif parts[1] == 'to': - data = SubEventSelection( - selection="timerange", - end=datetime.datetime.fromisoformat(parts[3]), - ) - return SubEventSelectionWrapper( - data=data - ) - - -class SubeventSelectionWidget(forms.MultiWidget): - template_name = 'pretixcontrol/forms/widgets/subeventselection.html' - parts = SubEventSelection - - def __init__(self, event: Event, status_choices, subevent_choices, *args, **kwargs): - widgets = subeventselectionparts( - selection=forms.RadioSelect( - choices=status_choices, - - ), - subevents=Select2( - attrs={ - 'class': 'simple-subevent-choice', - 'data-model-select2': 'event', - 'data-select2-url': reverse('control:event.subevents.select2', kwargs={ - 'event': event.slug, - 'organizer': event.organizer.slug, - }), - 'data-placeholder': pgettext_lazy('subevent', 'All dates') - }, - ), - start=SplitDateTimePickerWidget(), - end=SplitDateTimePickerWidget(), - - ) - widgets.subevents.choices = subevent_choices - super().__init__(widgets=widgets, *args, **kwargs) - - def decompress(self, value): - - if isinstance(value, str): - value = SubEventSelectionWrapper.from_string(value) - if isinstance(value, subeventselectionparts): - return value - - return subeventselectionparts(selection='subevent', start=None, end=None, subevents=None) - - -class SubeventSelectionField(forms.MultiValueField): - widget = SubeventSelectionWidget - - def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event') - - choices = [ - ("subevent", _("Subevent")), - ("timerange", _("Timerange")) - ] - - fields = SubEventSelection( - selection=forms.ChoiceField( - choices=choices, - required=True, - ), - subevents=forms.ModelChoiceField( - required=False, - queryset=self.event.subevents, - empty_label=pgettext_lazy('subevent', 'All dates') - ), - start=SplitDateTimeField( - required=False, - ), - end=SplitDateTimeField( - required=False, - ), - ) - - kwargs['widget'] = SubeventSelectionWidget( - event=self.event, - status_choices=choices, - subevent_choices=fields.subevents.widget.choices, - ) - - super().__init__( - fields=fields, require_all_fields=False, *args, **kwargs - ) - - def compress(self, data_list): - if not data_list: - return None - return SubEventSelectionWrapper(data=SubEventSelection(*data_list)).to_string() - - def clean(self, value): - data = subeventselectionparts(*value) - - if data.selection == "timerange": - if (data.start != ["", ""] and data.end != ["", ""]) and data.end < data.start: - raise ValidationError(_("The end date must be after the start date.")) - - if (data.start == ["", ""]) and (data.end == ["", ""]): - raise ValidationError(_('At least one of start and end must be specified.')) - - return super().clean(value) - - class QuestionFilterForm(forms.Form): STATUS_VARIANTS = [ ("", _("All orders")), diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 1b6ccdbbe..716b36207 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -82,7 +82,7 @@ from pretix.control.forms.item import ( ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm, ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm, ItemVariationsFormSet, QuestionFilterForm, QuestionForm, - QuestionOptionForm, QuotaForm + QuestionOptionForm, QuotaForm, ) from pretix.control.permissions import ( EventPermissionRequiredMixin, event_permission_required,