[REF] survey : make survey form a widget

Purpose of this commit is
- to make the survey js controller a widget, to be more 'odoo-standard'.
- to lessen number of RPCs and clean old code for prefilling and validation of
surveys.

Clean Prefill :
Prefill can be done directly in the template as the template has already
all the needed values (in answer object).

Clean Validation :
Validation is done at server side and is independant from prefilling values.

Dates:
Dates are now formatted directly in the template, at rendering, using a
format_date fonction pointer called in the template.

Don't use widget in review mode:
Remove o_survey_form class from review template as only dates were processed in
the widget for review template.

Breadcrumb :
Remove button previous, prev=prev and go_back mechanism :
The previous page is handled by the breadcrump
Remove redirect url mechanism.
Breadcrumb now saves the answers when going to a previous page.
Move o_survey_form class to a higher div to englobe breadcrumb in the widget
and ease his handling.

Remove locale load as already done in session.js#load_modules

This commit modifies the route type of survey submit to work in json.
The js survey form controller calls now manually the route via rpc.

Task ID : 1930132
PR #32419
This commit is contained in:
David Beguin
2019-11-19 12:51:55 +00:00
parent 52a6b00fa7
commit b1e32976a6
8 changed files with 568 additions and 515 deletions
+73 -166
View File
@@ -13,7 +13,7 @@ from odoo import fields, http, _
from odoo.addons.base.models.ir_ui_view import keep_query
from odoo.exceptions import UserError
from odoo.http import request, content_disposition
from odoo.tools import ustr
from odoo.tools import ustr, format_datetime, format_date
_logger = logging.getLogger(__name__)
@@ -223,219 +223,125 @@ class Survey(http.Controller):
else:
return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.token))
# Survey direct link to a specific page
@http.route('/survey/page/<string:survey_token>/<string:answer_token>/<int:page_id>',
type='http', auth='public', website=True)
def survey_change_page(self, survey_token, answer_token, page_id, **post):
""" Method called when the user switches from one page to another using the breadcrumbs links
in the survey layout.
TODO: Right now, the answers that are not submitted are LOST when changing from one page to another
using this method.
The survey "submit" mechanism needs to be refactored entirely to make this more user-friendly."""
# Controls if the survey can be displayed
access_data = self._get_access_data(survey_token, answer_token, ensure_token=False)
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.render('survey.survey', {
'survey': survey_sudo,
'page': request.env['survey.question'].sudo().browse(page_id),
'answer': answer_sudo
})
@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, prev=None, **post):
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']
data = {
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
'format_date': lambda date: format_date(request.env, date)
}
page_or_question_key = 'question' if survey_sudo.questions_layout == 'page_per_question' else 'page'
# Bypass all if page_id is specified (comes from breadcrumb or previous button)
if 'previous_page_id' in post:
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', 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()
})
page_or_question_key = 'question' if survey_sudo.questions_layout == 'page_per_question' else 'page'
# Select the right page
if answer_sudo.state == 'new': # First page
page_or_question_id, last = survey_sudo.next_page_or_question(answer_sudo, 0, go_back=False)
data = {
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 last:
'answer': answer_sudo,
})
if is_last:
data.update({'last': True})
return request.render('survey.survey', data)
elif answer_sudo.state == 'done': # Display success message
return request.render('survey.sfinished', self._prepare_survey_finished_values(survey_sudo, answer_sudo))
elif answer_sudo.state == 'skip':
flag = (True if prev and prev == 'prev' else False)
page_or_question_id, last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag)
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)
#special case if you click "previous" from the last page, then leave the survey, then reopen it from the URL, avoid crash
if not page_or_question_id:
page_or_question_id, last = survey_sudo.next_page_or_question(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True)
data = {
data.update({
'survey': survey_sudo,
page_or_question_key: page_or_question_id,
'answer': answer_sudo
}
if last:
'answer': answer_sudo,
'previous_page_id': previous_id
})
if is_last:
data.update({'last': True})
return request.render('survey.survey', data)
else:
return request.render("survey.403", {'survey': survey_sudo})
@http.route('/survey/prefill/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
def survey_get_answers(self, survey_token, answer_token, page_or_question_id=None, **post):
""" TDE NOTE: original comment: # AJAX prefilling of a survey -> AJAX / http ?? """
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':
return {}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
try:
page_or_question_id = int(page_or_question_id)
except:
page_or_question_id = None
# Fetch previous answers
if survey_sudo.questions_layout == 'one_page' or not page_or_question_id:
previous_answers = answer_sudo.user_input_line_ids
elif survey_sudo.questions_layout == 'page_per_section':
previous_answers = answer_sudo.user_input_line_ids.filtered(lambda line: line.page_id.id == page_or_question_id)
else:
previous_answers = answer_sudo.user_input_line_ids.filtered(lambda line: line.question_id.id == page_or_question_id)
# Return non empty answers in a JSON compatible format
ret = {}
for answer in previous_answers:
if not answer.skipped:
answer_tag = '%s_%s' % (answer.survey_id.id, answer.question_id.id)
answer_value = None
if answer.answer_type == 'free_text':
answer_value = answer.value_free_text
elif answer.answer_type == 'text' and answer.question_id.question_type == 'textbox':
answer_value = answer.value_text
elif answer.answer_type == 'text' and answer.question_id.question_type != 'textbox':
# here come comment answers for matrices, simple choice and multiple choice
answer_tag = "%s_%s" % (answer_tag, 'comment')
answer_value = answer.value_text
elif answer.answer_type == 'number':
answer_value = str(answer.value_number)
elif answer.answer_type == 'date':
answer_value = fields.Datetime.to_string(answer.value_date)
elif answer.answer_type == 'datetime':
answer_value = fields.Datetime.to_string(answer.value_datetime)
elif answer.answer_type == 'suggestion' and not answer.value_suggested_row:
answer_value = answer.value_suggested.id
elif answer.answer_type == 'suggestion' and answer.value_suggested_row:
answer_tag = "%s_%s" % (answer_tag, answer.value_suggested_row.id)
answer_value = answer.value_suggested.id
if answer_value:
ret.setdefault(answer_tag, []).append(answer_value)
else:
_logger.warning("[survey] No answer has been found for question %s marked as non skipped" % answer_tag)
return json.dumps(ret, default=str)
@http.route('/survey/scores/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
def survey_get_scores(self, survey_id, answer_token, page_id=None, **post):
""" TDE NOTE: original comment: # AJAX scores loading for quiz correction mode -> AJAX / http ?? """
access_data = self._get_access_data(survey_id, answer_token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
# Compute score for each question
ret = {}
for answer in answer_sudo.user_input_line_ids:
tmp_score = ret.get(answer.question_id.id, 0.0)
ret.update({answer.question_id.id: tmp_score + answer.answer_score})
return json.dumps(ret)
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='http', methods=['POST'], auth='public', website=True)
@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):
""" Submit a page from the survey.
This will take into account the validation errors and store the answers to the questions.
If the time limit is reached, errors will be skipped, answers wil be ignored and
survey state will be forced to 'done'
TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """
If the time limit is reached, errors will be skipped, answers will be ignored and
survey state will be forced to 'done'"""
# Validation
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
return {
'error': access_data['validity_code'],
}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
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'))
errors = {}
for question in questions:
answer_tag = "%s_%s" % (survey_sudo.id, question.id)
errors.update(question.validate_question(post, answer_tag))
if errors:
return {
'error': 'validation',
'fields': errors,
}
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 {}
if survey_sudo.questions_layout == 'page_per_section':
page_id = int(post['page_id'])
questions = request.env['survey.question'].sudo().search([('survey_id', '=', survey_sudo.id), ('page_id', '=', page_id)])
# we need the intersection of the questions of this page AND the questions prepared for that user_input
# (because randomized surveys do not use all the questions of every page)
questions = questions & answer_sudo.question_ids
page_or_question_id = page_id
elif survey_sudo.questions_layout == 'page_per_question':
question_id = int(post['question_id'])
questions = request.env['survey.question'].sudo().browse(question_id)
page_or_question_id = question_id
else:
questions = survey_sudo.question_ids
questions = questions & answer_sudo.question_ids
errors = {}
# Answer validation
# Submitting survey
if not answer_sudo.is_time_limit_reached:
for question in questions:
answer_tag = "%s_%s" % (survey_sudo.id, question.id)
errors.update(question.validate_question(post, answer_tag))
request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
ret = {}
if len(errors):
# Return errors messages to webpage
ret['errors'] = errors
else:
if not answer_sudo.is_time_limit_reached:
for question in questions:
answer_tag = "%s_%s" % (survey_sudo.id, question.id)
request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
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'])
elif 'button_submit' in post:
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}
vals = {}
if answer_sudo.is_time_limit_reached or survey_sudo.questions_layout == 'one_page':
go_back = False
if next_page is None:
answer_sudo._mark_done()
elif 'button_submit' in post:
go_back = post['button_submit'] == 'previous'
next_page, last = request.env['survey.survey'].next_page_or_question(answer_sudo, page_or_question_id, go_back=go_back)
vals = {'last_displayed_page_id': page_or_question_id}
if next_page is None and not go_back:
answer_sudo._mark_done()
else:
vals.update({'state': 'skip'})
if 'breadcrumb_redirect' in post:
ret['redirect'] = post['breadcrumb_redirect']
else:
if vals:
answer_sudo.write(vals)
vals.update({'state': 'skip'})
ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
if go_back:
ret['redirect'] += '?prev=prev'
answer_sudo.write(vals)
return json.dumps(ret)
return '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
# ------------------------------------------------------------
# COMPLETED SURVEY ROUTES
@@ -460,8 +366,9 @@ class Survey(http.Controller):
'review': review,
'survey': survey_sudo,
'answer': answer_sudo,
'page_nr': 0,
'quizz_correction': survey_sudo.scoring_type != 'scoring_without_answers' and answer_sudo})
'format_datetime': lambda dt: format_datetime(request.env, dt, dt_format=False),
'format_date': lambda date: format_date(request.env, date)
})
@http.route('/survey/results/<model("survey.survey"):survey>', type='http', auth='user', website=True)
def survey_report(self, survey, answer_token=None, **post):
+58 -25
View File
@@ -348,19 +348,42 @@ class Survey(models.Model):
# ------------------------------------------------------------
# ACTIONS
# ------------------------------------------------------------
@api.model
def _get_pages_or_questions(self, user_input):
if self.questions_layout == 'one_page':
return None
elif self.questions_layout == 'page_per_question' and self.questions_selection == 'random':
return list(enumerate(
user_input.question_ids
))
else:
return list(enumerate(
self.question_ids if self.questions_layout == 'page_per_question' else self.page_ids
))
@api.model
def next_page_or_question(self, user_input, page_or_question_id, go_back=False):
def _previous_page_or_question_id(self, user_input, page_or_question_id):
survey = user_input.survey_id
pages_or_questions = survey._get_pages_or_questions(user_input)
current_page_index = pages_or_questions.index(next(p for p in pages_or_questions if p[1].id == page_or_question_id))
# is first page
if current_page_index == 0:
return None
previous_page_id = pages_or_questions[current_page_index - 1][1].id
return previous_page_id
@api.model
def next_page_or_question(self, user_input, page_or_question_id):
""" The next page to display to the user, knowing that page_id is the id
of the last displayed page.
If page_id == 0, it will always return the first page of the survey.
If all the pages have been displayed and go_back == False, it will
return None
If go_back == True, it will return the *previous* page instead of the
next page.
If all the pages have been displayed, it will return None
.. note::
It is assumed here that a careful user will not try to set go_back
@@ -369,16 +392,9 @@ class Survey(models.Model):
"""
survey = user_input.survey_id
if survey.questions_layout == 'one_page':
pages_or_questions = survey._get_pages_or_questions(user_input)
if not pages_or_questions:
return (None, False)
elif survey.questions_layout == 'page_per_question' and survey.questions_selection == 'random':
pages_or_questions = list(enumerate(
user_input.question_ids
))
else:
pages_or_questions = list(enumerate(
survey.question_ids if survey.questions_layout == 'page_per_question' else survey.page_ids
))
# First page
if page_or_question_id == 0:
@@ -387,18 +403,35 @@ class Survey(models.Model):
current_page_index = pages_or_questions.index(next(p for p in pages_or_questions if p[1].id == page_or_question_id))
# All the pages have been displayed
if current_page_index == len(pages_or_questions) - 1 and not go_back:
if current_page_index == len(pages_or_questions) - 1:
return (None, False)
# Let's get back, baby!
elif go_back and survey.users_can_go_back:
return (pages_or_questions[current_page_index - 1][1], False)
else:
# This will show the last page
if current_page_index == len(pages_or_questions) - 2:
return (pages_or_questions[current_page_index + 1][1], True)
# This will show a regular page
else:
return (pages_or_questions[current_page_index + 1][1], False)
show_last = current_page_index == len(pages_or_questions) - 2
return (pages_or_questions[current_page_index + 1][1], show_last)
def _get_survey_questions(self, answer=None, page_id=None, question_id=None):
questions, page_or_question_id = None, None
if self.questions_layout == 'page_per_section':
if not page_id:
raise ValueError("Page id is needed for question layout 'page_per_section'")
page_id = int(page_id)
questions = self.env['survey.question'].sudo().search([('survey_id', '=', self.id), ('page_id', '=', page_id)])
page_or_question_id = page_id
elif self.questions_layout == 'page_per_question':
if not question_id:
raise ValueError("Question id is needed for question layout 'page_per_question'")
question_id = int(question_id)
questions = self.env['survey.question'].sudo().browse(question_id)
page_or_question_id = question_id
else:
questions = self.question_ids
# we need the intersection of the questions of this page AND the questions prepared for that user_input
# (because randomized surveys do not use all the questions of every page)
if answer:
questions = questions & answer.question_ids
return questions, page_or_question_id
def action_draft(self):
self.write({'state': 'draft'})
-244
View File
@@ -1,244 +0,0 @@
odoo.define('survey.survey', function (require) {
'use strict';
require('web.dom_ready');
var core = require('web.core');
var time = require('web.time');
var ajax = require('web.ajax');
var field_utils = require('web.field_utils');
var _t = core._t;
/*
* This file is intended to add interactivity to survey forms
*/
var the_form = $('.js_surveyform');
if(!the_form.length) {
return Promise.reject("DOM doesn't contain '.js_surveyform'");
}
var prefill_controller = the_form.attr("data-prefill");
var submit_controller = the_form.attr("data-submit");
var scores_controller = the_form.attr("data-scores");
var print_mode = false;
var quiz_correction_mode = false;
// Printing mode: will disable all the controls in the form
if (_.isUndefined(submit_controller)) {
$(".js_surveyform .input-group-text span.fa-calendar").css("pointer-events", "none");
$('.js_surveyform :input').prop('disabled', true);
print_mode = true;
}
// Quiz correction mode
if (! _.isUndefined(scores_controller)) {
quiz_correction_mode = true;
}
// Custom code for right behavior of radio buttons with comments box
$('.js_comments>input[type="text"]').focusin(function(){
$(this).prev().find('>input').attr("checked","checked");
});
$('.js_radio input[type="radio"][data-oe-survey-otherr!="1"]').click(function(){
$(this).closest('.js_radio').find('.js_comments>input[type="text"]').val("");
});
$('.js_comments input[type="radio"]').click(function(){
$(this).closest('.js_comments').find('>input[data-oe-survey-othert="1"]').focus();
});
// Custom code for right behavior of dropdown menu with comments
$('.js_drop input[data-oe-survey-othert="1"]').hide();
$('.js_drop select').change(function(){
var other_val = $(this).find('.js_other_option').val();
if($(this).val() === other_val){
$(this).parent().removeClass('col-lg-12').addClass('col-lg-6');
$(this).closest('.js_drop').find('input[data-oe-survey-othert="1"]').show().focus();
}
else{
$(this).parent().removeClass('col-lg-6').addClass('col-lg-12');
$(this).closest('.js_drop').find('input[data-oe-survey-othert="1"]').val("").hide();
}
});
// Custom code for right behavior of checkboxes with comments box
$('.js_ck_comments>input[type="text"]').focusin(function(){
$(this).prev().find('>input').attr("checked","checked");
});
$('.js_ck_comments input[type="checkbox"]').change(function(){
if (! $(this).prop("checked")){
$(this).closest('.js_ck_comments').find('input[type="text"]').val("");
}
});
// Pre-filling of the form with previous answers
function prefill(){
if (! _.isUndefined(prefill_controller)) {
var prefill_def = $.ajax(prefill_controller, {dataType: "json"})
.done(function(json_data){
_.each(json_data, function(value, key){
// prefill of text/number/date boxes
var input = the_form.find(".form-control[name=" + key + "]");
if (input.hasClass('datetimepicker-input')) {
// display dates in user timezone
var moment_date = field_utils.parse.date(value[0]);
if ($(input).closest('.input-group.date').data('questiontype') === 'datetime') {
value = field_utils.format.datetime(moment_date, null, {timezone: true});
}
else {
value = field_utils.format.date(moment_date, null, {timezone: true});
}
}
input.val(value);
// special case for comments under multiple suggestions questions
if (_.string.endsWith(key, "_comment") &&
(input.parent().hasClass("js_comments") || input.parent().hasClass("js_ck_comments"))) {
input.siblings().find('>input').attr("checked","checked");
}
// checkboxes and radios
the_form.find("input[name^='" + key + "_'][type='checkbox']").each(function(){
$(this).val(value);
});
the_form.find("input[name=" + key + "][type!='text']").each(function(){
$(this).val(value);
});
});
})
.fail(function(){
console.warn("[survey] Unable to load prefill data");
});
return prefill_def;
}
}
// Display score if quiz correction mode
function display_scores(){
if (! _.isUndefined(scores_controller)) {
var score_def = $.ajax(scores_controller, {dataType: "json"})
.done(function(json_data){
_.each(json_data, function(value, key){
the_form.find("span[data-score-question=" + key + "]").text("Your score: " + value);
});
})
.fail(function(){
console.warn("[survey] Unable to load score data");
});
return score_def;
}
}
$('.o_survey_header .breadcrumb-item a').each(function() {
var $valObj = $(this);
$valObj.click(function(event) {
event.preventDefault();
$('<input>').attr({type: 'hidden', name: 'breadcrumb_redirect', value: $valObj.attr('href')}).appendTo('.js_surveyform');
$('.js_surveyform').submit();
});
});
// Parameters for form submission
$('.js_surveyform').ajaxForm({
url: submit_controller,
type: 'POST', // submission type
dataType: 'json', // answer expected type
beforeSubmit: function(formData, $form, options){ // hide previous errmsg before resubmitting
var date_fields = $form.find('div.date > input.form-control');
for (var i=0; i < date_fields.length; i++) {
var el = date_fields[i];
var questiontype = $(el).closest('.input-group.date').data('questiontype');
var moment_date = questiontype === 'datetime' ? field_utils.parse.datetime(el.value, null, {timezone: true}) : field_utils.parse.date(el.value);
var field_obj = _.findWhere(formData, {'name': el.name});
field_obj.value = moment_date ? moment_date.toJSON() : '';
}
$('.js_errzone').html("").hide();
},
success: function(response, status, xhr, wfe){ // submission attempt
if(_.has(response, 'errors')){ // some questions have errors
_.each(_.keys(response.errors), function(key){
$("#" + key + '>.js_errzone').append('<p>' + response.errors[key] + '</p>').show();
if (_.keys(response.errors)[_.keys(response.errors).length - 1] === key) {
$('html, body').animate({
scrollTop: $('.js_errzone:visible:first').closest('.js_question-wrapper').offset().top - $('.o_main_navbar').height()
}, 500);
}
});
return false;
}
else if (_.has(response, 'redirect')){ // form is ok
window.location.replace(response.redirect);
return true;
}
else { // server sends bad data
console.error("Incorrect answer sent by server");
return false;
}
},
timeout: 5000,
error: function(jqXHR, textStatus, errorThrown){ // failure of AJAX request
$('#AJAXErrorModal').modal('show');
}
});
function load_locale(){
var url = "/web/webclient/locale/" + (document.documentElement.getAttribute('lang') || 'en_US').replace('-', '_');
return ajax.loadJS(url);
}
// datetimepicker use moment locale to display date format according to language
// frontend does not load moment locale at all.
// so wait until DOM ready with locale then init datetimepicker
load_locale().then(function(){
_.each($('.input-group.date'), function(date_field){
var disabledDates = []
if ($(date_field).data('mindate')) {
var minDate = field_utils.format.datetime(moment($(date_field).data('mindate')), null, {timezone: true});
}
if ($(date_field).data('maxdate')) {
var maxDate = field_utils.format.datetime(moment($(date_field).data('maxdate')), null, {timezone: true});
}
else {
var minDate = $(date_field).data('mindate') || moment({ y: 1900 });
var maxDate = $(date_field).data('maxdate') || moment().add(200, "y");
}
var datetimepickerFormat = time.getLangDateFormat()
if ($(date_field).data('questiontype') === 'datetime') {
datetimepickerFormat = time.getLangDatetimeFormat()
}
else{
maxDate = moment(maxDate).add(1, "d");
disabledDates = [maxDate]
}
$('#' + date_field.id).datetimepicker({
format : datetimepickerFormat,
minDate: minDate,
maxDate: maxDate,
disabledDates: disabledDates,
useCurrent: false,
viewDate: moment(new Date()).hours(0).minutes(0).seconds(0).milliseconds(0),
calendarWeeks: true,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'fa fa-chevron-right',
previous: 'fa fa-chevron-left',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
},
locale : moment.locale(),
allowInputToggle: true,
keyBinds: null,
});
$('#' + date_field.id).on('error.datetimepicker', function () {
return false;
});
});
// Launch prefilling
prefill();
if(quiz_correction_mode === true){
display_scores();
}
});
});
+322
View File
@@ -0,0 +1,322 @@
odoo.define('survey.form', function(require) {
'use strict';
var ajax = require('web.ajax');
var field_utils = require('web.field_utils');
var publicWidget = require('web.public.widget');
var time = require('web.time');
publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
selector: '.o_survey_form',
events: {
'change .o_survey_form_choice_item': '_onChangeChoiceItem',
'click button[name="button_submit"]': '_onSubmit',
'click .o_survey_header .breadcrumb-item a': '_onBreadcrumbClick',
},
//--------------------------------------------------------------------------
// Widget
//--------------------------------------------------------------------------
/**
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function() {
self.options = self.$target.find('form').data()
var $timer = self.$('.o_survey_timer');
if ($timer.length) {
this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this);
this.surveyTimerWidget.attachTo($timer);
this.surveyTimerWidget.on('time_up', this, function (ev) {
self.$el.find('button[name="button_submit"]').click();
});
}
self.$('div.o_survey_form_date').each(function () {
self._initDateTimePicker($(this));
});
});
},
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
// 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
*
* @private
* @param {Event} event
*/
_onChangeChoiceItem: function (event) {
var $choiceItemGroup = $(event.currentTarget).parents('.o_survey_form_choice');
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();
$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();
var $target = $(event.currentTarget);
var submitValue = $target.val();
this._submitForm({'button_submit': submitValue})
},
_submitForm: function (params) {
var self = this;
var $form = this.$('form');
var formData = new FormData($form[0]);
this.$('div.o_survey_form_date').each(function () {
self._updateDateForSubmit($(this), formData);
});
this._prepareSubmitValues(formData, params);
this._resetErrors();
return self._rpc({
route: '/survey/submit/' + self.options['surveyToken'] + '/' + self.options['answerToken'],
params: params,
}).then(function (result) {
return self._onSubmitDone(result, params);
});
},
_onSubmitDone: function (result, params) {
var self = this;
if (result && !result.error) {
window.location = result;
}
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);
return false;
}
},
// INIT FIELDS
// -------------------------------------------------------------------------
/*
* Convert the client side date format into server side date format before submit
*/
_updateDateForSubmit: function (dateGroup, formData) {
var input = $(dateGroup).find('input');
var dateValue = input.val();
var questionType = $(input).closest('.o_survey_form_date').data('questiontype');
if (dateValue) {
var momentDate = questionType === 'datetime' ? field_utils.parse.datetime(dateValue, null, {timezone: true}) : field_utils.parse.date(dateValue);
var newDate = momentDate ? momentDate.toJSON() : '';
formData.set(input.attr('name'), newDate);
input.val(newDate);
}
},
// TOOLS
// -------------------------------------------------------------------------
_prepareSubmitValues: function (formData, params) {
var self = this;
formData.forEach(function(value, key){
if (value !== -1) {
// Handles Comment
if (key.indexOf('_comment') !== -1){
key = key.split('_comment')[0];
value = {'comment': value};
}
// Handles Matrix - Matrix answer_tag are composed like : 'questionId_rowId_colId'
// and are the only ones with this structure.
var splitKey = key.split('_');
if (splitKey.length === 3 && splitKey[2] === value) {
params = self._prepareSubmitMatrix(params, splitKey, value);
}
// Handles the rest
else {
params = self._prepareSubmitOther(params, key, value);
}
}
});
},
/**
* Prepare answer before submitting form if question type is matrix.
* This method regroups answers by question and by row to make a object like :
* params = { 'matrixQuestionId' : { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] } }
*/
_prepareSubmitMatrix: function(params, splitKey, value) {
var key = splitKey[0];
var rowId = splitKey[1];
var colId = splitKey[2];
value = key in params ? params[key] : {};
if (rowId in value) {
value[rowId].push(colId);
} else {
value[rowId] = [colId];
}
params[key] = value;
return params;
},
/**
* Prepare answer before submitting form (any kind of answer - except Matrix -).
* This method regroups answers by question.
* Lonely answer are directly assigned to questionId. Multiple answers are regrouped in an array:
* params = { 'questionId1' : lonelyAnswer, 'questionId2' : [multipleAnswer1, multipleAnswer2, ...] }
*/
_prepareSubmitOther: function(params, key, value) {
if (key in params) {
if (params[key].constructor === Array) {
params[key].push(value);
} else {
params[key] = [params[key], value];
}
} else {
params[key] = value;
}
return params;
},
/**
* Convert date value in client current timezone (if already answered)
*/
_formatDateValue: function ($dateGroup) {
var input = $dateGroup.find('input');
var dateValue = input.val();
if (dateValue !== '') {
var momentDate = field_utils.parse.date(dateValue);
if ($dateGroup.data('questiontype') === 'datetime') {
dateValue = field_utils.format.datetime(momentDate, null, {timezone: true});
} else {
dateValue = field_utils.format.date(momentDate, null, {timezone: true});
}
}
input.val(dateValue);
return dateValue
},
/**
* Initialize datetimepicker in correct format and with constraints
*/
_initDateTimePicker: function ($dateGroup) {
var disabledDates = []
var minDateData = $dateGroup.data('mindate')
var minDate = minDateData ? this._formatDateTime(minDateData) : moment({ y: 1900 });
var maxDateData = $dateGroup.data('maxdate')
var maxDate = maxDateData ? this._formatDateTime(maxDateData) : moment().add(200, "y");
var datetimepickerFormat = time.getLangDateFormat()
if ($dateGroup.data('questiontype') === 'datetime') {
datetimepickerFormat = time.getLangDatetimeFormat()
} else {
// Include min and max date in selectable values
maxDate = moment(maxDate).add(1, "d");
minDate = moment(minDate).subtract(1, "d");
disabledDates = [minDate, maxDate]
}
$dateGroup.datetimepicker({
format : datetimepickerFormat,
minDate: minDate,
maxDate: maxDate,
disabledDates: disabledDates,
useCurrent: false,
viewDate: moment(new Date()).hours(0).minutes(0).seconds(0).milliseconds(0),
calendarWeeks: true,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'fa fa-chevron-right',
previous: 'fa fa-chevron-left',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
},
locale : moment.locale(),
allowInputToggle: true,
});
$dateGroup.on('error.datetimepicker', function () {
return false;
});
},
_formatDateTime: function (datetimeValue){
return field_utils.format.datetime(moment(datetimeValue), null, {timezone: true});
},
_scrollToError: function ($target) {
var scrollLocation = $target.offset().top;
var navbarHeight = $('.o_main_navbar').height();
if (navbarHeight != undefined) {
scrollLocation -= navbarHeight
}
$('html, body').animate({
scrollTop: scrollLocation
}, 500);
},
/**
* 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);
},
});
return publicWidget.registry.SurveyFormWidget;
});
+1 -1
View File
@@ -35,7 +35,7 @@ var updateTimer = function () {
$timer.html(formatTime(timeLeftMinutes) + ':' + formatTime(timeLeftSeconds));
} else {
clearInterval(interval);
$('.js_surveyform').submit();
$('.o_survey_form').submit();
}
};
+3 -1
View File
@@ -215,7 +215,9 @@ class TestSurveyCommon(common.SavepointCase):
return self.url_open('/survey/fill/%s/%s' % (survey.access_token, token))
def _access_submit(self, survey, token, post_data):
return self.url_open('/survey/submit/%s/%s' % (survey.access_token, token), data=post_data)
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
url = base_url + '/survey/submit/%s/%s' % (survey.access_token, token)
return self.opener.post(url=url, json={'params': post_data})
def _find_csrf_token(self, text):
csrf_token_re = re.compile("(input.+csrf_token.+value=\")([_a-zA-Z0-9]{51})", re.MULTILINE)
+2 -2
View File
@@ -7,9 +7,9 @@
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
<script type="text/javascript" src="/survey/static/src/js/survey.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_timer.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" />
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
+109 -76
View File
@@ -198,7 +198,7 @@
<t t-call="survey.layout">
<div class="wrap">
<div class="oe_structure" id="oe_structure_survey_survey_1"/>
<div class="container">
<div class="container o_survey_form">
<t t-if="answer.test_entry" t-call="survey.back" />
<t t-call="survey.survey_header" />
<t t-call="survey.page" />
@@ -233,7 +233,7 @@
<t t-set="can_go_back" t-value="False" />
</t>
<t t-if="can_go_back">
<a t-att-href="'/survey/page/%s/%s/%s' % (survey.access_token, answer.token, breadcrumb_page.id)">
<a href="#" t-att-data-previous-page-id="breadcrumb_page.id">
<span t-field='breadcrumb_page.title' />
</a>
</t>
@@ -248,18 +248,20 @@
<!-- A page -->
<template id="page" name="Page">
<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" />
<form role="form" method="post" class="js_surveyform" t-att-name="survey.id"
t-att-action="'/survey/fill/%s/%s' % (survey.access_token, answer.token)"
t-att-data-prefill="'/survey/prefill/%s/%s?page_or_question_id=%s' % (survey.access_token, answer.token, page_or_question_id)"
t-att-data-validate="'/survey/validate/%s/%s' % (survey.access_token, answer.token)"
t-att-data-submit="'/survey/submit/%s/%s' % (survey.access_token, answer.token)">
<form role="form" method="post" t-att-name="survey.id"
t-att-data-answer-token="answer.token"
t-att-data-survey-token="survey.access_token">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="token" t-att-value="answer.token" />
<div class="o_survey_error alert alert-danger d-none" role="alert">
<p>Their 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" />
@@ -281,7 +283,8 @@
</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">Previous page</button>
<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 t-if="not last" type="submit" class="btn btn-primary" name="button_submit" value="next">Next page</button>
<button t-if="last" type="submit" class="btn btn-primary" name="button_submit" value="finish">Submit Survey</button>
</div>
@@ -292,7 +295,8 @@
<t t-call="survey.question" />
<div class="text-center mt16 mb256">
<button t-if="survey.users_can_go_back and question != answer.question_ids[0]" type="submit" class="btn btn-secondary" name="button_submit" value="previous">Previous question</button>
<button t-if="survey.users_can_go_back and question != answer.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 t-if="not last" type="submit" class="btn btn-primary" name="button_submit" value="next">Next question</button>
<button t-if="last" type="submit" class="btn btn-primary" name="button_submit" value="finish">Submit Survey</button>
</div>
@@ -316,6 +320,7 @@
<template id="question" name="Question">
<t t-set="prefix" t-value="'%s_%s' % (survey.id, question.id)" />
<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="prefix">
<h4>
<span t-field='question.question' />
@@ -330,110 +335,131 @@
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.simple_choice"/></t>
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.multiple_choice"/></t>
<t t-if="question.question_type == 'matrix'"><t t-call="survey.matrix"/></t>
<div class="js_errzone alert alert-danger" style="display:none;" role="alert"></div>
<div class="o_survey_question_error alert alert-danger d-none" role="alert"></div>
</div>
</template>
<!-- Question widgets -->
<template id="free_text" name="Free text box">
<textarea class="form-control" rows="3" t-att-name="prefix"></textarea>
<textarea class="form-control" rows="3" t-att-name="prefix"><t t-if="answer_lines" t-esc="answer_lines[0].value_free_text or None"/></textarea>
</template>
<template id="textbox" name="Text box">
<input type="text" class="form-control" t-att-name="prefix"/>
<input type="text" class="form-control" t-att-name="prefix" t-att-value="answer_lines[0].value_text if answer_lines else None"/>
</template>
<template id="numerical_box" name="Numerical box">
<input type="number" step="any" class="form-control" t-att-name="prefix"/>
<input type="number" step="any" class="form-control" t-att-name="prefix" t-att-value="answer_lines[0].value_number if answer_lines else None"/>
</template>
<template id="date" name="Date box">
<div class="form-group">
<div class="input-group date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_date"
t-att-data-maxdate="question.validation_max_date">
<input type="text" class="form-control datetimepicker-input" t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="prefix"/>
<div class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_date"
t-att-data-maxdate="question.validation_max_date">
<input type="text" class="form-control datetimepicker-input" t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="prefix"
t-att-value="format_date(answer_lines[0].value_date) if answer_lines else None"/>
<div t-if="not survey_form_readonly" class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
<template id="datetime" name="DateTime box">
<div class="form-group">
<div class="input-group date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_datetime"
t-att-data-maxdate="question.validation_max_datetime"
t-att-data-questiontype="question.question_type">
<input type="text" class="form-control datetimepicker-input" t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="prefix"/>
<div class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_datetime"
t-att-data-maxdate="question.validation_max_datetime"
t-att-data-questiontype="question.question_type">
<input type="text" class="form-control datetimepicker-input" t-attf-data-target="#datetimepicker_#{question.id}" t-att-name="prefix"
t-att-value="format_datetime(answer_lines[0].value_datetime) if answer_lines else None"/>
<div t-if="not survey_form_readonly" class="input-group-append" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fa fa-calendar"></i></div>
</div>
</div>
</template>
<template id="simple_choice" name="Simple choice">
<div t-if="question.display_mode == 'dropdown' and answer.token" class="js_drop row">
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.value_suggested)"/>
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_text)"/>
<div t-if="question.display_mode == 'dropdown'" class="o_survey_form_choice row">
<div class="col-lg-12">
<select class="form-control" t-att-name="prefix">
<option disabled="1" selected="1" value="">Choose...</option>
<select class="form-control o_survey_form_choice_item" t-att-name="prefix">
<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.labels_ids' t-as='label'>
<option t-att-value='label.id'><t t-esc='label.value'/></option>
<option t-att-value='label.id'
t-att-selected="answer_line and answer_line.value_suggested == 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="js_other_option" value="-1"><span t-esc="question.comments_message" /></option>
<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" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"/>
<textarea type="text" class="form-control"
t-attf-class="form-control #{'' if comment_line else 'd-none'}"
t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"
t-att-value="comment_line.value_text if comment_line else ''"/>
</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" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')"/>
<textarea type="text" class="form-control o_survey_comment" t-att-name="'%s_%s' % (prefix, 'comment')"
t-att-value="comment_line.value_text if comment_line else ''"/>
</div>
</div>
<div t-if="question.display_mode == 'columns' or not answer.token" class="row js_radio">
<div t-if="question.display_mode == 'columns'" class="row o_survey_form_choice">
<div t-foreach='question.labels_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-name="prefix" t-att-value='label.id' />
<input type="radio" t-att-name="prefix" t-att-value='label.id' class="o_survey_form_choice_item"
t-att-checked="answer_line and answer_line.value_suggested.id == label.id and 'checked' or None"/>
<span t-field='label.value'/>
</label>
</div>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_comments col-lg-12" >
<label>
<input type="radio" t-att-name="prefix" value="-1"/>
<input type="radio" class="o_survey_form_choice_item o_survey_js_form_other_comment" t-att-name="prefix" value="-1"
t-att-checked="comment_line and 'checked' or None"/>
<span t-field="question.comments_message" />
</label>
<textarea type="text" class="form-control" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')"/>
<textarea type="text" class="form-control" rows="1"
t-attf-disabled="#{'' if comment_line else 'disabled'}"
t-att-name="'%s_%s' % (prefix, 'comment')"/>
</div>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12">
<span t-field="question.comments_message"/>
<textarea type="text" class="form-control" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"/>
<textarea type="text" class="form-control o_survey_comment" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"/>
</div>
</div>
</template>
<template id="multiple_choice" name="Multiple choice">
<div class="row">
<t t-set="comment_line" t-value="answer_lines.filtered(lambda line: line.value_text)"/>
<div class="row o_survey_form_choice">
<div t-foreach='question.labels_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
<t t-set="answer_line" t-value="answer_lines.filtered(lambda line: line.value_suggested == label)"/>
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
<input type="checkbox" t-att-name="'%s_%s' % (prefix, label.id)" t-att-value='label.id' />
<input type="checkbox" t-att-name="'%s_%s' % (prefix, label.id)" t-att-value='label.id' class="o_survey_form_choice_item"
t-att-checked="'checked' if answer_line else None"/>
<span t-field='label.value'/>
</label>
</div>
<div t-if='question.comments_allowed and question.comment_count_as_answer' class="js_ck_comments col-lg-12" >
<label>
<input type="checkbox" t-att-name="'%s_%s' % (prefix, -1)" value="-1" />
<input type="checkbox" t-att-name="'%s_%s' % (prefix, -1)" class="o_survey_form_choice_item o_survey_js_form_other_comment" value="-1"
t-att-checked="'checked' if comment_line else None"/>
<span t-field="question.comments_message" />
</label>
<textarea type="text" class="form-control" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')"/>
<textarea type="text" class="form-control"
t-attf-disabled="#{'' if comment_line else 'disabled'}"
t-att-name="'%s_%s' % (prefix, 'comment')"
t-att-value="comment_line.value_text if comment_line else None"/>
</div>
<div t-if='question.comments_allowed and not question.comment_count_as_answer' class="col-lg-12">
<span t-field="question.comments_message"/>
<textarea type="text" class="form-control" rows="1" t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"/>
<textarea type="text" class="form-control" t-att-name="'%s_%s' % (prefix, 'comment')" data-oe-survey-othert="1"
t-att-value="comment_line.value_text if comment_line else None"/>
</div>
</div>
</template>
@@ -450,8 +476,11 @@
<tr t-foreach="question.labels_ids_2" t-as="row_label">
<th><span t-field="row_label.value" /></th>
<td t-foreach="question.labels_ids" t-as="col_label">
<input t-if="question.matrix_subtype == 'simple'" type="radio" t-att-name="'%s_%s' % (prefix, row_label.id)" t-att-value='col_label.id' />
<input t-if="question.matrix_subtype == 'multiple'" type="checkbox" t-att-name="'%s_%s_%s' % (prefix, row_label.id, col_label.id)" t-att-value='col_label.id' />
<t t-set="answer" t-value="answer_lines.filtered(lambda line: line.value_suggested == col_label and line.value_suggested_row == row_label)"/>
<input t-if="question.matrix_subtype == 'simple'" type="radio" t-att-name="'%s_%s' % (prefix, row_label.id)" t-att-value='col_label.id'
t-att-checked="'checked' if answer else None"/>
<input t-if="question.matrix_subtype == 'multiple'" type="checkbox" t-att-name="'%s_%s_%s' % (prefix, row_label.id, col_label.id)" t-att-value='col_label.id'
t-att-checked="'checked' if answer else None"/>
</td>
</tr>
</tbody>
@@ -465,6 +494,7 @@
<!-- Printable view of a survey (all pages) -->
<template id="survey_print" name="Survey">
<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.back" />
@@ -473,34 +503,37 @@
<t t-if="survey.description"><div t-field='survey.description' class="oe_no_empty"/></t>
<t t-if="review" t-call="survey.retake_survey_button"/>
</div>
<div role="form" class="js_surveyform" t-att-name="'%s' % (survey.id)" t-att-data-prefill="'/survey/prefill/%s/%s' % (survey.access_token, answer.token)">
<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.question_ids)" >
<t t-set="prefix" t-value="'%s_%s' % (survey.id, question.id)" />
<div class="js_question-wrapper" t-att-id="prefix">
<h2>
<span t-field='question.question' />
<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 == 'free_text'"><t t-call="survey.free_text"/></t>
<t t-if="question.question_type == 'textbox'"><t t-call="survey.textbox"/></t>
<t t-if="question.question_type == 'numerical_box'"><t t-call="survey.numerical_box"/></t>
<t t-if="question.question_type == 'date'"><t t-call="survey.date"/></t>
<t t-if="question.question_type == 'datetime'"><t t-call="survey.datetime"/></t>
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.simple_choice"/></t>
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.multiple_choice"/></t>
<t t-if="question.question_type == 'matrix'"><t t-call="survey.matrix"/></t>
<div class="js_errzone alert alert-danger" style="display:none;" role="alert"></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.question_ids)" >
<t t-set="prefix" t-value="'%s_%s' % (survey.id, question.id)" />
<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="prefix">
<h2>
<span t-field='question.question' />
<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 == 'free_text'"><t t-call="survey.free_text"/></t>
<t t-if="question.question_type == 'textbox'"><t t-call="survey.textbox"/></t>
<t t-if="question.question_type == 'numerical_box'"><t t-call="survey.numerical_box"/></t>
<t t-if="question.question_type == 'date'"><t t-call="survey.date"/></t>
<t t-if="question.question_type == 'datetime'"><t t-call="survey.datetime"/></t>
<t t-if="question.question_type == 'simple_choice'"><t t-call="survey.simple_choice"/></t>
<t t-if="question.question_type == 'multiple_choice'"><t t-call="survey.multiple_choice"/></t>
<t t-if="question.question_type == 'matrix'"><t t-call="survey.matrix"/></t>
<div class="o_survey_question_error alert alert-danger d-none" role="alert"></div>
</div>
</t>
</t>
</t>
</fieldset>
</div>
</div>
</div>