diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index dfa0a49687f..3b9b29a9036 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -460,7 +460,7 @@ class Survey(http.Controller): errors = {} # Prepare answers / comment by question, validate and save answers - inactive_questions = answer_sudo._get_inactive_conditional_questions() + inactive_questions = request.env['survey.question'] if answer_sudo.is_session_answer else answer_sudo._get_inactive_conditional_questions() for question in questions: if question in inactive_questions: # if question is inactive, skip validation and save continue @@ -472,7 +472,7 @@ 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. + if not answer_sudo.is_session_answer: answer_sudo._clear_inactive_conditional_answers() if answer_sudo.survey_time_limit_reached or survey_sudo.questions_layout == 'one_page': diff --git a/addons/survey/controllers/survey_session_manage.py b/addons/survey/controllers/survey_session_manage.py index 929ac7e4819..ad7154b8753 100644 --- a/addons/survey/controllers/survey_session_manage.py +++ b/addons/survey/controllers/survey_session_manage.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import datetime import json +from dateutil.relativedelta import relativedelta from werkzeug.exceptions import NotFound from werkzeug.urls import url_join -from odoo import http +from odoo import fields, http from odoo.http import request @@ -57,10 +59,25 @@ class UserInputSession(http.Controller): if survey.session_state == 'ready': survey._session_open() - survey._session_trigger_next_question() - template_values = self._prepare_manage_session_values(survey) - template_values['is_rpc_call'] = True - return request.env.ref('survey.user_input_session_manage_content').render(template_values) + next_question = survey._get_session_next_question() + + # using datetime.datetime because we want the millis portion + if next_question: + now = datetime.datetime.now() + survey.sudo().write({ + 'session_question_id': next_question.id, + 'session_question_start_time': fields.Datetime.now() + relativedelta(seconds=2) + }) + request.env['bus.bus'].sendone(survey.access_token, { + 'question_start': now.timestamp(), + 'type': 'next_question' + }) + + template_values = self._prepare_manage_session_values(survey) + template_values['is_rpc_call'] = True + return request.env.ref('survey.user_input_session_manage_content').render(template_values) + else: + return False @http.route('/survey/session/results/', type='json', auth='user', website=True) def survey_session_results(self, survey_token, **kwargs): diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 781e66b6e98..9af1f059d08 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import datetime import json import random import uuid import werkzeug -from dateutil.relativedelta import relativedelta - from odoo import api, exceptions, fields, models, _ from odoo.exceptions import AccessError from odoo.osv import expression @@ -566,7 +563,7 @@ class Survey(models.Model): self.sudo().write({'session_state': 'in_progress'}) self.sudo().flush(['session_state']) - def _session_trigger_next_question(self): + def _get_session_next_question(self): """ Triggers the next question of the session. We artificially add 2 seconds to the 'current_question_start_time' to account for server delay. @@ -585,28 +582,52 @@ class Survey(models.Model): return if not self.session_question_id: - question = self.question_ids[0] + next_question = self.question_ids[0] else: - questions = list(enumerate(self.question_ids)) - current_question_index = questions.index( - next(question for question in questions if question[1] == self.session_question_id) - ) - if current_question_index < len(self.question_ids) - 1: - question = self.question_ids[current_question_index + 1] - else: - question = self.session_question_id + 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) - # using datetime.datetime because we want the millis portion - now = datetime.datetime.now() - self.sudo().write({ - 'session_question_id': question.id, - 'session_question_start_time': fields.Datetime.now() + relativedelta(seconds=2) - }) - self.env['bus.bus'].sendone(self.access_token, { - 'question_start': now.timestamp(), - 'type': 'next_question' + return next_question + + 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 + many users, we need to extract the most chosen answers, to determine the next questions to display. """ + + # get user_inputs from current session + current_user_inputs = self.user_input_ids.filtered(lambda input: input.create_date > self.session_start_time) + current_user_input_lines = current_user_inputs.mapped('user_input_line_ids').filtered(lambda answer: answer.suggested_answer_id) + + # count the number of vote per answer + votes_by_answer = dict.fromkeys(current_user_input_lines.mapped('suggested_answer_id'), 0) + for answer in current_user_input_lines: + votes_by_answer[answer.suggested_answer_id] += 1 + + # extract most voted answer for each question + most_voted_answer_by_questions = dict.fromkeys(current_user_input_lines.mapped('question_id')) + for question in most_voted_answer_by_questions.keys(): + for answer in votes_by_answer.keys(): + if answer.question_id != question: + continue + most_voted_answer = most_voted_answer_by_questions[question] + if not most_voted_answer or votes_by_answer[most_voted_answer] < votes_by_answer[answer]: + most_voted_answer_by_questions[question] = answer + + # return a fake 'audiance' user_input + fake_user_input = self.env['survey.user_input'].new({ + 'survey_id': self.id, }) + fake_user_input_lines = self.env['survey.user_input.line'] + for question, answer in most_voted_answer_by_questions.items(): + fake_user_input_lines |= self.env['survey.user_input.line'].new({ + 'question_id': question.id, + 'suggested_answer_id': answer.id, + 'survey_id': self.id, + 'user_input_id': fake_user_input.id + }) + + return fake_user_input + def _prepare_leaderboard_values(self): """" The leaderboard is descending and takes the total of the attendee points up to the current question. """ self.ensure_one() diff --git a/addons/survey/static/src/js/survey_session_leaderboard.js b/addons/survey/static/src/js/survey_session_leaderboard.js index 0126a00f1c7..ae050ddedcf 100644 --- a/addons/survey/static/src/js/survey_session_leaderboard.js +++ b/addons/survey/static/src/js/survey_session_leaderboard.js @@ -24,14 +24,21 @@ publicWidget.registry.SurveySessionLeaderboard = publicWidget.Widget.extend({ * * The width of the progress bars is set after the rendering to enable a width css animation. */ - showLeaderboard: function () { + showLeaderboard: function (fadeOut) { var self = this; var resolveFadeOut; - var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; }); - self.$sessionResults.fadeOut(400, function () { - resolveFadeOut(); - }); + var fadeOutPromise; + if (fadeOut) { + fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; }); + self.$sessionResults.fadeOut(400, function () { + resolveFadeOut(); + }); + } else { + fadeOutPromise = Promise.resolve(); + self.$sessionResults.hide(); + self.$('.o_survey_session_leaderboard_container').empty(); + } var leaderboardPromise = this._rpc({ route: _.str.sprintf('/survey/session/leaderboard/%s', this.surveyAccessToken) diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js index 0d473cdc9b9..aec3c1f76bf 100644 --- a/addons/survey/static/src/js/survey_session_manage.js +++ b/addons/survey/static/src/js/survey_session_manage.js @@ -161,7 +161,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({ this.$('.o_survey_session_leaderboard_title').text(_('Final Leaderboard')); this.$('.o_survey_session_navigation_next').addClass('d-none'); } - this.leaderBoard.showLeaderboard(); + this.leaderBoard.showLeaderboard(true); } else { if (!this.isLastQuestion) { this._nextQuestion(); @@ -317,12 +317,23 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({ } Promise.all([fadeOutPromise, nextQuestionPromise]).then(function (results) { - var $renderedTemplate = $(results[1]); - self.$el.replaceWith($renderedTemplate); - self.attachTo($renderedTemplate); - self.$el.fadeIn(1000, function () { - self._startTimer(); - }); + if (results[1]) { + var $renderedTemplate = $(results[1]); + self.$el.replaceWith($renderedTemplate); + self.attachTo($renderedTemplate); + self.$el.fadeIn(1000, function () { + self._startTimer(); + }); + } else { + // Display last screen + self.isLastQuestion = true; + self._setupLeaderboard().then(function () { + self.$('.o_survey_session_leaderboard_title').text(_('Final Leaderboard')); + self.$('.o_survey_session_navigation_next').addClass('d-none'); + self.$('.o_survey_leaderboard_buttons').removeClass('d-none'); + self.leaderBoard.showLeaderboard(false); + }); + } }); }, diff --git a/addons/survey/views/survey_templates_user_input_session.xml b/addons/survey/views/survey_templates_user_input_session.xml index 995673a0966..6f3ff38d282 100644 --- a/addons/survey/views/survey_templates_user_input_session.xml +++ b/addons/survey/views/survey_templates_user_input_session.xml @@ -130,7 +130,7 @@