[IMP] survey: allow user to navigate freely
This commit allows the user to navigate freely through a survey with mandatory questions. He will be able to skip questions and come back to them later. If he still hasn't answered them at the final submit they will automatically be displayed with an error message to invite the user to answer them. The user can navigate through the skipped questions with a button within the error box. Task-2605657 closes odoo/odoo#119315 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -257,11 +257,13 @@ class Survey(http.Controller):
|
||||
""" This method prepares all the data needed for template rendering, in function of the survey user input state.
|
||||
:param post:
|
||||
- previous_page_id : come from the breadcrumb or the back button and force the next questions to load
|
||||
to be the previous ones. """
|
||||
to be the previous ones.
|
||||
- next_skipped_page : force the display of next skipped question or page if any."""
|
||||
data = {
|
||||
'is_html_empty': is_html_empty,
|
||||
'survey': survey_sudo,
|
||||
'answer': answer_sudo,
|
||||
'skipped_questions': answer_sudo._get_skipped_questions(),
|
||||
'breadcrumb_pages': [{
|
||||
'id': page.id,
|
||||
'title': page.title,
|
||||
@@ -306,17 +308,23 @@ class Survey(http.Controller):
|
||||
return data
|
||||
|
||||
if answer_sudo.state == 'in_progress':
|
||||
next_page_or_question = None
|
||||
if answer_sudo.is_session_answer:
|
||||
next_page_or_question = survey_sudo.session_question_id
|
||||
else:
|
||||
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 'next_skipped_page' in post:
|
||||
next_page_or_question = answer_sudo._get_next_skipped_page_or_question()
|
||||
if not 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 next_page_or_question:
|
||||
data.update({
|
||||
'survey_last': survey_sudo._is_last_page_or_question(answer_sudo, next_page_or_question)
|
||||
})
|
||||
if answer_sudo.survey_first_submitted:
|
||||
survey_last = answer_sudo._is_last_skipped_page_or_question(next_page_or_question)
|
||||
else:
|
||||
survey_last = survey_sudo._is_last_page_or_question(answer_sudo, next_page_or_question)
|
||||
data.update({'survey_last': survey_last})
|
||||
|
||||
if answer_sudo.is_session_answer and next_page_or_question.is_time_limited:
|
||||
data.update({
|
||||
@@ -377,6 +385,7 @@ class Survey(http.Controller):
|
||||
background_image_url = survey_data['page'].background_image_url
|
||||
|
||||
return {
|
||||
'has_skipped_questions': any(answer_sudo._get_skipped_questions()),
|
||||
'survey_content': survey_content,
|
||||
'survey_progress': survey_progress,
|
||||
'survey_navigation': request.env['ir.qweb']._render('survey.survey_navigation', survey_data),
|
||||
@@ -534,16 +543,31 @@ class Survey(http.Controller):
|
||||
answer_sudo._mark_done()
|
||||
elif 'previous_page_id' in post:
|
||||
# when going back, save the last displayed to reload the survey where the user left it.
|
||||
answer_sudo.write({'last_displayed_page_id': post['previous_page_id']})
|
||||
answer_sudo.last_displayed_page_id = post['previous_page_id']
|
||||
# Go back to specific page using the breadcrumb. Lines are saved and survey continues
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
elif 'next_skipped_page_or_question' in post:
|
||||
answer_sudo.last_displayed_page_id = page_or_question_id
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, next_skipped_page=True)
|
||||
else:
|
||||
if not answer_sudo.is_session_answer:
|
||||
next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id)
|
||||
page_or_question = request.env['survey.question'].sudo().browse(page_or_question_id)
|
||||
if answer_sudo.survey_first_submitted and answer_sudo._is_last_skipped_page_or_question(page_or_question):
|
||||
next_page = request.env['survey.question']
|
||||
else:
|
||||
next_page = survey_sudo._get_next_page_or_question(answer_sudo, page_or_question_id)
|
||||
if not next_page:
|
||||
answer_sudo._mark_done()
|
||||
if survey_sudo.users_can_go_back and answer_sudo.user_input_line_ids.filtered(
|
||||
lambda a: a.skipped and a.question_id.constr_mandatory):
|
||||
answer_sudo.write({
|
||||
'last_displayed_page_id': page_or_question_id,
|
||||
'survey_first_submitted': True,
|
||||
})
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, next_skipped_page=True)
|
||||
else:
|
||||
answer_sudo._mark_done()
|
||||
|
||||
answer_sudo.write({'last_displayed_page_id': page_or_question_id})
|
||||
answer_sudo.last_displayed_page_id = page_or_question_id
|
||||
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo)
|
||||
|
||||
|
||||
@@ -403,11 +403,11 @@ class SurveyQuestion(models.Model):
|
||||
if isinstance(answer, str):
|
||||
answer = answer.strip()
|
||||
# Empty answer to mandatory question
|
||||
if self.constr_mandatory and not answer and self.question_type not in ['simple_choice', 'multiple_choice']:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
|
||||
# because in choices question types, comment can count as answer
|
||||
if answer or self.question_type in ['simple_choice', 'multiple_choice']:
|
||||
if not answer and self.question_type not in ['simple_choice', 'multiple_choice']:
|
||||
if self.constr_mandatory and not self.survey_id.users_can_go_back:
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
else:
|
||||
if self.question_type == 'char_box':
|
||||
return self._validate_char_box(answer)
|
||||
elif self.question_type == 'numerical_box':
|
||||
@@ -473,7 +473,8 @@ class SurveyQuestion(models.Model):
|
||||
|
||||
def _validate_choice(self, answer, comment):
|
||||
# Empty comment
|
||||
if self.constr_mandatory \
|
||||
if not self.survey_id.users_can_go_back \
|
||||
and self.constr_mandatory \
|
||||
and not answer \
|
||||
and not (self.comments_allowed and self.comment_count_as_answer and comment):
|
||||
return {self.id: self.constr_error_msg or _('This question requires an answer.')}
|
||||
|
||||
@@ -52,6 +52,7 @@ class SurveyUserInput(models.Model):
|
||||
scoring_percentage = fields.Float("Score (%)", compute="_compute_scoring_values", store=True, compute_sudo=True) # stored for perf reasons
|
||||
scoring_total = fields.Float("Total Score", compute="_compute_scoring_values", store=True, compute_sudo=True) # stored for perf reasons
|
||||
scoring_success = fields.Boolean('Quizz Passed', compute='_compute_scoring_success', store=True, compute_sudo=True) # stored for perf reasons
|
||||
survey_first_submitted = fields.Boolean(string='Survey First Submitted')
|
||||
# live sessions
|
||||
is_session_answer = fields.Boolean('Is in a Session', help="Is that user input part of a survey session or not.")
|
||||
question_time_limit_reached = fields.Boolean("Question Time Limit Reached", compute='_compute_question_time_limit_reached')
|
||||
@@ -610,6 +611,55 @@ class SurveyUserInput(models.Model):
|
||||
inactive_questions = self._get_inactive_conditional_questions()
|
||||
return survey.question_ids - inactive_questions
|
||||
|
||||
def _get_next_skipped_page_or_question(self):
|
||||
"""Get next skipped question or page in case the option 'can_go_back' is set on the survey
|
||||
It loops to the first skipped question or page if 'last_displayed_page_id' is the last
|
||||
skipped question or page."""
|
||||
self.ensure_one()
|
||||
skipped_mandatory_answer_ids = self.user_input_line_ids.filtered(
|
||||
lambda answer: answer.skipped and answer.question_id.constr_mandatory)
|
||||
|
||||
if not skipped_mandatory_answer_ids:
|
||||
return self.env['survey.question']
|
||||
|
||||
page_or_question_key = 'page_id' if self.survey_id.questions_layout == 'page_per_section' else 'question_id'
|
||||
page_or_question_ids = skipped_mandatory_answer_ids.mapped(page_or_question_key).sorted()
|
||||
|
||||
if self.last_displayed_page_id not in page_or_question_ids\
|
||||
or self.last_displayed_page_id == page_or_question_ids[-1]:
|
||||
return page_or_question_ids[0]
|
||||
|
||||
current_page_index = page_or_question_ids.ids.index(self.last_displayed_page_id.id)
|
||||
return page_or_question_ids[current_page_index + 1]
|
||||
|
||||
def _get_skipped_questions(self):
|
||||
self.ensure_one()
|
||||
|
||||
return self.user_input_line_ids.filtered(
|
||||
lambda answer: answer.skipped and answer.question_id.constr_mandatory).question_id
|
||||
|
||||
def _is_last_skipped_page_or_question(self, page_or_question):
|
||||
"""In case of a submitted survey tells if the question or page is the last
|
||||
skipped page or question.
|
||||
|
||||
This is used to :
|
||||
|
||||
- Display a Submit button if the actual question is the last skipped question.
|
||||
- Avoid displaying a Submit button on the last survey question if there are
|
||||
still skipped questions before.
|
||||
- Avoid displaying the next page if submitting the latest skipped question.
|
||||
|
||||
:param page_or_question: page if survey's layout is page_per_section, question if page_per_question.
|
||||
"""
|
||||
if self.survey_id.questions_layout == 'one_page':
|
||||
return True
|
||||
skipped = self._get_skipped_questions()
|
||||
if not skipped:
|
||||
return True
|
||||
if self.survey_id.questions_layout == 'page_per_section':
|
||||
skipped = skipped.page_id
|
||||
return skipped == page_or_question
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MESSAGING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -130,8 +130,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
if (keyCode === 13 || keyCode === 39) { // Enter or arrow-right: go Next
|
||||
event.preventDefault();
|
||||
if (!this.preventEnterSubmit) {
|
||||
var isFinish = this.$('button[value="finish"]').length !== 0;
|
||||
this._submitForm({isFinish: isFinish});
|
||||
this._submitForm({
|
||||
isFinish: this.el.querySelectorAll('button[value="finish"]').length !== 0,
|
||||
nextSkipped: this.el.querySelectorAll('button[value="next_skipped"]').length !== 0 ? keyCode === 13 : false,
|
||||
});
|
||||
}
|
||||
} else if (keyCode === 37) { // arrow-left: previous (if available)
|
||||
// It's easier to actually click on the button (if in the DOM) as it contains necessary
|
||||
@@ -172,7 +174,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
.closest('.o_survey_form_choice')
|
||||
.find('.o_survey_comment').length !== 0;
|
||||
if (!questionHasComment) {
|
||||
this._submitForm({});
|
||||
this._submitForm({'nextSkipped': $choiceItemGroup.data('isSkippedQuestion')});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,11 +245,13 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
|
||||
_onSubmit: function (event) {
|
||||
event.preventDefault();
|
||||
var options = {};
|
||||
var $target = $(event.currentTarget);
|
||||
if ($target.val() === 'previous') {
|
||||
options.previousPageId = $target.data('previousPageId');
|
||||
} else if ($target.val() === 'finish') {
|
||||
const options = {};
|
||||
const target = event.currentTarget;
|
||||
if (target.value === 'previous') {
|
||||
options.previousPageId = parseInt(target.dataset['previousPageId']);
|
||||
} else if (target.value === 'next_skipped') {
|
||||
options.nextSkipped = true;
|
||||
} else if (target.value === 'finish') {
|
||||
options.isFinish = true;
|
||||
}
|
||||
this._submitForm(options);
|
||||
@@ -365,6 +369,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
if (options.previousPageId) {
|
||||
params.previous_page_id = options.previousPageId;
|
||||
}
|
||||
if (options.nextSkipped) {
|
||||
params.next_skipped_page_or_question = true;
|
||||
}
|
||||
var route = "/survey/submit";
|
||||
|
||||
if (this.options.isStartScreen) {
|
||||
@@ -417,7 +424,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
var fadeOutPromise = new Promise(function (resolve, reject) {resolveFadeOut = resolve;});
|
||||
|
||||
var selectorsToFadeout = ['.o_survey_form_content'];
|
||||
if (options.isFinish) {
|
||||
if (options.isFinish && !this.nextScreenResult.has_skipped_questions) {
|
||||
selectorsToFadeout.push('.breadcrumb', '.o_survey_timer');
|
||||
deleteCookie('survey_' + self.options.surveyToken);
|
||||
}
|
||||
@@ -454,7 +461,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
var self = this;
|
||||
var result = this.nextScreenResult;
|
||||
|
||||
if (!(options && options.isFinish)
|
||||
if ((!(options && options.isFinish) || result.has_skipped_questions)
|
||||
&& !this.options.sessionInProgress) {
|
||||
this.preventEnterSubmit = false;
|
||||
}
|
||||
@@ -489,7 +496,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
this.surveyTimerWidget.destroy();
|
||||
}
|
||||
}
|
||||
if (options && options.isFinish) {
|
||||
if (options && options.isFinish && !result.has_skipped_questions) {
|
||||
this._initResultWidget();
|
||||
if (this.surveyBreadcrumbWidget) {
|
||||
this.$('.o_survey_breadcrumb_container').addClass('d-none');
|
||||
@@ -518,6 +525,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
|
||||
this.$('button[type="submit"]').removeClass('disabled');
|
||||
|
||||
this._scrollToFirstError();
|
||||
self._focusOnFirstInput();
|
||||
} else if (result && result.fields && result.error === 'validation') {
|
||||
this.$('.o_survey_form_content').fadeIn(0);
|
||||
@@ -1126,13 +1134,22 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
var self = this;
|
||||
var errorKeys = Object.keys(errors || {});
|
||||
errorKeys.forEach(key => {
|
||||
self.$("#" + key + '>.o_survey_question_error').append($('<p>', {text: errors[key]})).addClass("slide_in");
|
||||
self.$("#" + key + '>.o_survey_question_error').append($('<span>', {text: errors[key]})).addClass("slide_in");
|
||||
if (errorKeys[0] === key) {
|
||||
self._scrollToError(self.$('.js_question-wrapper#' + key));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is used to scroll to error generated in the backend.
|
||||
* (Those errors are displayed when the user skip mandatory question(s))
|
||||
*/
|
||||
_scrollToFirstError: function() {
|
||||
const errorElem = this.el.querySelector('.o_survey_question_error :not(:empty)');
|
||||
errorElem?.scrollIntoView();
|
||||
},
|
||||
|
||||
_scrollToError: function ($target) {
|
||||
var scrollLocation = $target.offset().top;
|
||||
var navbarHeight = $('.o_main_navbar').height();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
|
||||
registry.category('web_tour.tours').add('test_survey_roaming_mandatory_questions', {
|
||||
test: true,
|
||||
url: '/survey/start/853ebb30-40f2-43bf-a95a-bbf0e367a365',
|
||||
steps: () => [{
|
||||
content: 'Click on Start',
|
||||
trigger: 'button.btn:contains("Start")',
|
||||
}, {
|
||||
content: 'Skip question Q1',
|
||||
trigger: 'button.btn:contains("Continue")',
|
||||
}, {
|
||||
content: 'Skip question Q2',
|
||||
extra_trigger: 'div.js_question-wrapper:contains("Q2")',
|
||||
trigger: 'button.btn:contains("Continue")',
|
||||
}, {
|
||||
content: 'Check if Q3 button is Submit',
|
||||
trigger: 'button.btn:contains("Submit")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Go back to Q2',
|
||||
trigger: 'button.btn[value="previous"]',
|
||||
}, {
|
||||
content: 'Check if the alert box is present',
|
||||
trigger: 'div.o_survey_question_error span',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Skip question Q2 again',
|
||||
trigger: 'button.btn:contains("Continue")',
|
||||
}, {
|
||||
content: 'Answer Q3',
|
||||
trigger: 'div.js_question-wrapper:contains("Q3") label:contains("Answer 1")',
|
||||
}, {
|
||||
content: 'Click on Submit',
|
||||
trigger: 'button.btn:contains("Submit")',
|
||||
}, {
|
||||
content: 'Check if question is Q1',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Click on "Next Skipped" button',
|
||||
trigger: 'button.btn:contains("Next Skipped")',
|
||||
}, {
|
||||
content: 'Check if question is Q2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Click on "Next Skipped" button',
|
||||
trigger: 'button.btn:contains("Next Skipped")',
|
||||
}, {
|
||||
content: 'Check if question is Q1 again (should loop on skipped questions)',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Answer Q1',
|
||||
trigger: 'div.js_question-wrapper:contains("Q1") label:contains("Answer 2")',
|
||||
}, {
|
||||
content: 'Check if the visible question is the skipped question Q2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2")',
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: 'Answer Q2',
|
||||
trigger: 'div.js_question-wrapper:contains("Q2") label:contains("Answer 3")',
|
||||
}, {
|
||||
content: 'Click on Submit',
|
||||
trigger: 'button.btn:contains("Submit")',
|
||||
}, {
|
||||
content: 'Check if the survey is done',
|
||||
trigger: 'div.o_survey_finished h1:contains("Thank you!")',
|
||||
isCheck: true,
|
||||
}],
|
||||
});
|
||||
@@ -292,3 +292,48 @@ class TestUiFeedback(HttpCaseWithUserDemo):
|
||||
def test_06_survey_prefill(self):
|
||||
access_token = self.survey_feedback.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey_prefill')
|
||||
|
||||
def test_07_survey_roaming_mandatory_questions(self):
|
||||
survey_with_mandatory_questions = self.env['survey.survey'].create({
|
||||
'title': 'Survey With Mandatory questions',
|
||||
'access_token': '853ebb30-40f2-43bf-a95a-bbf0e367a365',
|
||||
'access_mode': 'public',
|
||||
'users_can_go_back': True,
|
||||
'questions_layout': 'page_per_question',
|
||||
'description': "<p>Test survey with roaming freely option and mandatory questions</p>",
|
||||
'question_and_page_ids': [
|
||||
Command.create({
|
||||
'title': 'Q1',
|
||||
'sequence': 1,
|
||||
'question_type': 'simple_choice',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
Command.create({'value': 'Answer 3'}),
|
||||
],
|
||||
}), Command.create({
|
||||
'title': 'Q2',
|
||||
'sequence': 2,
|
||||
'question_type': 'simple_choice',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
Command.create({'value': 'Answer 3'}),
|
||||
],
|
||||
}), Command.create({
|
||||
'title': 'Q3',
|
||||
'sequence': 3,
|
||||
'question_type': 'simple_choice',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
Command.create({'value': 'Answer 1'}),
|
||||
Command.create({'value': 'Answer 2'}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
access_token = survey_with_mandatory_questions.access_token
|
||||
self.start_tour("/survey/start/%s" % access_token, 'test_survey_roaming_mandatory_questions')
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
invisible="questions_layout == 'one_page' or survey_type == 'live_session'"/>
|
||||
<field name="questions_selection" widget="radio"
|
||||
invisible="survey_type == 'live_session'"/>
|
||||
<field name="users_can_go_back" string="Back Button"
|
||||
<field name="users_can_go_back" string="Allow Roaming"
|
||||
invisible="questions_layout == 'one_page' or survey_type == 'live_session'"/>
|
||||
</group>
|
||||
<group string="Participants" name="participants">
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
</t>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<button type="submit" value="finish" class="btn btn-primary disabled">Submit</button>
|
||||
<button type="submit" value="finish" class="btn btn-secondary disabled">Submit</button>
|
||||
<span class="fw-bold text-muted ms-2 d-none d-md-inline">
|
||||
<span id="enter-tooltip">or press Enter</span>
|
||||
</span>
|
||||
@@ -209,9 +209,12 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 text-center mt16">
|
||||
<button type="submit" t-att-value="'next' if not survey_last else 'finish'" class="btn btn-primary disabled">
|
||||
<t t-if="not survey_last">Continue</t>
|
||||
<t t-else="">Submit</t>
|
||||
<t t-set="submit_value" t-value="'finish' if survey_last or answer.is_session_answer else 'next_skipped'
|
||||
if answer.survey_first_submitted and skipped_questions.page_id and page in skipped_questions.page_id else 'next'"/>
|
||||
<button type="submit" t-att-value="submit_value" t-attf-class="btn #{'btn-secondary' if survey_last else 'btn-primary'} disabled">
|
||||
<t t-if="submit_value == 'finish'">Submit</t>
|
||||
<t t-elif="submit_value == 'next_skipped'">Next Skipped</t>
|
||||
<t t-else="">Continue</t>
|
||||
</button>
|
||||
<span class="fw-bold text-muted ms-2 d-none d-md-inline" id="enter-tooltip"> or press Enter</span>
|
||||
</div>
|
||||
@@ -247,8 +250,11 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 text-center mt16">
|
||||
<button type="submit" t-att-value="'next' if not survey_last else 'finish'" class="btn btn-primary disabled">
|
||||
<t t-if="answer.is_session_answer or survey_last">Submit</t>
|
||||
<t t-set="submit_value" t-value="'finish' if survey_last or answer.is_session_answer else
|
||||
'next_skipped' if answer.survey_first_submitted and skipped_questions and question in skipped_questions else 'next'"/>
|
||||
<button type="submit" t-att-value="submit_value" t-attf-class="btn #{'btn-secondary' if survey_last else 'btn-primary'} disabled">
|
||||
<t t-if="submit_value == 'finish'">Submit</t>
|
||||
<t t-elif="submit_value == 'next_skipped'">Next Skipped</t>
|
||||
<t t-else="">Continue</t>
|
||||
</button>
|
||||
<span class="fw-bold text-muted ms-2 d-none d-md-inline">
|
||||
@@ -318,10 +324,12 @@
|
||||
<t t-set="default_constr_error_msg">This question requires an answer.</t>
|
||||
<t t-set="default_validation_error_msg">The answer you entered is not valid.</t>
|
||||
<t t-set="default_comments_message">If other, please specify:</t>
|
||||
<t t-set="is_skipped_question" t-value="skipped_questions and question in skipped_questions"/>
|
||||
<div t-att-class="'js_question-wrapper pb-4 %s %s' % ('d-none' if not display_question else '', 'me-2' if extra_right_margin else '')"
|
||||
t-att-id="question.id" t-att-data-required="question.constr_mandatory"
|
||||
t-att-data-constr-error-msg="question.constr_error_msg or default_constr_error_msg"
|
||||
t-att-data-validation-error-msg="question.validation_error_msg or default_validation_error_msg">
|
||||
t-att-id="question.id"
|
||||
t-att-data-required="bool(question.constr_mandatory and (not survey.users_can_go_back or survey.questions_layout == 'one_page')) or None"
|
||||
t-att-data-constr-error-msg="question.constr_error_msg or default_constr_error_msg if question.constr_mandatory else None"
|
||||
t-att-data-validation-error-msg="question.validation_error_msg or default_validation_error_msg if question.validation_required else None">
|
||||
<div class="mb-4">
|
||||
<h3 t-if="not hide_question_title">
|
||||
<span t-field='question.title' class="text-break" />
|
||||
@@ -337,7 +345,10 @@
|
||||
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
|
||||
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
|
||||
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
|
||||
<div class="o_survey_question_error overflow-hidden border-0 py-0 px-3 alert alert-danger" role="alert"></div>
|
||||
<div t-attf-class="o_survey_question_error d-flex align-items-center justify-content-between overflow-hidden
|
||||
border-0 py-0 px-3 alert alert-danger #{'slide_in' if is_skipped_question else ''}" role="alert">
|
||||
<span t-if="is_skipped_question" t-out="question.constr_error_msg or default_constr_error_msg"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -414,6 +425,7 @@
|
||||
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_char_box)"/>
|
||||
<div class="row o_survey_form_choice"
|
||||
t-att-data-name="question.id"
|
||||
t-att-data-is-skipped-question="is_skipped_question or None"
|
||||
data-question-type="simple_choice_radio">
|
||||
<t t-set="item_idx" t-value="0"/>
|
||||
<div t-attf-class="col-lg-12 d-flex flex-wrap">
|
||||
|
||||
Reference in New Issue
Block a user