[REF] survey: clean results page and its code generating statistics

PURPOSE

As new features are about to land in survey, notably live interactions [1]
and new survey building [2] performing a pre cleaning is necessary. In this
PR we clean survey models by: removing unnecessary fields, cleaning some code
and finally renaming models.

SPECIFICATIONS

Purpose of this commit is to globally rewrite the way statistics are
pre computed and used in results page of surveys. Indeed current code is
very complex, having a lot of calls to the same methods while taking only
part of its results. It also have a mix of model and controller methods,
data is computed several times a bit differently, ...

In this commit we rewrite that part. Including

  * a simpler code flow;
  * a simpler data structure;
  * use a better naming for methods instead of "prepare_results" called
    randomly within the code and re-using part of its returned content;
  * use record sets / records instead of giving sub-elements of a record
    like question.title, label.value, ...

Data structure

  * simply give a question_and_page_ids to the template that will iterate
    on pages / questions;
  * have a graph_data, table_data dictionaries available for graph and
    table display with simple entries;
  * graph_data: holding all necessary but minimalist data for graph display
    if question allows it;
  * table_data: holding all necessary but minimalist data for table / summary
    display;
  * hold record sets of done / failed answers and answer lines;

In this commit we also simplify and clean pagination in survey graph results

  * remove page_range helper tool that can be inlined in the templates
    directly, doing a simple range;
  * fix and improve pagination code;

LINKS

[0] Related to Task ID 2061901 (survey models cleaning and preparation)
[1] Task ID 1972640 (live interactions)
[2] Task ID 2119587 (new frontend for building surveys)

