forked from CGM_Public/pretix_original
Allow dependencies between questions (#1202)
- [x] data model - [x] api - [x] backend editor - [x] backend validation logic - [x] frontend display logic - [x] frontend validation logic - [x] test checkout step - [x] test modify order in frontend - [x] test modify order in backend - [x] validation tests - [x] correctly evaluate dependency tree in frontend? - [x] copy events
This commit is contained in:
@@ -159,12 +159,20 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier')
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
return value
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and 'options' in data:
|
||||
@@ -176,6 +184,18 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
TimePickerWidget, UploadedFileWidget,
|
||||
)
|
||||
from pretix.base.models import InvoiceAddress, Question
|
||||
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
||||
from pretix.base.models.tax import EU_COUNTRIES
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
@@ -145,6 +145,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
item = pos.item
|
||||
questions = pos.item.questions_to_ask
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -173,6 +174,7 @@ class BaseQuestionsForm(forms.Form):
|
||||
tz = pytz.timezone(event.settings.timezone)
|
||||
help_text = rich_text(q.help_text)
|
||||
label = escape(q.question) # django-bootstrap3 calls mark_safe
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if q.required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
@@ -187,26 +189,26 @@ class BaseQuestionsForm(forms.Form):
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
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(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
@@ -214,44 +216,46 @@ class BaseQuestionsForm(forms.Form):
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=q.options,
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=q.options,
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
to_field_name='identifier',
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
field = forms.FileField(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
field = forms.DateField(
|
||||
label=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=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=label, required=q.required,
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=dateutil.parser.parse(initial.answer).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 = SplitDateTimeField(
|
||||
label=label, required=q.required,
|
||||
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')),
|
||||
@@ -260,6 +264,15 @@ class BaseQuestionsForm(forms.Form):
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
|
||||
if q.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-value'] = q.dependency_value
|
||||
if q.type != 'M':
|
||||
field.widget.attrs['required'] = q.required and not self.all_optional
|
||||
field._required = q.required and not self.all_optional
|
||||
field.required = False
|
||||
|
||||
self.fields['question_%s' % q.id] = field
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
@@ -270,6 +283,40 @@ class BaseQuestionsForm(forms.Form):
|
||||
self.fields[key] = value
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)}
|
||||
|
||||
def question_is_visible(parentid, qval):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value):
|
||||
return False
|
||||
if 'question_%d' % parentid not in d:
|
||||
return False
|
||||
dval = d.get('question_%d' % parentid)
|
||||
if qval == 'True':
|
||||
return dval
|
||||
elif qval == 'False':
|
||||
return not dval
|
||||
elif isinstance(dval, QuestionOption):
|
||||
return dval.identifier == qval
|
||||
else:
|
||||
return qval in [o.identifier for o in dval]
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
q.required and
|
||||
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value))
|
||||
)
|
||||
|
||||
if not self.all_optional:
|
||||
for q in question_cache.values():
|
||||
if question_is_required(q) and not d.get('question_%d' % q.pk):
|
||||
raise ValidationError({'question_%d' % q.pk: [_('This field is required')]})
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
vat_warning = False
|
||||
|
||||
27
src/pretix/base/migrations/0113_auto_20190312_0942.py
Normal file
27
src/pretix/base/migrations/0113_auto_20190312_0942.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-12 09:42
|
||||
|
||||
import django.db.models.deletion
|
||||
import jsonfallback.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0112_auto_20190304_1726'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='dependency_question',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.Question'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='dependency_value',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -515,6 +515,10 @@ class Event(EventMixin, LoggedModel):
|
||||
o.question = q
|
||||
o.save()
|
||||
|
||||
for q in self.questions.filter(dependency_question__isnull=False):
|
||||
q.dependency_question = question_map[q.dependency_question_id]
|
||||
q.save(update_fields=['dependency_question'])
|
||||
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||
items = list(cl.limit_products.all())
|
||||
cl.pk = None
|
||||
|
||||
@@ -702,6 +702,10 @@ class Question(LoggedModel):
|
||||
:type ask_during_checkin: bool
|
||||
:param identifier: An arbitrary, internal identifier
|
||||
:type identifier: str
|
||||
:param dependency_question: This question will only show up if the referenced question is set to `dependency_value`.
|
||||
:type dependency_question: Question
|
||||
:param dependency_value: The value that `dependency_question` needs to be set to for this question to be applicable.
|
||||
:type dependency_value: str
|
||||
"""
|
||||
TYPE_NUMBER = "N"
|
||||
TYPE_STRING = "S"
|
||||
@@ -771,6 +775,10 @@ class Question(LoggedModel):
|
||||
'pretixdesk 0.2 or newer.'),
|
||||
default=False
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_value = models.TextField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question")
|
||||
|
||||
@@ -17,6 +17,7 @@ from pretix.base.models import (
|
||||
|
||||
class BaseQuestionsViewMixin:
|
||||
form_class = BaseQuestionsForm
|
||||
all_optional = False
|
||||
|
||||
@staticmethod
|
||||
def _keyfunc(pos):
|
||||
@@ -47,6 +48,7 @@ class BaseQuestionsViewMixin:
|
||||
prefix=cr.id,
|
||||
cartpos=cartpos,
|
||||
orderpos=orderpos,
|
||||
all_optional=self.all_optional,
|
||||
data=(self.request.POST if self.request.method == 'POST' else None),
|
||||
files=(self.request.FILES if self.request.method == 'POST' else None))
|
||||
form.pos = cartpos or orderpos
|
||||
@@ -154,7 +156,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
qqs = Question.objects.all()
|
||||
qqs = self.request.event.questions.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(ask_during_checkin=False)
|
||||
return list(self.order.positions.select_related(
|
||||
@@ -173,7 +175,7 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
),
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
))
|
||||
|
||||
|
||||
@@ -36,14 +36,39 @@ class CategoryForm(I18nModelForm):
|
||||
class QuestionForm(I18nModelForm):
|
||||
question = I18nFormField(
|
||||
label=_("Question"),
|
||||
widget_kwargs={'attrs': {'rows': 5}},
|
||||
widget_kwargs={'attrs': {'rows': 2}},
|
||||
widget=I18nTextarea
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['items'].queryset = self.instance.event.items.all()
|
||||
self.fields['dependency_question'].queryset = self.instance.event.questions.filter(
|
||||
type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE)
|
||||
)
|
||||
if self.instance.pk:
|
||||
self.fields['dependency_question'].queryset = self.fields['dependency_question'].queryset.exclude(
|
||||
pk=self.instance.pk
|
||||
)
|
||||
self.fields['identifier'].required = False
|
||||
self.fields['help_text'].widget.attrs['rows'] = 3
|
||||
|
||||
def clean_dependency_question(self):
|
||||
dep = val = self.cleaned_data.get('dependency_question')
|
||||
if dep:
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
return val
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if d.get('dependency_question') and not d.get('dependency_value'):
|
||||
raise ValidationError({'dependency_value': [_('This field is required')]})
|
||||
return d
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
@@ -55,12 +80,15 @@ class QuestionForm(I18nModelForm):
|
||||
'required',
|
||||
'ask_during_checkin',
|
||||
'identifier',
|
||||
'items'
|
||||
'items',
|
||||
'dependency_question',
|
||||
'dependency_value'
|
||||
]
|
||||
widgets = {
|
||||
'items': forms.CheckboxSelectMultiple(
|
||||
attrs={'class': 'scrolling-multiple-choice'}
|
||||
),
|
||||
'dependency_value': forms.Select,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
||||
@@ -70,7 +71,10 @@
|
||||
</head>
|
||||
<body data-datetimeformat="{{ js_datetime_format }}" data-timeformat="{{ js_time_format }}"
|
||||
data-dateformat="{{ js_date_format }}" data-datetimelocale="{{ js_locale }}"
|
||||
data-pretixlocale="{{ request.LANGUAGE_CODE }}"
|
||||
data-payment-weekdays-disabled="{{ js_payment_weekdays_disabled }}"
|
||||
{% if request.organizer %}data-organizer="{{ request.organizer.slug }}"{% endif %}
|
||||
{% if request.event %}data-event="{{ request.event.slug }}"{% endif %}
|
||||
data-select2-locale="{{ select2locale }}" data-longdateformat="{{ js_long_date_format }}" class="nojs">
|
||||
<div id="wrapper">
|
||||
<nav class="navbar navbar-inverse navbar-static-top" role="navigation">
|
||||
|
||||
@@ -21,15 +21,9 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Apply to products" %}</legend>
|
||||
{% bootstrap_field form.items layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
</fieldset>
|
||||
<div class="alert alert-info alert-required-boolean">
|
||||
{% blocktrans trimmed %}
|
||||
@@ -110,6 +104,26 @@
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_dependency_question">
|
||||
{% trans "Question dependency" %}
|
||||
<br><span class="optional">{% trans "Optional" context "form" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<script type="text/plain" id="dependency_value_val">{{ form.instance.dependency_value }}</script>
|
||||
{% bootstrap_field form.dependency_value layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -414,10 +414,34 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
|
||||
for cp in self._positions_for_questions:
|
||||
answ = {
|
||||
aw.question_id: aw.answer for aw in cp.answerlist
|
||||
aw.question_id: aw for aw in cp.answerlist
|
||||
}
|
||||
question_cache = {
|
||||
q.pk: q for q in cp.item.questions_to_ask
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qval):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value):
|
||||
return False
|
||||
if parentid not in answ:
|
||||
return False
|
||||
if qval == 'True':
|
||||
return answ[parentid].answer == 'True'
|
||||
elif qval == 'False':
|
||||
return answ[parentid].answer == 'False'
|
||||
else:
|
||||
return qval in [o.identifier for o in answ[parentid].options.all()]
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
q.required and
|
||||
(not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value))
|
||||
)
|
||||
|
||||
for q in cp.item.questions_to_ask:
|
||||
if q.required and q.id not in answ:
|
||||
print("question", q, "is required", question_is_required(q), "has answer", q.id in answ)
|
||||
if question_is_required(q) and not answ.get(q.id):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<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 "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.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/details.js" %}"></script>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</h4>
|
||||
</summary>
|
||||
<div>
|
||||
<div class="panel-body" data-idx="{{ forloop.counter0 }}">
|
||||
<div class="panel-body questions-form" data-idx="{{ forloop.counter0 }}">
|
||||
{% if pos.addons.all %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
@@ -97,7 +97,7 @@
|
||||
{# Add-Ons #}
|
||||
<legend>+ {{ form.pos.item }}</legend>
|
||||
{% endif %}
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
</h4>
|
||||
</summary>
|
||||
<div id="cp{{ pos.id }}">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body questions-form">
|
||||
{% for form in forms %}
|
||||
{% if form.pos.item != pos.item %}
|
||||
{# Add-Ons #}
|
||||
<legend>+ {{ form.pos.item }}</legend>
|
||||
{% endif %}
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ class QuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def _positions_for_questions(self):
|
||||
qqs = Question.objects.all()
|
||||
qqs = self.request.event.questions.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(ask_during_checkin=False)
|
||||
cart = get_cart(self.request).select_related(
|
||||
@@ -33,7 +33,7 @@ class QuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
),
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
)
|
||||
return sorted(list(cart), key=self._keyfunc)
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/*global $,gettext*/
|
||||
|
||||
function question_page_toggle_view() {
|
||||
var show = $("#id_type").val() == "C" || $("#id_type").val() == "M";
|
||||
$("#answer-options").toggle(show);
|
||||
|
||||
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
|
||||
$(".alert-required-boolean").toggle(show);
|
||||
}
|
||||
|
||||
var waitingDialog = {
|
||||
show: function (message) {
|
||||
"use strict";
|
||||
@@ -34,6 +26,26 @@ var ajaxErrDialog = {
|
||||
}
|
||||
};
|
||||
|
||||
var apiGET = function (url, callback) {
|
||||
$.getJSON(url, function (data) {
|
||||
callback(data);
|
||||
});
|
||||
};
|
||||
|
||||
var i18nToString = function (i18nstring) {
|
||||
var locale = $("body").attr("data-pretixlocale");
|
||||
if (i18nstring[locale]) {
|
||||
return i18nstring[locale];
|
||||
} else if (i18nstring["en"]) {
|
||||
return i18nstring["en"];
|
||||
}
|
||||
for (key in i18nstring) {
|
||||
if (i18nstring[key]) {
|
||||
return i18nstring[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ajaxError(function (event, jqXHR, settings, thrownError) {
|
||||
waitingDialog.hide();
|
||||
var c = $(jqXHR.responseText).filter('.container');
|
||||
@@ -423,6 +435,9 @@ var form_handlers = function (el) {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
|
||||
questions_toggle_dependent();
|
||||
};
|
||||
|
||||
$(function () {
|
||||
@@ -491,14 +506,6 @@ $(function () {
|
||||
window.location.hash = e.target.hash;
|
||||
});
|
||||
|
||||
// Question editor
|
||||
if ($("#answer-options").length) {
|
||||
|
||||
$("#id_type").change(question_page_toggle_view);
|
||||
$("#id_required").change(question_page_toggle_view);
|
||||
question_page_toggle_view();
|
||||
}
|
||||
|
||||
// Event wizard
|
||||
$("#event-slug-random-generate").click(function () {
|
||||
var url = $(this).attr("data-rng-url");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*global $, Morris, gettext*/
|
||||
$(function () {
|
||||
// Question view
|
||||
if (!$("#question-stats").length) {
|
||||
return;
|
||||
}
|
||||
@@ -73,3 +74,66 @@ $(function () {
|
||||
|
||||
// N, S, T
|
||||
});
|
||||
|
||||
$(function () {
|
||||
// Question editor
|
||||
|
||||
if (!$("#answer-options").length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Question editor
|
||||
$("#id_type").change(question_page_toggle_view);
|
||||
$("#id_required").change(question_page_toggle_view);
|
||||
question_page_toggle_view();
|
||||
|
||||
function question_page_toggle_view() {
|
||||
var show = $("#id_type").val() == "C" || $("#id_type").val() == "M";
|
||||
$("#answer-options").toggle(show);
|
||||
|
||||
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
|
||||
$(".alert-required-boolean").toggle(show);
|
||||
}
|
||||
|
||||
var $val = $("#id_dependency_value");
|
||||
var $dq = $("#id_dependency_question");
|
||||
var oldval = $("#dependency_value_val").text();
|
||||
function update_dependency_options() {
|
||||
$val.parent().find(".loading-indicator").remove();
|
||||
$("#id_dependency_value option").remove();
|
||||
$("#id_dependency_value").prop("required", false);
|
||||
|
||||
var val = $dq.children("option:selected").val();
|
||||
if (!val) {
|
||||
$("#id_dependency_value").show();
|
||||
$val.show();
|
||||
return;
|
||||
}
|
||||
|
||||
$("#id_dependency_value").prop("required", true);
|
||||
$val.hide();
|
||||
$val.parent().append("<div class=\"help-block loading-indicator\"><span class=\"fa" +
|
||||
" fa-cog fa-spin\"></span></div>");
|
||||
|
||||
apiGET('/api/v1/organizers/' + $("body").attr("data-organizer") + '/events/' + $("body").attr("data-event") + '/questions/' + val + '/', function (data) {
|
||||
if (data.type === "B") {
|
||||
$val.append($("<option>").attr("value", "True").text(gettext("Ja")));
|
||||
$val.append($("<option>").attr("value", "False").text(gettext("Nein")));
|
||||
} else {
|
||||
for (var i = 0; i < data.options.length; i++) {
|
||||
var opt = data.options[i];
|
||||
var $opt = $("<option>").attr("value", opt.identifier).text(i18nToString(opt.answer));
|
||||
$val.append($opt);
|
||||
}
|
||||
}
|
||||
if (oldval) {
|
||||
$val.val(oldval);
|
||||
}
|
||||
$val.parent().find(".loading-indicator").remove();
|
||||
$val.show();
|
||||
});
|
||||
}
|
||||
|
||||
update_dependency_options();
|
||||
$dq.change(update_dependency_options);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ function gettext(msgid) {
|
||||
}
|
||||
return msgid;
|
||||
}
|
||||
|
||||
function ngettext(singular, plural, count) {
|
||||
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
|
||||
return django.ngettext(singular, plural, count);
|
||||
@@ -14,7 +15,7 @@ function ngettext(singular, plural, count) {
|
||||
}
|
||||
|
||||
var form_handlers = function (el) {
|
||||
el.find(".datetimepicker").each(function() {
|
||||
el.find(".datetimepicker").each(function () {
|
||||
$(this).datetimepicker({
|
||||
format: $("body").attr("data-datetimeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
@@ -37,7 +38,7 @@ var form_handlers = function (el) {
|
||||
}
|
||||
});
|
||||
|
||||
el.find(".datepickerfield").each(function() {
|
||||
el.find(".datepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $("body").attr("data-dateformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
@@ -71,7 +72,7 @@ var form_handlers = function (el) {
|
||||
}
|
||||
});
|
||||
|
||||
el.find(".timepickerfield").each(function() {
|
||||
el.find(".timepickerfield").each(function () {
|
||||
var opts = {
|
||||
format: $("body").attr("data-timeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
@@ -90,7 +91,7 @@ var form_handlers = function (el) {
|
||||
}
|
||||
};
|
||||
$(this).datetimepicker(opts);
|
||||
});
|
||||
});
|
||||
|
||||
el.find("script[data-replace-with-qr]").each(function () {
|
||||
var $div = $("<div>");
|
||||
@@ -104,7 +105,10 @@ var form_handlers = function (el) {
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
|
||||
questions_toggle_dependent();
|
||||
};
|
||||
|
||||
|
||||
$(function () {
|
||||
@@ -130,7 +134,7 @@ $(function () {
|
||||
$("#voucher-box").slideDown();
|
||||
$("#voucher-toggle").slideUp();
|
||||
});
|
||||
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
|
||||
@@ -140,7 +144,7 @@ $(function () {
|
||||
$('.toggle-variation-description').click(function () {
|
||||
$(this).parent().find('.addon-variation-description').slideToggle();
|
||||
});
|
||||
|
||||
|
||||
// Copy answers
|
||||
$(".js-copy-answers").click(function (e) {
|
||||
e.preventDefault();
|
||||
@@ -153,7 +157,7 @@ $(function () {
|
||||
// Subevent choice
|
||||
if ($(".subevent-toggle").length) {
|
||||
$(".subevent-list").hide();
|
||||
$(".subevent-toggle").css("display", "block").click(function() {
|
||||
$(".subevent-toggle").css("display", "block").click(function () {
|
||||
$(".subevent-list").slideToggle(300);
|
||||
});
|
||||
}
|
||||
@@ -165,7 +169,7 @@ $(function () {
|
||||
var update_cart_form = function () {
|
||||
var is_enabled = $(".product-row input[type=checkbox]:checked, .variations input[type=checkbox]:checked, .product-row input[type=radio]:checked, .variations input[type=radio]:checked").length;
|
||||
if (!is_enabled) {
|
||||
$(".input-item-count").each(function() {
|
||||
$(".input-item-count").each(function () {
|
||||
if ($(this).val() && $(this).val() !== "0") {
|
||||
is_enabled = true;
|
||||
}
|
||||
@@ -185,18 +189,18 @@ $(function () {
|
||||
|
||||
// Invoice address form
|
||||
$("input[data-required-if]").each(function () {
|
||||
var dependent = $(this),
|
||||
dependency = $($(this).attr("data-required-if")),
|
||||
update = function (ev) {
|
||||
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
|
||||
if (!dependent.is("[data-no-required-attr]")) {
|
||||
dependent.prop('required', enabled);
|
||||
}
|
||||
dependent.closest('.form-group').toggleClass('required', enabled);
|
||||
};
|
||||
update();
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
|
||||
var dependent = $(this),
|
||||
dependency = $($(this).attr("data-required-if")),
|
||||
update = function (ev) {
|
||||
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
|
||||
if (!dependent.is("[data-no-required-attr]")) {
|
||||
dependent.prop('required', enabled);
|
||||
}
|
||||
dependent.closest('.form-group').toggleClass('required', enabled);
|
||||
};
|
||||
update();
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
|
||||
});
|
||||
|
||||
$("input[data-display-dependency]").each(function () {
|
||||
@@ -219,16 +223,16 @@ $(function () {
|
||||
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
|
||||
});
|
||||
|
||||
form_handlers($("body"));
|
||||
form_handlers($("body"));
|
||||
|
||||
// Lightbox
|
||||
lightbox.init();
|
||||
});
|
||||
|
||||
function copy_answers(idx) {
|
||||
var elements = $('*[data-idx="'+idx+'"] input, *[data-idx="'+idx+'"] select, *[data-idx="'+idx+'"] textarea');
|
||||
function copy_answers(idx) {
|
||||
var elements = $('*[data-idx="' + idx + '"] input, *[data-idx="' + idx + '"] select, *[data-idx="' + idx + '"] textarea');
|
||||
var firstAnswers = $('*[data-idx="0"] input, *[data-idx="0"] select, *[data-idx="0"] textarea');
|
||||
elements.each(function(index){
|
||||
elements.each(function (index) {
|
||||
var input = $(this),
|
||||
tagName = input.prop('tagName').toLowerCase(),
|
||||
attributeType = input.attr('type'),
|
||||
@@ -250,14 +254,20 @@ function copy_answers(idx) {
|
||||
break;
|
||||
case "checkbox":
|
||||
case "radio":
|
||||
input.prop("checked", firstAnswers.filter("[name$=" + suffix + "]").prop("checked"));
|
||||
if (input.attr('value')) {
|
||||
input.prop("checked", firstAnswers.filter("[name$=" + suffix + "][value=" + input.attr('value') + "]").prop("checked"));
|
||||
} else {
|
||||
input.prop("checked", firstAnswers.filter("[name$=" + suffix + "]").prop("checked"));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
input.val(firstAnswers.filter("[name$=" + suffix + "]").val());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
input.val(firstAnswers.filter("[name$=" + suffix + "]").val());
|
||||
}
|
||||
}
|
||||
});
|
||||
questions_toggle_dependent(true);
|
||||
}
|
||||
|
||||
|
||||
71
src/pretix/static/pretixpresale/js/ui/questions.js
Normal file
71
src/pretix/static/pretixpresale/js/ui/questions.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/*global $ */
|
||||
|
||||
function questions_toggle_dependent(ev) {
|
||||
function q_should_be_shown($el) {
|
||||
if (!$el.attr('data-question-dependency')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var dependency_name = $el.attr("name").split("_")[0] + "_" + $el.attr("data-question-dependency");
|
||||
var dependency_value = $el.attr("data-question-dependency-value");
|
||||
var $dependency_el;
|
||||
|
||||
if ($("select[name=" + dependency_name + "]").length) {
|
||||
// dependency is type C
|
||||
$dependency_el = $("select[name=" + dependency_name + "]");
|
||||
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
|
||||
return q_should_be_shown($dependency_el) && $dependency_el.val() === dependency_value;
|
||||
}
|
||||
} else if ($("input[type=checkbox][name=" + dependency_name + "]").length) {
|
||||
// dependency type is B or M
|
||||
if (dependency_value === "True" || dependency_value === "False") {
|
||||
$dependency_el = $("input[name=" + dependency_name + "]");
|
||||
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
|
||||
if (dependency_value === "True") {
|
||||
return q_should_be_shown($dependency_el) && $dependency_el.prop('checked');
|
||||
} else {
|
||||
return q_should_be_shown($dependency_el) && !$dependency_el.prop('checked');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dependency_el = $("input[value=" + dependency_value + "][name=" + dependency_name + "]");
|
||||
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
|
||||
return q_should_be_shown($dependency_el) && $dependency_el.prop('checked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$("[data-question-dependency]").each(function () {
|
||||
var $dependent = $(this).closest(".form-group");
|
||||
var is_shown = !$dependent.hasClass("dependency-hidden");
|
||||
var should_be_shown = q_should_be_shown($(this));
|
||||
|
||||
if (should_be_shown && !is_shown) {
|
||||
$dependent.stop().removeClass("dependency-hidden");
|
||||
if (!ev) {
|
||||
$dependent.show();
|
||||
} else {
|
||||
$dependent.slideDown();
|
||||
}
|
||||
$dependent.find("input.required-hidden, select.required-hidden, textarea.required-hidden").each(function () {
|
||||
$(this).prop("required", true).removeClass("required-hidden");
|
||||
});
|
||||
} else if (!should_be_shown && is_shown) {
|
||||
if ($dependent.hasClass("has-error") || $dependent.find(".has-error").length) {
|
||||
// Do not hide things with invalid validation
|
||||
return;
|
||||
}
|
||||
$dependent.stop().addClass("dependency-hidden");
|
||||
if (!ev) {
|
||||
$dependent.hide();
|
||||
} else {
|
||||
$dependent.slideUp();
|
||||
}
|
||||
$dependent.find("input[required], select[required], textarea[required]").each(function () {
|
||||
$(this).prop("required", false).addClass("required-hidden");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user