[MERGE][REF] survey: refactor survey page transition in ajax
Purpose ======= To have a better UX, the transition between questions in a survey should not be done by loading another page, but by loading next questions in AJAX. Specifications =========== Do the transition in ajax, no more page loading: - During submit, load and display next questions in a smooth transition (fade in / fade out). - Include start and end screen of the survey inside the survey form (same process, load next screen in smooth transition) - Integrate timer and start only after start survey. - Integrate breadcrumb and handle manually (at frontend) transition about clickable / active / inactive breadcrumb item. Make the survey breadcrumb a widget to ease the navigation transitions and updates. - Validate questions at frontend before sending data for submit. (to avoid fading out then fading in the same questions with error messages) Also include some improvements and fixes : - Save questions on time limit reached, to take into account unsubmitted answer (if user does not have time to click on submit) - Block submit if survey has been already completed by the user. - Do not use previous id on one page mode (fix) - Remove display mode for simple choice question : that will prepare the next redesign of survey and allow to handle simple choice in a more standard way for all kind of question types. See sub commit for more detail. Task ID : 2152223 closes odoo/odoo#42908 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -36,6 +36,7 @@ sent mails with personal token for the invitation of the survey.
|
||||
'views/survey_question_views.xml',
|
||||
'views/survey_templates.xml',
|
||||
'views/survey_templates_management.xml',
|
||||
'views/survey_templates_print.xml',
|
||||
'views/survey_templates_statistics.xml',
|
||||
'views/gamification_badge_views.xml',
|
||||
'wizard/survey_invite_views.xml',
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields, http, _
|
||||
@@ -50,7 +50,6 @@ class Survey(http.Controller):
|
||||
* token_wrong: given token not recognized;
|
||||
* token_required: no token given although it is necessary to access the
|
||||
survey;
|
||||
* answer_done: token linked to a finished answer;
|
||||
* answer_deadline: token linked to an expired answer;
|
||||
|
||||
:param ensure_token: whether user input existence based on given access token
|
||||
@@ -79,9 +78,6 @@ class Survey(http.Controller):
|
||||
if (not survey_sudo.page_ids and survey_sudo.questions_layout == 'page_per_section') or not survey_sudo.question_ids:
|
||||
return 'survey_void'
|
||||
|
||||
if answer_sudo and answer_sudo.state == 'done':
|
||||
return 'answer_done'
|
||||
|
||||
if answer_sudo and answer_sudo.deadline and answer_sudo.deadline < datetime.now():
|
||||
return 'answer_deadline'
|
||||
|
||||
@@ -139,8 +135,6 @@ class Survey(http.Controller):
|
||||
return request.render("survey.survey_auth_required", {'survey': survey_sudo, 'redirect_url': redirect_url})
|
||||
elif error_key == 'answer_deadline' and answer_sudo.access_token:
|
||||
return request.render("survey.survey_closed_expired", {'survey': survey_sudo})
|
||||
elif error_key == 'answer_done' and answer_sudo.access_token:
|
||||
return request.render("survey.survey_closed_finished", self._prepare_survey_finished_values(survey_sudo, answer_sudo, token=answer_sudo.access_token))
|
||||
|
||||
return werkzeug.utils.redirect("/")
|
||||
|
||||
@@ -160,7 +154,7 @@ class Survey(http.Controller):
|
||||
""" This route is called whenever the user has attempts left and hits the 'Retry' button
|
||||
after failing the survey."""
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True and access_data['validity_code'] != 'answer_done':
|
||||
if access_data['validity_code'] is not True:
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
@@ -223,22 +217,20 @@ class Survey(http.Controller):
|
||||
else:
|
||||
return request.render("survey.survey_403_page", {'survey': survey_sudo})
|
||||
|
||||
# Select the right page
|
||||
if answer_sudo.state == 'new': # Intro page
|
||||
data = {'survey': survey_sudo, 'answer': answer_sudo, 'page': 0}
|
||||
return request.render('survey.survey_page_start', data)
|
||||
else:
|
||||
return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token))
|
||||
|
||||
@http.route('/survey/fill/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
|
||||
def survey_display_page(self, survey_token, answer_token, **post):
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
return request.redirect('/survey/%s/%s' % (survey_sudo.access_token, answer_sudo.access_token))
|
||||
|
||||
def _prepare_survey_data(self, survey_sudo, answer_sudo, **post):
|
||||
""" This method prepares all the data needed for template rendering, in function of the survey user input state.
|
||||
:param post:
|
||||
- previous_page_id : come from the breadcrumb or the back button and force the next questions to load
|
||||
to be the previous ones. """
|
||||
data = {
|
||||
'survey': survey_sudo,
|
||||
'answer': answer_sudo,
|
||||
'breadcrumb_pages': [{
|
||||
'id': page.id,
|
||||
'title': page.title,
|
||||
} for page in survey_sudo.page_ids],
|
||||
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
|
||||
'format_date': lambda date: format_date(request.env, date)
|
||||
}
|
||||
@@ -250,48 +242,64 @@ class Survey(http.Controller):
|
||||
previous_page_or_question_id = int(post['previous_page_id'])
|
||||
new_previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, previous_page_or_question_id)
|
||||
data.update({
|
||||
'survey': survey_sudo,
|
||||
page_or_question_key: request.env['survey.question'].sudo().browse(previous_page_or_question_id),
|
||||
'answer': answer_sudo,
|
||||
'previous_page_id': new_previous_id
|
||||
})
|
||||
return request.render('survey.survey_page_main', data)
|
||||
return data
|
||||
|
||||
if survey_sudo.is_time_limited and not answer_sudo.start_datetime:
|
||||
# init start date when user starts filling in the survey
|
||||
answer_sudo.write({
|
||||
'start_datetime': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# Select the right page
|
||||
if answer_sudo.state == 'new': # First page
|
||||
page_or_question_id, is_last = survey_sudo.next_page_or_question(answer_sudo, 0)
|
||||
data.update({
|
||||
'survey': survey_sudo,
|
||||
page_or_question_key: page_or_question_id,
|
||||
'answer': answer_sudo,
|
||||
})
|
||||
if is_last:
|
||||
data.update({'last': True})
|
||||
return request.render('survey.survey_page_main', data)
|
||||
elif answer_sudo.state == 'done': # Display success message
|
||||
return request.render('survey.survey_closed_finished', self._prepare_survey_finished_values(survey_sudo, answer_sudo))
|
||||
elif answer_sudo.state == 'skip':
|
||||
page_or_question_id, is_last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id)
|
||||
previous_id = survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id)
|
||||
if answer_sudo.state == 'in_progress':
|
||||
page_or_question_id, is_last = survey_sudo.next_page_or_question(
|
||||
answer_sudo,
|
||||
answer_sudo.last_displayed_page_id.id if answer_sudo.last_displayed_page_id else 0)
|
||||
|
||||
data.update({
|
||||
'survey': survey_sudo,
|
||||
page_or_question_key: page_or_question_id,
|
||||
'answer': answer_sudo,
|
||||
'previous_page_id': previous_id
|
||||
})
|
||||
if survey_sudo.questions_layout != 'one_page':
|
||||
data.update({
|
||||
'previous_page_id': survey_sudo._previous_page_or_question_id(answer_sudo, page_or_question_id.id)
|
||||
})
|
||||
if is_last:
|
||||
data.update({'last': True})
|
||||
elif answer_sudo.state == 'done' or answer_sudo.is_time_limit_reached: # Display success message
|
||||
return self._prepare_survey_finished_values(survey_sudo, answer_sudo)
|
||||
|
||||
return request.render('survey.survey_page_main', data)
|
||||
else:
|
||||
return request.render("survey.survey_403_page", {'survey': survey_sudo})
|
||||
return data
|
||||
|
||||
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)
|
||||
if answer_sudo.state == 'done':
|
||||
return request.env.ref('survey.survey_fill_form_done').render(data).decode('UTF-8')
|
||||
return request.env.ref('survey.survey_fill_form_in_progress').render(data).decode('UTF-8')
|
||||
|
||||
@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):
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
||||
return request.render('survey.survey_page_fill',
|
||||
self._prepare_survey_data(access_data['survey_sudo'], access_data['answer_sudo'], **post))
|
||||
|
||||
@http.route('/survey/begin/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True)
|
||||
def survey_begin(self, survey_token, answer_token, **post):
|
||||
""" Route used to start the survey user input and display the first survey page. """
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return {'error': access_data['validity_code']}
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
if answer_sudo.state != "new":
|
||||
return {'error': _("The survey has already started.")}
|
||||
|
||||
# init start date when user starts filling in the survey
|
||||
answer_sudo.write({
|
||||
'start_datetime': fields.Datetime.now(),
|
||||
'state': 'in_progress'
|
||||
})
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
|
||||
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='json', auth='public', website=True)
|
||||
def survey_submit(self, survey_token, answer_token, **post):
|
||||
@@ -302,47 +310,42 @@ class Survey(http.Controller):
|
||||
# Survey Validation
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
|
||||
if access_data['validity_code'] is not True:
|
||||
return {
|
||||
'error': access_data['validity_code'],
|
||||
}
|
||||
return {'error': access_data['validity_code']}
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
if answer_sudo.state == 'done':
|
||||
return {'error': 'unauthorized'}
|
||||
|
||||
questions, page_or_question_id = survey_sudo._get_survey_questions(answer=answer_sudo,
|
||||
page_id=post.get('page_id'),
|
||||
question_id=post.get('question_id'))
|
||||
|
||||
if not answer_sudo.test_entry and not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email, answer_sudo.invite_token):
|
||||
# prevent cheating with users creating multiple 'user_input' before their last attempt
|
||||
return {}
|
||||
return {'error': 'unauthorized'}
|
||||
|
||||
if not answer_sudo.is_time_limit_reached:
|
||||
# Prepare answers and comment by question
|
||||
prepared_questions = {}
|
||||
for question in questions:
|
||||
answer_full = post.get(str(question.id))
|
||||
answer_without_comment, comment = self._extract_comment_from_answers(question, answer_full)
|
||||
prepared_questions[question.id] = {'answer': answer_without_comment, 'comment': comment}
|
||||
if answer_sudo.is_time_limit_reached:
|
||||
time_limit = answer_sudo.start_datetime + timedelta(minutes=survey_sudo.time_limit)
|
||||
if fields.Datetime.now() > (time_limit + timedelta(seconds=10)):
|
||||
# prevent cheating with users blocking the JS timer and taking all their time to answer
|
||||
return {'error': 'unauthorized'}
|
||||
|
||||
# Questions Validation
|
||||
errors = {}
|
||||
for question in questions:
|
||||
answer = prepared_questions[question.id]['answer']
|
||||
comment = prepared_questions[question.id]['comment']
|
||||
errors.update(question.validate_question(answer, comment))
|
||||
if errors:
|
||||
return {'error': 'validation', 'fields': errors}
|
||||
|
||||
# Submitting questions
|
||||
for question in questions:
|
||||
answer = prepared_questions[question.id]['answer']
|
||||
comment = prepared_questions[question.id]['comment']
|
||||
errors = {}
|
||||
# Prepare answers / comment by question, validate and save answers
|
||||
for question in questions:
|
||||
answer, comment = self._extract_comment_from_answers(question, post.get(str(question.id)))
|
||||
errors.update(question.validate_question(answer, comment))
|
||||
if not errors.get(question.id):
|
||||
answer_sudo.save_lines(question, answer, comment)
|
||||
|
||||
if errors and not answer_sudo.is_time_limit_reached:
|
||||
return {'error': 'validation', 'fields': errors}
|
||||
|
||||
if answer_sudo.is_time_limit_reached or survey_sudo.questions_layout == 'one_page':
|
||||
answer_sudo._mark_done()
|
||||
elif 'previous_page_id' in post:
|
||||
# Go back to specific page using the breadcrumb. Lines are saved and survey continues
|
||||
return '/survey/fill/%s/%s?previous_page_id=%s' % (survey_sudo.access_token, answer_token, post['previous_page_id'])
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo, **post)
|
||||
else:
|
||||
next_page, unused = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id)
|
||||
vals = {'last_displayed_page_id': page_or_question_id}
|
||||
@@ -350,11 +353,11 @@ class Survey(http.Controller):
|
||||
if next_page is None:
|
||||
answer_sudo._mark_done()
|
||||
else:
|
||||
vals.update({'state': 'skip'})
|
||||
vals.update({'state': 'in_progress'})
|
||||
|
||||
answer_sudo.write(vals)
|
||||
|
||||
return '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
|
||||
return self._prepare_question_html(survey_sudo, answer_sudo)
|
||||
|
||||
def _extract_comment_from_answers(self, question, answers):
|
||||
""" Answers is a custom structure depending of the question type
|
||||
@@ -412,7 +415,7 @@ class Survey(http.Controller):
|
||||
access_data = self._get_access_data(survey_token, answer_token, ensure_token=False)
|
||||
if access_data['validity_code'] is not True and (
|
||||
access_data['has_survey_access'] or
|
||||
access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void', 'answer_done']):
|
||||
access_data['validity_code'] not in ['token_required', 'survey_closed', 'survey_void']):
|
||||
return self._redirect_with_error(access_data, access_data['validity_code'])
|
||||
|
||||
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">Do we sell Acoustic Bloc Screens?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="constr_mandatory" eval="True" />
|
||||
</record>
|
||||
<record model="survey.question.answer" id="vendor_certification_page_1_question_1_choice_1">
|
||||
@@ -145,7 +144,6 @@
|
||||
<field name="sequence">5</field>
|
||||
<field name="title">How many versions of the Corner Desk do we have?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="constr_mandatory" eval="True" />
|
||||
</record>
|
||||
<record model="survey.question.answer" id="vendor_certification_page_1_question_4_choice_1">
|
||||
@@ -192,7 +190,6 @@
|
||||
<field name="sequence">8</field>
|
||||
<field name="title">How much do we sell our Cable Management Box?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="constr_mandatory" eval="True" />
|
||||
</record>
|
||||
<record model="survey.question.answer" id="vendor_certification_page_2_question_1_choice_1">
|
||||
@@ -280,7 +277,6 @@
|
||||
<field name="sequence">10</field>
|
||||
<field name="title">What do you think about our prices (not rated)?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="constr_mandatory" eval="True" />
|
||||
</record>
|
||||
<record model="survey.question.answer" id="vendor_certification_page_2_question_3_choice_1">
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
<field name="sequence">4</field>
|
||||
<field name="title">How frequently do you buy products online ?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
@@ -217,7 +216,7 @@
|
||||
<field name="survey_id" ref="survey.survey_feedback" />
|
||||
<field name="partner_id" eval="False"/>
|
||||
<field name="email">Martin Tamarre <martin@example.com></field>
|
||||
<field name="state">skip</field>
|
||||
<field name="state">in_progress</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
<field name="title">Which category does a tomato belong to</field>
|
||||
<field name="description" type="html"><span>"Red" is not a category, I know what you are trying to do ;)</span></field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="True"/>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
@@ -118,7 +117,6 @@
|
||||
<field name="sequence">22</field>
|
||||
<field name="title">Which of the following would you use to pollinate</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">columns</field>
|
||||
<field name="comments_allowed" eval="True"/>
|
||||
<field name="comment_count_as_answer" eval="False"/>
|
||||
<field name="constr_mandatory" eval="True"/>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
<field name="partner_id" ref="base.res_partner_address_28"/>
|
||||
<field name="email">colleen.diaz83@example.com</field>
|
||||
<field name="state">skip</field>
|
||||
<field name="state">in_progress</field>
|
||||
</record>
|
||||
<record id="survey_demo_quiz_answer_6" model="survey.user_input">
|
||||
<field name="survey_id" ref="survey.survey_demo_quiz"/>
|
||||
|
||||
@@ -14,7 +14,7 @@ class SurveyQuestion(models.Model):
|
||||
""" Questions that will be asked in a survey.
|
||||
|
||||
Each question can have one of more suggested answers (eg. in case of
|
||||
dropdown choices, multi-answer checkboxes, radio buttons...).
|
||||
multi-answer checkboxes, radio buttons...).
|
||||
|
||||
Technical note:
|
||||
|
||||
@@ -98,9 +98,6 @@ class SurveyQuestion(models.Model):
|
||||
('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')],
|
||||
string='Number of columns', default='12',
|
||||
help='These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.')
|
||||
display_mode = fields.Selection(
|
||||
[('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')],
|
||||
string='Display Mode', default='columns', help='Display mode of simple choice questions.')
|
||||
# -- comments (simple choice, multiple choice, matrix (without count as an answer))
|
||||
comments_allowed = fields.Boolean('Show Comments Field')
|
||||
comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
|
||||
|
||||
@@ -25,7 +25,7 @@ class SurveyUserInput(models.Model):
|
||||
deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers")
|
||||
state = fields.Selection([
|
||||
('new', 'Not started yet'),
|
||||
('skip', 'Partially completed'),
|
||||
('in_progress', 'In Progress'),
|
||||
('done', 'Completed')], string='Status', default='new', readonly=True)
|
||||
test_entry = fields.Boolean(readonly=True)
|
||||
last_displayed_page_id = fields.Many2one('survey.question', string='Last displayed question/page')
|
||||
@@ -72,8 +72,11 @@ class SurveyUserInput(models.Model):
|
||||
def _compute_is_time_limit_reached(self):
|
||||
""" Checks that the user_input is not exceeding the survey's time limit. """
|
||||
for user_input in self:
|
||||
user_input.is_time_limit_reached = user_input.survey_id.is_time_limited and fields.Datetime.now() \
|
||||
> user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit)
|
||||
if user_input.start_datetime and user_input.survey_id.is_time_limited:
|
||||
datetime_limit = user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit)
|
||||
user_input.is_time_limit_reached = fields.Datetime.now() > datetime_limit
|
||||
else:
|
||||
user_input.is_time_limit_reached = False
|
||||
|
||||
@api.depends('state', 'test_entry', 'survey_id.is_attempts_limited', 'partner_id', 'email', 'invite_token')
|
||||
def _compute_attempts_number(self):
|
||||
@@ -261,7 +264,7 @@ class SurveyUserInput(models.Model):
|
||||
return vals
|
||||
|
||||
if answer_type == 'suggestion':
|
||||
vals['suggested_answer_id'] = answer
|
||||
vals['suggested_answer_id'] = int(answer)
|
||||
elif answer_type == 'numerical_box':
|
||||
vals['value_numerical_box'] = float(answer)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
odoo.define('survey.breadcrumb', function (require) {
|
||||
'use strict';
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
publicWidget.registry.SurveyBreadcrumbWidget = publicWidget.Widget.extend({
|
||||
xmlDependencies: ['/survey/static/src/xml/survey_breadcrumb_templates.xml'],
|
||||
template: "survey.survey_breadcrumb_template",
|
||||
events: {
|
||||
'click .breadcrumb-item a': '_onBreadcrumbClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this._super.apply(this, arguments);
|
||||
this.canGoBack = options.canGoBack;
|
||||
this.currentPageId = options.currentPageId;
|
||||
this.pages = options.pages;
|
||||
},
|
||||
|
||||
// Handlers
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
_onBreadcrumbClick: function (event) {
|
||||
event.preventDefault();
|
||||
this.trigger_up('breadcrumb_click', {
|
||||
'previousPageId': this.$(event.currentTarget)
|
||||
.closest('.breadcrumb-item')
|
||||
.data('pageId')
|
||||
});
|
||||
},
|
||||
|
||||
// PUBLIC METHODS
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
updateBreadcrumb: function (pageId) {
|
||||
if (pageId) {
|
||||
this.currentPageId = pageId;
|
||||
this.renderElement();
|
||||
} else {
|
||||
this.$('.breadcrumb').addClass('d-none');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return publicWidget.registry.SurveyBreadcrumbWidget;
|
||||
|
||||
});
|
||||
@@ -1,16 +1,20 @@
|
||||
odoo.define('survey.form', function(require) {
|
||||
odoo.define('survey.form', function (require) {
|
||||
'use strict';
|
||||
|
||||
var field_utils = require('web.field_utils');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var time = require('web.time');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
selector: '.o_survey_form',
|
||||
events: {
|
||||
'change .o_survey_form_choice_item': '_onChangeChoiceItem',
|
||||
'click button[type="submit"]': '_onSubmit',
|
||||
'click .o_survey_header .breadcrumb-item a': '_onBreadcrumbClick',
|
||||
},
|
||||
custom_events: {
|
||||
'breadcrumb_click': '_onBreadcrumbClick',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -22,20 +26,12 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function() {
|
||||
this.fadeInOutDelay = 400;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.options = self.$target.find('form').data();
|
||||
var $timer = $('.o_survey_timer');
|
||||
if ($timer.length) {
|
||||
var timeLimitMinutes = self.options.timeLimitMinutes;
|
||||
var timer = self.options.timer;
|
||||
self.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(self, {
|
||||
'timer': timer,
|
||||
'timeLimitMinutes': timeLimitMinutes
|
||||
});
|
||||
self.surveyTimerWidget.attachTo($timer);
|
||||
self.surveyTimerWidget.on('time_up', self, function (ev) {
|
||||
self.$el.find('button[type="submit"]').click();
|
||||
});
|
||||
if (!self.options.isStartScreen) {
|
||||
self._initTimer();
|
||||
self._initBreadcrumb();
|
||||
}
|
||||
self.$('div.o_survey_form_date').each(function () {
|
||||
self._initDateTimePicker($(this));
|
||||
@@ -50,12 +46,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
// Handlers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
/**
|
||||
* Checks, if the 'other' choice is checked. Applies only if the comment count as answer.
|
||||
* In case of dropdownlist:
|
||||
* If not checked : Clear the comment textarea and hide it
|
||||
* If checked : show the comment textarea and focus on it
|
||||
* In case of radio buttons
|
||||
* If not checked : Clear the comment textarea and disable it
|
||||
* If checked : enable the comment textarea and focus on it
|
||||
*
|
||||
@@ -67,93 +59,260 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
var $otherItem = $choiceItemGroup.find('.o_survey_js_form_other_comment');
|
||||
var $commentInput = $choiceItemGroup.find('textarea[type="text"]');
|
||||
|
||||
// Handle dropdownlist
|
||||
var $dropdownlist = $choiceItemGroup.find('select');
|
||||
var isDropdown = $dropdownlist.length > 0;
|
||||
|
||||
var isOtherSelected = (isDropdown && $dropdownlist.val() === $otherItem.val()) || $otherItem.prop('checked');
|
||||
|
||||
if (isOtherSelected || $commentInput.hasClass('o_survey_comment')) {
|
||||
if (isDropdown) {
|
||||
$commentInput.toggleClass('d-none', false);
|
||||
}
|
||||
if (isOtherSelected) {
|
||||
$commentInput.enable();
|
||||
if ($otherItem.prop('checked') || $commentInput.hasClass('o_survey_comment')) {
|
||||
$commentInput.enable();
|
||||
if ($otherItem.prop('checked')) {
|
||||
$commentInput.focus();
|
||||
}
|
||||
} else {
|
||||
if (isDropdown) {
|
||||
$commentInput.toggleClass('d-none', true);
|
||||
}
|
||||
$commentInput.val('');
|
||||
$commentInput.enable(false);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* When clicking on a breadcrumb enabled item, redirect to the target page.
|
||||
* Uses the submit flow to validate and save the answer before going to the specified page.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} event
|
||||
*/
|
||||
_onBreadcrumbClick: function (event) {
|
||||
event.preventDefault();
|
||||
this._submitForm({'previous_page_id': $(event.currentTarget).data('previousPageId')});
|
||||
},
|
||||
|
||||
_onSubmit: function (event) {
|
||||
event.preventDefault();
|
||||
if ($(event.currentTarget).val() === 'previous') {
|
||||
this._submitForm({'previous_page_id': $(event.currentTarget).data('previousPageId')});
|
||||
} else {
|
||||
this._submitForm({});
|
||||
var options = {};
|
||||
var $target = $(event.currentTarget);
|
||||
if ($target.val() === 'previous') {
|
||||
options.previousPageId = $target.data('previousPageId');
|
||||
} else if ($target.val() === 'finish') {
|
||||
options.isFinish = true;
|
||||
}
|
||||
this._submitForm(options);
|
||||
},
|
||||
|
||||
_submitForm: function (params) {
|
||||
// Custom Events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
_onBreadcrumbClick: function (event) {
|
||||
this._submitForm({'previousPageId': event.data.previousPageId});
|
||||
},
|
||||
|
||||
// SUBMIT
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This function will send a json rpc call to the server to
|
||||
* - start the survey (if we are on start screen)
|
||||
* - submit the answers of the current page
|
||||
* Before submitting the answers, they are first validated to avoid latency from the server
|
||||
* and allow a fade out/fade in transition of the next question.
|
||||
*
|
||||
* @param {Array} [options]
|
||||
* @param {Integer} [options.previousPageId] navigates to page id
|
||||
* @param {Boolean} [options.skipValidation] skips JS validation
|
||||
* @param {Boolean} [options.isFinish] fades out breadcrumb and timer
|
||||
* @private
|
||||
*/
|
||||
_submitForm: function (options) {
|
||||
var self = this;
|
||||
var $form = this.$('form');
|
||||
var formData = new FormData($form[0]);
|
||||
var params = {};
|
||||
if (options.previousPageId) {
|
||||
params.previous_page_id = options.previousPageId;
|
||||
}
|
||||
var route = "/survey/submit";
|
||||
|
||||
this._prepareSubmitValues(formData, params);
|
||||
if (this.options.isStartScreen) {
|
||||
route = "/survey/begin";
|
||||
} else {
|
||||
var $form = this.$('form');
|
||||
var formData = new FormData($form[0]);
|
||||
|
||||
this._resetErrors();
|
||||
if (!options.skipValidation) {
|
||||
// Validation pre submit
|
||||
if (!this._validateForm($form, formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return self._rpc({
|
||||
route: '/survey/submit/' + self.options.surveyToken + '/' + self.options.answerToken ,
|
||||
this._prepareSubmitValues(formData, params);
|
||||
}
|
||||
|
||||
var resolveFadeOut;
|
||||
var fadeOutPromise = new Promise(function (resolve, reject) {resolveFadeOut = resolve;});
|
||||
|
||||
var selectorsToFadeout = ['.o_survey_form_content'];
|
||||
if (options.isFinish) {
|
||||
selectorsToFadeout.push('.breadcrumb', '.o_survey_timer');
|
||||
}
|
||||
self.$(selectorsToFadeout.join(',')).fadeOut(this.fadeInOutDelay, function () {
|
||||
resolveFadeOut();
|
||||
});
|
||||
var submitPromise = self._rpc({
|
||||
route: _.str.sprintf('%s/%s/%s', route, self.options.surveyToken, self.options.answerToken),
|
||||
params: params,
|
||||
}).then(function (result) {
|
||||
return self._onSubmitDone(result, params);
|
||||
});
|
||||
Promise.all([fadeOutPromise, submitPromise]).then(function (results) {
|
||||
return self._onSubmitDone(results[1], options.isFinish);
|
||||
});
|
||||
},
|
||||
|
||||
_onSubmitDone: function (result, params) {
|
||||
/**
|
||||
* Follow the submit and handle the transition from one screen to another
|
||||
* Also handle server side validation and displays eventual error messages.
|
||||
*/
|
||||
_onSubmitDone: function (result, isFinish) {
|
||||
var self = this;
|
||||
|
||||
if (result && !result.error) {
|
||||
window.location = result;
|
||||
this.$(".o_survey_form_content").empty();
|
||||
this.$(".o_survey_form_content").html(result);
|
||||
this.$('div.o_survey_form_date').each(function () {
|
||||
self._initDateTimePicker($(this));
|
||||
});
|
||||
if (this.options.isStartScreen) {
|
||||
this._initTimer();
|
||||
this.options.isStartScreen = false;
|
||||
}
|
||||
if (isFinish) {
|
||||
this._initResultWidget();
|
||||
if (this.surveyBreadcrumbWidget) {
|
||||
this.$('.o_survey_breadcrumb_container').addClass('d-none');
|
||||
this.surveyBreadcrumbWidget.destroy();
|
||||
}
|
||||
if (this.surveyTimerWidget) {
|
||||
this.surveyTimerWidget.destroy();
|
||||
}
|
||||
} else {
|
||||
this._updateBreadcrumb();
|
||||
}
|
||||
this.$('.o_survey_form_content').fadeIn(this.fadeInOutDelay);
|
||||
$("html, body").animate({ scrollTop: 0 }, this.fadeInOutDelay);
|
||||
}
|
||||
else if (result && result.fields && result.error === 'validation') {
|
||||
var fieldKeys = _.keys(result.fields);
|
||||
_.each(fieldKeys, function (key) {
|
||||
self.$("#" + key + '>.o_survey_question_error').append($('<p>', {text: result.fields[key]})).toggleClass('d-none', false);
|
||||
if (fieldKeys[fieldKeys.length - 1] === key) {
|
||||
self._scrollToError(self.$('.o_survey_question_error:visible:first').closest('.js_question-wrapper'));
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
var $target = self.$('.o_survey_error');
|
||||
$target.toggleClass('d-none', false);
|
||||
self._scrollToError($target);
|
||||
this.$('.o_survey_form_content').fadeIn(0);
|
||||
this._showErrors(result.fields);
|
||||
} else {
|
||||
var $errorTarget = this.$('.o_survey_error');
|
||||
$errorTarget.removeClass("d-none");
|
||||
this._scrollToError($errorTarget);
|
||||
}
|
||||
},
|
||||
|
||||
// VALIDATION TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
/**
|
||||
* Validation is done in frontend before submit to avoid latency from the server.
|
||||
* If the validation is incorrect, the errors are displayed before submitting and
|
||||
* fade in / out of submit is avoided.
|
||||
*
|
||||
* Each question type gets its own validation process.
|
||||
*
|
||||
* There is a special use case for the 'required' questions, where we use the constraint
|
||||
* error message that comes from the question configuration ('constr_error_msg' field).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_validateForm: function ($form, formData) {
|
||||
var self = this;
|
||||
var errors = {};
|
||||
var validationEmailMsg = _t("This answer must be an email address.");
|
||||
var validationDateMsg = _t("This is not a date");
|
||||
|
||||
this._resetErrors();
|
||||
|
||||
var data = {};
|
||||
formData.forEach(function (value, key) {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
$form.find('[data-question-type]').each(function () {
|
||||
var $input = $(this);
|
||||
var $questionWrapper = $input.closest(".js_question-wrapper");
|
||||
var constrErrorMsg = $questionWrapper.data('constrErrorMsg');
|
||||
var validationErrorMsg = $questionWrapper.data('validationErrorMsg');
|
||||
var questionId = $questionWrapper.attr('id');
|
||||
var questionRequired = $questionWrapper.data('required');
|
||||
switch ($input.data('questionType')) {
|
||||
case 'char_box':
|
||||
if (questionRequired && !$input.val()) {
|
||||
errors[questionId] = constrErrorMsg;
|
||||
} else if ($input.val() && $input.attr('type') === 'email' && !self._validateEmail($input.val())) {
|
||||
errors[questionId] = validationEmailMsg;
|
||||
} else {
|
||||
var lengthMin = $input.data('validationLengthMin');
|
||||
var lengthMax = $input.data('validationLengthMax');
|
||||
var length = $input.val().length;
|
||||
if (lengthMin && (lengthMin > length || length > lengthMax)) {
|
||||
errors[questionId] = validationErrorMsg;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'numerical_box':
|
||||
if (questionRequired && !data[questionId]) {
|
||||
errors[questionId] = constrErrorMsg;
|
||||
} else {
|
||||
var floatMin = $input.data('validationFloatMin');
|
||||
var floatMax = $input.data('validationFloatMax');
|
||||
var value = parseFloat($input.val());
|
||||
if (floatMin && (floatMin > value || value > floatMax)) {
|
||||
errors[questionId] = validationErrorMsg;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
if (questionRequired && !data[questionId]) {
|
||||
errors[questionId] = constrErrorMsg;
|
||||
} else if (data[questionId]) {
|
||||
var momentDate = moment($input.val());
|
||||
if (!momentDate.isValid()) {
|
||||
errors[questionId] = validationDateMsg;
|
||||
} else {
|
||||
var $dateDiv = $questionWrapper.find('.o_survey_form_date');
|
||||
var maxDate = $dateDiv.data('maxdate');
|
||||
var minDate = $dateDiv.data('mindate');
|
||||
if ((maxDate && momentDate.isAfter(moment(maxDate)))
|
||||
|| (minDate && momentDate.isBefore(moment(minDate)))) {
|
||||
errors[questionId] = validationErrorMsg;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'simple_choice_radio':
|
||||
case 'multiple_choice':
|
||||
if (questionRequired) {
|
||||
var $textarea = $questionWrapper.find('textarea');
|
||||
if (!data[questionId]) {
|
||||
errors[questionId] = constrErrorMsg;
|
||||
} else if (data[questionId] === '-1' && !$textarea.val()) {
|
||||
// if other has been checked and value is null
|
||||
errors[questionId] = constrErrorMsg;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'matrix':
|
||||
if (questionRequired) {
|
||||
var subQuestionsIds = $questionWrapper.find('table').data('subQuestions');
|
||||
subQuestionsIds.forEach(function (id) {
|
||||
if (!((questionId + '_' + id) in data)) {
|
||||
errors[questionId] = constrErrorMsg;
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (_.keys(errors).length > 0) {
|
||||
this._showErrors(errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the email has an '@', a left part and a right part
|
||||
* @private
|
||||
*/
|
||||
_validateEmail: function (email) {
|
||||
var emailParts = email.split('@');
|
||||
return emailParts.length === 2 && emailParts[0] && emailParts[1];
|
||||
},
|
||||
|
||||
// PREPARE SUBMIT TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
/*
|
||||
/**
|
||||
* For each type of question, extract the answer from inputs or textarea (comment or answer)
|
||||
*
|
||||
*
|
||||
@@ -162,8 +321,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
*/
|
||||
_prepareSubmitValues: function (formData, params) {
|
||||
var self = this;
|
||||
// Get all context params -- TODO : Use formData instead (test if input with no name are in formData)
|
||||
formData.forEach(function(value, key){
|
||||
formData.forEach(function (value, key){
|
||||
switch (key) {
|
||||
case 'csrf_token':
|
||||
case 'token':
|
||||
@@ -188,12 +346,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
case 'datetime':
|
||||
params = self._prepareSubmitDates(params, this.name, this.value, true);
|
||||
break;
|
||||
case 'simple_choice_dropdown':
|
||||
params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'option:selected');
|
||||
break;
|
||||
case 'simple_choice_radio':
|
||||
case 'multiple_choice':
|
||||
params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'input:checked');
|
||||
params = self._prepareSubmitChoices(params, $(this), $(this).data('name'));
|
||||
break;
|
||||
case 'matrix':
|
||||
params = self._prepareSubmitAnswersMatrix(params, $(this));
|
||||
@@ -207,7 +362,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
* Convert date value from client current timezone to UTC Date to correspond to the server format.
|
||||
* return params = { 'dateQuestionId' : '2019-05-23', 'datetimeQuestionId' : '2019-05-23 14:05:12' }
|
||||
*/
|
||||
_prepareSubmitDates: function(params, questionId, value, isDateTime) {
|
||||
_prepareSubmitDates: function (params, questionId, value, isDateTime) {
|
||||
var momentDate = isDateTime ? field_utils.parse.datetime(value, null, {timezone: true}) : field_utils.parse.date(value);
|
||||
var formattedDate = momentDate ? momentDate.toJSON() : '';
|
||||
params[questionId] = formattedDate;
|
||||
@@ -219,9 +374,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
* If the answer is not the 'comment selection' (=Other), calls the _prepareSubmitAnswer method to add the answer to the params
|
||||
* If there is a comment linked to that question, calls the _prepareSubmitComment method to add the comment to the params
|
||||
*/
|
||||
_prepareSubmitChoices: function(params, $parent, questionId, selector) {
|
||||
_prepareSubmitChoices: function (params, $parent, questionId) {
|
||||
var self = this;
|
||||
$parent.find(selector).each(function () {
|
||||
$parent.find('input:checked').each(function () {
|
||||
if (this.value !== '-1') {
|
||||
params = self._prepareSubmitAnswer(params, questionId, this.value);
|
||||
}
|
||||
@@ -271,7 +426,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
* Lonely answer are directly assigned to questionId. Multiple answers are regrouped in an array:
|
||||
* params = { 'questionId1' : lonelyAnswer, 'questionId2' : [multipleAnswer1, multipleAnswer2, ...] }
|
||||
*/
|
||||
_prepareSubmitAnswer: function(params, questionId, value) {
|
||||
_prepareSubmitAnswer: function (params, questionId, value) {
|
||||
if (questionId in params) {
|
||||
if (params[questionId].constructor === Array) {
|
||||
params[questionId].push(value);
|
||||
@@ -290,7 +445,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
* with the new value. At the end, the result looks like :
|
||||
* params = { 'questionId1' : {'comment': commentValue}, 'questionId2' : [multipleAnswer1, {'comment': commentValue}, ...] }
|
||||
*/
|
||||
_prepareSubmitComment: function(params, $parent, questionId, isMatrix) {
|
||||
_prepareSubmitComment: function (params, $parent, questionId, isMatrix) {
|
||||
var self = this;
|
||||
$parent.find('textarea').each(function () {
|
||||
if (this.value) {
|
||||
@@ -308,6 +463,58 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
// INIT FIELDS TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Will initialize the breadcrumb widget that handles navigation to a previously filled in page.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initBreadcrumb: function () {
|
||||
var $breadcrumb = this.$('.o_survey_breadcrumb_container');
|
||||
var pageId = this.$('input[name=page_id]').val();
|
||||
if ($breadcrumb.length) {
|
||||
this.surveyBreadcrumbWidget = new publicWidget.registry.SurveyBreadcrumbWidget(this, {
|
||||
'canGoBack': $breadcrumb.data('canGoBack'),
|
||||
'currentPageId': pageId ? parseInt(pageId) : 0,
|
||||
'pages': $breadcrumb.data('pages'),
|
||||
});
|
||||
this.surveyBreadcrumbWidget.appendTo($breadcrumb);
|
||||
$breadcrumb.removeClass('d-none'); // hidden by default to avoid having ghost div in start screen
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after survey submit to update the breadcrumb to the right page.
|
||||
*/
|
||||
_updateBreadcrumb: function () {
|
||||
if (this.surveyBreadcrumbWidget) {
|
||||
var pageId = this.$('input[name=page_id]').val();
|
||||
this.surveyBreadcrumbWidget.updateBreadcrumb(parseInt(pageId));
|
||||
} else {
|
||||
this._initBreadcrumb();
|
||||
}
|
||||
},
|
||||
|
||||
_initTimer: function () {
|
||||
var self = this;
|
||||
var $timer = this.$('.o_survey_timer');
|
||||
if ($timer.length) {
|
||||
var timeLimitMinutes = this.options.timeLimitMinutes;
|
||||
var timer = this.options.timer;
|
||||
this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this, {
|
||||
'timer': timer,
|
||||
'timeLimitMinutes': timeLimitMinutes
|
||||
});
|
||||
this.surveyTimerWidget.attachTo($timer);
|
||||
this.surveyTimerWidget.on('time_up', this, function (ev) {
|
||||
self._submitForm({
|
||||
'skipValidation': true,
|
||||
'isFinish': true
|
||||
});
|
||||
});
|
||||
$timer.removeClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize datetimepicker in correct format and with constraints
|
||||
*/
|
||||
@@ -319,7 +526,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
var maxDateData = $dateGroup.data('maxdate');
|
||||
var maxDate = maxDateData ? this._formatDateTime(maxDateData) : moment().add(200, "y");
|
||||
|
||||
var datetimepickerFormat = time.getLangDateFormat()
|
||||
var datetimepickerFormat = time.getLangDateFormat();
|
||||
if ($dateGroup.find('input').data('questionType') === 'datetime') {
|
||||
datetimepickerFormat = time.getLangDatetimeFormat();
|
||||
} else {
|
||||
@@ -353,13 +560,33 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
});
|
||||
},
|
||||
|
||||
_formatDateTime: function (datetimeValue){
|
||||
_formatDateTime: function (datetimeValue) {
|
||||
return field_utils.format.datetime(moment(datetimeValue), null, {timezone: true});
|
||||
},
|
||||
|
||||
_initResultWidget: function () {
|
||||
var $result = this.$('.o_survey_result');
|
||||
if ($result.length) {
|
||||
this.surveyResultWidget = new publicWidget.registry.SurveyResultWidget(this);
|
||||
this.surveyResultWidget.attachTo($result);
|
||||
$result.fadeIn(this.fadeInOutDelay);
|
||||
}
|
||||
},
|
||||
|
||||
// ERRORS TOOLS
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
_showErrors: function (errors) {
|
||||
var self = this;
|
||||
var errorKeys = _.keys(errors);
|
||||
_.each(errorKeys, function (key) {
|
||||
self.$("#" + key + '>.o_survey_question_error').append($('<p>', {text: errors[key]})).addClass("slide_in");
|
||||
if (errorKeys[0] === key) {
|
||||
self._scrollToError(self.$('.js_question-wrapper#' + key));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_scrollToError: function ($target) {
|
||||
var scrollLocation = $target.offset().top;
|
||||
var navbarHeight = $('.o_main_navbar').height();
|
||||
@@ -375,8 +602,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
|
||||
* Clean all form errors in order to clean DOM before a new validation
|
||||
*/
|
||||
_resetErrors: function () {
|
||||
this.$('.o_survey_question_error').empty().toggleClass("d-none", true);
|
||||
this.$('.o_survey_error').toggleClass("d-none", true);
|
||||
this.$('.o_survey_question_error').empty().removeClass('slide_in');
|
||||
this.$('.o_survey_error').addClass('d-none');
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -3,10 +3,6 @@ odoo.define('survey.timer', function (require) {
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var interval = null;
|
||||
var $timer = null;
|
||||
var countDownDate = null;
|
||||
|
||||
publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
// Widget
|
||||
@@ -17,8 +13,9 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
*/
|
||||
init: function (parent, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.$timer = params.timer;
|
||||
this.$timeLimitMinutes = params.timeLimitMinutes;
|
||||
this.timer = params.timer;
|
||||
this.timeLimitMinutes = params.timeLimitMinutes;
|
||||
this.surveyTimerInterval = null;
|
||||
},
|
||||
|
||||
|
||||
@@ -30,13 +27,13 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
start: function () {
|
||||
var superDef = this._super.apply(this, arguments);
|
||||
|
||||
countDownDate = moment.utc(this.$timer).add(this.$timeLimitMinutes, 'minutes');
|
||||
if (this.$timeLimitMinutes <= 0 || countDownDate.diff(moment.utc(), 'seconds') < 0) {
|
||||
this.countDownDate = moment.utc(this.timer).add(this.timeLimitMinutes, 'minutes');
|
||||
if (this.timeLimitMinutes <= 0 || this.countDownDate.diff(moment.utc(), 'seconds') < 0) {
|
||||
this.trigger_up('time_up');
|
||||
} else {
|
||||
$timer = this.$el.find('.timer');
|
||||
this.$timer = this.$el.find('.timer');
|
||||
this._updateTimer(this);
|
||||
interval = setInterval(this._updateTimer.bind(this), 1000);
|
||||
this.surveyTimerInterval = setInterval(this._updateTimer.bind(this), 1000);
|
||||
}
|
||||
|
||||
return superDef;
|
||||
@@ -55,14 +52,14 @@ publicWidget.registry.SurveyTimerWidget = publicWidget.Widget.extend({
|
||||
* When the time runs out, it triggers a 'time_up' event to notify the parent widget.
|
||||
*/
|
||||
_updateTimer: function () {
|
||||
var timeLeft = countDownDate.diff(moment.utc(), 'seconds');
|
||||
var timeLeft = this.countDownDate.diff(moment.utc(), 'seconds');
|
||||
|
||||
if (timeLeft >= 0) {
|
||||
var timeLeftMinutes = parseInt(timeLeft / 60);
|
||||
var timeLeftSeconds = timeLeft - (timeLeftMinutes * 60);
|
||||
$timer.html(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds));
|
||||
this.$timer.html(this._formatTime(timeLeftMinutes) + ':' + this._formatTime(timeLeftSeconds));
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
clearInterval(this.surveyTimerInterval);
|
||||
this.trigger_up('time_up');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,3 +10,19 @@
|
||||
.js_question-wrapper {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.o_survey_breadcrumb_container {
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.o_survey_question_error {
|
||||
overflow: hidden;
|
||||
height: 0px;
|
||||
border: 0px;
|
||||
padding: 0rem 0.75rem;
|
||||
transition: height .5s ease;
|
||||
line-height: 4rem;
|
||||
&.slide_in {
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="survey.survey_breadcrumb_template">
|
||||
<ol class="breadcrumb justify-content-end">
|
||||
<t t-set="canGoBack" t-value="widget.canGoBack"/>
|
||||
<t t-foreach="widget.pages" t-as="page">
|
||||
<li t-att-class="'breadcrumb-item' + (page.id === widget.currentPageId ? ' active' : '')"
|
||||
t-att-data-page-id="page.id"
|
||||
t-att-data-page-title="page.title">
|
||||
<t t-if="widget.currentPageId === page.id">
|
||||
<!-- Users can only go back and not forward -->
|
||||
<!-- As soon as we reach the current page, set "can_go_back" to False -->
|
||||
<t t-set="canGoBack" t-value="false" />
|
||||
</t>
|
||||
<t t-if="canGoBack">
|
||||
<a href="#">
|
||||
<span t-esc="page.title" />
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="page.title" />
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,6 +1,17 @@
|
||||
odoo.define('survey.tour_test_certification_failure', function (require) {
|
||||
'use strict';
|
||||
|
||||
var SurveyFormWidget = require('survey.form');
|
||||
/**
|
||||
* Speed up fade-in fade-out to avoid useless delay in tests.
|
||||
*/
|
||||
SurveyFormWidget.include({
|
||||
_submitForm: function () {
|
||||
this.fadeInOutDelay = 0;
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This tour will test that, for the demo certification allowing 2 attempts, a user can
|
||||
* try and fail twice and will no longer be able to take the certification.
|
||||
@@ -10,11 +21,10 @@ var tour = require('web_tour.tour');
|
||||
|
||||
var failSteps = [{ // Page-1
|
||||
content: "Clicking on Start Certification",
|
||||
trigger: 'a.btn.btn-primary.btn-lg:contains("Start Certification")',
|
||||
trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")',
|
||||
}, { // Question: Do we sell Acoustic Bloc Screens?
|
||||
content: "Selecting answer 'No'",
|
||||
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") select',
|
||||
run: 'text No',
|
||||
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("No") input',
|
||||
}, { // Question: Select all the existing products
|
||||
content: "Ticking answer 'Fanta'",
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Fanta") input'
|
||||
@@ -32,19 +42,14 @@ var failSteps = [{ // Page-1
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Height") input'
|
||||
}, { // Question: How many versions of the Corner Desk do we have?
|
||||
content: "Selecting answer '2'",
|
||||
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") select',
|
||||
run: 'text 2',
|
||||
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2") input',
|
||||
}, { // Question: Do you think we have missing products in our catalog? (not rated)
|
||||
content: "Missing products",
|
||||
trigger: 'div.js_question-wrapper:contains("Do you think we have missing products in our catalog") textarea',
|
||||
run: "text I don't know products enough to be able to answer that",
|
||||
}, { // Page-2 Question: How much do we sell our Cable Management Box?
|
||||
content: "Selecting answer '80$'",
|
||||
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select',
|
||||
run: function () {
|
||||
var $select = $('div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select');
|
||||
$select.val($('option:contains("80$")').val());
|
||||
}
|
||||
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$") input',
|
||||
}, { // Question: Select all the the products that sell for 100$ or more
|
||||
content: "Ticking answer 'Corner Desk Right Sit'",
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit") input'
|
||||
@@ -55,11 +60,7 @@ var failSteps = [{ // Page-1
|
||||
content: "Ticking answer 'Office Chair Black'",
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Office Chair Black") input'
|
||||
}, { // Question: What do you think about our prices (not rated)?
|
||||
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") select',
|
||||
run: function () {
|
||||
var $select = $('div.js_question-wrapper:contains("What do you think about our prices") select');
|
||||
$select.val($('option:contains("Correctly priced")').val());
|
||||
}
|
||||
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Correctly priced") input',
|
||||
}, {
|
||||
content: "Finish Survey",
|
||||
trigger: 'button[type="submit"]',
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
odoo.define('survey.tour_test_certification_success', function (require) {
|
||||
'use strict';
|
||||
|
||||
var SurveyFormWidget = require('survey.form');
|
||||
/**
|
||||
* Speed up fade-in fade-out to avoid useless delay in tests.
|
||||
*/
|
||||
SurveyFormWidget.include({
|
||||
_submitForm: function () {
|
||||
this.fadeInOutDelay = 0;
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('test_certification_success', {
|
||||
@@ -9,11 +20,10 @@ tour.register('test_certification_success', {
|
||||
},
|
||||
[{ // Page-1
|
||||
content: "Clicking on Start Certification",
|
||||
trigger: 'a.btn.btn-primary.btn-lg:contains("Start Certification")',
|
||||
trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")',
|
||||
}, { // Question: Do we sell Acoustic Bloc Screens?
|
||||
content: "Selecting answer 'Yes'",
|
||||
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") select',
|
||||
run: 'text Yes',
|
||||
trigger: 'div.js_question-wrapper:contains("Do we sell Acoustic Bloc Screens") label:contains("Yes") input',
|
||||
}, { // Question: Select all the existing products
|
||||
content: "Ticking answer 'Chair floor protection'",
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the existing products") label:contains("Chair floor protection") input'
|
||||
@@ -31,19 +41,14 @@ tour.register('test_certification_success', {
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the available customizations for our Customizable Desk") label:contains("Legs") input'
|
||||
}, { // Question: How many versions of the Corner Desk do we have?
|
||||
content: "Selecting answer '2'",
|
||||
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") select',
|
||||
run: 'text 2',
|
||||
trigger: 'div.js_question-wrapper:contains("How many versions of the Corner Desk do we have") label:contains("2") input',
|
||||
}, { // Question: Do you think we have missing products in our catalog? (not rated)
|
||||
content: "Missing products",
|
||||
trigger: 'div.js_question-wrapper:contains("Do you think we have missing products in our catalog") textarea',
|
||||
run: "text I think we should make more versions of the customizable desk, it's such an amazing product!",
|
||||
}, { // Page-2 Question: How much do we sell our Cable Management Box?
|
||||
content: "Selecting answer '80$' (wrong one)",
|
||||
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select',
|
||||
run: function () {
|
||||
var $select = $('div.js_question-wrapper:contains("How much do we sell our Cable Management Box") select');
|
||||
$select.val($('option:contains("80$")').val());
|
||||
}
|
||||
trigger: 'div.js_question-wrapper:contains("How much do we sell our Cable Management Box") label:contains("80$") input',
|
||||
}, { // Question: Select all the the products that sell for 100$ or more
|
||||
content: "Ticking answer 'Corner Desk Right Sit'",
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Corner Desk Right Sit") input'
|
||||
@@ -55,11 +60,7 @@ tour.register('test_certification_success', {
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the the products that sell for 100$ or more") label:contains("Large Desk") input'
|
||||
}, { // Question: What do you think about our prices (not rated)?
|
||||
content: "Selecting answer 'Underpriced'",
|
||||
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") select',
|
||||
run: function () {
|
||||
var $select = $('div.js_question-wrapper:contains("What do you think about our prices") select');
|
||||
$select.val($('option:contains("Underpriced")').val());
|
||||
}
|
||||
trigger: 'div.js_question-wrapper:contains("What do you think about our prices") label:contains("Underpriced") input',
|
||||
}, {
|
||||
content: "Finish Survey",
|
||||
trigger: 'button[type="submit"]',
|
||||
|
||||
@@ -10,7 +10,7 @@ tour.register('test_survey', {
|
||||
// Page-1
|
||||
{
|
||||
content: 'Click on Start',
|
||||
trigger: 'a.btn:contains("Start")',
|
||||
trigger: 'button.btn:contains("Start")',
|
||||
}, {
|
||||
content: 'Answer Where do you live',
|
||||
trigger: 'div.js_question-wrapper:contains("Where do you live") input',
|
||||
@@ -21,11 +21,7 @@ tour.register('test_survey', {
|
||||
run: 'text 05/05/1980',
|
||||
}, {
|
||||
content: 'Answer How frequently do you buy products online',
|
||||
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online") select',
|
||||
run: function () {
|
||||
var $select = $('div.js_question-wrapper:contains("How frequently do you buy products online") select');
|
||||
$select.val($('option:contains("Once a month")').val());
|
||||
}
|
||||
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online") label:contains("Once a month") input',
|
||||
}, {
|
||||
content: 'Answer How many times did you order products on our website',
|
||||
trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website") input',
|
||||
|
||||
@@ -8,7 +8,7 @@ tour.register('test_survey_prefill', {
|
||||
url: '/survey/start/b137640d-14d4-4748-9ef6-344caaaaaae'
|
||||
},
|
||||
[{ // Page-1
|
||||
trigger: 'a.btn.btn-primary.btn-lg:contains("Start Survey")',
|
||||
trigger: 'button.btn.btn-primary.btn-lg:contains("Start Survey")',
|
||||
}, { // Question: Where do you live ?
|
||||
trigger: 'div.js_question-wrapper:contains("Where do you live ?") input',
|
||||
run: 'text Grand-Rosiere',
|
||||
@@ -16,11 +16,7 @@ tour.register('test_survey_prefill', {
|
||||
trigger: 'div.js_question-wrapper:contains("When is your date of birth ?") input',
|
||||
run: 'text 05/05/1980',
|
||||
}, { // Question: How frequently do you buy products online ?
|
||||
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online ?") select',
|
||||
run: function() {
|
||||
var optionID = $('option:contains("Once a week")').val();
|
||||
$('div.js_question-wrapper:contains("How frequently do you buy products online ?") select').val(optionID);
|
||||
},
|
||||
trigger: 'div.js_question-wrapper:contains("How frequently do you buy products online ?") label:contains("Once a week") input',
|
||||
}, { // Question: How many times did you order products on our website ?
|
||||
trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website ?") input',
|
||||
run: 'text 42',
|
||||
@@ -80,8 +76,8 @@ tour.register('test_survey_prefill', {
|
||||
return;
|
||||
}
|
||||
|
||||
var $selectQ3 = $('div.js_question-wrapper:contains("How frequently do you buy products online ?") select');
|
||||
if ($selectQ3.val() !== $('option:contains("Once a week")').val()) {
|
||||
var $inputQ3 = $('div.js_question-wrapper:contains("How frequently do you buy products online ?") label:contains("Once a week") input');
|
||||
if (!$inputQ3.is(':checked')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ class TestSurveyCommon(common.SavepointCase):
|
||||
return self.url_open('/survey/start/%s' % survey.access_token)
|
||||
|
||||
def _access_page(self, survey, token):
|
||||
return self.url_open('/survey/fill/%s/%s' % (survey.access_token, token))
|
||||
return self.url_open('/survey/%s/%s' % (survey.access_token, token))
|
||||
|
||||
def _access_submit(self, survey, token, post_data):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestSurveyFlow(common.TestSurveyCommon, HttpCase):
|
||||
answers.invalidate_cache() # TDE note: necessary as lots of sudo in controllers messing with cache
|
||||
|
||||
# -> this should have generated answer lines
|
||||
self.assertAnswer(answers, 'skip', page_0)
|
||||
self.assertAnswer(answers, 'in_progress', page_0)
|
||||
self.assertAnswerLines(page_0, answers, answer_data)
|
||||
|
||||
# Customer is redirected on second page and begins filling it
|
||||
|
||||
@@ -37,7 +37,6 @@ class TestUiCertification(HttpCaseWithUserDemo):
|
||||
'title': 'Do we sell Acoustic Bloc Screens?',
|
||||
'sequence': 2,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
@@ -116,7 +115,6 @@ class TestUiCertification(HttpCaseWithUserDemo):
|
||||
'title': 'How many versions of the Corner Desk do we have?',
|
||||
'sequence': 5,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
@@ -149,7 +147,6 @@ class TestUiCertification(HttpCaseWithUserDemo):
|
||||
'title': 'How much do we sell our Cable Management Box?',
|
||||
'sequence': 8,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
@@ -213,7 +210,6 @@ class TestUiCertification(HttpCaseWithUserDemo):
|
||||
'title': 'What do you think about our prices (not rated)?',
|
||||
'sequence': 10,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
|
||||
@@ -40,7 +40,6 @@ class TestUiFeedback(HttpCaseWithUserDemo):
|
||||
'title': 'How frequently do you buy products online ?',
|
||||
'sequence': 4,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'comments_allowed': True,
|
||||
'comment_count_as_answer': True,
|
||||
'constr_mandatory': True,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/survey/static/src/js/survey_timer.js" />
|
||||
<script type="text/javascript" src="/survey/static/src/js/survey_breadcrumb.js" />
|
||||
<script type="text/javascript" src="/survey/static/src/js/survey_form.js" />
|
||||
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
|
||||
|
||||
|
||||
@@ -148,9 +148,8 @@
|
||||
<field name="matrix_subtype" attrs="{'invisible':[('question_type','not in',['matrix'])],'required':[('question_type','=','matrix')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Display mode" attrs="{'invisible':[('question_type','not in',['simple_choice', 'multiple_choice'])]}">
|
||||
<field name="display_mode" string="Format" attrs="{'invisible':[('question_type','not in',['simple_choice'])],'required':[('question_type','=','simple_choice')]}"/>
|
||||
<field name="column_nb" string="Number of columns" attrs="{'invisible':[('display_mode','=','dropdown'), ('question_type','=','simple_choice')]}"/>
|
||||
<group string="Display" attrs="{'invisible':[('question_type','not in',['simple_choice', 'multiple_choice'])]}">
|
||||
<field name="column_nb" string="Number of columns"/>
|
||||
</group>
|
||||
<group string="Allow Comments" attrs="{'invisible':[('question_type','not in',['simple_choice','multiple_choice', 'matrix'])]}">
|
||||
<field name='comments_allowed' />
|
||||
|
||||
@@ -15,108 +15,56 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- First page of a survey -->
|
||||
<template id="survey_page_start" name="Survey: start page">
|
||||
<template id="survey_page_fill" name="Survey: main page (take survey)">
|
||||
<t t-call="survey.layout">
|
||||
<div class="wrap">
|
||||
<div class="oe_structure" id="oe_structure_survey_init_1"/>
|
||||
<div class="container">
|
||||
<t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
|
||||
<div class='jumbotron mt32'>
|
||||
<h1 t-field='survey.title' />
|
||||
<div t-field='survey.description' class="oe_no_empty"/>
|
||||
<div t-if="survey.is_time_limited">
|
||||
<p>
|
||||
<span>Time limit for this survey: </span>
|
||||
<span class="font-weight-bold text-danger" t-field="survey.time_limit" t-options="{'widget': 'duration', 'unit': 'minute'}"></span>
|
||||
</p>
|
||||
</div>
|
||||
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/fill/%s/%s' % (survey.access_token, answer.access_token)">
|
||||
<t t-if="survey.certification">
|
||||
Start Certification
|
||||
</t>
|
||||
<t t-else="">
|
||||
Start Survey
|
||||
</t>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_survey_init_2"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="survey_page_main" name="Survey: main page (take survey)">
|
||||
<t t-call="survey.layout">
|
||||
<div class="wrap">
|
||||
<div class="oe_structure" id="oe_structure_survey_survey_1"/>
|
||||
<div class="container o_survey_form">
|
||||
<t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
|
||||
<t t-call="survey.survey_page_main_header" />
|
||||
<t t-call="survey.survey_page_content" />
|
||||
|
||||
<t t-call="survey.survey_fill_header" />
|
||||
<t t-call="survey.survey_fill_form" />
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_survey_survey_2"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="survey_page_content" name="Survey: main page content">
|
||||
<t t-set="survey_form_readonly" t-value="false"/>
|
||||
<t t-if="survey.questions_layout == 'one_page'" t-set="page_or_question_id" t-value="None" />
|
||||
<t t-if="survey.questions_layout == 'page_per_section'" t-set="page_or_question_id" t-value="page.id" />
|
||||
<t t-if="survey.questions_layout == 'page_per_question'" t-set="page_or_question_id" t-value="question.id" />
|
||||
<template id="survey_fill_header" name="Survey: main page header">
|
||||
<div class="o_page_header o_survey_header">
|
||||
<div class="container m-0 p-0">
|
||||
<div class="row">
|
||||
<div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
|
||||
<div t-if="survey.is_time_limited and answer.state != 'done'" class="o_survey_timer col-lg-4 d-none">
|
||||
<h1 class="timer text-right">00:00</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-att-class="'o_survey_breadcrumb_container mt8 bg-200' + (' d-none ' if answer.state != 'in_progress' else '')"
|
||||
t-if="survey.questions_layout == 'page_per_section' and answer.state != 'done'"
|
||||
t-att-data-can-go-back="survey.users_can_go_back"
|
||||
t-att-data-pages="json.dumps(breadcrumb_pages)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="survey_fill_form" name="Survey: main page content">
|
||||
<t t-set="survey_form_readonly" t-value="false"/>
|
||||
<form role="form" method="post" t-att-name="survey.id"
|
||||
t-att-data-answer-token="answer.access_token"
|
||||
t-att-data-survey-token="survey.access_token"
|
||||
t-att-data-timer="answer.start_datetime.isoformat() if survey.is_time_limited else False"
|
||||
t-att-data-time-limit-minutes="survey.time_limit">
|
||||
t-att-data-timer="answer.start_datetime.isoformat() if survey.is_time_limited
|
||||
and answer.start_datetime and answer.state != 'done' else False"
|
||||
t-att-data-time-limit-minutes="survey.time_limit"
|
||||
t-att-data-is-start-screen="answer.state == 'new'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="token" t-att-value="answer.access_token" />
|
||||
<div class="o_survey_error alert alert-danger d-none" role="alert">
|
||||
<p>There was an error during the validation of the survey.</p>
|
||||
</div>
|
||||
<t t-if="survey.questions_layout == 'one_page'">
|
||||
<t t-foreach='survey.question_and_page_ids' t-as='question'>
|
||||
<h2 t-if="question.is_page" t-field='question.title' class="o_survey_title" />
|
||||
<t t-if="not question.is_page and question in answer.predefined_question_ids" t-call="survey.question_container"/>
|
||||
</t>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<button type="submit" class="btn btn-primary">Submit Survey</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="survey.questions_layout == 'page_per_section'">
|
||||
<h2 t-field='page.title' class="o_survey_title" />
|
||||
<div t-field='page.description' class="oe_no_empty"/>
|
||||
|
||||
<input type="hidden" name="page_id" t-att-value="page.id" />
|
||||
<t t-foreach='page.question_ids' t-as='question'>
|
||||
<t t-if="question in answer.predefined_question_ids" t-call="survey.question_container"/>
|
||||
</t>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<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">Previous page</button>
|
||||
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
|
||||
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="survey.questions_layout == 'page_per_question'">
|
||||
<input type="hidden" name="question_id" t-att-value="question.id" />
|
||||
<t t-call="survey.question_container"/>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<button t-if="survey.users_can_go_back and question != answer.predefined_question_ids[0]" type="submit" class="btn btn-secondary"
|
||||
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Previous page</button>
|
||||
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
|
||||
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_survey_form_content">
|
||||
<t t-if="answer.state == 'new'" t-call="survey.survey_fill_form_start"/>
|
||||
<t t-elif="answer.state == 'in_progress'" t-call="survey.survey_fill_form_in_progress" />
|
||||
<t t-else="" t-call="survey.survey_fill_form_done"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal used to display error message, i.c.o. ajax error -->
|
||||
@@ -134,43 +82,131 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="survey_page_main_header" name="Survey: main page header">
|
||||
<div class="o_page_header o_survey_header">
|
||||
<div class="container m-0 p-0">
|
||||
<template id="survey_fill_form_start" name="Survey: start form content">
|
||||
<div class="wrap o_survey_start">
|
||||
<div class='mt32 mb32'>
|
||||
<div t-field='survey.description' class="oe_no_empty"/>
|
||||
<div t-if="survey.is_time_limited">
|
||||
<p>
|
||||
<span>Time limit for this survey: </span>
|
||||
<span class="font-weight-bold text-danger" t-field="survey.time_limit" t-options="{'widget': 'duration', 'unit': 'minute'}"></span>
|
||||
</p>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<t t-if="survey.certification">
|
||||
Start Certification
|
||||
</t>
|
||||
<t t-else="">
|
||||
Start Survey
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="survey_fill_form_in_progress" name="Survey: form with questions">
|
||||
<t t-if="survey.questions_layout == 'one_page'">
|
||||
<t t-foreach='survey.question_and_page_ids' t-as='question'>
|
||||
<h2 t-if="question.is_page" t-field='question.title' class="o_survey_title" />
|
||||
<t t-if="not question.is_page and question in answer.predefined_question_ids" t-call="survey.question_container"/>
|
||||
</t>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<button type="submit" value="finish" class="btn btn-primary">Submit Survey</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="survey.questions_layout == 'page_per_section'">
|
||||
<h2 t-field='page.title' class="o_survey_title" />
|
||||
<div t-field='page.description' class="oe_no_empty"/>
|
||||
|
||||
<input type="hidden" name="page_id" t-att-value="page.id" />
|
||||
<t t-foreach='page.question_ids' t-as='question'>
|
||||
<t t-if="question in answer.predefined_question_ids" t-call="survey.question_container"/>
|
||||
</t>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<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">Previous page</button>
|
||||
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
|
||||
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="survey.questions_layout == 'page_per_question'">
|
||||
<input type="hidden" name="question_id" t-att-value="question.id" />
|
||||
<t t-call="survey.question_container"/>
|
||||
|
||||
<div class="text-center mt16 mb256">
|
||||
<button t-if="survey.users_can_go_back and question != answer.predefined_question_ids[0]" type="submit" class="btn btn-secondary"
|
||||
name="button_submit" value="previous" t-att-data-previous-page-id="previous_page_id">Previous page</button>
|
||||
<button type="submit" t-att-value="'next' if not last else 'finish'" class="btn btn-primary">
|
||||
<t t-esc="'Next Page' if not last else 'Submit Survey'"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</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">
|
||||
<h1>Thank you!</h1>
|
||||
<div t-field="survey.description_done" class="oe_no_empty" />
|
||||
<div class="row">
|
||||
<div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
|
||||
<div t-if="survey.is_time_limited" class="o_survey_timer col-lg-4">
|
||||
<h1 class="timer text-right">00:00</h1>
|
||||
<div class="col">
|
||||
<t t-if="survey.scoring_type != 'no_scoring'">
|
||||
<div>You scored <t t-esc="answer.scoring_percentage" />%</div>
|
||||
<t t-if="answer.scoring_success">
|
||||
<div>Congratulations, you have passed the test!</div>
|
||||
|
||||
<div t-if="survey.certification" class="mt16 mb16">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-lg"
|
||||
t-att-href="'/survey/%s/get_certification' % survey.id">
|
||||
<i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/>
|
||||
Download certification
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div>Unfortunately, you have failed the test.</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="survey.survey_button_retake"/>
|
||||
<div t-if="survey.scoring_type != 'scoring_without_answers'">
|
||||
If you wish, you can <a t-att-href="'/survey/print/%s?answer_token=%s&review=True' % (survey.access_token, answer.access_token)">review your answers</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 text-center" t-if="survey.certification_give_badge and answer.scoring_success">
|
||||
<img t-att-src="'/web/image/gamification.badge/%s/image_128' % survey.certification_badge_id.id"/>
|
||||
<div>You received the badge <span class="font-weight-bold" t-esc="survey.certification_badge_id.name"/>!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_survey_result p-4" t-if="graph_data">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active survey_graph"
|
||||
t-att-data-scoring_percentage="answer.scoring_percentage"
|
||||
t-att-id="'survey_graph_question_%d' % answer.id"
|
||||
t-att-data-question_id="answer.id"
|
||||
data-graph-type="doughnut"
|
||||
t-att-data-graph-data="graph_data">
|
||||
<canvas id="doughnut_chart"></canvas>
|
||||
<span class="o_overall_performance"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol t-if="survey.questions_layout == 'page_per_section'" class="breadcrumb mt8 justify-content-end">
|
||||
<t t-set="can_go_back" t-value="survey.users_can_go_back" />
|
||||
<t t-foreach='survey.page_ids' t-as='breadcrumb_page'>
|
||||
<li t-attf-class="breadcrumb-item #{'active' if breadcrumb_page == page else ''}">
|
||||
<t t-if="page == breadcrumb_page">
|
||||
<!-- Users can only go back and not forward -->
|
||||
<!-- As soon as we reach the current page, set "can_go_back" to False -->
|
||||
<t t-set="can_go_back" t-value="False" />
|
||||
</t>
|
||||
<t t-if="can_go_back">
|
||||
<a href="#" t-att-data-previous-page-id="breadcrumb_page.id">
|
||||
<span t-field='breadcrumb_page.title' />
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field='breadcrumb_page.title' />
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Question widgets -->
|
||||
<template id="question_container" name="Survey: question container">
|
||||
<t t-set="answer_lines" t-value="answer.user_input_line_ids.filtered(lambda line: line.question_id == question)"/>
|
||||
<div class="js_question-wrapper" t-att-id="question.id">
|
||||
<div class="js_question-wrapper" 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>
|
||||
@@ -184,7 +220,7 @@
|
||||
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
|
||||
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
|
||||
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
|
||||
<div class="o_survey_question_error alert alert-danger d-none" role="alert"></div>
|
||||
<div class="o_survey_question_error alert alert-danger" role="alert"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -194,13 +230,19 @@
|
||||
</template>
|
||||
|
||||
<template id="question_char_box" name="Question: text box">
|
||||
<input type="text" class="form-control o_survey_question_text_box" 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"/>
|
||||
<input t-att-type="'email' if question.validation_email else 'text'"
|
||||
class="form-control o_survey_question_text_box" 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"
|
||||
t-att-data-validation-length-max="question.validation_length_max if question.validation_required else False"/>
|
||||
</template>
|
||||
|
||||
<template id="question_numerical_box" name="Question: numerical box">
|
||||
<input type="number" step="any" class="form-control o_survey_question_numerical_box" 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-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"/>
|
||||
</template>
|
||||
|
||||
<template id="question_date" name="Question: date box">
|
||||
@@ -234,40 +276,13 @@
|
||||
<template id="question_simple_choice" name="Question: simple choice">
|
||||
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id)"/>
|
||||
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_char_box)"/>
|
||||
<div t-if="question.display_mode == 'dropdown'" class="o_survey_form_choice o_survey_question_simple_choice_dropdown row"
|
||||
t-att-data-name="question.id"
|
||||
data-question-type="simple_choice_dropdown">
|
||||
<div class="col-lg-12">
|
||||
<select class="form-control o_survey_form_choice_item">
|
||||
<option disabled="1" selected="1" value=""
|
||||
t-att-selected="not answer_line and (not comment_line or not question.comment_count_as_answer) and 'selected' or None">Choose...</option>
|
||||
<t t-foreach='question.suggested_answer_ids' t-as='label'>
|
||||
<option t-att-value='label.id'
|
||||
t-att-selected="answer_line and answer_line.suggested_answer_id == label and 'selected' or None"><t t-esc='label.value'/></option>
|
||||
</t>
|
||||
<t t-if='question.comments_allowed and question.comment_count_as_answer'>
|
||||
<option class="o_survey_js_form_other_comment" value="-1"
|
||||
t-att-selected="comment_line and question.comment_count_as_answer and 'selected' or None">
|
||||
<span t-esc="question.comments_message"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="col-lg-6">
|
||||
<textarea type="text" class="form-control"
|
||||
t-attf-class="form-control #{'' if comment_line else 'd-none'}"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
|
||||
</div>
|
||||
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12 mt16">
|
||||
<span t-field="question.comments_message"/>
|
||||
<textarea type="text" class="form-control o_survey_comment"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="question.display_mode == 'columns'" class="row o_survey_form_choice o_survey_question_simple_choice_radio"
|
||||
<div class="row o_survey_form_choice"
|
||||
t-att-data-name="question.id"
|
||||
data-question-type="simple_choice_radio">
|
||||
<div t-foreach='question.suggested_answer_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
|
||||
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
|
||||
<input type="radio" t-att-value='label.id' class="o_survey_form_choice_item"
|
||||
t-att-name='question.id'
|
||||
t-att-checked="answer_line and answer_line.suggested_answer_id.id == label.id and 'checked' or None"/>
|
||||
<span t-field='label.value'/>
|
||||
</label>
|
||||
@@ -275,6 +290,7 @@
|
||||
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12" >
|
||||
<label>
|
||||
<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment" value="-1"
|
||||
t-att-name='question.id'
|
||||
t-att-checked="comment_line and 'checked' or None"/>
|
||||
<span t-field="question.comments_message" />
|
||||
</label>
|
||||
@@ -297,6 +313,7 @@
|
||||
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.suggested_answer_id == label)"/>
|
||||
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
|
||||
<input type="checkbox" t-att-value='label.id' class="o_survey_form_choice_item"
|
||||
t-att-name="question.id"
|
||||
t-att-checked="'checked' if answer_line else None"/>
|
||||
<span t-field='label.value'/>
|
||||
</label>
|
||||
@@ -305,6 +322,7 @@
|
||||
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_ck_comments col-lg-12" >
|
||||
<label>
|
||||
<input type="checkbox" class="o_survey_form_choice_item o_survey_js_form_other_comment" value="-1"
|
||||
t-att-name="question.id"
|
||||
t-att-checked="comment_line and 'checked' or None"/>
|
||||
<span t-field="question.comments_message" />
|
||||
</label>
|
||||
@@ -322,7 +340,8 @@
|
||||
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_char_box)"/>
|
||||
<table class="table table-hover o_survey_question_matrix"
|
||||
t-att-data-name="question.id"
|
||||
t-att-data-question-type="question.question_type">
|
||||
t-att-data-question-type="question.question_type"
|
||||
t-att-data-sub-questions="question.matrix_row_ids.ids">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
@@ -335,7 +354,7 @@
|
||||
<td 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)"/>
|
||||
<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'
|
||||
t-att-name="'%s_%s' % (question.id, row_label.id)" t-att-value='col_label.id'
|
||||
t-att-checked="'checked' if answer else None"
|
||||
t-att-data-row-id="row_label.id"/>
|
||||
</td>
|
||||
@@ -347,53 +366,5 @@
|
||||
<textarea type="text" class="form-control" t-att-name="'%s_%s' % (question.id, 'comment')"><t t-esc="comment_line.value_char_box if comment_line else ''"/></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Survey: printable page view (all pages) -->
|
||||
<template id="survey_page_print" name="Survey: print page">
|
||||
<t t-call="survey.layout">
|
||||
<t t-set="survey_form_readonly" t-value="true"/>
|
||||
<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'/></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"/>
|
||||
</div>
|
||||
<div role="form">
|
||||
<fieldset disabled="disabled">
|
||||
<t t-foreach='survey.question_and_page_ids' t-as='question'>
|
||||
<hr t-if="question.is_page and question != survey.page_ids[0]" />
|
||||
<div t-if="question.is_page" class="o_page_header">
|
||||
<h1 t-field='question.title' />
|
||||
<div t-if="question.description" t-field='question.description' class="oe_no_empty"/>
|
||||
</div>
|
||||
<t t-if="not question.is_page and not answer or (question in answer.predefined_question_ids)" >
|
||||
<t t-set="answer_lines" t-value="answer.user_input_line_ids.filtered(lambda line: line.question_id == question)"/>
|
||||
<div class="js_question-wrapper" t-att-id="question.id">
|
||||
<h2>
|
||||
<span t-field='question.title'/>
|
||||
<span t-if="question.constr_mandatory" class="text-danger">*</span>
|
||||
<span t-if="quizz_correction" class="badge badge-pill" t-att-data-score-question="question.id"></span>
|
||||
</h2>
|
||||
<t t-if="question.description"><div class="text-muted oe_no_empty" t-field='question.description'/></t>
|
||||
<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>
|
||||
<t t-if="question.question_type == 'date'"><t t-call="survey.question_date"/></t>
|
||||
<t t-if="question.question_type == 'datetime'"><t t-call="survey.question_datetime"/></t>
|
||||
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
|
||||
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
|
||||
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
|
||||
<div class="o_survey_question_error alert alert-danger d-none" role="alert"></div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -69,65 +69,6 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Finished (taken and finished) survey page -->
|
||||
<template id="survey_closed_finished" name="Survey: finished">
|
||||
<t t-call="survey.layout">
|
||||
<div class="wrap">
|
||||
<div class="container">
|
||||
<t t-if="answer.test_entry" t-call="survey.survey_button_form_view" />
|
||||
<div class="jumbotron mt32">
|
||||
<h1>Thank you!</h1>
|
||||
<div t-field="survey.description_done" class="oe_no_empty" />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<t t-if="survey.scoring_type != 'no_scoring'">
|
||||
<div>You scored <t t-esc="answer.scoring_percentage" />%</div>
|
||||
<t t-if="answer.scoring_success">
|
||||
<div>Congratulations, you have passed the test!</div>
|
||||
|
||||
<div t-if="survey.certification" class="mt16 mb16">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-lg"
|
||||
t-att-href="'/survey/%s/get_certification' % survey.id">
|
||||
<i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/>
|
||||
Download certification
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div>Unfortunately, you have failed the test.</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="survey.survey_button_retake"/>
|
||||
<div t-if="survey.scoring_type != 'scoring_without_answers'">
|
||||
If you wish, you can <a t-att-href="'/survey/print/%s?answer_token=%s&review=True' % (survey.access_token, answer.access_token)">review your answers</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 text-center" t-if="survey.certification_give_badge and answer.scoring_success">
|
||||
<img t-att-src="'/web/image/gamification.badge/%s/image_128' % survey.certification_badge_id.id"/>
|
||||
<div>You received the badge <span class="font-weight-bold" t-esc="survey.certification_badge_id.name"/>!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_survey_result p-4" t-if="graph_data">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active survey_graph"
|
||||
t-att-data-scoring_percentage="answer.scoring_percentage"
|
||||
t-att-id="'survey_graph_question_%d' % answer.id"
|
||||
t-att-data-question_id="answer.id"
|
||||
data-graph-type="doughnut"
|
||||
t-att-data-graph-data="graph_data">
|
||||
<canvas id="doughnut_chart"></canvas>
|
||||
<span class="o_overall_performance"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Tools / Utilities -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Survey: printable page view (all pages) -->
|
||||
<template id="survey_page_print" name="Survey: print page">
|
||||
<t t-call="survey.layout">
|
||||
<t t-set="survey_form_readonly" t-value="true"/>
|
||||
<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'/></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"/>
|
||||
</div>
|
||||
<div role="form">
|
||||
<fieldset disabled="disabled">
|
||||
<t t-foreach='survey.question_and_page_ids' t-as='question'>
|
||||
<hr t-if="question.is_page and question != survey.page_ids[0]" />
|
||||
<div t-if="question.is_page" class="o_page_header">
|
||||
<h1 t-field='question.title' />
|
||||
<div t-if="question.description" t-field='question.description' class="oe_no_empty"/>
|
||||
</div>
|
||||
<t t-if="not question.is_page and not answer or (question in answer.predefined_question_ids)" >
|
||||
<t t-set="answer_lines" t-value="answer.user_input_line_ids.filtered(lambda line: line.question_id == question)"/>
|
||||
<div class="js_question-wrapper" t-att-id="question.id">
|
||||
<h2>
|
||||
<span t-field='question.title'/>
|
||||
<span t-if="question.constr_mandatory" class="text-danger">*</span>
|
||||
<span t-if="quizz_correction" class="badge badge-pill" t-att-data-score-question="question.id"></span>
|
||||
</h2>
|
||||
<t t-if="question.description"><div class="text-muted oe_no_empty" t-field='question.description'/></t>
|
||||
<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>
|
||||
<t t-if="question.question_type == 'date'"><t t-call="survey.question_date"/></t>
|
||||
<t t-if="question.question_type == 'datetime'"><t t-call="survey.question_datetime"/></t>
|
||||
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.question_simple_choice"/></t>
|
||||
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.question_multiple_choice"/></t>
|
||||
<t t-if="question.question_type == 'matrix'"><t t-call="survey.question_matrix"/></t>
|
||||
<div class="o_survey_question_error alert alert-danger" role="alert"></div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -4,7 +4,6 @@
|
||||
<template id="survey_page_statistics" name="Survey: result statistics page">
|
||||
<t t-call="survey.layout">
|
||||
<t t-set="page_record_limit" t-value="10"/><!-- Change this record_limit to change number of record per page-->
|
||||
<div class="oe_structure" id="oe_structure_survey_result_1"/>
|
||||
<div class="container o_survey_result">
|
||||
<t t-call="survey.survey_button_form_view" />
|
||||
<div class="jumbotron mt32">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<field name="email"/>
|
||||
<field name="partner_id"/>
|
||||
<filter name="completed" string="Completed" domain="[('state', '=', 'done')]"/>
|
||||
<filter string="Partially Completed" name="partially_completed" domain="[('state', '=', 'skip')]"/>
|
||||
<filter string="In Progress" name="in_progress" domain="[('state', '=', 'in_progress')]"/>
|
||||
<filter string="New" name="new" domain="[('state', '=', 'new')]"/>
|
||||
<separator/>
|
||||
<filter string="Quizz passed" name="scoring_success" domain="[('scoring_success','=', True)]"/>
|
||||
@@ -122,7 +122,7 @@
|
||||
<field name="create_date"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right mr4">
|
||||
<field name="state" widget="label_selection" options="{'classes': {'new': 'default', 'done': 'success', 'skip':'warning'}}"/>
|
||||
<field name="state" widget="label_selection" options="{'classes': {'new': 'default', 'done': 'success', 'in_progress':'warning'}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,6 @@ class TestUi(TestUICommon):
|
||||
'title': 'What type of wood is the best for furniture?',
|
||||
'sequence': 2,
|
||||
'question_type': 'simple_choice',
|
||||
'display_mode': 'dropdown',
|
||||
'constr_mandatory': True,
|
||||
'suggested_answer_ids': [
|
||||
(0, 0, {
|
||||
|
||||
@@ -86,11 +86,10 @@ var buyCertificationSteps = [{
|
||||
|
||||
var failCertificationSteps = [{
|
||||
content: 'eLearning: start certification',
|
||||
trigger: 'a:contains("Start Certification")'
|
||||
trigger: 'button:contains("Start Certification")'
|
||||
}, { // Question: What type of wood is the best for furniture?
|
||||
content: 'Survey: selecting answer "Fir"',
|
||||
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") select',
|
||||
run: 'text Fir',
|
||||
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Fir") input',
|
||||
}, { // Question: Select all the furniture shown in the video
|
||||
content: 'Survey: ticking answer "Table"',
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Table") input'
|
||||
@@ -109,11 +108,10 @@ var retrySteps = [{
|
||||
|
||||
var succeedCertificationSteps = [{
|
||||
content: 'eLearning: start certification',
|
||||
trigger: 'a:contains("Start Certification")'
|
||||
trigger: 'button:contains("Start Certification")'
|
||||
}, { // Question: What type of wood is the best for furniture?
|
||||
content: 'Survey: selecting answer "Oak"',
|
||||
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") select',
|
||||
run: 'text Oak',
|
||||
trigger: 'div.js_question-wrapper:contains("What type of wood is the best for furniture") label:contains("Oak") input',
|
||||
}, { // Question: Select all the furniture shown in the video
|
||||
content: 'Survey: ticking answer "Chair"',
|
||||
trigger: 'div.js_question-wrapper:contains("Select all the furniture shown in the video") label:contains("Chair") input'
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<field name="sequence">2</field>
|
||||
<field name="title">What type of wood is the best for furniture?</field>
|
||||
<field name="question_type">simple_choice</field>
|
||||
<field name="display_mode">dropdown</field>
|
||||
<field name="constr_mandatory" eval="True" />
|
||||
</record>
|
||||
<record model="survey.question.answer" id="furniture_certification_page_1_question_1_choice_1">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="survey_closed_finished_inherit_website_slides" inherit_id="survey.survey_closed_finished">
|
||||
<xpath expr="//div[hasclass('jumbotron')]/div[last()]" position="after">
|
||||
<template id="survey_fill_form_done_inherit_website_slides" inherit_id="survey.survey_fill_form_done">
|
||||
<xpath expr="//div[hasclass('o_survey_finished')]/div[last()]" position="after">
|
||||
<div t-if="channel_url" class="mt16">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-lg"
|
||||
|
||||
Reference in New Issue
Block a user