[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:
amdi-odoo
2023-02-24 17:13:05 +01:00
parent aa252adf4f
commit deaf9939b5
9 changed files with 448 additions and 64 deletions
+114 -36
View File
@@ -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,
}
+8
View File
@@ -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)])
+36
View File
@@ -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
+15 -8
View File
@@ -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".')
+2
View File
@@ -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
+78 -3
View File
@@ -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)
+128
View File
@@ -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>