PR #40765
This commit is contained in:
Thibault Delavallée
2019-12-05 15:21:28 +00:00
parent bb3d4634df
commit 351a4439b3
8 changed files with 513 additions and 623 deletions
+55 -151
View File
@@ -7,13 +7,13 @@ import werkzeug
from datetime import datetime
from dateutil.relativedelta import relativedelta
from math import ceil
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, format_datetime, format_date
from odoo.osv import expression
from odoo.tools import format_datetime, format_date
_logger = logging.getLogger(__name__)
@@ -484,161 +484,65 @@ class Survey(http.Controller):
def survey_report(self, survey, answer_token=None, **post):
""" Display survey Results & Statistics for given survey.
Quick retroengineering of what is injected into the template for now:
(TODO: flatten and simplify this)
survey: a browse record of the survey
survey_dict: very messy dict containing all the info to display answers
{'page_ids': [
...
{'page': browse record of the page,
'question_ids': [
...
{'graph_data': data to be displayed on the graph
'input_summary': number of answered, skipped...
'prepare_result': {
answers displayed in the tables
}
'question': browse record of the question_ids
}
...
]
}
...
]
}
page_range: pager helper function
current_filters: a list of ids
filter_display_data: [{'labels': ['a', 'b'], question_text} ... ]
filter_finish: boolean => only finished surveys or not
New structure: {
'survey': current survey browse record,
'question_and_page_data': see ``SurveyQuestion._prepare_statistics()``,
'survey_data'= see ``SurveySurvey._prepare_statistics()``
'search_filters': [],
'search_finished': either filter on finished inputs only or not,
}
"""
current_filters = []
filter_display_data = []
user_input_lines, search_filters = self._extract_filters_data(survey, post)
survey_data = survey._prepare_statistics(user_input_lines)
question_and_page_data = survey.question_and_page_ids._prepare_statistics(user_input_lines)
answers = survey.user_input_ids.filtered(lambda answer: answer.state != 'new' and not answer.test_entry)
filter_finish = post.get('finished') == 'true'
if post or filter_finish:
filter_data = self._get_filter_data(post)
current_filters = survey.filter_input_ids(filter_data, filter_finish)
filter_display_data = survey.get_filter_display_data(filter_data)
return request.render('survey.survey_page_statistics', {
# survey and its statistics
'survey': survey,
'answers': answers,
'survey_dict': self._prepare_result_dict(survey, current_filters),
'page_range': self.page_range,
'current_filters': current_filters,
'filter_display_data': filter_display_data,
'filter_finish': filter_finish
'question_and_page_data': question_and_page_data,
'survey_data': survey_data,
# search
'search_filters': search_filters,
'search_finished': post.get('finished') == 'true',
})
def _prepare_result_dict(self, survey, current_filters=None):
"""Returns dictionary having values for rendering template"""
current_filters = current_filters if current_filters else []
result = {'page_ids': []}
# First append questions without page
questions_without_page = [self._prepare_question_values(question,current_filters) for question in survey.question_ids if not question.page_id]
if questions_without_page:
result['page_ids'].append({'page': request.env['survey.question'], 'question_ids': questions_without_page})
# Then, questions in sections
for page in survey.page_ids:
page_dict = {'page': page, 'question_ids': [self._prepare_question_values(question,current_filters) for question in page.question_ids]}
result['page_ids'].append(page_dict)
if survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers']:
scoring_data = self._get_scoring_data(survey)
result['success_rate'] = scoring_data['success_rate']
result['scoring_graph_data'] = json.dumps(scoring_data['graph_data'])
return result
def _prepare_question_values(self, question, current_filters):
Survey = request.env['survey.survey']
return {
'question': question,
'input_summary': Survey.get_input_summary(question, current_filters),
'prepare_result': Survey.prepare_result(question, current_filters),
'graph_data': self._get_graph_data(question, current_filters),
}
def _get_filter_data(self, post):
"""Returns data used for filtering the result"""
filters = []
filters_data = post.get('filters')
if filters_data:
for data in filters_data.split('|'):
try:
row_id, answer_id = data.split(',')
filters.append({'row_id': int(row_id), 'answer_id': int(answer_id)})
except:
return filters
return filters
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 _get_graph_data(self, question, current_filters=None):
'''Returns formatted data required by graph library on basis of filter'''
# TODO refactor this terrible method and merge it with _prepare_result_dict
current_filters = current_filters if current_filters else []
Survey = request.env['survey.survey']
result = []
if question.question_type == 'multiple_choice':
result.append({'key': ustr(question.title),
'values': Survey.prepare_result(question, current_filters)['answers']
})
if question.question_type == 'simple_choice':
result = Survey.prepare_result(question, current_filters)['answers']
if question.question_type == 'matrix':
data = Survey.prepare_result(question, current_filters)
for answer in data['answers']:
values = []
for row in data['rows']:
values.append({'text': data['rows'].get(row), 'count': data['result'].get((row, answer))})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
def _get_scoring_data(self, survey):
"""Performs a read_group to fetch the count of failed/passed tests in a single query."""
count_data = request.env['survey.user_input'].read_group(
[('survey_id', '=', survey.id), ('state', '=', 'done'), ('test_entry', '=', False)],
['scoring_success', 'id:count_distinct'],
['scoring_success']
)
scoring_success_count = 0
scoring_failed_count = 0
for count_data_item in count_data:
if count_data_item['scoring_success']:
scoring_success_count = count_data_item['scoring_success_count']
def _extract_filters_data(self, survey, post):
search_filters = []
line_filter_domain, line_choices = [], []
for data in post.get('filters', '').split('|'):
try:
row_id, answer_id = (int(item) for item in data.split(','))
except:
pass
else:
scoring_failed_count = count_data_item['scoring_success_count']
if row_id and answer_id:
line_filter_domain = expression.AND([
['&', ('matrix_row_id', '=', row_id), ('suggested_answer_id', '=', answer_id)],
line_filter_domain
])
answers = request.env['survey.question.answer'].browse([row_id, answer_id])
elif answer_id:
line_choices.append(answer_id)
answers = request.env['survey.question.answer'].browse([answer_id])
if answer_id:
search_filters.append({
'question': answers[0].question_id.title,
'answers': '%s%s' % (answers[0].value, ': %s' % answers[1].value if len(answers) > 1 else '')
})
if line_choices:
line_filter_domain = expression.AND([[('suggested_answer_id', 'in', line_choices)], line_filter_domain])
graph_data = [{
'text': _('Passed'),
'count': scoring_success_count,
'color': '#2E7D32'
}, {
'text': _('Missed'),
'count': scoring_failed_count,
'color': '#C62828'
}]
user_input_domain = ['&', ('test_entry', '=', False), ('survey_id', '=', survey.id)]
if line_filter_domain:
matching_line_ids = request.env['survey.user_input.line'].sudo().search(line_filter_domain).ids
user_input_domain = expression.AND([
[('user_input_line_ids', 'in', matching_line_ids)],
user_input_domain
])
if post.get('finished'):
user_input_domain = expression.AND([[('state', '=', 'done')], user_input_domain])
else:
user_input_domain = expression.AND([[('state', '!=', 'new')], user_input_domain])
user_input_lines = request.env['survey.user_input'].sudo().search(user_input_domain).mapped('user_input_line_ids')
total_scoring_success = scoring_success_count + scoring_failed_count
return {
'success_rate': round((scoring_success_count / total_scoring_success) * 100, 1) if total_scoring_success > 0 else 0,
'graph_data': graph_data
}
return user_input_lines, search_filters
+148 -4
View File
@@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import collections
import json
import itertools
import operator
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
@@ -92,7 +97,7 @@ class SurveyQuestion(models.Model):
display_mode = fields.Selection(
[('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')],
string='Display Mode', default='columns', help='Display mode of simple choice questions.')
# -- comments
# -- comments (simple choice, multiple choice, matrix (without count as an answer))
comments_allowed = fields.Boolean('Show Comments Field')
comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice')
@@ -273,10 +278,149 @@ class SurveyQuestion(models.Model):
self.ensure_one()
return list(self.survey_id.question_and_page_ids).index(self)
def get_correct_answer_ids(self):
self.ensure_one()
# ------------------------------------------------------------
# STATISTICS / REPORTING
# ------------------------------------------------------------
return self.suggested_answer_ids.filtered(lambda label: label.is_correct)
def _prepare_statistics(self, user_input_lines):
""" Compute statistical data for questions by counting number of vote per choice on basis of filter """
all_questions_data = []
for question in self:
question_data = {'question': question, 'is_page': question.is_page}
if question.is_page:
all_questions_data.append(question_data)
continue
# fetch answer lines, separate comments from real answers
all_lines = user_input_lines.filtered(lambda line: line.question_id == question)
if question.question_type in ['simple_choice', 'multiple_choice', 'matrix']:
answer_lines = all_lines.filtered(
lambda line: line.answer_type == 'suggestion' or (
line.answer_type == 'char_box' and question.comment_count_as_answer)
)
comment_line_ids = all_lines.filtered(lambda line: line.answer_type == 'char_box')
else:
answer_lines = all_lines
comment_line_ids = self.env['survey.user_input.line']
skipped_lines = answer_lines.filtered(lambda line: line.skipped)
done_lines = answer_lines - skipped_lines
question_data.update(
answer_line_ids=answer_lines,
answer_line_done_ids=done_lines,
answer_input_done_ids=done_lines.mapped('user_input_id'),
answer_input_skipped_ids=skipped_lines.mapped('user_input_id'),
comment_line_ids=comment_line_ids)
question_data.update(question._get_stats_summary_data(answer_lines))
# prepare table and graph data
table_data, graph_data = question._get_stats_data(answer_lines)
question_data['table_data'] = table_data
question_data['graph_data'] = json.dumps(graph_data)
all_questions_data.append(question_data)
return all_questions_data
def _get_stats_data(self, user_input_lines):
if self.question_type in ['simple_choice']:
return self._get_stats_data_answers(user_input_lines)
elif self.question_type in ['multiple_choice']:
table_data, graph_data = self._get_stats_data_answers(user_input_lines)
return table_data, [{'key': self.title, 'values': graph_data}]
elif self.question_type in ['matrix']:
return self._get_stats_graph_data_matrix(user_input_lines)
return [line for line in user_input_lines], []
def _get_stats_data_answers(self, user_input_lines):
""" Statistics for question.answer based questions (simple choice, multiple
choice.). A corner case with a void record survey.question.answer is added
to count comments that should be considered as valid answers. This small hack
allow to have everything available in the same standard structure. """
suggested_answers = [answer for answer in self.mapped('suggested_answer_ids')]
if self.comment_count_as_answer:
suggested_answers += [self.env['survey.question.answer']]
count_data = dict.fromkeys(suggested_answers, 0)
for line in user_input_lines:
if line.suggested_answer_id or (line.value_char_box and self.comment_count_as_answer):
count_data[line.suggested_answer_id] += 1
table_data = [{
'value': _('Other (see comments)') if not sug_answer else sug_answer.value,
'suggested_answer': sug_answer,
'count': count_data[sug_answer]
}
for sug_answer in suggested_answers]
graph_data = [{
'text': _('Other (see comments)') if not sug_answer else sug_answer.value,
'count': count_data[sug_answer]
}
for sug_answer in suggested_answers]
return table_data, graph_data
def _get_stats_graph_data_matrix(self, user_input_lines):
suggested_answers = self.mapped('suggested_answer_ids')
matrix_rows = self.mapped('matrix_row_ids')
count_data = dict.fromkeys(itertools.product(matrix_rows, suggested_answers), 0)
for line in user_input_lines:
if line.matrix_row_id and line.suggested_answer_id:
count_data[(line.matrix_row_id, line.suggested_answer_id)] += 1
table_data = [{
'row': row,
'columns': [{
'suggested_answer': sug_answer,
'count': count_data[(row, sug_answer)]
} for sug_answer in suggested_answers],
} for row in matrix_rows]
graph_data = [{
'key': sug_answer.value,
'values': [{
'text': row.value,
'count': count_data[(row, sug_answer)]
}
for row in matrix_rows
]
} for sug_answer in suggested_answers]
return table_data, graph_data
def _get_stats_summary_data(self, user_input_lines):
if self.question_type in ['simple_choice', 'multiple_choice']:
return self._get_stats_summary_data_choice(user_input_lines)
if self.question_type in ['numerical_box']:
return self._get_stats_summary_data_numerical(user_input_lines)
return {}
def _get_stats_summary_data_choice(self, user_input_lines):
right_inputs, partial_inputs = self.env['survey.user_input'], self.env['survey.user_input']
right_answers = self.suggested_answer_ids.filtered(lambda label: label.is_correct)
if self.question_type == 'multiple_choice':
for user_input, lines in tools.groupby(user_input_lines, operator.itemgetter('user_input_id')):
user_input_answers = self.env['survey.user_input.line'].concat(*lines).filtered(lambda l: l.answer_is_correct).mapped('suggested_answer_id')
if user_input_answers and user_input_answers < right_answers:
partial_inputs += user_input
elif user_input_answers:
right_inputs += user_input
else:
right_inputs = user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id')
return {
'right_answers': right_answers,
'right_inputs': right_inputs,
'partial_inputs': partial_inputs,
}
def _get_stats_summary_data_numerical(self, user_input_lines):
all_values = user_input_lines.filtered(lambda line: not line.skipped).mapped('value_numerical_box')
lines_sum = sum(all_values)
return {
'numerical_max': max(all_values, default=0),
'numerical_min': min(all_values, default=0),
'numerical_average': round(lines_sum / len(all_values) or 1, 2),
'numerical_common_lines': collections.Counter(all_values).most_common(5),
}
class SurveyQuestionAnswer(models.Model):
+34 -122
View File
@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import random
import uuid
import werkzeug
from collections import Counter, OrderedDict
from itertools import product
from odoo import api, exceptions, fields, models, _
from odoo.osv import expression
@@ -554,129 +552,43 @@ class Survey(models.Model):
# GRAPH / RESULTS
# ------------------------------------------------------------
def filter_input_ids(self, filters, finished=False):
"""If user applies any filters, then this function returns list of
filtered user_input_id and label's strings for display data in web.
:param filters: list of dictionary (having: row_id, ansewr_id)
:param finished: True for completely filled survey,Falser otherwise.
:returns list of filtered user_input_ids.
"""
self.ensure_one()
if filters:
domain_filter, choice = [], []
for current_filter in filters:
row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
if row_id == 0:
choice.append(answer_id)
else:
domain_filter.extend(['|', ('matrix_row_id.id', '=', row_id), ('suggested_answer_id.id', '=', answer_id)])
if choice:
domain_filter.insert(0, ('suggested_answer_id.id', 'in', choice))
else:
domain_filter = domain_filter[1:]
input_lines = self.env['survey.user_input.line'].search(domain_filter)
filtered_input_ids = [input_line.user_input_id.id for input_line in input_lines]
def _prepare_statistics(self, user_input_lines=None):
if user_input_lines:
user_input_domain = [
('survey_id', 'in', self.ids),
('id', 'in', user_input_lines.mapped('user_input_id').ids)
]
else:
filtered_input_ids = []
if finished:
UserInput = self.env['survey.user_input']
if not filtered_input_ids:
user_inputs = UserInput.search([('survey_id', '=', self.id)])
user_input_domain = [
('survey_id', 'in', self.ids),
('state', '=', 'done'),
('test_entry', '=', False)
]
count_data = self.env['survey.user_input'].sudo().read_group(user_input_domain, ['scoring_success', 'id:count_distinct'], ['scoring_success'])
scoring_success_count = 0
scoring_failed_count = 0
for count_data_item in count_data:
if count_data_item['scoring_success']:
scoring_success_count += count_data_item['scoring_success_count']
else:
user_inputs = UserInput.browse(filtered_input_ids)
return user_inputs.filtered(lambda input_item: input_item.state == 'done').ids
return filtered_input_ids
scoring_failed_count += count_data_item['scoring_success_count']
@api.model
def get_filter_display_data(self, filters):
"""Returns data to display current filters
:param filters: list of dictionary (having: row_id, answer_id)
:returns list of dict having data to display filters.
"""
filter_display_data = []
if filters:
Label = self.env['survey.question.answer']
for current_filter in filters:
row_id, answer_id = current_filter['row_id'], current_filter['answer_id']
label = Label.browse(answer_id)
question = label.question_id
if row_id == 0:
labels = label
else:
labels = Label.browse([row_id, answer_id])
filter_display_data.append({'question_text': question.title,
'labels': labels.mapped('value')})
return filter_display_data
success_graph = json.dumps([{
'text': _('Passed'),
'count': scoring_success_count,
'color': '#2E7D32'
}, {
'text': _('Missed'),
'count': scoring_failed_count,
'color': '#C62828'
}])
@api.model
def prepare_result(self, question, current_filters=None):
""" Compute statistical data for questions by counting number of vote per choice on basis of filter """
current_filters = current_filters if current_filters else []
result_summary = {}
input_lines = question.user_input_line_ids.filtered(lambda line: not line.user_input_id.test_entry)
# Calculate and return statistics for choice
if question.question_type in ['simple_choice', 'multiple_choice']:
comments = []
answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id, 'answer_score': label.answer_score}) for label in question.suggested_answer_ids)
for input_line in input_lines:
if input_line.answer_type == 'suggestion' and answers.get(input_line.suggested_answer_id.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
answers[input_line.suggested_answer_id.id]['count'] += 1
if input_line.answer_type == 'char_box' and (not(current_filters) or input_line.user_input_id.id in current_filters):
comments.append(input_line)
result_summary = {'answers': list(answers.values()), 'comments': comments}
# Calculate and return statistics for matrix
if question.question_type == 'matrix':
rows = OrderedDict()
answers = OrderedDict()
res = dict()
comments = []
[rows.update({label.id: label.value}) for label in question.matrix_row_ids]
[answers.update({label.id: label.value}) for label in question.suggested_answer_ids]
for cell in product(rows, answers):
res[cell] = 0
for input_line in input_lines:
if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.matrix_row_id:
res[(input_line.matrix_row_id.id, input_line.suggested_answer_id.id)] += 1
if input_line.answer_type == 'char_box' and (not(current_filters) or input_line.user_input_id.id in current_filters):
comments.append(input_line)
result_summary = {'answers': answers, 'rows': rows, 'result': res, 'comments': comments}
# Calculate and return statistics for text_box, char_box, date
if question.question_type in ['text_box', 'char_box', 'date', 'datetime']:
result_summary = []
for input_line in input_lines:
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.question_type == 'numerical_box':
result_summary = {'input_lines': []}
all_inputs = []
for input_line in input_lines:
if not(current_filters) or input_line.user_input_id.id in current_filters:
all_inputs.append(input_line.value_numerical_box)
result_summary['input_lines'].append(input_line)
if all_inputs:
result_summary.update({'average': round(sum(all_inputs) / len(all_inputs), 2),
'max': round(max(all_inputs), 2),
'min': round(min(all_inputs), 2),
'sum': sum(all_inputs),
'most_common': Counter(all_inputs).most_common(5)})
return result_summary
@api.model
def get_input_summary(self, question, current_filters=None):
""" Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter """
current_filters = current_filters if current_filters else []
result = {}
search_line_ids = current_filters if current_filters else question.user_input_line_ids.ids
result['answered'] = len([line for line in question.user_input_line_ids if line.user_input_id.state != 'new' and not line.user_input_id.test_entry and not line.skipped])
result['skipped'] = len([line for line in question.user_input_line_ids if line.user_input_id.state != 'new' and not line.user_input_id.test_entry and line.skipped])
return result
total = scoring_success_count + scoring_failed_count
return {
'global_success_rate': round((scoring_success_count / total) * 100, 1) if total > 0 else 0,
'global_success_graph': success_graph
}
# ------------------------------------------------------------
# GAMIFICATION / BADGES
+1
View File
@@ -342,6 +342,7 @@ class SurveyUserInputLine(models.Model):
value_text_box = fields.Text('Free Text answer')
suggested_answer_id = fields.Many2one('survey.question.answer', string="Suggested answer")
matrix_row_id = fields.Many2one('survey.question.answer', string="Row answer")
# scoring
answer_score = fields.Float('Score')
answer_is_correct = fields.Boolean('Correct', compute='_compute_answer_is_correct')
+5 -20
View File
@@ -14,7 +14,7 @@ var D3_COLORS = ["#1f77b4","#ff7f0e","#aec7e8","#ffbb78","#2ca02c","#98df8a","#d
// -> this is ugly / not efficient, needs to be refactored
publicWidget.registry.SurveyResultPagination = publicWidget.Widget.extend({
events: {
'click li a': '_onPageClick',
'click li.o_survey_js_results_pagination a': '_onPageClick',
},
//--------------------------------------------------------------------------
@@ -38,7 +38,6 @@ publicWidget.registry.SurveyResultPagination = publicWidget.Widget.extend({
var self = this;
return this._super.apply(this, arguments).then(function () {
self.limit = self.$el.data("record_limit");
self._changePage(1);
});
},
@@ -52,24 +51,9 @@ publicWidget.registry.SurveyResultPagination = publicWidget.Widget.extend({
*/
_onPageClick: function (ev) {
ev.preventDefault();
this.$('li.o_survey_js_results_pagination').removeClass('active');
var pageNumber = $(ev.currentTarget).text();
this._changePage(pageNumber);
},
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
/**
* Shows / hides the records based on the selected page.
*
* @private
* @param {string} pageNumber
*/
_changePage: function (pageNumber) {
this.$('li').removeClass('active');
var $target = this.$(_.str.sprintf('li:contains("%s")', pageNumber));
var $target = $(ev.currentTarget);
$target.closest('li').addClass('active');
this.$questionsEl.find('tbody tr').addClass('d-none');
@@ -82,6 +66,7 @@ publicWidget.registry.SurveyResultPagination = publicWidget.Widget.extend({
this.$questionsEl.find('tbody tr:lt('+ this.limit * num +'):gt(' + min + ')')
.removeClass('d-none');
}
},
});
@@ -343,7 +328,7 @@ publicWidget.registry.SurveyResultWidget = publicWidget.Widget.extend({
self.$('.pagination').each(function (){
var questionId = $(this).data("question_id");
new publicWidget.registry.SurveyResultPagination(self, {
'questionsEl': self.$('#table_question_'+ questionId)
'questionsEl': self.$('#survey_table_question_'+ questionId)
}).attachTo($(this));
});
-50
View File
@@ -120,53 +120,3 @@ class TestSurveyInternals(common.TestSurveyCommon):
question.validate_question('valid'),
{}
)
@users('survey_manager')
def test_result_data_simple_multiple_choice(self):
question = self._add_question(
self.page_0, 'Q0', 'simple_choice',
labels=[{'value': 'Choice0'}, {'value': 'Choice1'}]
)
for i in range(3):
answer = self._add_answer(self.survey, False, email='public@example.com')
self._add_answer_line(
question, answer, random.choice(question.suggested_answer_ids.ids),
answer_type='suggestion', answer_fname='suggested_answer_id')
lines = [line.suggested_answer_id.id for line in question.user_input_line_ids]
answers = [{'text': label.value, 'count': lines.count(label.id), 'answer_id': label.id, 'answer_score': label.answer_score} for label in question.suggested_answer_ids]
prp_result = self.env['survey.survey'].prepare_result(question)['answers']
self.assertItemsEqual(prp_result, answers)
@users('survey_manager')
def test_result_data_matrix(self):
question = self._add_question(
self.page_0, 'Q0', 'matrix', matrix_subtype='simple',
labels=[{'value': 'Column0'}, {'value': 'Column1'}],
labels_2=[{'value': 'Row0'}, {'value': 'Row1'}]
)
for i in range(3):
answer = self._add_answer(self.survey, False, email='public@example.com')
self._add_answer_line(
question, answer, random.choice(question.suggested_answer_ids.ids),
answer_type='suggestion', answer_fname='suggested_answer_id', matrix_row_id=random.choice(question.matrix_row_ids.ids)
)
lines = [(line.matrix_row_id.id, line.suggested_answer_id.id) for line in question.user_input_line_ids]
res = {}
for i in product(question.matrix_row_ids.ids, question.suggested_answer_ids.ids):
res[i] = lines.count((i))
self.assertEqual(self.env['survey.survey'].prepare_result(question)['result'], res)
@users('survey_manager')
def test_result_data_numeric_box(self):
question = self._add_question(self.page_0, 'Q0', 'numerical_box')
num = [float(n) for n in random.sample(range(1, 100), 3)]
nsum = sum(num)
for i in range(3):
answer = self._add_answer(self.survey, False, email='public@example.com')
self._add_answer_line(question, answer, num[i])
exresult = {
'average': round((nsum / len(num)), 2), 'max': round(max(num), 2),
'min': round(min(num), 2), 'sum': nsum, 'most_common': Counter(num).most_common(5)}
result = self.env['survey.survey'].prepare_result(question)
for key in exresult:
self.assertEqual(result[key], exresult[key])
@@ -112,7 +112,12 @@
</div>
<div class="container o_survey_result p-4" t-if="graph_data">
<div class="tab-content">
<div role="tabpanel" class="tab-pane active survey_graph" t-att-data-scoring_percentage="answer.scoring_percentage" t-att-id="'graph_question_%d' % answer.id" t-att-data-question_id="answer.id" data-graph-type="doughnut" t-att-data-graph-data="graph_data">
<div role="tabpanel" class="tab-pane active survey_graph"
t-att-data-scoring_percentage="answer.scoring_percentage"
t-att-id="'survey_graph_question_%d' % answer.id"
t-att-data-question_id="answer.id"
data-graph-type="doughnut"
t-att-data-graph-data="graph_data">
<canvas id="doughnut_chart"></canvas>
<span class="o_overall_performance"></span>
</div>
@@ -3,111 +3,106 @@
<data>
<template id="survey_page_statistics" name="Survey: result statistics page">
<t t-call="survey.layout">
<t t-set="page_record_limit" t-value="10"/><!-- Change this record_limit to change number of record per page-->
<div class="oe_structure" id="oe_structure_survey_result_1"/>
<div class="container o_survey_result">
<t t-call="survey.survey_button_form_view" />
<div class="jumbotron mt32">
<h1><span t-field="survey.title" />
<span style="font-size:1.5em;" t-attf-class="fa fa-bar-chart-o #{'fa-bar-chart-o' if survey.scoring_type == 'no_scoring' else 'fa-trophy' if survey.certification else 'fa-question-circle-o'} float-right " role="img" aria-label="Chart" title="Chart"/></h1>
<div t-field="survey.description" class="oe_no_empty" />
<h2 t-if="not answers">
<h1>
<span t-field="survey.title"/>
<span style="font-size:1.5em;"
t-attf-class="fa fa-bar-chart-o #{'fa-bar-chart-o' if survey.scoring_type == 'no_scoring' else 'fa-trophy' if survey.certification else 'fa-question-circle-o'} float-right " role="img" aria-label="Chart" title="Chart"/>
</h1>
<div t-field="survey.description" class="oe_no_empty"/>
<h2 t-if="not question_and_page_data">
Sorry, no one answered this survey yet.
</h2>
</div>
<div t-if="answers" class="card d-print-none">
<div class="card-header"><span class="fa fa-filter"></span> Filters <span t-if="filter_display_data" class="float-right text-primary clear_survey_filter"><i class="fa fa-times"></i> Clear All Filters</span></div>
<div t-if="question_and_page_data" class="card d-print-none">
<div class="card-header"><span class="fa fa-filter"></span> Filters <span t-if="search_filters" class="float-right text-primary clear_survey_filter"><i class="fa fa-times"></i> Clear All Filters</span></div>
<div class="card-body">
<span t-if="filter_finish == True">
<span class="badge badge-secondary only_left_radius filter-all">All surveys</span><span class="badge badge-primary only_right_radius filter-finished o_active_filter">Finished surveys</span>
</span>
<span t-if="filter_finish == False">
<span class="badge badge-primary only_left_radius filter-all o_active_filter">All surveys</span><span class="badge badge-secondary only_right_radius filter-finished">Finished surveys</span>
</span>
<span t-foreach="filter_display_data" t-as="filter_data">
<span t-att-class="'badge only_left_radius filter-all %s' % 'badge-secondary' if search_finished else 'badge-primary o_active_filter'">All surveys</span>
<span t-att-class="'badge only_right_radius filter-finished %s' % 'badge-secondary' if not search_finished else 'badge-primary o_active_filter'">Finished surveys</span>
<span t-foreach="search_filters" t-as="filter_data">
<span class="badge badge-primary only_left_radius"><i class="fa fa-filter" role="img" aria-label="Filter" title="Filter"></i></span><span class="badge badge-primary no_radius" t-esc="filter_data['question_text']"></span><span class="badge badge-success only_right_radius" t-esc="' > '.join(filter_data['labels'])"></span>
</span>
</div>
</div>
<div t-if="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers']">
<h1 class="mt16">Results Overview</h1>
<div>Success rate: <mark class="font-weight-bold"><t t-esc="survey_dict['success_rate']"></t>%</mark></div>
<div class="survey_graph" data-graph-type="pie" t-att-data-graph-data="survey_dict['scoring_graph_data']">
<div>Success rate: <mark class="font-weight-bold"><t t-esc="survey_data['global_success_rate']"></t>%</mark></div>
<div class="survey_graph" data-graph-type="pie" t-att-data-graph-data="survey_data['global_success_graph']">
<!-- canvas element for drawing pie chart -->
<canvas/>
</div>
<hr/>
</div>
<div t-if="answers" t-foreach="survey_dict['page_ids']" t-as='page_ids'>
<t t-set="page" t-value="page_ids['page']"/>
<h1 class="mt16" t-field='page.title'></h1>
<div t-field="page.description" class="oe_no_empty" />
<hr/>
<div t-foreach="page_ids['question_ids']" t-as='question_ids' class="mt16">
<t t-set="input_summary" t-value="question_ids['input_summary']"/>
<t t-set="question" t-value="question_ids['question']"/>
<t t-set="graph_data" t-value="question_ids['graph_data']"/>
<t t-set="prepare_result" t-value="question_ids['prepare_result']"/>
<t t-set="question_with_correct_answers" t-value="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers'] and question.question_type in ['simple_choice', 'multiple_choice']" />
<h4>
<b>Question </b>
<span t-field='question.title'></span>
<t t-if="question.question_type == 'matrix'">
<small><span class="badge badge-secondary">Matrix: <span t-field='question.matrix_subtype'/></span></small>
</t>
<t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
<small><span t-field='question.question_type' class="badge badge-secondary"></span></small>
</t>
<span class="float-right">
<span class="badge badge-success"><span t-esc="input_summary['answered']"></span> Answered</span>
<span class="badge badge-warning"><span t-esc="input_summary['skipped']"></span> Skipped</span>
</span>
</h4>
<t t-if="question_with_correct_answers">
<t t-set="correct_answers" t-value="question.get_correct_answer_ids()" />
<div t-if="correct_answers">
<t t-if="question.question_type == 'simple_choice'">
<p><span>Correct answer</span>: <span class="font-weight-bold" t-esc="correct_answers[0].value"></span></p>
</t>
<t t-if="question.question_type == 'multiple_choice'">
<span>Correct answers</span>:
<ul>
<li t-foreach="correct_answers" t-as="correct_answer">
<t t-esc="correct_answer.value" />
</li>
</ul>
</t>
<div t-foreach="question_and_page_data" t-as='question_data'>
<t t-set="question" t-value="question_data['question']"/>
<t t-if="question_data['is_page']">
<h2 class="mt16 text-uppercase text-muted" t-field="question.title"/>
<div t-field="question.description" class="oe_no_empty" />
<hr class="mt-2 pb-1 mb-1"/>
</t>
<div t-else="" class="ml-4 mt-4">
<t t-set="question_scoring" t-value="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers'] and question.question_type in ['simple_choice', 'multiple_choice']"/>
<t t-set="comment_lines" t-value="question_data['comment_line_ids']"/>
<t t-set="graph_data" t-value="question_data['graph_data']"/>
<t t-set="table_data" t-value="question_data['table_data']"/>
<div class="d-flex align-items-start mb-2">
<div class="mr-auto">
<h5 t-field="question.title" class="mb-1"/>
<div t-if="question_scoring">
<t t-if="len(question_data['right_answers']) >= 1">
<span t-if="len(question_data['right_answers']) == 1">Right answer: </span>
<span t-if="len(question_data['right_answers']) > 1">Right answers: </span>
<strong>
<t t-foreach="question_data['right_answers']" t-as="answer">
<span t-if="answer_index != 0">, </span><span t-esc="answer.value"/>
</t>
</strong>
</t>
</div>
<div class="ml-3 text-muted" t-field="question.description"/>
</div>
</t>
<t t-if="input_summary['answered'] != 0">
<t t-if="question.description">
<div class="text-muted oe_no_empty" t-field="question.description" />
</t>
<t t-if="question.question_type in ['char_box', 'text_box', 'date', 'datetime']">
<t t-call="survey.question_result_text"></t>
</t>
<t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
<t t-call="survey.question_result_choice"></t>
</t>
<!-- Question info -->
<span class="badge badge-info" t-field='question.question_type'/>
<t t-if="question.question_type == 'matrix'">
<t t-call="survey.question_result_matrix"></t>
<span class="badge badge-info ml-2" t-field='question.matrix_subtype'/>
</t>
<t t-if="question.question_type == 'numerical_box'">
<t t-call="survey.question_result_number"></t>
<!-- Scoring info -->
<t t-if="question_scoring">
<span class="badge badge-success ml-3"><span t-esc="len(question_data['right_inputs'])"></span> Correct</span>
<span class="badge badge-warning ml-1" t-if="question.question_type == 'multiple_choice'">
<span t-esc="len(question_data['partial_inputs'])"></span> Partial
</span>
</t>
<!-- Inputs info -->
<span class="badge badge-info ml-3"><span t-esc="len(question_data['answer_input_done_ids'])"></span> Answered</span>
<span class="badge badge-info ml-1"><span t-esc="len(question_data['answer_input_skipped_ids'])"></span> Skipped</span>
</div>
<t t-if="question.question_type in ['text_box', 'char_box', 'date', 'datetime']">
<t t-call="survey.question_result_text"/>
</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 t-elif="question.question_type in ['numerical_box']">
<t t-call="survey.question_result_number"></t>
</t>
<t t-elif="question.question_type in ['simple_choice', 'multiple_choice']">
<t t-call="survey.question_result_choice"></t>
</t>
<t t-elif="question.question_type in ['matrix']">
<t t-call="survey.question_result_matrix"></t>
</t>
</div>
</div>
</div>
<div class="oe_structure" id="oe_structure_survey_result_2"/>
</t>
</template>
<!-- Result for text_box,char_box and date -->
<template id="question_result_text" name="Question: text result">
<table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
<template id="question_result_text" name="Question: text result (text_box, char_box, date, datetime)">
<table class="table table-hover table-sm" t-att-id="'survey_table_question_%d' % question.id">
<thead>
<tr>
<th>#</th>
@@ -115,222 +110,74 @@
</tr>
</thead>
<tbody>
<t t-set="text_result" t-value="prepare_result"/>
<tr t-foreach="text_result" t-as="user_input">
<td><a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)"><t t-esc="user_input_index + 1"></t></a></td>
<t t-if="question.question_type == 'text_box'">
<t t-foreach="table_data" t-as="input_line">
<tr t-att-class="'d-none' if input_line_index >= page_record_limit else ''">
<td>
<a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)" t-field="user_input.value_text_box"></a><br/>
<a t-att-href="input_line.user_input_id.get_print_url()"><t t-esc="input_line_index + 1"></t></a>
</td>
</t>
<t t-if="question.question_type == 'char_box'">
<td>
<a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)" t-field="user_input.value_char_box"></a><br/>
<t t-if="question.question_type == 'text_box'">
<a t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_text_box"></a>
</t>
<t t-if="question.question_type == 'char_box'">
<a t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_char_box"></a>
</t>
<t t-if="question.question_type == 'date'">
<a t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_date"></a>
</t>
<t t-if="question.question_type == 'datetime'">
<a t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_datetime"></a>
</t>
</td>
</t>
<t t-if="question.question_type == 'date'">
<td>
<a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)" t-field="user_input.value_date"></a><br/>
</td>
</t>
<t t-if="question.question_type == 'datetime'">
<td>
<a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)" t-field="user_input.value_datetime"></a><br/>
</td>
</t>
</tr>
</tbody>
</table>
<t t-call="survey.question_pagination"/>
</template>
<template id="question_result_comments" name="Question: text tesult">
<!-- a 'comments' variable must be set an must contain a list of browse records of user input lines -->
<table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
<thead>
<tr>
<th>#</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
<tr t-foreach="comments" t-as="user_input">
<td><a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)"><t t-esc="user_input_index + 1"></t></a></td>
<td>
<span t-field="user_input.value_char_box"></span><br/>
</td>
</tr>
</tbody>
</table>
</template>
<template id="question_result_choice" name="Question: choice result">
<div>
<!-- Tabs -->
<ul class="nav nav-tabs d-print-none" role="tablist">
<li class="nav-item" t-if="question.question_type != 'simple_choice'">
<a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-bar-chart-o"></i> Graph
</a>
</li>
<li class="nav-item" t-if="question.question_type == 'simple_choice'">
<a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-bar-chart-o"></i> Pie Chart
</a>
</li>
<li class="nav-item">
<a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i> Data
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type != 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph-type="bar" t-att-data-graph-data="graph_data">
<!-- canvas element for drawing bar chart -->
<canvas/>
</div>
<div role="tabpanel" class="tab-pane active survey_graph" t-if="question.question_type == 'simple_choice'" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id="question.id" data-graph-type="pie" t-att-data-graph-data="graph_data">
<!-- canvas element for drawing pie chart -->
<canvas/>
</div>
<div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Answer Choices</th>
<th>User Responses</th>
<th t-if="question_with_correct_answers">Answer Score</th>
</tr>
</thead>
<tbody>
<tr t-foreach="prepare_result['answers']" 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="badge badge-primary">Vote</span>
<i class="fa fa-filter text-primary survey_filter" t-att-data-question_id="question.id" t-att-data-answer_id="user_input['answer_id']" role="img" aria-label="Filter question" title="Filter question"/>
</td>
<td t-if="question_with_correct_answers" t-esc="user_input['answer_score']">
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- handle comments -->
<div>
<t t-set="comments" t-value="prepare_result['comments']" />
<t t-if="comments">
<t t-call="survey.question_result_comments" />
</tr>
</t>
</div>
</div>
</tbody>
</table>
<t t-call="survey.question_table_pagination"/>
</template>
<template id="question_result_matrix" name="Question: matrix result">
<t t-set="matrix_result" t-value="prepare_result"/>
<!-- Tabs -->
<template id="question_result_number" name="Question: number result (numerical_box)">
<span class="float-right mt8">
<span class="badge badge-secondary only_left_radius">Maximum </span> <span class="badge badge-success only_right_radius" t-esc="question_data['numerical_max']"></span>
<span class="badge badge-secondary only_left_radius">Minimum </span> <span class="badge badge-danger only_right_radius" t-esc="question_data['numerical_min']"></span>
<span class="badge badge-secondary only_left_radius">Average </span> <span class="badge badge-warning only_right_radius" t-esc="question_data['numerical_average']"></span>
</span>
<ul class="nav nav-tabs d-print-none" role="tablist">
<li class="nav-item">
<a t-att-href="'#graph_question_%d' % question.id" t-att-aria-controls="'graph_question_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-bar-chart"></i>
Graph
<a t-att-href="'#survey_stats_question_%d' % question.id"
t-att-aria-controls="'survey_stats_question_%d' % question.id"
class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-list-ol"></i> Most Common
</a>
</li>
<li class="nav-item">
<a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i>
Data
<a t-att-href="'#survey_data_question_%d' % question.id"
t-att-aria-controls="'survey_data_question_%d' % question.id"
class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i> All Data
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions survey_graph" t-att-id="'graph_question_%d' % question.id" t-att-data-question_id= "question.id" data-graph-type= "multi_bar" t-att-data-graph-data="graph_data">
<!-- canvas element for drawing Multibar chart -->
<canvas/>
</div>
<div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
<table class="table table-hover table-sm text-right">
<div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions" t-att-id="'survey_stats_question_%d' % question.id">
<table class="table table-hover table-sm">
<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>
<th>User Responses</th>
<th>Occurence</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="badge badge-primary" t-esc="matrix_result['result'][(row_id,answer_id)]"></span><i class="fa fa-filter text-primary survey_filter" t-att-data-question_id="question.id" t-att-data-row_id="row_id" t-att-data-answer_id="answer_id" role="img" aria-label="Survey filter" title="Survey filter"></i>
</td>
<tr t-foreach="question_data['numerical_common_lines']" t-as="common_line">
<td><span t-esc="common_line[0]"></span></td>
<td><span t-esc="common_line[1]"></span></td>
</tr>
</tbody>
</table>
</div>
<!-- handle comments to matrix -->
<div>
<t t-set="comments" t-value="matrix_result['comments']" />
<t t-if="comments">
<t t-call="survey.question_result_comments" />
</t>
</div>
</div>
</template>
<template id="question_result_number" name="Question:; number result">
<t t-set="number_result" t-value="prepare_result"/>
<t t-set="text_result" t-value="number_result['input_lines']" />
<span class="float-right mt8">
<span class="badge badge-secondary only_left_radius">Sum </span> <span class="badge badge-info only_right_radius" t-esc="number_result['sum']"></span>
<span class="badge badge-secondary only_left_radius">Maximum </span> <span class="badge badge-success only_right_radius" t-esc="number_result['max']"></span>
<span class="badge badge-secondary only_left_radius">Minimum </span> <span class="badge badge-danger only_right_radius" t-esc="number_result['min']"></span>
<span class="badge badge-secondary only_left_radius">Average </span> <span class="badge badge-warning only_right_radius" t-esc="number_result['average']"></span>
</span>
<ul class="nav nav-tabs d-print-none" role="tablist">
<li class="nav-item">
<a t-att-href="'#most_common_%d' % question.id" t-att-aria-controls="'most_common_%d' % question.id" class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-list-ol"></i>
Most Common <span t-esc="len(number_result['most_common'])"></span>
</a>
</li>
<li class="nav-item">
<a t-att-href="'#data_question_%d' % question.id" t-att-aria-controls="'data_question_%d' % question.id" class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i>
All Data
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions" t-att-id="'most_common_%d' % question.id">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>User Responses</th>
<th>Occurence</th>
</tr>
</thead>
<tbody>
<tr t-foreach="number_result['most_common']" t-as="row">
<td>
<span t-esc="row[0]"></span>
</td>
<td>
<span t-esc="row[1]"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" t-att-id="'data_question_%d' % question.id">
<table class="table table-hover table-sm" t-att-id="'table_question_%d' % question.id">
<div role="tabpanel" class="tab-pane" t-att-id="'survey_data_question_%d' % question.id">
<table class="table table-hover table-sm" t-att-id="'survey_table_question_%d' % question.id">
<thead>
<tr>
<th>#</th>
@@ -338,22 +185,164 @@
</tr>
</thead>
<tbody>
<tr class="d-none" t-foreach="number_result['input_lines']" t-as="user_input">
<td><a t-att-href="'/survey/print/%s?answer_token=%s' % (user_input.survey_id.access_token, user_input.user_input_id.access_token)"><t t-esc="user_input_index + 1"></t></a></td>
<td><span t-field="user_input.value_numerical_box"></span><br/></td>
</tr>
<t t-foreach="table_data" t-as="input_line">
<tr t-att-class="'d-none' if input_line_index >= page_record_limit else ''">
<td>
<a t-att-href="input_line.user_input_id.get_print_url()"><t t-esc="input_line_index + 1"></t></a>
</td>
<td><span t-field="input_line.value_numerical_box"></span><br/></td>
</tr>
</t>
</tbody>
</table>
<t t-call="survey.question_pagination"/>
<t t-call="survey.question_table_pagination"/>
</div>
</div>
</template>
<template id="question_pagination" name="Survey: statistics pages pagination">
<t t-set="record_limit" t-value="10"/><!-- 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" class="page-item">
<template id="question_result_choice" name="Question: choice result (simple_choice, multiple_choice)">
<ul class="nav nav-tabs d-print-none" role="tablist">
<li class="nav-item">
<a t-att-href="'#survey_graph_question_%d' % question.id"
t-att-aria-controls="'survey_graph_question_%d' % question.id"
class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-bar-chart-o"></i>
<span t-if="question.question_type == 'simple_choice'">Pie Graph</span>
<span t-else="">Graph</span>
</a>
</li>
<li class="nav-item">
<a t-att-href="'#survey_data_question_%d' % question.id"
t-att-aria-controls="'survey_data_question_%d' % question.id"
class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i> Data
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active survey_graph"
t-att-id="'survey_graph_question_%d' % question.id"
t-att-data-question_id="question.id"
t-att-data-graph-type="'pie' if question.question_type == 'simple_choice' else 'bar'"
t-att-data-graph-data="graph_data">
<!-- canvas element for drawing bar chart -->
<canvas/>
</div>
<div role="tabpanel" class="tab-pane" t-att-id="'survey_data_question_%d' % question.id">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Answer</th>
<th>User Choice</th>
<th t-if="question_scoring">Score</th>
</tr>
</thead>
<tbody>
<tr t-foreach="table_data" t-as="choice_data">
<td>
<p t-esc="choice_data['value']"></p>
</td>
<td class="survey_answer">
<span t-esc="round(choice_data['count'] * 100.0/ (len(question_data['answer_line_done_ids']) or 1), 2)"></span> %
<span t-esc="'%s Votes' % choice_data['count']" class="badge badge-primary"/>
<i class="fa fa-filter text-primary survey_filter"
t-att-data-question_id="question.id"
t-att-data-answer_id="choice_data['suggested_answer'].id"
role="img" aria-label="Filter question" title="Filter question"/>
</td>
<td t-if="question_scoring" t-esc="choice_data['suggested_answer'].answer_score"/>
</tr>
</tbody>
</table>
</div>
</div>
<div t-if="comment_lines" t-call="survey.question_result_comments" />
</template>
<template id="question_result_matrix" name="Question: matrix result (matrix)">
<t t-set="graph_data" t-value="question_data['graph_data']"/>
<t t-set="table_data" t-value="question_data['table_data']"/>
<ul class="nav nav-tabs d-print-none" role="tablist">
<li class="nav-item">
<a t-att-href="'#survey_graph_question_%d' % question.id"
t-att-aria-controls="'survey_graph_question_%d' % question.id"
class="nav-link active" data-toggle="tab" role="tab">
<i class="fa fa-bar-chart"></i> Graph
</a>
</li>
<li class="nav-item">
<a t-att-href="'#survey_data_question_%d' % question.id"
t-att-aria-controls="'survey_data_question_%d' % question.id"
class="nav-link" data-toggle="tab" role="tab">
<i class="fa fa-list-alt"></i> Data
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active with-3d-shadow with-transitions survey_graph"
t-att-id="'survey_graph_question_%d' % question.id"
t-att-data-question_id= "question.id"
data-graph-type= "multi_bar"
t-att-data-graph-data="graph_data">
<!-- canvas element for drawing Multibar chart -->
<canvas/>
</div>
<div role="tabpanel" class="tab-pane" t-att-id="'survey_data_question_%d' % question.id">
<table class="table table-hover table-sm text-right">
<thead>
<tr>
<th></th>
<th class="text-right" t-foreach="table_data[0]['columns']" t-as="column_data">
<span t-esc="column_data['suggested_answer'].value"></span>
</th>
</tr>
</thead>
<tbody>
<tr t-foreach="table_data" t-as="choice_data">
<td>
<span t-esc="choice_data['row'].value"></span>
</td>
<td class="survey_answer" t-foreach="choice_data['columns']" t-as="column_data">
<span t-esc="round(column_data['count'] * 100.0/ (len(question_data['answer_line_done_ids']) or 1), 2)"></span> %
<span class="badge badge-primary" t-esc="column_data['count']"></span>
<i class="fa fa-filter text-primary survey_filter"
t-att-data-question_id="question.id"
t-att-data-row_id="choice_data['row'].id"
t-att-data-answer_id="column_data['suggested_answer'].id" role="img" aria-label="Survey filter" title="Survey filter"></i>
</td>
</tr>
</tbody>
</table>
</div>
<div t-if="comment_lines" t-call="survey.question_result_comments" />
</div>
</template>
<template id="question_result_comments" name="Question: comments">
<table class="table table-hover table-sm" t-att-id="'survey_table_question_%d' % question.id">
<thead>
<tr>
<th>#</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
<tr t-foreach="comment_lines" t-as="input_line">
<td><a t-att-href="input_line.user_input_id.get_print_url()"><t t-esc="input_line_index + 1"></t></a></td>
<td>
<span t-field="input_line.value_char_box"></span><br/>
</td>
</tr>
</tbody>
</table>
</template>
<template id="question_table_pagination" name="Survey: statistics table pagination">
<ul t-att-id="'pagination_%d' % question.id" class="pagination" t-att-data-question_id="question.id" t-att-data-record_limit="page_record_limit">
<t t-if="len(table_data) > page_record_limit">
<t t-set="total" t-value="ceil(len(table_data) / page_record_limit) + 1"/>
<li t-foreach="range(1, total)" t-as="num"
t-att-class="'page-item o_survey_js_results_pagination %s' % ('active' if num == 1 else '')">
<a href="#" class="page-link" t-esc="num"></a>
</li>
</t>