summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2020-03-31 19:07:19 +0200
committerGitHub <noreply@github.com>2020-03-31 19:07:19 +0200
commitb6e2bb655b8f539053a88246040ff0689b74a934 (patch)
tree7d3f40d1e21c3974bf104b1aa1063f566fc6e5fd
parentc5807af0abd8cee238e141622b8dbddc785499cf (diff)
parent280c6bf1d6d823cde7aae1608aca4e4c36a80f22 (diff)
[IMP] survey: improve sessions usability and design
PURPOSE Improve the various sessions flow to make it easier for attendees to reach the session and to allow better interactions between the host and his audience. This comes with small usability improvements as well as increased "beautifulness" by using custom layouts & animations. SPECS General specifications, see sub-commits for details. - Currently, the "short link" feature is good but it still needs to be improved with the following points: - Make the default "code" shorter (4) and digit only. - Allow to customize the "code" - Check for potential collisions - When attendees reach the survey through the code, they need to land on the "waiting screen" even if the session is not started yet. This will allow the host to show the survey link even if he forgets to click on "start session" first. - Reduce the fade out / fade in delay when moving from one question to another. It's currently 2 seconds, change it to 1 second. Right now the delay gives a "slowness" impression. - Display matrix choices on the host screen. It will allow attendees that are not participating to have more context. - Add python test/tours to test the survey session flow from the host point of view - Improve survey form navigation (moved buttons, added keyboard navigation) - Auto submit questions and matrix with simple choice DESIGN TWEAKS Slightly improve the survey form by adding a few cosmetic changes, notably: - Add/reduce some margins/paddings - Adjust colors/font-weight - Adjust heights - Change some icons by more fitting replacements - Center matrix choices - Move survey progression to bottom-right - Remove ugly jumbotrons - Hide survey title in page_per_question layout - When there are 5 or less option for simple_choice or multiple_choice questions, display the options vertically. - Only show the textarea for the "comment answer" if the user clicks on the related option. LINKS Upgrade PR odoo/upgrade#897 Task 2208574 closes odoo/odoo#46768 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
-rw-r--r--addons/survey/controllers/main.py92
-rw-r--r--addons/survey/controllers/survey_session_manage.py80
-rw-r--r--addons/survey/data/survey_demo_certification.xml1
-rw-r--r--addons/survey/models/survey_survey.py102
-rw-r--r--addons/survey/static/src/js/survey_form.js89
-rw-r--r--addons/survey/static/src/js/survey_quick_access.js6
-rw-r--r--addons/survey/static/src/js/survey_session_manage.js10
-rw-r--r--addons/survey/static/src/scss/survey_form.scss134
-rw-r--r--addons/survey/static/tests/tours/survey_session_manage_test.js19
-rw-r--r--addons/survey/static/tests/tours/survey_tour_session_create.js20
-rw-r--r--addons/survey/static/tests/tours/survey_tour_session_manage.js440
-rw-r--r--addons/survey/static/tests/tours/survey_tour_session_start.js32
-rw-r--r--addons/survey/static/tests/tours/survey_tour_session_tools.js21
-rw-r--r--addons/survey/tests/__init__.py1
-rw-r--r--addons/survey/tests/test_survey_ui_session.py210
-rw-r--r--addons/survey/views/assets.xml5
-rw-r--r--addons/survey/views/survey_survey_views.xml12
-rw-r--r--addons/survey/views/survey_templates.xml229
-rw-r--r--addons/survey/views/survey_templates_management.xml23
-rw-r--r--addons/survey/views/survey_templates_print.xml4
-rw-r--r--addons/survey/views/survey_templates_statistics.xml4
-rw-r--r--addons/survey/views/survey_templates_user_input_session.xml13
22 files changed, 1286 insertions, 261 deletions
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index d12348a33a7..0d0a32ee9a2 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -40,16 +40,6 @@ class Survey(http.Controller):
], limit=1)
return survey_sudo, answer_sudo
- def _fetch_from_access_code(self, survey_short_token):
- if survey_short_token and len(survey_short_token) == 6:
- matching_survey = request.env['survey.survey'].sudo().search([
- ('state', '=', 'open'),
- ('access_token', 'like', f'{survey_short_token}%'),
- ])
- if len(matching_survey) == 1:
- return matching_survey
- return False
-
def _check_validity(self, survey_token, answer_token, ensure_token=True):
""" Check survey is open and can be taken. This does not checks for
security rules, only functional / business rules. It returns a string key
@@ -151,30 +141,6 @@ class Survey(http.Controller):
return werkzeug.utils.redirect("/")
# ------------------------------------------------------------
- # QUICK ACCESS SURVEY ROUTES
- # ------------------------------------------------------------
-
- @http.route('/s', type='http', auth='public', website=True, sitemap=False)
- def survey_access_code(self, **post):
- """ Renders the survey access code page route.
- This page allows the user to enter the short code of the survey.
- It's mainly used in 'session mode' when attendees have to manually type the
- URL, to ease survey access. """
- return request.render("survey.survey_access_code")
-
- @http.route('/s/<string:survey_short_token>', type='http', auth='public', website=True)
- def survey_start_short(self, survey_short_token):
- """" Redirects to 'survey_start' route using a shortened link & token.
- We match the 6 first characters of the token for open surveys.
- This route is mostly used in survey sessions where we need short links for people to type. """
-
- survey = self._fetch_from_access_code(survey_short_token)
- if survey:
- return werkzeug.utils.redirect("/survey/start/%s" % survey.access_token)
-
- return werkzeug.utils.redirect("/")
-
- # ------------------------------------------------------------
# TEST / RETRY SURVEY ROUTES
# ------------------------------------------------------------
@@ -302,10 +268,12 @@ class Survey(http.Controller):
if 'previous_page_id' in post:
previous_page_or_question_id = int(post['previous_page_id'])
new_previous_id = survey_sudo._get_next_page_or_question(answer_sudo, previous_page_or_question_id, go_back=True).id
+ page_or_question = request.env['survey.question'].sudo().browse(previous_page_or_question_id)
data.update({
- page_or_question_key: request.env['survey.question'].sudo().browse(previous_page_or_question_id),
+ page_or_question_key: page_or_question,
'previous_page_id': new_previous_id,
- 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id.id == new_previous_id)
+ 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id.id == new_previous_id),
+ 'can_go_back': survey_sudo._can_go_back(answer_sudo, page_or_question),
})
return data
@@ -318,9 +286,9 @@ class Survey(http.Controller):
answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0)
if next_page_or_question:
- pages_or_questions = survey_sudo._get_pages_or_questions(answer_sudo)
- if pages_or_questions.ids.index(next_page_or_question.id) == len(pages_or_questions) - 1:
- data.update({'survey_last': True})
+ data.update({
+ 'survey_last': survey_sudo._is_last_page_or_question(answer_sudo, next_page_or_question)
+ })
if answer_sudo.is_session_answer and next_page_or_question.is_time_limited:
data.update({
@@ -330,7 +298,8 @@ class Survey(http.Controller):
data.update({
page_or_question_key: next_page_or_question,
- 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id == next_page_or_question)
+ 'has_answered': answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id == next_page_or_question),
+ 'can_go_back': survey_sudo._can_go_back(answer_sudo, next_page_or_question),
})
if survey_sudo.questions_layout != 'one_page':
data.update({
@@ -345,10 +314,36 @@ 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."""
- data = self._prepare_survey_data(survey_sudo, answer_sudo, **post)
+ survey_data = self._prepare_survey_data(survey_sudo, answer_sudo, **post)
+
+ survey_content = False
if answer_sudo.state == 'done':
- return request.env.ref('survey.survey_fill_form_done').render(data)
- return request.env.ref('survey.survey_fill_form_in_progress').render(data)
+ survey_content = request.env.ref('survey.survey_fill_form_done').render(survey_data)
+ else:
+ survey_content = request.env.ref('survey.survey_fill_form_in_progress').render(survey_data)
+
+ survey_progress = False
+ if answer_sudo.state == 'in_progress' and not survey_data.get('question', request.env['survey.question']).is_page:
+ if survey_sudo.questions_layout == 'page_per_section':
+ page_ids = survey_sudo.page_ids.ids
+ survey_progress = request.env.ref('survey.survey_progression').render({
+ 'survey': survey_sudo,
+ 'page_ids': page_ids,
+ 'page_number': page_ids.index(survey_data['page'].id) + (1 if survey_sudo.progression_mode == 'number' else 0)
+ })
+ elif survey_sudo.questions_layout == 'page_per_question':
+ page_ids = survey_sudo.question_ids.ids
+ survey_progress = request.env.ref('survey.survey_progression').render({
+ 'survey': survey_sudo,
+ 'page_ids': page_ids,
+ 'page_number': page_ids.index(survey_data['question'].id)
+ })
+
+ return {
+ 'survey_content': survey_content,
+ 'survey_progress': survey_progress,
+ 'survey_navigation': request.env.ref('survey.survey_navigation').render(survey_data),
+ }
@http.route('/survey/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
def survey_display_page(self, survey_token, answer_token, **post):
@@ -492,17 +487,6 @@ class Survey(http.Controller):
return self._prepare_question_html(survey_sudo, answer_sudo)
- @http.route('/survey/check_access_code/<string:access_code>', type='json', auth='public', website=True)
- def survey_access_code_check(self, access_code):
- """ Checks if the given code is matching a survey access_token.
- If yes, redirect to /s/code route.
- If not, return error. The user is invited to type again the code. """
- survey = self._fetch_from_access_code(access_code)
- if survey:
- return {"survey_url": "/survey/start/%s" % survey.access_token}
-
- return {"error": "survey_wrong"}
-
def _extract_comment_from_answers(self, question, answers):
""" Answers is a custom structure depending of the question type
that can contain question answers but also comments that need to be
diff --git a/addons/survey/controllers/survey_session_manage.py b/addons/survey/controllers/survey_session_manage.py
index b93a31d94ff..e81626ce95b 100644
--- a/addons/survey/controllers/survey_session_manage.py
+++ b/addons/survey/controllers/survey_session_manage.py
@@ -3,10 +3,10 @@
import datetime
import json
+import werkzeug
from dateutil.relativedelta import relativedelta
from werkzeug.exceptions import NotFound
-from werkzeug.urls import url_join
from odoo import fields, http
from odoo.http import request
@@ -19,6 +19,27 @@ class UserInputSession(http.Controller):
Unlike the regular survey controller, user trying to access the survey must have full access rights! """
return request.env['survey.survey'].search([('access_token', '=', survey_token)])
+ def _fetch_from_session_code(self, session_code):
+ """ Matches a survey against a passed session_code.
+ We force the session_state to be reachable (ready / in_progress) to avoid people
+ using this route to access other (private) surveys.
+ We limit to sessions opened within the last 7 days to avoid potential abuses. """
+ if session_code:
+ matching_survey = request.env['survey.survey'].sudo().search([
+ ('state', '=', 'open'),
+ ('session_state', 'in', ['ready', 'in_progress']),
+ ('session_start_time', '>', fields.Datetime.now() - relativedelta(days=7)),
+ ('session_code', '=', session_code),
+ ], limit=1)
+ if matching_survey:
+ return matching_survey
+
+ return False
+
+ # ------------------------------------------------------------
+ # SURVEY SESSION MANAGEMENT
+ # ------------------------------------------------------------
+
@http.route('/survey/session/manage/<string:survey_token>', type='http', auth='user', website=True)
def survey_session_manage(self, survey_token, **kwargs):
""" Main route used by the host to 'manager' the session.
@@ -37,8 +58,7 @@ class UserInputSession(http.Controller):
if survey.session_state == 'ready':
return request.render('survey.user_input_session_open', {
- 'survey': survey,
- 'survey_url': url_join(survey.get_base_url(), survey.get_start_short_url())
+ 'survey': survey
})
else:
template_values = self._prepare_manage_session_values(survey)
@@ -49,7 +69,19 @@ class UserInputSession(http.Controller):
""" This route is called when the host goes to the next question of the session.
It's not a regular 'request.render' route because we handle the transition between
- questions using a AJAX call to be able to display a bioutiful fade in/out effect. """
+ questions using a AJAX call to be able to display a bioutiful fade in/out effect.
+
+ It triggers the next question of the session.
+
+ We artificially add 1 second to the 'current_question_start_time' to account for server delay.
+ As the timing can influence the attendees score, we try to be fair with everyone by giving them
+ an extra second before we start counting down.
+
+ Frontend should take the delay into account by displaying the appropriate animations.
+
+ 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. """
survey = self._fetch_from_token(survey_token)
@@ -67,7 +99,7 @@ class UserInputSession(http.Controller):
now = datetime.datetime.now()
survey.sudo().write({
'session_question_id': next_question.id,
- 'session_question_start_time': fields.Datetime.now() + relativedelta(seconds=2)
+ 'session_question_start_time': fields.Datetime.now() + relativedelta(seconds=1)
})
request.env['bus.bus'].sendone(survey.access_token, {
'question_start': now.timestamp(),
@@ -119,15 +151,49 @@ class UserInputSession(http.Controller):
'leaderboard': survey._prepare_leaderboard_values()
})
+ # ------------------------------------------------------------
+ # QUICK ACCESS SURVEY ROUTES
+ # ------------------------------------------------------------
+
+ @http.route('/s', type='http', auth='public', website=True, sitemap=False)
+ def survey_session_code(self, **post):
+ """ Renders the survey session code page route.
+ This page allows the user to enter the session code of the survey.
+ It is mainly used to ease survey access for attendees in session mode. """
+ return request.render("survey.survey_session_code")
+
+ @http.route('/s/<string:session_code>', type='http', auth='public', website=True)
+ def survey_start_short(self, session_code):
+ """" Redirects to 'survey_start' route using a shortened link & token.
+ We match the session_code for open surveys.
+ This route is used in survey sessions where we need short links for people to type. """
+
+ survey = self._fetch_from_session_code(session_code)
+ if survey:
+ return werkzeug.utils.redirect("/survey/start/%s" % survey.access_token)
+
+ return werkzeug.utils.redirect("/s")
+
+ @http.route('/survey/check_session_code/<string:session_code>', type='json', auth='public', website=True)
+ def survey_check_session_code(self, session_code):
+ """ Checks if the given code is matching a survey session_code.
+ If yes, redirect to /s/code route.
+ If not, return error. The user is invited to type again the code. """
+ survey = self._fetch_from_session_code(session_code)
+ if survey:
+ return {"survey_url": "/survey/start/%s" % survey.access_token}
+
+ return {"error": "survey_wrong"}
+
def _prepare_manage_session_values(self, survey):
is_last_question = False
if survey.question_ids:
- is_last_question = survey.session_question_id == survey.question_ids[-1]
+ most_voted_answers = survey._get_session_most_voted_answers()
+ is_last_question = survey._is_last_page_or_question(most_voted_answers, survey.session_question_id)
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']))
diff --git a/addons/survey/data/survey_demo_certification.xml b/addons/survey/data/survey_demo_certification.xml
index 6b85585deb7..e2cf3e0938b 100644
--- a/addons/survey/data/survey_demo_certification.xml
+++ b/addons/survey/data/survey_demo_certification.xml
@@ -14,6 +14,7 @@
<field name="access_token">4ead4bc8-b8f2-4760-a682-1fde8ddb95ac</field>
<field name="state">open</field>
<field name="access_mode">public</field>
+ <field name="questions_layout">page_per_question</field>
<field name="users_can_go_back" eval="True" />
<field name="users_login_required" eval="True" />
<field name="scoring_type" >scoring_with_answers</field>
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
index 41022901c9e..60558a94924 100644
--- a/addons/survey/models/survey_survey.py
+++ b/addons/survey/models/survey_survey.py
@@ -23,6 +23,27 @@ class Survey(models.Model):
def _get_default_access_token(self):
return str(uuid.uuid4())
+ def _get_default_session_code(self):
+ """ Attempt to generate a session code for our survey.
+ The method will first try to generate 20 codes with 4 digits each and check if any are colliding.
+ If we have at least one non-colliding code, we use it.
+ If all 20 generated codes are colliding, we try with 20 codes of 5 digits,
+ then 6, ... up to 10 digits. """
+
+ for digits_count in range(4, 10):
+ range_lower_bound = 1 * (10 ** (digits_count - 1))
+ range_upper_bound = (range_lower_bound * 10) - 1
+ code_candidates = set([str(random.randint(range_lower_bound, range_upper_bound)) for i in range(20)])
+ colliding_codes = self.sudo().search_read(
+ [('session_code', 'in', list(code_candidates))],
+ ['session_code']
+ )
+ code_candidates -= set([colliding_code['session_code'] for colliding_code in colliding_codes])
+ if code_candidates:
+ return list(code_candidates)[0]
+
+ return False # could not generate a code
+
# description
title = fields.Char('Survey Title', required=True, translate=True)
color = fields.Integer('Color Index', default=0)
@@ -113,6 +134,9 @@ class Survey(models.Model):
('ready', 'Ready'),
('in_progress', 'In Progress'),
], string="Session State", copy=False)
+ session_code = fields.Char('Session Code', default=lambda self: self._get_default_session_code(), copy=False,
+ help="This code will be used by your attendees to reach your session. Feel free to customize it however you like!")
+ session_link = fields.Char('Session Link', compute='_compute_session_link')
# live sessions - current question fields
session_question_id = fields.Many2one('survey.question', string="Current Question", copy=False,
help="The current question of the survey session.")
@@ -130,6 +154,7 @@ class Survey(models.Model):
_sql_constraints = [
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
+ ('session_code_unique', 'unique(session_code)', 'Session code should be unique'),
('certification_check', "CHECK( scoring_type!='no_scoring' OR certification=False )",
'You can only create certifications for surveys that have a scoring mechanism.'),
('time_limit_check', "CHECK( (is_time_limited=False) OR (time_limit is not null AND time_limit > 0) )",
@@ -192,10 +217,14 @@ class Survey(models.Model):
""" We have to loop since our result is dependent of the survey.session_start_time.
This field is currently used to display the count about a single survey, in the
context of sessions, so it should not matter too much. """
+
for survey in self:
answer_count = 0
input_count = self.env['survey.user_input'].read_group(
- [('survey_id', '=', survey.id), ('create_date', '>=', survey.session_start_time)],
+ [('survey_id', '=', survey.id),
+ ('is_session_answer', '=', True),
+ ('state', '!=', 'done'),
+ ('create_date', '>=', survey.session_start_time)],
['create_uid:count'],
['survey_id'],
)
@@ -224,6 +253,18 @@ class Survey(models.Model):
survey.session_question_answer_count = answer_count
+ @api.depends('session_code')
+ def _compute_session_link(self):
+ for survey in self:
+ if survey.session_code:
+ survey.session_link = werkzeug.urls.url_join(
+ survey.get_base_url(),
+ '/s/%s' % survey.session_code)
+ else:
+ survey.session_link = werkzeug.urls.url_join(
+ survey.get_base_url(),
+ survey.get_start_url())
+
@api.depends('scoring_type', 'question_and_page_ids.save_as_nickname')
def _compute_session_show_leaderboard(self):
for survey in self:
@@ -404,6 +445,30 @@ class Survey(models.Model):
return questions
+ def _can_go_back(self, answer, page_or_question):
+ """ Check if the user can go back to the previous question/page for the currently
+ viewed question/page.
+ Back button needs to be configured on survey and, depending on the layout:
+ - In 'page_per_section', we can go back if we're not on the first page
+ - In 'page_per_question', we can go back if:
+ - It is not a session answer (doesn't make sense to go back in session context)
+ - We are not on the first question
+ - The survey does not have pages OR this is not the first page of the survey
+ (pages are displayed in 'page_per_question' layout when they have a description, see PR#44271)
+ """
+ self.ensure_one()
+
+ if self.users_can_go_back and answer.state == 'in_progress':
+ if self.questions_layout == 'page_per_section' and page_or_question != self.page_ids[0]:
+ return True
+ elif self.questions_layout == 'page_per_question' and \
+ not answer.is_session_answer and \
+ page_or_question != answer.predefined_question_ids[0] \
+ and (not self.page_ids or page_or_question != self.page_ids[0]):
+ return True
+
+ return False
+
def _has_attempts_left(self, partner, email, invite_token):
self.ensure_one()
@@ -484,7 +549,7 @@ class Survey(models.Model):
if page_or_question_id == 0:
return pages_or_questions[0]
- current_page_index = pages_or_questions.ids.index(next(p.id for p in pages_or_questions if p.id == page_or_question_id))
+ current_page_index = pages_or_questions.ids.index(page_or_question_id)
# Get previous and we are on first page OR Get Next and we are on last page
if (go_back and current_page_index == 0) or (not go_back and current_page_index == len(pages_or_questions) - 1):
@@ -517,6 +582,27 @@ class Survey(models.Model):
return Question
+ def _is_last_page_or_question(self, user_input, page_or_question):
+ """ This method checks if the given question or page is the last one.
+ This includes conditional questions configuration. If the given question is normally not the last one but
+ every following questions are inactive due to conditional questions configurations (and user choices),
+ the given question will be the last one.
+ For section, we check in each following section if there is an active question.
+ If yes, the given page is not the last one.
+ """
+ pages_or_questions = self._get_pages_or_questions(user_input)
+ current_page_index = pages_or_questions.ids.index(page_or_question.id)
+ next_page_or_question_candidates = pages_or_questions[current_page_index + 1:]
+ if next_page_or_question_candidates:
+ inactive_questions = user_input._get_inactive_conditional_questions()
+ if self.questions_layout == 'page_per_question':
+ return not any(next_question not in inactive_questions for next_question in next_page_or_question_candidates)
+ elif self.questions_layout == 'page_per_section':
+ for section in next_page_or_question_candidates:
+ return not any(next_question not in inactive_questions for next_question in section.question_ids)
+
+ return True
+
def _get_survey_questions(self, answer=None, page_id=None, question_id=None):
""" Returns a tuple containing: the survey question and the passed question_id / page_id
based on the question_layout and the fact that it's a session or not.
@@ -591,18 +677,6 @@ class Survey(models.Model):
self.sudo().flush(['session_state'])
def _get_session_next_question(self):
- """ Triggers the next question of the session.
-
- We artificially add 2 seconds to the 'current_question_start_time' to account for server delay.
- As the timing can influence the attendees score, we try to be fair with everyone by giving them
- an extra few seconds before we start counting down.
-
- Frontend should take the delay into account by displaying the appropriate animations.
-
- 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. """
-
self.ensure_one()
if not self.question_ids or not self.env.user.has_group('survey.group_survey_user'):
diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js
index c9e9d5942a3..7d77dcab579 100644
--- a/addons/survey/static/src/js/survey_form.js
+++ b/addons/survey/static/src/js/survey_form.js
@@ -48,13 +48,19 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
self._focusOnFirstInput();
// Init event listener
if (!self.readonly) {
- $(document).on('keypress', self._onKeyPress.bind(self));
+ $(document).on('keydown', self._onKeyDown.bind(self));
}
if (self.options.sessionInProgress &&
(self.options.isStartScreen || self.options.hasAnswered || self.options.isPageDescription)) {
self.preventEnterSubmit = true;
}
self._initSessionManagement();
+
+ // Needs global selector as progress/navigation are not within the survey form, but need
+ //to be updated at the same time
+ self.$surveyProgress = $('.o_survey_progress_wrapper');
+ self.$surveyNavigation = $('.o_survey_navigation_wrapper');
+ self.$surveyNavigation.find('.o_survey_navigation_submit').on('click', self._onSubmit.bind(self));
});
},
@@ -65,9 +71,18 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
// Handlers
// -------------------------------------------------------------------------
- _onKeyPress: function (event) {
- // If user is answering a textarea, do not handle keyPress
- if (this.$("textarea").is(":focus")) {
+ /**
+ * Handle keyboard navigation:
+ * - 'enter' or 'arrow-right' => submit form
+ * - 'arrow-left' => submit form (but go back backwards)
+ * - other alphabetical character ('a', 'b', ...)
+ * Select the related option in the form (if available)
+ *
+ * @param {Event} event
+ */
+ _onKeyDown: function (event) {
+ // If user is answering a text input, do not handle keydown
+ if (this.$("textarea").is(":focus") || this.$('input').is(':focus')) {
return;
}
@@ -76,12 +91,17 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
var letter = String.fromCharCode(keyCode).toUpperCase();
// Handle Start / Next / Submit
- if (keyCode === 13) { // Enter : go Next
+ if (keyCode === 13 || keyCode === 39) { // Enter or arrow-right: go Next
event.preventDefault();
if (!this.preventEnterSubmit) {
var isFinish = this.$('button[value="finish"]').length !== 0;
this._submitForm({isFinish: isFinish});
}
+ } else if (keyCode === 37) { // arrow-left: previous (if available)
+ // It's easier to actually click on the button (if in the DOM) as it contains necessary
+ // data that are used in the event handler.
+ // Again, global selector necessary since the navigation is outside of the form.
+ $('.o_survey_navigation_submit[value="previous"]').click();
} else if (self.options.questionsLayout === 'page_per_question'
&& letter.match(/[a-z]/i)) {
var $choiceInput = this.$(`input[data-selection-key=${letter}]`);
@@ -91,6 +111,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
} else {
$choiceInput.prop("checked", !$choiceInput.prop("checked")).trigger('change');
}
+
// Avoid selection key to be typed into the textbox if 'other' is selected by key
event.preventDefault();
}
@@ -99,8 +120,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
/**
* Checks, if the 'other' choice is checked. Applies only if the comment count as answer.
- * If not checked : Clear the comment textarea and disable it
- * If checked : enable the comment textarea and focus on it
+ * If not checked : Clear the comment textarea, hide and disable it
+ * If checked : enable the comment textarea, show and focus on it
*
* @private
* @param {Event} event
@@ -114,19 +135,32 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
if ($otherItem.prop('checked') || $commentInput.hasClass('o_survey_comment')) {
$commentInput.enable();
+ $commentInput.closest('.o_survey_comment_container').removeClass('d-none');
if ($otherItem.prop('checked')) {
$commentInput.focus();
}
} else {
$commentInput.val('');
+ $commentInput.closest('.o_survey_comment_container').addClass('d-none');
$commentInput.enable(false);
}
var $matrixBtn = $target.closest('.o_survey_matrix_btn');
if ($target.attr('type') === 'radio') {
+ var isQuestionComplete = false;
if ($matrixBtn.length > 0) {
$matrixBtn.closest('tr').find('td').removeClass('o_survey_selected');
$matrixBtn.addClass('o_survey_selected');
+ if (this.options.questionsLayout === 'page_per_question') {
+ var subQuestionsIds = $matrixBtn.closest('table').data('subQuestions');
+ var completedQuestions = [];
+ subQuestionsIds.forEach(function (id) {
+ if (self.$('tr#' + id).find('input:checked').length !== 0) {
+ completedQuestions.push(id);
+ }
+ });
+ isQuestionComplete = completedQuestions.length === subQuestionsIds.length;
+ }
} else {
var previouslySelectedAnswer = $choiceItemGroup.find('label.o_survey_selected');
previouslySelectedAnswer.removeClass('o_survey_selected');
@@ -134,6 +168,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
var newlySelectedAnswer = $target.closest('label');
if (newlySelectedAnswer.find('input').val() !== previouslySelectedAnswer.find('input').val()) {
newlySelectedAnswer.addClass('o_survey_selected');
+ isQuestionComplete = this.options.questionsLayout === 'page_per_question';
}
// Conditional display
@@ -165,6 +200,11 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
}
}
}
+ // Submit Form
+ var isLastQuestion = this.$('button[value="finish"]').length !== 0;
+ if (!isLastQuestion && this.options.usersCanGoBack && isQuestionComplete) {
+ this._submitForm({});
+ }
} else { // $target.attr('type') === 'checkbox'
if ($matrixBtn.length > 0) {
$matrixBtn.toggleClass('o_survey_selected', !$matrixBtn.hasClass('o_survey_selected'));
@@ -234,18 +274,18 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
* If the trigger is 'next_question', we handle some extra computation to find
* a suitable "fadeInOutDelay" based on the delay between the time of the question
* change by the host and the time of reception of the event.
- * This will allow us to account for a little bit of server lag (up to 2 seconds)
+ * This will allow us to account for a little bit of server lag (up to 1 second)
* while giving everyone a fair experience on the quiz.
*
* e.g 1:
* - The host switches the question
- * - We receive the event 500 ms later due to server lag
- * - -> The fadeInOutDelay will be 750 ms (500ms delay + 750ms * 2 fade in fade out)
+ * - We receive the event 200 ms later due to server lag
+ * - -> The fadeInOutDelay will be 400 ms (200ms delay + 400ms * 2 fade in fade out)
*
* e.g 2:
* - The host switches the question
- * - We receive the event 1500 ms later due to bigger server lag
- * - -> The fadeInOutDelay will be 250ms (1500ms delay + 250ms * 2 fade in fade out)
+ * - We receive the event 600 ms later due to bigger server lag
+ * - -> The fadeInOutDelay will be 200ms (600ms delay + 200ms * 2 fade in fade out)
*
* @private
* @param {Array[]} notifications structured as specified by the bus feature
@@ -275,14 +315,16 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
var serverDelayMS = moment.utc().valueOf() - moment.unix(nextPageEvent.question_start).utc().valueOf();
if (serverDelayMS < 0) {
serverDelayMS = 0;
- } else if (serverDelayMS > 2000) {
- serverDelayMS = 2000;
+ } else if (serverDelayMS > 1000) {
+ serverDelayMS = 1000;
}
- this.fadeInOutDelay = (2000 - serverDelayMS) / 2;
+ this.fadeInOutDelay = (1000 - serverDelayMS) / 2;
} else {
this.fadeInOutDelay = 400;
}
+ this.$('.o_survey_main_title:visible').fadeOut(400);
+
this.preventEnterSubmit = false;
this.readonly = false;
this._nextScreen(
@@ -324,6 +366,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
if (this.options.isStartScreen) {
route = "/survey/begin";
+ // Hide survey title in 'page_per_question' layout: it takes too much space
+ if (this.options.questionsLayout === 'page_per_question') {
+ this.$('.o_survey_main_title').fadeOut(400);
+ }
} else {
var $form = this.$('form');
var formData = new FormData($form[0]);
@@ -396,7 +442,18 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
if (result && !result.error) {
this.$(".o_survey_form_content").empty();
- this.$(".o_survey_form_content").html(result);
+ this.$(".o_survey_form_content").html(result.survey_content);
+
+ if (result.survey_progress && this.$surveyProgress.length !== 0) {
+ this.$surveyProgress.html(result.survey_progress);
+ } else if (options.isFinish && this.$surveyProgress.length !== 0) {
+ this.$surveyProgress.remove();
+ }
+
+ if (result.survey_navigation && this.$surveyNavigation.length !== 0) {
+ this.$surveyNavigation.html(result.survey_navigation);
+ this.$surveyNavigation.find('.o_survey_navigation_submit').on('click', self._onSubmit.bind(self));
+ }
// Hide timer if end screen (if page_per_question in case of conditional questions)
if (self.options.questionsLayout === 'page_per_question' && this.$('.o_survey_finished').length > 0) {
diff --git a/addons/survey/static/src/js/survey_quick_access.js b/addons/survey/static/src/js/survey_quick_access.js
index 0534aa41878..ff0f3e88473 100644
--- a/addons/survey/static/src/js/survey_quick_access.js
+++ b/addons/survey/static/src/js/survey_quick_access.js
@@ -23,6 +23,8 @@ publicWidget.registry.SurveyQuickAccessWidget = publicWidget.Widget.extend({
if (!self.readonly) {
$(document).on('keypress', self._onKeyPress.bind(self));
}
+
+ self.$('input').focus();
});
},
@@ -48,9 +50,9 @@ publicWidget.registry.SurveyQuickAccessWidget = publicWidget.Widget.extend({
_submitCode: function () {
var self = this;
this.$('.o_survey_error').addClass("d-none");
- var $accessCodeInput = this.$('input#access_code');
+ var $sessionCodeInput = this.$('input#session_code');
this._rpc({
- route: `/survey/check_access_code/${$accessCodeInput.val()}`,
+ route: `/survey/check_session_code/${$sessionCodeInput.val()}`,
}).then(function (response) {
if (response.survey_url) {
window.location = response.survey_url;
diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js
index 4a1fdf13798..897aab6d7b3 100644
--- a/addons/survey/static/src/js/survey_session_manage.js
+++ b/addons/survey/static/src/js/survey_session_manage.js
@@ -304,7 +304,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
var resolveFadeOut;
var fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
- this.$el.fadeOut(1000, function () {
+ this.$el.fadeOut(500, function () {
resolveFadeOut();
});
@@ -323,7 +323,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
var $renderedTemplate = $(results[1]);
self.$el.replaceWith($renderedTemplate);
self.attachTo($renderedTemplate);
- self.$el.fadeIn(1000, function () {
+ self.$el.fadeIn(500, function () {
self._startTimer();
});
} else {
@@ -376,7 +376,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
_refreshResults: function () {
var self = this;
- this._rpc({
+ return this._rpc({
route: _.str.sprintf('/survey/session/results/%s', self.surveyAccessToken)
}).then(function (questionResults) {
if (questionResults) {
@@ -398,6 +398,8 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
);
}
}
+
+ return Promise.resolve();
}, function () {
// on failure, stop refreshing
clearInterval(self.resultsRefreshInterval);
@@ -412,7 +414,7 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend({
_refreshAttendeesCount: function () {
var self = this;
- self._rpc({
+ return self._rpc({
model: 'survey.survey',
method: 'read',
args: [[self.surveyId], ['session_answer_count']],
diff --git a/addons/survey/static/src/scss/survey_form.scss b/addons/survey/static/src/scss/survey_form.scss
index aaa62d7f38f..97b1343b2fe 100644
--- a/addons/survey/static/src/scss/survey_form.scss
+++ b/addons/survey/static/src/scss/survey_form.scss
@@ -18,7 +18,24 @@ nav#oe_main_menu_navbar {
min-height: 100%;
}
-.o_survey_form, .o_survey_print, .o_survey_quick_access {
+.o_survey_progress_wrapper {
+ min-width: 11rem;
+ max-width: 15rem;
+
+ .o_survey_progress {
+ height:0.5em;
+ }
+}
+
+.o_survey_navigation_wrapper .o_survey_navigation_submit {
+ cursor: pointer;
+}
+
+.o_survey_brand_message {
+ background-color: rgba(255,255,255,0.7);
+}
+
+.o_survey_form, .o_survey_print, .o_survey_session_manage, .o_survey_quick_access {
.o_survey_question_error {
height: 0px;
transition: height .5s ease;
@@ -42,27 +59,27 @@ nav#oe_main_menu_navbar {
.o_survey_question_datetime,
.o_survey_question_numerical_box {
border: 0px;
- border-bottom: 2px solid $primary;
+ border-bottom: 1px solid $primary;
&:disabled {
color: black !important;
border-color: $gray-600;
- border-bottom: 2px solid $gray-600;
+ border-bottom: 1px solid $gray-600;
}
&:focus {
- border-color: $primary;
+ box-shadow: none;
}
}
.o_survey_form_date .input-group-append {
right: 0;
- .input-group-text i {
- font-size: large;
- }
+ bottom: 5px;
+ top: auto;
}
.o_survey_choice_btn {
transition: background-color 0.3s ease;
flex: 1 0 300px;
+ color: $primary;
span {
line-height: 25px;
@@ -83,6 +100,24 @@ nav#oe_main_menu_navbar {
}
}
+ input::placeholder, textarea::placeholder {
+ font-weight: 300;
+ }
+
+ .o_survey_page_per_question.o_survey_simple_choice.o_survey_minimized_display,
+ .o_survey_page_per_question.o_survey_multiple_choice.o_survey_minimized_display,
+ .o_survey_page_per_question.o_survey_numerical_box,
+ .o_survey_page_per_question.o_survey_date,
+ .o_survey_page_per_question.o_survey_datetime {
+ // 'pixel perfect' layouting for choice questions having less than 5 choices in page_per_question mode
+ // we use media queries instead of bootstrap classes because they don't provide everything needed here
+ @media (min-width: 768px) {
+ width: 50%;
+ position: relative;
+ left: 25%;
+ }
+ }
+
.o_survey_question_matrix {
td {
min-width: 100px;
@@ -93,8 +128,13 @@ nav#oe_main_menu_navbar {
display: inline;
}
}
- .o_survey_choice_key > span > span {
- top: 0px;
+ .o_survey_choice_key {
+ left: 10px;
+ right: auto;
+ top: 12px;
+ > span > span {
+ top: 0px;
+ }
}
&.o_survey_selected {
@@ -106,6 +146,33 @@ nav#oe_main_menu_navbar {
}
}
}
+ thead {
+ th:first-child {
+ border-top-left-radius: .25rem;
+ }
+ th:last-child {
+ border-top-right-radius: .25rem;
+ }
+ }
+ tbody tr:last-child {
+ th {
+ border-bottom-left-radius: .25rem;
+ }
+ td:last-child {
+ border-bottom-right-radius: .25rem;
+ }
+ }
+ }
+}
+
+.o_survey_form, .o_survey_session_manage {
+ .o_survey_question_matrix {
+ th {
+ background-color: $primary;
+ }
+ td {
+ background-color: rgba($primary, 0.2);
+ }
}
}
@@ -114,23 +181,19 @@ nav#oe_main_menu_navbar {
**********************************************************/
.o_survey_form {
- min-height: 100%;
-
- .o_survey_progress {
- height:0.5em;
- }
+ min-height: 25rem;
.o_survey_choice_btn {
cursor: pointer;
- background-color: rgba($primary, 0.5);
- border: 2px solid $primary;
+ background-color: rgba($primary, 0.1);
+ box-shadow: $primary 0px 0px 0px 1px;
&.o_survey_selected {
- background-color: $primary;
+ box-shadow: $primary 0px 0px 0px 2px;
}
&:hover {
- background-color: rgba($primary, 0.8);
+ background-color: rgba($primary, 0.3);
.o_survey_choice_key span.o_survey_key {
opacity: 1;
}
@@ -140,36 +203,31 @@ nav#oe_main_menu_navbar {
.o_survey_choice_key {
width: 25px;
height: 25px;
- border: 2px solid $primary;
+ border: 1px solid $primary;
span {
font-size: smaller;
- top: -2px;
+ top: -1px;
&.o_survey_key {
width: inherit;
right: 10px;
- border: 2px solid $primary;
+ border: 1px solid $primary;
border-right: 0px;
height: 25px;
transition: opacity 0.4s ease;
white-space: nowrap;
opacity: 0;
+ span {
+ top: -2px;
+ }
}
}
}
- .o_survey_question_matrix {
- th {
- background-color: $primary;
- }
- 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_question_matrix td:hover {
+ background-color: rgba($primary, 0.5);
+ cursor: pointer;
+ .o_survey_choice_key span.o_survey_key {
+ opacity: 1;
}
}
}
@@ -216,8 +274,8 @@ nav#oe_main_menu_navbar {
height: 100vh;
width: 85vw;
- .js_question-wrapper {
- padding-bottom: 0px;
+ .o_survey_choice_key {
+ display: none;
}
&.pt-6 {
@@ -315,6 +373,8 @@ nav#oe_main_menu_navbar {
background-color: $gray-500;
border-color: transparent;
cursor: default;
+ color: white; // not bootstrap to customize for survey_print only
+ font-weight: bold; // not bootstrap to customize for survey_print only
&.bg-success, &.bg-danger {
opacity: 0.6;
@@ -323,7 +383,7 @@ nav#oe_main_menu_navbar {
background-color: $gray-600;
opacity: 1;
}
- i.fa-square, i.fa-circle {
+ i.fa-square-o, i.fa-circle-thin {
display: none;
}
}
diff --git a/addons/survey/static/tests/tours/survey_session_manage_test.js b/addons/survey/static/tests/tours/survey_session_manage_test.js
new file mode 100644
index 00000000000..acf3d99c945
--- /dev/null
+++ b/addons/survey/static/tests/tours/survey_session_manage_test.js
@@ -0,0 +1,19 @@
+odoo.define('survey.session_manage_test', function (require) {
+"use strict";
+
+var SessionManager = require('survey.session_manage');
+/**
+ * Small override for test/tour purposes.
+ * We trigger the fetch of answer results immediately at the start.
+ * (Instead of wasting 2 seconds waiting after the start).
+ */
+SessionManager.include({
+ start: function () {
+ return this._super.apply(this, arguments)
+ .then(this._refreshResults.bind(this));
+ }
+});
+
+return SessionManager;
+
+});
diff --git a/addons/survey/static/tests/tours/survey_tour_session_create.js b/addons/survey/static/tests/tours/survey_tour_session_create.js
new file mode 100644
index 00000000000..ac0c2f275d9
--- /dev/null
+++ b/addons/survey/static/tests/tours/survey_tour_session_create.js
@@ -0,0 +1,20 @@
+odoo.define('survey.test_survey_session_create_tour', function (require) {
+"use strict";
+
+var tour = require('web_tour.tour');
+var surveySessionTools = require('survey.session_tour_tools');
+
+/**
+ * Small tour that will simply start the session and wait for attendees.
+ */
+tour.register('test_survey_session_create_tour', {
+ url: "/web",
+ test: true,
+}, [].concat(surveySessionTools.accessSurveySteps, [{
+ trigger: 'button[name="action_start_session"]',
+}, {
+ trigger: '.o_survey_session_attendees_count:contains("0")',
+ run: function () {} // check session is correctly started
+}]));
+
+});
diff --git a/addons/survey/static/tests/tours/survey_tour_session_manage.js b/addons/survey/static/tests/tours/survey_tour_session_manage.js
new file mode 100644
index 00000000000..36b5d8dfeca
--- /dev/null
+++ b/addons/survey/static/tests/tours/survey_tour_session_manage.js
@@ -0,0 +1,440 @@
+odoo.define('survey.test_survey_session_manage_tour', function (require) {
+"use strict";
+
+var tour = require('web_tour.tour');
+var surveySessionTools = require('survey.session_tour_tools');
+
+/**
+ * Since the chart is rendered using SVG, we can't use jQuery triggers to check if everything
+ * is correctly rendered.
+ * This helper method returns the chart data (Chartjs framework specific) in the following structure:
+ * [{ value, backgroundColor, labelColor }]
+ */
+var getChartData = function () {
+ var chartData = [];
+ var rootWidget = odoo.__DEBUG__.services['root.widget'];
+ var surveyManagePublicWidget = rootWidget.publicWidgets.find(function (widget) {
+ return widget.$el.hasClass('o_survey_session_manage');
+ });
+
+ if (!surveyManagePublicWidget) {
+ return chartData;
+ }
+
+ surveyManagePublicWidget.resultsChart.chart.data.datasets[0].data.forEach(function (value, index) {
+ chartData.push({
+ value: value,
+ backgroundColor: surveyManagePublicWidget.resultsChart._getBackgroundColor({dataIndex: index}),
+ labelColor: surveyManagePublicWidget.resultsChart._getLabelColor({dataIndex: index}),
+ });
+ });
+
+ return chartData;
+};
+
+var nextScreen = function () {
+ var e = $.Event('keydown');
+ e.keyCode = 39; // arrow-right
+ $(document).trigger(e);
+};
+
+var previousScreen = function () {
+ var e = $.Event('keydown');
+ e.keyCode = 37; // arrow-left
+ $(document).trigger(e);
+};
+
+var REGULAR_ANSWER_COLOR = '#212529';
+var CORRECT_ANSWER_COLOR = '#2CBB70';
+var WRONG_ANSWER_COLOR = '#D9534F';
+
+/**
+ * A 'regular' answer is an answer that is nor correct, nor incorrect.
+ * The check is based on the specific opacity (0.8) and color of those answers.
+ */
+var isRegularAnswer = function (answer) {
+ return answer.backgroundColor.includes('0.8') &&
+ answer.labelColor === REGULAR_ANSWER_COLOR;
+};
+
+/**
+ * The check is based on the specific opacity (0.8) and color of correct answers.
+ */
+var isCorrectAnswer = function (answer) {
+ return answer.backgroundColor.includes('0.8') &&
+ answer.labelColor === CORRECT_ANSWER_COLOR;
+};
+
+/**
+ * The check is based on the specific opacity (0.3) and color of incorrect answers.
+ */
+var isIncorrectAnswer = function (answer) {
+ return answer.backgroundColor.includes('0.3') &&
+ answer.labelColor === WRONG_ANSWER_COLOR;
+};
+
+/**
+ * Tour that will test the whole survey session from the host point of view.
+ *
+ * Break down of the main points:
+ * - Open the 'session manager' (the session was already created by a previous tour)
+ * - Display the nickname question, and move to the next one (as answers are not displayed)
+ * - Check answers are correctly displayed for the 3 'simple' question types (text, date, datetime)
+ * - Move to the choice question and check that answers are displayed
+ * (The check is rather complex, see 'getChartData' for details)
+ * - If everything is correctly displayed, move to the next question
+ * - On the scored choice question, check that the screens are correctly chained:
+ * no results displayed -> results displayed -> correct/incorrect answers -> leaderboard
+ * - On the scored + timed multiple choice question, check the same than previous question,
+ * except that the results are supposed to be displayed automatically when the question timer runs out
+ * - Test the 'back' behavior and check that screens are reversed correctly
+ * - Check that our final leaderboard is correct based on attendees answers
+ * - Close the survey session
+ */
+tour.register('test_survey_session_manage_tour', {
+ url: "/web",
+ test: true,
+}, [].concat(surveySessionTools.accessSurveySteps, [{
+ trigger: 'button[name="action_open_session_manager"]',
+}, {
+ trigger: 'h1:contains("Nickname")',
+ run: function () {} // check nickname question is displayed
+}, {
+ trigger: 'h1',
+ run: nextScreen
+}, {
+ trigger: 'h1:contains("Text Question")',
+ run: function () {} // check text question is displayed
+}, {
+ trigger: '.o_survey_session_progress_small:contains("3 / 3")',
+ run: function () {} // check we have 3 answers
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("Attendee 1 is the best")',
+ run: function () {} // check attendee 1 answer is displayed
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("Attendee 2 rulez")',
+ run: function () {} // check attendee 2 answer is displayed
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("Attendee 3 will crush you")',
+ run: function () {} // check attendee 3 answer is displayed
+}, {
+ trigger: 'h1',
+ run: nextScreen
+}, {
+ trigger: '.o_survey_session_progress_small:contains("2 / 3")',
+ run: function () {} // check we have 2 answers
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("10/10/2010")',
+ run: function () {} // check attendee 1 answer is displayed
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("11/11/2011")',
+ run: function () {} // check attendee 2 answer is displayed
+}, {
+ trigger: 'h1',
+ run: nextScreen
+}, {
+ trigger: '.o_survey_session_progress_small:contains("2 / 3")',
+ run: function () {} // check we have 2 answers
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("10/10/2010 10:00:00")',
+ run: function () {} // check attendee 2 answer is displayed
+}, {
+ trigger: '.o_survey_session_text_answer_container:contains("11/11/2011 15:55:55")',
+ run: function () {} // check attendee 3 answer is displayed
+}, {
+ trigger: 'h1',
+ run: nextScreen
+}, {
+ trigger: 'h1:contains("Regular Simple Choice")',
+ run: function () {
+ var chartData = getChartData();
+ if (chartData.length !== 3) {
+ console.error('Chart data should contain 3 records!');
+ return;
+ }
+
+ var firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 2 || !isRegularAnswer(firstAnswerData)) {
+ console.error('First answer should be picked by 2 users!');
+ return;
+ }
+
+ var secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 1 || !isRegularAnswer(secondAnswerData)) {
+ console.error('Second answer should be picked by 1 user!');
+ return;
+ }
+
+ var thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 0 || !isRegularAnswer(thirdAnswerData)) {
+ console.error('Third answer should be picked by no users!');
+ return;
+ }
+
+ nextScreen();
+ }
+}, {
+ trigger: 'h1:contains("Scored Simple Choice")',
+ run: function () {
+ var chartData = getChartData();
+ if (chartData.length !== 4) {
+ console.error('Chart data should contain 4 records!');
+ return;
+ }
+
+ for (var i = 0; i < chartData.length; i++) {
+ if (chartData[i].value !== 0) {
+ console.error(
+ 'Chart data should all be 0 because "next screen" that shows ' +
+ 'answers values is not triggered yet!');
+ return;
+ }
+ }
+
+ nextScreen();
+ chartData = getChartData();
+
+ var firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 1 || !isRegularAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 1 user and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ var secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 1 || !isRegularAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 1 user and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ var thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isRegularAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ var fourthAnswerData = chartData[3];
+ if (fourthAnswerData.value !== 0 || !isRegularAnswer(fourthAnswerData)) {
+ console.error(
+ 'Fourth answer should be picked by no users and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ nextScreen();
+ chartData = getChartData();
+
+ firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 1 || !isCorrectAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 1 user and it should be correct!'
+ );
+ return;
+ }
+
+ secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 1 || !isIncorrectAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 1 user and it should be incorrect!'
+ );
+ return;
+ }
+
+ thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isIncorrectAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and it should be incorrect!'
+ );
+ return;
+ }
+
+ fourthAnswerData = chartData[3];
+ if (fourthAnswerData.value !== 0 || !isIncorrectAnswer(fourthAnswerData)) {
+ console.error(
+ 'Fourth answer should be picked by no users and it should be incorrect!'
+ );
+ return;
+ }
+
+ nextScreen();
+ nextScreen();
+ }
+}, {
+ trigger: 'h1:contains("Timed Scored Multiple Choice")',
+ run: function () {
+ var chartData = getChartData();
+ if (chartData.length !== 3) {
+ console.error('Chart data should contain 4 records!');
+ return;
+ }
+
+ for (var i = 0; i < chartData.length; i++) {
+ if (chartData[i].value !== 0) {
+ console.error(
+ 'Chart data should all be 0 because "next screen" that shows ' +
+ 'answers values is not triggered yet!');
+ return;
+ }
+ }
+
+ // after 1 second, results are displayed automatically because question timer
+ // runs out
+ setTimeout(function () {
+ chartData = getChartData();
+ var firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 2 || !isRegularAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 2 users and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ var secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 2 || !isRegularAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 2 users and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ var thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isRegularAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and its correctness should not be shown yet!'
+ );
+ return;
+ }
+
+ nextScreen();
+ chartData = getChartData();
+
+ firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 2 || !isCorrectAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 2 users and it should be correct!'
+ );
+ return;
+ }
+
+ secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 2 || !isCorrectAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 2 users and it should be correct!'
+ );
+ return;
+ }
+
+ thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isIncorrectAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and it should be incorrect!'
+ );
+ return;
+ }
+
+ nextScreen();
+ }, 1500);
+ }
+}, {
+ trigger: 'h1:contains("Final Leaderboard")',
+ run: function () {} // Final Leaderboard is displayed
+}, {
+ trigger: 'h1',
+ run: function () {
+ // previous screen testing
+ previousScreen();
+ var chartData = getChartData();
+
+ var firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 2 || !isCorrectAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 2 users and it should be correct!'
+ );
+ return;
+ }
+
+ var secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 2 || !isCorrectAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 2 users and it should be correct!'
+ );
+ return;
+ }
+
+ var thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isIncorrectAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and it should be incorrect!'
+ );
+ return;
+ }
+
+ previousScreen();
+ chartData = getChartData();
+
+ firstAnswerData = chartData[0];
+ if (firstAnswerData.value !== 2 || !isRegularAnswer(firstAnswerData)) {
+ console.error(
+ 'First answer should be picked by 2 users and its correctness should not be shown!'
+ );
+ return;
+ }
+
+ secondAnswerData = chartData[1];
+ if (secondAnswerData.value !== 2 || !isRegularAnswer(secondAnswerData)) {
+ console.error(
+ 'Second answer should be picked by 2 users and its correctness should not be shown!'
+ );
+ return;
+ }
+
+ thirdAnswerData = chartData[2];
+ if (thirdAnswerData.value !== 1 || !isRegularAnswer(thirdAnswerData)) {
+ console.error(
+ 'Third answer should be picked by 1 user and its correctness should not be shown!'
+ );
+ return;
+ }
+
+ previousScreen();
+ chartData = getChartData();
+
+ for (var i = 0; i < chartData.length; i++) {
+ if (chartData[i].value !== 0) {
+ console.error(
+ 'Chart data should all be 0 because "next screen" that shows ' +
+ 'answers values is not triggered yet!');
+ return;
+ }
+ }
+
+ // Now we go forward to the "Final Leaderboard" again (4 times)
+ for (i = 0; i < 4; i++) {
+ nextScreen();
+ }
+ }
+}, {
+ trigger: 'h1:contains("Final Leaderboard")',
+ run: function () {} // Final Leaderboard is displayed
+}, {
+ trigger: '.o_survey_session_leaderboard_item:contains("azerty"):contains("1."):contains("40")',
+ run: function () {} // 'azerty' is first with 40 points
+}, {
+ trigger: '.o_survey_session_leaderboard_item:contains("xxxTheB"):contains("2."):contains("20")',
+ run: function () {} // 'xxxTheBestxxx' is second with 20 points
+}, {
+ trigger: '.o_survey_session_leaderboard_item:contains("nicktalope"):contains("3."):contains("10")',
+ run: function () {} // 'nicktalope' is third with 10 points
+}, {
+ trigger: '.o_survey_session_close:has("i.fa-close")'
+}, {
+ trigger: 'button[name="action_start_session"]',
+ run: function () {} // check that we can start another session
+}]));
+
+});
diff --git a/addons/survey/static/tests/tours/survey_tour_session_start.js b/addons/survey/static/tests/tours/survey_tour_session_start.js
new file mode 100644
index 00000000000..309ce0afaeb
--- /dev/null
+++ b/addons/survey/static/tests/tours/survey_tour_session_start.js
@@ -0,0 +1,32 @@
+odoo.define('survey.test_survey_session_start_tour', function (require) {
+"use strict";
+
+var tour = require('web_tour.tour');
+var surveySessionTools = require('survey.session_tour_tools');
+
+/**
+ * Small tour that will open the session manager and check
+ * that the attendees are accounted for, then start the session
+ * by going to the first question.
+ */
+tour.register('test_survey_session_start_tour', {
+ url: "/web",
+ test: true,
+}, [].concat(surveySessionTools.accessSurveySteps, [{
+ trigger: 'button[name="action_open_session_manager"]',
+}, {
+ trigger: '.o_survey_session_attendees_count:contains("3")',
+ run: function () {} // check attendees count
+}, {
+ trigger: 'h1',
+ run: function () {
+ var e = $.Event('keydown');
+ e.keyCode = 39; // arrow-right
+ $(document).trigger(e); // start session
+ }
+}, {
+ trigger: 'h1:contains("Nickname")',
+ run: function () {} // check first question is displayed
+}]));
+
+});
diff --git a/addons/survey/static/tests/tours/survey_tour_session_tools.js b/addons/survey/static/tests/tours/survey_tour_session_tools.js
new file mode 100644
index 00000000000..a42d72e26cf
--- /dev/null
+++ b/addons/survey/static/tests/tours/survey_tour_session_tools.js
@@ -0,0 +1,21 @@
+odoo.define('survey.session_tour_tools', function (require) {
+'use strict';
+
+var tour = require('web_tour.tour');
+
+/**
+ * Tool that gathers common steps to every 'survey session' tours.
+ */
+return {
+ accessSurveySteps: [tour.stepUtils.showAppsMenuItem(), {
+ trigger: '.o_app[data-menu-xmlid="survey.menu_surveys"]',
+ edition: 'community'
+ }, {
+ trigger: '.o_app[data-menu-xmlid="survey.menu_surveys"]',
+ edition: 'enterprise'
+ }, {
+ trigger: '.oe_kanban_card:contains("User Session Survey")',
+ }]
+};
+
+});
diff --git a/addons/survey/tests/__init__.py b/addons/survey/tests/__init__.py
index 76df1c8efa8..a7d6dc8d60a 100644
--- a/addons/survey/tests/__init__.py
+++ b/addons/survey/tests/__init__.py
@@ -10,5 +10,6 @@ from . import test_survey_security
from . import test_survey_randomize
from . import test_survey_ui_certification
from . import test_survey_ui_feedback
+from . import test_survey_ui_session
from . import test_survey_compute_pages_questions
from . import test_certification_badge
diff --git a/addons/survey/tests/test_survey_ui_session.py b/addons/survey/tests/test_survey_ui_session.py
new file mode 100644
index 00000000000..53dd81ea62b
--- /dev/null
+++ b/addons/survey/tests/test_survey_ui_session.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from dateutil.relativedelta import relativedelta
+
+from odoo import fields
+from odoo.tests.common import tagged, HttpCase
+
+
+@tagged('post_install', '-at_install')
+class TestUiSession(HttpCase):
+ def test_admin_survey_session(self):
+ """ This method tests a full 'survey session' flow.
+ Break down of different steps:
+ - Create the test data
+ - A scored survey
+ - A nickname question
+ - "Simple" type questions (text, date, datetime)
+ - A regular simple choice
+ - A scored simple choice
+ - A scored AND timed multiple choice
+ - Create a new survey session
+ - Register 3 attendees to it
+ - Open the session manager to check that our attendees are accounted for
+ - Create some answers to our survey questions.
+ - Then run the 'big' manage session tour (see JS doc for details)
+ - And finally check that our session and attendees inputs are correctly closed. """
+
+ # =======================
+ # CREATE SURVEY TEST DATA
+ # =======================
+
+ test_start_time = fields.Datetime.now()
+
+ survey_session = self.env['survey.survey'].create({
+ 'title': 'User Session Survey',
+ 'access_token': 'b137640d-14d4-4748-9ef6-344caaaaafe',
+ 'state': 'open',
+ 'access_mode': 'public',
+ 'users_can_go_back': False,
+ 'questions_layout': 'page_per_question',
+ 'scoring_type': 'scoring_without_answers'
+ })
+
+ nickname_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Nickname',
+ 'save_as_nickname': True,
+ 'sequence': 1,
+ 'question_type': 'char_box',
+ })
+ text_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Text Question',
+ 'sequence': 2,
+ 'question_type': 'char_box',
+ })
+ date_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Date Question',
+ 'sequence': 3,
+ 'question_type': 'date',
+ })
+ datetime_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Datetime Question',
+ 'sequence': 4,
+ 'question_type': 'datetime',
+ })
+ simple_choice_answer_1 = self.env['survey.question.answer'].create({
+ 'value': 'First'
+ })
+ simple_choice_answer_2 = self.env['survey.question.answer'].create({
+ 'value': 'Second'
+ })
+ simple_choice_answer_3 = self.env['survey.question.answer'].create({
+ 'value': 'Third'
+ })
+ simple_choice_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Regular Simple Choice',
+ 'sequence': 5,
+ 'question_type': 'simple_choice',
+ 'suggested_answer_ids': [
+ (4, simple_choice_answer_1.id),
+ (4, simple_choice_answer_2.id),
+ (4, simple_choice_answer_3.id)],
+ })
+ scored_choice_answer_1 = self.env['survey.question.answer'].create({
+ 'value': 'Correct',
+ 'is_correct': True,
+ 'answer_score': 30
+ })
+ scored_choice_answer_2 = self.env['survey.question.answer'].create({
+ 'value': 'Incorrect 1'
+ })
+ scored_choice_answer_3 = self.env['survey.question.answer'].create({
+ 'value': 'Incorrect 2'
+ })
+ scored_choice_answer_4 = self.env['survey.question.answer'].create({
+ 'value': 'Incorrect 3'
+ })
+ scored_choice_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Scored Simple Choice',
+ 'sequence': 6,
+ 'question_type': 'simple_choice',
+ 'suggested_answer_ids': [
+ (4, scored_choice_answer_1.id),
+ (4, scored_choice_answer_2.id),
+ (4, scored_choice_answer_3.id),
+ (4, scored_choice_answer_4.id)],
+ })
+ timed_scored_choice_answer_1 = self.env['survey.question.answer'].create({
+ 'value': 'Correct',
+ 'is_correct': True,
+ 'answer_score': 30
+ })
+ timed_scored_choice_answer_2 = self.env['survey.question.answer'].create({
+ 'value': 'Also correct but less points',
+ 'is_correct': True,
+ 'answer_score': 10
+ })
+ timed_scored_choice_answer_3 = self.env['survey.question.answer'].create({
+ 'value': 'Incorrect',
+ 'answer_score': -40
+ })
+ timed_scored_choice_question = self.env['survey.question'].create({
+ 'survey_id': survey_session.id,
+ 'title': 'Timed Scored Multiple Choice',
+ 'sequence': 6,
+ 'question_type': 'multiple_choice',
+ 'is_time_limited': True,
+ 'time_limit': 1,
+ 'suggested_answer_ids': [
+ (4, timed_scored_choice_answer_1.id),
+ (4, timed_scored_choice_answer_2.id),
+ (4, timed_scored_choice_answer_3.id)],
+ })
+
+ # =======================
+ # PART 1 : CREATE SESSION
+ # =======================
+
+ self.start_tour('/web', 'test_survey_session_create_tour', login='admin')
+
+ # tricky part: we only take into account answers created after the session_start_time
+ # the create_date of the answers we just saved is set to the beginning of the test.
+ # but the session_start_time is set after that.
+ # So we cheat on the session start date to be able to count answers properly.
+ survey_session.write({'session_start_time': test_start_time - relativedelta(minutes=10)})
+
+ attendee_1 = survey_session._create_answer()
+ attendee_2 = survey_session._create_answer()
+ attendee_3 = survey_session._create_answer()
+ all_attendees = [attendee_1, attendee_2, attendee_3]
+
+ self.assertEqual('ready', survey_session.session_state)
+ self.assertTrue(all(attendee.is_session_answer for attendee in all_attendees),
+ "Created answers should be within the session.")
+ self.assertTrue(all(attendee.state == 'new' for attendee in all_attendees),
+ "Created answers should be in the 'new' state.")
+
+ # =========================================
+ # PART 2 : OPEN SESSION AND CHECK ATTENDEES
+ # =========================================
+
+ self.start_tour('/web', 'test_survey_session_start_tour', login='admin')
+
+ self.assertEqual('in_progress', survey_session.session_state)
+ self.assertTrue(bool(survey_session.session_start_time))
+
+ # ========================================
+ # PART 3 : CREATE ANSWERS & MANAGE SESSION
+ # ========================================
+
+ # create a few answers beforehand to avoid having to back and forth too
+ # many times between the tours and the python test
+
+ attendee_1.save_lines(nickname_question, 'xxxTheBestxxx')
+ attendee_2.save_lines(nickname_question, 'azerty')
+ attendee_3.save_lines(nickname_question, 'nicktalope')
+ self.assertEqual('xxxTheBestxxx', attendee_1.nickname)
+ self.assertEqual('azerty', attendee_2.nickname)
+ self.assertEqual('nicktalope', attendee_3.nickname)
+
+ attendee_1.save_lines(text_question, 'Attendee 1 is the best')
+ attendee_2.save_lines(text_question, 'Attendee 2 rulez')
+ attendee_3.save_lines(text_question, 'Attendee 3 will crush you')
+ attendee_1.save_lines(date_question, '2010-10-10')
+ attendee_2.save_lines(date_question, '2011-11-11')
+ attendee_2.save_lines(datetime_question, '2010-10-10 10:00:00')
+ attendee_3.save_lines(datetime_question, '2011-11-11 15:55:55')
+ attendee_1.save_lines(simple_choice_question, simple_choice_answer_1.id)
+ attendee_2.save_lines(simple_choice_question, simple_choice_answer_1.id)
+ attendee_3.save_lines(simple_choice_question, simple_choice_answer_2.id)
+ attendee_1.save_lines(scored_choice_question, scored_choice_answer_1.id)
+ attendee_2.save_lines(scored_choice_question, scored_choice_answer_2.id)
+ attendee_3.save_lines(scored_choice_question, scored_choice_answer_3.id)
+ attendee_1.save_lines(timed_scored_choice_question,
+ [timed_scored_choice_answer_1.id, timed_scored_choice_answer_3.id])
+ attendee_2.save_lines(timed_scored_choice_question,
+ [timed_scored_choice_answer_1.id, timed_scored_choice_answer_2.id])
+ attendee_3.save_lines(timed_scored_choice_question,
+ [timed_scored_choice_answer_2.id])
+
+ self.start_tour('/web', 'test_survey_session_manage_tour', login='admin')
+
+ self.assertFalse(bool(survey_session.session_state))
+ self.assertTrue(all(answer.state == 'done' for answer in all_attendees))
diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml
index 19aaac1a2a0..a05c8f9340e 100644
--- a/addons/survey/views/assets.xml
+++ b/addons/survey/views/assets.xml
@@ -58,6 +58,11 @@
<script type="text/javascript" src="/survey/static/tests/tours/certification_success.js"></script>
<script type="text/javascript" src="/survey/static/tests/tours/survey.js"></script>
<script type="text/javascript" src="/survey/static/tests/tours/survey_prefill.js"></script>
+ <script type="text/javascript" src="/survey/static/tests/tours/survey_tour_session_tools.js"></script>
+ <script type="text/javascript" src="/survey/static/tests/tours/survey_tour_session_create.js"></script>
+ <script type="text/javascript" src="/survey/static/tests/tours/survey_tour_session_start.js"></script>
+ <script type="text/javascript" src="/survey/static/tests/tours/survey_tour_session_manage.js"></script>
+ <script type="text/javascript" src="/survey/static/tests/tours/survey_session_manage_test.js"></script>
</xpath>
</template>
</data>
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
index 90dde68c632..0be4d340b02 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -14,11 +14,11 @@
<button name="action_result_survey" string="See results" type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('state', '=', 'draft'), ('answer_done_count', '&lt;=', 0)]}"/>
<button name="action_start_session" string="Create Live Session" type="object"
- attrs="{'invisible': ['|', ('session_state', '!=', False), ('state', '=', 'draft')]}" />
+ attrs="{'invisible': ['|', ('session_state', '!=', False), '|', ('state', '!=', 'open'), ('certification', '=', True)]}" />
<button name="action_open_session_manager" string="Open Session Manager" type="object"
attrs="{'invisible': ['|', ('session_state', '=', False), ('state', '=', 'draft')]}" />
<button name="action_end_session" string="Close Live Session" type="object"
- attrs="{'invisible': ['|', ('session_state', '!=', 'in_progress'), ('state', '=', 'draft')]}" />
+ attrs="{'invisible': ['|', ('session_state', 'not in', ['ready', 'in_progress']), ('state', '=', 'draft')]}" />
<button name="action_draft" string="Set to draft" type="object" states="closed"/>
<button name="action_test_survey" string="Test" type="object" attrs="{'invisible': ['|', ('state', '=', 'closed'), ('id', '=', False)]}"/>
<button name="action_print_survey" string="Print" type="object" attrs="{'invisible': [('id', '=', False)]}"/>
@@ -135,6 +135,14 @@
</group>
<group string="Live Session">
<field name="session_speed_rating" />
+ <div class="o_td_label">
+ <label for="session_code" string="Session Code" class="oe_edit_only o_form_label" />
+ <label for="session_link" string="Session Link" class="oe_read_only o_form_label" />
+ </div>
+ <div class="d-flex">
+ <field name="session_code" nolabel="1" class="mr-2 oe_edit_only" />
+ <field name="session_link" nolabel="1" widget="CopyClipboardChar" class="oe_read_only" />
+ </div>
</group>
</group>
</page>
diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml
index b8addaaab7f..7a008e7e162 100644
--- a/addons/survey/views/survey_templates.xml
+++ b/addons/survey/views/survey_templates.xml
@@ -21,12 +21,27 @@
<div id="wrap" class="oe_structure oe_empty"/>
</xpath>
<xpath expr="//footer" position="after">
- <div class="py-3">
- <div class="float-right px-5 py-2 bg-white rounded-left">
- <t t-call="web.brand_promotion_message">
+ <div class="py-3 m-0 p-0 text-right">
+ <div class="o_survey_progress_wrapper d-inline-block pr-5 text-left">
+ <t t-call="survey.survey_progression"
+ t-if="survey and survey.questions_layout != 'one_page' and answer and answer.state == 'in_progress' and (not question or not question.is_page)">
+ <t t-if="survey.questions_layout == 'page_per_section'">
+ <t t-set="page_ids" t-value="survey.page_ids.ids"/>
+ <t t-set="page_number" t-value="page_ids.index(page.id) + (1 if survey.progression_mode == 'number' else 0)"/>
+ </t>
+ <t t-else="">
+ <t t-set="page_ids" t-value="survey.question_ids.ids"/>
+ <t t-set="page_number" t-value="page_ids.index(question.id)"/>
+ </t>
+ </t>
+ </div>
+ <div class="o_survey_brand_message float-right rounded mr-3 border">
+ <div class="px-2 py-2 d-inline-block" t-call="web.brand_promotion_message">
<t t-set="_message"></t>
<t t-set="_utm_medium" t-valuef="survey"/>
- </t>
+ </div>
+ <div class="o_survey_navigation_wrapper d-inline-block" t-call="survey.survey_navigation">
+ </div>
</div>
</div>
</xpath>
@@ -35,9 +50,9 @@
<!-- Main survey template -->
<template id="survey_page_fill" name="Survey: main page (take survey)">
<t t-call="survey.layout">
- <div class="wrap o_survey_wrap mt16 pb16 d-flex">
- <div class="container o_survey_form d-flex flex-column">
- <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
+ <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
+ <div class="wrap o_survey_wrap d-flex align-items-center">
+ <div class="container o_survey_form d-flex flex-column mb-5">
<t t-call="survey.survey_fill_header" />
<t t-call="survey.survey_fill_form" />
@@ -47,10 +62,13 @@
</template>
<template id="survey_fill_header" name="Survey: main page header">
- <div class="o_survey_nav pt16 pb24">
+ <div class="o_survey_nav pt16 mb-2">
<div class="container m-0 p-0">
<div class="row">
- <div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
+ <div class="col-lg-8">
+ <h1 t-if="answer.state == 'new' or survey.questions_layout != 'page_per_question'"
+ t-esc="survey.title" class="o_survey_main_title"></h1>
+ </div>
<div class="o_survey_timer col-lg-4">
<h1 class="o_survey_timer_container timer text-right">
</h1>
@@ -67,9 +85,10 @@
<template id="survey_fill_form" name="Survey: main page content">
<t t-set="survey_form_readonly" t-value="answer.state == 'done'"/>
<form role="form" method="post" t-att-name="survey.id"
- class="d-flex flex-grow-1 align-items-center"
+ class="d-flex flex-grow-1"
t-att-data-answer-token="answer.access_token"
t-att-data-survey-token="survey.access_token"
+ t-att-data-users-can-go-back="survey.users_can_go_back"
t-att-data-session-in-progress="answer.is_session_answer"
t-att-data-is-start-screen="answer.state == 'new'"
t-att-data-readonly="survey_form_readonly"
@@ -107,10 +126,10 @@
</template>
<template id="survey_fill_form_start" name="Survey: start form content">
- <div class="wrap o_survey_start pb256">
- <div class='mt32 mb32'>
- <div t-field='survey.description' class="oe_no_empty"/>
- <t t-if="survey.session_state == 'ready'">
+ <div class="wrap o_survey_start">
+ <div class='mb32'>
+ <div t-field='survey.description' class="oe_no_empty pb-5"/>
+ <t t-if="answer.is_session_answer">
<div class="font-weight-bold">
The session will begin automatically when the host starts.
</div>
@@ -130,7 +149,7 @@
Start Survey
</t>
</button>
- <span class="o_survey_enter font-weight-bold ml-2">or press Enter</span>
+ <span class="o_survey_enter font-weight-bold text-muted ml-2">or press Enter</span>
</t>
</div>
</div>
@@ -152,7 +171,7 @@
<div class="text-center mt16 mb256">
<button type="submit" value="finish" class="btn btn-primary">
- Submit</button><span class="font-weight-bold ml-2 d-none d-md-inline">or press Enter</span>
+ Submit</button><span class="font-weight-bold text-muted ml-2 d-none d-md-inline">or press Enter</span>
</div>
</t>
@@ -166,78 +185,59 @@
</t>
<div class="row">
- <div class="offset-3 col-6 text-center mt16 mb128">
- <button t-if="survey.users_can_go_back and page != survey.page_ids[0]" type="submit" class="btn btn-secondary"
- name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
+ <div class="offset-3 col-6 text-center mt16">
<button type="submit" t-att-value="'next' if not survey_last else 'finish'" class="btn btn-primary">
- Continue</button><span class="font-weight-bold ml-2 d-none d-md-inline"> or press Enter</span>
- </div>
- <div class="col-3">
- <div class="float-right">
- <t t-call="survey.survey_progression">
- <t t-set="page_ids" t-value="survey.page_ids.ids"/>
- <t t-set="page_number" t-value="page_ids.index(page.id) + (1 if survey.progression_mode == 'number' else 0)"/>
- </t>
- </div>
+ <t t-if="not survey_last">Continue</t>
+ <t t-else="">Submit</t>
+ </button><span class="font-weight-bold text-muted ml-2 d-none d-md-inline"> or press Enter</span>
</div>
</div>
</t>
- <t t-if="survey.questions_layout == 'page_per_question'">
+ <!-- If we have a choice question and less than 6 options, we want minimized display.
+ Minimized display means we display the choices vertically (instead of optimized based on screen space).
+ An exception is made for options with images, where we always want to optimize screen space.
+ Numeric, date and datetime questions are also displayed "minimized", with a smaller screen width.-->
+ <t t-set="minimized_display" t-value="survey.questions_layout == 'page_per_question' and len(question.suggested_answer_ids) &lt;= 5 and not any(suggestion.value_image for suggestion in question.suggested_answer_ids)" />
+ <div t-if="survey.questions_layout == 'page_per_question'"
+ t-attf-class="o_survey_page_per_question o_survey_#{question.question_type} #{'o_survey_minimized_display' if minimized_display else ''}">
<input type="hidden" name="question_id" t-att-value="question.id" />
<!-- User has already answered for this session -->
<t t-if="answer.is_session_answer and (has_answered or answer.question_time_limit_reached)">
<fieldset disabled="disabled">
<t t-set="survey_form_readonly" t-value="True" />
- <t t-call="survey.question_container" />
+ <div class="mt-5">
+ <t t-call="survey.question_container" />
+ </div>
</fieldset>
- <div t-if="answer.question_time_limit_reached and not has_answered" class="font-weight-bold mb128">Sorry, you have not been fast enough.</div>
- <div t-else="" class="font-weight-bold mb128">We have registered your answer! Please wait for the host to go to the next question.</div>
+ <div t-if="answer.question_time_limit_reached and not has_answered" class="font-weight-bold">Sorry, you have not been fast enough.</div>
+ <div t-else="" class="font-weight-bold">We have registered your answer! Please wait for the host to go to the next question.</div>
</t>
<t t-elif="answer.is_session_answer and question.is_page and not is_html_empty(question.description)">
<div class="font-weight-bold mt-5">Pay attention to the host screen until the next question.</div>
</t>
<t t-else="">
- <t t-call="survey.question_container"/>
+ <div class="mt-5">
+ <t t-call="survey.question_container"/>
+ </div>
<div class="row">
- <div class="offset-3 col-6 text-center mt16 mb128">
- <!-- Can go back if:
- - Back button is allowed in configuration
- - This is not a session answer (doesn't make sense to go back in session context)
- - This is not the first question
- - The survey doesn't have pages OR this is not the first page of the survey
- (pages are displayed in 'page_per_question' layout when they have a description, see PR#44271)
- -->
- <button t-if="survey.users_can_go_back and not answer.is_session_answer and question != answer.predefined_question_ids[0] and (not survey.page_ids or question != survey.page_ids[0])"
- type="submit" class="btn btn-secondary" name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Back</button>
+ <div class="offset-3 col-6 text-center mt16">
<button type="submit" t-att-value="'next' if not survey_last else 'finish'" class="btn btn-primary">
- <t t-if="answer.is_session_answer">
- Submit
- </t>
- <t t-else="">
- Continue
- </t>
+ <t t-if="answer.is_session_answer or survey_last">Submit</t>
+ <t t-else="">Continue</t>
</button>
- <span class="font-weight-bold ml-2 d-none d-md-inline">or press Enter</span>
- </div>
- <div class="col-3">
- <div class="float-right">
- <t t-if="not question.is_page" 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(question.id)"/>
- </t>
- </div>
+ <span class="font-weight-bold text-muted ml-2 d-none d-md-inline">or press Enter</span>
</div>
</div>
</t>
- </t>
+ </div>
</template>
<!-- Finished (taken and finished) survey page -->
<template id="survey_fill_form_done" name="Survey: finished">
<div class="wrap">
- <div class="o_survey_finished mt32 mb32 pb256">
+ <div class="o_survey_finished mt32 mb32">
<h1>Thank you!</h1>
<div t-field="survey.description_done" class="oe_no_empty" />
<div class="row">
@@ -298,15 +298,19 @@
<!--Use Key selection if number of choices is < 26 to keep Z for other choice if any-->
<t t-set="letters" t-value="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<t t-set="useKeySelection" t-value="len(question.suggested_answer_ids) &lt; len(letters) and survey.questions_layout == 'page_per_question'"/>
- <div t-att-class="'js_question-wrapper pb-4 %s' % ('d-none' if not display_question else '')"
+ <!-- Extra 'right' margin is added on layouts that are not "page_per_question" to align with choices questions, since all choices have a mr-2 class (pixel perfect yay...) -->
+ <t t-set="extra_right_margin" t-value="survey.questions_layout != 'page_per_question' and question.question_type not in ['simple_choice', 'multiple_choice']"/>
+ <div t-att-class="'js_question-wrapper pb-4 %s %s' % ('d-none' if not display_question else '', 'mr-2' if extra_right_margin else '')"
t-att-id="question.id" t-att-data-required="question.constr_mandatory"
t-att-data-constr-error-msg="question.constr_error_msg"
t-att-data-validation-error-msg="question.validation_error_msg">
- <h4>
- <span t-field='question.title' />
- <span t-if="question.constr_mandatory" class="text-danger">*</span>
- </h4>
- <div t-field='question.description' class="text-muted oe_no_empty"/>
+ <div class="mb-4">
+ <h3 t-if="not hide_question_title">
+ <span t-field='question.title' />
+ <span t-if="question.constr_mandatory" class="text-danger">*</span>
+ </h3>
+ <div t-if="not is_html_empty(question.description)" t-field='question.description' class="text-muted oe_no_empty mt-1"/>
+ </div>
<t t-if="question.question_type == 'text_box'"><t t-call="survey.question_text_box"/></t>
<t t-if="question.question_type == 'char_box'"><t t-call="survey.question_char_box"/></t>
<t t-if="question.question_type == 'numerical_box'"><t t-call="survey.question_numerical_box"/></t>
@@ -320,16 +324,16 @@
</template>
<template id="question_text_box" name="Question: free text box">
- <div class="o_survey_comment_container py-0 px-1">
- <textarea class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0" rows="3" t-att-name="question.id"
+ <div class="o_survey_comment_container p-0">
+ <textarea class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0" rows="3" t-att-name="question.id"
t-att-data-question-type="question.question_type"><t t-if="answer_lines" t-esc="answer_lines[0].value_text_box or None"/></textarea>
</div>
</template>
<template id="question_char_box" name="Question: text box">
- <div class="o_survey_comment_container py-0 px-1">
+ <div class="o_survey_comment_container p-0">
<input t-att-type="'email' if question.validation_email else 'text'"
- class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0" t-att-name="question.id"
+ class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0" t-att-name="question.id"
t-att-value="answer_lines[0].value_char_box if answer_lines else None"
t-att-data-question-type="question.question_type"
t-att-data-validation-length-min="question.validation_length_min if question.validation_required else False"
@@ -338,7 +342,8 @@
</template>
<template id="question_numerical_box" name="Question: numerical box">
- <input type="number" step="any" class="form-control o_survey_question_numerical_box font-weight-bold bg-transparent text-primary rounded-0" t-att-name="question.id" t-att-value="answer_lines[0].value_numerical_box if answer_lines else None"
+ <input type="number" step="any" class="form-control o_survey_question_numerical_box bg-transparent text-dark rounded-0 p-0"
+ t-att-name="question.id" t-att-value="answer_lines[0].value_numerical_box if answer_lines else None"
t-att-data-question-type="question.question_type"
t-att-data-validation-float-min="question.validation_min_float_value if question.validation_required else False"
t-att-data-validation-float-max="question.validation_max_float_value if question.validation_required else False"/>
@@ -348,12 +353,12 @@
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_date"
t-att-data-maxdate="question.validation_max_date">
- <input type="text" class="form-control datetimepicker-input o_survey_question_date font-weight-bold bg-transparent text-primary rounded-0"
+ <input type="text" class="form-control datetimepicker-input o_survey_question_date bg-transparent text-dark rounded-0 p-0"
t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="question.id"
t-att-value="format_date(answer_lines[0].value_date) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="input-group-append position-absolute" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
- <div class="input-group-text text-primary border-0 bg-transparent"><i class="fa fa-calendar"></i></div>
+ <div class="input-group-text text-primary border-0 bg-transparent p-0"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
@@ -362,12 +367,12 @@
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_datetime"
t-att-data-maxdate="question.validation_max_datetime">
- <input type="text" class="form-control datetimepicker-input o_survey_question_datetime font-weight-bold bg-transparent text-primary rounded-0"
+ <input type="text" class="form-control datetimepicker-input o_survey_question_datetime bg-transparent text-dark rounded-0 p-0"
t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="question.id"
t-att-value="format_datetime(answer_lines[0].value_datetime) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="input-group-append position-absolute" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
- <div class="input-group-text text-primary border-0 bg-transparent"><i class="fa fa-calendar"></i></div>
+ <div class="input-group-text text-primary border-0 bg-transparent p-0"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
@@ -392,7 +397,7 @@
t-att-data-name="question.id"
data-question-type="simple_choice_radio">
<t t-set="item_idx" t-value="0"/>
- <div class="d-flex flex-wrap font-weight-bold col-lg-12">
+ <div t-attf-class="col-lg-12 d-flex flex-wrap">
<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
<t t-foreach='question.suggested_answer_ids' t-as='label'>
<t t-set="item_idx" t-value="label_index"/>
@@ -405,9 +410,9 @@
<t t-set="answer_class" t-elif="not is_correct" t-value="'bg-danger'" />
<label t-att-for="str(question.id) + '_' + str(label.id)"
- t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s %s' % (answer_class, 'o_survey_selected' if answer_selected else '')">
+ t-att-class="'o_survey_choice_btn mr-2 mb-2 py-1 px-3 rounded %s %s' % (answer_class, 'o_survey_selected' if answer_selected else '')">
<t t-call="survey.survey_selection_key">
- <t t-set="selection_key_class" t-value="'o_survey_radio_btn float-left d-flex'"/>
+ <t t-set="selection_key_class" t-value="'position-relative o_survey_radio_btn float-left d-flex'"/>
</t>
<span class="ml-2" t-field='label.value'/>
<input t-att-id="str(question.id) + '_' + str(label.id)" type="radio" t-att-value='label.id' class="o_survey_form_choice_item invisible position-absolute"
@@ -415,17 +420,17 @@
t-att-checked="answer_line and answer_line.suggested_answer_id.id == label.id and 'checked' or None"
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<i class="fa fa-check-circle float-right mt-1 position-relative"></i>
- <i class="fa fa-circle float-right mt-1 position-relative"></i>
+ <i class="fa fa-circle-thin float-right mt-1 position-relative"></i>
<t t-call="survey.question_suggested_value_image"/>
</label>
</t>
</div>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12" >
- <div class="d-flex flex-wrap font-weight-bold">
- <label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s' % ('o_survey_selected' if comment_line else '')">
+ <div class="d-flex flex-wrap">
+ <label t-att-class="'o_survey_choice_btn mr-2 py-1 px-3 rounded %s' % ('o_survey_selected' if comment_line else '')">
<t t-set="item_idx" t-value="item_idx + 1"/>
<t t-call="survey.survey_selection_key">
- <t t-set="selection_key_class" t-value="'o_survey_radio_btn float-left d-flex'"/>
+ <t t-set="selection_key_class" t-value="'position-relative o_survey_radio_btn float-left d-flex'"/>
</t>
<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute" value="-1"
t-att-name='question.id'
@@ -433,17 +438,17 @@
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field="question.comments_message" />
<i class="fa fa-check-circle float-right mt-1 position-relative"></i>
- <i class="fa fa-circle float-right mt-1 position-relative"></i>
+ <i class="fa fa-circle-thin float-right mt-1 position-relative"></i>
</label>
</div>
- <div class="o_survey_comment_container py-0 px-1">
- <textarea type="text" class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
+ <div t-attf-class="o_survey_comment_container mt-3 py-0 px-1 #{'d-none' if not comment_line else ''}">
+ <textarea type="text" class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
- <div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 py-0 pl-3 pr-4">
- <textarea type="text" class="form-control o_survey_comment o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
- t-att-placeholder="question.comments_message"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
+ <div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 mt-3 pl-3 pr-4">
+ <textarea type="text" class="form-control o_survey_comment o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
+ t-att-placeholder="question.comments_message if not survey_form_readonly else ''"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
</template>
@@ -454,7 +459,7 @@
t-att-data-name="question.id"
t-att-data-question-type="question.question_type">
<t t-set="item_idx" t-value="0"/>
- <div class="d-flex flex-wrap font-weight-bold col-lg-12">
+ <div class="d-flex flex-wrap col-lg-12">
<t t-set="has_correct_answer" t-value="scoring_display_correction and any(label.is_correct for label in question.suggested_answer_ids)"/>
<t t-foreach='question.suggested_answer_ids' t-as='label'>
<t t-set="item_idx" t-value="label_index"/>
@@ -467,9 +472,9 @@
<t t-set="answer_class" t-elif="is_correct" t-value="'bg-success'" />
<t t-set="answer_class" t-elif="not is_correct" t-value="'bg-danger'" />
- <label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s %s' % (answer_class, 'o_survey_selected' if answer_line else '')">
+ <label t-att-class="'o_survey_choice_btn mr-2 py-1 px-3 rounded %s %s' % (answer_class, 'o_survey_selected' if answer_line else '')">
<t t-call="survey.survey_selection_key">
- <t t-set="selection_key_class" t-value="'float-left d-flex'"/>
+ <t t-set="selection_key_class" t-value="'position-relative float-left d-flex'"/>
</t>
<input type="checkbox" t-att-value='label.id' class="o_survey_form_choice_item invisible position-absolute"
t-att-name="question.id"
@@ -477,17 +482,17 @@
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field='label.value'/>
<i class="fa fa-check-square float-right mt-1 position-relative"></i>
- <i class="fa fa-square float-right mt-1 position-relative"></i>
+ <i class="fa fa-square-o float-right mt-1 position-relative"></i>
<t t-call="survey.question_suggested_value_image"/>
</label>
</t>
</div>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_ck_comments col-lg-12" >
- <div class="d-flex flex-wrap font-weight-bold">
- <label t-att-class="'o_survey_choice_btn m-1 py-1 px-3 text-white rounded %s' % ('o_survey_selected' if comment_line else '')">
+ <div class="d-flex flex-wrap">
+ <label t-att-class="'o_survey_choice_btn mr-2 py-1 px-3 rounded %s' % ('o_survey_selected' if comment_line else '')">
<t t-set="item_idx" t-value="item_idx + 1"/>
<t t-call="survey.survey_selection_key">
- <t t-set="selection_key_class" t-value="'float-left d-flex'"/>
+ <t t-set="selection_key_class" t-value="'position-relative float-left d-flex'"/>
</t>
<input type="checkbox" class="o_survey_form_choice_item o_survey_js_form_other_comment invisible position-absolute" value="-1"
t-att-name="question.id"
@@ -495,17 +500,17 @@
t-att-data-selection-key="letters[item_idx] if useKeySelection else ''"/>
<span class="ml-2" t-field="question.comments_message" />
<i class="fa fa-check-square float-right mt-1 position-relative"></i>
- <i class="fa fa-square float-right mt-1 position-relative"></i>
+ <i class="fa fa-square-o float-right mt-1 position-relative"></i>
</label>
</div>
- <div class="o_survey_comment_container py-0 px-1">
- <textarea type="text" class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
+ <div t-attf-class="o_survey_comment_container mt-3 py-0 px-1 #{'d-none' if not comment_line else ''}">
+ <textarea type="text" class="form-control o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
t-att-disabled="None if comment_line else 'disabled'"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
- <div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 py-0 pl-3 pr-4">
- <textarea type="text" class="form-control o_survey_comment o_survey_question_text_box font-weight-bold bg-transparent text-primary rounded-0"
- t-att-placeholder="question.comments_message"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
+ <div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 o_survey_comment_container mx-1 mt-3 py-0 pl-3 pr-4">
+ <textarea type="text" class="form-control o_survey_comment o_survey_question_text_box bg-transparent text-dark rounded-0 p-0"
+ t-att-placeholder="question.comments_message if not survey_form_readonly else ''"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</div>
</template>
@@ -519,16 +524,18 @@
<thead>
<tr>
<th class="border-0"> </th>
- <th class="border-0" t-foreach="question.suggested_answer_ids" t-as="col_label"><span t-field="col_label.value" /></th>
+ <th class="border-0 font-weight-normal" t-foreach="question.suggested_answer_ids" t-as="col_label"><span t-field="col_label.value" /></th>
</tr>
</thead>
<tbody>
<t t-set="item_idx" t-value="0"/>
- <tr class="bg-white text-white" t-foreach="question.matrix_row_ids" t-as="row_label">
- <th class="border-0"><span t-field="row_label.value" /></th>
+ <!-- For matrix, we have an extra check because we have rows * columns total options -->
+ <t t-set="useKeySelection" t-value="useKeySelection and (len(question.suggested_answer_ids) * len(question.matrix_row_ids)) &lt; len(letters)" />
+ <tr class="bg-white text-white" t-foreach="question.matrix_row_ids" t-as="row_label" t-att-id="row_label.id">
+ <th class="border-0 font-weight-normal text-left"><span t-field="row_label.value" /></th>
<t t-foreach="question.suggested_answer_ids" t-as="col_label">
<t t-set="answer" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id == col_label and line.matrix_row_id == row_label)"/>
- <td t-att-class="'o_survey_matrix_btn text-primary border-0 %s'
+ <td t-att-class="'o_survey_matrix_btn text-primary border-0 position-relative %s'
% ('o_survey_selected' if answer else '')">
<input t-att-type="'checkbox' if question.matrix_subtype == 'multiple' else 'radio'"
t-att-name="'%s_%s' % (question.id, row_label.id)" t-att-value='col_label.id'
@@ -538,11 +545,11 @@
class="o_survey_form_choice_item d-none"/>
<i t-att-class="'o_survey_matrix_empty_checkbox fa fa-%s position-relative'
% ('square-o' if question.matrix_subtype == 'multiple' else 'circle-thin')"></i>
- <i t-att-class="'fa fa-check-%s position-relative'
- % ('square' if question.matrix_subtype == 'multiple' else 'circle')"></i>
+ <i t-att-class="'fa fa-%s position-relative'
+ % ('check-square' if question.matrix_subtype == 'multiple' else 'check-circle')"></i>
<t t-call="survey.survey_selection_key">
<t t-set="selection_key_class"
- t-value="'float-right font-weight-bold %s' % ('o_survey_radio_btn' if question.matrix_subtype != 'multiple' else '')"/>
+ t-value="'position-absolute float-right font-weight-bold %s' % ('o_survey_radio_btn' if question.matrix_subtype != 'multiple' else '')"/>
</t>
<t t-set="item_idx" t-value="item_idx + 1"/>
</td>
@@ -551,14 +558,14 @@
</tbody>
</table>
<div t-if='question.comments_allowed'>
- <textarea type="text" class="form-control o_survey_question_text_box o_survey_comment font-weight-bold bg-transparent text-primary rounded-0"
- t-att-placeholder="question.comments_message"
+ <textarea type="text" class="form-control o_survey_question_text_box o_survey_comment bg-transparent text-dark rounded-0 p-0 mt-3"
+ t-att-placeholder="question.comments_message if not survey_form_readonly else ''"
t-att-name="'%s_%s' % (question.id, 'comment')"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
</div>
</template>
<template id="survey_selection_key">
- <div t-if="useKeySelection" t-att-class="'o_survey_choice_key bg-white position-relative rounded %s' % selection_key_class">
+ <div t-if="useKeySelection" t-att-class="'o_survey_choice_key bg-white rounded %s' % selection_key_class">
<span class="o_survey_key text-center position-absolute bg-white rounded-left py-0 pr-4 pl-2"><span class="text-primary text-center text-center w-100 position-relative">Key</span></span>
<span class="text-primary text-center w-100 position-relative" t-esc="letters[item_idx]"/>
</div>
diff --git a/addons/survey/views/survey_templates_management.xml b/addons/survey/views/survey_templates_management.xml
index c8af8b020af..3dab12f72e9 100644
--- a/addons/survey/views/survey_templates_management.xml
+++ b/addons/survey/views/survey_templates_management.xml
@@ -23,9 +23,9 @@
<!-- Error: void survey -->
<template id="survey_void_content" name="Survey: void content">
<t t-call="survey.layout">
+ <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
<div class="wrap">
<div class="container">
- <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
<div class="jumbotron mt32">
<h1><span t-field="survey.title"/> survey is void</h1>
<p>Please make sure you have at least one question in your survey. You also need at least one section if you chose the "Page per section" layout.<br />
@@ -74,7 +74,7 @@
<!-- ============================================================ -->
<template id="survey_button_form_view" name="Survey: back to form view">
- <div groups="survey.group_survey_manager" t-ignore="true" class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_editable_mode_hidden">
+ <div groups="survey.group_survey_manager" t-ignore="true" class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_editable_mode_hidden mb-0">
<div t-ignore="true" class="text-center">
<a t-attf-href="/web#view_type=form&amp;model=survey.survey&amp;id=#{survey.id}&amp;action=survey.action_survey_form"><i class="fa fa-fw fa-arrow-right"/><span t-if="answer and answer.test_entry">This is a test survey. </span>Edit Survey.</a>
</div>
@@ -101,9 +101,9 @@
</div>
</template>
- <!-- Survey Home page - Access Code
- Mainly used in 'session mode' to give an easy access to the survey through the '/s' route. -->
- <template id="survey_access_code" name="Survey: Access Code page">
+ <!-- Survey Home page - Session Code
+ Used in 'session mode' to give an easy access to the survey through the '/s' route. -->
+ <template id="survey_session_code" name="Survey: Access Code page">
<t t-call="survey.layout">
<div class="wrap o_survey_wrap pb16 d-flex">
<div class="container o_survey_quick_access d-flex flex-column">
@@ -114,7 +114,7 @@
</div>
<div class="row">
<div class="offset-md-4 col-md-4 text-center">
- <input id="access_code" type="text" placeholder="ABC123"
+ <input id="session_code" type="text" placeholder="4812"
class="form-control o_survey_question_text_box font-weight-bold bg-transparent text-primary text-center rounded-0 p-2 w-100"/>
</div>
<div class="col-3 text-center o_survey_error text-danger d-none pt-2" role="alert">
@@ -134,5 +134,16 @@
</t>
</template>
+ <template id="survey_navigation" name="Survey: Navigation">
+ <button t-if="can_go_back"
+ type="submit" class="btn p-0 shadow-none o_survey_navigation_submit" name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">
+ <i class="border-left fa fa-chevron-left p-2" />
+ </button>
+ <button t-if="survey and survey.questions_layout in ['page_per_question', 'page_per_section'] and answer and answer.state != 'done' and not answer.is_session_answer"
+ type="submit" class="btn p-0 shadow-none o_survey_navigation_submit" t-att-value="'next' if not survey_last else 'finish'">
+ <i class="border-left fa fa-chevron-right p-2" />
+ </button>
+ </template>
+
</data>
</odoo>
diff --git a/addons/survey/views/survey_templates_print.xml b/addons/survey/views/survey_templates_print.xml
index d06947c2e20..b5d128af7b1 100644
--- a/addons/survey/views/survey_templates_print.xml
+++ b/addons/survey/views/survey_templates_print.xml
@@ -5,10 +5,10 @@
<template id="survey_page_print" name="Survey: print page">
<t t-call="survey.layout">
<t t-set="survey_form_readonly" t-value="true"/>
+ <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
<div class="wrap">
<div class="o_survey_print container">
- <t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
- <div class='jumbotron mt32'>
+ <div class='py-5 mt32'>
<h1><span t-field='survey.title'/></h1>
<t t-if="survey.description"><div t-field='survey.description' class="oe_no_empty"/></t>
<t t-if="review" t-call="survey.survey_button_retake"/>
diff --git a/addons/survey/views/survey_templates_statistics.xml b/addons/survey/views/survey_templates_statistics.xml
index c22f2b7b9ca..95889498b21 100644
--- a/addons/survey/views/survey_templates_statistics.xml
+++ b/addons/survey/views/survey_templates_statistics.xml
@@ -3,9 +3,9 @@
<data>
<template id="survey_page_statistics" name="Survey: result statistics page">
<t t-call="survey.layout">
+ <t t-call="survey.survey_button_form_view" />
<t t-set="page_record_limit" t-value="10"/><!-- Change this record_limit to change number of record per page-->
<div class="container o_survey_result">
- <t t-call="survey.survey_button_form_view" />
<t t-call="survey.survey_page_statistics_header" />
<t t-call="survey.survey_page_statistics_inner" />
</div>
@@ -13,7 +13,7 @@
</template>
<template id="survey_page_statistics_header" name="Survey: result statistics header">
- <div class="jumbotron mt32">
+ <div class="py-5 mt32">
<h1>
<span t-field="survey.title"/>
<span style="font-size:1.5em;"
diff --git a/addons/survey/views/survey_templates_user_input_session.xml b/addons/survey/views/survey_templates_user_input_session.xml
index 19dbddc76f2..005dd879bb2 100644
--- a/addons/survey/views/survey_templates_user_input_session.xml
+++ b/addons/survey/views/survey_templates_user_input_session.xml
@@ -32,9 +32,9 @@
<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>
+ <span>Go to <a t-att-href="survey.session_link" t-esc="survey.session_link" 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" />
+ <input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey.session_link" />
</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.session_answer_count" />
@@ -78,9 +78,9 @@
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>
+ <span>Go to <a t-att-href="survey.session_link" t-esc="survey.session_link" 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" />
+ <input class="o_survey_session_copy_url d-none" type="text" t-att-value="survey.session_link" />
</h3>
<a role="button"
class="font-weight-bold fa fa-chevron-right o_survey_session_navigation o_survey_session_navigation_next p-3" />
@@ -125,6 +125,11 @@
<div class="progress">
<div class="progress-bar font-weight-bold" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
+ <fieldset disabled="disabled" class="mt-5" t-if="question.question_type == 'matrix'" t-call="survey.question_container">
+ <t t-set="hide_question_title" t-value="true" />
+ <t t-set="answer" t-value="env['survey.user_input']" />
+ <t t-set="survey_form_readonly" t-value="True"/>
+ </fieldset>
</div>
</div>
</div>