Fix #732 -- Add date and time question types (#732)

* [WIP] add date/time question type

* Date/time questions python classes, types and form handling

* use own timepicker

* Fix argument naming

* Add css and js for datetimepickers

* remove not needed str call

* seperate splitdatetime widget template and fix date/time questions

* change date placeholder to dec 31

* do not show seconds in presale time pickers

* improve codestyle

* add new question types to api doc

* add test

* expand test to datetime question

* add new questiontypes to changelog

remove duplicate parens

* remove timezone from time only question answers

* improve codestyle

* Fix date and time formatting in control question overview
This commit is contained in:
Felix Rindt
2018-01-14 14:29:38 +01:00
committed by Raphael Michel
parent b8c041d0d6
commit 251d62f3c4
21 changed files with 304 additions and 25 deletions

View File

@@ -23,6 +23,9 @@ type string The expected ty
* ``C`` choice from a list * ``C`` choice from a list
* ``M`` multiple choice from a list * ``M`` multiple choice from a list
* ``F`` file upload * ``F`` file upload
* ``D`` date
* ``H`` time
* ``W`` date and time
required boolean If ``True``, the question needs to be filled out. required boolean If ``True``, the question needs to be filled out.
position integer An integer, used for sorting position integer An integer, used for sorting
items list of integers List of item IDs this question is assigned to. items list of integers List of item IDs this question is assigned to.
@@ -32,6 +35,9 @@ options list of objects In case of ques
└ answer multi-lingual string The displayed value of this option └ answer multi-lingual string The displayed value of this option
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 1.12
The values ``D``, ``H``, and ``W`` for the field ``type`` are now allowed.
Endpoints Endpoints
--------- ---------

View File

