[FIX] survey: fix minor sessions / free text pages / navigation issues
PURPOSE After the latest developments on survey, notable sessions and "free text questions", a few minor bugs have been introduced. SPECIFICATIONS General specifications, see sub-commits for details: - When showing correct/incorrect answers on bar charts, the correct answers bars should not be faded out. - When going from the Leaderboard to the "Final Leaderboard", it should not duplicate the results. - Correctly handle "free text sections" in survey sessions. - Exclude pages that have no description in 'page_per_question' layout - Correctly navigate through survey with conditional questions activated - Fix minor issues with the "back" button on survey - Skip section with no active questions (when conditional questions configured) Task 2208574 closes odoo/odoo#48442 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -13,7 +13,7 @@ from odoo.addons.base.models.ir_ui_view import keep_query
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request, content_disposition
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import format_datetime, format_date
|
||||
from odoo.tools import format_datetime, format_date, is_html_empty
|
||||
|
||||
from odoo.addons.web.controllers.main import Binary
|
||||
|
||||
@@ -266,6 +266,7 @@ class Survey(http.Controller):
|
||||
- previous_page_id : come from the breadcrumb or the back button and force the next questions to load
|
||||
to be the previous ones. """
|
||||
data = {
|
||||
'is_html_empty': is_html_empty,
|
||||
'survey': survey_sudo,
|
||||
'answer': answer_sudo,
|
||||
'breadcrumb_pages': [{
|
||||
@@ -283,9 +284,9 @@ class Survey(http.Controller):
|
||||
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()},
|
||||
answer.id: triggered_questions_by_answer[answer].ids
|
||||
for answer in triggered_questions_by_answer.keys()
|
||||
},
|
||||
'selected_answers': selected_answers.ids
|
||||
})
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from werkzeug.urls import url_join
|
||||
|
||||
from odoo import fields, http
|
||||
from odoo.http import request
|
||||
from odoo.tools import is_html_empty
|
||||
|
||||
|
||||
class UserInputSession(http.Controller):
|
||||
@@ -165,6 +166,7 @@ class UserInputSession(http.Controller):
|
||||
} for line in full_statistics.get('table_data', request.env['survey.user_input.line'])[:100]]
|
||||
|
||||
return {
|
||||
'is_html_empty': is_html_empty,
|
||||
'question_statistics_graph': full_statistics.get('graph_data'),
|
||||
'input_line_values': input_line_values,
|
||||
'answers_validity': json.dumps(answers_validity),
|
||||
|
||||
@@ -9,6 +9,7 @@ import werkzeug
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import is_html_empty
|
||||
|
||||
|
||||
class Survey(models.Model):
|
||||
@@ -437,23 +438,38 @@ class Survey(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_pages_or_questions(self, user_input):
|
||||
if self.questions_layout == 'one_page':
|
||||
return self.env['survey.question']
|
||||
elif self.questions_layout == 'page_per_question' and self.questions_selection == 'random':
|
||||
return user_input.predefined_question_ids
|
||||
else:
|
||||
return self.question_and_page_ids if self.questions_layout == 'page_per_question' else self.page_ids
|
||||
""" Returns the pages or questions (depending on the layout) that will be shown
|
||||
to the user taking the survey.
|
||||
In 'page_per_question' layout, we also want to show pages that have a description. """
|
||||
|
||||
result = self.env['survey.question']
|
||||
if self.questions_layout == 'page_per_section':
|
||||
result = self.page_ids
|
||||
elif self.questions_layout == 'page_per_question':
|
||||
if self.questions_selection == 'random':
|
||||
result = user_input.predefined_question_ids
|
||||
else:
|
||||
result = self.question_and_page_ids.filtered(
|
||||
lambda question: not question.is_page or not is_html_empty(question.description))
|
||||
|
||||
return result
|
||||
|
||||
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.
|
||||
""" Generalized logic to retrieve the next question or page to show on the survey.
|
||||
It's based on the page_or_question_id parameter, that is usually the currently displayed question/page.
|
||||
|
||||
There is a special case when the survey is configured with conditional questions:
|
||||
- for "page_per_question" layout, the next question to display depends on the selected answers and
|
||||
the questions 'hierarchy'.
|
||||
- for "page_per_section" layout, before returning the result, we check that it contains at least a question
|
||||
(all section questions could be disabled based on previously selected answers)
|
||||
|
||||
The whole logic is inverted if "go_back" is passed as True.
|
||||
|
||||
: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
|
||||
:param go_back: reverse the logic and get the PREVIOUS question/page
|
||||
:return: next or previous question/page
|
||||
"""
|
||||
|
||||
survey = user_input.survey_id
|
||||
@@ -471,19 +487,31 @@ class Survey(models.Model):
|
||||
current_page_index = pages_or_questions.ids.index(next(p.id for p in pages_or_questions if p.id == page_or_question_id))
|
||||
|
||||
# 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:
|
||||
if (go_back and current_page_index == 0) or (not go_back and 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
|
||||
if survey.has_conditional_questions and triggered_questions_by_answer:
|
||||
if survey.questions_layout == 'page_per_question':
|
||||
question_candidates = pages_or_questions[0:current_page_index] if go_back \
|
||||
else pages_or_questions[current_page_index + 1:]
|
||||
for question in question_candidates.sorted(reverse=go_back):
|
||||
triggering_answer = triggering_answer_by_question.get(question)
|
||||
if not triggering_answer or triggering_answer in selected_answers:
|
||||
# question is visible because not conditioned or conditioned by a selected answer
|
||||
# -> return it
|
||||
return question
|
||||
elif survey.questions_layout == 'page_per_section':
|
||||
inactive_questions = user_input._get_inactive_conditional_questions()
|
||||
section_candidates = pages_or_questions[0:current_page_index] if go_back \
|
||||
else pages_or_questions[current_page_index + 1:]
|
||||
for section in section_candidates.sorted(reverse=go_back):
|
||||
if any(question not in inactive_questions for question in section.question_ids):
|
||||
# section contains at least one active question
|
||||
# -> return it
|
||||
return section
|
||||
return Question
|
||||
else:
|
||||
return pages_or_questions[current_page_index + (1 if not go_back else -1)]
|
||||
|
||||
@@ -580,13 +608,10 @@ class Survey(models.Model):
|
||||
if not self.question_ids or not self.env.user.has_group('survey.group_survey_user'):
|
||||
return
|
||||
|
||||
if not self.session_question_id:
|
||||
next_question = self.question_ids[0]
|
||||
else:
|
||||
most_voted_answers = self._get_session_most_voted_answers()
|
||||
next_question = self._get_next_page_or_question(most_voted_answers, self.session_question_id.id)
|
||||
|
||||
return next_question
|
||||
most_voted_answers = self._get_session_most_voted_answers()
|
||||
return self._get_next_page_or_question(
|
||||
most_voted_answers,
|
||||
self.session_question_id.id if self.session_question_id else 0)
|
||||
|
||||
def _get_session_most_voted_answers(self):
|
||||
""" In sessions of survey that has conditional questions, as the survey is passed at the same time by
|
||||
|
||||
@@ -51,7 +51,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
$(document).on('keypress', self._onKeyPress.bind(self));
|
||||
}
|
||||
if (self.options.sessionInProgress &&
|
||||
(self.options.isStartScreen || self.options.hasAnswered)) {
|
||||
(self.options.isStartScreen || self.options.hasAnswered || self.options.isPageDescription)) {
|
||||
self.preventEnterSubmit = true;
|
||||
}
|
||||
self._initSessionManagement();
|
||||
@@ -428,6 +428,12 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
}
|
||||
self._initChoiceItems();
|
||||
self._initTextArea();
|
||||
|
||||
if (this.options.sessionInProgress && this.$('.o_survey_form_content_data').data('isPageDescription')) {
|
||||
// prevent enter submit if we're on a page description (there is nothing to submit)
|
||||
this.preventEnterSubmit = true;
|
||||
}
|
||||
|
||||
this.$('.o_survey_form_content').fadeIn(this.fadeInOutDelay);
|
||||
$("html, body").animate({ scrollTop: 0 }, this.fadeInOutDelay);
|
||||
self._focusOnFirstInput();
|
||||
|
||||
@@ -199,8 +199,8 @@ publicWidget.registry.SurveySessionChart = publicWidget.Widget.extend({
|
||||
_getBackgroundColor: function (metaData) {
|
||||
var opacity = '0.8';
|
||||
if (this.showAnswers && this.hasCorrectAnswers) {
|
||||
if (this._isValidAnswer(metaData.dataIndex)){
|
||||
opacity = 0.3;
|
||||
if (!this._isValidAnswer(metaData.dataIndex)){
|
||||
opacity = '0.3';
|
||||
}
|
||||
}
|
||||
var rgb = SESSION_CHART_COLORS[metaData.dataIndex];
|
||||
|
||||
@@ -156,15 +156,17 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
|
||||
delete this.resultsRefreshInterval;
|
||||
this.$('.o_survey_session_navigation_previous').removeClass('d-none');
|
||||
} else if (['leaderboard', 'leaderboardFinal'].includes(screenToDisplay)
|
||||
&& this.currentScreen !== 'leaderboardFinal') {
|
||||
if (screenToDisplay === 'leaderboardFinal') {
|
||||
this.$('.o_survey_session_leaderboard_title').text(_('Final Leaderboard'));
|
||||
&& !['leaderboard', 'leaderboardFinal'].includes(this.currentScreen)) {
|
||||
if (this.isLastQuestion) {
|
||||
this.$('.o_survey_session_navigation_next').addClass('d-none');
|
||||
}
|
||||
this.leaderBoard.showLeaderboard(true);
|
||||
} else {
|
||||
if (!this.isLastQuestion) {
|
||||
this._nextQuestion();
|
||||
} else if (!this.sessionShowLeaderboard) {
|
||||
// If we have no leaderboard to show, directly end the session
|
||||
this.$('.o_survey_session_close').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
t-att-data-is-start-screen="answer.state == 'new'"
|
||||
t-att-data-readonly="survey_form_readonly"
|
||||
t-att-data-has-answered="bool(has_answered)"
|
||||
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-selected-answers="json.dumps(selected_answers)">
|
||||
@@ -139,6 +140,7 @@
|
||||
<div class="o_survey_form_content_data d-none"
|
||||
t-att-data-question-time-limit-reached="answer.question_time_limit_reached"
|
||||
t-att-data-has-answered="bool(has_answered)"
|
||||
t-att-data-is-page-description="bool(question and question.is_page and not is_html_empty(question.description))"
|
||||
t-att-data-timer="timer_start"
|
||||
t-att-data-time-limit-minutes="time_limit_minutes"/>
|
||||
<t t-if="survey.questions_layout == 'one_page'">
|
||||
@@ -192,13 +194,23 @@
|
||||
<div t-if="answer.question_time_limit_reached and not has_answered" class="font-weight-bold mb128">Sorry, you have not been fast enough.</div>
|
||||
<div t-else="" class="font-weight-bold mb128">We have registered your answer! Please wait for the host to go to the next question.</div>
|
||||
</t>
|
||||
<t t-elif="answer.is_session_answer and question.is_page and not is_html_empty(question.description)">
|
||||
<div class="font-weight-bold mt-5">Pay attention to the host screen until the next question.</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="survey.question_container"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="offset-3 col-6 text-center mt16 mb128">
|
||||
<button t-if="not answer.is_session_answer and survey.users_can_go_back and question != answer.predefined_question_ids[0]" type="submit" class="btn btn-secondary"
|
||||
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
|
||||
<!-- Can go back if:
|
||||
- Back button is allowed in configuration
|
||||
- This is not a session answer (doesn't make sense to go back in session context)
|
||||
- This is not the first question
|
||||
- The survey doesn't have pages OR this is not the first page of the survey
|
||||
(pages are displayed in 'page_per_question' layout when they have a description, see PR#44271)
|
||||
-->
|
||||
<button t-if="survey.users_can_go_back and not answer.is_session_answer and question != answer.predefined_question_ids[0] and (not survey.page_ids or question != survey.page_ids[0])"
|
||||
type="submit" class="btn btn-secondary" name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
|
||||
<button type="submit" t-att-value="'next' if not survey_last else 'finish'" class="btn btn-primary">
|
||||
<t t-if="answer.is_session_answer">
|
||||
Submit
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
</div>
|
||||
<div t-elif="show_text_answers" class="p-2 pt-4 o_survey_session_text_answers_container">
|
||||
</div>
|
||||
<div t-elif="question.is_page and not is_html_empty(question.description)" class="mb-6 o_survey_manage_fontsize_14" t-field="question.description" />
|
||||
<div t-else="" class="mb-6">
|
||||
<h2 class="font-weight-normal mb-3">
|
||||
<span>Waiting for attendees...</span>
|
||||
@@ -129,7 +130,10 @@
|
||||
</div>
|
||||
<div class="o_survey_session_leaderboard w-100 flex-column flex-grow-1" style="display: none;">
|
||||
<div class="d-flex">
|
||||
<h1 class="o_survey_session_leaderboard_title flex-grow-1">Leaderboard</h1>
|
||||
<h1 class="o_survey_session_leaderboard_title flex-grow-1">
|
||||
<span t-if="is_last_question">Final Leaderboard</span>
|
||||
<span t-else="">Leaderboard</span>
|
||||
</h1>
|
||||
<div t-att-class="'o_survey_leaderboard_buttons font-weight-bold %s' % 'd-none' if not is_last_question else ''">
|
||||
<a href="#" role="button" class="o_survey_session_close btn btn-primary mr-4"><i class="fa fa-close"/> Close</a>
|
||||
<a href="#" role="button" class="o_survey_session_close btn btn-primary" t-att-data-show-results="True"><i class="fa fa-bar-chart"/> Results</a>
|
||||
|
||||
Reference in New Issue
Block a user