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:
Raphael Michel
2019-03-13 16:49:20 +01:00
committed by GitHub
parent d10cbd07a7
commit f95e8f374d
22 changed files with 825 additions and 211 deletions

View File

@@ -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

View File

@@ -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

View 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),
),
]

View File

@@ -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

View File

@@ -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")

View File

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

View File

@@ -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,
}

View File

@@ -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">

View File

@@ -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" %}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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");

View File

@@ -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);
});

View File

@@ -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);
}

View 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");
});
}
});
}