[IMP] survey: add every question type filters
Purpose ======= Improve the results page filter to allow the filtering on questions of type text_box, char_box, numerical_box, date and datetime. Specifications ============== Updating the URL filters representation to avoid passing the question types directly in the URL parameters. Answer matching per question type: - char_box, text_box: 'ilike' - numerical_box, date, datetime: '=' Adding a new filter restricts the current filter results. Handling the filters depending on the 2 different answer models: 'survey.question.answer': matrix, simple_choice, multiple_choice 'survey.user_input.line': char_box, text_box, numerical_box, date, datetime. The filters can be combined but their query count doesn't add up if their related answers data are stored in the same model. Task-3138245 closes odoo/odoo#110252 Related: odoo/enterprise#36383 Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import json
|
||||
import logging
|
||||
import werkzeug
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
@@ -712,14 +713,8 @@ class Survey(http.Controller):
|
||||
('Content-Disposition', report_content_disposition),
|
||||
])
|
||||
|
||||
def _get_user_input_domain(self, survey, line_filter_domain, **post):
|
||||
def _get_results_page_user_input_domain(self, survey, **post):
|
||||
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:
|
||||
@@ -732,35 +727,118 @@ class Survey(http.Controller):
|
||||
return user_input_domain
|
||||
|
||||
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:
|
||||
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:
|
||||
question_id = answers[0].matrix_question_id or answers[0].question_id
|
||||
search_filters.append({
|
||||
'row_id': row_id,
|
||||
'answer_id': answer_id,
|
||||
'question': 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])
|
||||
""" Extracts the filters from the URL to returns the related user_input_lines and
|
||||
the parameters used to render/remove the filters on the results page (search_filters).
|
||||
|
||||
user_input_domain = self._get_user_input_domain(survey, line_filter_domain, **post)
|
||||
user_input_lines = request.env['survey.user_input'].sudo().search(user_input_domain).mapped('user_input_line_ids')
|
||||
The matching user_input_lines are all the lines tied to the user inputs which respect
|
||||
the survey base domain and which have lines matching all the filters.
|
||||
For example, with the filter 'Where do you live?|Brussels', we need to display ALL the lines
|
||||
of the survey user inputs which have answered 'Brussels' to this question.
|
||||
|
||||
:return (recordset, List[dict]): all matching user input lines, each search filter data
|
||||
"""
|
||||
user_input_line_subdomains = []
|
||||
search_filters = []
|
||||
|
||||
answer_by_column, user_input_lines_ids = self._get_filters_from_post(post)
|
||||
|
||||
# Matrix, Multiple choice, Simple choice filters
|
||||
if answer_by_column:
|
||||
answer_ids, row_ids = [], []
|
||||
for answer_column_id, answer_row_ids in answer_by_column.items():
|
||||
answer_ids.append(answer_column_id)
|
||||
row_ids += answer_row_ids
|
||||
|
||||
answers_and_rows = request.env['survey.question.answer'].browse(answer_ids+row_ids)
|
||||
# For performance, accessing 'a.matrix_question_id' caches all useful fields of the
|
||||
# answers and rows records, avoiding unnecessary queries.
|
||||
answers = answers_and_rows.filtered(lambda a: not a.matrix_question_id)
|
||||
|
||||
for answer in answers:
|
||||
if not answer_by_column[answer.id]:
|
||||
# Simple/Multiple choice
|
||||
user_input_line_subdomains.append(answer._get_answer_matching_domain())
|
||||
search_filters.append(self._prepare_search_filter_answer(answer))
|
||||
else:
|
||||
# Matrix
|
||||
for row_id in answer_by_column[answer.id]:
|
||||
row = answers_and_rows.filtered(lambda answer_or_row: answer_or_row.id == row_id)
|
||||
user_input_line_subdomains.append(answer._get_answer_matching_domain(row_id))
|
||||
search_filters.append(self._prepare_search_filter_answer(answer, row))
|
||||
|
||||
# Char_box, Text_box, Numerical_box, Date, Datetime filters
|
||||
if user_input_lines_ids:
|
||||
user_input_lines = request.env['survey.user_input.line'].browse(user_input_lines_ids)
|
||||
for input_line in user_input_lines:
|
||||
user_input_line_subdomains.append(input_line._get_answer_matching_domain())
|
||||
search_filters.append(self._prepare_search_filter_input_line(input_line))
|
||||
|
||||
# Compute base domain
|
||||
user_input_domain = self._get_results_page_user_input_domain(survey, **post)
|
||||
|
||||
# Add filters domain to the base domain
|
||||
if user_input_line_subdomains:
|
||||
all_required_lines_domains = [
|
||||
[('user_input_line_ids', 'in', request.env['survey.user_input.line'].sudo()._search(subdomain))]
|
||||
for subdomain in user_input_line_subdomains
|
||||
]
|
||||
user_input_domain = expression.AND([user_input_domain, *all_required_lines_domains])
|
||||
|
||||
# Get the matching user input lines
|
||||
user_inputs_query = request.env['survey.user_input'].sudo()._search(user_input_domain)
|
||||
user_input_lines = request.env['survey.user_input.line'].search([('user_input_id', 'in', user_inputs_query)])
|
||||
|
||||
return user_input_lines, search_filters
|
||||
|
||||
def _get_filters_from_post(self, post):
|
||||
""" Extract the filters from post depending on the model that needs to be called to retrieve the filtered answer data.
|
||||
Simple choice and multiple choice question types are mapped onto empty row_id.
|
||||
Input/output example with respectively matrix, simple_choice and char_box filters:
|
||||
input: 'A,1,24|A,0,13|L,0,36'
|
||||
output:
|
||||
answer_by_column: {24: [1], 13: []}
|
||||
user_input_lines_ids: [36]
|
||||
|
||||
* Model short key = 'A' : Match a `survey.question.answer` record (simple_choice, multiple_choice, matrix)
|
||||
* Model short key = 'L' : Match a `survey.user_input.line` record (char_box, text_box, numerical_box, date, datetime)
|
||||
:rtype: (collections.defaultdict[int, list[int]], list[int])
|
||||
"""
|
||||
answer_by_column = defaultdict(list)
|
||||
user_input_lines_ids = []
|
||||
|
||||
for data in post.get('filters', '').split('|'):
|
||||
if not data:
|
||||
break
|
||||
model_short_key, row_id, answer_id = data.split(',')
|
||||
row_id, answer_id = int(row_id), int(answer_id)
|
||||
if model_short_key == 'A':
|
||||
if row_id:
|
||||
answer_by_column[answer_id].append(row_id)
|
||||
else:
|
||||
answer_by_column[answer_id] = []
|
||||
elif model_short_key == 'L' and not row_id:
|
||||
user_input_lines_ids.append(answer_id)
|
||||
|
||||
return answer_by_column, user_input_lines_ids
|
||||
|
||||
def _prepare_search_filter_answer(self, answer, row=False):
|
||||
""" Format parameters used to render/remove this filter on the results page."""
|
||||
return {
|
||||
'question_id': answer.question_id.id,
|
||||
'question': answer.question_id.title,
|
||||
'row_id': row.id if row else 0,
|
||||
'answer': '%s : %s' % (row.value, answer.value) if row else answer.value,
|
||||
'model_short_key': 'A',
|
||||
'record_id': answer.id,
|
||||
}
|
||||
|
||||
def _prepare_search_filter_input_line(self, user_input_line):
|
||||
""" Format parameters used to render/remove this filter on the results page."""
|
||||
return {
|
||||
'question_id': user_input_line.question_id.id,
|
||||
'question': user_input_line.question_id.title,
|
||||
'row_id': 0,
|
||||
'answer': user_input_line._get_answer_value(),
|
||||
'model_short_key': 'L',
|
||||
'record_id': user_input_line.id,
|
||||
}
|
||||
|
||||
@@ -707,6 +707,14 @@ class SurveyQuestionAnswer(models.Model):
|
||||
if not bool(label.question_id) != bool(label.matrix_question_id):
|
||||
raise ValidationError(_("A label must be attached to only one question."))
|
||||
|
||||
def _get_answer_matching_domain(self, row_id=False):
|
||||
self.ensure_one()
|
||||
if self.question_type == "matrix":
|
||||
return ['&', '&', ('question_id', '=', self.question_id.id), ('matrix_row_id', '=', row_id), ('suggested_answer_id', '=', self.id)]
|
||||
elif self.question_type in ('multiple_choice', 'simple_choice'):
|
||||
return ['&', ('question_id', '=', self.question_id.id), ('suggested_answer_id', '=', self.id)]
|
||||
return []
|
||||
|
||||
def unlink(self):
|
||||
""" Makes sure no question is left depending on the answer we're deleting."""
|
||||
depending_questions = self.env['survey.question'].search([('triggering_answer_id', 'in', self.ids)])
|
||||
|
||||
@@ -706,6 +706,27 @@ class SurveyUserInputLine(models.Model):
|
||||
res = super(SurveyUserInputLine, line).write(vals_copy) and res
|
||||
return res
|
||||
|
||||
def _get_answer_matching_domain(self):
|
||||
self.ensure_one()
|
||||
if self.answer_type in ('char_box', 'text_box', 'numerical_box', 'date', 'datetime'):
|
||||
value_field = {
|
||||
'char_box': 'value_char_box',
|
||||
'text_box': 'value_text_box',
|
||||
'numerical_box': 'value_numerical_box',
|
||||
'date': 'value_date',
|
||||
'datetime': 'value_datetime',
|
||||
}
|
||||
operators = {
|
||||
'char_box': 'ilike',
|
||||
'text_box': 'ilike',
|
||||
'numerical_box': '=',
|
||||
'date': '=',
|
||||
'datetime': '=',
|
||||
}
|
||||
return ['&', ('question_id', '=', self.question_id.id), (value_field[self.answer_type], operators[self.answer_type], self._get_answer_value())]
|
||||
elif self.answer_type == 'suggestion':
|
||||
return self.suggested_answer_id._get_answer_matching_domain(self.matrix_row_id.id if self.matrix_row_id else False)
|
||||
|
||||
@api.model
|
||||
def _get_answer_score_values(self, vals, compute_speed_score=True):
|
||||
""" Get values for: answer_is_correct and associated answer_score.
|
||||
@@ -782,3 +803,18 @@ class SurveyUserInputLine(models.Model):
|
||||
'answer_is_correct': answer_is_correct,
|
||||
'answer_score': answer_score
|
||||
}
|
||||
|
||||
def _get_answer_value(self):
|
||||
self.ensure_one()
|
||||
if self.answer_type == 'char_box':
|
||||
return self.value_char_box
|
||||
elif self.answer_type == 'text_box':
|
||||
return self.value_text_box
|
||||
elif self.answer_type == 'numerical_box':
|
||||
return self.value_numerical_box
|
||||
elif self.answer_type == 'date':
|
||||
return self.value_date
|
||||
elif self.answer_type == 'datetime':
|
||||
return self.value_datetime
|
||||
elif self.answer_type == 'suggestion':
|
||||
return self.suggested_answer_id.value
|
||||
|
||||
@@ -410,7 +410,7 @@ publicWidget.registry.SurveyResultWidget = publicWidget.Widget.extend({
|
||||
selector: '.o_survey_result',
|
||||
events: {
|
||||
'click .o_survey_results_topbar_clear_filters': '_onClearFiltersClick',
|
||||
'click i.filter-add-answer': '_onFilterAddAnswerClick',
|
||||
'click .filter-add-answer': '_onFilterAddAnswerClick',
|
||||
'click i.filter-remove-answer': '_onFilterRemoveAnswerClick',
|
||||
'click a.filter-finished-or-not': '_onFilterFinishedOrNotClick',
|
||||
'click a.filter-finished': '_onFilterFinishedClick',
|
||||
@@ -575,24 +575,31 @@ publicWidget.registry.SurveyResultWidget = publicWidget.Widget.extend({
|
||||
|
||||
/**
|
||||
* Returns the modified pathname string for filters after adding or removing an
|
||||
* answer filter (from click event). Filters are formatted as `"rowX,ansX", where
|
||||
* the row is used for matrix-type questions and set to 0 otherwise.
|
||||
* answer filter (from click event).
|
||||
* @private
|
||||
* @param {String} filters Existing answer filters, formatted as `rowX,ansX|rowY,ansY...`.
|
||||
* @param {String} filters Existing answer filters, formatted as
|
||||
* `modelX,rowX,ansX|modelY,rowY,ansY...` - row is used for matrix-type questions row id, 0 for others
|
||||
* "model" specifying the model to query depending on the question type we filter on.
|
||||
- 'A': 'survey.question.answer' ids: simple_choice, multiple_choice, matrix
|
||||
- 'L': 'survey.user_input.line' ids: char_box, text_box, numerical_box, date, datetime
|
||||
* @param {"add" | "remove"} operation Whether to add or remove the filter.
|
||||
* @param {Event} ev Event defining the filter.
|
||||
* @returns {String} Updated filters.
|
||||
*/
|
||||
_prepareAnswersFilters(filters, operation, ev) {
|
||||
const cell = $(ev.target);
|
||||
const eventFilter = `${cell.data('rowId') || 0},${cell.data('answerId')}`;
|
||||
const cellDataset = ev.currentTarget.dataset;
|
||||
const filter = `${cellDataset.modelShortKey},${cellDataset.rowId || 0},${cellDataset.recordId}`;
|
||||
|
||||
if (operation === 'add') {
|
||||
filters = filters ? filters + `|${eventFilter}` : eventFilter;
|
||||
if (filters) {
|
||||
filters = !filters.split("|").includes(filter) ? filters += `|${filter}` : filters;
|
||||
} else {
|
||||
filters = filter;
|
||||
}
|
||||
} else if (operation === 'remove') {
|
||||
filters = filters
|
||||
.split("|")
|
||||
.filter(filterItem => filterItem !== eventFilter)
|
||||
.filter(filterItem => filterItem !== filter)
|
||||
.join("|");
|
||||
} else {
|
||||
throw new Error('`operation` parameter for `_prepareAnswersFilters` must be either "add" or "remove".')
|
||||
|
||||
@@ -15,3 +15,5 @@ from . import test_survey_ui_feedback
|
||||
from . import test_survey_ui_session
|
||||
from . import test_survey_compute_pages_questions
|
||||
from . import test_certification_badge
|
||||
from . import test_survey_performance
|
||||
from . import test_survey_results
|
||||
|
||||
@@ -11,15 +11,17 @@ from odoo.tests import common
|
||||
|
||||
|
||||
class SurveyCase(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(SurveyCase, self).setUp()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(SurveyCase, cls).setUpClass()
|
||||
|
||||
""" Some custom stuff to make the matching between questions and answers
|
||||
:param dict _type_match: dict
|
||||
key: question type
|
||||
value: (answer type, answer field_name)
|
||||
"""
|
||||
self._type_match = {
|
||||
cls._type_match = {
|
||||
'text_box': ('text_box', 'value_text_box'),
|
||||
'char_box': ('char_box', 'value_char_box'),
|
||||
'numerical_box': ('numerical_box', 'value_numerical_box'),
|
||||
@@ -314,3 +316,76 @@ class TestSurveyCommon(SurveyCase):
|
||||
'sequence': 3,
|
||||
'question_type': 'numerical_box',
|
||||
})
|
||||
|
||||
|
||||
class TestSurveyResultsCommon(SurveyCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSurveyResultsCommon, cls).setUpClass()
|
||||
cls.survey_manager = mail_new_test_user(
|
||||
cls.env, name='Gustave Doré', login='survey_manager', email='survey.manager@example.com',
|
||||
groups='survey.group_survey_manager,base.group_user'
|
||||
)
|
||||
|
||||
# Create survey with questions
|
||||
cls.survey = cls.env['survey.survey'].create({
|
||||
'title': 'Test Survey Results',
|
||||
'questions_layout': 'one_page'
|
||||
})
|
||||
cls.question_char_box = cls._add_question(
|
||||
cls, None, 'What is your name', 'char_box', survey_id=cls.survey.id, sequence='1')
|
||||
cls.question_numerical_box = cls._add_question(
|
||||
cls, None, 'What is your age', 'numerical_box', survey_id=cls.survey.id, sequence='2')
|
||||
cls.question_sc = cls._add_question(
|
||||
cls, None, 'Are you a cat or a dog person', 'simple_choice', survey_id=cls.survey.id,
|
||||
sequence='3', labels=[{'value': 'Cat'},
|
||||
{'value': 'Dog'}])
|
||||
cls.question_mc = cls._add_question(
|
||||
cls, None, 'What do you like most in our tarte al djotte', 'multiple_choice', survey_id=cls.survey.id,
|
||||
sequence='4', labels=[{'value': 'The gras'},
|
||||
{'value': 'The bette'},
|
||||
{'value': 'The tout'},
|
||||
{'value': 'The regime is fucked up'}])
|
||||
cls.question_mx1 = cls._add_question(
|
||||
cls, None, 'When do you harvest those fruits', 'matrix', survey_id=cls.survey.id, sequence='5',
|
||||
labels=[{'value': 'Spring'}, {'value': 'Summer'}],
|
||||
labels_2=[{'value': 'Apples'},
|
||||
{'value': 'Strawberries'}])
|
||||
cls.question_mx2 = cls._add_question(
|
||||
cls, None, 'How often should you water those plants', 'matrix', survey_id=cls.survey.id, sequence='6',
|
||||
labels=[{'value': 'Once a month'}, {'value': 'Once a week'}],
|
||||
labels_2=[{'value': 'Cactus'},
|
||||
{'value': 'Ficus'}])
|
||||
|
||||
# Question answers ids
|
||||
[cls.cat_id, cls.dog_id] = cls.question_sc.suggested_answer_ids.ids
|
||||
[cls.gras_id, cls.bette_id, _, _] = cls.question_mc.suggested_answer_ids.ids
|
||||
[cls.apples_row_id, cls.strawberries_row_id] = cls.question_mx1.matrix_row_ids.ids
|
||||
[cls.spring_id, cls.summer_id] = cls.question_mx1.suggested_answer_ids.ids
|
||||
[cls.cactus_row_id, cls.ficus_row_id] = cls.question_mx2.matrix_row_ids.ids
|
||||
[cls.once_a_month_id, cls.once_a_week_id] = cls.question_mx2.suggested_answer_ids.ids
|
||||
|
||||
# Populate survey with answers
|
||||
cls.user_input_1 = cls._add_answer(cls, cls.survey, cls.survey_manager.partner_id)
|
||||
cls.answer_lukas = cls._add_answer_line(cls, cls.question_char_box, cls.user_input_1, 'Lukas')
|
||||
cls.answer_24 = cls._add_answer_line(cls, cls.question_numerical_box, cls.user_input_1, 24)
|
||||
cls.answer_cat = cls._add_answer_line(cls, cls.question_sc, cls.user_input_1, cls.cat_id)
|
||||
cls._add_answer_line(cls, cls.question_mc, cls.user_input_1, cls.gras_id)
|
||||
cls._add_answer_line(cls, cls.question_mx1, cls.user_input_1, cls.summer_id, **{'answer_value_row': cls.apples_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx1, cls.user_input_1, cls.spring_id, **{'answer_value_row': cls.strawberries_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx2, cls.user_input_1, cls.once_a_month_id, **{'answer_value_row': cls.cactus_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx2, cls.user_input_1, cls.once_a_week_id, **{'answer_value_row': cls.ficus_row_id})
|
||||
cls.user_input_1.state = 'done'
|
||||
|
||||
cls.user_input_2 = cls._add_answer(cls, cls.survey, cls.survey_manager.partner_id)
|
||||
cls.answer_pauline = cls._add_answer_line(cls, cls.question_char_box, cls.user_input_2, 'Pauline')
|
||||
cls._add_answer_line(cls, cls.question_numerical_box, cls.user_input_2, 24)
|
||||
cls.answer_dog = cls._add_answer_line(cls, cls.question_sc, cls.user_input_2, cls.dog_id)
|
||||
cls._add_answer_line(cls, cls.question_mc, cls.user_input_2, cls.gras_id)
|
||||
cls._add_answer_line(cls, cls.question_mc, cls.user_input_2, cls.bette_id)
|
||||
cls._add_answer_line(cls, cls.question_mx1, cls.user_input_2, cls.spring_id, **{'answer_value_row': cls.apples_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx1, cls.user_input_2, cls.spring_id, **{'answer_value_row': cls.strawberries_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx2, cls.user_input_2, cls.once_a_month_id, **{'answer_value_row': cls.cactus_row_id})
|
||||
cls._add_answer_line(cls, cls.question_mx2, cls.user_input_2, cls.once_a_month_id, **{'answer_value_row': cls.ficus_row_id})
|
||||
cls.user_input_2.state = 'done'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.survey.tests import common
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import warmup, HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class SurveyPerformance(common.TestSurveyResultsCommon, HttpCase):
|
||||
|
||||
@warmup
|
||||
def test_survey_results_with_multiple_filters_mixed_model(self):
|
||||
""" Check that, in comparison with having filters from the same model,
|
||||
having filters from different models needs only a few more queries.
|
||||
"""
|
||||
url = f'/survey/results/{self.survey.id}?filters=A,0,{self.gras_id}|L,0,{self.answer_pauline.id}'
|
||||
self.authenticate('survey_manager', 'survey_manager')
|
||||
with self.assertQueryCount(default=34): # com 34
|
||||
self.url_open(url)
|
||||
|
||||
@warmup
|
||||
def test_survey_results_with_multiple_filters_question_answer_model(self):
|
||||
""" Check that no matter the number of filters, if their answers
|
||||
data are stored in the same model (here survey.question.answer)
|
||||
the query count stay the same as having a single filter.
|
||||
"""
|
||||
url = f'/survey/results/{self.survey.id}?filters=A,0,{self.gras_id}|A,0,{self.cat_id}'
|
||||
self.authenticate('survey_manager', 'survey_manager')
|
||||
with self.assertQueryCount(default=32): # com 32
|
||||
self.url_open(url)
|
||||
|
||||
@warmup
|
||||
def test_survey_results_with_one_filter(self):
|
||||
url = f'/survey/results/{self.survey.id}?filters=A,0,{self.cat_id}'
|
||||
self.authenticate('survey_manager', 'survey_manager')
|
||||
with self.assertQueryCount(default=32): # com 32
|
||||
self.url_open(url)
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.survey.controllers.main import Survey
|
||||
from odoo.addons.survey.tests import common
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
|
||||
|
||||
class TestSurveyResults(common.TestSurveyResultsCommon):
|
||||
""" Check the results and the performance of the different filters combinations.
|
||||
The filters can be combined but their query count doesn't add up if their
|
||||
related question answers data are stored in the same model.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestSurveyResults, self).setUp()
|
||||
self.SurveyController = Survey()
|
||||
|
||||
def test_get_filters_from_post(self):
|
||||
""" Check that the filters are correctly retrieved from the post. """
|
||||
# Matrix filter | simple_choice or multiple_choice filter | char_box, text_box, numerical_box, date or datetime filter
|
||||
post = {'filters': 'A,14,101|A,0,58|L,0,2'}
|
||||
with MockRequest(self.env):
|
||||
answer_by_column, user_input_lines_ids = self.SurveyController._get_filters_from_post(post)
|
||||
self.assertEqual(answer_by_column, {101: [14], 58: []})
|
||||
self.assertEqual(user_input_lines_ids, [2])
|
||||
|
||||
# Multiple matrix filters
|
||||
post = {'filters': 'A,14,101|A,20,205'}
|
||||
with MockRequest(self.env):
|
||||
answer_by_column, user_input_lines_ids = self.SurveyController._get_filters_from_post(post)
|
||||
self.assertEqual(answer_by_column, {101: [14], 205: [20]})
|
||||
self.assertFalse(user_input_lines_ids)
|
||||
|
||||
# Multiple filters on the same matrix column
|
||||
post = {'filters': 'A,14,101|A,20,101'}
|
||||
with MockRequest(self.env):
|
||||
answer_by_column, user_input_lines_ids = self.SurveyController._get_filters_from_post(post)
|
||||
self.assertEqual(answer_by_column, {101: [14, 20]})
|
||||
self.assertFalse(user_input_lines_ids)
|
||||
|
||||
# No model associated with the J letter, the second filter should be ignored
|
||||
post = {'filters': 'A,0,9|J,40,3'}
|
||||
with MockRequest(self.env):
|
||||
answer_by_column, user_input_lines_ids = self.SurveyController._get_filters_from_post(post)
|
||||
self.assertEqual(answer_by_column, {9: []})
|
||||
self.assertFalse(user_input_lines_ids)
|
||||
|
||||
def test_results_page_filters_survey_matrix(self):
|
||||
""" Same as 'test_results_page_filters_survey_question_answer_model'
|
||||
but with a matrix-type question (additional record involved for the row)
|
||||
"""
|
||||
post = {'filters': f'A,{self.strawberries_row_id},{self.spring_id}'}
|
||||
expected_user_input_lines = self.user_input_1.user_input_line_ids + self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_matrix_mixed_models(self):
|
||||
""" Same as 'test_results_page_filters_survey_mixed_models'
|
||||
but with a matrix-type question (additional record involved for the row)
|
||||
"""
|
||||
post = {'filters': f'A,{self.strawberries_row_id},{self.spring_id}|L,0,{self.answer_pauline.id}'}
|
||||
expected_user_input_lines = self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 5)
|
||||
|
||||
def test_results_page_filters_survey_matrix_multiple(self):
|
||||
""" Same as 'test_results_page_filters_survey_question_answer_model_multiple'
|
||||
but with matrix-type questions (additional records involved for the rows)
|
||||
"""
|
||||
post = {'filters': f'A,{self.strawberries_row_id},{self.spring_id}|A,{self.ficus_row_id},{self.once_a_week_id}'}
|
||||
expected_user_input_lines = self.user_input_1.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_matrix_multiple_same_column(self):
|
||||
""" Same as 'test_results_page_filters_survey_matrix_multiple' but
|
||||
checking the case where the same answer id can be related to multiple row ids.
|
||||
"""
|
||||
post = {'filters': f'A,{self.strawberries_row_id},{self.spring_id}|A,{self.apples_row_id},{self.spring_id}'}
|
||||
expected_user_input_lines = self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_mixed_models(self):
|
||||
""" Check results and performance of matching filters across different answer models
|
||||
('survey.question.answer' and 'survey.user_input.line' models)
|
||||
"""
|
||||
post = {'filters': f'A,0,{self.gras_id}|L,0,{self.answer_pauline.id}'}
|
||||
expected_user_input_lines = self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 5)
|
||||
|
||||
def test_results_page_filters_survey_question_answer_model(self):
|
||||
""" Check results and performance of matching one filter using
|
||||
the 'survey.question.answer' answer model
|
||||
"""
|
||||
post = {'filters': f'A,0,{self.gras_id}'}
|
||||
expected_user_input_lines = self.user_input_1.user_input_line_ids + self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_question_answer_model_multiple(self):
|
||||
""" Check results and performance of matching multiple filters using
|
||||
the 'survey.question.answer' answer model
|
||||
"""
|
||||
post = {'filters': f'A,0,{self.gras_id}|A,0,{self.cat_id}'}
|
||||
expected_user_input_lines = self.user_input_1.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_user_input_line_model(self):
|
||||
""" Check results and performance of matching one filter using
|
||||
the 'survey.user_input.line' answer model
|
||||
"""
|
||||
post = {'filters': f'L,0,{self.answer_24.id}'}
|
||||
expected_user_input_lines = self.user_input_1.user_input_line_ids + self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def test_results_page_filters_survey_user_input_line_model_multiple(self):
|
||||
""" Check results and performance of matching multiple filters using
|
||||
the 'survey.user_input.line' answer model
|
||||
"""
|
||||
post = {'filters': f'L,0,{self.answer_24.id}|L,0,{self.answer_pauline.id}'}
|
||||
expected_user_input_lines = self.user_input_2.user_input_line_ids
|
||||
self._check_results_and_query_count(post, expected_user_input_lines, 3)
|
||||
|
||||
def _check_results_and_query_count(self, post, expected_user_input_lines, expected_query_count):
|
||||
""" Check that, depending on the URL filters, the _extract_filters_data method
|
||||
is correctly returning the expected user input lines.
|
||||
"""
|
||||
self.env.invalidate_all() # clear env cache to not impact the query count
|
||||
with MockRequest(self.env), self.assertQueryCount(expected_query_count):
|
||||
found_user_input_lines, _ = self.SurveyController._extract_filters_data(self.survey, post)
|
||||
self.assertEqual(expected_user_input_lines, found_user_input_lines)
|
||||
@@ -27,6 +27,7 @@
|
||||
</template>
|
||||
|
||||
<template id="survey_page_statistics_inner" name="Survey: result statistics content">
|
||||
<t t-set="already_filtered_questions" t-value="{filter['question_id'] for filter in search_filters}"/>
|
||||
<div t-if="survey.session_show_leaderboard" class="o_survey_session_leaderboard mb-5 mt-1">
|
||||
<h2 class="mt16 text-uppercase text-muted">Leaderboard</h2>
|
||||
<t t-call="survey.user_input_session_leaderboard"/>
|
||||
@@ -175,9 +176,11 @@
|
||||
<t t-if="search_filters">
|
||||
<li t-foreach="search_filters" t-as="filter_data" class="nav-item me-2 my-1">
|
||||
<span t-attf-class="btn btn-light filter-remove-answer cursor-default">
|
||||
<span t-esc="filter_data['question']"/> | <span t-esc="filter_data['answers']"></span>
|
||||
<span t-esc="filter_data['question']"/> | <span t-esc="filter_data['answer']"></span>
|
||||
<i class="fa fa-times filter-remove-answer text-primary"
|
||||
t-att-data-row-id="filter_data['row_id']" t-att-data-answer-id="filter_data['answer_id']"></i>
|
||||
t-att-data-model-short-key="filter_data['model_short_key']"
|
||||
t-att-data-row-id="filter_data['row_id']"
|
||||
t-att-data-record-id="filter_data['record_id']"></i>
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
@@ -217,13 +220,17 @@
|
||||
</t>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="question.question_type == 'text_box'">
|
||||
<span t-if="no_print_url" t-field="input_line.value_text_box" />
|
||||
<a t-else="" t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_text_box"></a>
|
||||
<!-- If answer already covered by filter or if there is only one line to display, do not allow filtering on it again -->
|
||||
<t t-if="question.id in already_filtered_questions or len(table_data) == 1">
|
||||
<t t-esc="input_line._get_answer_value()"/>
|
||||
</t>
|
||||
<t t-if="question.question_type == 'char_box'">
|
||||
<span t-if="no_print_url" t-field="input_line.value_char_box" />
|
||||
<a t-else="" t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_char_box"></a>
|
||||
<t t-else="">
|
||||
<a class="text-primary filter-add-answer"
|
||||
data-model-short-key="L" t-att-data-record-id="input_line.id"
|
||||
role="button" aria-label="Filter surveys" title="Only show survey results having selected this answer">
|
||||
<t t-esc="input_line._get_answer_value()"/>
|
||||
<i class="fa fa-filter"/>
|
||||
</a>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -331,11 +338,17 @@
|
||||
</t>
|
||||
</td>
|
||||
<td>
|
||||
<a t-if="question.question_type == 'numerical_box'" t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_numerical_box"/>
|
||||
<a t-if="question.question_type == 'date'" t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_date"/>
|
||||
<t t-if="question.question_type == 'datetime'">
|
||||
<span t-if="no_print_url" t-field="input_line.value_datetime"/>
|
||||
<a t-else="" t-att-href="input_line.user_input_id.get_print_url()" t-field="input_line.value_datetime"></a>
|
||||
<!-- If answer already covered by filter or if there is only one line to display, do not allow filtering on it again -->
|
||||
<t t-if="question.id in already_filtered_questions or len(table_data) == 1">
|
||||
<t t-esc="input_line._get_answer_value()"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a class="text-primary filter-add-answer"
|
||||
data-model-short-key="L" t-att-data-record-id="input_line.id"
|
||||
role="button" aria-label="Filter surveys" title="Only show survey results having selected this answer">
|
||||
<t t-esc="input_line._get_answer_value()"/>
|
||||
<i class="fa fa-filter"/>
|
||||
</a>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -410,8 +423,7 @@
|
||||
<span t-esc="'%s Votes' % choice_data['count']" class="badge text-bg-primary"/>
|
||||
<i t-if="choice_data['suggested_answer'].id and choice_data['count']"
|
||||
class="fa fa-filter text-primary filter-add-answer"
|
||||
t-att-data-question-id="question.id"
|
||||
t-att-data-answer-id="choice_data['suggested_answer'].id"
|
||||
data-model-short-key="A" t-att-data-record-id="choice_data['suggested_answer'].id"
|
||||
role="img" aria-label="Filter surveys" title="Only show survey results having selected this answer"/>
|
||||
</td>
|
||||
<td t-if="question.is_scored_question" t-call="survey.survey_remove_unnecessary_decimals">
|
||||
@@ -472,9 +484,9 @@
|
||||
<span t-esc="round(column_data['count'] * 100.0/ (len(question_data['answer_input_done_ids']) or 1), 2)"></span> %
|
||||
<span class="badge text-bg-primary" t-esc="column_data['count']"></span>
|
||||
<i t-if="column_data['count']" class="fa fa-filter text-primary filter-add-answer"
|
||||
t-att-data-question-id="question.id"
|
||||
data-model-short-key="A"
|
||||
t-att-data-row-id="choice_data['row'].id"
|
||||
t-att-data-answer-id="column_data['suggested_answer'].id" role="img" aria-label="Filter surveys"
|
||||
t-att-data-record-id="column_data['suggested_answer'].id" role="img" aria-label="Filter surveys"
|
||||
title="Only show survey results having selected this answer"></i>
|
||||
<i t-else="" class="o_survey_answer_matrix_whitespace"/>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user