Skip to content
Permalink
Browse files

[WIP][REF] survey: refactor results page

So much impressive retro engineering.

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

  * graph_data: holding all necessary but minimalist data for graph display
    if question allows it;
  * table_data: holdign all necessary but minimalist data for table / summary
    display;
  • Loading branch information
tde-banana-odoo committed Nov 27, 2019
1 parent d923a76 commit df995c0b57a5beda570e65158676c435398ab21d
@@ -12,6 +12,7 @@
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.osv import expression
from odoo.tools import ustr, format_datetime, format_date

_logger = logging.getLogger(__name__)
@@ -472,6 +473,32 @@ def survey_get_certification(self, survey_id, **kwargs):
def survey_report(self, survey, answer_token=None, **post):
""" Display survey Results & Statistics for given survey.
New structure: {
'search': {},
'statistics': [{ # page list
'page_id': survey.question br (may be void),
'questions': [{ # page question list
'question_id': survey.question br (required),
'line_ids': survey.user_input.line that are answers to that specific question with filter applied,
'comment_line_ids': survey.user_input.line that are comments not counting as answers,
'statistics': { # question type dependent
'answered_count': 0, # all
'skipped_count': 0, # all
'average': 0, # numeric
'min': 0, # numeric
'max': 0, # numeric
'sum': 0, # numeric
},
'graph_data': {
},
}, {...}
],
}, {...}
]
}
Graph data: template will call _get_graph_data_<type>(line_ids)
Table data: template will call _get_table_data_<type>(line_ids)
Quick retroengineering of what is injected into the template for now:
(TODO: flatten and simplify this)
@@ -508,6 +535,13 @@ def survey_report(self, survey, answer_token=None, **post):
filter_display_data: [{'labels': ['a', 'b'], question_text} ... ]
filter_finish: boolean => only finished surveys or not
"""
user_input_lines, search_filters = self._extract_filters_data(survey, post)
print(user_input_lines)
print(search_filters)
question_and_page_data = survey.question_and_page_ids._prepare_statistics(user_input_lines)
for a in question_and_page_data:
print(a)

current_filters = []
filter_display_data = []

