From 89f1f61b730fdd4f93b73a4304c84e8c70f056cb Mon Sep 17 00:00:00 2001 From: Mira Date: Thu, 16 Jan 2025 11:32:02 +0100 Subject: [PATCH] Refactor RelativeDate(Time)Field and -Widget (#4746) * refactor to use namedtuples for the sub-fields and sub-widgets * fix RelativeDateTimeField.set_event: apply relative_to filter not only to minutes, but to days as well * fix bug in RelativeDateTimeField.clean: validate days relation_to instead of minutes relation_to when "Relative date" is selected --------- Co-authored-by: Raphael Michel --- src/pretix/base/reldate.py | 232 +++++++++++++----- .../pretixbase/forms/widgets/reldate.html | 4 +- .../pretixbase/forms/widgets/reldatetime.html | 6 +- 3 files changed, 170 insertions(+), 72 deletions(-) diff --git a/src/pretix/base/reldate.py b/src/pretix/base/reldate.py index 815d1884c9..aeb9cd703f 100644 --- a/src/pretix/base/reldate.py +++ b/src/pretix/base/reldate.py @@ -185,48 +185,103 @@ BEFORE_AFTER_CHOICE = ( ) +reldatetimeparts = namedtuple('reldatetimeparts', ( + "status", # 0 + "absolute", # 1 + "rel_days_number", # 2 + "rel_mins_relationto", # 3 + "rel_days_timeofday", # 4 + "rel_mins_number", # 5 + "rel_days_relationto", # 6 + "rel_mins_relation", # 7 + "rel_days_relation" # 8 +)) +reldatetimeparts.indizes = reldatetimeparts(*range(9)) + + class RelativeDateTimeWidget(forms.MultiWidget): template_name = 'pretixbase/forms/widgets/reldatetime.html' + parts = reldatetimeparts def __init__(self, *args, **kwargs): self.status_choices = kwargs.pop('status_choices') base_choices = kwargs.pop('base_choices') - widgets = ( - forms.RadioSelect(choices=self.status_choices), - forms.DateTimeInput( + widgets = reldatetimeparts( + status=forms.RadioSelect(choices=self.status_choices), + absolute=forms.DateTimeInput( attrs={'class': 'datetimepicker'} ), - forms.NumberInput(), - forms.Select(choices=base_choices), - forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}), - forms.NumberInput(), - forms.Select(choices=base_choices), - forms.Select(choices=BEFORE_AFTER_CHOICE), - forms.Select(choices=BEFORE_AFTER_CHOICE), + rel_days_number=forms.NumberInput(), + rel_mins_relationto=forms.Select(choices=base_choices), + rel_days_timeofday=forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}), + rel_mins_number=forms.NumberInput(), + rel_days_relationto=forms.Select(choices=base_choices), + rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), + rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), ) super().__init__(widgets=widgets, *args, **kwargs) def decompress(self, value): if isinstance(value, str): value = RelativeDateWrapper.from_string(value) + if isinstance(value, reldatetimeparts): + return value if not value: - return ['unset', None, 1, 'date_from', None, 0, "date_from", "before", "before"] + return reldatetimeparts( + status="unset", + absolute=None, + rel_days_number=1, + rel_mins_relationto="date_from", + rel_days_timeofday=None, + rel_mins_number=0, + rel_days_relationto="date_from", + rel_mins_relation="before", + rel_days_relation="before" + ) elif isinstance(value.data, (datetime.datetime, datetime.date)): - return ['absolute', value.data, 1, 'date_from', None, 0, "date_from", "before", "before"] + return reldatetimeparts( + status="absolute", + absolute=value.data, + rel_days_number=1, + rel_mins_relationto="date_from", + rel_days_timeofday=None, + rel_mins_number=0, + rel_days_relationto="date_from", + rel_mins_relation="before", + rel_days_relation="before" + ) elif value.data.minutes is not None: - return ['relative_minutes', None, None, value.data.base_date_name, None, value.data.minutes, value.data.base_date_name, - "after" if value.data.is_after else "before", "after" if value.data.is_after else "before"] - return ['relative', None, value.data.days, value.data.base_date_name, value.data.time, 0, value.data.base_date_name, - "after" if value.data.is_after else "before", "after" if value.data.is_after else "before"] + return reldatetimeparts( + status="relative_minutes", + absolute=None, + rel_days_number=None, + rel_mins_relationto=value.data.base_date_name, + rel_days_timeofday=None, + rel_mins_number=value.data.minutes, + rel_days_relationto=value.data.base_date_name, + rel_mins_relation="after" if value.data.is_after else "before", + rel_days_relation="after" if value.data.is_after else "before" + ) + return reldatetimeparts( + status="relative", + absolute=None, + rel_days_number=value.data.days, + rel_mins_relationto=value.data.base_date_name, + rel_days_timeofday=value.data.time, + rel_mins_number=0, + rel_days_relationto=value.data.base_date_name, + rel_mins_relation="after" if value.data.is_after else "before", + rel_days_relation="after" if value.data.is_after else "before" + ) def get_context(self, name, value, attrs): ctx = super().get_context(name, value, attrs) ctx['required'] = self.status_choices[0][0] == 'unset' - ctx['rendered_subwidgets'] = [ + ctx['rendered_subwidgets'] = self.parts(*( self._render(w['template_name'], {**ctx, 'widget': w}) for w in ctx['widget']['subwidgets'] - ] + ))._asdict() return ctx @@ -245,36 +300,36 @@ class RelativeDateTimeField(forms.MultiValueField): choices = BASE_CHOICES if not kwargs.get('required', True): status_choices.insert(0, ('unset', _('Not set'))) - fields = ( - forms.ChoiceField( + fields = reldatetimeparts( + status=forms.ChoiceField( choices=status_choices, required=True ), - forms.DateTimeField( + absolute=forms.DateTimeField( required=False ), - forms.IntegerField( + rel_days_number=forms.IntegerField( required=False ), - forms.ChoiceField( + rel_mins_relationto=forms.ChoiceField( choices=choices, required=False ), - forms.TimeField( + rel_days_timeofday=forms.TimeField( required=False, ), - forms.IntegerField( + rel_mins_number=forms.IntegerField( required=False ), - forms.ChoiceField( + rel_days_relationto=forms.ChoiceField( choices=choices, required=False ), - forms.ChoiceField( + rel_mins_relation=forms.ChoiceField( choices=BEFORE_AFTER_CHOICE, required=False ), - forms.ChoiceField( + rel_days_relation=forms.ChoiceField( choices=BEFORE_AFTER_CHOICE, required=False ), @@ -288,32 +343,36 @@ class RelativeDateTimeField(forms.MultiValueField): ) def set_event(self, event): - self.widget.widgets[3].choices = [ + self.widget.widgets[reldatetimeparts.indizes.rel_days_relationto].choices = [ + (k, v) for k, v in BASE_CHOICES if getattr(event, k, None) + ] + self.widget.widgets[reldatetimeparts.indizes.rel_mins_relationto].choices = [ (k, v) for k, v in BASE_CHOICES if getattr(event, k, None) ] def compress(self, data_list): if not data_list: return None - if data_list[0] == 'absolute': - return RelativeDateWrapper(data_list[1]) - elif data_list[0] == 'unset': + data = reldatetimeparts(*data_list) + if data.status == 'absolute': + return RelativeDateWrapper(data.absolute) + elif data.status == 'unset': return None - elif data_list[0] == 'relative_minutes': + elif data.status == 'relative_minutes': return RelativeDateWrapper(RelativeDate( days=0, - base_date_name=data_list[3], + base_date_name=data.rel_mins_relationto, time=None, - minutes=data_list[5], - is_after=data_list[7] == "after", + minutes=data.rel_mins_number, + is_after=data.rel_mins_relation == "after", )) else: return RelativeDateWrapper(RelativeDate( - days=data_list[2], - base_date_name=data_list[6], - time=data_list[4], + days=data.rel_days_number, + base_date_name=data.rel_days_relationto, + time=data.rel_days_timeofday, minutes=None, - is_after=data_list[8] == "after", + is_after=data.rel_days_relation == "after", )) def has_changed(self, initial, data): @@ -322,29 +381,41 @@ class RelativeDateTimeField(forms.MultiValueField): return super().has_changed(initial, data) def clean(self, value): - if value[0] == 'absolute' and not value[1]: + data = reldatetimeparts(*value) + if data.status == 'absolute' and not data.absolute: raise ValidationError(self.error_messages['incomplete']) - elif value[0] == 'relative' and (value[2] is None or not value[3]): + elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto): raise ValidationError(self.error_messages['incomplete']) - elif value[0] == 'relative_minutes' and (value[5] is None or not value[3]): + elif data.status == 'relative_minutes' and (data.rel_mins_number is None or not data.rel_mins_relationto): raise ValidationError(self.error_messages['incomplete']) return super().clean(value) +reldateparts = namedtuple('reldateparts', ( + "status", # 0 + "absolute", # 1 + "rel_days_number", # 2 + "rel_days_relationto", # 3 + "rel_days_relation", # 4 +)) +reldateparts.indizes = reldateparts(*range(5)) + + class RelativeDateWidget(RelativeDateTimeWidget): template_name = 'pretixbase/forms/widgets/reldate.html' + parts = reldateparts def __init__(self, *args, **kwargs): self.status_choices = kwargs.pop('status_choices') - widgets = ( - forms.RadioSelect(choices=self.status_choices), - forms.DateInput( + widgets = reldateparts( + status=forms.RadioSelect(choices=self.status_choices), + absolute=forms.DateInput( attrs={'class': 'datepickerfield'} ), - forms.NumberInput(), - forms.Select(choices=kwargs.pop('base_choices')), - forms.Select(choices=BEFORE_AFTER_CHOICE), + rel_days_number=forms.NumberInput(), + rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')), + rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE), ) forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs) @@ -352,10 +423,30 @@ class RelativeDateWidget(RelativeDateTimeWidget): if isinstance(value, str): value = RelativeDateWrapper.from_string(value) if not value: - return ['unset', None, 1, 'date_from', 'before'] + return reldateparts( + status="unset", + absolute=None, + rel_days_number=1, + rel_days_relationto="date_from", + rel_days_relation="before" + ) + if isinstance(value, reldateparts): + return value elif isinstance(value.data, (datetime.datetime, datetime.date)): - return ['absolute', value.data, 1, 'date_from', 'before'] - return ['relative', None, value.data.days, value.data.base_date_name, "after" if value.data.is_after else "before"] + return reldateparts( + status="absolute", + absolute=value.data, + rel_days_number=1, + rel_days_relationto="date_from", + rel_days_relation="before" + ) + return reldateparts( + status="relative", + absolute=None, + rel_days_number=value.data.days, + rel_days_relationto=value.data.base_date_name, + rel_days_relation="after" if value.data.is_after else "before" + ) class RelativeDateField(RelativeDateTimeField): @@ -367,22 +458,22 @@ class RelativeDateField(RelativeDateTimeField): ] if not kwargs.get('required', True): status_choices.insert(0, ('unset', _('Not set'))) - fields = ( - forms.ChoiceField( + fields = reldateparts( + status=forms.ChoiceField( choices=status_choices, required=True ), - forms.DateField( + absolute=forms.DateField( required=False ), - forms.IntegerField( + rel_days_number=forms.IntegerField( required=False ), - forms.ChoiceField( + rel_days_relationto=forms.ChoiceField( choices=BASE_CHOICES, required=False ), - forms.ChoiceField( + rel_days_relation=forms.ChoiceField( choices=BEFORE_AFTER_CHOICE, required=False ), @@ -393,28 +484,35 @@ class RelativeDateField(RelativeDateTimeField): self, fields=fields, require_all_fields=False, *args, **kwargs ) + def set_event(self, event): + self.widget.widgets[reldateparts.indizes.rel_days_relationto].choices = [ + (k, v) for k, v in BASE_CHOICES if getattr(event, k, None) + ] + def compress(self, data_list): if not data_list: return None - if data_list[0] == 'absolute': - return RelativeDateWrapper(data_list[1]) - elif data_list[0] == 'unset': + data = reldateparts(*data_list) + if data.status == 'absolute': + return RelativeDateWrapper(data.absolute) + elif data.status == 'unset': return None else: return RelativeDateWrapper(RelativeDate( - days=data_list[2], - base_date_name=data_list[3], + days=data.rel_days_number, + base_date_name=data.rel_days_relationto, time=None, minutes=None, - is_after=data_list[4] == "after" + is_after=data.rel_days_relation == "after" )) def clean(self, value): - if value[0] == 'absolute' and not value[1]: + data = reldateparts(*value) + if data.status == 'absolute' and not data.absolute: raise ValidationError(self.error_messages['incomplete']) - elif value[0] == 'relative' and (value[2] is None or not value[3]): + elif data.status == 'relative' and (data.rel_days_number is None or not data.rel_days_relationto): raise ValidationError(self.error_messages['incomplete']) - return super().clean(value) + return forms.MultiValueField.clean(self, value) class ModelRelativeDateTimeField(models.CharField): diff --git a/src/pretix/base/templates/pretixbase/forms/widgets/reldate.html b/src/pretix/base/templates/pretixbase/forms/widgets/reldate.html index 2cf24d8e58..73b8ff7c3d 100644 --- a/src/pretix/base/templates/pretixbase/forms/widgets/reldate.html +++ b/src/pretix/base/templates/pretixbase/forms/widgets/reldate.html @@ -9,9 +9,9 @@ {{ selopt.label }} {% if selopt.value == "absolute" %} - {% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} + {{ rendered_subwidgets.absolute }} {% elif selopt.value == "relative" %} - {% blocktrans trimmed with number=rendered_subwidgets.2 relation=rendered_subwidgets.4 relation_to=rendered_subwidgets.3 %} + {% blocktrans trimmed with number=rendered_subwidgets.rel_days_number relation=rendered_subwidgets.rel_days_relation relation_to=rendered_subwidgets.rel_days_relationto %} {{ number }} days {{ relation }} {{ relation_to }} {% endblocktrans %} {% endif %} diff --git a/src/pretix/base/templates/pretixbase/forms/widgets/reldatetime.html b/src/pretix/base/templates/pretixbase/forms/widgets/reldatetime.html index 49879ae81e..3e618d0910 100644 --- a/src/pretix/base/templates/pretixbase/forms/widgets/reldatetime.html +++ b/src/pretix/base/templates/pretixbase/forms/widgets/reldatetime.html @@ -9,13 +9,13 @@ {{ selopt.label }} {% if selopt.value == "absolute" %} - {% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} + {{ rendered_subwidgets.absolute }} {% elif selopt.value == "relative_minutes" %} - {% blocktrans trimmed with number=rendered_subwidgets.5 relation=rendered_subwidgets.7 relation_to=rendered_subwidgets.3 %} + {% blocktrans trimmed with number=rendered_subwidgets.rel_mins_number relation=rendered_subwidgets.rel_mins_relation relation_to=rendered_subwidgets.rel_mins_relationto %} {{ number }} minutes {{ relation }} {{ relation_to }} {% endblocktrans %} {% elif selopt.value == "relative" %} - {% blocktrans trimmed with number=rendered_subwidgets.2 relation=rendered_subwidgets.8 relation_to=rendered_subwidgets.6 time_of_day=rendered_subwidgets.4 %} + {% blocktrans trimmed with number=rendered_subwidgets.rel_days_number relation=rendered_subwidgets.rel_days_relation relation_to=rendered_subwidgets.rel_days_relationto time_of_day=rendered_subwidgets.rel_days_timeofday %} {{ number }} days {{ relation }} {{ relation_to }} at {{ time_of_day }} {% endblocktrans %} {% endif %}