Compare commits

...

14 Commits

Author SHA1 Message Date
johan12345
df25ae3c7c add tests in test_checkout.py 2019-03-27 21:33:26 +01:00
johan12345
d686b386e3 add tests in test_items.py 2019-03-27 21:07:26 +01:00
johan12345
bdda68c82d move JS to the right location 2019-03-27 20:54:04 +01:00
johan12345
2aa36ee2a7 reorder migrations 2019-03-27 20:34:29 +01:00
johan12345
43affc22ab remove unused import 2019-03-27 18:43:44 +01:00
Johan von Forstner
60575377d7 move migration 2019-03-27 18:43:44 +01:00
Raphael Michel
5ec8c7ed96 Store date(times) in ISO formats and UTC 2019-03-27 18:43:44 +01:00
johan12345
ef55a018f8 fix datetime fields 2019-03-27 18:43:04 +01:00
johan12345
6bf9327f87 improvements after review 2019-03-27 18:42:42 +01:00
johan12345
f761f93550 add migration 2019-03-27 18:42:42 +01:00
Johan von Forstner
c996289563 fix default answers for booleans 2019-03-27 18:42:42 +01:00
Johan von Forstner
89bbed42e6 replace form field for default value depending on question type 2019-03-27 18:42:42 +01:00
Johan von Forstner
8e22c0f3a4 Show initial values in form 2019-03-27 18:42:01 +01:00
Johan von Forstner
3483c522da Add default_value model field, validation and form field 2019-03-27 18:41:40 +01:00
8 changed files with 261 additions and 9 deletions

View File

@@ -173,6 +173,7 @@ class BaseQuestionsForm(forms.Form):
initial = None
tz = pytz.timezone(event.settings.timezone)
help_text = rich_text(q.help_text)
default = q.default_value
label = escape(q.question) # django-bootstrap3 calls mark_safe
required = q.required and not self.all_optional
if q.type == Question.TYPE_BOOLEAN:
@@ -185,6 +186,8 @@ class BaseQuestionsForm(forms.Form):
if initial:
initialbool = (initial.answer == "True")
elif default:
initialbool = (default == "True")
else:
initialbool = False
@@ -197,21 +200,21 @@ class BaseQuestionsForm(forms.Form):
field = forms.DecimalField(
label=label, required=required,
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'),
)
elif q.type == Question.TYPE_STRING:
field = forms.CharField(
label=label, required=required,
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:
field = forms.CharField(
label=label, required=required,
help_text=help_text,
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:
field = forms.ModelChoiceField(
@@ -243,21 +246,24 @@ class BaseQuestionsForm(forms.Form):
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,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else (
dateutil.parser.parse(default).date() if default else None),
widget=DatePickerWidget(),
)
elif q.type == Question.TYPE_TIME:
field = forms.TimeField(
label=label, required=required,
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')),
)
elif q.type == Question.TYPE_DATETIME:
field = SplitDateTimeField(
label=label, required=required,
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')),
)
field.question = q

View File

@@ -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'),
),
]

View File

@@ -963,6 +963,11 @@ class Question(LoggedModel):
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
)
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:
verbose_name = _("Question")
@@ -980,6 +985,10 @@ class Question(LoggedModel):
def clean_identifier(self, code):
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
def _clean_identifier(event, code, instance=None):
qs = Question.objects.filter(event=event, identifier__iexact=code)
@@ -1007,8 +1016,8 @@ class Question(LoggedModel):
def __lt__(self, other) -> bool:
return self.sortkey < other.sortkey
def clean_answer(self, answer):
if self.required:
def clean_answer(self, answer, check_required=True):
if self.required and check_required:
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.'))
if not answer:

View File

@@ -1,8 +1,14 @@
import datetime
import dateutil
import pytz
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Max
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.utils import formats
from django.utils.translation import (
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):
question = I18nFormField(
label=_("Question"),
@@ -51,6 +114,8 @@ class QuestionForm(I18nModelForm):
pk=self.instance.pk
)
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
def clean_dependency_question(self):
@@ -80,6 +145,7 @@ class QuestionForm(I18nModelForm):
'help_text',
'type',
'required',
'default_value',
'ask_during_checkin',
'identifier',
'items',
@@ -90,7 +156,8 @@ class QuestionForm(I18nModelForm):
'items': forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
'dependency_value': forms.Select,
'default_value': AdjustableTypeField(),
'dependency_value': forms.Select
}

View File

@@ -107,6 +107,7 @@
<fieldset>
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.help_text layout="control" %}
{% bootstrap_field form.default_value layout="control" %}
{% bootstrap_field form.identifier layout="control" %}
{% bootstrap_field form.ask_during_checkin layout="control" %}

View File

@@ -93,6 +93,67 @@ $(function () {
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
$(".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");

View File

@@ -251,6 +251,41 @@ class QuestionsTest(ItemFormTest):
form_data)
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):

View File

@@ -1985,6 +1985,59 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
self.q3: 'False',
}, 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):
def setUp(self):