[IMP] Server side validation

bzr revid: rim@openerp.com-20131115151435-ht5eg6e6vfiydibe
This commit is contained in:
Richard Mathot (OpenERP)
2013-11-15 16:14:35 +01:00
parent 5a0e33855f
commit c5ff225a16
7 changed files with 325 additions and 209 deletions
+2 -2
View File
@@ -38,8 +38,8 @@ sent mails with user name and password for the invitation of the survey.
'website': 'https://www.openerp.com/',
'depends': ['email_template', 'mail', 'website'],
'data': [
#'security/survey_security.xml',
#'security/ir.model.access.csv',
'security/survey_security.xml',
'security/ir.model.access.csv',
'survey_view.xml',
'views/survey_templates.xml',
#'wizard/survey_answer.xml',
+147 -7
View File
@@ -24,7 +24,9 @@ from openerp.addons.web.http import request
from openerp.addons.website.models import website
import werkzeug
import json
import logging
import re
_logger = logging.getLogger(__name__)
@@ -57,7 +59,7 @@ class WebsiteSurvey(http.Controller):
if survey_obj.exists(cr, uid, survey.id, context=context) == []:
return werkzeug.utils.redirect("/survey/")
_logger.debug('Post request data: %s', post)
_logger.debug('Post data: %s', post)
# Store answer data
# (/!\ assumes JavaScript validation of answers has succeeded!)
@@ -96,14 +98,25 @@ class WebsiteSurvey(http.Controller):
# return {'valid': True, 'errors': None}
@website.route(['/survey/submit/<model("survey.survey"):survey>'],
type='json', auth='public', multilang=True)
def submit(self, survey=None, **post):
_logger.debug("Incoming json data: " + post.__str__())
type='http', auth='public', multilang=True)
def submit(self, survey, **post):
_logger.debug("Incoming data: " + post.__str__())
page_nr = int(post['current'])
questions = survey.page_ids[page_nr].question_ids
# transform post data into dictionary
# for each {K:V} in post, check
errors = {}
for question in questions:
answer_tag = "%s_%s_%s" % (survey.id, page_nr, question.id)
errors.update(self.validate_question(survey.id, page_nr, question, post, answer_tag))
return {'redirect': '/survey/'}
ret = {}
if (len(errors) != 0):
ret['errors'] = errors
else:
cr, uid, context = request.cr, request.uid, request.context
# Store here data if allowed
ret['redirect'] = "nexturl"
return json.dumps(ret)
# Printing routes
@website.route(['/survey/print/<model("survey.survey"):survey>/'],
@@ -114,3 +127,130 @@ class WebsiteSurvey(http.Controller):
return request.website.render('survey.survey_print',
{'survey': survey,
'pagination': pagination})
# Validation methods
def validate_question(self, survey_id, page_nr, question, post, answer_tag):
''' Routing to the right question valider, depending on question type '''
try:
checker = getattr(self, 'validate_' + question.type)
except AttributeError:
_logger.warning(question.type + ": This type of question has no validation method")
return {}
else:
return checker(survey_id, page_nr, question, post, answer_tag)
def validate_free_text(self, survey_id, page_nr, question, post, answer_tag):
errors = {}
answer = post[answer_tag].strip()
# Empty answer to mandatory question
if question.constr_mandatory and not answer:
errors.update({answer_tag: question.constr_error_msg})
return errors
def validate_textbox(self, survey_id, page_nr, question, post, answer_tag):
errors = {}
answer = post[answer_tag].strip()
# Empty answer to mandatory question
if question.constr_mandatory and not answer:
errors.update({answer_tag: question.constr_error_msg})
# Answer validation (if properly defined)
if answer and question.validation_required and question.validation_type:
# Length of the answer must be in a range
if question.validation_type == "has_length":
if not (question.validation_length_min <= len(answer) <= question.validation_length_max):
errors.update({answer_tag: question.validation_error_msg})
# Answer must be an integer in a particular range
elif question.validation_type == "is_integer":
try:
intanswer = int(answer)
# Answer is not an integer
except ValueError:
errors.update({answer_tag: question.validation_error_msg})
else:
# Answer is not in the right range
if not (question.validation_min_int_value <= intanswer <= question.validation_max_int_value):
errors.update({answer_tag: question.validation_error_msg})
# Answer must be a float in a particular range
elif question.validation_type == "is_decimal":
try:
floatanswer = float(answer)
# Answer is not an integer
except ValueError:
errors.update({answer_tag: question.validation_error_msg})
else:
# Answer is not in the right range
if not (question.validation_min_float_value <= floatanswer <= question.validation_max_float_value):
errors.update({answer_tag: question.validation_error_msg})
# Answer must be a date in a particular range
elif question.validation_type == "is_date":
raise Exception("Not implemented")
# Answer must be an email address
# Note: this validation is very basic:
# all the strings of the form
# <something>@<anything>.<extension>
# will be accepted
elif question.validation_type == "is_email":
if not re.match(r"[^@]+@[^@]+\.[^@]+", answer):
errors.update({answer_tag: question.validation_error_msg})
else:
pass
return errors
def validate_numerical_box(self, survey_id, page_nr, question, post, answer_tag):
errors = {}
answer = post[answer_tag].strip()
# Empty answer to mandatory question
if question.constr_mandatory and not answer:
errors.update({answer_tag: question.constr_error_msg})
# Checks if user input is a number
if answer:
try:
float(answer)
except ValueError:
errors.update({answer_tag: question.constr_error_msg})
return errors
def validate_datetime(self, survey_id, page_nr, question, post, answer_tag):
errors = {}
answer = post[answer_tag].strip()
# Empty answer to mandatory question
if question.constr_mandatory and not answer:
errors.update({answer_tag: question.constr_error_msg})
# Checks if user input is a datetime
# TODO when datepicker will be available
return errors
# def validate_simple_choice_scale(self, survey_id, page_nr, question, post, answer_tag):
# answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__()
# problems = []
# if question.constr_mandatory:
# problems = problems + self.__has_empty_input(question, post, answer_tag)
# return problems
# def validate_simple_choice_dropdown(self, survey_id, page_nr, question, post, answer_tag):
# answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__()
# problems = []
# if question.constr_mandatory:
# problems = problems + self.__has_empty_input(question, post, answer_tag)
# return problems
# def validate_multiple_choice(self, survey_id, page_nr, question, post, answer_tag):
# answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__()
# problems = []
# return problems
# def validate_matrix(self, survey_id, page_nr, question, post, answer_tag):
# answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__()
# problems = []
# return problems
def dict_keys_startswith(self, dictionary, string):
'''Returns a dictionary containing the elements of <dict> whose keys start
with <string>.
.. note::
This function uses dictionary comprehensions (Python >= 2.7)'''
return {k: dictionary[k] for k in filter(lambda key: key.startswith(string), dictionary.keys())}
+1 -1
View File
@@ -3,6 +3,6 @@ access_survey_user,survey.user,model_survey_survey,base.group_survey_user,1,0,0,
access_survey_manager,survey.manager,model_survey_survey,base.group_survey_manager,1,1,1,1
access_survey_page_manager,survey.page manager,model_survey_page,base.group_survey_manager,1,1,1,1
access_survey_question_manager,survey.question manager,model_survey_question,base.group_survey_manager,1,1,1,1
access_survey_suggestion_manager,survey.suggestion manager,model_survey_suggestion,base.group_survey_manager,1,1,1,1
access_survey_label_manager,survey.label manager,model_survey_label,base.group_survey_manager,1,1,1,1
access_survey_user_input_manager,survey.user_input manager,model_survey_user_input,base.group_survey_manager,1,1,1,1
access_survey_user_input_line_manager,survey.user_input_line manager,model_survey_user_input_line,base.group_survey_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_survey_manager survey.manager model_survey_survey base.group_survey_manager 1 1 1 1
4 access_survey_page_manager survey.page manager model_survey_page base.group_survey_manager 1 1 1 1
5 access_survey_question_manager survey.question manager model_survey_question base.group_survey_manager 1 1 1 1
6 access_survey_suggestion_manager access_survey_label_manager survey.suggestion manager survey.label manager model_survey_suggestion model_survey_label base.group_survey_manager 1 1 1 1
7 access_survey_user_input_manager survey.user_input manager model_survey_user_input base.group_survey_manager 1 1 1 1
8 access_survey_user_input_line_manager survey.user_input_line manager model_survey_user_input_line base.group_survey_manager 1 1 1 1
+27 -137
View File
@@ -1,15 +1,12 @@
$(document).ready(function () {
console.debug("[survey] Custom JS for survey is loading...");
var the_form = $('.js_surveyform');
var prefill_controller = the_form.attr("data-prefill");
var validate_controller = the_form.attr("data-validate");
var submit_controller = the_form.attr("data-submit");
console.log(prefill_controller);
console.log(validate_controller);
console.log(submit_controller);
// function prefill(form){
// return false;
// }
@@ -22,29 +19,30 @@ $(document).ready(function () {
// return false;
// }
// startsWith compatibility patch with old browsers
// if (typeof String.prototype.startsWith != 'function') {
// String.prototype.startsWith = function (str){
// return this.slice(0, str.length) == str;
// };
// }
// function that display error messages
var display_form_error = function (question_id, err_msg){
$('#survey_' + question_id + '>.js_errzone').append('<p>' + err_msg + '</p>').show();
};
//$('.js_errzone').hide();
console.debug("[survey] Custom JS for survey loaded");
// for each question
// check if answer exists and is not empty and is mandatory
// check if answer type is ok
// check if number of answers is ok
// check if answer has good qualities
// Parameters for form submission
$('.js_surveyform').ajaxForm({
url: submit_controller,
type: 'POST',
dataType: 'json', // answer expected type
beforeSubmit: function(){
$('.js_errzone').html("").hide();
},
success: function(response, status, xhr, wfe){
if(_.has(response, 'errors')){
_.each(_.keys(response.errors), function(key){
$("#" + key + '>.js_errzone').append('<p>' + response.errors[key] + '</p>').show();
});
return false;
}
else if (_.has(response, 'redirect')){
return true;
}
else {
console.error("Something bad happened during AJAX request :(");
return false;
}
}
});
// Handles the event when a question is focused out
$('.question-wrapper').focusout(
@@ -52,114 +50,6 @@ $(document).ready(function () {
console.debug("[survey] Focus lost on question " + $(this).attr("id"));
});
// Parameters for form submission
$('.js_surveyform').ajaxForm({
type: 'POST',
contentType: 'application/json', // request mimetype
dataType: 'json', // answer expected type
//success:
//error:
url: submit_controller
});
$('.js_surveyform').submit(function(){
$(this).ajaxSubmit();
return false;
});
// $('form').submit(
// function(){
// console.debug("[survey] Submit button clicked");
// var valid = true;
// $('.js_errzone').html("").hide();
// $('.question-wrapper').each(
// function(){
// var qparams = jQuery.parseJSON($(this).attr('data-oe-survey-qparams'));
// var question_id = $(this).attr('id').slice(7); //all the ids of question containers start with "survey_"
// var candidate_answers = _.filter(answers, function(item){return item.name.startsWith(question_id);});
// var answer_value;
// if(qparams.type === 'free_text'){
// answer_value = candidate_answers[0].value.trim();
// if(qparams.constr_mandatory === true && answer_value === ''){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// } else if (qparams.type === 'textbox'){
// answer_value = candidate_answers[0].value.trim();
// if(qparams.constr_mandatory === true && answer_value === ''){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// } else if (qparams.type === 'numerical_box'){
// answer_value = candidate_answers[0].value.trim();
// if(qparams.constr_mandatory === true && answer_value === ''){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// if(answer_value !== ''){
// if(_.isNaN(parseFloat(answer_value))){
// valid = false;
// display_form_error(question_id, "You must answer with a number");
// }
// }
// } else if (qparams.type === 'datetime'){
// answer_value = candidate_answers[0].value.trim();
// if(qparams.constr_mandatory === true && answer_value === ''){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// if(answer_value !== ''){
// console.warning("Date format validation is not yet implemented");
// }
// } else if (qparams.type === 'simple_choice_scale' || qparams.type === 'simple_choice_dropdown'){
// if(qparams.constr_mandatory === true && _.size(candidate_answers) === 0){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// // if answers:
// // answer_value = candidate_answers[0].value;
// } else if (qparams.type === 'multiple_choice'){
// var answ_nbr = _.size(candidate_answers);
// if(qparams.constr_mandatory === true && answ_nbr <= 0){
// valid = false;
// display_form_error(question_id, "This question is mandatory");
// }
// // } else if (qparams.type === 'vector'){
// // } else if (qparams.type === 'matrix'){
// } else {
// console.warning('Not supported question type!');
// }
// });
// return valid;
// // _.filter(answers, function(item){return item.name.startsWith(question_id);})
// return;
// });
console.debug("[survey] Custom JS for survey loaded!");
});
+32 -21
View File
@@ -345,24 +345,31 @@ class survey_question(osv.osv):
oldname='descriptive_text'),
# Answer
'type': fields.selection([('free_text', 'Free Text'),
'type': fields.selection([('free_text', 'Long text zone'),
('textbox', 'Text box'),
('numerical_box', 'Numerical box'),
('datetime', 'Date and Time'),
('simple_choice_scale', 'One choice on a scale'),
('simple_choice_dropdown', 'One choice in a menu'),
('multiple_choice', 'Some choices in checkboxes'),
('simple_choice', 'Multiple choice (one answer)'),
('multiple_choice', 'Multiple choice (multiple answers)'),
('matrix', 'Matrix')], 'Question Type', required=1),
'suggested_answers_ids': fields.one2many('survey.suggestion',
'labels_ids': fields.one2many('survey.label',
'question_id', 'Suggested answers', oldname='answer_choice_ids'),
# Display options
'simple_choice_display' : fields.selection([('1col', 'Buttons (1 column)'),
('2col', 'Buttons (2 columns)'),
('3col', 'Buttons (3 columns)'),
('horizontal', 'Buttons (horizontal)'),
('dropdown', 'Dropdown menu'),],
'Display mode', ),
# Comments
'comments_allowed': fields.boolean('Allow comments',
oldname="allow_comment"),
'comment_children_ids': fields.one2many('survey.question_id',
'parent_id', 'Comment question'), # one2one in fact
'comment_count_as_answer': fields.boolean('Make Comment Field an \
Answer Choice', oldname='make_comment_field'),
#'comment_children_ids': fields.one2many('survey.question_id',
# 'parent_id', 'Comment question'), # one2one in fact
'comment_count_as_answer': fields.boolean('Comment field is an answer choice',
oldname='make_comment_field'),
# Validation
'validation_required': fields.boolean('Validate entry',
@@ -371,7 +378,7 @@ class survey_question(osv.osv):
('has_length', 'Must have a specific length'),
('is_integer', 'Must be an integer'),
('is_decimal', 'Must be a decimal number'),
('is_date', 'Must be a date'),
#('is_date', 'Must be a date'),
('is_email', 'Must be an email address')
], 'Validation type'),
'validation_length_min': fields.integer('Minimum length'),
@@ -382,8 +389,7 @@ class survey_question(osv.osv):
'validation_max_int_value': fields.integer('Maximum value'),
'validation_min_date': fields.date('Start date range'),
'validation_max_date': fields.date('End date range'),
'validation_error_msg': fields.char("Error message if validation \
fails", oldname='validation_valid_err_msg'),
'validation_error_msg': fields.char("Error message", oldname='validation_valid_err_msg'),
# Constraints on number of answers
'constr_mandatory': fields.boolean('Mandatory question',
@@ -398,23 +404,28 @@ class survey_question(osv.osv):
oldname='maximum_req_ans'),
'constr_minimum_req_ans': fields.integer('Minimum Required Answer',
oldname='minimum_req_ans'),
'constr_error_msg': fields.char("Error message if constraints fails",
'constr_error_msg': fields.char("Error message",
oldname='req_error_msg'),
}
_defaults = {
'page_id': lambda s, cr, uid, c: c.get('page_id'),
'sequence': 1,
'type': 'free_text',
'validation_error_msg': lambda s, cr, uid, c: _('The answer you entered has an invalid format.'),
'simple_choice_display': '1col',
'constr_type': 'at least',
'constr_minimum_req_ans': 1,
'constr_error_msg': lambda s, cr, uid, c:
_('This question requires an answer.'),
'validation_error_msg': lambda s, cr, uid, c: _('The answer you entered has an invalid format.'),
'validation_required': 'False',
'validation_length_min': 0,
'validation_length_max': 5000,
}
_sql_constraints = [('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!')]
_sql_constraints = [
('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
('validation_int', 'CHECK (validation_min_int_value <= validation_max_int_value)', 'Max value cannot be smaller than min value!'),
('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!')
]
def on_change_page_id(self, cr, uid, ids, page_id, context=None):
if page_id:
@@ -527,9 +538,9 @@ class survey_question(osv.osv):
}
class survey_suggestion(osv.osv):
class survey_label(osv.osv):
''' A suggested answer for a question '''
_name = 'survey.suggestion'
_name = 'survey.label'
_rec_name = 'value'
_columns = {
@@ -678,7 +689,7 @@ class survey_user_input_line(osv.osv):
'value_number': fields.float("Numerical answer"),
'value_date': fields.datetime("Date answer"),
'value_free_text': fields.text("Free Text answer"),
'value_suggested': fields.many2one('survey.suggestion'),
'value_suggested': fields.many2one('survey.label'),
}
_defaults = {
'skipped': False,
+13 -13
View File
@@ -402,14 +402,14 @@
<field name="description" />
</group>
<!-- Suggestions -->
<group colspan="2" string="Choice of answers" attrs="{'invisible':[('type','not in',['simple_choice_dropdown',
'multiple_choice','simple_choice_scale'])]}">
<field name="suggested_answers_ids" nolabel="1" context="{'default_question_id': active_id}">
<!-- Labels -->
<group colspan="2" string="Choice of answers" attrs="{'invisible':[('type','not in',['simple_choice', 'multiple_choice'])]}">
<field colspan="2" name="labels_ids" nolabel="1" context="{'default_question_id': active_id}">
<tree string="Suggested answers" editable="bottom">
<field name="value" />
</tree>
</field>
<field colspan="2" name="simple_choice_display" string="Display mode"/>
</group>
<!-- Sub-questions -->
@@ -422,7 +422,7 @@
<group colspan="2" string="Question required">
<field name="constr_mandatory" string="Is this question mandatory?"/>
<group attrs="{'invisible':[('constr_mandatory','=',False)]}">
<group attrs="{'invisible':[('type','not in',['numerical_box'])]}">
<group attrs="{'invisible':[('type','not in',['matrix'])]}">
<field name="constr_type" string="Users must answer..."/>
<group attrs="{'invisible':[('constr_type','not in',['at least','a range'])]}">
<field name="constr_minimum_req_ans" nolabel="1" />
@@ -436,7 +436,7 @@
</group>
<!-- Answer validation -->
<group colspan="1" string="Answer Validation" attrs="{'invisible':[('type','not in',['textbox','numerical_box','datetime'])]}">
<group colspan="1" string="Answer Validation" attrs="{'invisible':[('type','not in',['textbox'])]}">
<group>
<field name="validation_required"/>
@@ -464,7 +464,7 @@
</group>
<!-- Comments -->
<group colspan="2" string="Comments" attrs="{'invisible':[('type','in',['free_text','textbox'])]}">
<group colspan="2" string="Comments" attrs="{'invisible':[('type','not in',['simple_choice','multiple_choice'])]}">
<field name='comments_allowed' />
<group attrs="{'invisible':[('comments_allowed','=',False)]}">
<field name='comment_count_as_answer' />
@@ -518,9 +518,9 @@
<field name="context">{'search_default_group_by_survey': True, 'search_default_group_by_page': True}</field>
</record>
<record model="ir.actions.act_window" id="action_survey_suggestion_form">
<field name="name">Survey Suggestions</field>
<field name="res_model">survey.suggestion</field>
<record model="ir.actions.act_window" id="action_survey_label_form">
<field name="name">Survey labels</field>
<field name="res_model">survey.label</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<!--<field name="view_id" ref="survey_question_tree"></field>
@@ -666,7 +666,7 @@
<act_window domain="[('question_id', '=', active_id)]"
id="act_survey_answer"
name="Answers"
res_model="survey.suggestion"
res_model="survey.label"
src_model="survey.question"/>
<!--
@@ -702,8 +702,8 @@
action="action_survey_question_form" parent="menu_surveys_configuration"
sequence="2"/>
<menuitem name="Survey Suggestions" id="menu_survey_suggestion_form1"
action="action_survey_suggestion_form" parent="menu_surveys_configuration"
<menuitem name="Survey labels" id="menu_survey_label_form1"
action="action_survey_label_form" parent="menu_surveys_configuration"
sequence="3"/>
<!-- web -->
+103 -28
View File
@@ -9,11 +9,10 @@
<h1>Open surveys</h1>
<div class="table-responsive">
<table class="table table-hover">
<tr><th>Title</th><th>Description</th><th>Category</th></tr>
<tr><th>Title</th><th>Category</th></tr>
<t t-foreach="surveys" t-as="survey">
<tr>
<td><a t-att-href="survey.public_url"><span t-field="survey.title" /></a></td>
<td><t t-if="survey.description"><span t-field="survey.description"/></t></td>
<td><span t-field="survey.category"/></td>
</tr>
</t>
@@ -112,6 +111,10 @@
<t t-if="question.type in ['free_text']"><t t-call="survey.free_text"/></t>
<t t-if="question.type in ['textbox']"><t t-call="survey.textbox"/></t>
<t t-if="question.type in ['numerical_box']"><t t-call="survey.numerical_box"/></t>
<t t-if="question.type in ['datetime']"><t t-call="survey.datetime"/></t>
<t t-if="question.type in ['simple_choice']"><t t-call="survey.simple_choice"/></t>
<!-- <h2><span t-field='question.question' /> <t t-if="question.constr_mandatory"><abbr title="This question is mandatory." class="text-danger">*</abbr></t></h2>
<p class="text-muted"><t t-if="question.description"><span t-field='question.description'/></t></p> -->
@@ -120,41 +123,19 @@
<!-- instanciating different types of questions -->
<!--
<t t-if="question.type in ['numerical_box']">
<input type="number" step=".1" class="form-control" t-att-name="prefix"/>
</t>
<t t-if="question.type in ['datetime']" >
<input type="datetime-local" class="form-control" t-att-name="prefix" placeholder="jj-mm-aaaaThh:mm" />
TODO: replace html5 datetime widget by OpenERP-CMS datetime widget
</t>
<t t-if="question.type in ['simple_choice_scale']">
<t t-foreach='question.suggested_answers_ids' t-as='suggestion'>
<div class="radio">
<label>
<input type="radio" t-att-name="prefix" t-att-value='suggestion.value' />
<span t-field='suggestion.value'/>
</label>
</div>
</t>
</t>
<t t-if="question.type in ['simple_choice_dropdown']">
<select class="form-control" t-att-name="prefix">
<option disabled="1" selected="1">Choose...</option>
<t t-foreach='question.suggested_answers_ids' t-as='suggestion'>
<option><t t-esc='suggestion.value'/></option>
</t>
</select>
</t>
<t t-if="question.type in ['multiple_choice']">
<t t-foreach='question.suggested_answers_ids' t-as='suggestion'>
<t t-foreach='question.suggested_answers_ids' t-as='label'>
<div class="checkbox">
<label>
<input type="checkbox" t-att-name="prefix + '_' + suggestion.id.__str__()" t-att-value='suggestion.value' />
<span t-field='suggestion.value'/>
<input type="checkbox" t-att-name="prefix + '_' + label.id.__str__()" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
@@ -194,6 +175,100 @@
<input type="text" class="form-control" t-att-name="prefix"/>
</template>
<template id="numerical_box" name="Numerical box">
<h2><span t-field='question.question' /><t t-if="question.constr_mandatory"><abbr title="This question is mandatory." class="text-danger">*</abbr></t></h2>
<p class="text-muted"><t t-if="question.description"><span t-field='question.description'/></t></p>
<input type="number" step=".1" class="form-control" t-att-name="prefix"/>
</template>
<template id="datetime" name="Numerical box">
<h2><span t-field='question.question' /><t t-if="question.constr_mandatory"><abbr title="This question is mandatory." class="text-danger">*</abbr></t></h2>
<p class="text-muted"><t t-if="question.description"><span t-field='question.description'/></t></p>
<input type="datetime-local" class="form-control" t-att-name="prefix" placeholder="jj-mm-aaaa hh:mm" />
</template>
<template id="simple_choice" name="Simple choice">
<h2><span t-field='question.question' /><t t-if="question.constr_mandatory"><abbr title="This question is mandatory." class="text-danger">*</abbr></t></h2>
<p class="text-muted"><t t-if="question.description"><span t-field='question.description'/></t></p>
<t t-if="question.simple_choice_display == 'dropdown'">
<select class="form-control" t-att-name="prefix">
<option disabled="1" selected="1">Choose...</option>
<t t-foreach='question.labels_ids' t-as='label'>
<option><t t-esc='label.value'/></option>
</t>
</select>
</t>
<t t-if="question.simple_choice_display == 'horizontal'">
<div>
<t t-foreach='question.labels_ids' t-as='label'>
<div class="radio-inline">
<label>
<input type="radio" t-att-name="prefix" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
</div>
</t>
<t t-if="question.simple_choice_display == '1col'">
<div>
<t t-foreach='question.labels_ids' t-as='label'>
<div class="radio">
<label>
<input type="radio" t-att-name="prefix" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
</div>
</t>
<t t-if="question.simple_choice_display == '2col'">
<div class="row">
<t t-foreach='question.labels_ids' t-as='label'>
<div class="radio col-md-6">
<label>
<input type="radio" t-att-name="prefix" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
</div>
</t>
<t t-if="question.simple_choice_display == '3col'">
<div class="row">
<t t-foreach='question.labels_ids' t-as='label'>
<div class="radio col-md-4">
<label>
<input type="radio" t-att-name="prefix" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
</div>
</t>
<!--
<t t-if="question.type in ['simple_choice_scale']">
<t t-foreach='question.suggested_answers_ids' t-as='label'>
<div class="radio">
<label>
<input type="radio" t-att-name="prefix" t-att-value='label.value' />
<span t-field='label.value'/>
</label>
</div>
</t>
</t>
<t t-if="question.type in ['simple_choice_dropdown']">
</t>
-->
</template>
<!-- Printable view of a survey (all pages) -->
<template id="survey_print" name="Survey">
<t t-call="website.layout">