@@ -543,7 +543,10 @@ class Question(LoggedModel):
* a multi-line string (``TYPE_TEXT``) * a multi-line string (``TYPE_TEXT``)
* a boolean (``TYPE_BOOLEAN``) * a boolean (``TYPE_BOOLEAN``)
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``) * a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
* a file upload (``TYPE_FILE``)) * a file upload (``TYPE_FILE``)
* a date (``TYPE_DATE``)
* a time (``TYPE_TIME``)
* a date and a time (``TYPE_DATETIME``)
:param event: The event this question belongs to :param event: The event this question belongs to
:type event: Event :type event: Event
@@ -562,6 +565,9 @@ class Question(LoggedModel):
TYPE_CHOICE = "C" TYPE_CHOICE = "C"
TYPE_CHOICE_MULTIPLE = "M" TYPE_CHOICE_MULTIPLE = "M"
TYPE_FILE = "F" TYPE_FILE = "F"
TYPE_DATE = "D"
TYPE_TIME = "H"
TYPE_DATETIME = "W"
TYPE_CHOICES = ( TYPE_CHOICES = (
(TYPE_NUMBER, _("Number")), (TYPE_NUMBER, _("Number")),
(TYPE_STRING, _("Text (one line)")), (TYPE_STRING, _("Text (one line)")),
@@ -570,6 +576,9 @@ class Question(LoggedModel):
(TYPE_CHOICE, _("Choose one from a list")), (TYPE_CHOICE, _("Choose one from a list")),
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")), (TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
(TYPE_FILE, _("File upload")), (TYPE_FILE, _("File upload")),
(TYPE_DATE, _("Date")),
(TYPE_TIME, _("Time")),
(TYPE_DATETIME, _("Date and time")),
) )
event = models.ForeignKey( event = models.ForeignKey(

View File

@@ -6,6 +6,7 @@ from datetime import datetime, time
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
import dateutil
import pytz import pytz
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
@@ -15,6 +16,7 @@ from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path from django.utils.encoding import escape_uri_path
from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
@@ -498,6 +500,18 @@ class QuestionAnswer(models.Model):
return str(_("No")) return str(_("No"))
elif self.question.type == Question.TYPE_FILE: elif self.question.type == Question.TYPE_FILE:
return str(_("<file>")) return str(_("<file>"))
elif self.question.type == Question.TYPE_DATETIME:
d = dateutil.parser.parse(self.answer)
if self.orderposition:
tz = pytz.timezone(self.orderposition.order.event.settings.timezone)
d = d.astimezone(tz)
return date_format(d, "SHORT_DATETIME_FORMAT")
elif self.question.type == Question.TYPE_DATE:
d = dateutil.parser.parse(self.answer)
return date_format(d, "SHORT_DATE_FORMAT")
elif self.question.type == Question.TYPE_TIME:
d = dateutil.parser.parse(self.answer)
return date_format(d, "TIME_FORMAT")
else: else:
return self.answer return self.answer

View File

@@ -0,0 +1,3 @@
<div class="splitdatetimerow">
{% include 'django/forms/widgets/multiwidget.html' %}
</div>

View File

@@ -103,6 +103,7 @@ class SlugWidget(forms.TextInput):
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget): 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):
attrs = attrs or {} attrs = attrs or {}
@@ -114,11 +115,10 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
time_attrs.setdefault('class', 'form-control splitdatetimepart') time_attrs.setdefault('class', 'form-control splitdatetimepart')
date_attrs['class'] += ' datepickerfield' date_attrs['class'] += ' datepickerfield'
time_attrs['class'] += ' timepickerfield' time_attrs['class'] += ' timepickerfield'
time_attrs['class'] += ' timepickerfield'
df = date_format or get_format('DATE_INPUT_FORMATS')[0] df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace( date_attrs['placeholder'] = now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0 year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df) ).strftime(df)
tf = time_format or get_format('TIME_INPUT_FORMATS')[0] tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace( time_attrs['placeholder'] = now().replace(
@@ -131,3 +131,39 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
) )
# Skip one hierarchy level # Skip one hierarchy level
forms.MultiWidget.__init__(self, widgets, attrs) forms.MultiWidget.__init__(self, widgets, attrs)
class DatePickerWidget(forms.DateInput):
def __init__(self, attrs=None, date_format=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
date_attrs = dict(attrs)
date_attrs.setdefault('class', 'form-control')
date_attrs['class'] += ' datepickerfield'
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
date_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
forms.DateInput.__init__(self, date_attrs, date_format)
class TimePickerWidget(forms.TimeInput):
def __init__(self, attrs=None, time_format=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
time_attrs = dict(attrs)
time_attrs.setdefault('class', 'form-control')
time_attrs['class'] += ' timepickerfield'
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
time_attrs['placeholder'] = now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(tf)
forms.TimeInput.__init__(self, time_attrs, time_format)

View File

@@ -9,10 +9,10 @@
<legend>{% trans "General information" %}</legend> <legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %} {% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.slug layout="control" %} {% bootstrap_field form.slug layout="control" %}
{% bootstrap_field form.date_from layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %} {% bootstrap_field form.location layout="control" %}
{% bootstrap_field form.date_admission layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.is_public layout="control" %} {% bootstrap_field form.is_public layout="control" %}
@@ -51,9 +51,9 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Timeline" %}</legend> <legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_start layout="control" %}
{% bootstrap_field sform.presale_start_show_date layout="control" %} {% bootstrap_field sform.presale_start_show_date layout="control" %}
{% bootstrap_field form.presale_end layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_end layout="control" %}
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %} {% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
{% bootstrap_field sform.last_order_modification_date layout="control" %} {% bootstrap_field sform.last_order_modification_date layout="control" %}
</fieldset> </fieldset>

View File

@@ -29,8 +29,8 @@
</div> </div>
</div> </div>
</div> </div>
{% bootstrap_field form.date_from layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %} {% bootstrap_field form.location layout="control" %}
{% bootstrap_field form.currency layout="control" %} {% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %} {% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
@@ -43,8 +43,8 @@
{% if form.presale_start %} {% if form.presale_start %}
<fieldset> <fieldset>
<legend>{% trans "Timeline" %}</legend> <legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_start layout="control" %}
{% bootstrap_field form.presale_end layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_end layout="control" %}
</fieldset> </fieldset>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -23,8 +23,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Availability" %}</legend> <legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.available_from layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.max_per_order layout="control" %} {% bootstrap_field form.max_per_order layout="control" %}
{% bootstrap_field form.min_per_order layout="control" %} {% bootstrap_field form.min_per_order layout="control" %}
{% bootstrap_field form.require_voucher layout="control" %} {% bootstrap_field form.require_voucher layout="control" %}

View File

@@ -22,10 +22,10 @@
<legend>{% trans "General information" %}</legend> <legend>{% trans "General information" %}</legend>
{% bootstrap_field form.name layout="control" %} {% bootstrap_field form.name layout="control" %}
{% bootstrap_field form.active layout="control" %} {% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.date_from layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %} {% bootstrap_field form.location layout="control" %}
{% bootstrap_field form.date_admission layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %} {% bootstrap_field form.frontpage_text layout="control" %}
{% if meta_forms %} {% if meta_forms %}
<div class="form-group metadata-group"> <div class="form-group metadata-group">
@@ -49,8 +49,8 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Timeline" %}</legend> <legend>{% trans "Timeline" %}</legend>
{% bootstrap_field form.presale_start layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_start layout="control" %}
{% bootstrap_field form.presale_end layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.presale_end layout="control" %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Quotas" %}</legend> <legend>{% trans "Quotas" %}</legend>

View File

@@ -38,7 +38,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Voucher details" %}</legend> <legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.valid_until layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.valid_until layout="control" %}
{% bootstrap_field form.block_quota layout="control" %} {% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %} {% bootstrap_field form.allow_ignore_quota layout="control" %}
<div class="form-group"> <div class="form-group">

View File

@@ -26,7 +26,7 @@
<legend>{% trans "Voucher details" %}</legend> <legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.code layout="control" %} {% bootstrap_field form.code layout="control" %}
{% bootstrap_field form.max_usages layout="control" %} {% bootstrap_field form.max_usages layout="control" %}
{% bootstrap_field form.valid_until layout="control" horizontal_field_class="col-md-9 splitdatetimerow" %} {% bootstrap_field form.valid_until layout="control" %}
{% bootstrap_field form.block_quota layout="control" %} {% bootstrap_field form.block_quota layout="control" %}
{% bootstrap_field form.allow_ignore_quota layout="control" %} {% bootstrap_field form.allow_ignore_quota layout="control" %}
<div class="form-group"> <div class="form-group">

View File

@@ -55,6 +55,20 @@ def get_javascript_format(format_name):
) )
def get_format_without_seconds(format_name):
formats = get_format(format_name)
formats_no_seconds = [f for f in formats if '%S' not in f]
return formats_no_seconds[0] if formats_no_seconds else formats[0]
def get_javascript_format_without_seconds(format_name):
f = get_format_without_seconds(format_name)
return toJavascript_re.sub(
lambda x: date_conversion_to_moment[x.group()],
f
)
def get_moment_locale(locale=None): def get_moment_locale(locale=None):
cur_lang = locale or translation.get_language() cur_lang = locale or translation.get_language()
if cur_lang in moment_locales: if cur_lang in moment_locales:

View File

@@ -430,6 +430,12 @@ class QuestionView(EventPermissionRequiredMixin, QuestionMixin, ChartContainingV
for a in qs: for a in qs:
a['answer'] = str(a['options__answer']) a['answer'] = str(a['options__answer'])
del a['options__answer'] del a['options__answer']
elif self.object.type in (Question.TYPE_TIME, Question.TYPE_DATE, Question.TYPE_DATETIME):
qs = qs.order_by('answer')
qs_model = qs
qs = qs.values('answer').annotate(count=Count('id')).order_by('-count')
for a, a_model in zip(qs, qs_model):
a['answer'] = str(a_model)
else: else:
qs = qs.order_by('answer').values('answer').annotate(count=Count('id')).order_by('-count') qs = qs.order_by('answer').values('answer').annotate(count=Count('id')).order_by('-count')

View File

@@ -4,6 +4,7 @@ from django.utils.translation import get_language_info
from i18nfield.strings import LazyI18nString from i18nfield.strings import LazyI18nString
from pretix.base.settings import GlobalSettingsObject from pretix.base.settings import GlobalSettingsObject
from pretix.control.utils.i18n import get_javascript_format_without_seconds, get_moment_locale
from .signals import footer_link, html_footer, html_head from .signals import footer_link, html_footer, html_head
@@ -71,4 +72,9 @@ def contextprocessor(request):
ctx['footer'] = _footer ctx['footer'] = _footer
ctx['site_url'] = settings.SITE_URL ctx['site_url'] = settings.SITE_URL
ctx['js_datetime_format'] = get_javascript_format_without_seconds('DATETIME_INPUT_FORMATS')
ctx['js_date_format'] = get_javascript_format_without_seconds('DATE_INPUT_FORMATS')
ctx['js_time_format'] = get_javascript_format_without_seconds('TIME_INPUT_FORMATS')
ctx['js_locale'] = get_moment_locale()
return ctx return ctx

View File

@@ -3,6 +3,8 @@ import os
from decimal import Decimal from decimal import Decimal
from itertools import chain from itertools import chain
import dateutil
import pytz
import vat_moss.errors import vat_moss.errors
import vat_moss.id import vat_moss.id
from django import forms from django import forms
@@ -18,8 +20,12 @@ from pretix.base.models import ItemVariation, Question
from pretix.base.models.orders import InvoiceAddress, OrderPosition from pretix.base.models.orders import InvoiceAddress, OrderPosition
from pretix.base.models.tax import EU_COUNTRIES, TAXED_ZERO from pretix.base.models.tax import EU_COUNTRIES, TAXED_ZERO
from pretix.base.templatetags.rich_text import rich_text from pretix.base.templatetags.rich_text import rich_text
from pretix.control.forms import (
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
)
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import contact_form_fields, question_form_fields from pretix.presale.signals import contact_form_fields, question_form_fields
from pretix.control.utils.i18n import get_format_without_seconds
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -235,6 +241,7 @@ class QuestionsForm(forms.Form):
initial = answers[0] initial = answers[0]
else: else:
initial = None initial = None
tz = pytz.timezone(event.settings.timezone)
if q.type == Question.TYPE_BOOLEAN: if q.type == Question.TYPE_BOOLEAN:
if q.required: if q.required:
# For some reason, django-bootstrap3 does not set the required attribute # For some reason, django-bootstrap3 does not set the required attribute
@@ -258,7 +265,7 @@ class QuestionsForm(forms.Form):
label=q.question, required=q.required, label=q.question, required=q.required,
help_text=q.help_text, help_text=q.help_text,
initial=initial.answer if initial else None, initial=initial.answer if initial 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(
@@ -295,7 +302,28 @@ class QuestionsForm(forms.Form):
label=q.question, required=q.required, label=q.question, required=q.required,
help_text=q.help_text, help_text=q.help_text,
initial=initial.file if initial else None, initial=initial.file if initial else None,
widget=UploadedFileWidget(position=pos, event=event, answer=initial) widget=UploadedFileWidget(position=pos, event=event, answer=initial),
)
elif q.type == Question.TYPE_DATE:
field = forms.DateField(
label=q.question, required=q.required,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None,
widget=DatePickerWidget(),
)
elif q.type == Question.TYPE_TIME:
field = forms.TimeField(
label=q.question, required=q.required,
help_text=q.help_text,
initial=dateutil.parser.parse(initial.answer).astimezone(tz).time() if initial and initial.answer else None,
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
field = forms.SplitDateTimeField(
label=q.question, required=q.required,
help_text=q.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')),
) )
field.question = q field.question = q
if answers: if answers:

View File

@@ -23,6 +23,7 @@
<script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script> <script type="text/javascript" src="{% static "moment/moment-with-locales.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script> <script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script> <script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script> <script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script> <script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
@@ -35,7 +36,7 @@
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}"> <link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %} {% block custom_header %}{% endblock %}
</head> </head>
<body data-locale="{{ request.LANGUAGE_CODE }}" data-now="{% now "U.u" %}"> <body data-locale="{{ request.LANGUAGE_CODE }}" data-now="{% now "U.u" %}" data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}" data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}">
{% block above %} {% block above %}
{% endblock %} {% endblock %}
<div class="container"> <div class="container">

