Compare commits

...

4 Commits

Author SHA1 Message Date
Raphael Michel
e2fe2500ba Code style 2025-01-15 17:20:40 +01:00
Mira Weller
13aee2b3bb fix RelativeDateTimeField.set_event: apply relative_to filter not only to minutes, but to days as well 2025-01-14 13:49:08 +01:00
Mira Weller
ba9d0823c7 use namedtuples for the parts of RelDate[Time]{Field,Widget} 2025-01-14 13:49:08 +01:00
Mira Weller
87768a2b74 fix bug in RelativeDateTimeField.clean
validate days relation_to instead of minutes relation_to when "Relative date" is selected
2025-01-14 13:49:08 +01:00
3 changed files with 170 additions and 72 deletions

View File

@@ -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): class RelativeDateTimeWidget(forms.MultiWidget):
template_name = 'pretixbase/forms/widgets/reldatetime.html' template_name = 'pretixbase/forms/widgets/reldatetime.html'
parts = reldatetimeparts
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices') self.status_choices = kwargs.pop('status_choices')
base_choices = kwargs.pop('base_choices') base_choices = kwargs.pop('base_choices')
widgets = ( widgets = reldatetimeparts(
forms.RadioSelect(choices=self.status_choices), status=forms.RadioSelect(choices=self.status_choices),
forms.DateTimeInput( absolute=forms.DateTimeInput(
attrs={'class': 'datetimepicker'} attrs={'class': 'datetimepicker'}
), ),
forms.NumberInput(), rel_days_number=forms.NumberInput(),
forms.Select(choices=base_choices), rel_mins_relationto=forms.Select(choices=base_choices),
forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}), rel_days_timeofday=forms.TimeInput(attrs={'placeholder': _('Time'), 'class': 'timepickerfield'}),
forms.NumberInput(), rel_mins_number=forms.NumberInput(),
forms.Select(choices=base_choices), rel_days_relationto=forms.Select(choices=base_choices),
forms.Select(choices=BEFORE_AFTER_CHOICE), rel_mins_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
forms.Select(choices=BEFORE_AFTER_CHOICE), rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
) )
super().__init__(widgets=widgets, *args, **kwargs) super().__init__(widgets=widgets, *args, **kwargs)
def decompress(self, value): def decompress(self, value):
if isinstance(value, str): if isinstance(value, str):
value = RelativeDateWrapper.from_string(value) value = RelativeDateWrapper.from_string(value)
if isinstance(value, reldatetimeparts):
return value
if not 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)): 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: 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, return reldatetimeparts(
"after" if value.data.is_after else "before", "after" if value.data.is_after else "before"] status="relative_minutes",
return ['relative', None, value.data.days, value.data.base_date_name, value.data.time, 0, value.data.base_date_name, absolute=None,
"after" if value.data.is_after else "before", "after" if value.data.is_after else "before"] 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): def get_context(self, name, value, attrs):
ctx = super().get_context(name, value, attrs) ctx = super().get_context(name, value, attrs)
ctx['required'] = self.status_choices[0][0] == 'unset' ctx['required'] = self.status_choices[0][0] == 'unset'
ctx['rendered_subwidgets'] = [ ctx['rendered_subwidgets'] = self.parts(*(
self._render(w['template_name'], {**ctx, 'widget': w}) self._render(w['template_name'], {**ctx, 'widget': w})
for w in ctx['widget']['subwidgets'] for w in ctx['widget']['subwidgets']
] ))._asdict()
return ctx return ctx
@@ -245,36 +300,36 @@ class RelativeDateTimeField(forms.MultiValueField):
choices = BASE_CHOICES choices = BASE_CHOICES
if not kwargs.get('required', True): if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set'))) status_choices.insert(0, ('unset', _('Not set')))
fields = ( fields = reldatetimeparts(
forms.ChoiceField( status=forms.ChoiceField(
choices=status_choices, choices=status_choices,
required=True required=True
), ),
forms.DateTimeField( absolute=forms.DateTimeField(
required=False required=False
), ),
forms.IntegerField( rel_days_number=forms.IntegerField(
required=False required=False
), ),
forms.ChoiceField( rel_mins_relationto=forms.ChoiceField(
choices=choices, choices=choices,
required=False required=False
), ),
forms.TimeField( rel_days_timeofday=forms.TimeField(
required=False, required=False,
), ),
forms.IntegerField( rel_mins_number=forms.IntegerField(
required=False required=False
), ),
forms.ChoiceField( rel_days_relationto=forms.ChoiceField(
choices=choices, choices=choices,
required=False required=False
), ),
forms.ChoiceField( rel_mins_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE, choices=BEFORE_AFTER_CHOICE,
required=False required=False
), ),
forms.ChoiceField( rel_days_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE, choices=BEFORE_AFTER_CHOICE,
required=False required=False
), ),
@@ -288,32 +343,36 @@ class RelativeDateTimeField(forms.MultiValueField):
) )
def set_event(self, event): 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) (k, v) for k, v in BASE_CHOICES if getattr(event, k, None)
] ]
def compress(self, data_list): def compress(self, data_list):
if not data_list: if not data_list:
return None return None
if data_list[0] == 'absolute': data = reldatetimeparts(*data_list)
return RelativeDateWrapper(data_list[1]) if data.status == 'absolute':
elif data_list[0] == 'unset': return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
return None return None
elif data_list[0] == 'relative_minutes': elif data.status == 'relative_minutes':
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=0, days=0,
base_date_name=data_list[3], base_date_name=data.rel_mins_relationto,
time=None, time=None,
minutes=data_list[5], minutes=data.rel_mins_number,
is_after=data_list[7] == "after", is_after=data.rel_mins_relation == "after",
)) ))
else: else:
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=data_list[2], days=data.rel_days_number,
base_date_name=data_list[6], base_date_name=data.rel_days_relationto,
time=data_list[4], time=data.rel_days_timeofday,
minutes=None, minutes=None,
is_after=data_list[8] == "after", is_after=data.rel_days_relation == "after",
)) ))
def has_changed(self, initial, data): def has_changed(self, initial, data):
@@ -322,29 +381,41 @@ class RelativeDateTimeField(forms.MultiValueField):
return super().has_changed(initial, data) return super().has_changed(initial, data)
def clean(self, value): 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']) 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']) 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']) raise ValidationError(self.error_messages['incomplete'])
return super().clean(value) 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): class RelativeDateWidget(RelativeDateTimeWidget):
template_name = 'pretixbase/forms/widgets/reldate.html' template_name = 'pretixbase/forms/widgets/reldate.html'
parts = reldateparts
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.status_choices = kwargs.pop('status_choices') self.status_choices = kwargs.pop('status_choices')
widgets = ( widgets = reldateparts(
forms.RadioSelect(choices=self.status_choices), status=forms.RadioSelect(choices=self.status_choices),
forms.DateInput( absolute=forms.DateInput(
attrs={'class': 'datepickerfield'} attrs={'class': 'datepickerfield'}
), ),
forms.NumberInput(), rel_days_number=forms.NumberInput(),
forms.Select(choices=kwargs.pop('base_choices')), rel_days_relationto=forms.Select(choices=kwargs.pop('base_choices')),
forms.Select(choices=BEFORE_AFTER_CHOICE), rel_days_relation=forms.Select(choices=BEFORE_AFTER_CHOICE),
) )
forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs) forms.MultiWidget.__init__(self, widgets=widgets, *args, **kwargs)
@@ -352,10 +423,30 @@ class RelativeDateWidget(RelativeDateTimeWidget):
if isinstance(value, str): if isinstance(value, str):
value = RelativeDateWrapper.from_string(value) value = RelativeDateWrapper.from_string(value)
if not 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)): elif isinstance(value.data, (datetime.datetime, datetime.date)):
return ['absolute', value.data, 1, 'date_from', 'before'] return reldateparts(
return ['relative', None, value.data.days, value.data.base_date_name, "after" if value.data.is_after else "before"] 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): class RelativeDateField(RelativeDateTimeField):
@@ -367,22 +458,22 @@ class RelativeDateField(RelativeDateTimeField):
] ]
if not kwargs.get('required', True): if not kwargs.get('required', True):
status_choices.insert(0, ('unset', _('Not set'))) status_choices.insert(0, ('unset', _('Not set')))
fields = ( fields = reldateparts(
forms.ChoiceField( status=forms.ChoiceField(
choices=status_choices, choices=status_choices,
required=True required=True
), ),
forms.DateField( absolute=forms.DateField(
required=False required=False
), ),
forms.IntegerField( rel_days_number=forms.IntegerField(
required=False required=False
), ),
forms.ChoiceField( rel_days_relationto=forms.ChoiceField(
choices=BASE_CHOICES, choices=BASE_CHOICES,
required=False required=False
), ),
forms.ChoiceField( rel_days_relation=forms.ChoiceField(
choices=BEFORE_AFTER_CHOICE, choices=BEFORE_AFTER_CHOICE,
required=False required=False
), ),
@@ -393,28 +484,35 @@ class RelativeDateField(RelativeDateTimeField):
self, fields=fields, require_all_fields=False, *args, **kwargs 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): def compress(self, data_list):
if not data_list: if not data_list:
return None return None
if data_list[0] == 'absolute': data = reldateparts(*data_list)
return RelativeDateWrapper(data_list[1]) if data.status == 'absolute':
elif data_list[0] == 'unset': return RelativeDateWrapper(data.absolute)
elif data.status == 'unset':
return None return None
else: else:
return RelativeDateWrapper(RelativeDate( return RelativeDateWrapper(RelativeDate(
days=data_list[2], days=data.rel_days_number,
base_date_name=data_list[3], base_date_name=data.rel_days_relationto,
time=None, minutes=None, time=None, minutes=None,
is_after=data_list[4] == "after" is_after=data.rel_days_relation == "after"
)) ))
def clean(self, value): 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']) 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']) raise ValidationError(self.error_messages['incomplete'])
return super().clean(value) return forms.MultiValueField.clean(self, value)
class ModelRelativeDateTimeField(models.CharField): class ModelRelativeDateTimeField(models.CharField):

View File

@@ -9,9 +9,9 @@
{{ selopt.label }} {{ selopt.label }}
</label> </label>
{% if selopt.value == "absolute" %} {% if selopt.value == "absolute" %}
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} {{ rendered_subwidgets.absolute }}
{% elif selopt.value == "relative" %} {% 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 }} {{ number }} days {{ relation }} {{ relation_to }}
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}

View File

@@ -9,13 +9,13 @@
{{ selopt.label }} {{ selopt.label }}
</label> </label>
{% if selopt.value == "absolute" %} {% if selopt.value == "absolute" %}
{% include widget.subwidgets.1.template_name with widget=widget.subwidgets.1 %} {{ rendered_subwidgets.absolute }}
{% elif selopt.value == "relative_minutes" %} {% 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 }} {{ number }} minutes {{ relation }} {{ relation_to }}
{% endblocktrans %} {% endblocktrans %}
{% elif selopt.value == "relative" %} {% 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 }} {{ number }} days {{ relation }} {{ relation_to }} at {{ time_of_day }}
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}