diff --git a/doc/api/resources/questions.rst b/doc/api/resources/questions.rst index eb3f916095..061fb7e5a3 100644 --- a/doc/api/resources/questions.rst +++ b/doc/api/resources/questions.rst @@ -54,11 +54,12 @@ dependency_question integer Internal ID of this attribute is set to the value given in ``dependency_value``. This cannot be combined with ``ask_during_checkin``. -dependency_value string The value ``dependency_question`` needs to be set to. - If ``dependency_question`` is set to a boolean - question, this should be ``"true"`` or ``"false"``. - Otherwise, it should be the ``identifier`` of a - question option. +dependency_values list of strings If ``dependency_question`` is set to a boolean + question, this should be ``["True"]`` or ``["False"]``. + Otherwise, it should be a list of ``identifier`` values + of question options. +dependency_value string An old version of ``dependency_values`` that only allows + for one value. **Deprecated.** ===================================== ========================== ======================================================= .. versionchanged:: 1.12 @@ -75,6 +76,10 @@ dependency_value string The value ``dep The attribute ``hidden`` and the question type ``CC`` have been added. +.. versionchanged:: 3.0 + + The attribute ``dependency_values`` has been added. + Endpoints --------- @@ -120,6 +125,7 @@ Endpoints "hidden": false, "dependency_question": null, "dependency_value": null, + "dependency_values": [], "options": [ { "id": 1, @@ -188,6 +194,7 @@ Endpoints "hidden": false, "dependency_question": null, "dependency_value": null, + "dependency_values": [], "options": [ { "id": 1, @@ -239,7 +246,7 @@ Endpoints "ask_during_checkin": false, "hidden": false, "dependency_question": null, - "dependency_value": null, + "dependency_values": [], "options": [ { "answer": {"en": "S"} @@ -274,6 +281,7 @@ Endpoints "hidden": false, "dependency_question": null, "dependency_value": null, + "dependency_values": [], "options": [ { "id": 1, @@ -346,6 +354,7 @@ Endpoints "hidden": false, "dependency_question": null, "dependency_value": null, + "dependency_values": [], "options": [ { "id": 1, diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index 7fb7fac146..c0cf2f5adf 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -201,15 +201,25 @@ class InlineQuestionOptionSerializer(I18nAwareModelSerializer): fields = ('id', 'identifier', 'answer', 'position') +class LegacyDependencyValueField(serializers.CharField): + + def to_representation(self, obj): + return obj[0] if obj else None + + def to_internal_value(self, data): + return [data] if data else [] + + class QuestionSerializer(I18nAwareModelSerializer): options = InlineQuestionOptionSerializer(many=True, required=False) identifier = serializers.CharField(allow_null=True) + dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True) class Meta: model = Question fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position', - 'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_value', - 'hidden') + 'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values', + 'hidden', 'dependency_value') def validate_identifier(self, value): Question._clean_identifier(self.context['event'], value, self.instance) @@ -263,6 +273,7 @@ class QuestionSerializer(I18nAwareModelSerializer): def create(self, validated_data): options_data = validated_data.pop('options') if 'options' in validated_data else [] items = validated_data.pop('items') + question = Question.objects.create(**validated_data) question.items.set(items) for opt_data in options_data: diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 19a2b6f203..80a633834c 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -1,4 +1,5 @@ import copy +import json import logging from decimal import Decimal from urllib.error import HTTPError @@ -10,6 +11,7 @@ import vat_moss.id from django import forms from django.contrib import messages from django.core.exceptions import ValidationError +from django.db.models import QuerySet from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import get_language, ugettext_lazy as _ @@ -25,6 +27,7 @@ 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 from pretix.control.forms import SplitDateTimeField +from pretix.helpers.escapejson import escapejson_attr from pretix.helpers.i18n import get_format_without_seconds from pretix.presale.signals import question_form_fields @@ -278,7 +281,7 @@ class BaseQuestionsForm(forms.Form): 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 + field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values)) if q.type != 'M': field.widget.attrs['required'] = q.required and not self.all_optional field._required = q.required and not self.all_optional @@ -299,26 +302,24 @@ class BaseQuestionsForm(forms.Form): question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)} - def question_is_visible(parentid, qval): + def question_is_visible(parentid, qvals): parentq = question_cache[parentid] - if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value): + if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): 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] + return ( + ('True' in qvals and dval) + or ('False' in qvals and not dval) + or (isinstance(dval, QuestionOption) and dval.identifier in qvals) + or (isinstance(dval, (list, QuerySet)) and any(qval in [o.identifier for o in dval] for qval in qvals)) + ) 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)) + (not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values)) ) if not self.all_optional: diff --git a/src/pretix/base/migrations/0127_auto_20190711_0705.py b/src/pretix/base/migrations/0127_auto_20190711_0705.py new file mode 100644 index 0000000000..226a53a243 --- /dev/null +++ b/src/pretix/base/migrations/0127_auto_20190711_0705.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.1 on 2019-07-11 07:05 + +from django.db import migrations + +import pretix.base.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0126_item_show_quota_left'), + ] + + operations = [ + migrations.RenameField( + model_name='question', + old_name='dependency_value', + new_name='dependency_values', + ), + migrations.AlterField( + model_name='question', + name='dependency_values', + field=pretix.base.models.fields.MultiStringField(default=['']), + ), + ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 260828e625..9635e47827 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -22,6 +22,7 @@ from i18nfield.fields import I18nCharField, I18nTextField from pretix.base.models import fields from pretix.base.models.base import LoggedModel +from pretix.base.models.fields import MultiStringField from pretix.base.models.tax import TaxedPrice from pretix.base.signals import quota_availability @@ -934,8 +935,8 @@ class Question(LoggedModel): :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 + :param dependency_values: The values that `dependency_question` needs to be set to for this question to be applicable. + :type dependency_values: list[str] """ TYPE_NUMBER = "N" TYPE_STRING = "S" @@ -1015,7 +1016,7 @@ class Question(LoggedModel): 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) + dependency_values = MultiStringField(default=[]) objects = ScopedManager(organizer='event__organizer') diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 266a1c4a0e..b549f6b172 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1038,18 +1038,17 @@ class AbstractPosition(models.Model): q.pk: q for q in questions } - def question_is_visible(parentid, qval): + def question_is_visible(parentid, qvals): parentq = question_cache[parentid] - if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value): + if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): return False if parentid not in self.answ: return False - if qval == 'True': - return self.answ[parentid].answer == 'True' - elif qval == 'False': - return self.answ[parentid].answer == 'False' - else: - return qval in [o.identifier for o in self.answ[parentid].options.all()] + return ( + ('True' in qvals and self.answ[parentid].answer == 'True') + or ('False' in qvals and self.answ[parentid].answer == 'False') + or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals)) + ) self.questions = [] for q in questions: @@ -1058,7 +1057,7 @@ class AbstractPosition(models.Model): q.answer.question = q # cache object else: q.answer = "" - if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_value): + if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values): self.questions.append(q) @property diff --git a/src/pretix/base/templatetags/escapejson.py b/src/pretix/base/templatetags/escapejson.py index 832cc711e2..e1b560a70e 100644 --- a/src/pretix/base/templatetags/escapejson.py +++ b/src/pretix/base/templatetags/escapejson.py @@ -1,3 +1,5 @@ +import json + from django import template from django.template.defaultfilters import stringfilter @@ -11,3 +13,9 @@ register = template.Library() def escapejs_filter(value): """Hex encodes characters for use in a application/json type script.""" return escapejson(value) + + +@register.filter("escapejson_dumps") +def escapejs_dumps_filter(value): + """Hex encodes characters for use in a application/json type script.""" + return escapejson(json.dumps(value)) diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index be6df06f44..43cae3c872 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -55,8 +55,13 @@ class QuestionForm(I18nModelForm): pk=self.instance.pk ) self.fields['identifier'].required = False + self.fields['dependency_values'].required = False self.fields['help_text'].widget.attrs['rows'] = 3 + def clean_dependency_values(self): + val = self.data.getlist('dependency_values') + return val + def clean_dependency_question(self): dep = val = self.cleaned_data.get('dependency_question') if dep: @@ -70,8 +75,8 @@ class QuestionForm(I18nModelForm): 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')]}) + if d.get('dependency_question') and not d.get('dependency_values'): + raise ValidationError({'dependency_values': [_('This field is required')]}) if d.get('dependency_question') and d.get('ask_during_checkin'): raise ValidationError(_('Dependencies between questions are not supported during check-in.')) return d @@ -89,13 +94,13 @@ class QuestionForm(I18nModelForm): 'identifier', 'items', 'dependency_question', - 'dependency_value' + 'dependency_values' ] widgets = { 'items': forms.CheckboxSelectMultiple( attrs={'class': 'scrolling-multiple-choice'} ), - 'dependency_value': forms.Select, + 'dependency_values': forms.SelectMultiple, } field_classes = { 'items': SafeModelMultipleChoiceField, diff --git a/src/pretix/control/templates/pretixcontrol/items/question_edit.html b/src/pretix/control/templates/pretixcontrol/items/question_edit.html index 93b7505127..64bd111854 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question_edit.html +++ b/src/pretix/control/templates/pretixcontrol/items/question_edit.html @@ -2,6 +2,7 @@ {% load i18n %} {% load bootstrap3 %} {% load formset_tags %} +{% load escapejson %} {% block title %} {% if question %} {% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %} @@ -120,8 +121,8 @@ {% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
- - {% bootstrap_field form.dependency_value layout="inline" form_group_class="inner" %} + + {% bootstrap_field form.dependency_values layout="inline" form_group_class="inner" %}
diff --git a/src/pretix/control/templates/pretixcontrol/items/quota_delete.html b/src/pretix/control/templates/pretixcontrol/items/quota_delete.html index d8d1f483c6..c6999d4d8b 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quota_delete.html +++ b/src/pretix/control/templates/pretixcontrol/items/quota_delete.html @@ -32,7 +32,7 @@ {% endif %} -
+
{% trans "Cancel" %} diff --git a/src/pretix/helpers/escapejson.py b/src/pretix/helpers/escapejson.py index 6628e54bef..abbe6d3d91 100644 --- a/src/pretix/helpers/escapejson.py +++ b/src/pretix/helpers/escapejson.py @@ -9,8 +9,23 @@ _json_escapes = { ord('&'): '\\u0026', } +_json_escapes_attr = { + ord('>'): '\\u003E', + ord('<'): '\\u003C', + ord('&'): '\\u0026', + ord('"'): '"', + ord("'"): ''', + ord("="): '=', +} + @keep_lazy(six.text_type, SafeText) def escapejson(value): """Hex encodes characters for use in a application/json type script.""" return mark_safe(force_text(value).translate(_json_escapes)) + + +@keep_lazy(six.text_type, SafeText) +def escapejson_attr(value): + """Hex encodes characters for use in a html attributw script.""" + return mark_safe(force_text(value).translate(_json_escapes_attr)) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 244fae4799..bfbfbf2dac 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -439,23 +439,22 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): q.pk: q for q in cp.item.questions_to_ask } - def question_is_visible(parentid, qval): + def question_is_visible(parentid, qvals): parentq = question_cache[parentid] - if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_value): + if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): 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()] + return ( + ('True' in qvals and answ[parentid].answer == 'True') + or ('False' in qvals and answ[parentid].answer == 'False') + or (any(qval in [o.identifier for o in answ[parentid].options.all()] for qval in qvals)) + ) 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)) + (not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values)) ) for q in cp.item.questions_to_ask: diff --git a/src/pretix/static/pretixcontrol/js/ui/question.js b/src/pretix/static/pretixcontrol/js/ui/question.js index ee534e8125..378dfe97c0 100644 --- a/src/pretix/static/pretixcontrol/js/ui/question.js +++ b/src/pretix/static/pretixcontrol/js/ui/question.js @@ -95,22 +95,22 @@ $(function () { $(".alert-required-boolean").toggle(show); } - var $val = $("#id_dependency_value"); + var $val = $("#id_dependency_values"); var $dq = $("#id_dependency_question"); - var oldval = $("#dependency_value_val").text(); + var oldval = JSON.parse($("#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); + $("#id_dependency_values option").remove(); + $("#id_dependency_values").prop("required", false); var val = $dq.children("option:selected").val(); if (!val) { - $("#id_dependency_value").show(); + $("#id_dependency_values").show(); $val.show(); return; } - $("#id_dependency_value").prop("required", true); + $("#id_dependency_values").prop("required", true); $val.hide(); $val.parent().append("
"); diff --git a/src/pretix/static/pretixpresale/js/ui/questions.js b/src/pretix/static/pretixpresale/js/ui/questions.js index ff43479ac6..ccee2b1cbc 100644 --- a/src/pretix/static/pretixpresale/js/ui/questions.js +++ b/src/pretix/static/pretixpresale/js/ui/questions.js @@ -7,30 +7,34 @@ function questions_toggle_dependent(ev) { } var dependency_name = $el.attr("name").split("_")[0] + "_" + $el.attr("data-question-dependency"); - var dependency_value = $el.attr("data-question-dependency-value"); + var dependency_values = JSON.parse($el.attr("data-question-dependency-values")); 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; + return q_should_be_shown($dependency_el) && $.inArray($dependency_el.val(), dependency_values) > -1; } } else if ($("input[type=checkbox][name=" + dependency_name + "]").length) { // dependency type is B or M - if (dependency_value === "True" || dependency_value === "False") { + if ($.inArray("True", dependency_values) > -1 || $.inArray("False", dependency_values) > -1) { $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'); - } + return q_should_be_shown($dependency_el) && ( + ($.inArray("True", dependency_values) > -1 && $dependency_el.prop('checked')) + || ($.inArray("False", dependency_values) > -1 && !$dependency_el.prop('checked')) + ); } } else { - $dependency_el = $("input[value=" + dependency_value + "][name=" + dependency_name + "]"); + var filter = ""; + for (var i = 0; i < dependency_values.length; i++) { + if (filter) filter += ", "; + filter += "input[value=" + dependency_values[i] + "][name=" + dependency_name + "]:checked"; + } + $dependency_el = $("input[value=" + dependency_values[0] + "][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'); + return q_should_be_shown($dependency_el) && $(filter).length; } } } diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py index 98194b20ed..0a5391c4c0 100644 --- a/src/tests/api/test_items.py +++ b/src/tests/api/test_items.py @@ -1651,6 +1651,7 @@ TEST_QUESTION_RES = { "position": 0, "dependency_question": None, "dependency_value": None, + "dependency_values": [], "options": [ { "id": 0, @@ -1883,6 +1884,49 @@ def test_question_delete(token_client, organizer, event, question): assert not event.questions.filter(pk=question.id).exists() +@pytest.mark.django_db +def test_question_update_dependency_values(token_client, organizer, event, question): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk), + { + "dependency_values": ["a", "b"] + }, + format='json' + ) + assert resp.status_code == 200 + question.refresh_from_db() + assert question.dependency_values == ["a", "b"] + + +@pytest.mark.django_db +def test_question_update_dependency_value_legacy(token_client, organizer, event, question): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk), + { + "dependency_value": "a" + }, + format='json' + ) + assert resp.status_code == 200 + question.refresh_from_db() + assert question.dependency_values == ["a"] + + +@pytest.mark.django_db +def test_question_update_dependency_value_legacy_conflict(token_client, organizer, event, question): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk), + { + "dependency_values": ["a", "b"], + "dependency_value": "a" + }, + format='json' + ) + assert resp.status_code == 200 + question.refresh_from_db() + assert question.dependency_values == ["a"] + + TEST_OPTIONS_RES = { "identifier": "LVETRWVU", "answer": {"en": "XL"}, diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py index fea6872126..703e066203 100644 --- a/src/tests/control/test_items.py +++ b/src/tests/control/test_items.py @@ -245,24 +245,24 @@ class QuestionsTest(ItemFormTest): form_data = extract_form_fields(doc.select('.container-fluid form')[0]) form_data['items'] = self.item1.id form_data['dependency_question'] = q1.pk - form_data['dependency_value'] = o1.identifier + form_data['dependency_values'] = o1.identifier doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q2.id), form_data) assert doc.select(".alert-success") q2.refresh_from_db() assert q2.dependency_question == q1 - assert q2.dependency_value == o1.identifier + assert q2.dependency_values == [o1.identifier] def test_set_dependency_circular(self): with scopes_disabled(): q1 = Question.objects.create(event=self.event1, question="What country are you from?", type="C", required=True) o1 = q1.options.create(answer='Germany') q2 = Question.objects.create(event=self.event1, question="What city are you from?", type="C", required=True, - dependency_question=q1, dependency_value=o1.identifier) + dependency_question=q1, dependency_values=[o1.identifier]) doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q1.id)) form_data = extract_form_fields(doc.select('.container-fluid form')[0]) form_data['dependency_question'] = q2.pk - form_data['dependency_value'] = '1' + form_data['dependency_values'] = '1' doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q1.id), form_data) assert not doc.select(".alert-success") @@ -274,7 +274,7 @@ class QuestionsTest(ItemFormTest): doc = self.get_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q2.id)) form_data = extract_form_fields(doc.select('.container-fluid form')[0]) form_data['dependency_question'] = q1.pk - form_data['dependency_value'] = '1' + form_data['dependency_values'] = '1' doc = self.post_doc('/control/event/%s/%s/questions/%s/change' % (self.orga1.slug, self.event1.slug, q2.id), form_data) assert not doc.select(".alert-success") diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 2e022ee452..8faae34316 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -2123,32 +2123,33 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase): ) self.q1.options.create(answer='Tech', identifier='TECH') self.q1.options.create(answer='Health', identifier='HEALTH') + self.q1.options.create(answer='IT', identifier='IT') self.q2a = self.event.questions.create( event=self.event, question='What is your occupation?', type=Question.TYPE_CHOICE_MULTIPLE, - required=False, dependency_question=self.q1, dependency_value='TECH' + required=False, dependency_question=self.q1, dependency_values=['TECH', 'IT'] ) self.q2a.options.create(answer='Software developer', identifier='DEV') self.q2a.options.create(answer='System administrator', identifier='ADMIN') self.q2b = self.event.questions.create( event=self.event, question='What is your occupation?', type=Question.TYPE_CHOICE_MULTIPLE, - required=True, dependency_question=self.q1, dependency_value='HEALTH' + required=True, dependency_question=self.q1, dependency_values=['HEALTH'] ) self.q2b.options.create(answer='Doctor', identifier='DOC') self.q2b.options.create(answer='Nurse', identifier='NURSE') self.q3 = self.event.questions.create( event=self.event, question='Do you like Python?', type=Question.TYPE_BOOLEAN, - required=False, dependency_question=self.q2a, dependency_value='DEV' + required=False, dependency_question=self.q2a, dependency_values=['DEV'] ) self.q4a = self.event.questions.create( event=self.event, question='Why?', type=Question.TYPE_TEXT, - required=True, dependency_question=self.q3, dependency_value='True' + required=True, dependency_question=self.q3, dependency_values=['True'] ) self.q4b = self.event.questions.create( event=self.event, question='Why not?', type=Question.TYPE_TEXT, - required=True, dependency_question=self.q3, dependency_value='False' + required=True, dependency_question=self.q3, dependency_values=['False'] ) self.ticket.questions.add(self.q1) self.ticket.questions.add(self.q2a) @@ -2188,6 +2189,15 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase): self.q4a: 'No curly braces!' }, should_fail=False) + def test_question_dependencies_second_path_alterative(self): + self._setup_dependency_questions() + self._test_question_input({ + self.q1: 'IT', + self.q2a: 'DEV', + self.q3: 'True', + self.q4a: 'No curly braces!' + }, should_fail=False) + def test_question_dependencies_subitem_required(self): self._setup_dependency_questions() self._test_question_input({ @@ -2202,6 +2212,14 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase): self.q3: 'True', }, should_fail=True) + def test_question_dependencies_subsubitem_required_alternative(self): + self._setup_dependency_questions() + self._test_question_input({ + self.q1: 'IT', + self.q2a: 'DEV', + self.q3: 'True', + }, should_fail=True) + def test_question_dependencies_parent_not_required(self): self._setup_dependency_questions() self._test_question_input({