mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
14 Commits
event-dial
...
794-questi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df25ae3c7c | ||
|
|
d686b386e3 | ||
|
|
bdda68c82d | ||
|
|
2aa36ee2a7 | ||
|
|
43affc22ab | ||
|
|
60575377d7 | ||
|
|
5ec8c7ed96 | ||
|
|
ef55a018f8 | ||
|
|
6bf9327f87 | ||
|
|
f761f93550 | ||
|
|
c996289563 | ||
|
|
89bbed42e6 | ||
|
|
8e22c0f3a4 | ||
|
|
3483c522da |
@@ -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
|
||||
|
||||
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal file
20
src/pretix/base/migrations/0116_auto_20180531_1739.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user