[IMP] survey: add background option per section

This commit adds the background per section feature.
If the section to display has a background image configured, the background
will be refreshed using that section background image.
If the section has no background, the survey background is used.
The background is refreshed at the same time that the next question is loaded.

To ease technical maintenance and to keep it simple, the background is always
faded out/in at each page change only if there are some sections on the
survey that has a specific background image. If no background image are set on
the survey sections, the background will never be faded out.

Note: The next section to display depends on free text (section with
description) configuration and on conditional questions.
The next section to display can be the next question's section or directly
the next question itself (if the question is a section).

This section's background image implementation works both for the regular
survey form as well as the "live session" mode of the survey module.

Task-2225393

closes odoo/odoo#79156

Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
David Beguin
2021-11-30 11:20:45 +00:00
committed by Aurélien Warnon
parent d5ea0bf26b
commit fd75481d52
15 changed files with 279 additions and 66 deletions
+1
View File
@@ -68,6 +68,7 @@ sent mails with personal token for the invitation of the survey.
'survey/static/src/js/survey_timer.js',
'survey/static/src/js/survey_breadcrumb.js',
'survey/static/src/js/survey_form.js',
'survey/static/src/js/survey_preload_image_mixin.js',
'survey/static/src/js/survey_print.js',
'survey/static/src/js/survey_result.js',
('include', 'web._assets_helpers'),
+32 -10
View File
@@ -319,10 +319,11 @@ class Survey(http.Controller):
def _prepare_question_html(self, survey_sudo, answer_sudo, **post):
""" Survey page navigation is done in AJAX. This function prepare the 'next page' to display in html
and send back this html to the survey_form widget that will inject it into the page."""
and send back this html to the survey_form widget that will inject it into the page.
Background url must be given to the caller in order to process its refresh as we don't have the next question
object at frontend side."""
survey_data = self._prepare_survey_data(survey_sudo, answer_sudo, **post)
survey_content = False
if answer_sudo.state == 'done':
survey_content = request.env.ref('survey.survey_fill_form_done')._render(survey_data)
else:
@@ -345,10 +346,17 @@ class Survey(http.Controller):
'page_number': page_ids.index(survey_data['question'].id)
})
background_image_url = survey_sudo.background_image_url
if 'question' in survey_data:
background_image_url = survey_data['question'].background_image_url
elif 'page' in survey_data:
background_image_url = survey_data['page'].background_image_url
return {
'survey_content': survey_content,
'survey_progress': survey_progress,
'survey_navigation': request.env.ref('survey.survey_navigation')._render(survey_data),
'background_image_url': background_image_url
}
@http.route('/survey/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
@@ -364,18 +372,32 @@ class Survey(http.Controller):
return request.render('survey.survey_page_fill',
self._prepare_survey_data(access_data['survey_sudo'], answer_sudo, **post))
@http.route('/survey/get_background_image/<string:survey_token>/<string:answer_token>', type='http', auth="public", website=True, sitemap=False)
def survey_get_background(self, survey_token, answer_token):
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
if access_data['validity_code'] is not True:
return werkzeug.exceptions.Forbidden()
# --------------------------------------------------------------------------
# ROUTES to handle question images + survey background transitions + Tool
# --------------------------------------------------------------------------
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
@http.route('/survey/<string:survey_token>/get_background_image',
type='http', auth="public", website=True, sitemap=False)
def survey_get_background(self, survey_token):
survey_sudo, dummy = self._fetch_from_access_token(survey_token, False)
return self._get_background_image(survey_sudo._name, survey_sudo.id)
@http.route('/survey/<string:survey_token>/<int:section_id>/get_background_image',
type='http', auth="public", website=True, sitemap=False)
def survey_section_get_background(self, survey_token, section_id):
survey_sudo, dummy = self._fetch_from_access_token(survey_token, False)
section = survey_sudo.page_ids.filtered(lambda q: q.id == section_id)
if not section:
# trying to access a question that is not in this survey
raise werkzeug.exceptions.Forbidden()
return self._get_background_image(section._name, section.id)
def _get_background_image(self, model_name, res_id):
status, headers, image_base64 = request.env['ir.http'].sudo().binary_content(
model='survey.survey', id=survey_sudo.id, field='background_image',
model=model_name, id=res_id, field='background_image',
default_mimetype='image/png')
return request.env['ir.http']._content_image_get_response(status, headers, image_base64)
@http.route('/survey/get_question_image/<string:survey_token>/<string:answer_token>/<int:question_id>/<int:suggested_answer_id>', type='http', auth="public", website=True, sitemap=False)
@@ -87,13 +87,17 @@ class UserInputSession(http.Controller):
Writing the next question on the survey is sudo'ed to avoid potential access right issues.
e.g: a survey user can create a live session from any survey but he can only write
on its own survey. """
on its own survey.
In addition to return a pre-rendered html template with the next question, we also return the background
to display. Background image depends on the next question to display and cannot be extracted from the
html rendered question template. The background needs to be changed at frontend side on a specific selector."""
survey = self._fetch_from_token(survey_token)
if not survey or not survey.session_state:
# no open session
return ''
return {}
if survey.session_state == 'ready':
survey._session_open()
@@ -107,13 +111,19 @@ class UserInputSession(http.Controller):
'session_question_id': next_question.id,
'session_question_start_time': fields.Datetime.now() + relativedelta(seconds=1)
})
request.env['bus.bus']._sendone(survey.access_token, 'next_question', {'question_start': now.timestamp()})
request.env['bus.bus']._sendone(survey.access_token, 'next_question', {
'question_start': now.timestamp()
})
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)
return {
'background_image_url': survey.session_question_id.background_image_url,
'question_html': request.env.ref('survey.user_input_session_manage_content')._render(template_values)
}
else:
return False
return {}
@http.route('/survey/session/results/<string:survey_token>', type='json', auth='user', website=True)
def survey_session_results(self, survey_token, **kwargs):
+30
View File
@@ -58,6 +58,8 @@ class SurveyQuestion(models.Model):
'Description', translate=True, sanitize=False, # TDE TODO: sanitize but find a way to keep youtube iframe media stuff
help="Use this field to add additional explanations about your question or to illustrate it with pictures or a video")
question_placeholder = fields.Char("Placeholder", translate=True, compute="_compute_question_placeholder", store=True, readonly=False)
background_image = fields.Image("Background Image", compute="_compute_background_image", store=True, readonly=False)
background_image_url = fields.Char("Background Url", compute="_compute_background_image_url")
survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade')
scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True)
sequence = fields.Integer('Sequence', default=10)
@@ -179,6 +181,34 @@ class SurveyQuestion(models.Model):
or not question.question_placeholder: # avoid CacheMiss errors
question.question_placeholder = False
@api.depends('is_page')
def _compute_background_image(self):
""" Background image is only available on sections. """
for question in self.filtered(lambda q: not q.is_page):
question.background_image = False
@api.depends('survey_id.access_token', 'background_image', 'page_id', 'survey_id.background_image_url')
def _compute_background_image_url(self):
""" How the background url is computed:
- For a question: it depends on the related section (see below)
- For a section:
- if a section has a background, then we create the background URL using this section's ID
- if not, then we fallback on the survey background url """
base_bg_url = "/survey/%s/%s/get_background_image"
for question in self:
if question.is_page:
background_section_id = question.id if question.background_image else False
else:
background_section_id = question.page_id.id if question.page_id.background_image else False
if background_section_id:
question.background_image_url = base_bg_url % (
question.survey_id.access_token,
background_section_id
)
else:
question.background_image_url = question.survey_id.background_image_url
@api.depends('is_page')
def _compute_question_type(self):
for question in self:
+8 -1
View File
@@ -54,7 +54,8 @@ class Survey(models.Model):
description_done = fields.Html(
"End Message", translate=True,
help="This message will be displayed when survey is completed")
background_image = fields.Binary("Background Image")
background_image = fields.Image("Background Image")
background_image_url = fields.Char('Background Url', compute="_compute_background_image_url")
active = fields.Boolean("Active", default=True)
user_id = fields.Many2one(
'res.users', string='Responsible',
@@ -171,6 +172,12 @@ class Survey(models.Model):
('badge_uniq', 'unique (certification_badge_id)', "The badge for each survey should be unique!"),
]
@api.depends('background_image', 'access_token')
def _compute_background_image_url(self):
self.background_image_url = False
for survey in self.filtered(lambda survey: survey.background_image and survey.access_token):
survey.background_image_url = "/survey/%s/get_background_image" % survey.access_token
def _compute_users_can_signup(self):
signup_allowed = self.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'
for survey in self:
@@ -22,7 +22,7 @@ var FormDescriptionPage = FieldChar.extend({
var $button = $(
'<div class="input-group-append">\
<button type="button" title="Open section" class="btn oe_edit_only o_icon_button">\
<i class="fa fa-fw o_button_icon fa-info-circle"/>\
<i class="fa fa-fw o_button_icon fa-external-link"/>\
</button>\
</div>'
);
+30 -6
View File
@@ -9,10 +9,12 @@ var Dialog = require('web.Dialog');
var dom = require('web.dom');
var utils = require('web.utils');
var SurveyPreloadImageMixin = require('survey.preload_image_mixin');
var _t = core._t;
var isMac = navigator.platform.toUpperCase().includes('MAC');
publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloadImageMixin, {
selector: '.o_survey_form',
events: {
'change .o_survey_form_choice_item': '_onChangeChoiceItem',
@@ -441,20 +443,35 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
self.$(selectorsToFadeout.join(',')).fadeOut(this.fadeInOutDelay, function () {
resolveFadeOut();
});
// Background management - Fade in / out on each transition
if (this.options.refreshBackground) {
$('div.o_survey_background').addClass('o_survey_background_transition');
}
Promise.all([fadeOutPromise, nextScreenPromise]).then(function (results) {
return self._onNextScreenDone(results[1], options);
var nextScreenWithBackgroundPromise = nextScreenPromise.then(function (result) {
self.nextScreenResult = result;
// once we have the next question, wait for the preload of the background
if (self.options.refreshBackground && result.background_image_url) {
return self._preloadBackground(result.background_image_url);
} else {
return Promise.resolve();
}
});
// Wait for the fade out and the preload of the next background. The next question have already been fetched.
Promise.all([fadeOutPromise, nextScreenWithBackgroundPromise]).then(function () {
return self._onNextScreenDone(options);
});
},
/**
* Handle server side validation and display eventual error messages.
*
* @param {string} result the HTML result of the screen to display
* @param {Object} options see '_submitForm' for details
*/
_onNextScreenDone: function (result, options) {
_onNextScreenDone: function (options) {
var self = this;
var result = this.nextScreenResult;
if (!(options && options.isFinish)
&& !this.options.sessionInProgress) {
@@ -511,7 +528,11 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
// prevent enter submit if we're on a page description (there is nothing to submit)
this.preventEnterSubmit = true;
}
// Background management - reset background overlay opacity to 0.7 to discover next background.
if (this.options.refreshBackground) {
$('div.o_survey_background').css("background-image", "url(" + result.background_image_url + ")");
$('div.o_survey_background').removeClass('o_survey_background_transition');
}
this.$('.o_survey_form_content').fadeIn(this.fadeInOutDelay);
$("html, body").animate({ scrollTop: 0 }, this.fadeInOutDelay);
self._focusOnFirstInput();
@@ -992,6 +1013,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
}
},
// OTHER TOOLS
// -------------------------------------------------------------------------
/**
* Will automatically focus on the first input to allow the user to complete directly the survey,
* without having to manually get the focus (only if the input has the right type - can write something inside -)
@@ -0,0 +1,34 @@
odoo.define('survey.preload_image_mixin', function (require) {
"use strict";
return {
/**
* Load the target section background and render it when loaded.
*
* This method is used to pre-load the image during the questions transitions (fade out) in order
* to be sure the image is fully loaded when setting it as background of the next question and
* finally display it (fade in)
*
* This idea is to wait until new background is loaded before changing the background
* (to avoid flickering or loading latency)
*
* @param {string} imageUrl
* @private
*/
_preloadBackground: async function (imageUrl) {
var resolvePreload;
// We have to manually create a promise here because the "onload" API does not provide one.
var preloadPromise = new Promise(function (resolve, reject) {resolvePreload = resolve;});
var background = new Image();
background.onload = function () {
resolvePreload(imageUrl);
};
background.src = imageUrl;
return preloadPromise;
}
};
});
@@ -2,13 +2,14 @@ odoo.define('survey.session_manage', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var SurveyPreloadImageMixin = require('survey.preload_image_mixin');
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;
publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend(SurveyPreloadImageMixin, {
selector: '.o_survey_session_manage',
events: {
'click .o_survey_session_copy': '_onCopySessionLink',
@@ -41,6 +42,10 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
// display props
self.showBarChart = self.$el.data('showBarChart');
self.showTextAnswers = self.$el.data('showTextAnswers');
// Question transition
self.stopNextQuestion = false;
// Background Management
self.refreshBackground = self.$el.data('refreshBackground');
var isRpcCall = self.$el.data('isRpcCall');
if (!isRpcCall) {
@@ -304,6 +309,12 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
_nextQuestion: function (goBack) {
var self = this;
// stop calling multiple times "get next question" process until next question is fully loaded.
if (this.stopNextQuestion) {
return;
}
this.stopNextQuestion = true;
this.isStartScreen = false;
if (this.surveyTimerWidget) {
this.surveyTimerWidget.destroy();
@@ -315,12 +326,9 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
resolveFadeOut();
});
var nextQuestionPromise = this._rpc({
route: _.str.sprintf('/survey/session/next_question/%s', self.surveyAccessToken),
params: {
'go_back': goBack,
}
});
if (this.refreshBackground) {
$('div.o_survey_background').addClass('o_survey_background_transition');
}
// avoid refreshing results while transitioning
if (this.resultsRefreshInterval) {
@@ -328,41 +336,72 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
delete this.resultsRefreshInterval;
}
Promise.all([fadeOutPromise, nextQuestionPromise]).then(async function (results) {
if (results[1]) {
var $renderedTemplate = $(results[1]);
self.$el.replaceWith($renderedTemplate);
// Ensure new question is fully loaded before force loading previous question screen.
await self.attachTo($renderedTemplate);
if (goBack) {
// As we arrive on "question" screen, simulate going to the results screen or leaderboard.
self._setShowInputs(true);
self._setShowAnswers(true);
if (self.sessionShowLeaderboard && self.isScoredQuestion) {
self.currentScreen = 'leaderboard';
self.leaderBoard.showLeaderboard(false, self.isScoredQuestion);
} else {
self.currentScreen = 'results';
self._refreshResults();
}
} else {
self._startTimer();
}
self.$el.fadeIn(self.fadeInOutTime);
} else if (self.sessionShowLeaderboard) {
// Display last screen if leaderboard activated
self.isLastQuestion = true;
self._setupLeaderboard().then(function () {
self.$('.o_survey_session_leaderboard_title').text(_t('Final Leaderboard'));
self.$('.o_survey_session_navigation_next').addClass('d-none');
self.$('.o_survey_leaderboard_buttons').removeClass('d-none');
self.leaderBoard.showLeaderboard(false, false);
});
var nextQuestionPromise = this._rpc({
route: _.str.sprintf('/survey/session/next_question/%s', self.surveyAccessToken),
params: {
'go_back': goBack,
}
}).then(function (result) {
self.nextQuestion = result;
if (self.refreshBackground && result.background_image_url) {
return self._preloadBackground(result.background_image_url);
} else {
self.$('.o_survey_session_close').click();
return Promise.resolve();
}
});
Promise.all([fadeOutPromise, nextQuestionPromise]).then(function () {
return self._onNextQuestionDone(goBack);
});
},
/**
* Refresh the screen with the next question's rendered template.
*
* @param {boolean} goBack Whether we are going back to the previous question or not
*/
_onNextQuestionDone: async function (goBack) {
var self = this;
if (this.nextQuestion.question_html) {
var $renderedTemplate = $(this.nextQuestion.question_html);
this.$el.replaceWith($renderedTemplate);
// Ensure new question is fully loaded before force loading previous question screen.
await this.attachTo($renderedTemplate);
if (goBack) {
// As we arrive on "question" screen, simulate going to the results screen or leaderboard.
this._setShowInputs(true);
this._setShowAnswers(true);
if (this.sessionShowLeaderboard && this.isScoredQuestion) {
this.currentScreen = 'leaderboard';
this.leaderBoard.showLeaderboard(false, this.isScoredQuestion);
} else {
this.currentScreen = 'results';
this._refreshResults();
}
} else {
this._startTimer();
}
this.$el.fadeIn(this.fadeInOutTime);
} else if (this.sessionShowLeaderboard) {
// Display last screen if leaderboard activated
this.isLastQuestion = true;
this._setupLeaderboard().then(function () {
self.$('.o_survey_session_leaderboard_title').text(_t('Final Leaderboard'));
self.$('.o_survey_session_navigation_next').addClass('d-none');
self.$('.o_survey_leaderboard_buttons').removeClass('d-none');
self.leaderBoard.showLeaderboard(false, false);
});
} else {
this.$('.o_survey_session_close').click();
}
// Background Management
if (this.refreshBackground) {
$('div.o_survey_background').css("background-image", "url(" + this.nextQuestion.background_image_url + ")");
$('div.o_survey_background').removeClass('o_survey_background_transition');
}
},
/**
@@ -14,6 +14,22 @@ nav#oe_main_menu_navbar {
/**********************************************************
Common Style
**********************************************************/
// the survey background image takes all the page background, with a translucent white overlay (box-shadow)
// When changing the background from one section to another, the overlay will become opaque to simulate a fade out of
// the background image. This ensure a smooth transition from one background to another, likewise the question
// transition.
.o_survey_background {
height: 100%;
overflow: auto;
transition: box-shadow 0.3s ease-in-out;
box-shadow: inset 0 0 0 10000px rgba(255,255,255,.7);
background: no-repeat fixed center;
background-size: cover;
&.o_survey_background_transition {
box-shadow: inset 0 0 0 10000px rgba(255,255,255,1);
}
}
.o_survey_wrap {
min-height: 100%;
}
@@ -13,6 +13,10 @@
<field name="sequence" invisible="1"/>
<field name="scoring_type" invisible="1"/>
<sheet>
<div class="float-right d-flex flex-column text-center" attrs="{'invisible': [('is_page', '=', False)]}">
<label for="background_image"/>
<field name="background_image" widget="image" class="oe_avatar"/>
</div>
<div class="oe_title" style="width: 100%;">
<label for="title" string="Section" attrs="{'invisible': [('is_page', '=', False)]}"/>
<label for="title" string="Question" attrs="{'invisible': [('is_page', '=', True)]}"/>
@@ -68,6 +68,7 @@
<tree decoration-bf="is_page" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="title" widget="survey_description_page"/>
<field name="background_image" invisible="1"/>
<field name="question_type" />
<field name="is_page" invisible="1"/>
<field name="questions_selection" invisible="1"/>
+16 -2
View File
@@ -8,7 +8,15 @@
<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(' + '/survey/get_background_image/%s/%s' % (survey.access_token, answer.access_token) + ') 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 and answer else 'height: 100%; overflow: auto;')"/>
<attribute name="t-att-style"
add="(('background-image: url(' + question.background_image_url + ');')
if question and question.background_image_url
else ('background-image: url(' + page.background_image_url + ');')
if page and page.background_image_url
else ('background-image: url(' + survey.background_image_url + ');')
if survey.background_image_url
else '')"/>
<attribute name="t-att-class" add="'o_survey_background'"/>
</xpath>
<xpath expr="//head/t[@t-call-assets][last()]" position="after">
<t t-call-assets="survey.survey_assets" lazy_load="True"/>
@@ -84,6 +92,11 @@
<template id="survey_fill_form" name="Survey: main page content">
<t t-set="survey_form_readonly" t-value="answer.state == 'done'"/>
<t t-set="background_image_url"
t-value="question.background_image_url
if question else page.background_image_url
if page else survey.background_image_url
if survey.background_image_url else False"/>
<form role="form" method="post" t-att-name="survey.id"
class="d-flex flex-grow-1 align-items-center"
t-att-data-answer-token="answer.access_token"
@@ -96,7 +109,8 @@
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)">
t-att-data-selected-answers="json.dumps(selected_answers)"
t-att-data-refresh-background="any(page.background_image for page in survey.page_ids)">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="token" t-att-value="answer.access_token" />
<div class="o_survey_error alert alert-danger d-none" role="alert">
@@ -46,6 +46,8 @@
</div>
</t>
</t>
<!--Set question to false to avoid background miss match as last question is still in context and is used in survey.layout t-call.-->
<t t-set="question" t-value="False"/>
</fieldset>
</div>
</div>
@@ -7,7 +7,13 @@
<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;'"/>
<attribute name="t-att-style"
add="(('background-image: url(' + survey.session_question_id.background_image_url + ');')
if survey.session_question_id and survey.session_question_id.background_image_url
else ('background-image: url(' + survey.background_image_url + ');')
if survey.background_image_url
else '')"/>
<attribute name="t-att-class" add="'o_survey_background'"/>
</xpath>
<xpath expr="//head/t[@t-call-assets][last()]" position="after">
<t t-call-assets="survey.survey_assets" lazy_load="True"/>
@@ -76,7 +82,8 @@
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">
t-att-data-show-text-answers="show_text_answers"
t-att-data-refresh-background="any(page.background_image for page in survey.page_ids)">
<div class="o_survey_question_header flex-wrap px-3 w-100 d-flex justify-content-between align-items-center position-absolute">
<h3>
<span>Go to <a t-att-href="survey.session_link" t-esc="survey.session_link" target="_blank" /></span>
@@ -137,6 +144,8 @@
</div>
</div>
<div class="o_survey_session_leaderboard w-100 flex-column flex-grow-1" style="display: none;">
<!--Set question to false to avoid background miss match when no question are displayed.-->
<t t-set="question" t-value="False"/>
<div class="d-flex">
<h1 class="o_survey_session_leaderboard_title flex-grow-1">
<span t-if="is_last_question">Final Leaderboard</span>