From c77e337deb1c180bb1930fd93e208f89edb21372 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 8 Sep 2020 10:26:15 +0200 Subject: [PATCH] [IMP] Conditional survey questions' usability * Allow to configure conditional questions before saving the page * Skip pages in the survey without visible questions * Hide questions and pages without visible questions in the print overview * Restore method inheritance * Adapt to Odoo 14.0 datamodel --- survey_conditional_questions/__manifest__.py | 27 ++---- .../controllers/main.py | 84 ++++++------------- survey_conditional_questions/i18n/en.po | 4 +- survey_conditional_questions/i18n/es.po | 4 +- .../i18n/survey_conditional_questions.pot | 4 +- .../migrations/11.0.1.2.0/pre-migrate.py | 37 ++++++++ .../models/__init__.py | 2 + .../models/survey_question.py | 33 ++------ .../models/survey_survey.py | 28 +++++++ .../models/survey_user_input.py | 24 +++--- .../models/survey_user_input_line.py | 58 +++++++++++++ .../src/js/survey_conditional_questions.js | 30 +++++++ .../tests/__init__.py | 1 + .../test_survey_conditional_questions.py | 56 +++++++++++++ .../views/survey_question_qweb.xml | 16 +++- .../views/survey_question_views.xml | 33 ++++++-- 16 files changed, 311 insertions(+), 130 deletions(-) create mode 100644 survey_conditional_questions/migrations/11.0.1.2.0/pre-migrate.py create mode 100644 survey_conditional_questions/models/survey_survey.py create mode 100644 survey_conditional_questions/models/survey_user_input_line.py create mode 100644 survey_conditional_questions/static/src/js/survey_conditional_questions.js create mode 100644 survey_conditional_questions/tests/__init__.py create mode 100644 survey_conditional_questions/tests/test_survey_conditional_questions.py diff --git a/survey_conditional_questions/__manifest__.py b/survey_conditional_questions/__manifest__.py index 2f5394c..b9e36ae 100644 --- a/survey_conditional_questions/__manifest__.py +++ b/survey_conditional_questions/__manifest__.py @@ -1,36 +1,19 @@ -############################################################################## -# -# Copyright (C) 2015 ADHOC SA (http://www.adhoc.com.ar) -# All Rights Reserved. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# © 2015 ADHOC SA (http://www.adhoc.com.ar) +# © 2020 Opener B.V. (https://opener.amsterdam) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { 'name': 'Survey Conditional Questions', - 'version': '11.0.1.1.0', + 'version': '11.0.1.2.0', 'category': 'Warehouse Management', 'sequence': 14, 'summary': '', - 'author': 'ADHOC SA', + 'author': 'ADHOC SA, Opener B.V.', 'website': 'www.adhoc.com.ar', 'license': 'AGPL-3', 'images': [ ], 'depends': [ 'survey', - 'website' ], 'data': [ 'views/survey_question_views.xml', diff --git a/survey_conditional_questions/controllers/main.py b/survey_conditional_questions/controllers/main.py index 320525e..f66114e 100644 --- a/survey_conditional_questions/controllers/main.py +++ b/survey_conditional_questions/controllers/main.py @@ -3,6 +3,7 @@ # directory ############################################################################## +import json import logging from odoo.addons.survey.controllers.main import WebsiteSurvey from odoo import http @@ -14,62 +15,29 @@ class SurveyConditional(WebsiteSurvey): - # TODO deberiamos heredar esto correctamente - @http.route() - def fill_survey(self, survey, token, prev=None, **post): - '''Display and validates a survey''' - Survey = request.env['survey.survey'] - UserInput = request.env['survey.user_input'] - - # Controls if the survey can be displayed - errpage = self._check_bad_cases(survey) - if errpage: - return errpage - - # Load the user_input - user_input = UserInput.sudo().search([('token', '=', token)], limit=1) - if not user_input: # Invalid token - return request.render("website.403") - - # Do not display expired survey (even if some pages have already been - # displayed -- There's a time for everything!) - errpage = self._check_deadline(user_input) - if errpage: - return errpage - - # Select the right page - if user_input.state == 'new': # First page - page, page_nr, last = Survey.next_page( - user_input, 0, go_back=False) - data = {'survey': survey, 'page': page, - 'page_nr': page_nr, 'token': user_input.token} - data['hide_question_ids'] = UserInput.get_list_questions( - survey, user_input) - if last: - data.update({'last': True}) - return request.render('survey.survey', data) - elif user_input.state == 'done': # Display success message - return request.render( - 'survey.sfinished', - {'survey': survey, 'token': token, 'user_input': user_input}) - elif user_input.state == 'skip': - flag = (True if prev and prev == 'prev' else False) - page, page_nr, last = Survey.next_page( - user_input, user_input.last_displayed_page_id.id, go_back=flag) - - # special case if you click "previous" from the last page, - # then leave the survey, then reopen it from the URL, avoid crash - if not page: - page, page_nr, last = Survey.next_page( - user_input, user_input.last_displayed_page_id.id, - go_back=True) - - data = {'survey': survey, 'page': page, - 'page_nr': page_nr, 'token': user_input.token} - if last: - data.update({'last': True}) - data['hide_question_ids'] = UserInput.get_list_questions( - survey, user_input) - return request.render('survey.survey', data) + @http.route( + ['/survey/hidden//' + '/'], + type='http', auth='public', website=True) + def hidden(self, survey, token, stored, **post): + """ Pass the lists of hidden questions and pages to be applied in the + Javascript. + :param stored: indicate if we can rely on stored answers from a + completed survey, or if we have to determine the hidden questions from + the answers filled in so far. """ + ret = {'hidden_pages': [], 'hidden_questions': []} + user_input = request.env['survey.user_input'].sudo().search( + [('token', '=', token)]) + if stored: + questions = user_input.user_input_line_ids.filtered( + 'hidden').mapped('question_id') else: - return request.render("website.403") + questions = user_input.get_hidden_questions() + for question in questions: + question_tag = '%s_%s_%s' % ( + question.survey_id.id, question.page_id.id, question.id) + ret['hidden_questions'].append(question_tag) + for page in questions.mapped('page_id'): + if not page.question_ids - questions: + ret['hidden_pages'].append(page.id) + return json.dumps(ret) diff --git a/survey_conditional_questions/i18n/en.po b/survey_conditional_questions/i18n/en.po index 4da32bb..162ff00 100644 --- a/survey_conditional_questions/i18n/en.po +++ b/survey_conditional_questions/i18n/en.po @@ -28,12 +28,12 @@ msgid "Conditional Question" msgstr "Conditional Question" #. module: survey_conditional_questions -#: help:survey.question,question_conditional_id:0 +#: help:survey.question,triggering_question_id:0 msgid "In order to edit this field you should first save the question" msgstr "In order to edit this field you should first save the question" #. module: survey_conditional_questions -#: field:survey.question,question_conditional_id:0 +#: field:survey.question,triggering_question_id:0 msgid "Question" msgstr "Question" diff --git a/survey_conditional_questions/i18n/es.po b/survey_conditional_questions/i18n/es.po index c32c0d0..a9af9bd 100644 --- a/survey_conditional_questions/i18n/es.po +++ b/survey_conditional_questions/i18n/es.po @@ -29,12 +29,12 @@ msgid "Conditional Question" msgstr "Pregunta Condicional" #. module: survey_conditional_questions -#: model:ir.model.fields,help:survey_conditional_questions.field_survey_question_question_conditional_id +#: model:ir.model.fields,help:survey_conditional_questions.field_survey_question_triggering_question_id msgid "In order to edit this field you should first save the question" msgstr "Para editar este campo , primero debe guardar la pregunta" #. module: survey_conditional_questions -#: model:ir.model.fields,field_description:survey_conditional_questions.field_survey_question_question_conditional_id +#: model:ir.model.fields,field_description:survey_conditional_questions.field_survey_question_triggering_question_id msgid "Question" msgstr "Pregunta" diff --git a/survey_conditional_questions/i18n/survey_conditional_questions.pot b/survey_conditional_questions/i18n/survey_conditional_questions.pot index 763b0f6..4de84ac 100644 --- a/survey_conditional_questions/i18n/survey_conditional_questions.pot +++ b/survey_conditional_questions/i18n/survey_conditional_questions.pot @@ -26,12 +26,12 @@ msgid "Conditional Question" msgstr "" #. module: survey_conditional_questions -#: help:survey.question,question_conditional_id:0 +#: help:survey.question,triggering_question_id:0 msgid "In order to edit this field you should first save the question" msgstr "" #. module: survey_conditional_questions -#: field:survey.question,question_conditional_id:0 +#: field:survey.question,triggering_question_id:0 msgid "Question" msgstr "" diff --git a/survey_conditional_questions/migrations/11.0.1.2.0/pre-migrate.py b/survey_conditional_questions/migrations/11.0.1.2.0/pre-migrate.py new file mode 100644 index 0000000..f85fb1e --- /dev/null +++ b/survey_conditional_questions/migrations/11.0.1.2.0/pre-migrate.py @@ -0,0 +1,37 @@ +import logging + + +def migrate(cr, version): + logger = logging.getLogger( + 'survey_conditional_questions.migrations.11.0.1.3.0') + + def rename(old, new): + """ Rename column if the old column exists and the new one does not """ + cr.execute( + """ SELECT EXISTS( + SELECT * + FROM information_schema.columns + WHERE table_name='survey_question' + AND column_name=%s) """, (new,)) + if cr.fetchone()[0]: + logger.info('Column %s already exists', new) + return + cr.execute( + """ SELECT EXISTS( + SELECT * + FROM information_schema.columns + WHERE table_name='survey_question' + AND column_name=%s) """, (old,)) + if not cr.fetchone()[0]: + logger.info('Column %s does not exist', old) + return + logger.info('Renaming column %s to %s', new, old) + cr.execute( + """ ALTER TABLE survey_question + RENAME COLUMN %s to %s """ % (old, new)) + + for old, new in [ + ('conditional', 'is_conditional'), + ('question_conditional_id', 'triggering_question_id'), + ('answer_id', 'triggering_answer_id')]: + rename(old, new) diff --git a/survey_conditional_questions/models/__init__.py b/survey_conditional_questions/models/__init__.py index c18232d..0186976 100644 --- a/survey_conditional_questions/models/__init__.py +++ b/survey_conditional_questions/models/__init__.py @@ -2,5 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## +from . import survey_survey from . import survey_question from . import survey_user_input +from . import survey_user_input_line diff --git a/survey_conditional_questions/models/survey_question.py b/survey_conditional_questions/models/survey_question.py index 5aef5b9..80acbda 100644 --- a/survey_conditional_questions/models/survey_question.py +++ b/survey_conditional_questions/models/survey_question.py @@ -3,29 +3,25 @@ # directory ############################################################################## from odoo import api, fields, models -import logging - -_logger = logging.getLogger(__name__) class SurveyQuestion(models.Model): _inherit = 'survey.question' - conditional = fields.Boolean( + is_conditional = fields.Boolean( 'Conditional Question', copy=False, # we add copy = false to avoid wrong link on survey copy, # should be improoved ) - question_conditional_id = fields.Many2one( + triggering_question_id = fields.Many2one( 'survey.question', 'Question', copy=False, - domain="[('survey_id', '=', survey_id)]", help="In order to edit this field you should" " first save the question" ) - answer_id = fields.Many2one( + triggering_answer_id = fields.Many2one( 'survey.label', 'Answer', copy=False, @@ -33,26 +29,15 @@ class SurveyQuestion(models.Model): @api.multi def validate_question(self, post, answer_tag): - ''' Validate question, depending on question - type and parameters ''' + """ Skip validation of hidden questions """ self.ensure_one() - try: - checker = getattr(self, 'validate_' + self.type) - except AttributeError: - _logger.warning( - checker.type + - ": This type of question has no validation method") - return {} - else: - # TODO deberiamos emprolijar esto - if not self.question_conditional_id: - return checker(post, answer_tag) + if self.triggering_question_id: input_answer_ids = self.env['survey.user_input_line'].search( [('user_input_id.token', '=', post.get('token')), - ('question_id', '=', self.question_conditional_id.id)]) + ('question_id', '=', self.triggering_question_id.id)]) for answers in input_answer_ids: value_suggested = answers.value_suggested - if self.conditional and self.answer_id != value_suggested: + if (self.is_conditional and + self.triggering_answer_id != value_suggested): return {} - else: - return checker(post, answer_tag) + return super(SurveyQuestion, self).validate_question(post, answer_tag) diff --git a/survey_conditional_questions/models/survey_survey.py b/survey_conditional_questions/models/survey_survey.py new file mode 100644 index 0000000..8e3fca7 --- /dev/null +++ b/survey_conditional_questions/models/survey_survey.py @@ -0,0 +1,28 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import api, models + + +class SurveySurvey(models.Model): + _inherit = 'survey.survey' + + @api.model + def next_page(self, user_input, page_id, go_back=False): + """ Skip pages that only have hidden questions on them, + except if its the last page or the first page (in which case there + is a configuration error in the survey). """ + questions_to_hide = user_input.get_hidden_questions() + res = super(SurveySurvey, self).next_page( + user_input, page_id, go_back=go_back) + page, index, last = res + if page and not (page.question_ids - questions_to_hide): + if (not go_back and not last) or (go_back and index): + # Mark every question on this hidden page as hidden. + for question in page.question_ids: + self.env['survey.user_input_line'].update_hidden( + user_input, question) + return self.next_page( + user_input, page.id, go_back=go_back) + return res diff --git a/survey_conditional_questions/models/survey_user_input.py b/survey_conditional_questions/models/survey_user_input.py index a651b43..0666762 100644 --- a/survey_conditional_questions/models/survey_user_input.py +++ b/survey_conditional_questions/models/survey_user_input.py @@ -9,17 +9,17 @@ class SurveyUserInput(models.Model): _inherit = 'survey.user_input' @api.model - def get_list_questions(self, survey, user_input): - obj_questions = self.env['survey.question'] - questions_to_hide = [] - question_ids = obj_questions.search( - [('survey_id', '=', survey.id)]) - for question in question_ids.filtered('conditional'): - for question2 in question_ids.filtered( - lambda x: x == question.question_conditional_id): - input_answer_ids = user_input.user_input_line_ids.filtered( + def get_hidden_questions(self): + """ Return the questions that should be hidden based on the current + user input """ + questions_to_hide = self.env['survey.question'] + questions = self.survey_id.mapped('page_ids.question_ids') + for question in questions.filtered('is_conditional'): + for question2 in questions.filtered( + lambda x: x == question.triggering_question_id): + input_answer_ids = self.user_input_line_ids.filtered( lambda x: x.question_id == question2) - for answers in input_answer_ids.filtered( - lambda x: x.value_suggested != question.answer_id): - questions_to_hide.append(question.id) + if question.triggering_answer_id not in ( + input_answer_ids.mapped('value_suggested')): + questions_to_hide += question return questions_to_hide diff --git a/survey_conditional_questions/models/survey_user_input_line.py b/survey_conditional_questions/models/survey_user_input_line.py new file mode 100644 index 0000000..3bbf48b --- /dev/null +++ b/survey_conditional_questions/models/survey_user_input_line.py @@ -0,0 +1,58 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import api, fields, models + + +class SurveyUserInputLine(models.Model): + _inherit = 'survey.user_input_line' + + hidden = fields.Boolean( + help=('Indicate whether this input\'s question was hidden on ' + 'condition of earlier questions in the survey.')) + + @api.model + def update_hidden(self, user_input, question, hidden=True): + """ If hidden, delete all preexisting values and replace by a dummy + one marked as hidden. If not hidden, delete any preexisting value + marked as hidden. """ + domain = [ + ('user_input_id', '=', user_input.id), + ('survey_id', '=', question.survey_id.id), + ('question_id', '=', question.id) + ] + if not hidden: + # Only select hidden values + domain.append(('hidden', '=', True)) + existing = self.search(domain) + if not hidden: + if existing: + # Remove the hidden value + existing.unlink() + return + if existing: + if len(existing) == 1 and existing.hidden: + # Nothing to do + return existing + # Else, wipe all values + existing.unlink() + return self.create({ + 'user_input_id': user_input.id, + 'question_id': question.id, + 'survey_id': question.survey_id.id, + 'skipped': True, + 'hidden': True, + }) + + @api.model + def save_lines(self, user_input_id, question, post, answer_tag): + """ Inject value for 'hidden' in context to be picked up in write and + create methods """ + user_input = self.env['survey.user_input'].browse(user_input_id) + hidden = question in user_input.get_hidden_questions() + self.update_hidden(user_input, question, hidden=hidden) + if hidden: + return True + return super(SurveyUserInputLine, self).save_lines( + user_input_id, question, post, answer_tag) diff --git a/survey_conditional_questions/static/src/js/survey_conditional_questions.js b/survey_conditional_questions/static/src/js/survey_conditional_questions.js new file mode 100644 index 0000000..e9cc29e --- /dev/null +++ b/survey_conditional_questions/static/src/js/survey_conditional_questions.js @@ -0,0 +1,30 @@ +odoo.define('survey.conditional_question', function (require) { +'use strict'; + + require('survey.survey'); + var the_form = $('.js_surveyform'); + + function hide_conditional_questions(){ + // Hide the marked questions and pages + var hidden_controller = the_form.attr("data-hidden"); + if (! _.isUndefined(hidden_controller)) { + var hidden_def = $.ajax(hidden_controller, {dataType: "json"}).done( + function(json_data){ + // For each of these, hide the question label and the answer + _.each(json_data.hidden_questions, function(key){ + the_form.find(".js_question-wrapper[id=" + key + "]").css("display", "none"); + }); + _.each(json_data.hidden_pages, function(key){ + var div = the_form.find("h1[data-oe-id=" + key + "][data-oe-model='survey.page']").parent(); + div.css("display", "none"); + // Also hide the adjacent hr tag + div.prev().css("display", "none"); + }); + }); + } + } + + if(the_form.length) { + hide_conditional_questions(); + } +}); diff --git a/survey_conditional_questions/tests/__init__.py b/survey_conditional_questions/tests/__init__.py new file mode 100644 index 0000000..5767f87 --- /dev/null +++ b/survey_conditional_questions/tests/__init__.py @@ -0,0 +1 @@ +from . import test_survey_conditional_questions diff --git a/survey_conditional_questions/tests/test_survey_conditional_questions.py b/survey_conditional_questions/tests/test_survey_conditional_questions.py new file mode 100644 index 0000000..5233f39 --- /dev/null +++ b/survey_conditional_questions/tests/test_survey_conditional_questions.py @@ -0,0 +1,56 @@ +from odoo.tests.common import TransactionCase + + +class TestSurveyConditionalQuestions(TransactionCase): + def test_survey_conditional_questions(self): + label = self.env.ref('survey.choice_1_1_1') + question = label.question_id + survey = question.survey_id + + conditional_question = survey.page_ids[1].question_ids[0] + conditional_values = { + 'is_conditional': True, + 'triggering_question_id': question.id, + 'triggering_answer_id': label.id, + } + conditional_question.write(conditional_values) + user_input = self.env['survey.user_input'].create({ + 'survey_id': survey.id, + 'partner_id': self.env.user.partner_id.id, + }) + + # Conditional question is hidden when original question not answered + self.assertIn( + conditional_question, user_input.get_hidden_questions()) + input_line = self.env['survey.user_input_line'].create({ + 'answer_type': 'suggestion', + 'question_id': question.id, + 'skipped': False, + 'user_input_id': user_input.id, + 'value_suggested': label.id, + }) + + # Conditional question is not hidden if the desired answer was given + self.assertNotIn( + conditional_question, user_input.get_hidden_questions()) + + # Conditional question is hidden for any other answer + input_line.value_suggested = self.env.ref('survey.choice_1_1_2') + self.assertIn(conditional_question, user_input.get_hidden_questions()) + + # Next page returns the next page + self.assertEqual( + survey.next_page(user_input, survey.page_ids[0].id)[0], + survey.page_ids[1]) + + # Hide all the questions on the page + survey.page_ids[1].question_ids.write(conditional_values) + # Next page skips the next page + self.assertEqual( + survey.next_page(user_input, survey.page_ids[0].id)[0], + survey.page_ids[2]) + # In both directions + self.assertEqual( + survey.next_page( + user_input, survey.page_ids[2].id, go_back=True)[0], + survey.page_ids[0]) diff --git a/survey_conditional_questions/views/survey_question_qweb.xml b/survey_conditional_questions/views/survey_question_qweb.xml index f4adaa0..324eb47 100644 --- a/survey_conditional_questions/views/survey_question_qweb.xml +++ b/survey_conditional_questions/views/survey_question_qweb.xml @@ -1,8 +1,18 @@ -