diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 1c07cb9a715..dfa0a49687f 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -275,6 +275,19 @@ class Survey(http.Controller):
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
'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()
+ 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]
+ },
+ 'triggered_questions_by_answer': {
+ answer.id: [
+ triggered_questions_by_answer[answer].ids
+ ] for answer in triggered_questions_by_answer.keys()},
+ 'selected_answers': selected_answers.ids
+ })
if not answer_sudo.is_session_answer and survey_sudo.is_time_limited and answer_sudo.start_datetime:
data.update({
@@ -287,7 +300,7 @@ class Survey(http.Controller):
# Bypass all if page_id is specified (comes from breadcrumb or previous button)
if 'previous_page_id' in post:
previous_page_or_question_id = int(post['previous_page_id'])
- new_previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, previous_page_or_question_id)
+ new_previous_id = survey_sudo._get_next_page_or_question(answer_sudo, previous_page_or_question_id, go_back=True).id
data.update({
page_or_question_key: request.env['survey.question'].sudo().browse(previous_page_or_question_id),
'previous_page_id': new_previous_id,
@@ -297,28 +310,31 @@ class Survey(http.Controller):
if answer_sudo.state == 'in_progress':
if answer_sudo.is_session_answer:
- page_or_question_id, is_last = survey_sudo.session_question_id, False
+ next_page_or_question = survey_sudo.session_question_id
else:
- page_or_question_id, is_last = survey_sudo.next_page_or_question(
+ next_page_or_question = survey_sudo._get_next_page_or_question(
answer_sudo,
answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0)
- if answer_sudo.is_session_answer and page_or_question_id.is_time_limited:
+ if next_page_or_question:
+ pages_or_questions = survey_sudo._get_pages_or_questions(answer_sudo)
+ if pages_or_questions.ids.index(next_page_or_question.id) == len(pages_or_questions) - 1:
+ data.update({'survey_last': True})
+
+ if answer_sudo.is_session_answer and next_page_or_question.is_time_limited:
data.update({
'timer_start': survey_sudo.session_question_start_time.isoformat(),
- 'time_limit_minutes': page_or_question_id.time_limit / 60
+ 'time_limit_minutes': next_page_or_question.time_limit / 60
})
data.update({
- page_or_question_key: page_or_question_id,
- 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id == page_or_question_id)
+ page_or_question_key: next_page_or_question,
+ 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id == next_page_or_question)
})
if survey_sudo.questions_layout != 'one_page':
data.update({
- 'previous_page_id': survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id)
+ 'previous_page_id': survey_sudo._get_next_page_or_question(answer_sudo, next_page_or_question.id, go_back=True).id
})
- if is_last:
- data.update({'last': True})
elif answer_sudo.state == 'done' or answer_sudo.survey_time_limit_reached:
# Display success message
return self._prepare_survey_finished_values(survey_sudo, answer_sudo)
@@ -444,7 +460,10 @@ class Survey(http.Controller):
errors = {}
# Prepare answers / comment by question, validate and save answers
+ inactive_questions = answer_sudo._get_inactive_conditional_questions()
for question in questions:
+ if question in inactive_questions: # if question is inactive, skip validation and save
+ continue
answer, comment = self._extract_comment_from_answers(question, post.get(str(question.id)))
errors.update(question.validate_question(answer, comment))
if not errors.get(question.id):
@@ -453,6 +472,9 @@ class Survey(http.Controller):
if errors and not (answer_sudo.survey_time_limit_reached or answer_sudo.question_time_limit_reached):
return {'error': 'validation', 'fields': errors}
+ if not answer_sudo.is_session_answer: # Ignore conditional configurations during survey session.
+ answer_sudo._clear_inactive_conditional_answers()
+
if answer_sudo.survey_time_limit_reached or survey_sudo.questions_layout == 'one_page':
answer_sudo._mark_done()
elif 'previous_page_id' in post:
@@ -461,8 +483,8 @@ class Survey(http.Controller):
else:
vals = {'last_displayed_page_id': page_or_question_id}
if not answer_sudo.is_session_answer:
- next_page, unused = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id)
- if next_page is None:
+ next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id)
+ if not next_page:
answer_sudo._mark_done()
answer_sudo.write(vals)
diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py
index 2ef1631c8ce..1e130d749c7 100644
--- a/addons/survey/models/survey_question.py
+++ b/addons/survey/models/survey_question.py
@@ -130,6 +130,23 @@ 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)]""")
+ 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 the current question.",
+ domain="[('question_id', '=', triggering_question_id)]")
+
_sql_constraints = [
('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
@@ -193,6 +210,25 @@ class SurveyQuestion(models.Model):
if question.question_type != 'char_box':
question.save_as_nickname = False
+ @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."""
+ 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
+
# Validation methods
def validate_question(self, answer, comment=None):
""" Validate question, depending on question type and parameters
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
index f589629ee5d..781e66b6e98 100644
--- a/addons/survey/models/survey_survey.py
+++ b/addons/survey/models/survey_survey.py
@@ -83,7 +83,8 @@ class Survey(models.Model):
string="Scoring", required=True, default='no_scoring')
scoring_success_min = fields.Float('Success %', default=80.0)
# attendees context: attempts and time limitation
- is_attempts_limited = fields.Boolean('Limited number of attempts', help="Check this option if you want to limit the number of attempts per user")
+ is_attempts_limited = fields.Boolean('Limited number of attempts', help="Check this option if you want to limit the number of attempts per user",
+ compute="_compute_is_attempts_limited", store=True, readonly=False)
attempts_limit = fields.Integer('Number of attempts', default=1)
is_time_limited = fields.Boolean('The survey is limited in time')
time_limit = fields.Float("Time limit (minutes)")
@@ -126,6 +127,8 @@ class Survey(models.Model):
session_show_leaderboard = fields.Boolean("Show Session Leaderboard", compute='_compute_session_show_leaderboard',
help="Whether or not we want to show the attendees leaderboard for this survey.")
session_speed_rating = fields.Boolean("Reward quick answers", help="Attendees get more points if they answer quickly")
+ # conditional questions management
+ has_conditional_questions = fields.Boolean("Contains conditional questions", compute="_compute_has_conditional_questions")
_sql_constraints = [
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
@@ -178,6 +181,14 @@ class Survey(models.Model):
survey.page_ids = survey.question_and_page_ids.filtered(lambda question: question.is_page)
survey.question_ids = survey.question_and_page_ids - survey.page_ids
+ @api.depends('question_and_page_ids.is_conditional', 'users_login_required', 'access_mode')
+ def _compute_is_attempts_limited(self):
+ for survey in self:
+ if any(question.is_conditional for question in survey.question_and_page_ids)\
+ or (survey.access_mode == 'public' and not survey.users_login_required)\
+ or survey.is_attempts_limited is None:
+ survey.is_attempts_limited = False
+
@api.depends('session_start_time', 'user_input_ids')
def _compute_session_answer_count(self):
""" We have to loop since our result is dependent of the survey.session_start_time.
@@ -221,6 +232,11 @@ 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')
+ 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)
+
@api.onchange('scoring_success_min')
def _onchange_scoring_success_min(self):
if self.scoring_success_min < 0 or self.scoring_success_min > 100:
@@ -232,11 +248,6 @@ class Survey(models.Model):
self.certification = False
self.is_time_limited = False
- @api.onchange('users_login_required', 'access_mode')
- def _onchange_access_mode(self):
- if self.access_mode == 'public' and not self.users_login_required:
- self.is_attempts_limited = False
-
@api.onchange('attempts_limit')
def _onchange_attempts_limit(self):
if self.attempts_limit <= 0:
@@ -430,63 +441,57 @@ class Survey(models.Model):
@api.model
def _get_pages_or_questions(self, user_input):
if self.questions_layout == 'one_page':
- return None
+ return self.env['survey.question']
elif self.questions_layout == 'page_per_question' and self.questions_selection == 'random':
- return list(enumerate(
- user_input.predefined_question_ids
- ))
+ return user_input.predefined_question_ids
else:
- return list(enumerate(
- self.question_ids if self.questions_layout == 'page_per_question' else self.page_ids
- ))
+ return self.question_ids if self.questions_layout == 'page_per_question' else self.page_ids
- @api.model
- def _previous_page_or_question_id(self, user_input, page_or_question_id):
- survey = user_input.survey_id
- pages_or_questions = survey._get_pages_or_questions(user_input)
-
- current_page_index = pages_or_questions.index(next(p for p in pages_or_questions if p[1].id == page_or_question_id))
- # is first page
- if current_page_index == 0:
- return None
-
- previous_page_id = pages_or_questions[current_page_index - 1][1].id
-
- return previous_page_id
-
- @api.model
- def next_page_or_question(self, user_input, page_or_question_id):
- """ The next page to display to the user, knowing that page_id is the id
- of the last displayed page.
-
- If page_id == 0, it will always return the first page of the survey.
-
- If all the pages have been displayed, it will return None
-
- .. note::
- It is assumed here that a careful user will not try to set go_back
- to True if she knows that the page to display is the first one!
- (doing this will probably cause a giant worm to eat her house)
+ def _get_next_page_or_question(self, user_input, page_or_question_id, go_back=False):
+ """
+ This methods gets the next question or page to display.
+ In case the survey contains conditional question, when navigating to next or previous questions,
+ in page_per_question layout only, the next or previous question to display depends on the selected answers
+ and the questions hierarchy. This methods returns the next question index to display among the survey_questions.
+ :param user_input: user's answers
+ :param page_or_question_id: current page or question id
+ :param go_back: must be True to get the previous question
+ :return: next or previous question.id
"""
survey = user_input.survey_id
-
pages_or_questions = survey._get_pages_or_questions(user_input)
- if not pages_or_questions:
- return (None, False)
+ Question = self.env['survey.question']
- # First page
- if page_or_question_id == 0:
- return (pages_or_questions[0][1], len(pages_or_questions) == 1)
+ # Get Next
+ if not go_back:
+ if not pages_or_questions:
+ return Question
+ # First page
+ if page_or_question_id == 0:
+ return pages_or_questions[0]
- current_page_index = pages_or_questions.index(next(p for p in pages_or_questions if p[1].id == page_or_question_id))
+ current_page_index = pages_or_questions.ids.index(next(p.id for p in pages_or_questions if p.id == page_or_question_id))
- # All the pages have been displayed
- if current_page_index == len(pages_or_questions) - 1:
- return (None, False)
+ # Get previous and we are on first page OR Get Next and we are on last page
+ if (go_back and current_page_index == 0) or current_page_index == len(pages_or_questions) - 1:
+ return Question
+
+ # Conditional Questions Management
+ triggering_answer_by_question, triggered_questions_by_answer, selected_answers = user_input._get_conditional_values()
+ if survey.has_conditional_questions and survey.questions_layout == 'page_per_question' and triggered_questions_by_answer:
+ potential_next_questions = pages_or_questions[0:current_page_index] if go_back \
+ else pages_or_questions[current_page_index+1:]
+ for question in sorted(potential_next_questions, key=lambda q: q.id, reverse=go_back):
+ triggering_answer = triggering_answer_by_question.get(question)
+ if not triggering_answer or triggering_answer in selected_answers:
+ # If next question found
+ return question
else:
- show_last = current_page_index == len(pages_or_questions) - 2
- return (pages_or_questions[current_page_index + 1][1], show_last)
+ return pages_or_questions[current_page_index + (1 if not go_back else -1)]
+
+ return Question
+
def _get_survey_questions(self, answer=None, page_id=None, question_id=None):
""" Returns a tuple containing: the survey question and the passed question_id / page_id
@@ -530,6 +535,23 @@ class Survey(models.Model):
questions = questions & answer.predefined_question_ids
return questions, page_or_question_id
+ # ------------------------------------------------------------
+ # CONDITIONAL QUESTIONS MANAGEMENT
+ # ------------------------------------------------------------
+
+ def _get_conditional_maps(self):
+ triggering_answer_by_question = {}
+ triggered_questions_by_answer = {}
+ for question in self.question_ids:
+ triggering_answer_by_question[question] = question.is_conditional and question.triggering_answer_id
+
+ 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
+
# ------------------------------------------------------------
# SESSIONS MANAGEMENT
# ------------------------------------------------------------
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
index af5d745145c..d6ea0b1fed8 100644
--- a/addons/survey/models/survey_user.py
+++ b/addons/survey/models/survey_user.py
@@ -55,7 +55,7 @@ class SurveyUserInput(models.Model):
('unique_token', 'UNIQUE (access_token)', 'An access token must be unique!'),
]
- @api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id')
+ @api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id', 'predefined_question_ids')
def _compute_scoring_values(self):
for user_input in self:
total_possible_score = sum([
@@ -209,6 +209,9 @@ class SurveyUserInput(models.Model):
if user_input.survey_id.certification_give_badge:
badge_ids.append(user_input.survey_id.certification_badge_id.id)
+ # Update predefined_question_id to remove inactive questions
+ user_input.predefined_question_ids -= user_input._get_inactive_conditional_questions()
+
if badge_ids:
challenges = Challenge.search([('reward_id', 'in', badge_ids)])
if challenges:
@@ -359,6 +362,90 @@ class SurveyUserInput(models.Model):
{'text': _("Unanswered"), 'count': res[user_input]['skipped']}
] for user_input in self]
+ # ------------------------------------------------------------
+ # Conditional Questions Management
+ # ------------------------------------------------------------
+
+ def _get_conditional_values(self):
+ """ For survey containing conditional questions, we need a triggered_questions_by_answer map that contains
+ {key: answer, value: the question that the answer triggers, if selected},
+ The idea is to be able to verify, on every answer check, if this answer is triggering the display
+ of another question.
+ If answer is not in the conditional map:
+ - nothing happens.
+ If the answer is in the conditional map:
+ - If we are in ONE PAGE survey : (handled at CLIENT side)
+ -> display immediately the depending question
+ - If we are in PAGE PER SECTION : (handled at CLIENT side)
+ - If related question is on the same page :
+ -> display immediately the depending question
+ - If the related question is not on the same page :
+ -> keep the answers in memory and check at next page load if the depending question is in there and
+ display it, if so.
+ - If we are in PAGE PER QUESTION : (handled at SERVER side)
+ -> During submit, determine which is the next question to display getting the next question
+ 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)
+ - 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
+ """
+ triggering_answer_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()
+ selected_answers = self._get_selected_suggested_answers()
+
+ return triggering_answer_by_question, triggered_questions_by_answer, selected_answers
+
+ def _get_selected_suggested_answers(self):
+ """
+ For now, only simple and multiple choices question type are handled by the conditional questions feature.
+ Mapping all the suggested answers selected by the user will also include answers from matrix question type,
+ Those ones won't be used.
+ Maybe someday, conditional questions feature will be extended to work with matrix question.
+ :return: all the suggested answer selected by the user.
+ """
+ return self.mapped('user_input_line_ids.suggested_answer_id')
+
+ def _clear_inactive_conditional_answers(self):
+ """
+ Clean eventual answers on conditional questions that should not have been displayed to user.
+ This method is used mainly for page per question survey, a similar method does the same treatment
+ at client side for the other survey layouts.
+ E.g.: if depending answer was uncheck after answering conditional question, we need to clear answers
+ of that conditional question, for two reasons:
+ - ensure correct scoring
+ - if the selected answer triggers another question later in the survey, if the answer is not cleared,
+ a question that should not be displayed to the user will be.
+
+ TODO DBE: Maybe this can be the only cleaning method, even for section_per_page or one_page where
+ conditional questions are, for now, cleared in JS directly. But this can be annoying if user typed a long
+ answer, changed his mind unchecking depending answer and changed again his mind by rechecking the depending
+ answer -> For now, the long answer will be lost. If we use this as the master cleaning method,
+ long answer will be cleared only during submit.
+ """
+ inactive_questions = self._get_inactive_conditional_questions()
+
+ # delete user.input.line on question that should not be answered.
+ answers_to_delete = self.user_input_line_ids.filtered(lambda answer: answer.question_id in inactive_questions)
+ answers_to_delete.unlink()
+
+ def _get_inactive_conditional_questions(self):
+ triggering_answer_by_question, triggered_questions_by_answer, 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
+ return inactive_questions
+
class SurveyUserInputLine(models.Model):
_name = 'survey.user_input.line'
diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js
index 21bad10c7fd..34e429eacee 100644
--- a/addons/survey/static/src/js/survey_form.js
+++ b/addons/survey/static/src/js/survey_form.js
@@ -34,6 +34,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
return this._super.apply(this, arguments).then(function () {
self.options = self.$target.find('form').data();
self.readonly = self.options.readonly;
+ self.selectedAnswers = self.options.selectedAnswers;
// Init fields
if (!self.options.isStartScreen && !self.readonly) {
self._initTimer();
@@ -105,6 +106,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
* @param {Event} event
*/
_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');
@@ -126,8 +128,42 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
$matrixBtn.closest('tr').find('td').removeClass('o_survey_selected');
$matrixBtn.addClass('o_survey_selected');
} else {
- $choiceItemGroup.find('label').removeClass('o_survey_selected');
- $target.closest('label').addClass('o_survey_selected');
+ var previouslySelectedAnswer = $choiceItemGroup.find('label.o_survey_selected');
+ previouslySelectedAnswer.removeClass('o_survey_selected');
+
+ var newlySelectedAnswer = $target.closest('label');
+ if (newlySelectedAnswer.find('input').val() !== previouslySelectedAnswer.find('input').val()) {
+ newlySelectedAnswer.addClass('o_survey_selected');
+ }
+
+ // 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
+ this.selectedAnswers.push(parseInt($target.val()));
+ }
+ }
}
} else { // $target.attr('type') === 'checkbox'
if ($matrixBtn.length > 0) {
@@ -135,6 +171,25 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
} else {
var $label = $target.closest('label');
$label.toggleClass('o_survey_selected', !$label.hasClass('o_survey_selected'));
+
+ // 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()));
+ }
+ }
}
}
},
@@ -342,6 +397,12 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
if (result && !result.error) {
this.$(".o_survey_form_content").empty();
this.$(".o_survey_form_content").html(result);
+
+ // Hide timer if end screen (if page_per_question in case of conditional questions)
+ if (self.options.questionsLayout === 'page_per_question' && this.$('.o_survey_finished').length > 0) {
+ options.isFinish = true;
+ }
+
this.$('div.o_survey_form_date').each(function () {
self._initDateTimePicker($(this));
});
@@ -408,16 +469,24 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
data[key] = value;
});
+ var inactiveQuestionIds = this.options.sessionInProgress ? [] : this._getInactiveConditionalQuestionIds();
+
$form.find('[data-question-type]').each(function () {
var $input = $(this);
var $questionWrapper = $input.closest(".js_question-wrapper");
+ var questionId = $questionWrapper.attr('id');
+
+ // If question is inactive, skip validation.
+ if (inactiveQuestionIds.includes(parseInt(questionId))) {
+ return;
+ }
+
+ var questionRequired = $questionWrapper.data('required');
var constrErrorMsg = $questionWrapper.data('constrErrorMsg');
var validationErrorMsg = $questionWrapper.data('validationErrorMsg');
- var questionId = $questionWrapper.attr('id');
- var questionRequired = $questionWrapper.data('required');
switch ($input.data('questionType')) {
case 'char_box':
- if (questionRequired && !$input.val()) {
+ if (questionRequired && !questionInactive && !$input.val()) {
errors[questionId] = constrErrorMsg;
} else if ($input.val() && $input.attr('type') === 'email' && !self._validateEmail($input.val())) {
errors[questionId] = validationEmailMsg;
@@ -835,6 +904,47 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
}
},
+ // 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.
+ *
+ * @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);
+ });
+ }
+ });
+ }
+ return inactiveQuestionIds;
+ },
+
// ERRORS TOOLS
// -------------------------------------------------------------------------
diff --git a/addons/survey/static/src/scss/survey_views.scss b/addons/survey/static/src/scss/survey_views.scss
index f81f5a660aa..fcfc0ffc9a2 100644
--- a/addons/survey/static/src/scss/survey_views.scss
+++ b/addons/survey/static/src/scss/survey_views.scss
@@ -20,3 +20,7 @@ table.o_section_list_view tr.o_data_row.o_is_section {
border-top: 1px solid #BBB;
border-bottom: 1px solid #BBB;
}
+
+.icon_rotates {
+ transform: rotate(180deg);
+}
diff --git a/addons/survey/views/survey_question_views.xml b/addons/survey/views/survey_question_views.xml
index e273f0b1f83..64336d5743b 100644
--- a/addons/survey/views/survey_question_views.xml
+++ b/addons/survey/views/survey_question_views.xml
@@ -157,9 +157,19 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -190,6 +200,9 @@
+
+
@@ -203,6 +216,7 @@
+
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
index f029e3c64f9..47c0ab99b08 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -57,6 +57,7 @@
+
@@ -68,7 +69,11 @@
+
+
+
@@ -96,8 +101,9 @@
-
-