[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:
Odoo's Mergebot
2020-03-27 16:41:16 +01:00
committed by GitHub
8 changed files with 94 additions and 42 deletions
+5 -4
View File
@@ -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),
+54 -29
View File
@@ -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
+7 -1
View File
@@ -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();
}
}
+14 -2
View File
@@ -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>