From 278fb50dbcac7f4295d13676194360050850e29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Tue, 4 Feb 2020 08:31:19 +0000 Subject: [PATCH 1/8] [REM] website_event_question: remove unusable questions reporting PURPOSE The commit removes the reporting on the event.question model along with its created SQL view 'event.question.report' because they're simply not usable. SPECS Based on latest testing, the reporting was completely unusable if you have more than a hundred questions (all events combined) and needs to be refactored based on new specifications. As we need to re-work event questions to be able to add "free text" questions, we prefer to temporarily drop the reporting instead of having to adapt it. LINKS PR #45497 Upgrade PR odoo/upgrade#799 Task 2189529 --- addons/website_event_questions/__init__.py | 1 - .../website_event_questions/__manifest__.py | 1 - .../report/__init__.py | 3 - .../report/report_event_question_view.xml | 63 ------------------- .../report_event_registrations_questions.py | 39 ------------ .../security/ir.model.access.csv | 1 - 6 files changed, 108 deletions(-) delete mode 100644 addons/website_event_questions/report/__init__.py delete mode 100644 addons/website_event_questions/report/report_event_question_view.xml delete mode 100644 addons/website_event_questions/report/report_event_registrations_questions.py diff --git a/addons/website_event_questions/__init__.py b/addons/website_event_questions/__init__.py index 8114cc7a69b..5a31f331a2a 100644 --- a/addons/website_event_questions/__init__.py +++ b/addons/website_event_questions/__init__.py @@ -3,5 +3,4 @@ from . import controllers from . import models -from . import report from . import tests diff --git a/addons/website_event_questions/__manifest__.py b/addons/website_event_questions/__manifest__.py index 099abeef1a7..029ad56fe74 100644 --- a/addons/website_event_questions/__manifest__.py +++ b/addons/website_event_questions/__manifest__.py @@ -9,7 +9,6 @@ 'data': [ 'views/event_views.xml', 'views/event_templates.xml', - 'report/report_event_question_view.xml', 'security/ir.model.access.csv', ], 'demo': [ diff --git a/addons/website_event_questions/report/__init__.py b/addons/website_event_questions/report/__init__.py deleted file mode 100644 index bebed12eef7..00000000000 --- a/addons/website_event_questions/report/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import report_event_registrations_questions diff --git a/addons/website_event_questions/report/report_event_question_view.xml b/addons/website_event_questions/report/report_event_question_view.xml deleted file mode 100644 index a495d66fc7d..00000000000 --- a/addons/website_event_questions/report/report_event_question_view.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - event.question.report.graph - event.question.report - - - - - - - - - - - event.question.report.pivot - event.question.report - - - - - - - - - - - - - event.question.report.search - event.question.report - - - - - - - - - - - - - - - - - Questions Analysis - event.question.report - graph,pivot - - - - - - - diff --git a/addons/website_event_questions/report/report_event_registrations_questions.py b/addons/website_event_questions/report/report_event_registrations_questions.py deleted file mode 100644 index 6bbd52d043f..00000000000 --- a/addons/website_event_questions/report/report_event_registrations_questions.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, tools - - -class ReportEventRegistrationQuestions(models.Model): - _name = "event.question.report" - _auto = False - _description = 'Event Question Report' - - attendee_id = fields.Many2one(comodel_name='event.registration', string='Registration') - question_id = fields.Many2one(comodel_name='event.question', string='Question') - answer_id = fields.Many2one(comodel_name='event.answer', string='Answer') - event_id = fields.Many2one(comodel_name='event.event', string='Event') - - def init(self): - """ Event Question main report """ - tools.drop_view_if_exists(self._cr, 'event_question_report') - self._cr.execute(""" CREATE VIEW event_question_report AS ( - SELECT - att_answer.id as id, - att_answer.event_registration_id as attendee_id, - answer.question_id as question_id, - answer.id as answer_id, - question.event_id as event_id - FROM - event_registration_answer as att_answer - LEFT JOIN - event_answer as answer ON answer.id = att_answer.event_answer_id - LEFT JOIN - event_question as question ON question.id = answer.question_id - GROUP BY - attendee_id, - event_id, - question_id, - answer_id, - att_answer.id - )""") diff --git a/addons/website_event_questions/security/ir.model.access.csv b/addons/website_event_questions/security/ir.model.access.csv index 79ab8690426..66d25208471 100644 --- a/addons/website_event_questions/security/ir.model.access.csv +++ b/addons/website_event_questions/security/ir.model.access.csv @@ -3,5 +3,4 @@ event_question_all,event.question.all,model_event_question,,1,0,0,0 event_question_event_user,event.question.event.user,model_event_question,event.group_event_user,1,1,1,1 event_answer_all,event.answer.all,model_event_answer,,1,0,0,0 event_answer_event_user,event.answer.event.user,model_event_answer,event.group_event_user,1,1,1,1 -event_question_report_all,event.question.report.all,model_event_question_report,event.group_event_user,1,1,1,1 event_registration_answer_all,access_event_registration_answer,model_event_registration_answer,,1,1,1,1 From 7b79c3244d0b52eb74f5c87da903c98f1175040b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Fri, 27 Mar 2020 10:40:09 +0000 Subject: [PATCH 2/8] [IMP] website_event_questions: allow 'free text' questions PURPOSE Allow "free text" questions in the event questions asked to attendees, in addition to the existing "selection" type. e.g: "What are your allergies?" SPECS The implementation is heavily based on the modelisation we chose for the survey module since the features become similar in terms of question types and inputs. - Introduce a 'question_type' that is either 'simple_choice' (= previous behavior) or 'text_box' that will contain the attendee text answer. - Rename the event.answer model to event.question.answer to make it more obvious that it contains answer "suggestions" to a question of type 'simple_choice'. - Introduce a 'event.user_input.line' that is an answer to a specific question for a specific registration. It works very similarly to 'survey.user_input.line' but it's not contained in a 'user_input' model, it's directly linked to the registration using a 'user_input_line_ids' o2m field. The frontend view has been adapted accordingly to display either a selection input or a textarea based on the question type. The frontend orm processing now takes in both answer types (id of suggested answer or text input). On the backend side, views have been adapted to display event questions and attendee answers in a more convenient and logical manner. LINKS PR #45497 Upgrade PR odoo/upgrade#799 Task ID 2189529 --- .../website_event_questions/__manifest__.py | 2 +- .../controllers/main.py | 37 ++++++--- .../data/event_question_demo.xml | 4 +- .../models/event_event.py | 1 + .../models/event_question.py | 24 +++++- .../models/event_registration.py | 32 ++++---- .../security/ir.model.access.csv | 6 +- .../website_event_questions/tests/common.py | 8 ++ .../tests/test_event_internals.py | 29 +++++-- .../views/event_templates.xml | 35 +++++--- .../views/event_views.xml | 79 ++++++++++++------- 11 files changed, 174 insertions(+), 83 deletions(-) diff --git a/addons/website_event_questions/__manifest__.py b/addons/website_event_questions/__manifest__.py index 029ad56fe74..d183e1b0244 100644 --- a/addons/website_event_questions/__manifest__.py +++ b/addons/website_event_questions/__manifest__.py @@ -4,7 +4,7 @@ 'name': 'Questions on Events', 'description': 'Questions on Events', 'category': 'Marketing', - 'version': '1.0', + 'version': '1.1', 'depends': ['website_event'], 'data': [ 'views/event_views.xml', diff --git a/addons/website_event_questions/controllers/main.py b/addons/website_event_questions/controllers/main.py index a24ccc941f9..fdcc1f66313 100644 --- a/addons/website_event_questions/controllers/main.py +++ b/addons/website_event_questions/controllers/main.py @@ -1,28 +1,47 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.http import request + from odoo.addons.website_event.controllers.main import WebsiteEventController class WebsiteEvent(WebsiteEventController): def _process_attendees_form(self, event, form_details): - ''' Process data posted from the attendee details form. ''' + """ Process data posted from the attendee details form. + Extracts question answers: + - For both questions asked 'once_per_order' and questions asked to every attendee + - For questions of type 'simple_choice', extracting the suggested answer id + - For questions of type 'text_box', extracting the text answer of the attendee. """ registrations = super(WebsiteEvent, self)._process_attendees_form(event, form_details) for registration in registrations: - registration['answer_ids'] = [] + registration['registration_answer_ids'] = [] general_answer_ids = [] for key, value in form_details.items(): - if 'answer_ids' in key: - index = int(key.split('-')[0]) - if not index: - general_answer_ids.append([4, int(value)]) - else: - registrations[index-1]['answer_ids'].append([4, int(value)]) + if 'question_answer' in key and value: + dummy, registration_index, question_id = key.split('-') + question = request.env['event.question'].browse(int(question_id)) + answer_values = None + if question.question_type == 'simple_choice': + answer_values = { + 'question_id': int(question_id), + 'value_answer_id': int(value) + } + elif question.question_type == 'text_box': + answer_values = { + 'question_id': int(question_id), + 'value_text_box': value + } + + if answer_values and not int(registration_index): + general_answer_ids.append((0, 0, answer_values)) + elif answer_values: + registrations[int(registration_index) - 1]['registration_answer_ids'].append((0, 0, answer_values)) for registration in registrations: - registration['answer_ids'].extend(general_answer_ids) + registration['registration_answer_ids'].extend(general_answer_ids) return registrations diff --git a/addons/website_event_questions/data/event_question_demo.xml b/addons/website_event_questions/data/event_question_demo.xml index 414ddb44735..fc45310dd52 100644 --- a/addons/website_event_questions/data/event_question_demo.xml +++ b/addons/website_event_questions/data/event_question_demo.xml @@ -6,12 +6,12 @@ - + Yes 1 - + No 2 diff --git a/addons/website_event_questions/models/event_event.py b/addons/website_event_questions/models/event_event.py index 3a7984a30a0..bd2461a75cd 100644 --- a/addons/website_event_questions/models/event_event.py +++ b/addons/website_event_questions/models/event_event.py @@ -33,6 +33,7 @@ class EventEvent(models.Model): event.question_ids = [(5, 0, 0)] + [ (0, 0, { 'title': question.title, + 'question_type': question.question_type, 'sequence': question.sequence, 'once_per_order': question.once_per_order, }) diff --git a/addons/website_event_questions/models/event_question.py b/addons/website_event_questions/models/event_question.py index 1081f96f6c9..4fbafa62eae 100644 --- a/addons/website_event_questions/models/event_question.py +++ b/addons/website_event_questions/models/event_question.py @@ -12,9 +12,12 @@ class EventQuestion(models.Model): _description = 'Event Question' title = fields.Char(required=True, translate=True) + question_type = fields.Selection([ + ('simple_choice', 'Selection'), + ('text_box', 'Text Input')], default='simple_choice', string="Question Type", required=True) event_type_id = fields.Many2one('event.type', 'Event Type', ondelete='cascade') event_id = fields.Many2one('event.event', 'Event', ondelete='cascade') - answer_ids = fields.One2many('event.answer', 'question_id', "Answers", required=True, copy=True) + answer_ids = fields.One2many('event.question.answer', 'question_id', "Answers", copy=True) sequence = fields.Integer(default=10) once_per_order = fields.Boolean('Ask only once per order', help="If True, this question will be asked only once and its value will be propagated to every attendees." @@ -37,11 +40,24 @@ class EventQuestion(models.Model): }) for answer in event.event_type_id.question_ids.filtered(lambda question: question.title == vals.get('title')).mapped('answer_ids')] return super(EventQuestion, self).create(vals) + def write(self, vals): + """ We add a check to prevent changing the question_type of a question that already has answers. + Indeed, it would mess up the event.registration.answer (answer type not matching the question type). """ -class EventAnswer(models.Model): - _name = 'event.answer' + if 'question_type' in vals: + questions_new_type = self.filtered(lambda question: question.question_type != vals['question_type']) + if questions_new_type: + answer_count = self.env['event.registration.answer'].search_count([('question_id', 'in', questions_new_type.ids)]) + if answer_count > 0: + raise UserError(_("You cannot change the question type of a question that already has answers!")) + return super(EventQuestion, self).write(vals) + + +class EventQuestionAnswer(models.Model): + """ Contains suggested answers to a 'simple_choice' event.question. """ + _name = 'event.question.answer' _order = 'sequence,id' - _description = 'Event Answer' + _description = 'Event Question Answer' name = fields.Char('Answer', required=True, translate=True) question_id = fields.Many2one('event.question', required=True, ondelete='cascade') diff --git a/addons/website_event_questions/models/event_registration.py b/addons/website_event_questions/models/event_registration.py index b8f23da99d7..5bb81b48703 100644 --- a/addons/website_event_questions/models/event_registration.py +++ b/addons/website_event_questions/models/event_registration.py @@ -4,22 +4,24 @@ from odoo import fields, models -class EventRegistrationAnswer(models.Model): - ''' This m2m table has to be explicitly instanciated as we need unique ids - in the reporting view event.question.report. - - This model is purely technical. ''' - - _name = 'event.registration.answer' - _table = 'event_registration_answer' - _description = 'Event Registration Answer' - - event_answer_id = fields.Many2one('event.answer', required=True, ondelete='cascade') - event_registration_id = fields.Many2one('event.registration', required=True, ondelete='cascade') - - class EventRegistration(models.Model): """ Store answers on attendees. """ _inherit = 'event.registration' - answer_ids = fields.Many2many('event.answer', 'event_registration_answer', string='Answers') + registration_answer_ids = fields.One2many('event.registration.answer', 'registration_id', string='Attendee Answers') + +class EventRegistrationAnswer(models.Model): + """ Represents the user input answer for a single event.question """ + _name = 'event.registration.answer' + _description = 'Event Registration Answer' + + question_id = fields.Many2one('event.question', required=True, ondelete='cascade') + registration_id = fields.Many2one('event.registration', required=True, ondelete='cascade') + event_id = fields.Many2one('event.event', related='registration_id.event_id') + question_type = fields.Selection(related='question_id.question_type') + value_answer_id = fields.Many2one('event.question.answer', string="Suggested answer") + value_text_box = fields.Text('Text answer') + + _sql_constraints = [ + ('value_check', "CHECK(value_answer_id IS NOT NULL OR COALESCE(value_text_box, '') <> '')", "There must be a suggested value or a text value.") + ] diff --git a/addons/website_event_questions/security/ir.model.access.csv b/addons/website_event_questions/security/ir.model.access.csv index 66d25208471..ea509ea3f85 100644 --- a/addons/website_event_questions/security/ir.model.access.csv +++ b/addons/website_event_questions/security/ir.model.access.csv @@ -1,6 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink event_question_all,event.question.all,model_event_question,,1,0,0,0 event_question_event_user,event.question.event.user,model_event_question,event.group_event_user,1,1,1,1 -event_answer_all,event.answer.all,model_event_answer,,1,0,0,0 -event_answer_event_user,event.answer.event.user,model_event_answer,event.group_event_user,1,1,1,1 -event_registration_answer_all,access_event_registration_answer,model_event_registration_answer,,1,1,1,1 +event_question_answer_all,event.question.answer.all,model_event_question_answer,,1,0,0,0 +event_question_answer_event_user,event.question.answer.event.user,model_event_question_answer,event.group_event_user,1,1,1,1 +event_registration_answer_event_user,event.registration.answer.event.user,model_event_registration_answer,event.group_event_user,1,1,1,1 diff --git a/addons/website_event_questions/tests/common.py b/addons/website_event_questions/tests/common.py index 431eb1c376d..7ad2697a81f 100644 --- a/addons/website_event_questions/tests/common.py +++ b/addons/website_event_questions/tests/common.py @@ -12,6 +12,7 @@ class TestEventQuestionCommon(TestEventCommon): cls.event_question_1 = cls.env['event.question'].create({ 'title': 'Question1', + 'question_type': 'simple_choice', 'event_type_id': cls.event_type_complex.id, 'once_per_order': False, 'answer_ids': [ @@ -21,6 +22,7 @@ class TestEventQuestionCommon(TestEventCommon): }) cls.event_question_2 = cls.env['event.question'].create({ 'title': 'Question2', + 'question_type': 'simple_choice', 'event_type_id': cls.event_type_complex.id, 'once_per_order': True, 'answer_ids': [ @@ -28,4 +30,10 @@ class TestEventQuestionCommon(TestEventCommon): (0, 0, {'name': 'Q2-Answer2'}) ], }) + cls.event_question_3 = cls.env['event.question'].create({ + 'title': 'Question3', + 'question_type': 'text_box', + 'event_type_id': cls.event_type_complex.id, + 'once_per_order': True, + }) cls.event_type_complex.write({'use_questions': True}) diff --git a/addons/website_event_questions/tests/test_event_internals.py b/addons/website_event_questions/tests/test_event_internals.py index d3494cae49c..c2cfe1f40d1 100644 --- a/addons/website_event_questions/tests/test_event_internals.py +++ b/addons/website_event_questions/tests/test_event_internals.py @@ -23,13 +23,18 @@ class TestEventData(TestEventQuestionCommon): 'date_end': FieldsDatetime.to_string(datetime.today() + timedelta(days=15)), }) + self.assertEqual( + event.question_ids.mapped('question_type'), + ['simple_choice', 'simple_choice', 'text_box']) self.assertEqual(event.specific_question_ids.title, 'Question1') self.assertEqual( set(event.specific_question_ids.mapped('answer_ids.name')), set(['Q1-Answer1', 'Q1-Answer2'])) - self.assertEqual(event.general_question_ids.title, 'Question2') + self.assertEqual(len(event.general_question_ids), 2) + self.assertEqual(event.general_question_ids[0].title, 'Question2') + self.assertEqual(event.general_question_ids[1].title, 'Question3') self.assertEqual( - set(event.general_question_ids.mapped('answer_ids.name')), + set(event.general_question_ids[0].mapped('answer_ids.name')), set(['Q2-Answer1', 'Q2-Answer2'])) def test_process_attendees_form(self): @@ -45,20 +50,28 @@ class TestEventData(TestEventQuestionCommon): '1-email': 'pixis@gmail.com', '1-phone': '+32444444444', '1-event_ticket_id': '2', - '1-answer_ids-8': '5', '2-name': 'Geluchat', '2-email': 'geluchat@gmail.com', '2-phone': '+32777777777', '2-event_ticket_id': '3', - '2-answer_ids-8': '9', - '0-answer_ids-3': '7', - '0-answer_ids-4': '1', + 'question_answer-1-%s' % self.event_question_1.id: '5', + 'question_answer-2-%s' % self.event_question_1.id: '9', + 'question_answer-0-%s' % self.event_question_2.id: '7', + 'question_answer-0-%s' % self.event_question_3.id: 'Free Text', } with MockRequest(self.env): registrations = WebsiteEvent()._process_attendees_form(event, form_details) self.assertEqual(registrations, [ - {'name': 'Pixis', 'email': 'pixis@gmail.com', 'phone': '+32444444444', 'event_ticket_id': 2, 'answer_ids': [[4, 5], [4, 7], [4, 1]]}, - {'name': 'Geluchat', 'email': 'geluchat@gmail.com', 'phone': '+32777777777', 'event_ticket_id': 3, 'answer_ids': [[4, 9], [4, 7], [4, 1]]} + {'name': 'Pixis', 'email': 'pixis@gmail.com', 'phone': '+32444444444', 'event_ticket_id': 2, + 'registration_answer_ids': [ + (0, 0, {'question_id': self.event_question_1.id, 'value_answer_id': 5}), + (0, 0, {'question_id': self.event_question_2.id, 'value_answer_id': 7}), + (0, 0, {'question_id': self.event_question_3.id, 'value_text_box': 'Free Text'})]}, + {'name': 'Geluchat', 'email': 'geluchat@gmail.com', 'phone': '+32777777777', 'event_ticket_id': 3, + 'registration_answer_ids': [ + (0, 0, {'question_id': self.event_question_1.id, 'value_answer_id': 9}), + (0, 0, {'question_id': self.event_question_2.id, 'value_answer_id': 7}), + (0, 0, {'question_id': self.event_question_3.id, 'value_text_box': 'Free Text'})]} ]) diff --git a/addons/website_event_questions/views/event_templates.xml b/addons/website_event_questions/views/event_templates.xml index 5b63aaa9321..ef6d1a4c791 100644 --- a/addons/website_event_questions/views/event_templates.xml +++ b/addons/website_event_questions/views/event_templates.xml @@ -7,12 +7,9 @@
-
@@ -21,14 +18,12 @@