@@ -519,13 +553,59 @@ def survey_report(self, survey, answer_token=None, **post):
filter_display_data = survey.get_filter_display_data(filter_data)
return request.render('survey.survey_page_statistics', {
'survey': survey,
'question_and_page_data': question_and_page_data,
'answers': answers,
'survey_dict': self._prepare_result_dict(survey, current_filters),
'current_filters': current_filters,
'filter_display_data': filter_display_data,
'filter_finish': filter_finish
})

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 = data.split(',')
row_id = int(row_id)
answer_id = int(answer_id)
except:
pass
else:
if row_id and answer_id:
new_domain = ['&', ('matrix_row_id', '=', row_id), ('suggested_answer_id', '=', answer_id)]
line_filter_domain = expression.AND([new_domain, 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)

if line_filter_domain:
# here we go from input lines to user input to avoid a domain like
# ('user_input.survey_id', '=', survey.id) which would return a lot of data
# on huge surveys, especially with test entries and draft entries in mind
matching_user_inputs = request.env['survey.user_input.line'].sudo().search(line_filter_domain).mapped('user_input_id')
if post.get('finished'):
user_input_lines = matching_user_inputs.filtered(lambda ui: not ui.test_entry and ui.state == 'done').mapped('user_input_line_ids')
else:
user_input_lines = matching_user_inputs.filtered(lambda ui: not ui.test_entry and ui.state != 'new').mapped('user_input_line_ids')
else:
user_input_domain = ['&', ('test_entry', '=', False), ('survey_id', '=', survey.id)]
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')

return user_input_lines, search_filters

def _parse_post_filters(self, post):
"""Returns data used for filtering the result"""
filters = []
@@ -563,6 +643,10 @@ def _prepare_result_dict(self, survey, current_filters=None):

def _prepare_question_values(self, question, current_filters):
Survey = request.env['survey.survey']
print('-------------------')
print(question.title)
print(self._get_graph_data(question, current_filters))
print('-------------------')
return {
'question': question,
'input_summary': Survey.get_input_summary(question, current_filters),
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import collections
import json
import itertools

from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError

@@ -278,6 +282,132 @@ def get_correct_answer_ids(self):

return self.suggested_answer_ids.filtered(lambda label: label.is_correct)

# STATISTICS

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_line_ids = user_input_lines.filtered(lambda line: line.question_id == question)
if question.question_type in ['simple_choice', 'multiple_choice']:
answer_line_ids = all_line_ids.filtered(lambda line: line.answer_type != 'text')
elif question.question_type == 'matrix':
answer_line_ids = all_line_ids.filtered(lambda line: line.answer_type != 'text')
else:
answer_line_ids = all_line_ids
comment_line_ids = all_line_ids - answer_line_ids
question_data.update(
all_line_ids=all_line_ids,
answer_line_ids=answer_line_ids,
answer_line_done_ids=answer_line_ids.filtered(lambda line: not line.skipped),
answer_line_skipped_ids=answer_line_ids.filtered(lambda line: line.skipped),
comment_line_ids=comment_line_ids)

# prepare table and graph data
question_data['graph_data'] = json.dumps(question._get_stats_graph_data(answer_line_ids))
question_data['table_data'] = question._get_stats_table_data(answer_line_ids)

all_questions_data.append(question_data)
return all_questions_data

def _get_stats_graph_data(self, user_input_lines):
if self.question_type in ['simple_choice']:
return self._get_stats_graph_data_pie(user_input_lines)
elif self.question_type in ['multiple_choice']:
return self._get_stats_graph_data_bar(user_input_lines)
return ''

def _get_stats_graph_data_pie(self, user_input_lines):
""" retro compat: [{"text": "Once a day", "count": 0, "answer_id": 1, "answer_score": 0.0}, ...] """
suggested_answers = self.mapped('suggested_answer_ids')

count_data = dict.fromkeys(suggested_answers, 0)
for line in user_input_lines:
if line.suggested_answer_id:
count_data[line.suggested_answer_id] += 1

return [{
'text': sug_answer.value,
'count': count_data[sug_answer]
}
for sug_answer in suggested_answers
]

def _get_stats_graph_data_bar(self, user_input_lines):
""" retro compat: [{"key": "which blah", "values": [{"text": "High quality", "count": 1, "answer_id": 5, "answer_score": 0.0}, ... }] """
suggested_answers = self.mapped('suggested_answer_ids')

count_data = dict.fromkeys(suggested_answers, 0)
for line in user_input_lines:
if line.suggested_answer_id:
count_data[line.suggested_answer_id] += 1

return [{
'key': self.title,
'values': [{
'text': sug_answer.value,
'count': count_data[sug_answer]
}
for sug_answer in suggested_answers
]
}]

def _get_stats_table_data(self, user_input_lines):
if self.question_type in ['simple_choice', 'multiple_choice']:
return self._get_stats_table_data_choice(user_input_lines)
elif self.question_type in ['matrix']:
return self._get_stats_table_data_matrix(user_input_lines)
elif self.question_type in ['free_text', 'textbox', 'date', 'datetime', 'numerical_box']:
return self._get_stats_table_data_linear(user_input_lines)
else:
raise ValueError(_('Unexpected %s question' % self.question_type))

def _get_stats_table_data_choice(self, user_input_lines):
""" Table data for simple choice and multiple choice """
suggested_answers = self.mapped('suggested_answer_ids')

count_data = dict.fromkeys(suggested_answers, 0)
for line in user_input_lines:
if line.suggested_answer_id:
count_data[line.suggested_answer_id] += 1

return [{
'suggested_answer': sug_answer,
'count': count_data[sug_answer]
}
for sug_answer in suggested_answers
]

def _get_stats_table_data_matrix(self, user_input_lines):
""" Table data for matrix """
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

return [{
'row': row,
'suggested_answer': sug_answer,
'count': count_data[(row, sug_answer)]
}
for row in matrix_rows
for sug_answer in suggested_answers
]

def _get_stats_table_data_linear(self, user_input_lines):
""" Table data for free_text, text_box, date, datetime, numerical_box """
return [line for line in user_input_lines]


class SurveyQuestionAnswer(models.Model):
""" A preconfigured answer for a question. This model stores values used
@@ -544,6 +544,40 @@ def get_print_url(self):
# GRAPH / RESULTS
# ------------------------------------------------------------

def _prepare_global_results(self, user_input_domain=None):
""" Extract global statistics on the survey user inputs, with an
optional domain to fetch user inputs. """
user_input_domain = user_input_domain if user_input_domain else [
('state', '=', 'done'),
('test_entry', '=', False)
]
user_input_domain = expression.AND([[('survey_id', 'in', self.ids)], user_input_domain])
count_data = self.env['survey.user_input'].sudo().read_group(user_input_domain, ['quizz_passed', 'id:count_distinct'], ['quizz_passed'])

quizz_passed_count = 0
quizz_failed_count = 0
for count_data_item in count_data:
if count_data_item['quizz_passed']:
quizz_passed_count = count_data_item['quizz_passed_count']
else:
quizz_failed_count = count_data_item['quizz_passed_count']

graph_data = [{
'text': _('Passed'),
'count': quizz_passed_count,
'color': '#2E7D32'
}, {
'text': _('Missed'),
'count': quizz_failed_count,
'color': '#C62828'
}]

total_quizz_passed = quizz_passed_count + quizz_failed_count
return {
'global_success_rate': round((quizz_passed_count / total_quizz_passed) * 100, 1) if total_quizz_passed > 0 else 0,
'global_graph_data': graph_data
}

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.

0 comments on commit df995c0

Please sign in to comment.
You can’t perform that action at this time.