View File

@@ -12,6 +12,88 @@ function ngettext(singular, plural, count) {
} }
return plural; return plural;
} }
var form_handlers = function (el) {
el.find(".datetimepicker").each(function() {
$(this).datetimepicker({
format: $("body").attr("data-datetimeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
});
if (!$(this).val()) {
$(this).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0));
}
});
el.find(".datepickerfield").each(function() {
var opts = {
format: $("body").attr("data-dateformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
},
};
$(this).datetimepicker(opts);
if ($(this).parent().is('.splitdatetimerow')) {
$(this).on("dp.change", function (ev) {
var $timepicker = $(this).closest(".splitdatetimerow").find(".timepickerfield");
var date = $(this).data('DateTimePicker').date();
if (date === null) {
return;
}
if ($timepicker.val() === "") {
date.set({'hour': 0, 'minute': 0, 'second': 0});
$timepicker.data('DateTimePicker').date(date);
}
});
}
});
el.find(".timepickerfield").each(function() {
var opts = {
format: $("body").attr("data-timeformat"),
locale: $("body").attr("data-datetimelocale"),
useCurrent: false,
showClear: !$(this).prop("required"),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
previous: 'fa fa-chevron-left',
next: 'fa fa-chevron-right',
today: 'fa fa-screenshot',
clear: 'fa fa-trash',
close: 'fa fa-remove'
}
};
$(this).datetimepicker(opts);
});
}
$(function () { $(function () {
"use strict"; "use strict";
@@ -119,6 +201,8 @@ $(function () {
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update); dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
}); });
form_handlers($("body"));
// Lightbox // Lightbox
lightbox.init(); lightbox.init();
}); });

