[MERGE][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?"

Rework existing reporting on Questions to ease implementation and give more
meaningful answers analysis.

SPECS

**Free Text Questions**

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.

**Questions Reporting**

Based on latest testing, the reporting on event questions was completely
unusable if you have more than a hundred questions (all events combined) and
needs to be refactored based on new specifications.

We dropped the current reporting and re-introduced a new one after the commits
that let us handled free text questions.

The new reporting allow users to conveniently analyze attendees answers on a
per-question basis. It shows:
- A graph view showing counts of each suggestions for simple_choice questions
- A pivot view showing registrations vs answers for simple_choice questions
- A tree view showing textual answers values for text_box questions

FIXIMP

When changing the event.type, configured event questions that have answers
(type 'simple_choice') new have their answers properly copied onto the event.

LINKS

Upgrade PR odoo/upgrade#799
Task ID 2189529

closes odoo/odoo#45497

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-03-31 14:03:14 +02:00
committed by GitHub
23 changed files with 559 additions and 283 deletions
@@ -3,5 +3,4 @@
from . import controllers
from . import models
from . import report
from . import tests
@@ -4,12 +4,15 @@
'name': 'Questions on Events',
'description': 'Questions on Events',
'category': 'Marketing',
'version': '1.0',
'version': '1.1',
'depends': ['website_event'],
'data': [
'views/assets.xml',
'views/event_views.xml',
'views/event_registration_answer_views.xml',
'views/event_registration_views.xml',
'views/event_question_views.xml',
'views/event_templates.xml',
'report/report_event_question_view.xml',
'security/ir.model.access.csv',
],
'demo': [
@@ -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
@@ -6,12 +6,12 @@
<field name="once_per_order" eval="False"/>
<field name="event_type_id" ref="event.event_type_data_conference"/>
</record>
<record id="event_type_data_conference_question_0_answer_0" model="event.answer">
<record id="event_type_data_conference_question_0_answer_0" model="event.question.answer">
<field name="name">Yes</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event_questions.event_type_data_conference_question_0"/>
</record>
<record id="event_type_data_conference_question_0_answer_1" model="event.answer">
<record id="event_type_data_conference_question_0_answer_1" model="event.question.answer">
<field name="name">No</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event_questions.event_type_data_conference_question_0"/>
@@ -20,22 +20,50 @@
<!-- EVENT SPECIFIC -->
<record id="event_0_question_0" model="event.question">
<field name="title">Meal Type</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_0_answer_0" model="event.answer">
<record id="event_0_question_0_answer_0" model="event.question.answer">
<field name="name">Mixed</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
</record>
<record id="event_0_question_0_answer_1" model="event.answer">
<record id="event_0_question_0_answer_1" model="event.question.answer">
<field name="name">Vegetarian</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
</record>
<record id="event_0_question_0_answer_2" model="event.answer">
<record id="event_0_question_0_answer_2" model="event.question.answer">
<field name="name">Pastafarian</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event_questions.event_0_question_0"/>
</record>
<record id="event_0_question_1" model="event.question">
<field name="title">Allergies</field>
<field name="question_type">text_box</field>
<field name="once_per_order" eval="False"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_2" model="event.question">
<field name="title">How did you learn about this event?</field>
<field name="question_type">simple_choice</field>
<field name="once_per_order" eval="True"/>
<field name="event_id" ref="event.event_0"/>
</record>
<record id="event_0_question_2_answer_0" model="event.question.answer">
<field name="name">Our website</field>
<field name="sequence">1</field>
<field name="question_id" ref="website_event_questions.event_0_question_2"/>
</record>
<record id="event_0_question_2_answer_1" model="event.question.answer">
<field name="name">Commercials</field>
<field name="sequence">2</field>
<field name="question_id" ref="website_event_questions.event_0_question_2"/>
</record>
<record id="event_0_question_2_answer_2" model="event.question.answer">
<field name="name">A friend</field>
<field name="sequence">3</field>
<field name="question_id" ref="website_event_questions.event_0_question_2"/>
</record>
</data></odoo>
@@ -1,18 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="event.event_registration_0_0" model="event.registration">
<field name="answer_ids" eval="[
(5, 0),
(4, ref('website_event_questions.event_0_question_0_answer_0'))]"/>
<odoo>
<data>
<record id="event_registration_0_0_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_0_answer_0" />
<field name="question_id" ref="website_event_questions.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
<record id="event.event_registration_0_1" model="event.registration">
<field name="answer_ids" eval="[
(5, 0),
(4, ref('website_event_questions.event_0_question_0_answer_1'))]"/>
<record id="event_registration_0_0_registration_answer_1" model="event.registration.answer">
<field name="value_text_box">Fish
Nuts</field>
<field name="question_id" ref="website_event_questions.event_0_question_1" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
<record id="event.event_registration_0_2" model="event.registration">
<field name="answer_ids" eval="[
(5, 0),
(4, ref('website_event_questions.event_0_question_0_answer_2'))]"/>
<record id="event_registration_0_0_registration_answer_2" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_2_answer_0" />
<field name="question_id" ref="website_event_questions.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_0" />
</record>
</data></odoo>
<record id="event_registration_0_1_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_0_answer_1" />
<field name="question_id" ref="website_event_questions.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_1" />
</record>
<record id="event_registration_0_1_registration_answer_1" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_2_answer_0" />
<field name="question_id" ref="website_event_questions.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_1" />
</record>
<record id="event_registration_0_2_registration_answer_0" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_0_answer_2" />
<field name="question_id" ref="website_event_questions.event_0_question_0" />
<field name="registration_id" ref="event.event_registration_0_2" />
</record>
<record id="event_registration_0_2_registration_answer_1" model="event.registration.answer">
<field name="value_answer_id" ref="website_event_questions.event_0_question_2_answer_2" />
<field name="question_id" ref="website_event_questions.event_0_question_2" />
<field name="registration_id" ref="event.event_registration_0_2" />
</record>
</data>
</odoo>
@@ -33,8 +33,13 @@ 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,
'answer_ids': [(0, 0, {
'name': answer.name,
'sequence': answer.sequence
}) for answer in question.answer_ids],
})
for question in event.event_type_id.question_ids
]
@@ -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."
@@ -25,23 +28,37 @@ class EventQuestion(models.Model):
if any(question.event_type_id and question.event_id for question in self):
raise UserError(_('Question cannot belong to both the event category and itself.'))
@api.model
def create(self, vals):
event_id = vals.get('event_id', False)
if event_id:
event = self.env['event.event'].browse([event_id])
if event.event_type_id.use_questions and event.event_type_id.question_ids:
vals['answer_ids'] = vals.get('answer_ids', []) + [(0, 0, {
'name': answer.name,
'sequence': answer.sequence,
}) 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). """
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 EventAnswer(models.Model):
_name = 'event.answer'
def action_view_question_answers(self):
""" Allow analyzing the attendees answers to event questions in a convenient way:
- A graph view showing counts of each suggestions for simple_choice questions
(Along with secondary pivot and tree views)
- A tree view showing textual answers values for text_box questions. """
self.ensure_one()
action = self.env.ref('website_event_questions.action_event_registration_report').read()[0]
action['domain'] = [('question_id', '=', self.id)]
if self.question_type == 'simple_choice':
action['views'] = [(False, 'graph'), (False, 'pivot'), (False, 'tree')]
elif self.question_type == 'text_box':
action['views'] = [(False, 'tree')]
return action
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')
@@ -4,22 +4,25 @@
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')
partner_id = fields.Many2one('res.partner', related='registration_id.partner_id')
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.")
]
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import report_event_registrations_questions
@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Graph view for question analysis-->
<record model="ir.ui.view" id="view_event_question_report_graph">
<field name="name">event.question.report.graph</field>
<field name="model">event.question.report</field>
<field name="arch" type="xml">
<graph string="Questions Analysis">
<field name="answer_id"/>
<field name="question_id"/>
</graph>
</field>
</record>
<!-- Pivot view for question analysis -->
<record model="ir.ui.view" id="view_event_question_report_pivot">
<field name="name">event.question.report.pivot</field>
<field name="model">event.question.report</field>
<field name="arch" type="xml">
<pivot string="Questions Analysis" disable_linking="True">
<field name="event_id" type="row"/>
<field name="attendee_id" type="row"/>
<field name="question_id" type="col"/>
<field name="answer_id" type="col"/>
</pivot>
</field>
</record>
<!-- Seach view for question analysis-->
<record model="ir.ui.view" id="view_event_question_report_search">
<field name="name">event.question.report.search</field>
<field name="model">event.question.report</field>
<field name="arch" type="xml">
<search string="Questions Analysis">
<field name="event_id" string="Event"/>
<field name="attendee_id" string="Registration"/>
<group expand="1" string="Group By">
<filter string="Event" name="group_by_event" context="{'group_by': 'event_id'}" />
<filter string="Question" name="group_by_question" context="{'group_by': 'question_id'}"/>
<filter string="Answer" name="group_by_answer" context="{'group_by': 'answer_id'}" />
<filter name="exclude_cancelled"
string="Exclude cancelled registrations"
domain="[('attendee_id.state', '!=', 'cancel')]" />
</group>
</search>
</field>
</record>
<!-- Action for reporting -->
<record model="ir.actions.act_window" id="action_event_question_report">
<field name="name">Questions Analysis</field>
<field name="res_model">event.question.report</field>
<field name="view_mode">graph,pivot</field>
</record>
<!-- Menu -->
<menuitem name="Questions" id="menu_report_event_questions"
parent="event.menu_reporting_events" action="action_event_question_report" sequence="4"/>
</data>
</odoo>
@@ -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
)""")
@@ -1,7 +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_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
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 event_question_all event.question.all model_event_question 1 0 0 0
3 event_question_event_user event.question.event.user model_event_question event.group_event_user 1 1 1 1
4 event_answer_all event_question_answer_all event.answer.all event.question.answer.all model_event_answer model_event_question_answer 1 0 0 0
5 event_answer_event_user event_question_answer_event_user event.answer.event.user event.question.answer.event.user model_event_answer model_event_question_answer event.group_event_user 1 1 1 1
6 event_question_report_all event_registration_answer_event_user event.question.report.all event.registration.answer.event.user model_event_question_report model_event_registration_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
@@ -0,0 +1,54 @@
odoo.define('website_event_questions.tour_test_tickets_questions', function (require) {
'use strict';
var tour = require('web_tour.tour');
tour.register('test_tickets_questions', {
test: true,
url: '/event'
}, [{
content: "Click on the Design Fair event",
trigger: 'article:contains("Design Fair New York")',
}, {
content: "Click on Buy tickets button",
trigger: 'a:contains("Tickets")',
}, {
content: "Select 2 'Free' tickets to buy",
trigger: 'div.row:contains("Free") select.custom-select',
run: 'text 2'
}, {
content: "Click on Register tickets button",
trigger: 'button:contains("Register")',
run: 'click'
}, {
trigger: 'div:contains("Ticket #1").modal-body input[name*="name"]',
run: 'text Attendee A'
}, {
trigger: 'div:contains("Ticket #1").modal-body input[name*="email"]',
run: 'text attendee-a@gmail.com'
}, {
trigger: 'div:contains("Ticket #1").modal-body input[name*="phone"]',
run: 'text +32499123456'
}, {
trigger: 'div:contains("Ticket #1").modal-body select[name*="question_answer"]',
run: 'text Vegetarian'
}, {
trigger: 'div:contains("Ticket #1").modal-body textarea[name*="question_answer"]',
run: 'text Fish and Nuts'
}, {
trigger: 'div:contains("Ticket #2").modal-body input[name*="name"]',
run: 'text Attendee B'
}, {
trigger: 'div:contains("Ticket #2").modal-body input[name*="email"]',
run: 'text attendee-b@gmail.com'
}, {
trigger: 'div:contains("Ticket #2").modal-body select[name*="question_answer"]',
run: 'text Pastafarian'
}, {
trigger: 'div:contains("General questions").modal-body select[name*="question_answer"]',
run: 'text A friend'
}, {
trigger: 'button:contains("Continue")',
run: 'click'
}]);
});
@@ -3,3 +3,4 @@
from . import common
from . import test_event_internals
from . import test_event_ui
@@ -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})
@@ -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'})]}
])
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields, tests
@tests.tagged('post_install', '-at_install')
class TestUi(tests.HttpCase):
def test_01_tickets_questions(self):
""" Will execute the tour that fills up two tickets with a few questions answers
and then assert that the answers are correctly saved for each attendee. """
self.design_fair_event = self.env['event.event'].create({
'name': 'Design Fair New York',
'date_begin': fields.Datetime.now() - relativedelta(days=15),
'date_end': fields.Datetime.now() + relativedelta(days=15),
'event_ticket_ids': [(0, 0, {
'name': 'Free',
'start_sale_date': fields.Datetime.now() - relativedelta(days=15)
}), (0, 0, {
'name': 'Other',
'start_sale_date': fields.Datetime.now() - relativedelta(days=15)
})],
'website_published': True,
'question_ids': [(0, 0, {
'title': 'Meal Type',
'question_type': 'simple_choice',
'answer_ids': [
(0, 0, {'name': 'Mixed'}),
(0, 0, {'name': 'Vegetarian'}),
(0, 0, {'name': 'Pastafarian'})
]
}), (0, 0, {
'title': 'Allergies',
'question_type': 'text_box'
}), (0, 0, {
'title': 'How did you learn about this event?',
'question_type': 'simple_choice',
'once_per_order': True,
'answer_ids': [
(0, 0, {'name': 'Our website'}),
(0, 0, {'name': 'Commercials'}),
(0, 0, {'name': 'A friend'})
]
})]
})
self.start_tour("/", 'test_tickets_questions', login="portal")
registrations = self.env['event.registration'].search([
('email', 'in', ['attendee-a@gmail.com', 'attendee-b@gmail.com'])
])
self.assertEqual(len(registrations), 2)
first_registration = registrations.filtered(lambda reg: reg.email == 'attendee-a@gmail.com')
second_registration = registrations.filtered(lambda reg: reg.email == 'attendee-b@gmail.com')
self.assertEqual(first_registration.name, 'Attendee A')
self.assertEqual(first_registration.phone, '+32499123456')
self.assertEqual(second_registration.name, 'Attendee B')
event_questions = registrations.mapped('event_id.question_ids')
self.assertEqual(len(event_questions), 3)
first_registration_answers = first_registration.registration_answer_ids
self.assertEqual(len(first_registration_answers), 3)
self.assertEqual(first_registration_answers.filtered(
lambda answer: answer.question_id.title == 'Meal Type'
).value_answer_id.name, 'Vegetarian')
self.assertEqual(first_registration_answers.filtered(
lambda answer: answer.question_id.title == 'Allergies'
).value_text_box, 'Fish and Nuts')
self.assertEqual(first_registration_answers.filtered(
lambda answer: answer.question_id.title == 'How did you learn about this event?'
).value_answer_id.name, 'A friend')
second_registration_answers = second_registration.registration_answer_ids
self.assertEqual(len(second_registration_answers), 2)
self.assertEqual(second_registration_answers.filtered(
lambda answer: answer.question_id.title == 'Meal Type'
).value_answer_id.name, 'Pastafarian')
self.assertEqual(first_registration_answers.filtered(
lambda answer: answer.question_id.title == 'How did you learn about this event?'
).value_answer_id.name, 'A friend')
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="assets_tests" name="Website Event Questions Assets Tests" inherit_id="web.assets_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_event_questions/static/tests/tours/tickets_questions.js"></script>
</xpath>
</template>
</data>
</odoo>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_question_view_form" model="ir.ui.view">
<field name="name">event.question.view.form</field>
<field name="model">event.question</field>
<field name="arch" type="xml">
<form string="Question">
<sheet>
<h1><field name="title" /></h1>
<group>
<group>
<div colspan="2">
<field name="once_per_order"/>
<label for="once_per_order"/>
</div>
<field name="question_type" widget="radio" options="{'horizontal': true}" />
</group>
</group>
<notebook attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}">
<page string="Answers" name="answers">
<field name="answer_ids">
<tree editable="bottom">
<!-- 'display_name' is necessary for the many2many_tags to work on the event view -->
<field name="display_name" invisible="1" />
<field name="sequence" widget="handle" />
<field name="name"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_registration_answer_view_search" model="ir.ui.view">
<field name="name">event.registration.answer.view.search</field>
<field name="model">event.registration.answer</field>
<field name="arch" type="xml">
<search>
<field name="value_text_box" />
<field name="value_answer_id" />
<field name="question_id" />
</search>
</field>
</record>
<record id="event_registration_answer_view_tree" model="ir.ui.view">
<field name="name">event.registration.answer.view.tree</field>
<field name="model">event.registration.answer</field>
<field name="arch" type="xml">
<tree string="Answer Breakdown" create="0">
<field name="registration_id" optional="show" />
<field name="partner_id" optional="hide" />
<field name="question_id" optional="show" />
<field name="value_text_box" />
<field name="value_answer_id" string="Selected answer" />
</tree>
</field>
</record>
<record id="event_registration_answer_view_graph" model="ir.ui.view">
<field name="name">event.registration.answer.view.graph</field>
<field name="model">event.registration.answer</field>
<field name="arch" type="xml">
<graph string="Answer Breakdown">
<field name="value_answer_id" />
</graph>
</field>
</record>
<record id="event_registration_answer_view_pivot" model="ir.ui.view">
<field name="name">event.registration.answer.view.pivot</field>
<field name="model">event.registration.answer</field>
<field name="arch" type="xml">
<pivot string="Answer Breakdown">
<field name="registration_id" type="row"/>
<field name="value_answer_id" type="col"/>
</pivot>
</field>
</record>
<record id="action_event_registration_report" model="ir.actions.act_window">
<field name="name">Answer Breakdown</field>
<field name="res_model">event.registration.answer</field>
<field name="view_mode">search,tree,graph,pivot</field>
</record>
</odoo>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_registration_view_form_inherit_question" model="ir.ui.view">
<field name="name">event.registration.view.form.inherit.question</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_form" />
<field name="arch" type="xml">
<sheet position="inside">
<notebook>
<page string="Questions" name="questions">
<field name="registration_answer_ids" widget="one2many">
<tree editable="bottom">
<field name="event_id" invisible="1" />
<field name="question_id" domain="[('event_id', '=', event_id)]" options="{'no_create': True}" />
<field name="question_type" string="Type" />
<field name="value_answer_id"
attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}"
domain="[('question_id', '=', question_id)]" options="{'no_create': True}"/>
<field name="value_text_box" attrs="{'invisible': [('question_type', '!=', 'text_box')]}" />
</tree>
</field>
</page>
</notebook>
</sheet>
</field>
</record>
</odoo>
@@ -7,12 +7,9 @@
<t t-if="event.specific_question_ids">
<div t-foreach="event.specific_question_ids" t-as="question" class="row">
<div class="col-lg-6 mt-2">
<label t-esc="question.title"/>
<select t-attf-name="#{counter}-answer_ids-#{question.id}" class="custom-select" required="true">
<t t-foreach="question.answer_ids" t-as="answer">
<option t-esc="answer.name" t-att-value="answer.id"/>
</t>
</select>
<t t-call="website_event_questions.registration_event_question">
<t t-set="registration_index" t-value="counter"/>
</t>
</div>
</div>
</t>
@@ -21,14 +18,12 @@
<xpath expr="//*[hasclass('modal-footer')]" position="before">
<t t-if="event.general_question_ids">
<div class="modal-body bg-light border-bottom">
<h5>General questions</h5>
<div t-foreach="event.general_question_ids" t-as="question" class="row">
<div class="col-lg-6">
<label class="h5" t-esc="question.title"/>
<select t-attf-name="0-answer_ids-#{question.id}" class="custom-select" required="true">
<t t-foreach="question.answer_ids" t-as="answer">
<option t-esc="answer.name" t-att-value="answer.id"/>
</t>
</select>
<div class="col-lg-6 mt-2">
<t t-call="website_event_questions.registration_event_question">
<t t-set="registration_index" t-value="0"/>
</t>
</div>
</div>
</div>
@@ -36,4 +31,18 @@
</xpath>
</template>
<template id="registration_event_question" name="Registration Event Question">
<label t-esc="question.title"/>
<t t-if="question.question_type == 'simple_choice'">
<select t-attf-name="question_answer-#{registration_index}-#{question.id}" class="custom-select" required="true">
<t t-foreach="question.answer_ids" t-as="answer">
<option t-esc="answer.name" t-att-value="answer.id"/>
</t>
</select>
</t>
<t t-elif="question.question_type == 'text_box'">
<textarea t-attf-name="question_answer-#{registration_index}-#{question.id}" class="col-lg-12 form-control"/>
</t>
</template>
</odoo>
@@ -1,101 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="view_event_question_form">
<field name="name">event.question.form</field>
<field name="model">event.question</field>
<field name="arch" type="xml">
<form string="Question">
<h1><field name="title" /></h1>
<field name="once_per_order"/>
<label for="once_per_order"/>
<field name="answer_ids">
<tree editable="bottom">
<field name="sequence" widget="handle" />
<field name="name"/>
</tree>
</field>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_event_answer_simplified_form">
<field name="name">event.answer.simplified.form</field>
<field name="model">event.answer</field>
<field name="arch" type="xml">
<form string="Question">
<group>
<field name="name"/>
</group>
</form>
</field>
</record>
<record id="event_type_view_form_inherit_question" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.question</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="arch" type="xml">
<div name="event_type_attendees_auto_confirm" position="after">
<div class="col-12 col-lg-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="use_questions"/>
</div>
<div class="o_setting_right_pane">
<label for="use_questions"/>
<div class="row mt16" attrs="{'invisible': [('use_questions', '=', False)]}">
<div class="col-lg-9">
<field name="question_ids"/>
<record id="event_type_view_form_inherit_question" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.question</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="arch" type="xml">
<div name="event_type_attendees_auto_confirm" position="after">
<div class="col-12 col-lg-12 o_setting_box">
<div class="o_setting_left_pane">
<field name="use_questions"/>
</div>
<div class="o_setting_right_pane">
<label for="use_questions"/>
<div class="row mt16" attrs="{'invisible': [('use_questions', '=', False)]}">
<div class="col-lg-9">
<field name="question_ids">
<tree>
<field name="title"/>
<field name="question_type" />
</tree>
</field>
</div>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</field>
</record>
<record model="ir.ui.view" id="view_event_form_inherit_question">
<field name="name">event.event.view.form.inherit.question</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="website_event.event_event_view_form_inherit_website"/>
<field name="arch" type="xml">
<data>
<page name="event_notes" position="before">
<page string="Questions" name="questions">
<field name="question_ids" nolabel="1">
<tree>
<field name="sequence" widget="handle" />
<field name="title"/>
<field name="once_per_order"/>
<field name="answer_ids"/>
</tree>
</field>
<record id="event_event_view_form_inherit_question" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.question</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="website_event.event_event_view_form_inherit_website"/>
<field name="arch" type="xml">
<data>
<page name="event_notes" position="before">
<page string="Questions" name="questions">
<field name="question_ids" nolabel="1">
<tree>
<field name="sequence" widget="handle" />
<field name="title"/>
<field name="once_per_order"/>
<field name="question_type" string="Type" />
<field name="answer_ids" widget="many2many_tags"
attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}" />
<button name="action_view_question_answers" type="object" class="fa fa-bar-chart p-0" title="Answer Breakdown" />
</tree>
<!-- Need to repeat the whole tree form here to be able to create answers properly
Without this, the sub-fields of answer_ids are unknown to the web framework.
We need this because we create questions and answers when the event type changes. -->
<form string="Question">
<sheet>
<h1><field name="title" /></h1>
<group class="mb-0">
<group class="mb-0">
<div colspan="2">
<field name="once_per_order"/>
<label for="once_per_order"/>
</div>
<field name="question_type" widget="radio" options="{'horizontal': true}" />
</group>
</group>
<notebook attrs="{'invisible': [('question_type', '!=', 'simple_choice')]}">
<page string="Answers" name="answers">
<field name="answer_ids">
<tree editable="bottom">
<!-- 'display_name' is necessary for the many2many_tags to work on the event view -->
<field name="display_name" invisible="1" />
<field name="sequence" widget="handle" />
<field name="name"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</page>
</page>
</page>
</data>
</field>
</record>
<record model="ir.ui.view" id="view_event_registration_form_inherit_question">
<field name="name">event.registration.form.inherit.question</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_form" />
<field name="arch" type="xml">
<group name="event" position="inside">
<field name="answer_ids" widget="many2many_tags" domain="[('question_id.event_id', '=', event_id)]" options="{'no_create_edit': True}"/>
</group>
</field>
</record>
<record model="ir.ui.view" id="view_registration_search_inherit_question">
<field name="name">event.registration.search.inherit.question</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_registration_search"/>
<field name="arch" type="xml">
<search position="inside">
<field name="answer_ids" string="Answers"/>
</search>
</field>
</record>
</data>
</field>
</record>
</odoo>