From deaf9939b52013f64c7b68b114589b6cdd9a523e Mon Sep 17 00:00:00 2001 From: amdi-odoo Date: Thu, 23 Feb 2023 07:50:00 +0000 Subject: [PATCH] [IMP] survey: add every question type filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- addons/survey/controllers/main.py | 150 +++++++++++++----- addons/survey/models/survey_question.py | 8 + addons/survey/models/survey_user_input.py | 36 +++++ addons/survey/static/src/js/survey_result.js | 23 ++- addons/survey/tests/__init__.py | 2 + addons/survey/tests/common.py | 81 +++++++++- .../survey/tests/test_survey_performance.py | 38 +++++ addons/survey/tests/test_survey_results.py | 128 +++++++++++++++ .../views/survey_templates_statistics.xml | 46 ++++-- 9 files changed, 448 insertions(+), 64 deletions(-) create mode 100644 addons/survey/tests/test_survey_performance.py create mode 100644 addons/survey/tests/test_survey_results.py diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 37568529f89..e15cddb88f3 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -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, + } diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 6e13b07c4cf..b0bb39807ae 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -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)]) diff --git a/addons/survey/models/survey_user_input.py b/addons/survey/models/survey_user_input.py index 23815a170a5..50f13936c6b 100644 --- a/addons/survey/models/survey_user_input.py +++ b/addons/survey/models/survey_user_input.py @@ -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 diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js index 2077a684325..163857a0bd7 100644 --- a/addons/survey/static/src/js/survey_result.js +++ b/addons/survey/static/src/js/survey_result.js @@ -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".') diff --git a/addons/survey/tests/__init__.py b/addons/survey/tests/__init__.py index 865529005cb..f52049f2faa 100644 --- a/addons/survey/tests/__init__.py +++ b/addons/survey/tests/__init__.py @@ -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 diff --git a/addons/survey/tests/common.py b/addons/survey/tests/common.py index 67687d5d336..42a1ee5af79 100644 --- a/addons/survey/tests/common.py +++ b/addons/survey/tests/common.py @@ -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' diff --git a/addons/survey/tests/test_survey_performance.py b/addons/survey/tests/test_survey_performance.py new file mode 100644 index 00000000000..692526642bc --- /dev/null +++ b/addons/survey/tests/test_survey_performance.py @@ -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) diff --git a/addons/survey/tests/test_survey_results.py b/addons/survey/tests/test_survey_results.py new file mode 100644 index 00000000000..bea7cfabd85 --- /dev/null +++ b/addons/survey/tests/test_survey_results.py @@ -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) diff --git a/addons/survey/views/survey_templates_statistics.xml b/addons/survey/views/survey_templates_statistics.xml index ff88df386f4..843959a11ab 100644 --- a/addons/survey/views/survey_templates_statistics.xml +++ b/addons/survey/views/survey_templates_statistics.xml @@ -27,6 +27,7 @@