[MERGE] Reporting tool for surveys + [REF] Serveral refactorings (code style, missing semicolons in JS, typos...) + [FIX] Security breach using unsafe eval

bzr revid: rim@openerp.com-20140303130544-xoage8hgkhtfhwpg
This commit is contained in:
Richard Mathot (OpenERP)
2014-03-03 14:05:44 +01:00
6 changed files with 639 additions and 8 deletions
+2 -1
View File
@@ -36,12 +36,13 @@ sent mails with personal token for the invitation of the survey.
'summary': 'Create surveys, collect answers and print statistics',
'author': 'OpenERP SA',
'website': 'https://www.openerp.com/apps/survey/',
'depends': ['email_template', 'mail', 'website', 'web_kanban_gauge'],
'depends': ['email_template', 'mail', 'website', 'web_kanban_gauge', 'web_graph'],
'data': [
'security/survey_security.xml',
'security/ir.model.access.csv',
'views/survey_views.xml',
'views/survey_templates.xml',
'views/survey_result.xml',
'wizard/survey_email_compose_message.xml',
'data/survey_cron.xml'
],
+147 -6
View File
@@ -19,16 +19,19 @@
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
from datetime import datetime
import werkzeug
import json
import logging
import werkzeug
from collections import Counter
from datetime import datetime
from itertools import product
from math import ceil
from openerp import SUPERUSER_ID
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.addons.website.controllers.main import Website as controllers
controllers = controllers()
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF
from openerp.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@@ -280,6 +283,144 @@ class WebsiteSurvey(http.Controller):
'token': token,
'page_nr': 0})
@http.route(['/survey/results/<model("survey.survey"):survey>'],
type='http', auth='user', multilang=True, website=True)
def survey_reporting(self, survey, token=None, **post):
'''Display survey Results & Statistics for given survey.'''
result_template, current_filters, filter_display_data = 'survey.result', [], []
if not survey.user_input_ids or not [input_id.id for input_id in survey.user_input_ids if input_id.state != 'new']:
result_template = 'survey.no_result'
if post:
current_filters, filter_display_data = self.filter_input_ids(post)
return request.website.render(result_template,
{'survey': survey,
'prepare_result': self.prepare_result,
'get_input_summary': self.get_input_summary,
'get_graph_data': self.get_graph_data,
'page_range': self.page_range,
'current_filters': current_filters,
'filter_display_data': filter_display_data
})
def filter_input_ids(self, filters):
'''If user applies any filters, then this function returns list of
filtered user_input_id and label's strings for display data in web'''
cr, uid, context = request.cr, request.uid, request.context
question_obj = request.registry['survey.question']
input_obj = request.registry['survey.user_input_line']
input_line_obj = request.registry['survey.user_input_line']
label_obj = request.registry['survey.label']
domain_filter, choice, filter_display_data = [], [], []
#if user add some random data in query URI
try:
for ids in filters:
row_id, answer_id = ids.split(',')
question_id = filters[ids]
question = question_obj.browse(cr, uid, int(question_id), context=context)
if row_id == '0':
choice.append(int(answer_id))
labels = label_obj.browse(cr, uid, [int(answer_id)], context=context)
else:
domain_filter.extend(['|', ('value_suggested_row.id', '=', int(row_id)), ('value_suggested.id', '=', int(answer_id))])
labels = label_obj.browse(cr, uid, [int(row_id), int(answer_id)], context=context)
filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]})
if choice:
domain_filter.insert(0, ('value_suggested.id', 'in', choice))
else:
domain_filter = domain_filter[1:]
except:
#if user add some random data in query URI
return([], [])
line_ids = input_line_obj.search(cr, uid, domain_filter, context=context)
filtered_input_ids = [input.user_input_id.id for input in input_obj.browse(cr, uid, line_ids, context=context)]
return (filtered_input_ids, filter_display_data)
def page_range(self, total_record, limit):
'''Returns number of pages required for pagination'''
total = ceil(total_record / float(limit))
return range(1, int(total + 1))
def prepare_result(self, question, current_filters=[]):
'''Prepare statistical data for questions by counting number of vote per choice on basis of filter'''
#Calculate and return statistics for choice
if question.type in ['simple_choice', 'multiple_choice']:
result_summary = {}
[result_summary.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
for input_line in question.user_input_line_ids:
if result_summary.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
result_summary[input_line.value_suggested.id]['count'] += 1
result_summary = result_summary.values()
#Calculate and return statistics for matrix
if question.type == 'matrix':
rows, answers, res = {}, {}, {}
[rows.update({label.id: label.value}) for label in question.labels_ids_2]
[answers.update({label.id: label.value}) for label in question.labels_ids]
for cell in product(rows.keys(), answers.keys()):
res[cell] = 0
for input_line in question.user_input_line_ids:
if not(current_filters) or input_line.user_input_id.id in current_filters:
res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1
result_summary = {'answers': answers, 'rows': rows, 'result': res}
#Calculate and return statistics for free_text, textbox, datetime
if question.type in ['free_text', 'textbox', 'datetime']:
result_summary = []
for input_line in question.user_input_line_ids:
if not(current_filters) or input_line.user_input_id.id in current_filters:
result_summary.append(input_line)
#Calculate and return statistics for numerical_box
if question.type == 'numerical_box':
result_summary = {'input_lines': []}
all_inputs = []
for input_line in question.user_input_line_ids:
if not(current_filters) or input_line.user_input_id.id in current_filters:
all_inputs.append(input_line.value_number)
result_summary['input_lines'].append(input_line)
result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
'max': round(max(all_inputs), 2),
'min': round(min(all_inputs), 2),
'most_comman': Counter(all_inputs).most_common(5)})
return result_summary
@http.route(['/survey/results/graph/<model("survey.question"):question>'],
type='http', auth='user', multilang=True, website=True)
def get_graph_data(self, question, **post):
'''Returns appropriate formated data required by graph library on basis of filter'''
current_filters = safe_eval(post.get('current_filters', '[]'))
result = []
if question.type in ['simple_choice', 'multiple_choice']:
result.append({'key': str(question.question),
'values': self.prepare_result(question, current_filters)})
if question.type == 'matrix':
data = self.prepare_result(question, current_filters)
for answer in data['answers']:
values = []
for res in data['result']:
if res[1] == answer:
values.append({'text': data['rows'][res[0]], 'count': data['result'][res]})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
def get_input_summary(self, question, current_filters=[]):
'''Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter'''
result = {}
if question.survey_id.user_input_ids:
total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
result['total_inputs'] = len(total_input_ids)
question_input_ids = []
for user_input in question.user_input_line_ids:
if not user_input.skipped:
question_input_ids.append(user_input.user_input_id.id)
result['answered'] = len(set(question_input_ids) & set(total_input_ids))
result['skipped'] = result['total_inputs'] - result['answered']
return result
def dict_soft_update(dictionary, key, value):
''' Insert the pair <key>: <value> into the <dictionary>. If <key> is
@@ -0,0 +1,42 @@
.only_right_radius{
border-top-right-radius:2em;
border-bottom-right-radius:2em;
border-top-left-radius:0;
border-bottom-left-radius:0;
}
.only_left_radius{
border-top-right-radius:0;
border-bottom-right-radius:0;
border-top-left-radius:2em;
border-bottom-left-radius:2em;
}
.no_radius{
border-radius:0;
}
.clear_survey_filter{
cursor:pointer;
}
.nvtooltip h5 {
margin: 0;
line-height: 18px;
font-weight: bold;
background-color: rgba(247,247,247,0.75);
text-align: center;
border-bottom: 1px solid #ebebeb;
-webkit-border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
border-radius: 5px 5px 0 0;
}
.survey_answer i{
padding:3px;
cursor:pointer;
}
.survey_answer i.invisible{
visibility: hidden!important;
}
@@ -0,0 +1,144 @@
/*
* OpenERP, Open Source Management Solution
* Copyright (C) 2004-TODAY OpenERP S.A. <http://www.openerp.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$(document).ready(function () {
'use strict';
console.debug("[survey] Survey Result JS is loading...");
//Script For Pagination
var survey_pagination = $('.pagination');
$.each(survey_pagination, function(index, pagination){
var question_id = $(pagination).attr("data-question_id");
var limit = $(pagination).attr("data-record_limit"); //Number of Record Par Page. If you want to change number of record per page, change record_limit in pagination template.
$('#table_question_'+ question_id +' tbody tr:lt('+limit+')').removeClass('hidden');
$('#pagination_'+question_id+' li:first').addClass('active');
$('#pagination_'+question_id+' li a').click(function(event){
event.preventDefault();
$('#pagination_'+question_id+' li').removeClass('active');
$(this).parent('li').addClass('active');
$('#table_question_'+ question_id +' tbody tr').addClass('hidden');
var num = $(this).text();
var min = (limit * (num-1))-1;
if (min == -1){
$('#table_question_'+ question_id +' tbody tr:lt('+ limit * num +')').removeClass('hidden');
}
else{
$('#table_question_'+question_id+' tbody tr:lt('+ limit * num +'):gt('+min+')').removeClass('hidden');
}
});
});
//initialize MultiBar Chart
function init_multibar_chart(){
var chart = nv.models.multiBarChart()
.x(function(d) { return d.text; })
.y(function(d) { return d.count; })
.staggerLabels(true);
// Replacing Library's Default Tooltip with our Custom One
chart.tooltip(function(key, x, y, e, graph) {
return '<h5 class="panel-primary"><div class="panel-heading">' + x + '</div></h5>' +
'<p>' + '<b>Responses : </b>' + key + '</p>' +
'<p>' + "<b>Total Vote : </b>" + y + '</p>';
});
return chart;
}
//initialize discreteBar Chart
function init_bar_chart(){
return nv.models.discreteBarChart()
.x(function(d) { return d.text; })
.y(function(d) { return d.count; })
.staggerLabels(true)
.tooltips(false)
.showValues(true);
}
//load chart to svg element chart:initialized chart, response:AJAX response, quistion_id:if of survey question, tick_limit:text length limit
function load_chart(chart, response, question_id, tick_limit){
// Custom Tick fuction for replacing long text with '...'
var customtick_function = function(d){
if(! this || d.length <= tick_limit){
return d;
}
else{
return d.slice(0,tick_limit)+'...';
}
};
chart.xAxis
.tickFormat(customtick_function);
chart.yAxis
.tickFormat(d3.format('d'));
d3.select('#graph_question_' + question_id + ' svg')
.datum(response)
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
}
//Script For Graph
var survey_graphs = $('.survey_graph');
$.each(survey_graphs, function(index, graph){
var question_id = $(graph).attr("data-question_id");
var graph_type = $(graph).attr("data-graph_type");
var current_filters = $(graph).attr("data-current_filters");
console.log(current_filters);
$.ajax({
url: '/survey/results/graph/'+question_id,
type: 'POST', // TODO check that something is posted
dataType: 'json',
data:{'current_filters': current_filters},
success: function(response, status, xhr, wfe){
if(graph_type == 'multi_bar'){
nv.addGraph(function(){
var chart = init_multibar_chart();
return load_chart(chart,response,question_id,25);
});
}
else if(graph_type == 'bar'){
nv.addGraph(function() {
var chart = init_bar_chart();
return load_chart(chart,response,question_id,35);
});
}
}
});
});
// Script for filter
$('td.survey_answer').hover(function(){$(this).find('i.fa-filter').removeClass('invisible');},function(){$(this).find('i.fa-filter').addClass('invisible');});
$('td.survey_answer i.fa-filter').click(function(){
var cell=$(this);
var question_id = cell.attr('data-question_id');
var row_id = cell.attr('data-row_id') | 0;
var answer_id = cell.attr('data-answer_id');
if(document.URL.indexOf("?") == -1){
window.location.href = document.URL+'?' + encodeURI(row_id + ','+answer_id + '=' + question_id);
}
else{
window.location.href = document.URL+'&' + encodeURI(row_id + ','+answer_id + '=' + question_id);
}
});
// for clear all filters
$('.clear_survey_filter').click(function(){
window.location.href = document.URL.substring(0,document.URL.indexOf("?"));
});
console.debug("[survey] Survey Result JS loaded!");
});
+4 -1
View File
@@ -104,7 +104,7 @@ class survey_survey(osv.Model):
""" Computes an URL for the survey results """
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid,
'web.base.url')
return {id: urljoin(base_url, "survey/result/%s/" % id) for id in ids}
return {id: urljoin(base_url, "survey/results/%s/" % id) for id in ids}
# Model fields #
@@ -479,6 +479,9 @@ class survey_question(osv.Model):
oldname='minimum_req_ans'),
'constr_error_msg': fields.char("Error message",
oldname='req_error_msg'),
'user_input_line_ids': fields.one2many('survey.user_input_line',
'question_id', 'Answers',
domain=[('skipped', '=', False)]),
}
_defaults = {
'page_id': lambda s, cr, uid, c: c.get('page_id'),
+300
View File
@@ -0,0 +1,300 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="result" name="Survey Result">
<t t-call="website.layout">
<t t-set="head">
<link href="/web_graph/static/src/css/nv.d3.css" rel="stylesheet" type="text/css"></link>
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
<script src="/web_graph/static/lib/nvd3/d3.v3.js"></script>
<script src="/web_graph/static/lib/nvd3/nv.d3.js"></script>
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
</t>
<div class="oe_structure" />
<div class="container">
<div class="jumbotron mt32">
<h1><span t-field="survey.title"></span> <span style="font-size:1.5em;" class="fa fa-bar-chart-o pull-right "/></h1>
<h2><span t-field="survey.description"></span></h2>
</div>
<div t-if="filter_display_data" class="filteres">
<span><h3><span class="fa fa-filter"></span> Filters <small class="pull-right clear_survey_filter"> <i class="fa fa-times"></i> Clear All Filters</small></h3></span>
<span t-foreach="filter_display_data" t-as="filter_data">
<span class="label label-primary only_left_radius"><i class="fa fa-filter"></i></span><span class="label label-primary no_radius" t-esc="filter_data['question_text']"></span><span class="label label-success only_right_radius" t-esc="' > '.join(filter_data['labels'])"></span>
</span>
</div>
<div t-foreach='survey.page_ids' t-as='page'>
<h1 class="mt16" t-field='page.title'></h1>
<hr/>
<div t-foreach='page.question_ids' t-as='question' class="mt16">
<t t-set="input_summary" t-value="get_input_summary(question, current_filters)"/>
<h4>
<b>Question </b>
<span t-field='question.question'></span>
<span class="pull-right">
<span class="label label-success"><span t-esc="input_summary['answered']"></span> Answered</span>
<span class="label label-danger"><span t-esc="input_summary['skipped']"></span> Skipped</span>
</span>
</h4>
<t t-if="question.description">
<h5><span class="text-muted" t-field="question.description"></span></h5>
</t>
<t t-if="question.type in ['textbox', 'free_text', 'datetime']">
<t t-call="survey.result_text"></t>
</t>
<t t-if="question.type in ['simple_choice', 'multiple_choice']">
<t t-call="survey.result_choice"></t>
</t>
<t t-if="question.type == 'matrix'">
<t t-call="survey.result_matrix"></t>
</t>
<t t-if="question.type == 'numerical_box'">
<t t-call="survey.result_number"></t>
</t>
</div>
</div>
</div>
<div class="oe_structure" />
</t>
</template>
<!-- Result for free_text,textbox and datetime -->
<template id="result_text" name="Text Result">
<t t-if="input_summary['answered'] != 0">
<table class="table table-hover" t-att-id="'table_question_'+str(question.id)">
<thead>
<tr>
<th>#</th>
<th>User Responses</th>
</tr>
</thead>
<tbody>
<t t-set="text_result" t-value="prepare_result(question, current_filters)"></t>
<tr class="hidden" t-foreach="text_result" t-as="user_input">
<td><t t-esc="user_input_index+1"></t></td>
<t t-if="question.type == 'free_text'">
<td>
<span t-field="user_input.value_free_text"></span><br/>
<small><p t-field="user_input.date_create" class="fa fa-calendar oe_date text-muted"></p></small>
<span class="pull-right">
<a t-att-href="user_input.user_input_id.print_url+user_input.user_input_id.token"> <i class="fa fa-print"></i> Print respondent's answers</a><br/>
</span>
</td>
</t>
<t t-if="question.type == 'textbox'">
<td>
<span t-field="user_input.value_text"></span><br/>
<small><p t-field="user_input.date_create" class="fa fa-calendar oe_date text-muted"></p>
<span class="pull-right">
<a t-att-href="user_input.user_input_id.print_url+user_input.user_input_id.token"> <i class="fa fa-print"></i> Print respondent's answers</a><br/>
</span>
</small>
</td>
</t>
<t t-if="question.type == 'datetime'">
<td>
<span class="oe_date" t-field="user_input.value_date"></span><br/>
<span class="pull-right">
<a t-att-href="user_input.user_input_id.print_url+user_input.user_input_id.token"> <i class="fa fa-print"></i> Print respondent's answers</a><br/>
</span>
</td>
</t>
</tr>
</tbody>
</table>
<t t-call="survey.pagination" />
</t>
<t t-if="input_summary['answered'] == 0">
<h2 style="padding-top:30px;padding-bottom:30px;text-align:center;" class="text-muted">Sorry, No one answered this question.</h2>
</t>
</template>
<!-- Result for simple_choice and multiple_choice -->
<template id="result_choice" name="Choice Result">
<div>
<!-- Tabs -->
<ul class="nav nav-tabs">
<li class="active">
<a t-att-href="'#graph_question_'+str(question.id)" data-toggle="tab">
<i class="fa fa-bar-chart-o"></i> Graph
</a>
</li>
<li>
<a t-att-href="'#data_question_'+str(question.id)" data-toggle="tab">
<i class="fa fa-list-alt"></i> Data
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active survey_graph" t-att-id="'graph_question_'+str(question.id)" t-att-data-question_id="question.id" data-graph_type="bar" t-att-data-current_filters="current_filters">
<!-- svg element for drawing bar chart -->
<svg style="height:20em"></svg>
</div>
<div class="tab-pane" t-att-id="'data_question_'+str(question.id)">
<table class="table table-hover">
<thead>
<tr>
<th>Answer Choices</th>
<th>User Responses</th>
</tr>
</thead>
<tbody>
<tr t-foreach="prepare_result(question, current_filters)" t-as="user_input">
<td>
<p t-esc="user_input['text']"></p>
</td>
<td class="survey_answer">
<span t-esc="round(user_input['count']*100.0/(input_summary['answered'] or 1),2)"></span> %
<span t-esc="user_input['count']" class="label label-primary">Vote</span>
<i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-answer_id="user_input['answer_id']"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<!-- Result for matrix -->
<template id="result_matrix" name="Matrix Result">
<t t-set="matrix_result" t-value="prepare_result(question, current_filters)" />
<!-- Tabs -->
<ul class="nav nav-tabs">
<li class="active">
<a t-att-href="'#graph_question_'+str(question.id)" data-toggle="tab">
<i class="fa fa-bar-chart-o"></i>
Graph
</a>
</li>
<li>
<a t-att-href="'#data_question_'+str(question.id)" data-toggle="tab">
<i class="fa fa-list-alt"></i>
Data
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active with-3d-shadow with-transitions survey_graph" t-att-id="'graph_question_'+str(question.id)" t-att-data-question_id= "question.id" data-graph_type= "multi_bar" t-att-data-current_filters="current_filters">
<!-- svg element for drawing Multibar chart -->
<svg style="height:20em"></svg>
</div>
<div class="tab-pane" t-att-id="'data_question_'+str(question.id)">
<table class="table table-hover text-right">
<thead>
<tr>
<th></th>
<th class="text-right" t-foreach="matrix_result['answers']" t-as="answer_id">
<span t-esc="matrix_result['answers'][answer_id]"></span>
</th>
</tr>
</thead>
<tbody>
<tr t-foreach="matrix_result['rows']" t-as="row_id">
<td>
<span t-esc="matrix_result['rows'][row_id]"></span>
</td>
<td class="survey_answer" t-foreach="matrix_result['answers']" t-as="answer_id">
<span t-esc="round(matrix_result['result'][(row_id,answer_id)]*100.0/(input_summary['answered'] or 1),2)"></span> %
<span class="label label-primary" t-esc="matrix_result['result'][(row_id,answer_id)]"></span><i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-row_id="row_id" t-att-data-answer_id="answer_id"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- Result for Numeric Data -->
<template id="result_number" name="Number Result">
<t t-set="number_result" t-value="prepare_result(question, current_filters)" />
<t t-set="text_result" t-value="number_result['input_lines']" />
<span class="pull-right mt8">
<span class="label label-default only_left_radius">Maximum </span> <span class="label label-success only_right_radius" t-esc="number_result['max']"></span>
<span class="label label-default only_left_radius">Minimum </span> <span class="label label-danger only_right_radius" t-esc="number_result['min']"></span>
<span class="label label-default only_left_radius">Average </span> <span class="label label-warning only_right_radius" t-esc="number_result['average']"></span>
</span>
<ul class="nav nav-tabs">
<li class="active">
<a t-att-href="'#most_common_'+str(question.id)" data-toggle="tab">
<i class="fa fa-list-ol"></i>
Most Common <span t-esc="len(number_result['most_comman'])"></span>
</a>
</li>
<li>
<a t-att-href="'#data_question_'+str(question.id)" data-toggle="tab">
<i class="fa fa-list-alt"></i>
All Data
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active with-3d-shadow with-transitions" t-att-id="'most_common_'+str(question.id)">
<table class="table table-hover">
<thead>
<tr>
<th>User Responce</th>
<th>Occurence</th>
</tr>
</thead>
<tbody>
<tr t-foreach="number_result['most_comman']" t-as="row">
<td>
<span t-esc="row[0]"></span>
</td>
<td>
<span t-esc="row[1]"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane" t-att-id="'data_question_'+str(question.id)">
<table class="table table-hover" t-att-id="'table_question_'+str(question.id)">
<thead>
<tr>
<th>#</th>
<th>User Responses</th>
</tr>
</thead>
<tbody>
<tr class="hidden" t-foreach="number_result['input_lines']" t-as="user_input">
<td><t t-esc="user_input_index+1"></t></td>
<td>
<span t-field="user_input.value_number"></span><br/>
<small><p t-field="user_input.date_create" class="fa fa-calendar oe_date text-muted"></p></small>
</td>
</tr>
</tbody>
</table>
<t t-call="survey.pagination"/>
</div>
</div>
</template>
<template id="no_result" name="Survey Result">
<t t-call="website.layout">
<div class="container">
<h2><span t-field='survey.title'></span></h2>
<div class="jumbotron mt32">
<h2 class="text-muted">
<span>
<i style="font-size:1.8em" class="fa fa-users pull-right"></i>
</span>
Sorry, No one answered this survey yet
</h2>
</div>
</div>
</t>
</template>
<!-- Pagination Element -->
<template id="pagination" name="Survey Result">
<t t-set="record_limit" t-value="5"/><!-- Change This record_limit to change number of record per page-->
<ul t-att-id="'pagination_%d' % question.id" class="pagination" t-att-data-question_id="question.id" t-att-data-record_limit="record_limit">
<t t-if="len(text_result) > record_limit">
<li t-foreach="page_range(len(text_result), record_limit)" t-as="num">
<a href="#" t-esc="num"></a>
</li>
</t>
</ul>
</template>
</data>
</openerp>