summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAurélien Warnon <awa@odoo.com>2020-02-11 15:31:50 +0000
committerThibault Delavallée <tde@odoo.com>2020-02-27 16:40:57 +0000
commitde2fa261656244688b6b9952459cb4052e4d22f8 (patch)
treef77784d3f3d2811c3a4bc201a16da6eae859363f
parent84e128980cfd3616f73c4b937c32711fddd50234 (diff)
[IMP] survey: improve survey session frontend layouts
PURPOSE Improve layouts and usability for the host screens when using survey sessions. We focus on the "host" screens, as the attendee screen is based on regular survey flows/design. SPECS In short: copy mentimeter "wow effect". Main points: - Remove the "question" container (keep only "Results/Ranking") - Remove the tabs concept, views are displayed in full screen - Purpose is to have a "clean view" - The display must be "full screen" with only relevant info - Enable a easy keyboard navigation with left/right keyboard arrows and spacebar - Switching from results to ranking to next question is always linear (next/previous instead of tabs) - Improve charts - Remove the pie chart : make bar charts instead for simple / multiple choices - Show the chart results "as people answer to it" Only if question is not scored, otherwise show when all answers are received / when user goes to "next" - For free text/date/datetime: show small bubbles with attendees answers - Add resize, fade in/out, ... animations for "wow effect" LINKS PR #45111 Upgrade PR odoo/upgrade#774 Task 2191924
-rw-r--r--addons/survey/__manifest__.py2
-rw-r--r--addons/survey/controllers/main.py4
-rw-r--r--addons/survey/controllers/survey_session_manage.py74
-rw-r--r--addons/survey/models/survey_survey.py12
-rw-r--r--addons/survey/models/survey_user.py2
-rw-r--r--addons/survey/static/src/js/survey_session_chart.js272
-rw-r--r--addons/survey/static/src/js/survey_session_colors.js21
-rw-r--r--addons/survey/static/src/js/survey_session_leaderboard.js71
-rw-r--r--addons/survey/static/src/js/survey_session_manage.js553
-rw-r--r--addons/survey/static/src/js/survey_session_text_answers.js74
-rw-r--r--addons/survey/static/src/scss/survey_form.scss217
-rw-r--r--addons/survey/static/src/xml/survey_session_text_answer_template.xml11
-rw-r--r--addons/survey/views/assets.xml5
-rw-r--r--addons/survey/views/survey_templates_statistics.xml6
-rw-r--r--addons/survey/views/survey_templates_user_input_session.xml244
15 files changed, 1142 insertions, 426 deletions
diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py
index 8eb338b3704..496d033a0e6 100644
--- a/addons/survey/__manifest__.py
+++ b/addons/survey/__manifest__.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Surveys',
- 'version': '3.0',
+ 'version': '3.1',
'category': 'Marketing/Survey',
'description': """
Create beautiful surveys and visualize answers
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 2c043b2300c..1c07cb9a715 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -616,8 +616,8 @@ class Survey(http.Controller):
'search_finished': post.get('finished') == 'true',
}
- if survey.session_show_ranking:
- template_values['ranking'] = survey._prepare_ranking_values()
+ if survey.session_show_leaderboard:
+ template_values['leaderboard'] = survey._prepare_leaderboard_values()
return request.render('survey.survey_page_statistics', template_values)
diff --git a/addons/survey/controllers/survey_session_manage.py b/addons/survey/controllers/survey_session_manage.py
index 8f5f32f60f1..c743d6e5faf 100644
--- a/addons/survey/controllers/survey_session_manage.py
+++ b/addons/survey/controllers/survey_session_manage.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
+
from werkzeug.exceptions import NotFound
from werkzeug.urls import url_join
@@ -21,8 +23,8 @@ class UserInputSession(http.Controller):
We render a template allowing the host to showcase the different options of the session
and to actually start the session.
- If the state of the session is 'in_progress'
- We render a template allowing the host to show the question results, display the attendee
- ranking or go to the next question of the session. """
+ We render a template allowing the host to show the question results, display the attendees
+ leaderboard or go to the next question of the session. """
survey = self._fetch_from_token(survey_token)
@@ -71,28 +73,20 @@ class UserInputSession(http.Controller):
if not survey or survey.session_state != 'in_progress':
# no open session
- return ''
+ return False
user_input_lines = request.env['survey.user_input.line'].search([
('survey_id', '=', survey.id),
('question_id', '=', survey.session_question_id.id)
])
- question_statistics = survey.session_question_id._prepare_statistics(user_input_lines)[0]
- return request.env.ref('survey.survey_page_statistics_question').render({
- 'page_record_limit': 10,
- 'hide_question_title': True,
- 'no_print_url': True,
- 'survey': survey,
- 'question': survey.session_question_id,
- 'question_data': question_statistics,
- })
+ return self._prepare_question_results_values(survey, user_input_lines)
- @http.route('/survey/session/ranking/<string:survey_token>', type='json', auth='user', website=True)
- def survey_session_ranking(self, survey_token, **kwargs):
- """ This route is called when the host shows the current question's attendees ranking.
+ @http.route('/survey/session/leaderboard/<string:survey_token>', type='json', auth='user', website=True)
+ def survey_session_leaderboard(self, survey_token, **kwargs):
+ """ This route is called when the host shows the current question's attendees leaderboard.
- It's not a regular 'request.render' route because we handle the display of the ranking
+ It's not a regular 'request.render' route because we handle the display of the leaderboard
using an AJAX request to be able to include the results in the currently displayed page. """
survey = self._fetch_from_token(survey_token)
@@ -101,9 +95,9 @@ class UserInputSession(http.Controller):
# no open session
return ''
- return request.env.ref('survey.user_input_session_ranking').render({
+ return request.env.ref('survey.user_input_session_leaderboard').render({
'animate_width': True,
- 'ranking': survey._prepare_ranking_values()
+ 'leaderboard': survey._prepare_leaderboard_values()
})
def _prepare_manage_session_values(self, survey):
@@ -111,8 +105,50 @@ class UserInputSession(http.Controller):
if survey.question_ids:
is_last_question = survey.session_question_id == survey.question_ids[-1]
- return {
+ values = {
'survey': survey,
'is_last_question': is_last_question,
'survey_url': url_join(survey.get_base_url(), survey.get_start_short_url()),
}
+
+ values.update(self._prepare_question_results_values(survey, request.env['survey.user_input.line']))
+
+ return values
+
+ def _prepare_question_results_values(self, survey, user_input_lines):
+ """ Prepares usefull values to display during the host session:
+
+ - question_statistics_graph
+ The graph data to display the bar chart for questions of type 'choice'
+ - input_lines_values
+ The answer values to text/date/datetime questions
+ - answers_validity
+ An array containing the is_correct value for all question answers.
+ We need this special variable because of Chartjs data structure.
+ The library determines the parameters (color/label/...) by only passing the answer 'index'
+ (and not the id or anything else we can identify).
+ In other words, we need to know if the answer at index 2 is correct or not.
+ - answer_count
+ The number of answers to the current question. """
+
+ question = survey.session_question_id
+ answers_validity = []
+ if (any(answer.is_correct for answer in question.suggested_answer_ids)):
+ answers_validity = [answer.is_correct for answer in question.suggested_answer_ids]
+ if question.comment_count_as_answer:
+ answers_validity.append(False)
+
+ full_statistics = question._prepare_statistics(user_input_lines)[0]
+ input_line_values = []
+ if question.question_type in ['char_box', 'date', 'datetime']:
+ input_line_values = [{
+ 'id': line.id,
+ 'value': line['value_%s' % question.question_type]
+ } for line in full_statistics.get('table_data', request.env['survey.user_input.line'])[:100]]
+
+ return {
+ 'question_statistics_graph': full_statistics.get('graph_data'),
+ 'input_line_values': input_line_values,
+ 'answers_validity': json.dumps(answers_validity),
+ 'answer_count': survey.session_question_answer_count
+ }
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
index 6f677693cf4..620a480981a 100644
--- a/addons/survey/models/survey_survey.py
+++ b/addons/survey/models/survey_survey.py
@@ -121,8 +121,8 @@ class Survey(models.Model):
help="The time at which the current question has started, used to handle the timer for attendees.")
session_question_answer_count = fields.Integer("Answers Count", compute='_compute_session_question_answer_count')
# live sessions - settings
- session_show_ranking = fields.Boolean("Show Session Ranking", compute='_compute_session_show_ranking',
- help="This mode will display a ranking chart of all attendees.")
+ 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")
_sql_constraints = [
@@ -194,9 +194,9 @@ class Survey(models.Model):
survey.session_question_answer_count = answer_count
@api.depends('scoring_type', 'question_and_page_ids.save_as_nickname')
- def _compute_session_show_ranking(self):
+ def _compute_session_show_leaderboard(self):
for survey in self:
- survey.session_show_ranking = survey.scoring_type != 'no_scoring' and \
+ survey.session_show_leaderboard = survey.scoring_type != 'no_scoring' and \
any(question.save_as_nickname for question in survey.question_and_page_ids)
@api.onchange('scoring_success_min')
@@ -563,8 +563,8 @@ class Survey(models.Model):
'type': 'next_question'
})
- def _prepare_ranking_values(self):
- """" The ranking is descending and takes the total of the attendee points up to the current question. """
+ 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()
return self.env['survey.user_input'].search([('survey_id', '=', self.id)],
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
index 7c94a8db82a..af5d745145c 100644
--- a/addons/survey/models/survey_user.py
+++ b/addons/survey/models/survey_user.py
@@ -40,7 +40,7 @@ class SurveyUserInput(models.Model):
invite_token = fields.Char('Invite token', readonly=True, copy=False) # no unique constraint, as it identifies a pool of attempts
partner_id = fields.Many2one('res.partner', string='Partner', readonly=True)
email = fields.Char('Email', readonly=True)
- nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify him in survey sessions rankings.")
+ nickname = fields.Char('Nickname', help="Attendee nickname, mainly used to identify him in the survey session leaderboard.")
# questions / answers
user_input_line_ids = fields.One2many('survey.user_input.line', 'user_input_id', string='Answers', copy=True)
predefined_question_ids = fields.Many2many('survey.question', string='Predefined Questions', readonly=True)
diff --git a/addons/survey/static/src/js/survey_session_chart.js b/addons/survey/static/src/js/survey_session_chart.js
new file mode 100644
index 00000000000..3dd2fac13ec
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_chart.js
@@ -0,0 +1,272 @@
+odoo.define('survey.session_chart', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+var SESSION_CHART_COLORS = require('survey.session_colors');
+
+publicWidget.registry.SurveySessionChart = publicWidget.Widget.extend({
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ this.questionType = options.questionType;
+ this.answersValidity = options.answersValidity;
+ this.hasCorrectAnswers = options.hasCorrectAnswers;
+ this.attendeesCount = options.attendeesCount;
+ this.questionStatistics = this._processQuestionStatistics(options.questionStatistics);
+ this.showInputs = options.showInputs;
+ this.showAnswers = false;
+ },
+
+ start: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self._setupChart();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates the chart data using the latest received question user inputs.
+ *
+ * By updating the numbers in the dataset, we take advantage of the Chartjs API
+ * that will automatically add animations to show the new number.
+ *
+ * @param {Object} questionStatistics object containing chart data (counts / labels / ...)
+ */
+ updateChart: function (questionStatistics) {
+ if (questionStatistics) {
+ this.questionStatistics = this._processQuestionStatistics(questionStatistics);
+ }
+
+ if (this.chart) {
+ // only a single dataset for our bar charts
+ var chartData = this.chart.data.datasets[0].data;
+ for (var i = 0; i < chartData.length; i++){
+ var value = 0;
+ if (this.showInputs) {
+ value = this.questionStatistics[i].count;
+ }
+ this.chart.data.datasets[0].data[i] = value;
+ }
+ this.chart.update();
+ }
+ },
+
+ /**
+ * Toggling this parameter will display or hide the correct and incorrect answers of the current
+ * question directly on the chart.
+ *
+ * @param {Boolean} showAnswers
+ */
+ setShowAnswers: function (showAnswers) {
+ this.showAnswers = showAnswers;
+ },
+
+ /**
+ * Toggling this parameter will display or hide the user inputs of the current question directly
+ * on the chart.
+ *
+ * @param {Boolean} showInputs
+ */
+ setShowInputs: function (showInputs) {
+ this.showInputs = showInputs;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _setupChart: function () {
+ var $canvas = this.$('canvas');
+ var ctx = $canvas.get(0).getContext('2d');
+
+ this.chart = new Chart(ctx, this._buildChartConfiguration());
+ },
+
+ /**
+ * Custom bar chart configuration for our survey session use case.
+ *
+ * Quick summary of enabled features:
+ * - background_color is one of the 10 custom colors from SESSION_CHART_COLORS
+ * (see _getBackgroundColor for details)
+ * - The ticks are bigger and bolded to be able to see them better on a big screen (projector)
+ * - We don't use tooltips to keep it as simple as possible
+ * - We use a "suggestedMax" being the attendeesCount +1
+ * The '+1' part is a small trick to avoid the datalabels to be clipped in height
+ * - We use a custom 'datalabels' plugin to be able to display the number value on top of the
+ * associated bar of the chart.
+ * This allows the host to discuss results with attendees in a more interactive way.
+ *
+ * @private
+ */
+ _buildChartConfiguration: function () {
+ return {
+ type: 'bar',
+ data: {
+ labels: this._extractChartLabels(),
+ datasets: [{
+ backgroundColor: this._getBackgroundColor.bind(this),
+ data: this._extractChartData(),
+ }]
+ },
+ options: {
+ maintainAspectRatio: false,
+ plugins: {
+ datalabels: {
+ color: this._getLabelColor.bind(this),
+ font: {
+ size: '25',
+ weight: 'bold',
+ },
+ anchor: 'end',
+ align: 'top',
+ }
+ },
+ legend: {
+ display: false,
+ },
+ scales: {
+ yAxes: [{
+ ticks: {
+ display: false,
+ suggestedMin: 0,
+ suggestedMax: this.attendeesCount + 1,
+ },
+ gridLines: {
+ display: false
+ }
+ }],
+ xAxes: [{
+ ticks: {
+ maxRotation: 0,
+ fontSize: '25',
+ fontStyle: 'bold',
+ fontColor: '#212529'
+ },
+ gridLines: {
+ display: false
+ }
+ }]
+ },
+ tooltips: {
+ enabled: false,
+ }
+ },
+ };
+ },
+
+ /**
+ * Returns the label of the associated survey.question.answer.
+ *
+ * @private
+ */
+ _extractChartLabels: function () {
+ return this.questionStatistics.map(function (point) {
+ return point.text.length > 20 ? point.text.substring(0, 17) + '...' : point.text;
+ });
+ },
+
+ /**
+ * We simply return an array of zeros as initial value.
+ * The chart will update afterwards as attendees add their user inputs.
+ *
+ * @private
+ */
+ _extractChartData: function () {
+ return this.questionStatistics.map(function () {
+ return 0;
+ });
+ },
+
+ /**
+ * Custom method that returns a color from SESSION_CHART_COLORS.
+ * It loops through the ten values and assign them sequentially.
+ *
+ * We have a special mechanic when the host shows the answers of a question.
+ * Wrong answers are "faded out" using a 0.3 opacity.
+ *
+ * @param {Object} metaData
+ * @param {Integer} metaData.dataIndex the index of the label, matching the index of the answer
+ * in 'this.answersValidity'
+ * @private
+ */
+ _getBackgroundColor: function (metaData) {
+ var opacity = '0.8';
+ if (this.showAnswers && this.hasCorrectAnswers) {
+ if (this._isValidAnswer(metaData.dataIndex)){
+ opacity = 0.3;
+ }
+ }
+ var rgb = SESSION_CHART_COLORS[metaData.dataIndex];
+ return `rgba(${rgb},${opacity})`;
+ },
+
+ /**
+ * Custom method that returns the survey.question.answer label color.
+ *
+ * Break-down of use cases:
+ * - Red if the host is showing answer, and the associated answer is not correct
+ * - Green if the host is showing answer, and the associated answer is correct
+ * - Black in all other cases
+ *
+ * @param {Object} metaData
+ * @param {Integer} metaData.dataIndex the index of the label, matching the index of the answer
+ * in 'this.answersValidity'
+ * @private
+ */
+ _getLabelColor: function (metaData) {
+ if (this.showAnswers && this.hasCorrectAnswers) {
+ if (this._isValidAnswer(metaData.dataIndex)){
+ return '#2CBB70';
+ } else {
+ return '#D9534F';
+ }
+ }
+ return '#212529';
+ },
+
+ /**
+ * Small helper method that returns the validity of the answer based on its index.
+ *
+ * We need this special handling because of Chartjs data structure.
+ * The library determines the parameters (color/label/...) by only passing the answer 'index'
+ * (and not the id or anything else we can identify).
+ *
+ * @param {Integer} answerIndex
+ * @private
+ */
+ _isValidAnswer: function (answerIndex) {
+ return this.answersValidity[answerIndex];
+ },
+
+ /**
+ * Special utility method that will process the statistics we receive from the
+ * survey.question#_prepare_statistics method.
+ *
+ * For multiple choice questions, the values we need are stored in a different place.
+ * We simply return the values to make the use of the statistics common for both simple and
+ * multiple choice questions.
+ *
+ * See survey.question#_get_stats_data for more details
+ *
+ * @param {Object} rawStatistics
+ * @private
+ */
+ _processQuestionStatistics: function (rawStatistics) {
+ if (this.questionType === 'multiple_choice') {
+ return rawStatistics[0].values;
+ }
+
+ return rawStatistics;
+ }
+});
+
+return publicWidget.registry.SurveySessionChart;
+
+});
diff --git a/addons/survey/static/src/js/survey_session_colors.js b/addons/survey/static/src/js/survey_session_colors.js
new file mode 100644
index 00000000000..728f2f68483
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_colors.js
@@ -0,0 +1,21 @@
+odoo.define('survey.session_colors', function (require) {
+'use strict';
+
+/**
+ * Small tool that returns common colors for survey session widgets.
+ * Source: https://www.materialui.co/colors
+ */
+return [
+ '25,118,210',
+ '48,63,159',
+ '175,180,43',
+ '0,121,107',
+ '56,142,60',
+ '93,64,55',
+ '97,97,97',
+ '123,31,162',
+ '69,90,100',
+ '211,47,47',
+];
+
+});
diff --git a/addons/survey/static/src/js/survey_session_leaderboard.js b/addons/survey/static/src/js/survey_session_leaderboard.js
new file mode 100644
index 00000000000..0126a00f1c7
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_leaderboard.js
@@ -0,0 +1,71 @@
+odoo.define('survey.session_leaderboard', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+var SESSION_CHART_COLORS = require('survey.session_colors');
+
+publicWidget.registry.SurveySessionLeaderboard = publicWidget.Widget.extend({
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ this.surveyAccessToken = options.surveyAccessToken;
+ this.$sessionResults = options.sessionResults;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Shows the question leaderboard on screen.
+ * It's based on the attendees score (descending).
+ *
+ * We fade out the $sessionResults to fade in our rendered template.
+ *
+ * The width of the progress bars is set after the rendering to enable a width css animation.
+ */
+ showLeaderboard: function () {
+ var self = this;
+
+ var resolveFadeOut;
+ var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
+ self.$sessionResults.fadeOut(400, function () {
+ resolveFadeOut();
+ });
+
+ var leaderboardPromise = this._rpc({
+ route: _.str.sprintf('/survey/session/leaderboard/%s', this.surveyAccessToken)
+ });
+
+ Promise.all([fadeOutPromise, leaderboardPromise]).then(function (results) {
+ var leaderboardResults = results[1];
+ var $renderedTemplate = $(leaderboardResults).addClass('py-3');
+ self.$('.o_survey_session_leaderboard_container').append($renderedTemplate);
+
+ self.$('.o_survey_session_leaderboard_bar').each(function (index) {
+ var rgb = SESSION_CHART_COLORS[index % 10];
+ $(this).css('background-color', `rgba(${rgb},${0.8})`);
+ });
+ self.$el.fadeIn(400, function () {
+ self.$('.o_survey_session_leaderboard_bar').each(function () {
+ $(this).css('width', `calc(calc(100% - 18rem) * ${$(this).data('widthRatio')})`);
+ });
+ });
+ });
+ },
+
+ /**
+ * Inverse the process, fading out our template to fade int the $sessionResults.
+ */
+ hideLeaderboard: function () {
+ var self = this;
+ this.$el.fadeOut(400, function () {
+ self.$('.o_survey_session_leaderboard_container').empty();
+ self.$sessionResults.fadeIn(400);
+ });
+ },
+});
+
+return publicWidget.registry.SurveySessionLeaderboard;
+
+});
diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js
index daa6979b4ab..e3e8091a190 100644
--- a/addons/survey/static/src/js/survey_session_manage.js
+++ b/addons/survey/static/src/js/survey_session_manage.js
@@ -2,6 +2,9 @@ odoo.define('survey.session_manage', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
+var SurveySessionChart = require('survey.session_chart');
+var SurveySessionTextAnswers = require('survey.session_text_answers');
+var SurveySessionLeaderBoard = require('survey.session_leaderboard');
var core = require('web.core');
var _t = core._t;
@@ -9,12 +12,9 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
selector: '.o_survey_session_manage',
events: {
'click .o_survey_session_copy': '_onCopySessionLink',
- 'click .o_survey_session_next, .o_survey_session_start': '_onNextQuestionClick',
- 'click .o_survey_session_end': '_onEndSessionClick',
- 'click .o_survey_session_results_href': '_onResultsTabClick',
- 'click .o_survey_session_ranking_href': '_onRankingTabClick',
- 'click .o_survey_session_refresh_results': '_onRefreshResultsClick',
- 'click .o_survey_session_refresh_ranking': '_onRefreshRankingClick',
+ 'click .o_survey_session_navigation_next, .o_survey_session_start': '_onNext',
+ 'click .o_survey_session_navigation_previous': '_onBack',
+ 'click .o_survey_session_close': '_onEndSessionClick',
},
/**
@@ -27,21 +27,34 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
+ // general survey props
self.surveyId = self.$el.data('surveyId');
self.surveyAccessToken = self.$el.data('surveyAccessToken');
self.isStartScreen = self.$el.data('isStartScreen');
+ self.isLastQuestion = self.$el.data('isLastQuestion');
+ // scoring props
+ self.isScoredQuestion = self.$el.data('isScoredQuestion');
+ self.sessionShowLeaderboard = self.$el.data('sessionShowLeaderboard');
+ self.hasCorrectAnswers = self.$el.data('hasCorrectAnswers');
+ // display props
+ self.attendeesCount = self.$el.data('attendeesCount');
+ self.showBarChart = self.$el.data('showBarChart');
+ self.showTextAnswers = self.$el.data('showTextAnswers');
var isRpcCall = self.$el.data('isRpcCall');
if (!isRpcCall) {
self._startTimer();
+ $(document).on('keydown', self._onKeyDown.bind(self));
}
- if (self.isStartScreen) {
- self._refreshAttendeesCount();
- } else if (!self.refreshingAnswers) {
- self.stopRefreshingAttendees = true;
- self._refreshReceivedAnswers();
- }
+ self._setupIntervals();
+ self._setupCurrentScreen();
+ var setupPromises = [];
+ setupPromises.push(self._setupTextAnswers());
+ setupPromises.push(self._setupChart());
+ setupPromises.push(self._setupLeaderboard());
+
+ return Promise.all(setupPromises);
});
},
@@ -91,6 +104,183 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
},
/**
+ * Listeners for keyboard arrow / spacebar keys.
+ *
+ * - 39 = arrow-right
+ * - 32 = spacebar
+ * - 37 = arrow-left
+ *
+ * @param {KeyboardEvent} ev
+ */
+ _onKeyDown: function (ev) {
+ var keyCode = ev.keyCode;
+
+ if (keyCode === 39 || keyCode === 32) {
+ this._onNext(ev);
+ } else if (keyCode === 37) {
+ this._onBack(ev);
+ }
+ },
+
+ /**
+ * Handles the "next screen" behavior.
+ * It happens when the host uses the keyboard key / button to go to the next screen.
+ * The result depends on the current screen we're on.
+ *
+ * Possible values of the "next screen" to display are:
+ * - 'userInputs' when going from a question to the display of attendees' survey.user_input.line
+ * for that question.
+ * - 'results' when going from the inputs to the actual correct / incorrect answers of that
+ * question. Only used for scored simple / multiple choice questions.
+ * - 'leaderboard' (or 'leaderboardFinal') when going from the correct answers of a question to
+ * the leaderboard of attendees. Only used for scored simple / multiple choice questions.
+ * - If it's not one of the above: we go to the next question, or end the session if we're on
+ * the last question of this session.
+ *
+ * See '_getNextScreen' for a detailed logic.
+ *
+ * @param {Event} ev
+ */
+ _onNext: function (ev) {
+ ev.preventDefault();
+
+ var screenToDisplay = this._getNextScreen();
+
+ if (screenToDisplay === 'userInputs') {
+ this._setShowInputs(true);
+ this.$('.o_survey_session_navigation_previous').removeClass('d-none');
+ } else if (screenToDisplay === 'results') {
+ this._setShowAnswers(true);
+ // when showing results, stop refreshing answers
+ clearInterval(this.resultsRefreshInterval);
+ 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'));
+ this.$('.o_survey_session_navigation_next').addClass('d-none');
+ }
+ this.leaderBoard.showLeaderboard();
+ } else {
+ if (!this.isLastQuestion) {
+ this._nextQuestion();
+ }
+ }
+
+ this.currentScreen = screenToDisplay;
+ },
+
+ /**
+ * Reverse behavior of '_onNext'.
+ *
+ * @param {Event} ev
+ */
+ _onBack: function (ev) {
+ ev.preventDefault();
+
+ var screenToDisplay = this._getPreviousScreen();
+
+ if (screenToDisplay === 'question') {
+ this._setShowInputs(false);
+ this.$('.o_survey_session_navigation_previous').addClass('d-none');
+ } else if (screenToDisplay === 'userInputs') {
+ this._setShowAnswers(false);
+ // resume refreshing answers if necessary
+ if (!this.resultsRefreshInterval) {
+ this.resultsRefreshInterval = setInterval(this._refreshResults.bind(this), 2000);
+ }
+ } else if (screenToDisplay === 'results') {
+ this.leaderBoard.hideLeaderboard();
+ // when showing results, stop refreshing answers
+ clearInterval(this.resultsRefreshInterval);
+ delete this.resultsRefreshInterval;
+ }
+
+ this.currentScreen = screenToDisplay;
+ },
+
+ /**
+ * Marks this session as 'done' and redirects the user to the results based on the clicked link.
+ *
+ * @param {MouseEvent} ev
+ * @private
+ */
+ _onEndSessionClick: function (ev) {
+ var self = this;
+ ev.preventDefault();
+
+ this._rpc({
+ model: 'survey.survey',
+ method: 'action_end_session',
+ args: [[this.surveyId]],
+ }).then(function () {
+ if ($(ev.currentTarget).data('showResults')) {
+ document.location = _.str.sprintf(
+ '/survey/results/%s',
+ self.surveyId
+ );
+ } else {
+ window.history.back();
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Business logic that determines the 'next screen' based on the current screen and the question
+ * configuration.
+ *
+ * Breakdown of use cases:
+ * - If we're on the 'question' screen, and the question is scored, we move to the 'userInputs'
+ * - If we're on the 'question' screen and it's NOT scored, then we move to
+ * - 'results' if the question has correct / incorrect answers
+ * (but not scored, which is kind of a corner case)
+ * - 'nextQuestion' otherwise
+ * - If we're on the 'userInputs' screen and the question has answers, we move to the 'results'
+ * - If we're on the 'results' and the question is scored, we move to the 'leaderboard'
+ * - In all other cases, we show the next question
+ * - (Small exception for the last question: we show the "final leaderboard")
+ *
+ * (For details about which screen shows what, see '_onNext')
+ */
+ _getNextScreen: function () {
+ if (this.currentScreen === 'question' && this.isScoredQuestion) {
+ return 'userInputs';
+ } else if (this.hasCorrectAnswers && ['question', 'userInputs'].includes(this.currentScreen)) {
+ return 'results';
+ } else if (['question', 'userInputs', 'results'].includes(this.currentScreen) &&
+ this.isScoredQuestion) {
+ return 'leaderboard';
+ } else if (this.sessionShowLeaderboard && this.isLastQuestion) {
+ return 'leaderboardFinal';
+ } else {
+ return 'nextQuestion';
+ }
+ },
+
+ /**
+ * Reverse behavior of '_getNextScreen'.
+ *
+ * @param {Event} ev
+ */
+ _getPreviousScreen: function () {
+ if (this.currentScreen === 'userInputs' && this.isScoredQuestion) {
+ return 'question';
+ } else if (this.currentScreen === 'results' ||
+ (this.currentScreen === 'leaderboard' && !this.isScoredQuestion)) {
+ return 'userInputs';
+ } else if (this.currentScreen === 'leaderboard' && this.isScoredQuestion) {
+ return 'results';
+ }
+
+ return this.currentScreen;
+ },
+
+ /**
* We use a fade in/out mechanism to display the next question of the session.
*
* The fade out happens at the same moment as the _rpc to get the new question template.
@@ -102,10 +292,13 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
* @param {MouseEvent} ev
* @private
*/
- _onNextQuestionClick: function (ev) {
+ _nextQuestion: function () {
var self = this;
- ev.preventDefault();
+
this.isStartScreen = false;
+ if (this.surveyTimerWidget) {
+ this.surveyTimerWidget.destroy();
+ }
var resolveFadeOut;
var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
@@ -117,6 +310,12 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
route: _.str.sprintf('/survey/session/next_question/%s', self.surveyAccessToken)
});
+ // avoid refreshing results while transitioning
+ if (this.resultsRefreshInterval) {
+ clearInterval(this.resultsRefreshInterval);
+ delete this.resultsRefreshInterval;
+ }
+
Promise.all([fadeOutPromise, nextQuestionPromise]).then(function (results) {
var $renderedTemplate = $(results[1]);
self.$el.replaceWith($renderedTemplate);
@@ -128,71 +327,13 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
},
/**
- * Marks this session as 'done' and redirects the user to the results.
- *
- * @param {MouseEvent} ev
- * @private
- */
- _onEndSessionClick: function (ev) {
- var self = this;
- ev.preventDefault();
-
- this._rpc({
- model: 'survey.survey',
- method: 'action_end_session',
- args: [[this.surveyId]],
- }).then(function () {
- document.location = _.str.sprintf(
- '/survey/results/%s',
- self.surveyId
- );
- });
- },
-
- _onResultsTabClick: function (ev) {
- this._onRefreshResultsClick(ev, true);
- },
-
- _onRankingTabClick: function (ev) {
- this._onRefreshRankingClick(ev, true);
- },
-
- _onRefreshResultsClick: function (ev, preventSpin) {
- ev.preventDefault();
-
- if (this.$('.o_survey_session_refresh_results').hasClass('fa-spin')) {
- return;
- }
-
- var refreshPromise = this._refreshResults();
- if (!preventSpin) {
- this._spinIcon(this.$('.o_survey_session_refresh_results'), refreshPromise);
- }
- },
-
- _onRefreshRankingClick: function (ev, preventSpin) {
- ev.preventDefault();
-
- if (this.$('.o_survey_session_refresh_ranking').hasClass('fa-spin')) {
- return;
- }
-
- var refreshPromise = this._refreshRanking();
-
- if (!preventSpin) {
- this._spinIcon(this.$('.o_survey_session_refresh_ranking'), refreshPromise);
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
* Will start the question timer so that the host may know when the question is done to display
- * the results and the ranking.
+ * the results and the leaderboard.
+ *
+ * If the question is scored, the timer ending triggers the display of attendees inputs.
*/
_startTimer: function () {
+ var self = this;
var $timer = this.$('.o_survey_timer');
if ($timer.length) {
@@ -203,119 +344,225 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
'timeLimitMinutes': timeLimitMinutes
});
this.surveyTimerWidget.attachTo($timer);
+ this.surveyTimerWidget.on('time_up', this, function () {
+ if (self.currentScreen === 'question' && this.isScoredQuestion) {
+ self.$('.o_survey_session_navigation_next').click();
+ }
+ });
}
},
/**
- * Refreshes the question results on screen.
+ * Refreshes the question results.
+ *
+ * What we get from this call:
+ * - The 'question statistics' used to display the bar chart when appropriate
+ * - The 'user input lines' that are used to display text/date/datetime answers on the screen
+ * - The number of answers, useful for refreshing the progress bar
*/
_refreshResults: function () {
var self = this;
- return this._rpc({
- route: _.str.sprintf('/survey/session/results/%s', this.surveyAccessToken)
- }).then(function (renderedTemplate) {
- var $renderedTemplate = $(renderedTemplate).addClass('py-3');
- self.$('.o_survey_result').empty().append($renderedTemplate);
- return new publicWidget.registry.SurveyResultWidget().attachTo($renderedTemplate);
+ this._rpc({
+ route: _.str.sprintf('/survey/session/results/%s', self.surveyAccessToken)
+ }).then(function (questionResults) {
+ if (questionResults) {
+ if (self.resultsChart && questionResults.question_statistics_graph) {
+ self.resultsChart.updateChart(JSON.parse(questionResults.question_statistics_graph));
+ } else if (self.textAnswers) {
+ self.textAnswers.updateTextAnswers(questionResults.input_line_values);
+ }
+
+ var max = self.attendeesCount > 0 ? self.attendeesCount : 1;
+ var percentage = Math.min(Math.round((questionResults.answer_count / max) * 100), 100);
+ self.$('.progress-bar').css('width', `${percentage}%`);
+
+ if (self.attendeesCount && self.attendeesCount > 0) {
+ var answerCount = Math.min(questionResults.answer_count, self.attendeesCount);
+ self.$('.o_survey_session_answer_count').text(answerCount);
+ self.$('.progress-bar.o_survey_session_progress_small span').text(
+ `${answerCount} / ${self.attendeesCount}`
+ );
+ }
+ }
+ }, function () {
+ // on failure, stop refreshing
+ clearInterval(self.resultsRefreshInterval);
+ delete self.resultsRefreshInterval;
});
},
/**
- * Refreshes the question ranking on screen.
- * We set the width of the progress bars after the rendering to enable a width css animation.
+ * We refresh the attendees count every 2 seconds while the user is on the start screen.
+ *
*/
- _refreshRanking: function () {
+ _refreshAttendeesCount: function () {
var self = this;
- return this._rpc({
- route: _.str.sprintf('/survey/session/ranking/%s', this.surveyAccessToken)
- }).then(function (renderedTemplate) {
- var $renderedTemplate = $(renderedTemplate).addClass('py-3');
- self.$('.o_survey_session_ranking_container').empty().append($renderedTemplate);
- // delay by 200ms to account for fade out / in
- setTimeout(function () {
- self.$('.o_survey_session_ranking_bar').each(function () {
- $(this).css('width', `calc(calc(100% - 10rem) * ${$(this).data('widthRatio')})`);
- });
- }, 200);
-
- return Promise.resolve();
+ self._rpc({
+ model: 'survey.survey',
+ method: 'read',
+ args: [[self.surveyId], ['answer_count']],
+ }).then(function (result) {
+ if (result && result.length === 1){
+ self.$('.o_survey_session_attendees_count').text(
+ result[0].answer_count
+ );
+ }
+ }, function () {
+ // on failure, stop refreshing
+ clearInterval(self.attendeesRefreshInterval);
});
},
/**
- * We refresh the attendees count every 2 seconds while the user is on the start screen.
- * When he leaves the start screen, the "stopRefreshingAttendees" becomes true and we stop
- * querying the answer_count.
+ * For simple/multiple choice questions, we display a bar chart with:
+ *
+ * - answers of attendees
+ * - correct / incorrect answers when relevant
+ *
+ * see SurveySessionChart widget doc for more information.
+ *
*/
- _refreshAttendeesCount: function () {
- var self = this;
+ _setupChart: function () {
+ if (this.resultsChart) {
+ this.resultsChart.setElement(null);
+ this.resultsChart.destroy();
+ delete this.resultsChart;
+ }
- setTimeout(function () {
- self._rpc({
- model: 'survey.survey',
- method: 'read',
- args: [[self.surveyId], ['answer_count']],
- }).then(function (result) {
- if (result && result.length === 1){
- self.$('.o_survey_session_attendees_count').text(
- result[0].answer_count
- );
- }
- if (!self.stopRefreshingAttendees) {
- self._refreshAttendeesCount();
- }
+ if (!this.isStartScreen && this.showBarChart) {
+ this.resultsChart = new SurveySessionChart(this, {
+ attendeesCount: this.attendeesCount,
+ questionType: this.$el.data('questionType'),
+ answersValidity: this.$el.data('answersValidity'),
+ hasCorrectAnswers: this.hasCorrectAnswers,
+ questionStatistics: this.$el.data('questionStatistics'),
+ showInputs: this.showInputs
});
- }, 2000);
+
+ return this.resultsChart.attachTo(this.$('.o_survey_session_chart'));
+ } else {
+ return Promise.resolve();
+ }
},
/**
- * We refresh the received answers count every 2 seconds for the current question.
- * This allows the host to know what he can move on to the next question.
- * (Without having to manually refresh the page).
+ * Leaderboard of all the attendees based on their score.
+ * see SurveySessionLeaderBoard widget doc for more information.
+ *
*/
- _refreshReceivedAnswers: function () {
- var self = this;
+ _setupLeaderboard: function () {
+ if (this.leaderBoard) {
+ this.leaderBoard.setElement(null);
+ this.leaderBoard.destroy();
+ delete this.leaderBoard;
+ }
- this.refreshingAnswers = true;
- setTimeout(function () {
- self._rpc({
- model: 'survey.survey',
- method: 'read',
- args: [[self.surveyId], ['session_question_answer_count']],
- }).then(function (result) {
- if (result && result.length === 1){
- self.$('.o_survey_session_answer_count').text(
- result[0].session_question_answer_count
- );
- }
- self._refreshReceivedAnswers();
+ if (this.isScoredQuestion || this.isLastQuestion) {
+ this.leaderBoard = new SurveySessionLeaderBoard(this, {
+ surveyAccessToken: this.surveyAccessToken,
+ sessionResults: this.$('.o_survey_session_results')
});
- }, 2000);
+
+ return this.leaderBoard.attachTo(this.$('.o_survey_session_leaderboard'));
+ } else {
+ return Promise.resolve();
+ }
},
/**
- * We need the refresh icon to spin for at least 1s to make the user feel like it's
- * refreshing something.
- * If we only wait for the 'refreshPromise', it can be so fast that the icon doesn't even move.
+ * Shows attendees answers for char_box/date and datetime questions.
+ * see SurveySessionTextAnswers widget doc for more information.
*
- * @param {$.Element} $target
- * @param {Promise} refreshPromise
*/
- _spinIcon: function ($target, refreshPromise) {
- $target.addClass('fa-spin');
+ _setupTextAnswers: function () {
+ if (this.textAnswers) {
+ this.textAnswers.setElement(null);
+ this.textAnswers.destroy();
+ delete this.textAnswers;
+ }
- var minimumSpinResolve;
- var minimumSpinPromise = new Promise(function (resolve) { minimumSpinResolve = resolve;});
- setTimeout(function () {
- minimumSpinResolve();
- }, 1000);
+ if (!this.isStartScreen && this.showTextAnswers) {
+ this.textAnswers = new SurveySessionTextAnswers(this, {
+ questionType: this.$el.data('questionType')
+ });
- Promise.all([minimumSpinPromise, refreshPromise]).then(function () {
- $target.removeClass('fa-spin');
- });
+ return this.textAnswers.attachTo(this.$('.o_survey_session_text_answers_container'));
+ } else {
+ return Promise.resolve();
+ }
+ },
+
+ /**
+ * Setup the 2 refresh intervals of 2 seconds for our widget:
+ * - The refresh of attendees count (only on the start screen)
+ * - The refresh of results (used for chart/text answers/progress bar)
+ */
+ _setupIntervals: function () {
+ this.attendeesCount = this.$el.data('attendeesCount');
+
+ if (this.isStartScreen) {
+ this.attendeesRefreshInterval = setInterval(this._refreshAttendeesCount.bind(this), 2000);
+ } else {
+ if (this.attendeesRefreshInterval) {
+ clearInterval(this.attendeesRefreshInterval);
+ }
+
+ if (!this.resultsRefreshInterval) {
+ this.resultsRefreshInterval = setInterval(this._refreshResults.bind(this), 2000);
+ }
+ }
+ },
+
+ /**
+ * Setup current screen based on question properties.
+ * If it's a non-scored question with a chart, we directly display the user inputs.
+ */
+ _setupCurrentScreen: function () {
+ if (this.isStartScreen) {
+ this.currentScreen = 'startScreen';
+ } else if (!this.isScoredQuestion && this.showBarChart) {
+ this.currentScreen = 'userInputs';
+ } else {
+ this.currentScreen = 'question';
+ }
+
+ this._setShowInputs(this.currentScreen === 'userInputs');
},
+
+ /**
+ * When we go from the 'question' screen to the 'userInputs' screen, we toggle this boolean
+ * and send the information to the chart.
+ * The chart will show attendees survey.user_input.lines.
+ *
+ * @param {Boolean} showInputs
+ */
+ _setShowInputs(showInputs) {
+ this.showInputs = showInputs;
+
+ if (this.resultsChart) {
+ this.resultsChart.setShowInputs(showInputs);
+ this.resultsChart.updateChart();
+ }
+ },
+
+ /**
+ * When we go from the 'userInputs' screen to the 'results' screen, we toggle this boolean
+ * and send the information to the chart.
+ * The chart will show the question survey.question.answers.
+ * (Only used for simple / multiple choice questions).
+ *
+ * @param {Boolean} showAnswers
+ */
+ _setShowAnswers(showAnswers) {
+ this.showAnswers = showAnswers;
+
+ if (this.resultsChart) {
+ this.resultsChart.setShowAnswers(showAnswers);
+ this.resultsChart.updateChart();
+ }
+ }
});
return publicWidget.registry.SurveySessionManage;
diff --git a/addons/survey/static/src/js/survey_session_text_answers.js b/addons/survey/static/src/js/survey_session_text_answers.js
new file mode 100644
index 00000000000..4e669c17ea0
--- /dev/null
+++ b/addons/survey/static/src/js/survey_session_text_answers.js
@@ -0,0 +1,74 @@
+odoo.define('survey.session_text_answers', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+var core = require('web.core');
+var time = require('web.time');
+var SESSION_CHART_COLORS = require('survey.session_colors');
+
+var QWeb = core.qweb;
+
+publicWidget.registry.SurveySessionTextAnswers = publicWidget.Widget.extend({
+ xmlDependencies: ['/survey/static/src/xml/survey_session_text_answer_template.xml'],
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ this.answerIds = [];
+ this.questionType = options.questionType;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Adds the attendees answers on the screen.
+ * This is used for char_box/date and datetime questions.
+ *
+ * We use some tricks with jQuery for wow effect:
+ * - force a width on the external div container, to reserve space for that answer
+ * - set the actual width of the answer, and enable a css width animation
+ * - set the opacity to 1, and enable a css opacity animation
+ *
+ * @param {Array} inputLineValues array of survey.user_input.line records in the form
+ * {id: line.id, value: line.[value_char_box/value_date/value_datetime]}
+ */
+ updateTextAnswers: function (inputLineValues) {
+ var self = this;
+
+ inputLineValues.forEach(function (inputLineValue) {
+ if (!self.answerIds.includes(inputLineValue.id) && inputLineValue.value) {
+ var textValue = inputLineValue.value;
+ if (self.questionType === 'char_box') {
+ textValue = textValue.length > 25 ?
+ textValue.substring(0, 22) + '...' :
+ textValue;
+ } else if (self.questionType === 'date') {
+ textValue = moment(textValue).format(time.getLangDateFormat());
+ } else if (self.questionType === 'datetime') {
+ textValue = moment(textValue).format(time.getLangDatetimeFormat());
+ }
+
+ var $textAnswer = $(QWeb.render('survey.survey_session_text_answer', {
+ value: textValue,
+ borderColor: `rgb(${SESSION_CHART_COLORS[self.answerIds.length % 10]})`
+ }));
+ self.$el.append($textAnswer);
+ var spanWidth = $textAnswer.find('span').width();
+ var calculatedWidth = `calc(${spanWidth}px + 1.2rem)`;
+ $textAnswer.css('width', calculatedWidth);
+ setTimeout(function () {
+ // setTimeout to force jQuery rendering
+ $textAnswer.find('.o_survey_session_text_answer_container')
+ .css('width', calculatedWidth)
+ .css('opacity', '1');
+ }, 1);
+ self.answerIds.push(inputLineValue.id);
+ }
+ });
+ },
+});
+
+return publicWidget.registry.SurveySessionTextAnswers;
+
+});
diff --git a/addons/survey/static/src/scss/survey_form.scss b/addons/survey/static/src/scss/survey_form.scss
index ef026c664d2..aaa62d7f38f 100644
--- a/addons/survey/static/src/scss/survey_form.scss
+++ b/addons/survey/static/src/scss/survey_form.scss
@@ -18,7 +18,7 @@ nav#oe_main_menu_navbar {
min-height: 100%;
}
-.o_survey_form, .o_survey_print, .o_survey_session_manage, .o_survey_quick_access {
+.o_survey_form, .o_survey_print, .o_survey_quick_access {
.o_survey_question_error {
height: 0px;
transition: height .5s ease;
@@ -109,12 +109,32 @@ nav#oe_main_menu_navbar {
}
}
-.o_survey_form, .o_survey_session_manage {
+/**********************************************************
+ Form Specific Style
+ **********************************************************/
+
+.o_survey_form {
min-height: 100%;
+ .o_survey_progress {
+ height:0.5em;
+ }
+
.o_survey_choice_btn {
+ cursor: pointer;
background-color: rgba($primary, 0.5);
border: 2px solid $primary;
+
+ &.o_survey_selected {
+ background-color: $primary;
+ }
+
+ &:hover {
+ background-color: rgba($primary, 0.8);
+ .o_survey_choice_key span.o_survey_key {
+ opacity: 1;
+ }
+ }
}
.o_survey_choice_key {
@@ -125,6 +145,13 @@ nav#oe_main_menu_navbar {
font-size: smaller;
top: -2px;
&.o_survey_key {
+ width: inherit;
+ right: 10px;
+ border: 2px solid $primary;
+ border-right: 0px;
+ height: 25px;
+ transition: opacity 0.4s ease;
+ white-space: nowrap;
opacity: 0;
}
}
@@ -136,155 +163,141 @@ nav#oe_main_menu_navbar {
}
td {
background-color: rgba($primary, 0.2);
+ &:hover {
+ background-color: rgba($primary, 0.5);
+ cursor: pointer;
+ .o_survey_choice_key span.o_survey_key {
+ opacity: 1;
+ }
+ }
}
}
-
- .o_survey_progress {
- height:0.5em;
- }
}
/**********************************************************
- Form Specific Style
+ Survey Session Specific Style
**********************************************************/
-.o_survey_form {
- .o_survey_choice_btn {
- cursor: pointer;
-
- &.o_survey_selected {
- background-color: $primary;
- }
-
- &:hover {
- background-color: rgba($primary, 0.8);
- .o_survey_choice_key span.o_survey_key {
- opacity: 1;
- }
- }
+.o_survey_session_manage {
+ h1 {
+ font-size: 4rem;
}
- .o_survey_choice_key span.o_survey_key {
- width: inherit;
- right: 10px;
- border: 2px solid $primary;
- border-right: 0px;
- height: 25px;
- transition: opacity 0.4s ease;
- white-space: nowrap;
+ h2 {
+ font-size: 3.2rem;
}
- .o_survey_question_matrix td:hover {
- background-color: rgba($primary, 0.5);
+ .o_survey_session_navigation {
+ position: fixed;
+ padding: 1rem;
+ top: calc(50% - 0.5rem);
cursor: pointer;
- .o_survey_choice_key span.o_survey_key {
- opacity: 1;
+
+ &.o_survey_session_navigation_next {
+ right: 2rem;
+ }
+
+ &.o_survey_session_navigation_previous {
+ left: 2rem;
}
}
-}
-/**********************************************************
- Survey Session Specific Style
- **********************************************************/
+ .o_survey_session_link_reminder {
+ position: absolute;
+ right: 2rem;
+ top: 2rem;
+ }
-.o_survey_session_manage {
.o_survey_manage_fontsize_14 {
font-size: 1.4rem;
}
.o_survey_session_manage_container {
- border: solid 1px #999999;
- box-shadow: 0px 1px 1px 0px #999999;
- min-height: 75vh;
+ height: 100vh;
+ width: 85vw;
.js_question-wrapper {
padding-bottom: 0px;
}
- }
-
- .tab-content > .o_survey_session_manage_question.tab-pane {
- display: none;
- }
-
- .tab-content > .o_survey_session_manage_question.active {
- display: flex;
- }
- .o_survey_session_copy {
- cursor: pointer;
- opacity: .75;
- transition: opacity .3s ease;
- &:hover {
- opacity: 1;
+ &.pt-6 {
+ padding-top: 5rem !important;
}
- }
- .btn {
- font-variant: small-caps;
- &.o_survey_session_end {
- right: 0px;
- }
- }
+ .o_survey_session_results {
+ display: flex; // here and not d-flex because we need to be able to fade-out
- .o_survey_session_results, .o_survey_session_ranking {
- .fa {
- font-size: .9rem;
- opacity: .75;
- &:hover {
- transition: opacity .4s ease;
- opacity: 1;
- cursor: pointer;
+ .mb-6 {
+ margin-bottom: 6rem;
}
- }
- .o_survey_result, .o_survey_session_ranking_container {
- overflow: hidden;
- }
+ .progress {
+ height: 2rem;
+ border-radius: 0.6rem;
+ font-size: 1.2rem;
+ background-color: #cfcfcf;
+ .progress-bar {
+ width: 0%;
+ transition: width 1s ease;
+ }
+ }
- .survey_filter {
- display: none;
+ .o_survey_session_text_answer {
+ .o_survey_session_text_answer_container {
+ border: solid 1.6px;
+ border-radius: 0.6rem;
+ font-size: 1.4rem;
+ width: 2rem;
+ opacity: .1;
+ transition: width .4s ease, opacity .4s ease;
+ overflow: hidden;
+ }
+
+ span {
+ white-space: nowrap;
+ }
+ }
}
- }
- .o_survey_result {
- .nav-tabs .nav-link.active {
- background-color: transparent;
- border-color: #dee2e6 #dee2e6 transparent;
+ .o_survey_session_leaderboard {
+ display: flex; // here and not d-flex because we need to be able to fade-out
+ .o_survey_leaderboard_buttons {
+ line-height: 4rem;
+ font-variant: small-caps;
+ }
}
}
- .nav-tabs {
- border-bottom: 0px;
- .nav-item .nav-link {
- border-radius: 0px;
- background-color: transparent;
- border: solid 1px transparent;
-
- &.active {
- border: solid 1px #999999;
- border-bottom: 0px;
- }
+ .o_survey_session_copy {
+ cursor: pointer;
+ opacity: .75;
+ transition: opacity .3s ease;
+ &:hover {
+ opacity: 1;
}
}
}
-.o_survey_session_ranking {
- .o_survey_session_ranking_item {
- line-height: 1.4rem;
+.o_survey_session_leaderboard {
+ font-size: 1.4rem;
+
+ .o_survey_session_leaderboard_item {
+ line-height: 1.8rem;
- .o_survey_session_ranking_index {
- width: 1.5rem;
+ .o_survey_session_leaderboard_index {
+ width: 9.5rem;
+ padding-top: .2rem;
}
- .o_survey_session_ranking_bar {
+ .o_survey_session_leaderboard_bar {
min-width: 90px;
- height: 1.4rem;
- color: white;
+ height: 2.2rem;
background-color: #007A77;
transition: width ease 1.5s;
}
- .o_survey_session_ranking_name {
+ .o_survey_session_leaderboard_name {
+ padding-top: .2rem;
width: 7.5rem;
overflow: hidden;
text-overflow: ellipsis;
diff --git a/addons/survey/static/src/xml/survey_session_text_answer_template.xml b/addons/survey/static/src/xml/survey_session_text_answer_template.xml
new file mode 100644
index 00000000000..d37e0864f73
--- /dev/null
+++ b/addons/survey/static/src/xml/survey_session_text_answer_template.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+<div t-name="survey.survey_session_text_answer" class="o_survey_session_text_answer d-inline-block m-1">
+ <div class="o_survey_session_text_answer_container d-inline-block p-2 font-weight-bold"
+ t-attf-style="border-color: #{borderColor}">
+ <span class="d-inline-block" t-esc="value" />
+ </div>
+</div>
+
+</templates>
diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml
index 854274013ba..c13669a5e4f 100644
--- a/addons/survey/views/assets.xml
+++ b/addons/survey/views/assets.xml
@@ -24,6 +24,11 @@
<!-- survey user input session assets -->
<template id="survey_user_input_session_assets" name="Survey User Input Session Assets">
+ <script type="text/javascript" src="/survey/static/src/js/libs/chartjs-plugin-datalabels.min.js" />
+ <script type="text/javascript" src="/survey/static/src/js/survey_session_colors.js" />
+ <script type="text/javascript" src="/survey/static/src/js/survey_session_chart.js" />
+ <script type="text/javascript" src="/survey/static/src/js/survey_session_text_answers.js" />
+ <script type="text/javascript" src="/survey/static/src/js/survey_session_leaderboard.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_session_manage.js" />
</template>
diff --git a/addons/survey/views/survey_templates_statistics.xml b/addons/survey/views/survey_templates_statistics.xml
index 266917786cf..d4492788614 100644
--- a/addons/survey/views/survey_templates_statistics.xml
+++ b/addons/survey/views/survey_templates_statistics.xml
@@ -27,9 +27,9 @@
</template>
<template id="survey_page_statistics_inner" name="Survey: result statistics content">
- <div t-if="survey.session_show_ranking" class="o_survey_session_ranking mb-5 mt-1">
- <h2 class="mt16 text-uppercase text-muted">Ranking</h2>
- <t t-call="survey.user_input_session_ranking" />
+ <div t-if="survey.session_show_leaderboard" class="o_survey_session_leaderboard mb-5 mt-1">
+ <h2 class="mt16 text-uppercase text-muted">Leaderboard</h2>
+ <t t-call="survey.user_input_session_leaderboard" />
</div>
<div t-if="question_and_page_data" class="card d-print-none">
<div class="card-header"><span class="fa fa-filter"></span> Filters <span t-if="search_filters" class="float-right text-primary clear_survey_filter"><i class="fa fa-times"></i> Clear All Filters</span></div>
diff --git a/addons/survey/views/survey_templates_user_input_session.xml b/addons/survey/views/survey_templates_user_input_session.xml
index 08166d6c815..3c5433f8c1a 100644
--- a/addons/survey/views/survey_templates_user_input_session.xml
+++ b/addons/survey/views/survey_templates_user_input_session.xml
@@ -2,12 +2,16 @@
<odoo>
<data>
<template id="user_input_session" name="Survey User Input Session" inherit_id="web.frontend_layout" primary="True">
+ <xpath expr="//div[@id='wrapwrap']" position="before">
+ <!--TODO DBE Fix me : If one day, there is a survey_livechat bridge module, put this in that module-->
+ <t t-set="no_livechat" t-value="True"/>
+ </xpath>
<xpath expr="//div[@id='wrapwrap']" position="attributes">
<attribute name="t-att-style" add="('height: 100%; overflow: auto; background: url(' + '/web/image/survey.survey/%s/background_image' % survey.id + ') no-repeat fixed center; box-shadow: inset 0 0 0 10000px rgba(255,255,255,.7); background-size: cover;') if survey and survey.background_image else 'height: 100%; overflow: auto;'"/>
</xpath>
<xpath expr="//head/t[@t-call-assets][last()]" position="after">
- <t t-call-assets="survey.survey_user_input_session_assets" lazy_load="True"/>
<t t-call-assets="survey.survey_assets" lazy_load="True"/>
+ <t t-call-assets="survey.survey_user_input_session_assets" lazy_load="True"/>
</xpath>
<xpath expr="//header" position="before">
<t t-set="no_header" t-value="True"/>
@@ -20,32 +24,24 @@
<template id="user_input_session_open" name="Survey: Open Session">
<t t-call="survey.user_input_session">
- <div t-att-data-survey-access-token="survey.access_token"
+ <div class="wrap py-3 min-vh-100 align-items-center justify-content-center d-flex o_survey_session_open o_survey_session_manage"
+ t-att-data-survey-access-token="survey.access_token"
t-att-data-survey-id="survey.id"
- t-att-data-is-start-screen="True"
- class="wrap py-3 min-vh-100 align-items-center justify-content-center d-flex o_survey_session_open o_survey_session_manage">
- <div class="w-75 p-4 o_survey_session_manage_container">
- <div class="mb-5 text-center">
- <h1 t-field="survey.title" />
- <div t-field="survey.description" />
- </div>
- <div class="row">
- <div class="col-lg-12 text-center mb-2" t-if="survey.access_mode == 'public'">
- <h1>
- <a t-att-href="survey_url" t-esc="survey_url" target="_blank" />
- <i class="fa fa-copy font-weight-normal ml-3 o_survey_session_copy" />
- </h1>
+ t-att-data-is-start-screen="True">
+ <div class="w-75 p-4">
+ <div class="text-center">
+ <h1 class="mb-4" t-field="survey.title" />
+ <h2 class="mb-5 font-weight-normal">
+ <span>Go to <a t-att-href="survey_url" t-esc="survey_url" target="_blank" /></span>
+ <i class="fa fa-copy font-weight-normal ml-3 o_survey_session_copy" />
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey_url" />
- </div>
- <div class="col-lg-12 text-center ml-3 font-weight-light o_survey_manage_fontsize_14">
- Participants <span class="ml-1 font-weight-bold o_survey_session_attendees_count" t-esc="survey.answer_count" />
- </div>
- </div>
- <div class="text-center mt-5">
- <a role="button" class="btn btn-primary font-weight-bold o_survey_session_start" href="#">
- Start Session
- </a>
+ </h2>
+ <h2 class="font-weight-normal"><span>Waiting for attendees...</span>
+ <span class="ml-1 font-weight-bold o_survey_session_attendees_count" t-esc="survey.answer_count" />
+ </h2>
</div>
+ <a role="button"
+ class="font-weight-bold fa fa-chevron-right o_survey_session_navigation o_survey_session_navigation_next p-3" />
</div>
</div>
</t>
@@ -58,142 +54,112 @@
</template>
<template id="user_input_session_manage_content" name="Survey User Input Session Manage">
- <div class="wrap py-3 pt-4 min-vh-100 align-items-center d-flex flex-column o_survey_session_manage"
+ <t t-set="question" t-value="survey.session_question_id" />
+ <t t-set="is_scored_question" t-value="any(answer.answer_score for answer in question.suggested_answer_ids)" />
+ <t t-set="show_bar_chart" t-value="question.question_type in ['simple_choice', 'multiple_choice']" />
+ <t t-set="show_text_answers" t-value="question.question_type in ['char_box', 'date', 'datetime'] and not question.save_as_email and not question.save_as_nickname" />
+ <div class="wrap min-vh-100 align-items-center justify-content-center d-flex flex-column o_survey_session_manage"
t-att-style="'display: none;' if is_rpc_call else ''"
t-att-data-is-rpc-call="is_rpc_call"
t-att-data-survey-id="survey.id"
+ t-att-data-attendees-count="survey.answer_count"
t-att-data-survey-access-token="survey.access_token"
t-att-data-timer="survey.session_question_start_time.isoformat()"
- t-att-data-time-limit-minutes="survey.session_question_id.time_limit / 60">
- <nav class="w-75 navbar navbar-expand-lg text-left font-weight-bold pl-0 pb-0">
- <div class="collapse navbar-collapse">
- <ul class="nav nav-tabs mr-auto" role="tablist">
- <li class="nav-item">
- <a data-toggle="tab" role="tab" class="nav-link active py-2 px-3"
- aria-controls="#o_survey_session_manage_question" href="#o_survey_session_manage_question">
- Question
- </a>
- </li>
- <li class="nav-item">
- <a data-toggle="tab" role="tab" class="nav-link py-2 px-3 o_survey_session_results_href"
- aria-controls="#o_survey_session_results" href="#o_survey_session_results">
- Results
- </a>
- </li>
- <li t-if="survey.session_show_ranking" class="nav-item">
- <a data-toggle="tab" role="tab" class="nav-link py-2 px-3 o_survey_session_ranking_href"
- aria-controls="#o_survey_session_ranking" href="#o_survey_session_ranking">
- Ranking
- </a>
- </li>
- </ul>
- </div>
- </nav>
- <div class="tab-content w-75">
- <div role="tabpanel" id="o_survey_session_manage_question"
- class="p-4 px-5 flex-column tab-pane fade show active o_survey_session_manage_question o_survey_session_manage_container">
- <t t-if="survey.access_mode == 'public'">
- <h4 class="text-right">
- <a t-att-href="survey_url" t-esc="survey_url" target="_blank" />
- <i class="fa fa-copy font-weight-normal ml-3 mr-1 o_survey_session_copy" />
- </h4>
+ t-att-data-time-limit-minutes="question.time_limit / 60"
+ t-att-data-is-scored-question="is_scored_question"
+ t-att-data-session-show-leaderboard="survey.session_show_leaderboard"
+ t-att-data-question-statistics="question_statistics_graph"
+ t-att-data-question-type="question.question_type"
+ t-att-data-has-correct-answers="any(answer.is_correct for answer in question.suggested_answer_ids)"
+ t-att-data-answers-validity="answers_validity"
+ t-att-data-is-last-question="is_last_question"
+ t-att-data-current-screen="'question' if is_scored_question else 'userInputs'"
+ t-att-data-show-bar-chart="show_bar_chart"
+ t-att-data-show-text-answers="show_text_answers">
+ <div class="p-4 d-flex flex-column o_survey_session_manage_container min-w-75 pt-6">
+ <h3 class="o_survey_session_link_reminder">
+ <span>Go to <a t-att-href="survey_url" t-esc="survey_url" target="_blank" /></span>
+ <i class="fa fa-copy font-weight-normal ml-3 mr-1 o_survey_session_copy" />
<input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey_url" />
- </t>
- <div class="d-flex flex-column justify-content-center flex-grow-1">
- <div>
- <fieldset disabled="disabled">
- <div class="row mb-4">
- <div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
- <h1 t-if="survey.session_question_id.is_time_limited" class="o_survey_timer_container col-lg-4 text-right">
- <span class="o_survey_timer"/>
- </h1>
- </div>
- <t t-set="question" t-value="survey.session_question_id" />
- <t t-set="answer" t-value="env['survey.user_input']" />
- <t t-set="answer_lines" t-value="env['survey.user_input.line']" />
- <t t-set="survey_form_readonly" t-value="True"/>
- <t t-call="survey.question_container" />
- </fieldset>
- <div class="mb-5">
- <div class="d-inline-block font-weight-bold">
- <t t-call="survey.survey_progression">
- <t t-set="page_ids" t-value="survey.question_ids.ids"/>
- <t t-set="page_number" t-value="page_ids.index(survey.session_question_id.id)"/>
- </t>
- </div>
- <div class="float-right d-inline-block font-weight-bold my-2">
- Received answers:
- <span class="o_survey_session_answer_count" t-esc="survey.session_question_answer_count" />
- /
- <t t-esc="survey.answer_count" />
+ </h3>
+ <a role="button"
+ class="font-weight-bold fa fa-chevron-right o_survey_session_navigation o_survey_session_navigation_next p-3" />
+ <a role="button"
+ class="font-weight-bold fa fa-chevron-left o_survey_session_navigation o_survey_session_navigation_previous p-3 d-none" />
+ <div class="o_survey_session_results flex-column flex-grow-1">
+ <div class="row">
+ <div class="col-lg-8"><h1 t-esc="question.title"></h1></div>
+ <div class="col-lg-4 text-right d-flex flex-column justify-content-center">
+ <h1 t-if="question.is_time_limited" class="o_survey_timer_container">
+ <span class="o_survey_timer"/>
+ </h1>
+ <div t-if="show_bar_chart or show_text_answers">
+ <div class="progress" title="Attendees are answering the question...">
+ <div class="progress-bar o_survey_session_progress_small font-weight-bold"
+ role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
+ <span class="px-2">0 / <t t-esc="survey.session_answer_count" /></span>
+ </div>
+ </div>
</div>
</div>
- <div class="text-center position-relative">
- <a t-if="not is_last_question" role="button" class="btn btn-primary font-weight-bold o_survey_session_next" href="#">
- Next Question
- </a>
- <a role="button" href="#"
- t-attf-class="btn font-weight-bold o_survey_session_end #{'position-absolute btn-secondary' if not is_last_question else 'btn-primary'}">
- End Session
- </a>
- </div>
</div>
- </div>
- </div>
- <div role="tabpanel" id="o_survey_session_results"
- class="p-4 px-5 tab-pane fade o_survey_session_manage_container o_survey_session_results">
- <div class="row m-0 pt-2 d-flex align-items-center">
- <div class="col-lg-10 m-0 p-0">
- <h3 class="m-0">Results</h3>
- </div>
- <div class="col-lg-2 m-0 p-0 text-right">
- <h5 class="m-0">
- <i class="fa fa-refresh o_survey_session_refresh_results mr-2" />
- </h5>
+ <div t-attf-class="d-flex flex-column flex-grow-1 #{'justify-content-center' if not show_text_answers else ''} #{'align-items-center' if show_bar_chart else ''}">
+ <!-- Has to stay in 'style' attribute for Chartjs -->
+ <div t-if="show_bar_chart" class="p-2 o_survey_session_chart"
+ style="position: relative; width: 75vw; height: 70vh;">
+ <!-- canvas element for drawing bar chart -->
+ <canvas />
+ </div>
+ <div t-elif="show_text_answers" class="p-2 pt-4 o_survey_session_text_answers_container">
+ </div>
+ <div t-else="" class="mb-6">
+ <h2 class="font-weight-normal mb-3">
+ <span>Waiting for attendees...</span>
+ <span>
+ <span class="o_survey_session_answer_count">0</span>
+ /
+ <span t-esc="survey.answer_count" />
+ </span>
+ </h2>
+ <div class="progress">
+ <div class="progress-bar font-weight-bold" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
+ </div>
+ </div>
</div>
</div>
- <div class="o_survey_result">
- </div>
- </div>
- <div role="tabpanel" id="o_survey_session_ranking"
- t-if="survey.session_show_ranking"
- class="p-4 px-5 tab-pane fade o_survey_session_manage_container o_survey_session_ranking">
- <div class="row m-0 pt-2 d-flex align-items-center">
- <div class="col-lg-10 m-0 p-0">
- <h3 class="m-0">Ranking</h3>
- </div>
- <div class="col-lg-2 m-0 p-0 text-right">
- <h5 class="m-0">
- <i class="fa fa-refresh o_survey_session_refresh_ranking mr-2" />
- </h5>
+ <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>
+ <div t-if="is_last_question" class="o_survey_leaderboard_buttons font-weight-bold">
+ <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>
+ </div>
</div>
+ <div class="justify-content-center d-flex flex-column flex-grow-1 mb-5 o_survey_session_leaderboard_container" />
</div>
- <div class="o_survey_session_ranking_container">
- </div>
- </div>
</div>
</div>
</template>
- <template id="user_input_session_ranking" name="Survey User Input Ranking">
- <div t-if="ranking">
- <t t-set="max_score" t-value="ranking[0].scoring_total or 1" />
- <t t-foreach="ranking" t-as="ranking_item">
- <div class="o_survey_session_ranking_item mb-1">
- <div t-esc="'%d.' % (ranking_item_index + 1)" class="o_survey_session_ranking_index d-inline-block font-weight-bold"></div>
+ <template id="user_input_session_leaderboard" name="Survey User Input Leaderboard">
+ <div t-if="leaderboard">
+ <t t-set="max_score" t-value="leaderboard[0].scoring_total or 1" />
+ <t t-foreach="leaderboard" t-as="leaderboard_item">
+ <div class="o_survey_session_leaderboard_item mb-1">
+ <div class="o_survey_session_leaderboard_index d-inline-block font-weight-bold align-top">
+ <div class="d-inline-block" t-esc="'%d.' % (leaderboard_item_index + 1)" />
+ <div class="d-inline-block float-right mr-2" t-esc="'%.0f p' % leaderboard_item.scoring_total" />
+ </div>
<!-- We keep "10rem" of space to display the points.
Then, the length of the bar is a percentage of the attendee's score compared to the max_score. -->
- <t t-set="width_ratio" t-value="round(ranking_item.scoring_total / max_score, 3)"/>
- <div class="o_survey_session_ranking_bar mr-1 pr-2 align-top d-inline-block text-right font-weight-bold mb-1"
- t-esc="ranking_item.nickname"
- t-att-style="'width: %s' % ('0%' if animate_width else ('calc(calc(%s - 10rem) * %s' % ('100%', width_ratio)))"
+ <t t-set="width_ratio" t-value="round(leaderboard_item.scoring_total / max_score, 3)"/>
+ <div class="o_survey_session_leaderboard_bar mr-1 pr-2 align-top d-inline-block text-right font-weight-bold"
+ t-att-style="'width: %s' % ('0%' if animate_width else ('calc(calc(%s - 18rem) * %s' % ('100%', width_ratio)))"
t-att-data-width-ratio="width_ratio">
- <span t-if="ranking_item.nickname" t-esc="ranking_item.nickname" />
- <span t-else="">Anonymous</span>
</div>
- <div class="o_survey_session_ranking_name font-weight-bold align-top d-inline-block">
- <span t-esc="'%.0f ' % ranking_item.scoring_total" />
- <span>points</span>
+ <div class="o_survey_session_leaderboard_name d-inline-block font-weight-bold">
+ <span t-if="leaderboard_item.nickname" t-esc="leaderboard_item.nickname" />
+ <span t-else="">Anonymous</span>
</div>
</div>
</t>