Compare commits

...
8 changed files with 261 additions and 9 deletions
+12 -6
View File
@@ -173,6 +173,7 @@ class BaseQuestionsForm(forms.Form):
initial = None initial = None
tz = pytz.timezone(event.settings.timezone) tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text) help_text = rich_text(q.help_text)
default = q.default_value
label = escape(q.question) # django-bootstrap3 calls mark_safe label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional required = q.required and not self.all_optional
if q.type == Question.TYPE_BOOLEAN: if q.type == Question.TYPE_BOOLEAN:
@@ -185,6 +186,8 @@ class BaseQuestionsForm(forms.Form):
if initial: if initial:
initialbool = (initial.answer == "True") initialbool = (initial.answer == "True")
elif default:
initialbool = (default == "True")
else: else:
initialbool = False initialbool = False
@@ -197,21 +200,21 @@ class BaseQuestionsForm(forms.Form):
field = forms.DecimalField( field = forms.DecimalField(
label=label, required=required, label=label, required=required,
help_text=q.help_text, help_text=q.help_text,
initial=initial.answer if initial else None, initial=initial.answer if initial else (default if default else None),
min_value=Decimal('0.00'), min_value=Decimal('0.00'),
) )
elif q.type == Question.TYPE_STRING: elif q.type == Question.TYPE_STRING:
field = forms.CharField( field = forms.CharField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=initial.answer if initial else None, initial=initial.answer if initial else (default if default else None),
) )
elif q.type == Question.TYPE_TEXT: elif q.type == Question.TYPE_TEXT:
field = forms.CharField( field = forms.CharField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
widget=forms.Textarea, widget=forms.Textarea,
initial=initial.answer if initial else None, initial=initial.answer if initial else (default if default else None),
) )
elif q.type == Question.TYPE_CHOICE: elif q.type == Question.TYPE_CHOICE:
field = forms.ModelChoiceField( field = forms.ModelChoiceField(
@@ -243,21 +246,24 @@ class BaseQuestionsForm(forms.Form):
field = forms.DateField( field = forms.DateField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None, initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else (
dateutil.parser.parse(default).date() if default else None),
widget=DatePickerWidget(), widget=DatePickerWidget(),
) )
elif q.type == Question.TYPE_TIME: elif q.type == Question.TYPE_TIME:
field = forms.TimeField( field = forms.TimeField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None, initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else (
dateutil.parser.parse(default).time() if default else None),
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')), widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
) )
elif q.type == Question.TYPE_DATETIME: elif q.type == Question.TYPE_DATETIME:
field = SplitDateTimeField( field = SplitDateTimeField(
label=label, required=required, label=label, required=required,
help_text=help_text, help_text=help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None, initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else (
dateutil.parser.parse(default).astimezone(tz) if default else None),
widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')), widget=SplitDateTimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
) )
field.question = q field.question = q
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-31 15:39
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0115_auto_20190323_2238'),
]
operations = [
migrations.AddField(
model_name='question',
name='default_value',
field=models.TextField(blank=True, help_text='The question will be filled with this response by default', null=True, verbose_name='Default answer'),
),
]
+11 -2
View File
@@ -963,6 +963,11 @@ class Question(LoggedModel):
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions' 'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
) )
dependency_value = models.TextField(null=True, blank=True) dependency_value = models.TextField(null=True, blank=True)
default_value = models.TextField(
verbose_name=_('Default answer'),
help_text=_('The question will be filled with this response by default'),
null=True, blank=True,
)
class Meta: class Meta:
verbose_name = _("Question") verbose_name = _("Question")
@@ -980,6 +985,10 @@ class Question(LoggedModel):
def clean_identifier(self, code): def clean_identifier(self, code):
Question._clean_identifier(self.event, code, self) Question._clean_identifier(self.event, code, self)
def clean(self):
if self.default_value is not None:
self.clean_answer(self.default_value, check_required=False)
@staticmethod @staticmethod
def _clean_identifier(event, code, instance=None): def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier__iexact=code) qs = Question.objects.filter(event=event, identifier__iexact=code)
@@ -1007,8 +1016,8 @@ class Question(LoggedModel):
def __lt__(self, other) -> bool: def __lt__(self, other) -> bool:
return self.sortkey < other.sortkey return self.sortkey < other.sortkey
def clean_answer(self, answer): def clean_answer(self, answer, check_required=True):
if self.required: if self.required and check_required:
if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)): if not answer or (self.type == Question.TYPE_BOOLEAN and answer not in ("true", "True", True)):
raise ValidationError(_('An answer to this question is required to proceed.')) raise ValidationError(_('An answer to this question is required to proceed.'))
if not answer: if not answer:
+68 -1
View File
@@ -1,8 +1,14 @@
import datetime
import dateutil
import pytz
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Max from django.db.models import Max
from django.forms.formsets import DELETION_FIELD_NAME from django.forms.formsets import DELETION_FIELD_NAME
from django.forms.utils import from_current_timezone, to_current_timezone
from django.urls import reverse from django.urls import reverse
from django.utils import formats
from django.utils.translation import ( from django.utils.translation import (
pgettext_lazy, ugettext as __, ugettext_lazy as _, pgettext_lazy, ugettext as __, ugettext_lazy as _,
) )
@@ -33,6 +39,63 @@ class CategoryForm(I18nModelForm):
] ]
class AdjustableTypeField(forms.Textarea):
def __init__(self, *args, type=None, **kwargs):
super().__init__(*args, **kwargs)
def format_value(self, value):
if getattr(self, 'type') == 'W' and value:
return str(formats.localize_input(to_current_timezone(dateutil.parser.parse(value))))
elif getattr(self, 'type') == 'D' and value:
return str(formats.localize_input(dateutil.parser.parse(value).date()))
elif getattr(self, 'type') == 'T' and value:
return str(formats.localize_input(dateutil.parser.parse(value).time()))
return super().format_value(value)
def value_from_datadict(self, data, files, name):
if 'type' in data and data['type'] == 'W' and 'default_value_date' in data and 'default_value_time' in data:
d_date = d_time = None
for format in formats.get_format('DATE_INPUT_FORMATS'):
try:
d_date = datetime.datetime.strptime(data['default_value_date'], format).date()
break
except (ValueError, TypeError):
continue
for format in formats.get_format('TIME_INPUT_FORMATS'):
try:
d_time = datetime.datetime.strptime(data['default_value_time'], format).time()
break
except (ValueError, TypeError):
continue
if d_date and d_time:
return from_current_timezone(datetime.datetime.combine(d_date, d_time)).astimezone(pytz.UTC).isoformat()
else:
return None
else:
val = super().value_from_datadict(data, files, name)
if 'type' in data and data['type'] == 'D' and val:
for format in formats.get_format('DATE_INPUT_FORMATS'):
try:
d_date = datetime.datetime.strptime(val, format).date()
val = d_date.isoformat()
break
except (ValueError, TypeError):
continue
elif 'type' in data and data['type'] == 'T' and val:
for format in formats.get_format('TIME_INPUT_FORMATS'):
try:
d_date = datetime.datetime.strptime(val, format).time()
val = d_date.isoformat()
break
except (ValueError, TypeError):
continue
return val
class QuestionForm(I18nModelForm): class QuestionForm(I18nModelForm):
question = I18nFormField( question = I18nFormField(
label=_("Question"), label=_("Question"),
@@ -51,6 +114,8 @@ class QuestionForm(I18nModelForm):
pk=self.instance.pk pk=self.instance.pk
) )
self.fields['identifier'].required = False self.fields['identifier'].required = False
if self.instance:
self.fields['default_value'].widget.type = self.instance.type
self.fields['help_text'].widget.attrs['rows'] = 3 self.fields['help_text'].widget.attrs['rows'] = 3
def clean_dependency_question(self): def clean_dependency_question(self):
@@ -80,6 +145,7 @@ class QuestionForm(I18nModelForm):
'help_text', 'help_text',
'type', 'type',
'required', 'required',
'default_value',
'ask_during_checkin', 'ask_during_checkin',
'identifier', 'identifier',
'items', 'items',
@@ -90,7 +156,8 @@ class QuestionForm(I18nModelForm):
'items': forms.CheckboxSelectMultiple( 'items': forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'} attrs={'class': 'scrolling-multiple-choice'}
), ),
'dependency_value': forms.Select, 'default_value': AdjustableTypeField(),
'dependency_value': forms.Select
} }
@@ -107,6 +107,7 @@
<fieldset> <fieldset>
<legend>{% trans "Advanced settings" %}</legend> <legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.help_text layout="control" %} {% bootstrap_field form.help_text layout="control" %}
{% bootstrap_field form.default_value layout="control" %}
{% bootstrap_field form.identifier layout="control" %} {% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %} {% bootstrap_field form.ask_during_checkin layout="control" %}
@@ -93,6 +93,67 @@ $(function () {
show = $("#id_type").val() == "B" && $("#id_required").prop("checked"); show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
$(".alert-required-boolean").toggle(show); $(".alert-required-boolean").toggle(show);
update_default_value_field()
}
function update_default_value_field() {
let input = $('#id_default_value');
let parent = input.parent();
let field = input.prop("tagName") == 'DIV' ? input.children().first() : input;
let common_attrs = ' name="default_value" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '" id="id_default_value"';
let value = field.val();
switch ($("#id_type").val()) {
case 'N':
input.replaceWith('<input type="number" class="form-control" value="' + value + '" ' + common_attrs + '>');
$('.form-group:has(#id_default_value)').show();
break;
case 'S':
input.replaceWith('<input type="text" maxlength="190" class="form-control" value="' + value + '" ' + common_attrs + '>');
$('.form-group:has(#id_default_value)').show();
break;
case 'T':
input.replaceWith('<textarea cols="40" rows="10" class="form-control" ' + common_attrs + '>' + value + '</textarea>');
$('.form-group:has(#id_default_value)').show();
break;
case 'B':
let checked = (value === 'True' || value === 'on' ? 'checked' : '');
input.replaceWith('<input type="checkbox" ' + common_attrs + ' ' + checked + '>');
$('.form-group:has(#id_default_value)').show();
break;
case 'D':
let dateField = input.replaceWith('<input type="text" class="form-control datepickerfield" value="' + value + '" ' + common_attrs + '>');
form_handlers(parent);
$('.form-group:has(#id_default_value)').show();
break;
case 'H':
let timeField = input.replaceWith('<input type="text" class="form-control timepickerfield" value="' + value + '" ' + common_attrs + '>');
form_handlers(parent);
$('.form-group:has(#id_default_value)').show();
break;
case 'W':
let split = value.split(' ');
let date, time;
if (split.length > 1) {
date = split[0];
time = split[1];
} else {
date = null;
time = null;
}
let dtField = input.replaceWith('<div class="splitdatetimerow" id="id_default_value">\n' +
'<input type="text" class="form-control splitdatetimepart datepickerfield" value="' + date + '" name="default_value_date" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '">\n' +
'<input type="text" class="form-control splitdatetimepart timepickerfield" value="' + time + '" name="default_value_time" placeholder="' + field.attr('placeholder') + '" title="' + field.attr('title') + '">\n' +
'</div>\n');
form_handlers(parent);
$('.form-group:has(#id_default_value)').show();
break;
default:
// file, choice, and multiple choice are not implemented
$('.form-group:has(#id_default_value)').hide();
input.val('')
}
} }
var $val = $("#id_dependency_value"); var $val = $("#id_dependency_value");
+35
View File
@@ -251,6 +251,41 @@ class QuestionsTest(ItemFormTest):
form_data) form_data)
assert not doc.select(".alert-success") assert not doc.select(".alert-success")
def test_set_default_value(self):
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_TEXT,
required=True)
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['default_value'] = 'Heidelberg'
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
form_data)
assert doc.select(".alert-success")
q.refresh_from_db()
assert q.default_value == 'Heidelberg'
def test_set_default_value_datetime(self):
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_DATETIME,
required=True)
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['default_value_date'] = '2019-01-01'
form_data['default_value_time'] = '10:00'
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
form_data)
assert doc.select(".alert-success")
q.refresh_from_db()
assert q.default_value == '2019-01-01T10:00:00+00:00'
def test_set_default_value_invalid(self):
q = Question.objects.create(event=self.event1, question="What city are you from?", type=Question.TYPE_DATE,
required=True)
doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['default_value'] = 'foobar'
doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q.id),
form_data)
assert not doc.select(".alert-success")
class QuotaTest(ItemFormTest): class QuotaTest(ItemFormTest):
+53
View File
@@ -1985,6 +1985,59 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
self.q3: 'False', self.q3: 'False',
}, should_fail=True) }, should_fail=True)
def test_datetime_defaultvalues(self):
""" Test timezone and format handling for default values """
q1 = Question.objects.create(
event=self.event, question='When did you wake up today?', type=Question.TYPE_TIME,
required=True, default_value='10:00'
)
q2 = Question.objects.create(
event=self.event, question='When was your last haircut?', type=Question.TYPE_DATE,
required=True, default_value='2019-01-01'
)
q3 = Question.objects.create(
event=self.event, question='When are you going to arrive?', type=Question.TYPE_DATETIME,
required=True, default_value='2019-01-01T10:00:00+00:00'
)
self.ticket.questions.add(q1)
self.ticket.questions.add(q2)
self.ticket.questions.add(q3)
cr = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '2019-01-01'
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '2019-01-01'
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '10:00'
# set to different timezone, this should affect the datetime question's default value displayed
self.event.settings.set('timezone', 'US/Central')
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '2019-01-01'
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '2019-01-01'
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '04:00'
# set locale, this should affect the date format
self.event.settings.set('locales', ['de'])
self.event.save()
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q1.id))[0]['value'] == '10:00'
assert doc.select('input[name="%s-question_%s"]' % (cr.id, q2.id))[0]['value'] == '01.01.2019'
assert doc.select('input[name="%s-question_%s_0"]' % (cr.id, q3.id))[0]['value'] == '01.01.2019'
assert doc.select('input[name="%s-question_%s_1"]' % (cr.id, q3.id))[0]['value'] == '04:00'
class CheckoutBundleTest(BaseCheckoutTestCase, TestCase): class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
def setUp(self): def setUp(self):