[IMP] survey: enable multiple trigger questions and answers
Purpose: allowing users to select multiple answers, even from different questions, as triggers to display a subsequent question. For example, we could ask the question "What qualities do you look for in a desk?" if the participant selected one of the following answers before: "What furniture did you already buy from us?" - "A desk" "What kind of furniture are you looking for?" - "Office furniture" Demo data and tests are adapted and new ones are added. We also take this opportunity to remove `is_conditional` because: 1. This field isn't useful anymore. 2. It could cause inconsistencies as it is not supported to check with a sql constraint that `suggested_answer_ids` is set when this flag is `True`. Task-2937533 Part-of: odoo/odoo#109903 Co-authored-by: Pratik Raval <prra@odoo.com>
This commit is contained in:
co-authored by
Pratik Raval
parent
c9af4abfd3
commit
55fa52be8a
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Surveys',
|
||||
'version': '3.5',
|
||||
'version': '3.6',
|
||||
'category': 'Marketing/Surveys',
|
||||
'description': """
|
||||
Create beautiful surveys and visualize answers
|
||||
|
||||
@@ -270,15 +270,15 @@ class Survey(http.Controller):
|
||||
'format_date': lambda date: format_date(request.env, date)
|
||||
}
|
||||
if survey_sudo.questions_layout != 'page_per_question':
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = answer_sudo._get_conditional_values()
|
||||
triggering_answers_by_question, triggered_questions_by_answer, selected_answers = answer_sudo._get_conditional_values()
|
||||
data.update({
|
||||
'triggering_answer_by_question': {
|
||||
question.id: triggering_answer_by_question[question].id for question in triggering_answer_by_question.keys()
|
||||
if triggering_answer_by_question[question]
|
||||
'triggering_answers_by_question': {
|
||||
question.id: triggering_answers.ids
|
||||
for question, triggering_answers in triggering_answers_by_question.items() if triggering_answers
|
||||
},
|
||||
'triggered_questions_by_answer': {
|
||||
answer.id: triggered_questions_by_answer[answer].ids
|
||||
for answer in triggered_questions_by_answer.keys()
|
||||
answer.id: triggered_questions.ids
|
||||
for answer, triggered_questions in triggered_questions_by_answer.items()
|
||||
},
|
||||
'selected_answers': selected_answers.ids
|
||||
})
|
||||
|
||||
@@ -67,9 +67,7 @@
|
||||
<field name="title">How long is the White Nile river?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q1"/>
|
||||
@@ -96,9 +94,7 @@
|
||||
<field name="title">What is the biggest city in the world?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q2"/>
|
||||
@@ -129,9 +125,7 @@
|
||||
<field name="title">Which is the highest volcano in Europe?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug1"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug1'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p2_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p2_q3"/>
|
||||
@@ -170,9 +164,7 @@
|
||||
<field name="title">When did Genghis Khan die?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q1"/>
|
||||
@@ -198,9 +190,7 @@
|
||||
<field name="title">Who is the architect of the Great Pyramid of Giza?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q2"/>
|
||||
@@ -231,9 +221,7 @@
|
||||
<field name="title">How many years did the 100 years war last?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug2"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug2'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p3_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p3_q3"/>
|
||||
@@ -272,9 +260,7 @@
|
||||
<field name="title">Who received a Nobel prize in Physics for the discovery of neutrino oscillations, which shows that neutrinos have mass?</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q1"/>
|
||||
@@ -307,9 +293,7 @@
|
||||
<field name="title">What is, approximately, the critical mass of plutonium-239?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q2"/>
|
||||
@@ -340,9 +324,7 @@
|
||||
<field name="title">Can Humans ever directly see a photon?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug3"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug3'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p4_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q3"/>
|
||||
@@ -354,7 +336,7 @@
|
||||
<record id="survey_demo_burger_quiz_p4_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p4_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No, it's to small for the human eye.</field>
|
||||
<field name="value">No, it's too small for the human eye.</field>
|
||||
</record>
|
||||
|
||||
<!-- Page 5 : Art & Culture -->
|
||||
@@ -371,9 +353,7 @@
|
||||
<field name="title">Which Musician is not in the 27th Club?</field>
|
||||
<field name="question_type">multiple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q1"/>
|
||||
@@ -406,9 +386,7 @@
|
||||
<field name="title">Which painting/drawing was not made by Pablo Picasso?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q2"/>
|
||||
@@ -443,9 +421,7 @@
|
||||
<field name="title">Which quote is from Jean-Claude Van Damme</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="is_conditional" eval="True"/>
|
||||
<field name="triggering_question_id" ref="survey_demo_burger_quiz_p1_q1"/>
|
||||
<field name="triggering_answer_id" ref="survey_demo_burger_quiz_p1_q1_sug4"/>
|
||||
<field name="triggering_answer_ids" eval="[Command.link(ref('survey.survey_demo_burger_quiz_p1_q1_sug4'))]"/>
|
||||
</record>
|
||||
<record id="survey_demo_burger_quiz_p5_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_burger_quiz_p5_q3"/>
|
||||
@@ -470,4 +446,110 @@
|
||||
<field name="value">I actually don't like thinking. I think people think I like to think a lot. And I don't. I do not like to think at all.</field> <!-- Kanye West -->
|
||||
</record>
|
||||
|
||||
<!-- Multiple triggers -->
|
||||
<record id="survey_demo_food_preferences" model="survey.survey">
|
||||
<field name="title">Food Preferences</field>
|
||||
<field name="survey_type">survey</field>
|
||||
<field name="access_token">foodpref-eren-ces1-abcd-344ca2tgb31e</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="access_mode">public</field>
|
||||
<field name="questions_layout">one_page</field>
|
||||
<field name="description" type="html">
|
||||
<p>Please give us your preferences for this event's dinner!</p>
|
||||
</field>
|
||||
<field name="description_done" type="html">
|
||||
<p>Got it!</p>
|
||||
<p>See you soon!</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q1" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="title">Are you vegetarian?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Yes</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q1_sug3" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q1"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="value">It depends</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q2" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">Would you prefer a veggie meal if possible?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug3')),
|
||||
]"/>
|
||||
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q2_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q2"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Yes</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q2_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q2"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">No</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q3" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="title">Choose your green meal</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug1')),
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q2_sug1')),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q3_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q3"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Vegetarian pizza</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q3_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q3"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Vegetarian burger</field>
|
||||
</record>
|
||||
|
||||
<record id="survey_demo_food_preferences_q4" model="survey.question">
|
||||
<field name="survey_id" ref="survey_demo_food_preferences"/>
|
||||
<field name="sequence">4</field>
|
||||
<field name="title">Choose your meal</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
<field name="triggering_answer_ids" eval="[
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q1_sug2')),
|
||||
Command.link(ref('survey.survey_demo_food_preferences_q2_sug2')),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q4_sug1" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q4"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="value">Steak with french fries</field>
|
||||
</record>
|
||||
<record id="survey_demo_food_preferences_q4_sug2" model="survey.question.answer">
|
||||
<field name="question_id" ref="survey_demo_food_preferences_q4"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="value">Fish</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import json
|
||||
import itertools
|
||||
import json
|
||||
import operator
|
||||
from textwrap import shorten
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
@@ -132,27 +133,28 @@ class SurveyQuestion(models.Model):
|
||||
'survey.user_input.line', 'question_id', string='Answers',
|
||||
domain=[('skipped', '=', False)], groups='survey.group_survey_user')
|
||||
|
||||
# Conditional display
|
||||
is_conditional = fields.Boolean(
|
||||
string='Conditional Display', copy=False, help="""If checked, this question will be displayed only
|
||||
if the specified conditional answer have been selected in a previous question""")
|
||||
triggering_question_id = fields.Many2one(
|
||||
'survey.question', string="Triggering Question", copy=False, compute="_compute_triggering_question_id",
|
||||
store=True, readonly=False, help="Question containing the triggering answer to display the current question.",
|
||||
domain="[('survey_id', '=', survey_id), \
|
||||
'&', ('question_type', 'in', ['simple_choice', 'multiple_choice']), \
|
||||
'|', \
|
||||
('sequence', '<', sequence), \
|
||||
'&', ('sequence', '=', sequence), ('id', '<', id)]")
|
||||
# Not stored, convenient for trigger display computation.
|
||||
triggering_question_ids = fields.Many2many(
|
||||
'survey.question', string="Triggering Questions", compute="_compute_triggering_question_ids",
|
||||
store=False, help="Questions containing the triggering answer(s) to display the current question.")
|
||||
|
||||
allowed_triggering_question_ids = fields.Many2many(
|
||||
'survey.question', string="Allowed Triggering Questions", copy=False, compute="_compute_allowed_triggering_question_ids")
|
||||
is_placed_before_trigger = fields.Boolean(
|
||||
string='Is misplaced?', help="Is this question placed before its trigger question?",
|
||||
string='Is misplaced?', help="Is this question placed before any of its trigger questions?",
|
||||
compute="_compute_allowed_triggering_question_ids")
|
||||
triggering_answer_id = fields.Many2one(
|
||||
'survey.question.answer', string="Triggering Answer", copy=False, compute="_compute_triggering_answer_id",
|
||||
store=True, readonly=False, help="Answer that will trigger the display of the current question.",
|
||||
domain="[('question_id', '=', triggering_question_id)]")
|
||||
triggering_answer_ids = fields.Many2many(
|
||||
'survey.question.answer', string="Triggering Answers", copy=False, store=True,
|
||||
readonly=False, help="Picking any of these answers will trigger this question.\n"
|
||||
"Leave the field empty if the question should always be displayed.",
|
||||
domain="""[
|
||||
('question_id.survey_id', '=', survey_id),
|
||||
'&', ('question_id.question_type', 'in', ['simple_choice', 'multiple_choice']),
|
||||
'|',
|
||||
('question_id.sequence', '<', sequence),
|
||||
'&', ('question_id.sequence', '=', sequence), ('question_id.id', '<', id)
|
||||
]"""
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
|
||||
@@ -166,12 +168,6 @@ class SurveyQuestion(models.Model):
|
||||
'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer'),
|
||||
('scored_date_have_answers', "CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)",
|
||||
'All "Is a scored question = True" and "Question Type: Date" questions need an answer'),
|
||||
('conditional_questions_have_triggering_question', 'CHECK (is_conditional != True OR triggering_question_id is not null)',
|
||||
'All conditional display questions need a triggering question.\n'
|
||||
'Please disable "Conditional Display" or specify a triggering question.'),
|
||||
('triggered_questions_have_triggering_answer', 'CHECK (triggering_question_id is null OR triggering_answer_id is not null)',
|
||||
'All questions triggered by another need a triggering answer.\n'
|
||||
'Please disable "Conditional Display" or specify a triggering answer.'),
|
||||
]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -280,19 +276,12 @@ class SurveyQuestion(models.Model):
|
||||
if not question.validation_required or question.question_type not in ['char_box', 'numerical_box', 'date', 'datetime']:
|
||||
question.validation_required = False
|
||||
|
||||
@api.depends('is_conditional', 'survey_id', 'survey_id.question_ids', 'triggering_question_id')
|
||||
@api.depends('survey_id', 'survey_id.question_ids', 'triggering_answer_ids')
|
||||
def _compute_allowed_triggering_question_ids(self):
|
||||
""" Although the question (and possible trigger questions) sequence
|
||||
"""Although the question (and possible trigger questions) sequence
|
||||
is used here, we do not add these fields to the dependency list to
|
||||
avoid cascading rpc calls when reordering questions via the webclient.
|
||||
"""
|
||||
conditional_questions = self.filtered(lambda q: q.is_conditional)
|
||||
non_conditional_questions = self - conditional_questions
|
||||
non_conditional_questions.allowed_triggering_question_ids = False
|
||||
non_conditional_questions.is_placed_before_trigger = False
|
||||
if not conditional_questions:
|
||||
return
|
||||
|
||||
possible_trigger_questions = self.search([
|
||||
('is_page', '=', False),
|
||||
('question_type', 'in', ['simple_choice', 'multiple_choice']),
|
||||
@@ -301,14 +290,14 @@ class SurveyQuestion(models.Model):
|
||||
])
|
||||
# Using the sequence stored in db is necessary for existing questions that are passed as
|
||||
# NewIds because the sequence provided by the JS client can be incorrect.
|
||||
(conditional_questions | possible_trigger_questions).flush_recordset()
|
||||
(self | possible_trigger_questions).flush_recordset()
|
||||
self.env.cr.execute(
|
||||
"SELECT id, sequence FROM survey_question WHERE id =ANY(%s)",
|
||||
[conditional_questions.ids]
|
||||
[self.ids]
|
||||
)
|
||||
conditional_questions_sequences = dict(self.env.cr.fetchall()) # id: sequence mapping
|
||||
|
||||
for question in conditional_questions:
|
||||
for question in self:
|
||||
question_id = question._origin.id
|
||||
if not question_id: # New question
|
||||
question.allowed_triggering_question_ids = possible_trigger_questions.filtered(
|
||||
@@ -322,28 +311,15 @@ class SurveyQuestion(models.Model):
|
||||
lambda q: q.survey_id.id == question.survey_id._origin.id
|
||||
and (q.sequence < question_sequence or q.sequence == question_sequence and q.id < question_id)
|
||||
)
|
||||
question.is_placed_before_trigger = (
|
||||
question.triggering_question_id
|
||||
and question.triggering_question_id.id not in question.allowed_triggering_question_ids.ids)
|
||||
question.is_placed_before_trigger = bool(
|
||||
set(question.triggering_answer_ids.question_id.ids)
|
||||
- set(question.allowed_triggering_question_ids.ids) # .ids necessary to match ids with newIds
|
||||
)
|
||||
|
||||
@api.depends('is_conditional')
|
||||
def _compute_triggering_question_id(self):
|
||||
""" Used as an 'onchange' : Reset the triggering question if user uncheck 'Conditional Display'
|
||||
Avoid CacheMiss : set the value to False if the value is not set yet."""
|
||||
@api.depends('triggering_answer_ids')
|
||||
def _compute_triggering_question_ids(self):
|
||||
for question in self:
|
||||
if not question.is_conditional or question.triggering_question_id is None:
|
||||
question.triggering_question_id = False
|
||||
|
||||
@api.depends('triggering_question_id')
|
||||
def _compute_triggering_answer_id(self):
|
||||
""" Used as an 'onchange' : Reset the triggering answer if user unset or change the triggering question
|
||||
or uncheck 'Conditional Display'.
|
||||
Avoid CacheMiss : set the value to False if the value is not set yet."""
|
||||
for question in self:
|
||||
if not question.triggering_question_id \
|
||||
or question.triggering_question_id != question.triggering_answer_id.question_id\
|
||||
or question.triggering_answer_id is None:
|
||||
question.triggering_answer_id = False
|
||||
question.triggering_question_ids = question.triggering_answer_ids.question_id
|
||||
|
||||
@api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box', 'suggested_answer_ids.is_correct')
|
||||
def _compute_is_scored_question(self):
|
||||
@@ -391,22 +367,10 @@ class SurveyQuestion(models.Model):
|
||||
def copy(self, default=None):
|
||||
self.ensure_one()
|
||||
clone = super().copy(default)
|
||||
if self.is_conditional:
|
||||
clone.is_conditional = True
|
||||
clone.triggering_question_id = self.triggering_question_id.id
|
||||
clone.triggering_answer_id = self.triggering_answer_id.id
|
||||
if self.triggering_answer_ids:
|
||||
clone.triggering_answer_ids = self.triggering_answer_ids
|
||||
return clone
|
||||
|
||||
def unlink(self):
|
||||
""" Makes sure no question is left depending on the question we're deleting."""
|
||||
depending_questions = self.env['survey.question'].search([('triggering_question_id', 'in', self.ids)])
|
||||
depending_questions.write({
|
||||
'is_conditional': False,
|
||||
'triggering_question_id': False,
|
||||
'triggering_answer_id': False,
|
||||
})
|
||||
return super().unlink()
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
@@ -698,9 +662,12 @@ class SurveyQuestionAnswer(models.Model):
|
||||
"""
|
||||
_name = 'survey.question.answer'
|
||||
_rec_name = 'value'
|
||||
_order = 'sequence, id'
|
||||
_rec_names_search = ['question_id.title', 'value']
|
||||
_order = 'question_id, sequence, id'
|
||||
_description = 'Survey Label'
|
||||
|
||||
MAX_ANSWER_NAME_LENGTH = 90 # empirically tested in client dropdown
|
||||
|
||||
# question and question related fields
|
||||
question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade')
|
||||
matrix_question_id = fields.Many2one('survey.question', string='Question (as matrix row)', ondelete='cascade')
|
||||
@@ -714,6 +681,27 @@ class SurveyQuestionAnswer(models.Model):
|
||||
is_correct = fields.Boolean('Correct')
|
||||
answer_score = fields.Float('Score', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
|
||||
|
||||
@api.depends('value', 'question_id.title')
|
||||
def _compute_display_name(self):
|
||||
"""Render an answer name as "Question title : Answer value" making sure it is not too long.
|
||||
|
||||
This implementation makes sure we have at least 30 characters for the question title,
|
||||
then we elide it, leaving the rest of the space for the answer.
|
||||
"""
|
||||
for answer in self:
|
||||
# _origin (or fallback title) is (likely temporarily) needed to support survey snapshot
|
||||
# during onchange for a deleted answer used as trigger in another question.
|
||||
title = answer._origin.question_id.title
|
||||
n_extra_characters = len(title) + len(answer.value) + 3 - self.MAX_ANSWER_NAME_LENGTH # 3 for `" : "`
|
||||
if n_extra_characters <= 0:
|
||||
answer.display_name = f'{title} : {answer.value}'
|
||||
else:
|
||||
answer.display_name = shorten(
|
||||
f'{shorten(title, max(30, len(title) - n_extra_characters), placeholder="...")} : {answer.value}',
|
||||
self.MAX_ANSWER_NAME_LENGTH,
|
||||
placeholder="..."
|
||||
)
|
||||
|
||||
@api.constrains('question_id', 'matrix_question_id')
|
||||
def _check_question_not_empty(self):
|
||||
"""Ensure that field question_id XOR field matrix_question_id is not null"""
|
||||
@@ -728,13 +716,3 @@ class SurveyQuestionAnswer(models.Model):
|
||||
elif self.question_type in ('multiple_choice', 'simple_choice'):
|
||||
return ['&', ('question_id', '=', self.question_id.id), ('suggested_answer_id', '=', self.id)]
|
||||
return []
|
||||
|
||||
def unlink(self):
|
||||
""" Makes sure no question is left depending on the answer we're deleting."""
|
||||
depending_questions = self.env['survey.question'].search([('triggering_answer_id', 'in', self.ids)])
|
||||
depending_questions.write({
|
||||
'is_conditional': False,
|
||||
'triggering_question_id': False,
|
||||
'triggering_answer_id': False,
|
||||
})
|
||||
return super().unlink()
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
@@ -253,12 +255,12 @@ class Survey(models.Model):
|
||||
survey.question_ids = survey.question_and_page_ids - survey.page_ids
|
||||
survey.question_count = len(survey.question_ids)
|
||||
|
||||
@api.depends('question_and_page_ids.is_conditional', 'users_login_required', 'access_mode')
|
||||
@api.depends('question_and_page_ids.triggering_answer_ids', 'users_login_required', 'access_mode')
|
||||
def _compute_is_attempts_limited(self):
|
||||
for survey in self:
|
||||
if not survey.is_attempts_limited or \
|
||||
(survey.access_mode == 'public' and not survey.users_login_required) or \
|
||||
any(question.is_conditional for question in survey.question_and_page_ids):
|
||||
any(question.triggering_answer_ids for question in survey.question_and_page_ids):
|
||||
survey.is_attempts_limited = False
|
||||
|
||||
@api.depends('session_start_time', 'user_input_ids')
|
||||
@@ -310,10 +312,10 @@ class Survey(models.Model):
|
||||
survey.session_show_leaderboard = survey.scoring_type != 'no_scoring' and \
|
||||
any(question.save_as_nickname for question in survey.question_and_page_ids)
|
||||
|
||||
@api.depends('question_and_page_ids.is_conditional')
|
||||
@api.depends('question_and_page_ids.triggering_answer_ids')
|
||||
def _compute_has_conditional_questions(self):
|
||||
for survey in self:
|
||||
survey.has_conditional_questions = any(question.is_conditional for question in survey.question_and_page_ids)
|
||||
survey.has_conditional_questions = any(question.triggering_answer_ids for question in survey.question_and_page_ids)
|
||||
|
||||
@api.depends('scoring_type')
|
||||
def _compute_certification(self):
|
||||
@@ -381,14 +383,14 @@ class Survey(models.Model):
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
""" Correctly copy the 'triggering_question_id' and 'triggering_answer_id' fields from the original
|
||||
to the clone.
|
||||
This needs to be done in post-processing to make sure we get references to the newly created
|
||||
answers/questions from the copy instead of references to the answers/questions of the original.
|
||||
This implementation assumes that the order of created questions/answers will be kept between
|
||||
"""Correctly copy the 'triggering_answer_ids' field from the original to the clone.
|
||||
|
||||
This needs to be done in post-processing to make sure we get references to the newly
|
||||
created answers from the copy instead of references to the answers of the original.
|
||||
This implementation assumes that the order of created answers will be kept between
|
||||
the original and the clone, using 'zip()' to match the records between the two.
|
||||
|
||||
Note that when question_ids is provided in the default parameter, it falls back to the
|
||||
Note that when `question_ids` is provided in the default parameter, it falls back to the
|
||||
standard copy, meaning that triggering logic will not be maintained.
|
||||
"""
|
||||
self.ensure_one()
|
||||
@@ -396,23 +398,18 @@ class Survey(models.Model):
|
||||
if default and 'question_ids' in default:
|
||||
return clone
|
||||
|
||||
src_questions = self.question_ids
|
||||
dst_questions = clone.question_ids.sorted()
|
||||
cloned_question_ids = clone.question_ids.sorted()
|
||||
|
||||
questions_map = {src.id: dst.id for src, dst in zip(src_questions, dst_questions)}
|
||||
answers_map = {
|
||||
src_answer.id: dst_answer.id
|
||||
for src, dst
|
||||
in zip(src_questions, dst_questions)
|
||||
in zip(self.question_ids, cloned_question_ids)
|
||||
for src_answer, dst_answer
|
||||
in zip(src.suggested_answer_ids, dst.suggested_answer_ids.sorted())
|
||||
}
|
||||
|
||||
for src, dst in zip(src_questions, dst_questions):
|
||||
if src.is_conditional:
|
||||
dst.is_conditional = True
|
||||
dst.triggering_question_id = questions_map.get(src.triggering_question_id.id)
|
||||
dst.triggering_answer_id = answers_map.get(src.triggering_answer_id.id)
|
||||
for src, dst in zip(self.question_ids, cloned_question_ids):
|
||||
if src.triggering_answer_ids:
|
||||
dst.triggering_answer_ids = [answers_map[src_answer_id.id] for src_answer_id in src.triggering_answer_ids]
|
||||
return clone
|
||||
|
||||
def copy_data(self, default=None):
|
||||
@@ -615,22 +612,31 @@ class Survey(models.Model):
|
||||
return result
|
||||
|
||||
def _get_pages_and_questions_to_show(self):
|
||||
"""
|
||||
:return: survey.question recordset excluding invalid conditional questions and pages without description
|
||||
"""
|
||||
"""Filter question_and_pages_ids to include only valid pages and questions.
|
||||
|
||||
Pages are invalid if they have no description. Questions are invalid if
|
||||
they are conditional and all their triggers are invalid.
|
||||
Triggers are invalid if they:
|
||||
- Are a page (not a question)
|
||||
- Have the wrong question type (`simple_choice` and `multiple_choice` are supported)
|
||||
- Are misplaced (positioned after the conditional question)
|
||||
- They are themselves conditional and were found invalid
|
||||
"""
|
||||
self.ensure_one()
|
||||
invalid_questions = self.env['survey.question']
|
||||
questions_and_valid_pages = self.question_and_page_ids.filtered(
|
||||
lambda question: not question.is_page or not is_html_empty(question.description))
|
||||
for question in questions_and_valid_pages.filtered(lambda q: q.is_conditional).sorted():
|
||||
trigger = question.triggering_question_id
|
||||
if (trigger in invalid_questions
|
||||
or trigger.is_page
|
||||
or trigger.question_type not in ['simple_choice', 'multiple_choice']
|
||||
or not trigger.suggested_answer_ids
|
||||
or trigger.sequence > question.sequence
|
||||
or (trigger.sequence == question.sequence and trigger.id > question.id)):
|
||||
|
||||
for question in questions_and_valid_pages.filtered(lambda q: q.triggering_answer_ids).sorted():
|
||||
for trigger in question.triggering_question_ids:
|
||||
if (trigger not in invalid_questions
|
||||
and not trigger.is_page
|
||||
and trigger.question_type in ['simple_choice', 'multiple_choice']
|
||||
and (trigger.sequence < question.sequence
|
||||
or (trigger.sequence == question.sequence and trigger.id < question.id))):
|
||||
break
|
||||
else:
|
||||
# No valid trigger found
|
||||
invalid_questions |= question
|
||||
return questions_and_valid_pages - invalid_questions
|
||||
|
||||
@@ -674,7 +680,7 @@ class Survey(models.Model):
|
||||
return Question
|
||||
|
||||
# Conditional Questions Management
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = user_input._get_conditional_values()
|
||||
triggering_answers_by_question, _, selected_answers = user_input._get_conditional_values()
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
if survey.questions_layout == 'page_per_question':
|
||||
question_candidates = pages_or_questions[0:current_page_index] if go_back \
|
||||
@@ -687,8 +693,8 @@ class Survey(models.Model):
|
||||
if contains_active_question or is_description_section:
|
||||
return question
|
||||
else:
|
||||
triggering_answer = triggering_answer_by_question.get(question)
|
||||
if not triggering_answer or triggering_answer in selected_answers:
|
||||
triggering_answers = triggering_answers_by_question.get(question)
|
||||
if not triggering_answers or triggering_answers & selected_answers:
|
||||
# question is visible because not conditioned or conditioned by a selected answer
|
||||
return question
|
||||
elif survey.questions_layout == 'page_per_section':
|
||||
@@ -726,7 +732,7 @@ class Survey(models.Model):
|
||||
next_page_or_question_candidates = pages_or_questions[current_page_index + 1:]
|
||||
if next_page_or_question_candidates:
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = user_input._get_conditional_values()
|
||||
_, triggered_questions_by_answer, _ = user_input._get_conditional_values()
|
||||
if self.questions_layout == 'page_per_question':
|
||||
next_active_question = any(next_question not in inactive_questions for next_question in next_page_or_question_candidates)
|
||||
is_triggering_question = any(triggering_answer in triggered_questions_by_answer.keys() for triggering_answer in page_or_question.suggested_answer_ids)
|
||||
@@ -794,17 +800,15 @@ class Survey(models.Model):
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_conditional_maps(self):
|
||||
triggering_answer_by_question = {}
|
||||
triggered_questions_by_answer = {}
|
||||
triggering_answers_by_question = defaultdict(lambda: self.env['survey.question.answer'])
|
||||
triggered_questions_by_answer = defaultdict(lambda: self.env['survey.question'])
|
||||
for question in self.question_ids:
|
||||
triggering_answer_by_question[question] = question.is_conditional and question.triggering_answer_id
|
||||
triggering_answers_by_question[question] |= question.triggering_answer_ids
|
||||
|
||||
if question.is_conditional:
|
||||
if question.triggering_answer_id in triggered_questions_by_answer:
|
||||
triggered_questions_by_answer[question.triggering_answer_id] |= question
|
||||
else:
|
||||
triggered_questions_by_answer[question.triggering_answer_id] = question
|
||||
return triggering_answer_by_question, triggered_questions_by_answer
|
||||
for triggering_answer_id in question.triggering_answer_ids:
|
||||
triggered_questions_by_answer[triggering_answer_id] |= question
|
||||
|
||||
return triggering_answers_by_question, triggered_questions_by_answer
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SESSIONS MANAGEMENT
|
||||
|
||||
@@ -536,20 +536,21 @@ class SurveyUserInput(models.Model):
|
||||
that is the next in sequence and that is either not triggered by another question's answer, or that
|
||||
is triggered by an already selected answer.
|
||||
To do all this, we need to return:
|
||||
- list of all selected answers: [answer_id1, answer_id2, ...] (for survey reloading, otherwise, this list is
|
||||
updated at client side)
|
||||
- triggering_answers_by_question: dict -> for a given question, the answers that triggers it
|
||||
Used mainly to ease template rendering
|
||||
- triggered_questions_by_answer: dict -> for a given answer, list of questions triggered by this answer;
|
||||
Used mainly for dynamic show/hide behaviour at client side
|
||||
- triggering_answer_by_question: dict -> for a given question, the answer that triggers it
|
||||
Used mainly to ease template rendering
|
||||
- list of all selected answers: [answer_id1, answer_id2, ...] (for survey reloading, otherwise, this list is
|
||||
updated at client side)
|
||||
"""
|
||||
triggering_answer_by_question, triggered_questions_by_answer = {}, {}
|
||||
triggering_answers_by_question = {}
|
||||
triggered_questions_by_answer = {}
|
||||
# Ignore conditional configuration if randomised questions selection
|
||||
if self.survey_id.questions_selection != 'random':
|
||||
triggering_answer_by_question, triggered_questions_by_answer = self.survey_id._get_conditional_maps()
|
||||
triggering_answers_by_question, triggered_questions_by_answer = self.survey_id._get_conditional_maps()
|
||||
selected_answers = self._get_selected_suggested_answers()
|
||||
|
||||
return triggering_answer_by_question, triggered_questions_by_answer, selected_answers
|
||||
return triggering_answers_by_question, triggered_questions_by_answer, selected_answers
|
||||
|
||||
def _get_selected_suggested_answers(self):
|
||||
"""
|
||||
@@ -585,14 +586,13 @@ class SurveyUserInput(models.Model):
|
||||
answers_to_delete.unlink()
|
||||
|
||||
def _get_inactive_conditional_questions(self):
|
||||
triggering_answer_by_question, triggered_questions_by_answer, selected_answers = self._get_conditional_values()
|
||||
triggering_answers_by_question, _, selected_answers = self._get_conditional_values()
|
||||
|
||||
# get questions that should not be answered
|
||||
inactive_questions = self.env['survey.question']
|
||||
for answer in triggered_questions_by_answer.keys():
|
||||
if answer not in selected_answers:
|
||||
for question in triggered_questions_by_answer[answer]:
|
||||
inactive_questions |= question
|
||||
for question, triggering_answers in triggering_answers_by_question.items():
|
||||
if triggering_answers and not triggering_answers & selected_answers:
|
||||
inactive_questions |= question
|
||||
return inactive_questions
|
||||
|
||||
def _get_print_questions(self):
|
||||
|
||||
@@ -142,20 +142,43 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle visibility of comment area and conditional questions
|
||||
* The form (page) is then automatically submitted if:
|
||||
* - Survey is configured with one page per question and participants are allowed to go back,
|
||||
* - It is not the last question of the survey,
|
||||
* - The question is not waiting for a comment (with "Other" answer),
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
_onChangeChoiceItem: function (event) {
|
||||
const $target = $(event.currentTarget);
|
||||
const $choiceItemGroup = $target.closest('.o_survey_form_choice');
|
||||
|
||||
this._applyCommentAreaVisibility($target);
|
||||
const isQuestionComplete = this._checkConditionalQuestionsConfiguration($target, $choiceItemGroup);
|
||||
if (isQuestionComplete && this.options.usersCanGoBack) {
|
||||
const isLastQuestion = this.$('button[value="finish"]').length !== 0;
|
||||
if (!isLastQuestion) {
|
||||
const questionHasComment = $target.hasClass('o_survey_js_form_other_comment') || $target
|
||||
.closest('.o_survey_form_choice')
|
||||
.find('.o_survey_comment').length !== 0;
|
||||
if (!questionHasComment) {
|
||||
this._submitForm({});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Checks, if the 'other' choice is checked. Applies only if the comment count as answer.
|
||||
* If not checked : Clear the comment textarea, hide and disable it
|
||||
* If checked : enable the comment textarea, show and focus on it
|
||||
*
|
||||
* @private
|
||||
* @param {Event} event
|
||||
* @param {JQuery<HTMLElement>} $choiceItemGroup
|
||||
*/
|
||||
_onChangeChoiceItem: function (event) {
|
||||
var self = this;
|
||||
var $target = $(event.currentTarget);
|
||||
var $choiceItemGroup = $target.closest('.o_survey_form_choice');
|
||||
var $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment');
|
||||
var $commentInput = $choiceItemGroup.find('textarea[type="text"]');
|
||||
_applyCommentAreaVisibility: function ($choiceItemGroup) {
|
||||
const $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment');
|
||||
const $commentInput = $choiceItemGroup.find('textarea[type="text"]');
|
||||
|
||||
if ($otherItem.prop('checked') || $commentInput.hasClass('o_survey_comment')) {
|
||||
$commentInput.each((idx, $input) => $input.disabled = false);
|
||||
@@ -168,10 +191,20 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
$commentInput.closest('.o_survey_comment_container').addClass('d-none');
|
||||
$commentInput.each((idx, $input) => $input.disabled = true);
|
||||
}
|
||||
},
|
||||
|
||||
var $matrixBtn = $target.closest('.o_survey_matrix_btn');
|
||||
/**
|
||||
* For single and multiple choice questions, propagate questions visibility
|
||||
* based on conditional questions and (de)selected triggers
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} $target
|
||||
* @param {JQuery<HTMLElement>} $choiceItemGroup
|
||||
* @returns {boolean} Whether the question is considered completed
|
||||
*/
|
||||
_checkConditionalQuestionsConfiguration: function ($target, $choiceItemGroup) {
|
||||
let isQuestionComplete = false;
|
||||
const $matrixBtn = $target.closest('.o_survey_matrix_btn');
|
||||
if ($target.attr('type') === 'radio') {
|
||||
var isQuestionComplete = false;
|
||||
if ($matrixBtn.length > 0) {
|
||||
$matrixBtn.closest('tr').find('td').removeClass('o_survey_selected');
|
||||
if ($target.is(':checked')) {
|
||||
@@ -181,86 +214,56 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
var subQuestionsIds = $matrixBtn.closest('table').data('subQuestions');
|
||||
var completedQuestions = [];
|
||||
subQuestionsIds.forEach(function (id) {
|
||||
if (self.$('tr#' + id).find('input:checked').length !== 0) {
|
||||
if (this.$('tr#' + id).find('input:checked').length !== 0) {
|
||||
completedQuestions.push(id);
|
||||
}
|
||||
});
|
||||
isQuestionComplete = completedQuestions.length === subQuestionsIds.length;
|
||||
}
|
||||
} else {
|
||||
var previouslySelectedAnswer = $choiceItemGroup.find('label.o_survey_selected');
|
||||
const previouslySelectedAnswer = $choiceItemGroup.find('label.o_survey_selected');
|
||||
previouslySelectedAnswer.removeClass('o_survey_selected');
|
||||
const previouslySelectedAnswerId = previouslySelectedAnswer.find('input').val();
|
||||
if (previouslySelectedAnswerId && this.options.questionsLayout !== 'page_per_question') {
|
||||
this.selectedAnswers.splice(this.selectedAnswers.indexOf(parseInt(previouslySelectedAnswerId)), 1);
|
||||
}
|
||||
|
||||
var newlySelectedAnswer = $target.closest('label');
|
||||
if (newlySelectedAnswer.find('input').val() !== previouslySelectedAnswer.find('input').val()) {
|
||||
const newlySelectedAnswer = $target.closest('label');
|
||||
const newlySelectedAnswerId = $target.val();
|
||||
const isNewSelection = newlySelectedAnswerId !== previouslySelectedAnswerId;
|
||||
if (isNewSelection) {
|
||||
newlySelectedAnswer.addClass('o_survey_selected');
|
||||
isQuestionComplete = this.options.questionsLayout === 'page_per_question';
|
||||
if (!isQuestionComplete) {
|
||||
this.selectedAnswers.push(parseInt(newlySelectedAnswerId));
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional display
|
||||
if (this.options.questionsLayout !== 'page_per_question') {
|
||||
var treatedQuestionIds = []; // Needed to avoid show (1st 'if') then immediately hide (2nd 'if') question during conditional propagation cascade
|
||||
if (Object.keys(this.options.triggeredQuestionsByAnswer).includes(previouslySelectedAnswer.find('input').val())) {
|
||||
// Hide and clear depending question
|
||||
this.options.triggeredQuestionsByAnswer[previouslySelectedAnswer.find('input').val()].forEach(function (questionId) {
|
||||
var dependingQuestion = $('.js_question-wrapper#' + questionId);
|
||||
|
||||
dependingQuestion.addClass('d-none');
|
||||
self._clearQuestionInputs(dependingQuestion);
|
||||
|
||||
treatedQuestionIds.push(questionId);
|
||||
});
|
||||
// Remove answer from selected answer
|
||||
self.selectedAnswers.splice(self.selectedAnswers.indexOf(parseInt($target.val())), 1);
|
||||
}
|
||||
if (Object.keys(this.options.triggeredQuestionsByAnswer).includes($target.val())) {
|
||||
// Display depending question
|
||||
this.options.triggeredQuestionsByAnswer[$target.val()].forEach(function (questionId) {
|
||||
if (!treatedQuestionIds.includes(questionId)) {
|
||||
var dependingQuestion = $('.js_question-wrapper#' + questionId);
|
||||
dependingQuestion.removeClass('d-none');
|
||||
|
||||
// Add answer to selected answer
|
||||
self.selectedAnswers.push(parseInt($target.val()));
|
||||
}
|
||||
});
|
||||
}
|
||||
const conditionalQuestionsToRecomputeVisibility = new Set(
|
||||
(this.options.triggeredQuestionsByAnswer[previouslySelectedAnswerId] || [])
|
||||
.concat(this.options.triggeredQuestionsByAnswer[newlySelectedAnswerId] || [])
|
||||
)
|
||||
this._applyConditionalQuestionsVisibility(conditionalQuestionsToRecomputeVisibility)
|
||||
}
|
||||
}
|
||||
// Auto Submit Form
|
||||
var isLastQuestion = this.$('button[value="finish"]').length !== 0;
|
||||
var questionHasComment = $target.closest('.o_survey_form_choice').find('.o_survey_comment').length !== 0
|
||||
|| $target.hasClass('o_survey_js_form_other_comment');
|
||||
if (!isLastQuestion && this.options.usersCanGoBack && isQuestionComplete && !questionHasComment) {
|
||||
this._submitForm({});
|
||||
}
|
||||
} else { // $target.attr('type') === 'checkbox'
|
||||
if ($matrixBtn.length > 0) {
|
||||
$matrixBtn.toggleClass('o_survey_selected', !$matrixBtn.hasClass('o_survey_selected'));
|
||||
} else {
|
||||
var $label = $target.closest('label');
|
||||
const $label = $target.closest('label');
|
||||
$label.toggleClass('o_survey_selected', !$label.hasClass('o_survey_selected'));
|
||||
const answerId = $target.val();
|
||||
|
||||
// Conditional display
|
||||
if (this.options.questionsLayout !== 'page_per_question' && Object.keys(this.options.triggeredQuestionsByAnswer).includes($target.val())) {
|
||||
var isInputSelected = $label.hasClass('o_survey_selected');
|
||||
// Hide and clear or display depending question
|
||||
this.options.triggeredQuestionsByAnswer[$target.val()].forEach(function (questionId) {
|
||||
var dependingQuestion = $('.js_question-wrapper#' + questionId);
|
||||
dependingQuestion.toggleClass('d-none', !isInputSelected);
|
||||
if (!isInputSelected) {
|
||||
self._clearQuestionInputs(dependingQuestion);
|
||||
}
|
||||
});
|
||||
// Add/remove answer to/from selected answer
|
||||
if (!isInputSelected) {
|
||||
self.selectedAnswers.splice(self.selectedAnswers.indexOf(parseInt($target.val())), 1);
|
||||
} else {
|
||||
self.selectedAnswers.push(parseInt($target.val()));
|
||||
}
|
||||
if (this.options.questionsLayout !== 'page_per_question') {
|
||||
$label.hasClass('o_survey_selected')
|
||||
? this.selectedAnswers.push(parseInt(answerId))
|
||||
: this.selectedAnswers.splice(this.selectedAnswers.indexOf(parseInt(answerId)), 1);
|
||||
this._applyConditionalQuestionsVisibility(this.options.triggeredQuestionsByAnswer[answerId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return isQuestionComplete;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -534,7 +537,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
*
|
||||
* @param {Object} options see '_submitForm' for details
|
||||
*/
|
||||
_onNextScreenDone: function (options) {
|
||||
_onNextScreenDone: function (options) {
|
||||
var self = this;
|
||||
var result = this.nextScreenResult;
|
||||
|
||||
@@ -1134,23 +1137,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
// CONDITIONAL QUESTIONS MANAGEMENT TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clear / Un-select all the input from the given question
|
||||
* + propagate conditional hierarchy by triggering change on choice inputs.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_clearQuestionInputs: function (question) {
|
||||
question.find('input').each(function () {
|
||||
if ($(this).attr('type') === 'text' || $(this).attr('type') === 'number') {
|
||||
$(this).val('');
|
||||
} else if ($(this).prop('checked')) {
|
||||
$(this).prop('checked', false).change();
|
||||
}
|
||||
});
|
||||
question.find('textarea').val('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get questions that are not supposed to be answered by the user.
|
||||
* Those are the ones triggered by answers that the user did not selected.
|
||||
@@ -1158,20 +1144,47 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
* @private
|
||||
*/
|
||||
_getInactiveConditionalQuestionIds: function () {
|
||||
var self = this;
|
||||
var inactiveQuestionIds = [];
|
||||
if (this.options.triggeredQuestionsByAnswer) {
|
||||
Object.keys(this.options.triggeredQuestionsByAnswer).forEach(function (answerId) {
|
||||
if (!self.selectedAnswers.includes(parseInt(answerId))) {
|
||||
self.options.triggeredQuestionsByAnswer[answerId].forEach(function (questionId) {
|
||||
inactiveQuestionIds.push(questionId);
|
||||
});
|
||||
}
|
||||
});
|
||||
const inactiveQuestionIds = [];
|
||||
for (const [questionId, answerIds] of Object.entries(this.options.triggeringAnswersByQuestion || {})) {
|
||||
if (!answerIds.some(answerId => this.selectedAnswers.includes(parseInt(answerId)))) {
|
||||
inactiveQuestionIds.push(parseInt(questionId));
|
||||
}
|
||||
}
|
||||
return inactiveQuestionIds;
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply visibility rules of conditional questions.
|
||||
*
|
||||
* @param {Number[] | String[] | Set | undefined} questionIds Conditional questions ids
|
||||
*/
|
||||
_applyConditionalQuestionsVisibility: function(questionIds) {
|
||||
if (!questionIds || (!questionIds.length && !questionIds.size)) {
|
||||
return;
|
||||
}
|
||||
for (const questionId of questionIds) {
|
||||
const dependingQuestion = document.querySelector(`.js_question-wrapper[id="${questionId}"]`);
|
||||
if (!dependingQuestion) { // Could be on different page
|
||||
continue;
|
||||
}
|
||||
const hasNoSelectedTriggers = !this.options.triggeringAnswersByQuestion[questionId]
|
||||
.some(answerId => this.selectedAnswers.includes(parseInt(answerId)));
|
||||
dependingQuestion.classList.toggle('d-none', hasNoSelectedTriggers);
|
||||
if (hasNoSelectedTriggers) {
|
||||
// Clear / Un-select all the input from the given question
|
||||
// + propagate conditional hierarchy by triggering change on choice inputs.
|
||||
$(dependingQuestion).find('input').each(function () {
|
||||
if ($(this).attr('type') === 'text' || $(this).attr('type') === 'number') {
|
||||
$(this).val('');
|
||||
} else if ($(this).prop('checked')) {
|
||||
$(this).prop('checked', false).change();
|
||||
}
|
||||
});
|
||||
$(dependingQuestion).find('textarea').val('');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ERRORS TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export class QuestionPageListRenderer extends ListRenderer {
|
||||
*/
|
||||
async onDeleteRecord(record) {
|
||||
const triggeredRecords = this.props.list.records.filter(
|
||||
(rec) => rec.data.triggering_question_id[0] === record.resId
|
||||
(rec) => rec.data.triggering_question_ids.records.map(a => a.resId).includes(record.resId)
|
||||
);
|
||||
if (triggeredRecords.length) {
|
||||
const res = await super.onDeleteRecord(record);
|
||||
|
||||
+52
-34
@@ -7,6 +7,11 @@ import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
const { Component, useEffect, useRef, useState } = owl;
|
||||
|
||||
export class SurveyQuestionTriggerWidget extends Component {
|
||||
static template = "survey.surveyQuestionTrigger";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.button = useRef('survey_question_trigger');
|
||||
@@ -15,19 +20,20 @@ export class SurveyQuestionTriggerWidget extends Component {
|
||||
triggerTooltip: "",
|
||||
});
|
||||
useEffect(() => {
|
||||
if (this.button && this.button.el) {
|
||||
const triggeringQuestionTitle = this.props.record.data.triggering_question_id[1];
|
||||
const triggerError = this.surveyQuestionTriggerError;
|
||||
if (this.button?.el && this.props.record.data.triggering_question_ids.records?.length !== 0) {
|
||||
const { triggerError, misplacedTriggerQuestionRecords } = this.surveyQuestionTriggerError;
|
||||
if (triggerError === "MISPLACED_TRIGGER_WARNING") {
|
||||
this.state.surveyIconWarning = true;
|
||||
this.state.triggerTooltip = _t(
|
||||
'⚠ This question is positioned before its trigger ("%s") and will be skipped.',
|
||||
triggeringQuestionTitle
|
||||
this.state.triggerTooltip = '⚠ ' + _t(
|
||||
'Triggers based on the following questions will not work because they are positioned after this question:\n"%s".',
|
||||
misplacedTriggerQuestionRecords
|
||||
.map((question) => question.data.title)
|
||||
.join('", "')
|
||||
);
|
||||
} else if (triggerError === "WRONG_QUESTIONS_SELECTION_WARNING") {
|
||||
this.state.surveyIconWarning = true;
|
||||
this.state.triggerTooltip = _t(
|
||||
"⚠ Conditional display is not available when questions are randomly picked."
|
||||
this.state.triggerTooltip = '⚠ ' + _t(
|
||||
"Conditional display is not available when questions are randomly picked."
|
||||
);
|
||||
} else if (triggerError === "MISSING_TRIGGER_ERROR") {
|
||||
// This case must be handled to not temporarily render the "normal" icon if previously
|
||||
@@ -36,9 +42,10 @@ export class SurveyQuestionTriggerWidget extends Component {
|
||||
} else {
|
||||
this.state.surveyIconWarning = false;
|
||||
this.state.triggerTooltip = _t(
|
||||
'Displayed if "%s: %s"',
|
||||
triggeringQuestionTitle,
|
||||
this.props.record.data.triggering_answer_id[1]
|
||||
'Displayed if "%s".',
|
||||
this.props.record.data.triggering_answer_ids.records
|
||||
.map((answer) => answer.data.display_name)
|
||||
.join('", "'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -55,12 +62,12 @@ export class SurveyQuestionTriggerWidget extends Component {
|
||||
* 2. Robustness, as sequences values do not always match between server
|
||||
* provided values when the records are not saved.
|
||||
*
|
||||
* @returns { String }
|
||||
* @returns {{ triggerError: String, misplacedTriggerQuestionRecords: Record[] }}
|
||||
* * `""`: No trigger error (also if `triggering_question_id`
|
||||
* field is not set).
|
||||
* * `"MISSING_TRIGGER_ERROR"`: `triggering_question_id` field is set
|
||||
* and trigger record is not found. This can happen when a question
|
||||
* used as trigger is deleted on the client but not yet saved to DB.
|
||||
* * `"MISSING_TRIGGER_ERROR"`: `triggering_questions_ids` field is set
|
||||
* but trigger record is not found. This can happen if all questions
|
||||
* used as triggers are deleted on the client but not yet saved to DB.
|
||||
* * `"MISPLACED_TRIGGER_WARNING"`: a `triggering_question_id` is set
|
||||
* but is positioned after the current record in the list. This can
|
||||
* happen if the triggering or the triggered question is moved.
|
||||
@@ -71,37 +78,48 @@ export class SurveyQuestionTriggerWidget extends Component {
|
||||
*/
|
||||
get surveyQuestionTriggerError() {
|
||||
const record = this.props.record;
|
||||
if (!record.data.triggering_question_id) {
|
||||
return "";
|
||||
if (!record.data.triggering_question_ids.records.length) {
|
||||
return { triggerError: "", misplacedTriggerQuestionRecords: [] };
|
||||
}
|
||||
if (this.props.record.data.questions_selection === 'random') {
|
||||
return { triggerError: 'WRONG_QUESTIONS_SELECTION_WARNING', misplacedTriggerQuestionRecords: [] };
|
||||
}
|
||||
const triggerId = record.data.triggering_question_id[0];
|
||||
let triggerRecord = record.model.root.data.question_and_page_ids.records.find(rec => rec.resId === triggerId);
|
||||
|
||||
if (!triggerRecord) {
|
||||
return "MISSING_TRIGGER_ERROR";
|
||||
const missingTriggerQuestionsIds = [];
|
||||
let triggerQuestionsRecords = [];
|
||||
for (const triggeringQuestion of record.data.triggering_question_ids.records) {
|
||||
const triggeringQuestionRecord = record.model.root.data.question_and_page_ids.records.find(
|
||||
rec => rec.resId === triggeringQuestion.resId);
|
||||
if (triggeringQuestionRecord) {
|
||||
triggerQuestionsRecords.push(triggeringQuestionRecord);
|
||||
} else { // Trigger question was deleted from the list
|
||||
missingTriggerQuestionsIds.push(triggeringQuestion.resId);
|
||||
}
|
||||
}
|
||||
if (record.data.questions_selection === 'random') {
|
||||
return "WRONG_QUESTIONS_SELECTION_WARNING";
|
||||
|
||||
if (missingTriggerQuestionsIds.length === this.props.record.data.triggering_question_ids.records.length) {
|
||||
return { triggerError: 'MISSING_TRIGGER_ERROR', misplacedTriggerQuestionRecords: [] }; // only if all are missing
|
||||
}
|
||||
if (record.data.sequence < triggerRecord.data.sequence ||
|
||||
(record.data.sequence === triggerRecord.data.sequence && record.resId < triggerId)) {
|
||||
return "MISPLACED_TRIGGER_WARNING";
|
||||
const misplacedTriggerQuestionRecords = [];
|
||||
for (const triggerQuestionRecord of triggerQuestionsRecords) {
|
||||
if (record.data.sequence < triggerQuestionRecord.data.sequence ||
|
||||
(record.data.sequence === triggerQuestionRecord.data.sequence && record.resId < triggerQuestionRecord.resId)) {
|
||||
misplacedTriggerQuestionRecords.push(triggerQuestionRecord);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
return {
|
||||
triggerError: misplacedTriggerQuestionRecords.length ? "MISPLACED_TRIGGER_WARNING" : "",
|
||||
misplacedTriggerQuestionRecords: misplacedTriggerQuestionRecords,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
SurveyQuestionTriggerWidget.template = "survey.surveyQuestionTrigger";
|
||||
SurveyQuestionTriggerWidget.props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
|
||||
export const surveyQuestionTriggerWidget = {
|
||||
component: SurveyQuestionTriggerWidget,
|
||||
displayName: "Trigger",
|
||||
fieldDependencies: [
|
||||
{ name: "triggering_question_id", type: "many2one" },
|
||||
{ name: "triggering_answer_id", type: "many2one" },
|
||||
{ name: "triggering_question_ids", type: "many2one" },
|
||||
{ name: "triggering_answer_ids", type: "many2one" },
|
||||
],
|
||||
};
|
||||
registry.category("view_widgets").add("survey_question_trigger", surveyQuestionTriggerWidget);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="survey.surveyQuestionTrigger">
|
||||
<button t-if="this.props.record.data.triggering_question_id" disabled="disabled" t-ref="survey_question_trigger"
|
||||
<button t-if="this.props.record.data.triggering_question_ids.records.length" disabled="disabled" t-ref="survey_question_trigger"
|
||||
class="btn btn-link px-1 py-0 pe-auto" t-att-class="this.state.surveyIconWarning ? 'opacity-100' : 'icon_rotates'">
|
||||
<i class="fa fa-fw o_button_icon " t-att-class="this.state.surveyIconWarning ? 'fa-exclamation-triangle text-warning' : 'fa-code-fork'"
|
||||
t-att-data-tooltip="this.state.triggerTooltip"/>
|
||||
|
||||
@@ -26,17 +26,17 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
survey_question: {
|
||||
fields: {
|
||||
sequence: { type: "number" },
|
||||
name: { type: "char", string: "name", },
|
||||
triggering_question_id: {
|
||||
type: "many2one",
|
||||
title: { type: "char", string: "title", },
|
||||
triggering_question_ids: {
|
||||
type: "many2many",
|
||||
string: "Triggering question",
|
||||
relation: "survey_question",
|
||||
required: false,
|
||||
searchable: true,
|
||||
},
|
||||
triggering_answer_id: {
|
||||
type: "many2one",
|
||||
string: "Triggering answer",
|
||||
triggering_answer_ids: {
|
||||
type: "many2many",
|
||||
string: "Triggering answers",
|
||||
relation: "survey_question_answer",
|
||||
required: false,
|
||||
searchable: true,
|
||||
@@ -46,15 +46,15 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
{
|
||||
id: 1,
|
||||
sequence: 1,
|
||||
name: "Question 1",
|
||||
triggering_question_id: null,
|
||||
triggering_answer_id: null,
|
||||
title: "Question 1",
|
||||
triggering_question_ids: null,
|
||||
triggering_answer_ids: null,
|
||||
}, {
|
||||
id: 2,
|
||||
sequence: 2,
|
||||
name: "Question 2",
|
||||
triggering_question_id: 1,
|
||||
triggering_answer_id: 1,
|
||||
title: "Question 2",
|
||||
triggering_question_ids: [1],
|
||||
triggering_answer_ids: [1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -65,7 +65,8 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Answer 1"
|
||||
name: "Answer 1",
|
||||
display_name: "Question 1: Answer 1",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -74,9 +75,9 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
"survey_question,false,form": `
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="triggering_answer_id" invisible="1"/>
|
||||
<field name="triggering_question_id" invisible="1"/>
|
||||
<field name="title"/>
|
||||
<field name="triggering_question_ids" invisible="1"/>
|
||||
<field name="triggering_answer_ids" invisible="1" widget="many2many_tags"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
@@ -97,9 +98,9 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
<field name="question_and_page_ids">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="triggering_answer_id" invisible="1"/>
|
||||
<field name="triggering_question_id" invisible="1"/>
|
||||
<field name="title"/>
|
||||
<field name="triggering_question_ids" invisible="1"/>
|
||||
<field name="triggering_answer_ids" invisible="1" widget="many2many_tags"/> <!-- widget to fetch display_name -->
|
||||
<widget name="survey_question_trigger"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -121,8 +122,8 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
// Question 2 is correctly placed after Question 1
|
||||
let triggerIcon = q2TriggerDiv.querySelector("button i");
|
||||
assert.doesNotHaveClass(triggerIcon, "text-warning");
|
||||
assert.hasAttrValue(triggerIcon, 'data-tooltip', 'Displayed if "Question 1: Answer 1"',
|
||||
'Trigger tooltip should be \'Displayed if "Question 1: Answer 1"\'.');
|
||||
assert.hasAttrValue(triggerIcon, 'data-tooltip', 'Displayed if "Question 1: Answer 1".',
|
||||
'Trigger tooltip should be \'Displayed if "Question 1: Answer 1".\'.');
|
||||
|
||||
// drag and drop Question 2 (triggered) before Question 1 (trigger)
|
||||
await dragAndDrop("tbody tr:nth-child(2) .o_handle_cell", "tbody tr:nth-child(1)");
|
||||
@@ -136,7 +137,7 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
assert.hasClass(triggerIcon, "text-warning");
|
||||
assert.strictEqual(
|
||||
triggerIcon.getAttribute('data-tooltip'),
|
||||
'⚠ This question is positioned before its trigger ("Question 1") and will be skipped.',
|
||||
'⚠ Triggers based on the following questions will not work because they are positioned after this question:\n"Question 1".',
|
||||
'Trigger tooltip should have been changed to misplacement error message.'
|
||||
);
|
||||
|
||||
@@ -148,7 +149,7 @@ QUnit.module("SurveyQuestionTriggerWidget", (hooks) => {
|
||||
|
||||
assert.strictEqual(rows[1].textContent, "Question 2");
|
||||
assert.doesNotHaveClass(rows[1].querySelector("td.o_data_cell div.o_widget_survey_question_trigger button i"), "text-warning");
|
||||
assert.hasAttrValue(triggerIcon, 'data-tooltip', 'Displayed if "Question 1: Answer 1"',
|
||||
'Trigger tooltip should be back to \'Displayed if "Question 1: Answer 1"\'.');
|
||||
assert.hasAttrValue(triggerIcon, 'data-tooltip', 'Displayed if "Question 1: Answer 1".',
|
||||
'Trigger tooltip should be back to \'Displayed if "Question 1: Answer 1".\'.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TourError } from "@web_tour/tour_service/tour_utils";
|
||||
|
||||
registry.category("web_tour.tours").add('test_survey_chained_conditional_questions', {
|
||||
test: true,
|
||||
@@ -19,17 +20,45 @@ registry.category("web_tour.tours").add('test_survey_chained_conditional_questio
|
||||
content: 'Answer Q3 with Answer 1',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3") label:contains("Answer 1")',
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 2', // This should hide all remaining questions.
|
||||
content: 'Answer Q1 with Answer 3', // This should hide Q2 but not Q3.
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 3")',
|
||||
}, {
|
||||
content: 'Check that Q2 was hidden',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3")',
|
||||
run : () => expectHiddenQuestion("Q2"),
|
||||
}, {
|
||||
content: 'Answer Q3 with Answer 2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 2', // This should hide all other questions.
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Check that only question 1 is now visible',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1")',
|
||||
run: () => {
|
||||
const selector = 'div.js_question-wrapper.d-none';
|
||||
if (document.querySelectorAll(selector).length !== 2) {
|
||||
throw new Error('Q2 and Q3 should have been hidden.');
|
||||
}
|
||||
}
|
||||
run : () => {
|
||||
expectHiddenQuestion("Q2", "Q2's trigger is gone.");
|
||||
expectHiddenQuestion("Q3", "No reason to show it now.");
|
||||
},
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 3', // This shows Q3.
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 3")',
|
||||
}, {
|
||||
content: 'Check that a question (Q2) is hidden',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1")',
|
||||
run : () => expectHiddenQuestion("Q2", "Q2 should stay hidden."),
|
||||
}, {
|
||||
content: 'Answer Q3 with Answer 2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Check that only question 1 is now the only one visible again',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1")',
|
||||
run : () => {
|
||||
expectHiddenQuestion("Q2", "Q2's trigger is gone, again.");
|
||||
expectHiddenQuestion("Q3", "As Q2's gone, so should this one.");
|
||||
},
|
||||
}, {
|
||||
content: 'Click Submit and finish the survey',
|
||||
trigger: 'button[value="finish"]',
|
||||
@@ -41,3 +70,9 @@ registry.category("web_tour.tours").add('test_survey_chained_conditional_questio
|
||||
}
|
||||
|
||||
]});
|
||||
|
||||
export function expectHiddenQuestion (questionTitle, msg){
|
||||
if ($(`div.js_question-wrapper.d-none:contains('${questionTitle}')`).length !== 1) {
|
||||
throw new TourError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { expectHiddenQuestion } from "@survey/../tests/tours/survey_chained_conditional_questions";
|
||||
|
||||
registry.category("web_tour.tours").add('test_survey_conditional_question_on_different_page', {
|
||||
test: true,
|
||||
url: '/survey/start/1cb935bd-2399-4ed1-9e10-c649318fb4dc',
|
||||
steps: () => [
|
||||
{
|
||||
content: 'Click on Start',
|
||||
trigger: 'button.btn:contains("Start")',
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 1',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 1")',
|
||||
}, {
|
||||
content: 'Go to next page',
|
||||
trigger: 'button[value="next"]',
|
||||
}, {
|
||||
content: 'Check that Q3 is visible',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Answer Q2 with Answer 2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Check that Q3 is still visible',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Go back',
|
||||
trigger: 'button[value="previous"]',
|
||||
}, {
|
||||
content: 'Answer Q1 with Answer 2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Go to next page',
|
||||
trigger: 'button[value="next"]',
|
||||
}, {
|
||||
content: 'Check that Q3 is hidden',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2")',
|
||||
run : () => expectHiddenQuestion("Q3", "Q3 should be hidden as q1_a1 trigger is not selected anymore"),
|
||||
}, {
|
||||
content: 'Answer Q2 with Answer 1',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2") label:contains("Answer 1")',
|
||||
}, {
|
||||
content: 'Check that Q3 is now visible again',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3")',
|
||||
isCheck: true,
|
||||
}
|
||||
],
|
||||
});
|
||||
@@ -35,26 +35,17 @@ registry.category("web_tour.tours").add('survey_tour_test_survey_form_triggers',
|
||||
in_modal: true,
|
||||
},
|
||||
...addTwoAnswers(),
|
||||
...toggleIsConditional(),
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Set a trigger question for the first question",
|
||||
trigger: ".o_field_widget[name=triggering_question_id] input",
|
||||
content: "Set a trigger for the first question",
|
||||
trigger: ".o_field_widget[name=triggering_answer_ids] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the first question as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 1")',
|
||||
content: "Set the first question's first answer as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 1 : Answer A")',
|
||||
run: 'click',
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set a trigger answer for first question",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_answer_id] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the first answer as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Answer A")',
|
||||
run: 'click',
|
||||
},
|
||||
...changeTab("answers"),
|
||||
...saveAndNew(),
|
||||
@@ -65,38 +56,27 @@ registry.category("web_tour.tours").add('survey_tour_test_survey_form_triggers',
|
||||
in_modal: true,
|
||||
},
|
||||
...addTwoAnswers(),
|
||||
...toggleIsConditional(),
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Set a trigger question for the second question",
|
||||
trigger: ".o_field_widget[name=triggering_question_id] input",
|
||||
content: "Set a trigger for the second question",
|
||||
trigger: ".o_field_widget[name=triggering_answer_ids] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the second question as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 2")',
|
||||
content: "Set the second question's second answer as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 2 : Answer B")',
|
||||
run: 'click',
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set a trigger answer for second question",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_answer_id] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the second answer as trigger",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Answer B")',
|
||||
run: 'click',
|
||||
},
|
||||
...changeTab("answers"),
|
||||
...stepUtils.saveForm(),
|
||||
// Q2 and Q3 should have fa-fork icons. Assumes that the Trigger widget's column is 2 places after the title's.
|
||||
{
|
||||
content: "Check that Question 2 has 'normal' trigger icon",
|
||||
trigger: "tr:contains('Question 2') button i.fa-code-fork",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Check that Question 3 has 'normal' trigger icon",
|
||||
trigger: "tr:contains('Question 3') button i.fa-code-fork",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Move Question 3 above its trigger (Question 2)",
|
||||
trigger: "tr.o_data_row:nth-child(3) td[name=sequence]",
|
||||
@@ -104,81 +84,107 @@ registry.category("web_tour.tours").add('survey_tour_test_survey_form_triggers',
|
||||
}, {
|
||||
content: "Check that Question 3 has 'warning' trigger icon",
|
||||
trigger: "tr:contains('Question 3') button i.fa-exclamation-triangle",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Open that question to check the server's misplacement evaluation agrees",
|
||||
trigger: "tr.o_data_row td[data-tooltip='Question 3']",
|
||||
trigger: "tr.o_data_row td:contains('Question 3')",
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Check that an alert is shown",
|
||||
trigger: ".o_form_sheet_bg div:first-child.alert-warning:contains('positioned before its trigger')",
|
||||
trigger: ".o_form_sheet_bg div:first-child.alert-warning:contains('positioned before some or all of its triggers')",
|
||||
in_modal: true,
|
||||
},
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Change trigger question",
|
||||
trigger: ".o_field_widget[name=triggering_question_id] input",
|
||||
content: "Remove invalid trigger",
|
||||
trigger: ".o_field_widget[name=triggering_answer_ids] span:contains('Question 2') a.o_delete",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the first question as trigger instead",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 1")',
|
||||
run: 'click',
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Check that the alert is gone",
|
||||
trigger: `.o_form_sheet_bg div:first-child:not(.alert-warning).o_form_sheet`,
|
||||
in_modal: true,
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Change the trigger answer as well",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_answer_id] input",
|
||||
content: "Choose a new valid trigger",
|
||||
trigger: ".o_field_widget[name=triggering_answer_ids] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the second answer as trigger, then",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Answer B")',
|
||||
content: "Set the first question's second answer as trigger, then",
|
||||
trigger: 'ul.ui-autocomplete a:contains("Question 1 : Answer B")',
|
||||
run: 'click',
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
{
|
||||
{
|
||||
content: "Check that Question 3 has its 'normal' trigger icon back",
|
||||
trigger: "tr:contains('Question 3') button i.fa-code-fork",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Move Question 3 back below Question 2",
|
||||
trigger: "tr.o_data_row:nth-child(2) td[name=sequence]",
|
||||
run: "drag_and_drop_native div[name=question_and_page_ids] table tbody tr:nth-child(3)",
|
||||
}, {
|
||||
content: "Open that question again",
|
||||
trigger: "tr.o_data_row td[data-tooltip='Question 3']",
|
||||
trigger: "tr.o_data_row td:contains('Question 3')",
|
||||
run: "click",
|
||||
},
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Change trigger to confirm we can now use Question 2 again",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_question_id] input",
|
||||
content: "Add a second trigger to confirm we can now use Question 2 again",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_answer_ids] input",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Question 2 is allowed as trigger again",
|
||||
trigger: '.modal-content ul.ui-autocomplete a:contains("Question 2")',
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Change the trigger answer back as well",
|
||||
trigger: ".modal-content .o_field_widget[name=triggering_answer_id] input",
|
||||
run: "text A",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Set the second answer as trigger, then",
|
||||
trigger: '.modal-content ul.ui-autocomplete a:contains("Answer B")',
|
||||
content: "Add the second question's second answer as trigger, then",
|
||||
trigger: '.modal-content ul.ui-autocomplete a:contains("Question 2 : Answer B")',
|
||||
run: "click",
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
// Move question 1 below question 3,
|
||||
{
|
||||
content: "Move Question 1 back below Question 3",
|
||||
trigger: "tr.o_data_row:nth-child(1) td[name=sequence]",
|
||||
run: "drag_and_drop_native div[name=question_and_page_ids] table tbody tr:nth-child(3)",
|
||||
}, {
|
||||
content: "Check that Question 3 has 'warning' trigger icon",
|
||||
trigger: "tr:contains('Question 3') button i.fa-exclamation-triangle",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Open that question again",
|
||||
trigger: "tr.o_data_row td:contains('Question 3')",
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Check that an alert is shown also when only one trigger is misplaced",
|
||||
trigger: ".o_form_sheet_bg div:first-child.alert-warning:contains('positioned before some or all of its triggers')",
|
||||
in_modal: true,
|
||||
},
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Remove temporarily used trigger",
|
||||
trigger: ".o_field_widget[name=triggering_answer_ids] span:contains('Question 1') a.o_delete",
|
||||
run: "click",
|
||||
in_modal: true,
|
||||
}, {
|
||||
content: "Check that the alert is gone in this case too",
|
||||
trigger: `.o_form_sheet_bg div:first-child:not(.alert-warning).o_form_sheet`,
|
||||
in_modal: true,
|
||||
isCheck: true,
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
{
|
||||
content: "Check that Question 3 has its 'normal' trigger icon back",
|
||||
trigger: "tr:contains('Question 3') button i.fa-code-fork",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Move Question 1 back above Question 2",
|
||||
trigger: "tr.o_data_row:nth-child(3) td[name=sequence]",
|
||||
run: "drag_and_drop_native div[name=question_and_page_ids] table tbody tr:nth-child(1)",
|
||||
},
|
||||
// Deleting trigger answers or whole question gracefully remove the trigger automatically
|
||||
{
|
||||
content: "Open Question 2 again",
|
||||
trigger: "tr.o_data_row td[data-tooltip='Question 2']",
|
||||
trigger: "tr.o_data_row td:contains('Question 2')",
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Delete Answer B",
|
||||
@@ -189,11 +195,11 @@ registry.category("web_tour.tours").add('survey_tour_test_survey_form_triggers',
|
||||
content: "Check that Question 3 no longer has a trigger icon",
|
||||
trigger: "div[name=question_and_page_ids] tr:contains('Question 3') div.o_widget_survey_question_trigger:not(:has(button))",
|
||||
allowInvisible: true,
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Check that Question 2 however still has a trigger icon",
|
||||
trigger: "tr:contains('Question 2') button i.fa-code-fork",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Delete Question 1",
|
||||
trigger: "tr:contains('Question 1') button[name=delete]",
|
||||
@@ -202,14 +208,14 @@ registry.category("web_tour.tours").add('survey_tour_test_survey_form_triggers',
|
||||
content: "Check that now Question 2 too does no longer have a trigger icon",
|
||||
trigger: "tr:contains('Question 2') div.o_widget_survey_question_trigger:not(:has(button))",
|
||||
allowInvisible: true,
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Go back to Kanban View',
|
||||
trigger: '[data-menu-xmlid="survey.menu_survey_form"]',
|
||||
}, {
|
||||
content: "Check that we arrived on the kanban view",
|
||||
trigger: ".o-kanban-button-new",
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}
|
||||
]});
|
||||
|
||||
@@ -246,22 +252,11 @@ function saveAndNew() {
|
||||
// suggested_answer_ids required even though in_modal is specified...
|
||||
trigger: "div[name=suggested_answer_ids] .o_list_table tbody tr:first-child:not(.o_data_row)", // empty answers list
|
||||
in_modal: true,
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function toggleIsConditional() {
|
||||
return [
|
||||
...changeTab("options"),
|
||||
{
|
||||
content: "Toggle is_conditional",
|
||||
trigger: "div[name=is_conditional] input",
|
||||
in_modal: true,
|
||||
run: "click",
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function changeTab(tabName) {
|
||||
// Currently, .modal-content is required even though "in_modal"
|
||||
@@ -274,7 +269,7 @@ function changeTab(tabName) {
|
||||
content: `Wait for tab ${tabName} tab`,
|
||||
trigger: `.modal-content a[name=${tabName}].nav-link.active`,
|
||||
in_modal: true,
|
||||
run: () => {}, // it's a check
|
||||
isCheck: true,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
+130
-144
@@ -33,31 +33,20 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
survey_1_q_1, survey_1_q_2, _ = survey_1.question_ids
|
||||
survey_2_q_1, survey_2_q_2, _ = survey_2.question_ids
|
||||
|
||||
# Make sure they are already configured as conditionally triggered
|
||||
survey_1_q_2.write({
|
||||
'is_conditional': True,
|
||||
'triggering_question_id': survey_1_q_1,
|
||||
'triggering_answer_id': survey_1_q_1.suggested_answer_ids[0]
|
||||
})
|
||||
survey_2_q_2.write({
|
||||
'is_conditional': True,
|
||||
'triggering_question_id': survey_2_q_1,
|
||||
'triggering_answer_id': survey_2_q_1.suggested_answer_ids[0]
|
||||
})
|
||||
|
||||
with self.subTest('Editing existing questions'):
|
||||
# Only previous questions from the same survey
|
||||
self.assertFalse(survey_1_q_2.allowed_triggering_question_ids & survey_2_q_2.allowed_triggering_question_ids)
|
||||
self.assertFalse(bool(survey_1_q_2.allowed_triggering_question_ids & survey_2_q_2.allowed_triggering_question_ids))
|
||||
self.assertEqual(survey_1_q_2.allowed_triggering_question_ids, survey_1_q_1)
|
||||
self.assertEqual(survey_2_q_2.allowed_triggering_question_ids, survey_2_q_1)
|
||||
|
||||
survey_1_new_question = self.env['survey.question'].new({'survey_id': survey_1, 'is_conditional': True})
|
||||
survey_2_new_question = self.env['survey.question'].new({'survey_id': survey_2, 'is_conditional': True})
|
||||
survey_1_new_question = self.env['survey.question'].new({'survey_id': survey_1})
|
||||
survey_2_new_question = self.env['survey.question'].new({'survey_id': survey_2})
|
||||
|
||||
with self.subTest('New questions'):
|
||||
# New questions should be allowed to use any question with choices from the same survey
|
||||
self.assertFalse(survey_1_new_question.allowed_triggering_question_ids
|
||||
& survey_2_new_question.allowed_triggering_question_ids)
|
||||
self.assertFalse(
|
||||
bool(survey_1_new_question.allowed_triggering_question_ids & survey_2_new_question.allowed_triggering_question_ids)
|
||||
)
|
||||
self.assertEqual(survey_1_new_question.allowed_triggering_question_ids.ids, survey_1.question_ids.ids)
|
||||
self.assertEqual(survey_2_new_question.allowed_triggering_question_ids.ids, survey_2.question_ids.ids)
|
||||
|
||||
@@ -358,19 +347,18 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
q_is_vegetarian_text = 'Are you vegetarian?'
|
||||
q_is_vegetarian = self._add_question(
|
||||
self.page_0, q_is_vegetarian_text, 'multiple_choice', survey_id=self.survey.id,
|
||||
sequence=100, labels=[{'value': 'Yes'}, {'value': 'No'}])
|
||||
sequence=100, labels=[{'value': 'Yes'}, {'value': 'No'}, {'value': 'Sometimes'}])
|
||||
q_food_vegetarian_text = 'Choose your green meal'
|
||||
self._add_question(self.page_0, q_food_vegetarian_text, 'multiple_choice',
|
||||
is_conditional=True, sequence=101,
|
||||
triggering_question_id=q_is_vegetarian.id,
|
||||
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[0].id,
|
||||
sequence=101,
|
||||
triggering_answer_ids=[q_is_vegetarian.suggested_answer_ids[0].id,
|
||||
q_is_vegetarian.suggested_answer_ids[2].id],
|
||||
survey_id=self.survey.id,
|
||||
labels=[{'value': 'Vegetarian pizza'}, {'value': 'Vegetarian burger'}])
|
||||
q_food_not_vegetarian_text = 'Choose your meal'
|
||||
q_food_not_vegetarian_text = 'Choose your meal in case we serve meet/fish'
|
||||
self._add_question(self.page_0, q_food_not_vegetarian_text, 'multiple_choice',
|
||||
is_conditional=True, sequence=102,
|
||||
triggering_question_id=q_is_vegetarian.id,
|
||||
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[1].id,
|
||||
sequence=102,
|
||||
triggering_answer_ids=q_is_vegetarian.suggested_answer_ids[1].ids,
|
||||
survey_id=self.survey.id,
|
||||
labels=[{'value': 'Steak with french fries'}, {'value': 'Fish'}])
|
||||
|
||||
@@ -382,29 +370,25 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
q_food_vegetarian_cloned = get_question_by_title(survey_clone, q_food_vegetarian_text)
|
||||
q_food_not_vegetarian_cloned = get_question_by_title(survey_clone, q_food_not_vegetarian_text)
|
||||
|
||||
self.assertFalse(q_is_vegetarian_cloned.is_conditional)
|
||||
self.assertFalse(bool(q_is_vegetarian_cloned.triggering_answer_ids))
|
||||
|
||||
# Vegetarian choice
|
||||
self.assertTrue(q_food_vegetarian_cloned)
|
||||
self.assertTrue(bool(q_food_vegetarian_cloned))
|
||||
# Correct conditional layout
|
||||
self.assertEqual(q_food_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian_cloned.id)
|
||||
self.assertEqual(q_food_vegetarian_cloned.triggering_answer_id.id,
|
||||
q_is_vegetarian_cloned.suggested_answer_ids[0].id)
|
||||
self.assertEqual(q_food_vegetarian_cloned.triggering_answer_ids.ids,
|
||||
[q_is_vegetarian_cloned.suggested_answer_ids[0].id, q_is_vegetarian_cloned.suggested_answer_ids[2].id])
|
||||
# Doesn't reference the original survey
|
||||
self.assertNotEqual(q_food_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian.id)
|
||||
self.assertNotEqual(q_food_vegetarian_cloned.triggering_answer_id.id,
|
||||
q_is_vegetarian.suggested_answer_ids[0].id)
|
||||
self.assertNotEqual(q_food_vegetarian_cloned.triggering_answer_ids.ids,
|
||||
[q_is_vegetarian.suggested_answer_ids[0].id, q_is_vegetarian.suggested_answer_ids[2].id])
|
||||
|
||||
# Not vegetarian choice
|
||||
self.assertTrue(q_food_not_vegetarian_cloned.is_conditional)
|
||||
self.assertTrue(bool(q_food_not_vegetarian_cloned.triggering_answer_ids))
|
||||
# Correct conditional layout
|
||||
self.assertEqual(q_food_not_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian_cloned.id)
|
||||
self.assertEqual(q_food_not_vegetarian_cloned.triggering_answer_id.id,
|
||||
q_is_vegetarian_cloned.suggested_answer_ids[1].id)
|
||||
self.assertEqual(q_food_not_vegetarian_cloned.triggering_answer_ids.ids,
|
||||
q_is_vegetarian_cloned.suggested_answer_ids[1].ids)
|
||||
# Doesn't reference the original survey
|
||||
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_question_id.id, q_is_vegetarian.id)
|
||||
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_answer_id.id,
|
||||
q_is_vegetarian.suggested_answer_ids[1].id)
|
||||
self.assertNotEqual(q_food_not_vegetarian_cloned.triggering_answer_ids.ids,
|
||||
q_is_vegetarian.suggested_answer_ids[1].ids)
|
||||
|
||||
@users('survey_manager')
|
||||
def test_copy_conditional_question_with_sequence_changed(self):
|
||||
@@ -427,11 +411,7 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
q_2.write({'sequence': 100})
|
||||
|
||||
# Set a conditional question on the first question
|
||||
q_1.write({
|
||||
'is_conditional': True,
|
||||
'triggering_question_id': q_2.id,
|
||||
'triggering_answer_id': q_2.suggested_answer_ids[0].id,
|
||||
})
|
||||
q_1.write({'triggering_answer_ids': [Command.set([q_2.suggested_answer_ids[0].id])]})
|
||||
|
||||
(q_1 | q_2).invalidate_recordset()
|
||||
|
||||
@@ -443,51 +423,101 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
self.assertEqual(get_question_by_title(cloned_survey, 'Q2').sequence, q_2.sequence)
|
||||
|
||||
# Check that the conditional question is correctly copied to the right question
|
||||
self.assertEqual(get_question_by_title(cloned_survey, 'Q1').triggering_question_id.title, q_1.triggering_question_id.title)
|
||||
self.assertFalse(get_question_by_title(cloned_survey, 'Q2').triggering_question_id)
|
||||
self.assertEqual(
|
||||
get_question_by_title(cloned_survey, 'Q1').triggering_answer_ids[0].value, q_1.triggering_answer_ids[0].value
|
||||
)
|
||||
self.assertFalse(bool(get_question_by_title(cloned_survey, 'Q2').triggering_answer_ids))
|
||||
|
||||
@users('survey_manager')
|
||||
def test_suggested_answer_display_name(self):
|
||||
"""Check that answers' display name is not too long and allows to identify the question & answer."""
|
||||
# A case's shape is: (question_title, answer value, expected display name)
|
||||
cases = [
|
||||
(
|
||||
'Question 1',
|
||||
'Answer A is short',
|
||||
'Question 1 : Answer A is short',
|
||||
), (
|
||||
'Question 2',
|
||||
'Answer B is a very long answer, so it should itself be shortened or we would go too far',
|
||||
'Question 2 : Answer B is a very long answer, so it should itself be shortened or we...',
|
||||
), (
|
||||
'Question 3 is a very long question, so what can we do?',
|
||||
'Answer A is short',
|
||||
'Question 3 is a very long question, so what can we do? : Answer A is short',
|
||||
), (
|
||||
'Question 4 is a very long question, so what can we do?',
|
||||
'Answer B is a bit too long for Q4 now',
|
||||
'Question 4 is a very long question, so what can... : Answer B is a bit too long for Q4 now',
|
||||
), (
|
||||
'Question 5 is a very long question, so what can we do?',
|
||||
'Answer C is so long that both the question and the answer will be shortened',
|
||||
'Question 5 is a very long... : Answer C is so long that both the question and the...',
|
||||
),
|
||||
]
|
||||
|
||||
for question_title, answer_value, exp_display_name in cases:
|
||||
question = self.env['survey.question'].create({
|
||||
'title': question_title,
|
||||
'suggested_answer_ids': [Command.create({'value': answer_value})],
|
||||
})
|
||||
|
||||
with self.subTest(question=question_title, answer=answer_value):
|
||||
self.assertEqual(question.suggested_answer_ids[0].display_name, exp_display_name)
|
||||
|
||||
@users('survey_manager')
|
||||
def test_unlink_triggers(self):
|
||||
# Create the survey questions
|
||||
q_is_vegetarian_text = 'Are you vegetarian?'
|
||||
q_is_vegetarian = self._add_question(
|
||||
self.page_0, q_is_vegetarian_text, 'multiple_choice', survey_id=self.survey.id,
|
||||
sequence=100, labels=[{'value': 'Yes'}, {'value': 'No'}])
|
||||
self.page_0, q_is_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=100,
|
||||
labels=[{'value': 'Yes'}, {'value': 'No'}, {'value': 'It depends'}], constr_mandatory=True,
|
||||
)
|
||||
|
||||
q_is_kinda_vegetarian_text = 'Would you prefer a veggie meal if possible?'
|
||||
q_is_kinda_vegetarian = self._add_question(
|
||||
self.page_0, q_is_kinda_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=101,
|
||||
labels=[{'value': 'Yes'}, {'value': 'No'}], constr_mandatory=True, triggering_answer_ids=[
|
||||
Command.link(q_is_vegetarian.suggested_answer_ids[1].id), # It depends
|
||||
],
|
||||
)
|
||||
|
||||
q_food_vegetarian_text = 'Choose your green meal'
|
||||
veggie_question = self._add_question(
|
||||
self.page_0, q_food_vegetarian_text, 'multiple_choice',
|
||||
is_conditional=True, sequence=101,
|
||||
triggering_question_id=q_is_vegetarian.id,
|
||||
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[0].id,
|
||||
survey_id=self.survey.id,
|
||||
labels=[{'value': 'Vegetarian pizza'}, {'value': 'Vegetarian burger'}])
|
||||
self.page_0, q_food_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=102,
|
||||
labels=[{'value': 'Vegetarian pizza'}, {'value': 'Vegetarian burger'}], constr_mandatory=True,
|
||||
triggering_answer_ids=[
|
||||
Command.link(q_is_vegetarian.suggested_answer_ids[0].id), # Veggie
|
||||
Command.link(q_is_kinda_vegetarian.suggested_answer_ids[0].id), # Would prefer veggie
|
||||
])
|
||||
|
||||
q_food_not_vegetarian_text = 'Choose your meal'
|
||||
not_veggie_question = self._add_question(
|
||||
self.page_0, q_food_not_vegetarian_text, 'multiple_choice',
|
||||
is_conditional=True, sequence=102,
|
||||
triggering_question_id=q_is_vegetarian.id,
|
||||
triggering_answer_id=q_is_vegetarian.suggested_answer_ids[1].id,
|
||||
survey_id=self.survey.id,
|
||||
labels=[{'value': 'Steak with french fries'}, {'value': 'Fish'}])
|
||||
self.page_0, q_food_not_vegetarian_text, 'simple_choice', survey_id=self.survey.id, sequence=103,
|
||||
labels=[{'value': 'Steak with french fries'}, {'value': 'Fish'}], constr_mandatory=True,
|
||||
triggering_answer_ids=[
|
||||
Command.link(q_is_vegetarian.suggested_answer_ids[1].id), # Not a veggie
|
||||
Command.link(q_is_kinda_vegetarian.suggested_answer_ids[1].id), # Would not prefer veggie
|
||||
],
|
||||
)
|
||||
|
||||
q_is_kinda_vegetarian.unlink()
|
||||
|
||||
# Deleting one trigger but maintaining another keeps conditional behavior
|
||||
self.assertTrue(bool(veggie_question.triggering_answer_ids))
|
||||
|
||||
q_is_vegetarian.suggested_answer_ids[0].unlink()
|
||||
|
||||
# Deleting answer Yes makes the following question always visible
|
||||
self.assertEqual(veggie_question.is_conditional, False)
|
||||
self.assertEqual(veggie_question.triggering_question_id.id, False)
|
||||
self.assertEqual(veggie_question.triggering_answer_id.id, False)
|
||||
self.assertFalse(bool(veggie_question.triggering_answer_ids))
|
||||
|
||||
# But the other is still conditional
|
||||
self.assertEqual(not_veggie_question.is_conditional, True)
|
||||
self.assertEqual(not_veggie_question.triggering_question_id.id, q_is_vegetarian.id)
|
||||
self.assertEqual(not_veggie_question.triggering_answer_id.id, q_is_vegetarian.suggested_answer_ids[0].id)
|
||||
self.assertEqual(not_veggie_question.triggering_answer_ids[0].id, q_is_vegetarian.suggested_answer_ids[0].id)
|
||||
|
||||
q_is_vegetarian.unlink()
|
||||
|
||||
# Now it will also be always visible
|
||||
self.assertEqual(not_veggie_question.is_conditional, False)
|
||||
self.assertEqual(not_veggie_question.triggering_question_id.id, False)
|
||||
self.assertEqual(not_veggie_question.triggering_answer_id.id, False)
|
||||
self.assertFalse(bool(not_veggie_question.triggering_answer_ids))
|
||||
|
||||
def test_get_pages_and_questions_to_show(self):
|
||||
"""
|
||||
@@ -497,20 +527,15 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
|
||||
Structure of the test survey:
|
||||
|
||||
sequence | type | trigger | validity
|
||||
sequence | type | trigger | validity
|
||||
----------------------------------------------------------------------
|
||||
1 | page, no description | / | X
|
||||
2 | text_box | trigger is 6 | X
|
||||
3 | numerical_box | trigger is 2 | X
|
||||
4 | simple_choice | / | V
|
||||
5 | page, description | / | V
|
||||
6 | multiple_choice | / | V
|
||||
7 | multiple_choice, no answers | / | V
|
||||
8 | text_box | trigger is 6 | V
|
||||
9 | matrix | trigger is 5 | X
|
||||
10 | simple_choice | trigger is 7 | X
|
||||
11 | simple_choice, no answers | trigger is 8 | X
|
||||
12 | text_box | trigger is 11 | X
|
||||
1 | page, no description | / | X
|
||||
2 | simple_choice | trigger is 5 | X
|
||||
3 | simple_choice | trigger is 2 | X
|
||||
4 | page, description | / | V
|
||||
5 | multiple_choice | / | V
|
||||
6 | text_box | triggers are 5+7 | V
|
||||
7 | multiple_choice | | V
|
||||
"""
|
||||
|
||||
my_survey = self.env['survey.survey'].create({
|
||||
@@ -521,17 +546,12 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
})
|
||||
[
|
||||
page_without_description,
|
||||
text_box_1,
|
||||
numerical_box,
|
||||
_simple_choice_1,
|
||||
page_with_description,
|
||||
multiple_choice_1,
|
||||
multiple_choice_2,
|
||||
text_box_2,
|
||||
matrix,
|
||||
simple_choice_1,
|
||||
simple_choice_2,
|
||||
simple_choice_3,
|
||||
text_box_3,
|
||||
_page_with_description,
|
||||
multiple_choice_1,
|
||||
text_box_2,
|
||||
multiple_choice_2,
|
||||
] = self.env['survey.question'].create([{
|
||||
'title': 'no desc',
|
||||
'survey_id': my_survey.id,
|
||||
@@ -540,86 +560,52 @@ class TestSurveyInternals(common.TestSurveyCommon):
|
||||
'is_page': True,
|
||||
'description': False,
|
||||
}, {
|
||||
'title': 'text_box with invalid trigger',
|
||||
'title': 'simple choice with invalid trigger',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 2,
|
||||
'is_page': False,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [(0, 0, {'value': 'a'})],
|
||||
}, {
|
||||
'title': 'numerical box with trigger that is invalid',
|
||||
'title': 'simple_choice with chained invalid trigger',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 3,
|
||||
'is_page': False,
|
||||
'question_type': 'numerical_box',
|
||||
}, {
|
||||
'title': 'valid simple_choice',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 4,
|
||||
'is_page': False,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [(0, 0, {'value': 'a'})],
|
||||
}, {
|
||||
'title': 'with desc',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 5,
|
||||
'sequence': 4,
|
||||
'is_page': True,
|
||||
'question_type': False,
|
||||
'description': 'This page has a description',
|
||||
}, {
|
||||
'title': 'multiple choice not conditional',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 6,
|
||||
'sequence': 5,
|
||||
'is_page': False,
|
||||
'question_type': 'multiple_choice',
|
||||
'suggested_answer_ids': [(0, 0, {'value': 'a'})]
|
||||
}, {
|
||||
'title': 'multiple_choice with no answers',
|
||||
'title': 'text_box with valid trigger',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 6,
|
||||
'is_page': False,
|
||||
'question_type': 'text_box',
|
||||
}, {
|
||||
'title': 'valid multiple_choice',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 7,
|
||||
'is_page': False,
|
||||
'question_type': 'multiple_choice',
|
||||
}, {
|
||||
'title': 'text_box with valid trigger',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 8,
|
||||
'is_page': False,
|
||||
'question_type': 'text_box',
|
||||
}, {
|
||||
'title': 'matrix with invalid trigger (page)',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 9,
|
||||
'is_page': False,
|
||||
'question_type': 'matrix',
|
||||
}, {
|
||||
'title': 'simple choice w/ invalid trigger (no suggested_answer_ids)',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 10,
|
||||
'is_page': False,
|
||||
'question_type': 'simple_choice',
|
||||
}, {
|
||||
'title': 'text_box w/ invalid trigger (not a mcq)',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 11,
|
||||
'is_page': False,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': False,
|
||||
}, {
|
||||
'title': 'text_box w/ invalid trigger (suggested_answer_ids is False)',
|
||||
'survey_id': my_survey.id,
|
||||
'sequence': 12,
|
||||
'is_page': False,
|
||||
'question_type': 'text_box',
|
||||
'suggested_answer_ids': [(0, 0, {'value': 'a'})]
|
||||
}])
|
||||
text_box_1.write({'is_conditional': True, 'triggering_question_id': multiple_choice_1.id})
|
||||
numerical_box.write({'is_conditional': True, 'triggering_question_id': text_box_1.id})
|
||||
text_box_2.write({'is_conditional': True, 'triggering_question_id': multiple_choice_1.id})
|
||||
matrix.write({'is_conditional': True, 'triggering_question_id': page_with_description.id})
|
||||
simple_choice_2.write({'is_conditional': True, 'triggering_question_id': multiple_choice_2.id})
|
||||
simple_choice_3.write({'is_conditional': True, 'triggering_question_id': text_box_2.id})
|
||||
text_box_3.write({'is_conditional': True, 'triggering_question_id': simple_choice_3.id})
|
||||
simple_choice_1.write({'triggering_answer_ids': multiple_choice_1.suggested_answer_ids})
|
||||
simple_choice_2.write({'triggering_answer_ids': multiple_choice_1.suggested_answer_ids})
|
||||
text_box_2.write({'triggering_answer_ids': (multiple_choice_1 | multiple_choice_2).suggested_answer_ids})
|
||||
|
||||
invalid_records = page_without_description + text_box_1 + numerical_box \
|
||||
+ matrix + simple_choice_2 + simple_choice_3 + text_box_3
|
||||
invalid_records = page_without_description + simple_choice_1 + simple_choice_2
|
||||
question_and_page_ids = my_survey.question_and_page_ids
|
||||
returned_questions_and_pages = my_survey._get_pages_and_questions_to_show()
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestSurveyFlowWithConditions(common.TestSurveyCommon, HttpCase):
|
||||
page_0, 'Question 2', 'simple_choice',
|
||||
sequence=2,
|
||||
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=survey.id,
|
||||
is_conditional=True, triggering_question_id=q01.id, triggering_answer_id=q01.suggested_answer_ids.filtered(lambda q: q.is_correct).id,
|
||||
triggering_answer_ids=q01.suggested_answer_ids.filtered(lambda q: q.is_correct),
|
||||
labels=[
|
||||
{'value': 'Answer 1'},
|
||||
{'value': 'Answer 2', 'is_correct': True, 'answer_score': 1.0},
|
||||
@@ -60,11 +60,13 @@ class TestSurveyFlowWithConditions(common.TestSurveyCommon, HttpCase):
|
||||
{'value': 'Answer 4', 'is_correct': True, 'answer_score': 1.0}
|
||||
])
|
||||
|
||||
q03_suggested_answers_triggering_q04 = q03.suggested_answer_ids.filtered(lambda q: q.is_correct)
|
||||
|
||||
self._add_question( # q04
|
||||
page_0, 'Question 4', 'simple_choice',
|
||||
sequence=2,
|
||||
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=survey.id,
|
||||
is_conditional=True, triggering_question_id=q03.id, triggering_answer_id=q03.suggested_answer_ids.filtered(lambda q: q.is_correct).id,
|
||||
triggering_answer_ids=q03_suggested_answers_triggering_q04,
|
||||
labels=[
|
||||
{'value': 'Answer 1'},
|
||||
{'value': 'Answer 2', 'is_correct': True, 'answer_score': 1.0},
|
||||
@@ -87,7 +89,23 @@ class TestSurveyFlowWithConditions(common.TestSurveyCommon, HttpCase):
|
||||
page_0, 'Question 6', 'simple_choice',
|
||||
sequence=2,
|
||||
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=survey.id,
|
||||
is_conditional=True, triggering_question_id=q05.id, triggering_answer_id=q05.suggested_answer_ids.filtered(lambda q: q.is_correct).id,
|
||||
triggering_answer_ids=q05.suggested_answer_ids.filtered(lambda q: q.is_correct),
|
||||
labels=[
|
||||
{'value': 'Answer 1'},
|
||||
{'value': 'Answer 2', 'is_correct': True, 'answer_score': 1.0},
|
||||
{'value': 'Answer 3'},
|
||||
{'value': 'Answer 4'}
|
||||
])
|
||||
|
||||
q03_suggested_answers_triggering_q07 = q03.suggested_answer_ids - q03_suggested_answers_triggering_q04
|
||||
# Make sure to have a case with multiple possible triggers.
|
||||
self.assertGreater(len(q03_suggested_answers_triggering_q07), 1)
|
||||
|
||||
q07 = self._add_question(
|
||||
page_0, 'Question 7', 'simple_choice',
|
||||
sequence=2,
|
||||
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=survey.id,
|
||||
triggering_answer_ids=q03_suggested_answers_triggering_q07,
|
||||
labels=[
|
||||
{'value': 'Answer 1'},
|
||||
{'value': 'Answer 2', 'is_correct': True, 'answer_score': 1.0},
|
||||
@@ -101,6 +119,7 @@ class TestSurveyFlowWithConditions(common.TestSurveyCommon, HttpCase):
|
||||
# -> this should have generated a new user_input with a token
|
||||
user_inputs = self.env['survey.user_input'].search([('survey_id', '=', survey.id)])
|
||||
self.assertEqual(len(user_inputs), 1)
|
||||
self.assertEqual(len(user_inputs.predefined_question_ids), 7)
|
||||
answer_token = user_inputs.access_token
|
||||
|
||||
# User begins survey with first page
|
||||
@@ -117,10 +136,12 @@ class TestSurveyFlowWithConditions(common.TestSurveyCommon, HttpCase):
|
||||
q03: q03.suggested_answer_ids[0], # Wrong
|
||||
q05: q05.suggested_answer_ids[3], # Right
|
||||
q06: q06.suggested_answer_ids[2], # Wrong
|
||||
q07: q07.suggested_answer_ids[1], # Right
|
||||
}
|
||||
|
||||
self._answer_page(page_0, answers, answer_token, csrf_token)
|
||||
self.assertEqual(len(user_inputs.predefined_question_ids), 6, "q04 should have been removed as not triggered.")
|
||||
|
||||
user_inputs.invalidate_recordset()
|
||||
self.assertEqual(round(user_inputs.scoring_percentage), 60, "Three right answers out of five (the fourth one is still hidden)")
|
||||
self.assertEqual(round(user_inputs.scoring_percentage), 67, "Four right answers out of six questions asked.")
|
||||
self.assertFalse(user_inputs.scoring_success)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
|
||||
from odoo import Command
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@@ -150,7 +150,6 @@ class TestUiFeedback(HttpCaseWithUserDemo):
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def test_01_admin_survey_tour(self):
|
||||
access_token = self.survey_feedback.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey', login="admin")
|
||||
@@ -187,41 +186,27 @@ class TestUiFeedback(HttpCaseWithUserDemo):
|
||||
'sequence': 1,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
'value': 'Answer 1',
|
||||
'sequence': 1,
|
||||
}), (0, 0, {
|
||||
'value': 'Answer 2',
|
||||
'sequence': 2,
|
||||
}),
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
Command.create({'value': 'Answer 3'}),
|
||||
],
|
||||
'constr_mandatory': True,
|
||||
}), (0, 0, {
|
||||
}), Command.create({
|
||||
'title': 'Q2',
|
||||
'sequence': 2,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
'value': 'Answer 1',
|
||||
'sequence': 1,
|
||||
}), (0, 0, {
|
||||
'value': 'Answer 2',
|
||||
'sequence': 2,
|
||||
})
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
],
|
||||
'constr_mandatory': True,
|
||||
}), (0, 0, {
|
||||
}), Command.create({
|
||||
'title': 'Q3',
|
||||
'sequence': 3,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
'value': 'Answer 1',
|
||||
'sequence': 1,
|
||||
}), (0, 0, {
|
||||
'value': 'Answer 2',
|
||||
'sequence': 2,
|
||||
})
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
],
|
||||
'constr_mandatory': True,
|
||||
}),
|
||||
@@ -230,24 +215,80 @@ class TestUiFeedback(HttpCaseWithUserDemo):
|
||||
|
||||
q1 = survey_with_triggers.question_ids.filtered(lambda q: q.title == 'Q1')
|
||||
q1_a1 = q1.suggested_answer_ids.filtered(lambda a: a.value == 'Answer 1')
|
||||
q1_a3 = q1.suggested_answer_ids.filtered(lambda a: a.value == 'Answer 3')
|
||||
|
||||
q2 = survey_with_triggers.question_ids.filtered(lambda q: q.title == 'Q2')
|
||||
q2_a1 = q2.suggested_answer_ids.filtered(lambda a: a.value == 'Answer 1')
|
||||
|
||||
q3 = survey_with_triggers.question_ids.filtered(lambda q: q.title == 'Q3')
|
||||
|
||||
q2.write({
|
||||
'is_conditional': True,
|
||||
'triggering_question_id': q1,
|
||||
'triggering_answer_id': q1_a1,
|
||||
})
|
||||
q3.write({
|
||||
'is_conditional': True,
|
||||
'triggering_question_id': q2,
|
||||
'triggering_answer_id': q2_a1,
|
||||
})
|
||||
q2.triggering_answer_ids = q1_a1
|
||||
q3.triggering_answer_ids = q1_a3 | q2_a1
|
||||
|
||||
access_token = survey_with_triggers.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey_chained_conditional_questions')
|
||||
|
||||
def test_05_public_survey_with_trigger_on_different_page(self):
|
||||
"""Check that conditional questions are shown when triggered from a different page too."""
|
||||
survey_with_trigger_on_different_page = self.env['survey.survey'].create({
|
||||
'title': 'Survey With Trigger on a different page',
|
||||
'access_token': '1cb935bd-2399-4ed1-9e10-c649318fb4dc',
|
||||
'access_mode': 'public',
|
||||
'users_can_go_back': True,
|
||||
'questions_layout': 'page_per_section',
|
||||
'description': "<p>Test survey with conditional questions triggered from a previous section</p>",
|
||||
'question_and_page_ids': [
|
||||
Command.create({
|
||||
'title': 'Section 1',
|
||||
'is_page': True,
|
||||
'sequence': 1,
|
||||
'question_type': False,
|
||||
}), Command.create({
|
||||
'title': 'Q1',
|
||||
'sequence': 2,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
Command.create({'value': 'Answer 3'}),
|
||||
],
|
||||
'constr_mandatory': False,
|
||||
}), Command.create({
|
||||
'title': 'Section 2',
|
||||
'is_page': True,
|
||||
'sequence': 3,
|
||||
'question_type': False,
|
||||
}), Command.create({
|
||||
'title': 'Q2',
|
||||
'sequence': 4,
|
||||
'question_type': 'simple_choice',
|
||||
'suggested_answer_ids': [
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
],
|
||||
'constr_mandatory': False,
|
||||
}), Command.create({
|
||||
'title': 'Q3',
|
||||
'sequence': 3,
|
||||
'question_type': 'numerical_box',
|
||||
'constr_mandatory': False,
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
q1 = survey_with_trigger_on_different_page.question_ids.filtered(lambda q: q.title == 'Q1')
|
||||
q1_a1 = q1.suggested_answer_ids.filtered(lambda a: a.value == 'Answer 1')
|
||||
|
||||
q2 = survey_with_trigger_on_different_page.question_ids.filtered(lambda q: q.title == 'Q2')
|
||||
q2_a1 = q2.suggested_answer_ids.filtered(lambda a: a.value == 'Answer 1')
|
||||
|
||||
q3 = survey_with_trigger_on_different_page.question_ids.filtered(lambda q: q.title == 'Q3')
|
||||
|
||||
q3.triggering_answer_ids = q1_a1 | q2_a1
|
||||
|
||||
access_token = survey_with_trigger_on_different_page.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey_conditional_question_on_different_page')
|
||||
|
||||
def test_06_survey_prefill(self):
|
||||
access_token = self.survey_feedback.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey_prefill')
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form string="Survey Question" create="false" class="o_survey_question_view_form">
|
||||
<field name="is_placed_before_trigger" invisible="1"/>
|
||||
<div class="alert alert-warning mb-0 text-center" role="alert" invisible="not is_placed_before_trigger">
|
||||
⚠️ This question is positioned before its trigger and will be skipped.
|
||||
⚠️ This question is positioned before some or all of its triggers and could be skipped.
|
||||
</div>
|
||||
<field name="is_page" invisible="1"/>
|
||||
<field name="page_id" invisible="1" required="False"/>
|
||||
@@ -217,19 +217,16 @@
|
||||
<field name='comment_count_as_answer'
|
||||
invisible="question_type not in ['simple_choice', 'multiple_choice', 'matrix'] or not comments_allowed"/>
|
||||
</group>
|
||||
<group string="Layout">
|
||||
<group string="Conditional display">
|
||||
<p class="text-muted" colspan="2" invisible="questions_selection == 'all'">
|
||||
Conditional display is not available when questions are randomly picked.
|
||||
</p>
|
||||
<field name="is_conditional" invisible="questions_selection == 'random'"/>
|
||||
<field name="allowed_triggering_question_ids" invisible="1"/>
|
||||
<field name="triggering_question_id" options="{'no_create_edit': True, 'no_open': True, 'no_create': True}"
|
||||
domain="[('id', 'in', allowed_triggering_question_ids)]" placeholder="Pick a previous multiple-choice question"
|
||||
invisible="questions_selection == 'random' or not is_conditional"
|
||||
required="is_conditional"/>
|
||||
<field name="triggering_answer_id" options="{'no_open': True, 'no_create': True}"
|
||||
invisible="questions_selection == 'random' or not triggering_question_id"
|
||||
required="is_conditional"/>
|
||||
<field name="triggering_answer_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}"
|
||||
domain="[('question_id', 'in', allowed_triggering_question_ids)]"
|
||||
invisible="questions_selection == 'random'"
|
||||
placeholder="Optional previous answers required"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
@@ -304,11 +301,10 @@
|
||||
<field name="name">survey.question.answer.view.tree</field>
|
||||
<field name="model">survey.question.answer</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Survey Label" create="false">
|
||||
<tree string="Survey Label" create="false" default_order="question_id">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="question_id"/>
|
||||
<field name="matrix_question_id"/>
|
||||
<field name="value"/>
|
||||
<field name="value" string="Answer"/>
|
||||
<field name="answer_score" groups="base.group_no_one"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -77,11 +77,8 @@
|
||||
<field name="random_questions_count"
|
||||
column_invisible="parent.questions_selection == 'all'"
|
||||
invisible="not is_page"/>
|
||||
<field name="is_conditional" column_invisible="True"/>
|
||||
<field name="triggering_question_id" column_invisible="True"/>
|
||||
<field name="triggering_answer_id" column_invisible="True"/>
|
||||
<field name="is_placed_before_trigger" column_invisible="True"/>
|
||||
<field name="allowed_triggering_question_ids" column_invisible="True"/>
|
||||
<field name="triggering_question_ids" column_invisible="True"/>
|
||||
<field name="triggering_answer_ids" column_invisible="True" widget="many2many_tags"/> <!-- widget to fetch display_name -->
|
||||
<widget name="survey_question_trigger"/>
|
||||
<button name="copy" type="object" icon="fa-clone" title="Duplicate Question"/>
|
||||
<control>
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
t-att-data-is-page-description="bool(question and question.is_page and not is_html_empty(question.description))"
|
||||
t-att-data-questions-layout="survey.questions_layout"
|
||||
t-att-data-triggered-questions-by-answer="json.dumps(triggered_questions_by_answer)"
|
||||
t-att-data-triggering-answers-by-question="json.dumps(triggering_answers_by_question)"
|
||||
t-att-data-selected-answers="json.dumps(selected_answers)"
|
||||
t-att-data-refresh-background="any(page.background_image for page in survey.page_ids)">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
@@ -304,8 +305,9 @@
|
||||
<t t-set="display_question"
|
||||
t-value="survey.questions_layout == 'page_per_question'
|
||||
or survey.questions_selection == 'random'
|
||||
or (survey.questions_layout == 'one_page' and not question.is_conditional)
|
||||
or (survey.questions_layout == 'page_per_section' and (not question.is_conditional or triggering_answer_by_question[question.id] in selected_answers))"/>
|
||||
or (survey.questions_layout == 'one_page' and not question.triggering_answer_ids)
|
||||
or (survey.questions_layout == 'page_per_section' and (not question.triggering_answer_ids
|
||||
or any(triggering_answer in selected_answers for triggering_answer in triggering_answers_by_question[question.id])))"/>
|
||||
|
||||
<t t-set="answer_lines" t-value="answer.user_input_line_ids.filtered(lambda line: line.question_id == question)"/>
|
||||
<!--Use Key selection if number of choices is < 26 to keep Z for other choice if any-->
|
||||
|
||||
Reference in New Issue
Block a user