mirror of
https://github.com/pretix/pretix.git
synced 2026-08-07 10:17:49 +00:00
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:
@@ -46,6 +46,16 @@ options list of objects In case of ques
|
||||
├ identifier string An arbitrary string that can be used for matching with
|
||||
other sources.
|
||||
└ answer multi-lingual string The displayed value of this option
|
||||
dependency_question integer Internal ID of a different question. The current
|
||||
question will only be shown if the question given in
|
||||
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.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.12
|
||||
@@ -100,6 +110,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -165,6 +177,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -214,6 +228,8 @@ Endpoints
|
||||
"items": [1, 2],
|
||||
"position": 1,
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"answer": {"en": "S"}
|
||||
@@ -245,6 +261,8 @@ Endpoints
|
||||
"position": 1,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -314,6 +332,8 @@ Endpoints
|
||||
"position": 2,
|
||||
"identifier": "WY3TP9SL",
|
||||
"ask_during_checkin": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
@@ -1302,6 +1302,8 @@ TEST_QUESTION_RES = {
|
||||
"ask_during_checkin": False,
|
||||
"identifier": "ABC",
|
||||
"position": 0,
|
||||
"dependency_question": None,
|
||||
"dependency_value": None,
|
||||
"options": [
|
||||
{
|
||||
"id": 0,
|
||||
@@ -1418,6 +1420,65 @@ def test_question_create(token_client, organizer, event, event2, item):
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"identifier":["This identifier is already used for a different question."]}'
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"question": "What's your name?",
|
||||
"type": "S",
|
||||
"required": True,
|
||||
"items": [item.pk],
|
||||
"position": 0,
|
||||
"ask_during_checkin": False,
|
||||
"dependency_question": question.pk,
|
||||
"dependency_value": "1",
|
||||
"identifier": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"dependency_question":["Question dependencies can only be set to boolean or choice questions."]}'
|
||||
|
||||
question.type = Question.TYPE_BOOLEAN
|
||||
question.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"question": "What's your name?",
|
||||
"type": "S",
|
||||
"required": True,
|
||||
"items": [item.pk],
|
||||
"position": 0,
|
||||
"ask_during_checkin": True,
|
||||
"dependency_question": question.pk,
|
||||
"dependency_value": "1",
|
||||
"identifier": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["Dependencies are not supported during check-in."]}'
|
||||
|
||||
question.type = Question.TYPE_BOOLEAN
|
||||
question.save()
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/questions/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"question": "What's your name?",
|
||||
"type": "S",
|
||||
"required": True,
|
||||
"items": [item.pk],
|
||||
"position": 0,
|
||||
"ask_during_checkin": False,
|
||||
"dependency_question": question.pk,
|
||||
"dependency_value": "1",
|
||||
"identifier": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
q2 = Question.objects.get(pk=resp.data['id'])
|
||||
assert q2.dependency_question == question
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_update(token_client, organizer, event, question):
|
||||
@@ -1429,13 +1490,26 @@ def test_question_update(token_client, organizer, event, question):
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
print(resp.content)
|
||||
assert resp.status_code == 200
|
||||
question = Question.objects.get(pk=resp.data['id'])
|
||||
assert question.question == "What's your shoe size?"
|
||||
assert question.type == "N"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_update_circular_dependency(token_client, organizer, event, question):
|
||||
q2 = event.questions.create(question="T-Shirt size", type="B", identifier="FOO", dependency_question=question)
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/questions/{}/'.format(organizer.slug, event.slug, question.pk),
|
||||
{
|
||||
"dependency_question": q2.pk
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["Circular dependency between questions detected."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_update_options(token_client, organizer, event, question, item):
|
||||
resp = token_client.patch(
|
||||
|
||||
@@ -212,6 +212,45 @@ class QuestionsTest(ItemFormTest):
|
||||
tbl = doc.select('.container-fluid table.table-bordered tbody')[0]
|
||||
assert tbl.select('tr')[0].select('td')[0].text.strip() == '42'
|
||||
|
||||
def test_set_dependency(self):
|
||||
q1 = Question.objects.create(event=self.event1, question="What country are you from?", type="C", required=True)
|
||||
q2 = Question.objects.create(event=self.event1, question="What city are you from?", type="T", required=True)
|
||||
o1 = q1.options.create(answer='Germany')
|
||||
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'] = 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
|
||||
|
||||
def test_set_dependency_circular(self):
|
||||
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)
|
||||
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'
|
||||
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")
|
||||
|
||||
def test_set_dependency_to_non_choice(self):
|
||||
q1 = Question.objects.create(event=self.event1, question="What country are you from?", type="N", required=True)
|
||||
q2 = Question.objects.create(event=self.event1, question="What city are you from?", type="T", required=True)
|
||||
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'
|
||||
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")
|
||||
|
||||
|
||||
class QuotaTest(ItemFormTest):
|
||||
|
||||
|
||||
+285
-135
@@ -22,7 +22,7 @@ from pretix.base.models.items import ItemAddOn, ItemVariation, SubEventItem
|
||||
from pretix.testutils.sessions import get_cart_session_key
|
||||
|
||||
|
||||
class CheckoutTestCase(TestCase):
|
||||
class BaseCheckoutTestCase:
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.orga = Organizer.objects.create(name='CCC', slug='ccc')
|
||||
@@ -60,6 +60,14 @@ class CheckoutTestCase(TestCase):
|
||||
self.workshopquota.variations.add(self.workshop2a)
|
||||
self.workshopquota.variations.add(self.workshop2b)
|
||||
|
||||
def _set_session(self, key, value):
|
||||
session = self.client.session
|
||||
session['carts'][get_cart_session_key(self.client, self.event)][key] = value
|
||||
session.save()
|
||||
|
||||
|
||||
class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
|
||||
def _enable_reverse_charge(self):
|
||||
self.tr19.eu_reverse_charge = True
|
||||
self.tr19.home_country = Country('DE')
|
||||
@@ -76,135 +84,6 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
def test_timezone(self):
|
||||
""" Test basic timezone change handling by date and time questions """
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='When did you wake up today?', type=Question.TYPE_TIME,
|
||||
required=True
|
||||
)
|
||||
q2 = Question.objects.create(
|
||||
event=self.event, question='When was your last haircut?', type=Question.TYPE_DATE,
|
||||
required=True
|
||||
)
|
||||
q3 = Question.objects.create(
|
||||
event=self.event, question='When are you going to arrive?', type=Question.TYPE_DATETIME,
|
||||
required=True
|
||||
)
|
||||
self.ticket.questions.add(q1)
|
||||
self.ticket.questions.add(q2)
|
||||
self.ticket.questions.add(q3)
|
||||
cr = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr.id, q1.id): '06:30',
|
||||
'%s-question_%s' % (cr.id, q2.id): '2005-12-31',
|
||||
'%s-question_%s_0' % (cr.id, q3.id): '2018-01-01',
|
||||
'%s-question_%s_1' % (cr.id, q3.id): '5:23',
|
||||
'email': 'admin@localhost',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), target_status_code=200)
|
||||
self.event.settings.set('timezone', 'US/Central')
|
||||
o1 = QuestionAnswer.objects.get(question=q1)
|
||||
o2 = QuestionAnswer.objects.get(question=q2)
|
||||
o3 = QuestionAnswer.objects.get(question=q3)
|
||||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||||
expires=now() + timedelta(days=3),
|
||||
total=4)
|
||||
op = OrderPosition.objects.create(order=order, item=self.ticket, price=42)
|
||||
o1.cartposition, o2.cartposition, o3.cartposition = None, None, None
|
||||
o1.orderposition, o2.orderposition, o3.orderposition = op, op, op
|
||||
# only time and date answers should be unaffected by timezone change
|
||||
self.assertEqual(str(o1), '06:30')
|
||||
self.assertEqual(str(o2), '2005-12-31')
|
||||
o3date, o3time = str(o3).split(' ')
|
||||
self.assertEqual(o3date, '2017-12-31')
|
||||
self.assertEqual(o3time, '23:23')
|
||||
|
||||
def test_addon_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
required=True
|
||||
)
|
||||
q1.items.add(self.ticket)
|
||||
q1.items.add(self.workshop1)
|
||||
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||
price_included=True)
|
||||
cp1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp1.answers.create(question=q1, answer='12')
|
||||
cp2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.workshop1, addon_to=cp1,
|
||||
price=0, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp2.answers.create(question=q1, answer='12')
|
||||
|
||||
self._set_session('payment', 'banktransfer')
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.ticket).first().answers.first().answer, '12')
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).first().answers.first().answer, '12')
|
||||
|
||||
def test_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
required=True
|
||||
)
|
||||
q2 = Question.objects.create(
|
||||
event=self.event, question='How have you heard from us?', type=Question.TYPE_STRING,
|
||||
required=False
|
||||
)
|
||||
self.ticket.questions.add(q1)
|
||||
self.ticket.questions.add(q2)
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cr2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=20, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr1.id, q1.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr2.id, q1.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr1.id, q2.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr2.id, q2.id))), 1)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr1.id, q1.id): '42',
|
||||
'%s-question_%s' % (cr2.id, q1.id): '',
|
||||
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
|
||||
'%s-question_%s' % (cr2.id, q2.id): '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr1.id, q1.id): '42',
|
||||
'%s-question_%s' % (cr2.id, q1.id): '23',
|
||||
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
|
||||
'%s-question_%s' % (cr2.id, q2.id): '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
cr2 = CartPosition.objects.get(id=cr2.id)
|
||||
self.assertEqual(cr1.answers.filter(question=q1).count(), 1)
|
||||
self.assertEqual(cr2.answers.filter(question=q1).count(), 1)
|
||||
self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
|
||||
self.assertFalse(cr2.answers.filter(question=q2).exists())
|
||||
|
||||
def test_reverse_charge(self):
|
||||
self.tr19.eu_reverse_charge = True
|
||||
self.tr19.home_country = Country('DE')
|
||||
@@ -900,11 +779,6 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
def _set_session(self, key, value):
|
||||
session = self.client.session
|
||||
session['carts'][get_cart_session_key(self.client, self.event)][key] = value
|
||||
session.save()
|
||||
|
||||
def test_subevent(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
@@ -1831,3 +1705,279 @@ class CheckoutTestCase(TestCase):
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
assert not Order.objects.last().testmode
|
||||
assert "0" not in Order.objects.last().code
|
||||
|
||||
|
||||
class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
|
||||
|
||||
def test_timezone(self):
|
||||
""" Test basic timezone change handling by date and time questions """
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='When did you wake up today?', type=Question.TYPE_TIME,
|
||||
required=True
|
||||
)
|
||||
q2 = Question.objects.create(
|
||||
event=self.event, question='When was your last haircut?', type=Question.TYPE_DATE,
|
||||
required=True
|
||||
)
|
||||
q3 = Question.objects.create(
|
||||
event=self.event, question='When are you going to arrive?', type=Question.TYPE_DATETIME,
|
||||
required=True
|
||||
)
|
||||
self.ticket.questions.add(q1)
|
||||
self.ticket.questions.add(q2)
|
||||
self.ticket.questions.add(q3)
|
||||
cr = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr.id, q1.id): '06:30',
|
||||
'%s-question_%s' % (cr.id, q2.id): '2005-12-31',
|
||||
'%s-question_%s_0' % (cr.id, q3.id): '2018-01-01',
|
||||
'%s-question_%s_1' % (cr.id, q3.id): '5:23',
|
||||
'email': 'admin@localhost',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), target_status_code=200)
|
||||
self.event.settings.set('timezone', 'US/Central')
|
||||
o1 = QuestionAnswer.objects.get(question=q1)
|
||||
o2 = QuestionAnswer.objects.get(question=q2)
|
||||
o3 = QuestionAnswer.objects.get(question=q3)
|
||||
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
|
||||
expires=now() + timedelta(days=3),
|
||||
total=4)
|
||||
op = OrderPosition.objects.create(order=order, item=self.ticket, price=42)
|
||||
o1.cartposition, o2.cartposition, o3.cartposition = None, None, None
|
||||
o1.orderposition, o2.orderposition, o3.orderposition = op, op, op
|
||||
# only time and date answers should be unaffected by timezone change
|
||||
self.assertEqual(str(o1), '06:30')
|
||||
self.assertEqual(str(o2), '2005-12-31')
|
||||
o3date, o3time = str(o3).split(' ')
|
||||
self.assertEqual(o3date, '2017-12-31')
|
||||
self.assertEqual(o3time, '23:23')
|
||||
|
||||
def test_addon_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
required=True
|
||||
)
|
||||
q1.items.add(self.ticket)
|
||||
q1.items.add(self.workshop1)
|
||||
ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1,
|
||||
price_included=True)
|
||||
cp1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp1.answers.create(question=q1, answer='12')
|
||||
cp2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.workshop1, addon_to=cp1,
|
||||
price=0, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cp2.answers.create(question=q1, answer='12')
|
||||
|
||||
self._set_session('payment', 'banktransfer')
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.ticket).first().answers.first().answer, '12')
|
||||
self.assertEqual(OrderPosition.objects.filter(item=self.workshop1).first().answers.first().answer, '12')
|
||||
|
||||
def test_questions(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Age', type=Question.TYPE_NUMBER,
|
||||
required=True
|
||||
)
|
||||
q2 = Question.objects.create(
|
||||
event=self.event, question='How have you heard from us?', type=Question.TYPE_STRING,
|
||||
required=False
|
||||
)
|
||||
self.ticket.questions.add(q1)
|
||||
self.ticket.questions.add(q2)
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
cr2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=20, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr1.id, q1.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr2.id, q1.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr1.id, q2.id))), 1)
|
||||
self.assertEqual(len(doc.select('input[name="%s-question_%s"]' % (cr2.id, q2.id))), 1)
|
||||
|
||||
# Not all required fields filled out, expect failure
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr1.id, q1.id): '42',
|
||||
'%s-question_%s' % (cr2.id, q1.id): '',
|
||||
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
|
||||
'%s-question_%s' % (cr2.id, q2.id): '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
|
||||
|
||||
# Corrected request
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'%s-question_%s' % (cr1.id, q1.id): '42',
|
||||
'%s-question_%s' % (cr2.id, q1.id): '23',
|
||||
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
|
||||
'%s-question_%s' % (cr2.id, q2.id): '',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
cr1 = CartPosition.objects.get(id=cr1.id)
|
||||
cr2 = CartPosition.objects.get(id=cr2.id)
|
||||
self.assertEqual(cr1.answers.filter(question=q1).count(), 1)
|
||||
self.assertEqual(cr2.answers.filter(question=q1).count(), 1)
|
||||
self.assertEqual(cr1.answers.filter(question=q2).count(), 1)
|
||||
self.assertFalse(cr2.answers.filter(question=q2).exists())
|
||||
|
||||
def _test_question_input(self, data, should_fail):
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
pl = {
|
||||
('%s-question_%s' % (cr1.id, k.id)): v for k, v in data.items() if v != 'False'
|
||||
}
|
||||
pl['email'] = 'admin@localhost'
|
||||
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), pl, follow=True)
|
||||
if should_fail:
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
assert doc.select('.has-error')
|
||||
assert doc.select('.alert-danger')
|
||||
else:
|
||||
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
cr1.answers.all().delete()
|
||||
|
||||
for k, v in data.items():
|
||||
a = cr1.answers.create(question=k, answer=str(v))
|
||||
if k.type in ('M', 'C'):
|
||||
a.options.add(*k.options.filter(identifier__in=(v if isinstance(v, list) else [v])))
|
||||
|
||||
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
if should_fail:
|
||||
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
assert doc.select('.alert-warning')
|
||||
else:
|
||||
assert response.status_code == 200
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
assert not doc.select('.alert-warning')
|
||||
|
||||
def _setup_dependency_questions(self):
|
||||
self.q1 = self.event.questions.create(
|
||||
event=self.event, question='What industry are you in?', type=Question.TYPE_CHOICE,
|
||||
required=True
|
||||
)
|
||||
self.q1.options.create(answer='Tech', identifier='TECH')
|
||||
self.q1.options.create(answer='Health', identifier='HEALTH')
|
||||
|
||||
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'
|
||||
)
|
||||
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'
|
||||
)
|
||||
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'
|
||||
)
|
||||
self.q4a = self.event.questions.create(
|
||||
event=self.event, question='Why?', type=Question.TYPE_TEXT,
|
||||
required=True, dependency_question=self.q3, dependency_value='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'
|
||||
)
|
||||
self.ticket.questions.add(self.q1)
|
||||
self.ticket.questions.add(self.q2a)
|
||||
self.ticket.questions.add(self.q2b)
|
||||
self.ticket.questions.add(self.q3)
|
||||
self.ticket.questions.add(self.q4a)
|
||||
self.ticket.questions.add(self.q4b)
|
||||
|
||||
def test_question_dependencies_first_path(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'HEALTH',
|
||||
self.q2b: 'NURSE'
|
||||
}, should_fail=False)
|
||||
|
||||
def test_question_dependencies_sidepath_ignored(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'HEALTH',
|
||||
self.q2b: 'NURSE',
|
||||
self.q2a: 'DEV',
|
||||
self.q3: 'True',
|
||||
}, should_fail=False)
|
||||
|
||||
def test_question_dependencies_first_path_required(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'HEALTH',
|
||||
}, should_fail=True)
|
||||
|
||||
def test_question_dependencies_second_path(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'TECH',
|
||||
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({
|
||||
self.q1: 'HEALTH',
|
||||
}, should_fail=True)
|
||||
|
||||
def test_question_dependencies_subsubitem_required(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'TECH',
|
||||
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({
|
||||
self.q1: 'TECH',
|
||||
}, should_fail=False)
|
||||
|
||||
def test_question_dependencies_conditional_require_bool(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'TECH',
|
||||
self.q2a: 'DEV',
|
||||
self.q3: 'False',
|
||||
self.q4b: 'No curly braces!'
|
||||
}, should_fail=False)
|
||||
|
||||
def test_question_dependencies_conditional_require_bool_fail(self):
|
||||
self._setup_dependency_questions()
|
||||
self._test_question_input({
|
||||
self.q1: 'TECH',
|
||||
self.q2a: 'DEV',
|
||||
self.q3: 'False',
|
||||
}, should_fail=True)
|
||||
|
||||
Reference in New Issue
Block a user