View File

@@ -68,3 +68,27 @@
.panel-default>.accordion-radio+.panel-collapse>.panel-body { .panel-default>.accordion-radio+.panel-collapse>.panel-body {
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
.splitdatetimerow {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.help-block {
width: 100%;
}
}
.splitdatetimepart {
width: 50%;
display: inline-block;
&.datepickerfield {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
&.timepickerfield {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
border-left: 0;
}
}

View File

@@ -1,6 +1,7 @@
@import "_variables.scss"; @import "_variables.scss";
@import "../../pretixbase/scss/colors.scss"; @import "../../pretixbase/scss/colors.scss";
@import "../../bootstrap/scss/_bootstrap.scss"; @import "../../bootstrap/scss/_bootstrap.scss";
@import "../../datetimepicker/_bootstrap-datetimepicker.scss";
@import "../../fontawesome/scss/font-awesome.scss"; @import "../../fontawesome/scss/font-awesome.scss";
@import "_event.scss"; @import "_event.scss";

View File

@@ -14,7 +14,7 @@ from django_countries.fields import Country
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.models import ( from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemCategory, Order, CartPosition, Event, InvoiceAddress, Item, ItemCategory, Order,
OrderPosition, Organizer, Question, Quota, Voucher, OrderPosition, Organizer, Question, QuestionAnswer, Quota, Voucher,
) )
from pretix.base.models.items import ItemAddOn, ItemVariation, SubEventItem from pretix.base.models.items import ItemAddOn, ItemVariation, SubEventItem
from pretix.testutils.sessions import get_cart_session_key from pretix.testutils.sessions import get_cart_session_key
@@ -37,6 +37,7 @@ class CheckoutTestCase(TestCase):
category=self.category, default_price=23, admission=True, category=self.category, default_price=23, admission=True,
tax_rule=self.tr19) tax_rule=self.tr19)
self.quota_tickets.items.add(self.ticket) self.quota_tickets.items.add(self.ticket)
self.event.settings.set('timezone', 'UTC')
self.event.settings.set('attendee_names_asked', False) self.event.settings.set('attendee_names_asked', False)
self.event.settings.set('payment_banktransfer__enabled', True) self.event.settings.set('payment_banktransfer__enabled', True)
@@ -73,6 +74,52 @@ class CheckoutTestCase(TestCase):
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug), self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
target_status_code=200) target_status_code=200)
def test_timezone(self):
""" Test basic timezone change handling by date and time questions """
q1 = Question.objects.create(
event=self.event, question='When did you wake up today?', type=Question.TYPE_TIME,
required=True
)
q2 = Question.objects.create(
event=self.event, question='When was your last haircut?', type=Question.TYPE_DATE,
required=True
)
q3 = Question.objects.create(
event=self.event, question='When are you going to arrive?', type=Question.TYPE_DATETIME,
required=True
)
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.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-question_%s' % (cr.id, q1.id): '06:30',
'%s-question_%s' % (cr.id, q2.id): '2005-12-31',
'%s-question_%s_0' % (cr.id, q3.id): '2018-01-01',
'%s-question_%s_1' % (cr.id, q3.id): '5:23',
'email': 'admin@localhost',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), target_status_code=200)
self.event.settings.set('timezone', 'US/Central')
o1 = QuestionAnswer.objects.get(question=q1)
o2 = QuestionAnswer.objects.get(question=q2)
o3 = QuestionAnswer.objects.get(question=q3)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
total=4)
op = OrderPosition.objects.create(order=order, item=self.ticket, price=42)
o1.cartposition, o2.cartposition, o3.cartposition = None, None, None
o1.orderposition, o2.orderposition, o3.orderposition = op, op, op
# only time and date answers should be unaffected by timezone change
self.assertEqual(str(o1), '06:30')
self.assertEqual(str(o2), '2005-12-31')
o3date, o3time = str(o3).split(' ')
self.assertEqual(o3date, '2017-12-31')
self.assertEqual(o3time, '23:23')
def test_addon_questions(self): def test_addon_questions(self):
q1 = Question.objects.create( q1 = Question.objects.create(
event=self.event, question='Age', type=Question.TYPE_NUMBER, event=self.event, question='Age', type=Question.TYPE_NUMBER,