mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Add min/max validation for date, datetime, and number questions (#1858)
This commit is contained in:
@@ -13,10 +13,13 @@ from babel import localedata
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone
|
||||
from django.utils.translation import (
|
||||
get_language, gettext_lazy as _, pgettext_lazy,
|
||||
)
|
||||
@@ -234,6 +237,43 @@ class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html'
|
||||
|
||||
|
||||
class MinDateValidator(MinValueValidator):
|
||||
def __call__(self, value):
|
||||
try:
|
||||
return super().__call__(value)
|
||||
except ValidationError as e:
|
||||
e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT')
|
||||
raise e
|
||||
|
||||
|
||||
class MinDateTimeValidator(MinValueValidator):
|
||||
def __call__(self, value):
|
||||
try:
|
||||
return super().__call__(value)
|
||||
except ValidationError as e:
|
||||
e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT')
|
||||
raise e
|
||||
|
||||
|
||||
class MaxDateValidator(MaxValueValidator):
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
return super().__call__(value)
|
||||
except ValidationError as e:
|
||||
e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT')
|
||||
raise e
|
||||
|
||||
|
||||
class MaxDateTimeValidator(MaxValueValidator):
|
||||
def __call__(self, value):
|
||||
try:
|
||||
return super().__call__(value)
|
||||
except ValidationError as e:
|
||||
e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT')
|
||||
raise e
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -392,9 +432,10 @@ class BaseQuestionsForm(forms.Form):
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=label, required=required,
|
||||
min_value=q.valid_number_min or Decimal('0.00'),
|
||||
max_value=q.valid_number_max,
|
||||
help_text=q.help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
min_value=Decimal('0.00'),
|
||||
)
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
@@ -453,12 +494,21 @@ class BaseQuestionsForm(forms.Form):
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
attrs = {}
|
||||
if q.valid_date_min:
|
||||
attrs['data-min'] = q.valid_date_min.isoformat()
|
||||
if q.valid_date_max:
|
||||
attrs['data-max'] = q.valid_date_max.isoformat()
|
||||
field = forms.DateField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
|
||||
widget=DatePickerWidget(),
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if q.valid_date_min:
|
||||
field.validators.append(MinDateValidator(q.valid_date_min))
|
||||
if q.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(q.valid_date_max))
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
field = forms.TimeField(
|
||||
label=label, required=required,
|
||||
@@ -471,8 +521,16 @@ class BaseQuestionsForm(forms.Form):
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None,
|
||||
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=q.valid_datetime_min,
|
||||
max_date=q.valid_datetime_max
|
||||
),
|
||||
)
|
||||
if q.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(q.valid_datetime_min))
|
||||
if q.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
babel_locale = 'en'
|
||||
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from django import forms
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@@ -92,7 +93,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
|
||||
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
|
||||
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None):
|
||||
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
|
||||
attrs = attrs or {}
|
||||
if 'placeholder' in attrs:
|
||||
del attrs['placeholder']
|
||||
@@ -106,6 +107,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
|
||||
time_attrs['class'] += ' timepickerfield'
|
||||
date_attrs['autocomplete'] = 'date-picker-do-not-autofill'
|
||||
time_attrs['autocomplete'] = 'time-picker-do-not-autofill'
|
||||
if min_date:
|
||||
date_attrs['data-min'] = (
|
||||
min_date if isinstance(min_date, date) else min_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
if max_date:
|
||||
date_attrs['data-max'] = (
|
||||
max_date if isinstance(max_date, date) else max_date.astimezone(get_current_timezone()).date()
|
||||
).isoformat()
|
||||
|
||||
def date_placeholder():
|
||||
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
|
||||
|
||||
49
src/pretix/base/migrations/0171_auto_20201126_1635.py
Normal file
49
src/pretix/base/migrations/0171_auto_20201126_1635.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 3.0.11 on 2020-11-26 16:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0170_remove_hidden_urls'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_date_max',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_date_min',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_datetime_max',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_datetime_min',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_number_max',
|
||||
field=models.DecimalField(decimal_places=6, max_digits=16, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_number_min',
|
||||
field=models.DecimalField(decimal_places=6, max_digits=16, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='seat',
|
||||
name='product',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seats', to='pretixbase.Item'),
|
||||
),
|
||||
]
|
||||
@@ -1084,6 +1084,18 @@ class Question(LoggedModel):
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[])
|
||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
|
||||
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
|
||||
valid_number_max = models.DecimalField(decimal_places=6, max_digits=16, null=True, blank=True,
|
||||
verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps'))
|
||||
valid_date_min = models.DateField(null=True, blank=True,
|
||||
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
|
||||
valid_date_max = models.DateField(null=True, blank=True,
|
||||
verbose_name=_('Maximum value'), help_text=_('Currently not supported in our apps'))
|
||||
valid_datetime_min = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_('Minimum value'), help_text=_('Currently not supported in our apps'))
|
||||
valid_datetime_max = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_('Maximum value'), help_text=_('Currently not supporetd in our apps'))
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
@@ -1173,14 +1185,24 @@ class Question(LoggedModel):
|
||||
answer = formats.sanitize_separators(answer)
|
||||
answer = str(answer).strip()
|
||||
try:
|
||||
return Decimal(answer)
|
||||
v = Decimal(answer)
|
||||
if self.valid_number_min is not None and v < self.valid_number_min:
|
||||
raise ValidationError(_('The number is to low.'))
|
||||
if self.valid_number_max is not None and v > self.valid_number_max:
|
||||
raise ValidationError(_('The number is to high.'))
|
||||
return v
|
||||
except DecimalException:
|
||||
raise ValidationError(_('Invalid number input.'))
|
||||
elif self.type == Question.TYPE_DATE:
|
||||
if isinstance(answer, date):
|
||||
return answer
|
||||
try:
|
||||
return dateutil.parser.parse(answer).date()
|
||||
dt = dateutil.parser.parse(answer).date()
|
||||
if self.valid_date_min is not None and dt < self.valid_date_min:
|
||||
raise ValidationError(_('Please choose a later date.'))
|
||||
if self.valid_date_max is not None and dt > self.valid_date_max:
|
||||
raise ValidationError(_('Please choose an earlier date.'))
|
||||
return dt
|
||||
except:
|
||||
raise ValidationError(_('Invalid date input.'))
|
||||
elif self.type == Question.TYPE_TIME:
|
||||
@@ -1197,9 +1219,14 @@ class Question(LoggedModel):
|
||||
dt = dateutil.parser.parse(answer)
|
||||
if is_naive(dt):
|
||||
dt = make_aware(dt, pytz.timezone(self.event.settings.timezone))
|
||||
return dt
|
||||
except:
|
||||
raise ValidationError(_('Invalid datetime input.'))
|
||||
else:
|
||||
if self.valid_datetime_min is not None and dt < self.valid_datetime_min:
|
||||
raise ValidationError(_('Please choose a later date.'))
|
||||
if self.valid_datetime_max is not None and dt > self.valid_datetime_max:
|
||||
raise ValidationError(_('Please choose an earlier date.'))
|
||||
return dt
|
||||
elif self.type == Question.TYPE_COUNTRYCODE and answer:
|
||||
c = Country(answer.upper())
|
||||
if c.name:
|
||||
|
||||
Reference in New Issue
Block a user