[IMP] website_event(_sale): adds new identification questions

This commit changes the way the identification questions (name,
email, phone) are asked when registering to an event. They aren't
hardcoded anymore and can be created per event the same way other
questions can be. They can be set as mandatory or not and the order
can be changed. One can now also ask for the attendee company name.

Task-3056380

closes odoo/odoo#112164

Related: odoo/upgrade#4313
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
Patrick Hoste
2023-05-12 21:50:44 +02:00
parent cc60fcd45c
commit 6b8daa880c
24 changed files with 320 additions and 195 deletions
+6 -6
View File
@@ -11,7 +11,7 @@
<field name="description">Sent automatically to someone after they registered to an event</field>
<field name="body_html" type="html">
<div>
Dear <t t-out="object.name or ''">Oscar Morgan</t>,<br/>
Dear <t t-out="object.name or 'Guest'"/>,<br/>
Thank you for your inquiry.<br/>
Here is your badge for the event <t t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</t>.<br/>
If you have any questions, please let us know.
@@ -50,7 +50,7 @@
<tr><td valign="middle">
<span style="font-size: 10px;">Your registration</span><br/>
<span style="font-size: 20px; font-weight: bold;">
<t t-out="object.name or ''">Oscar Morgan</t>
<t t-out="object.name or 'Guest'"/>
</span>
</td><td valign="middle" align="right">
<t t-if="is_online">
@@ -75,7 +75,7 @@
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<div>
Hello <t t-out="object.name or ''">Oscar Morgan</t>,<br/>
Hello <t t-out="object.name or 'Guest'"/>,<br/>
We are happy to confirm your registration to the event
<t t-if="is_online">
<a t-att-href="object.event_id.website_url" style="color:#875A7B;text-decoration:none;" t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</a>
@@ -83,7 +83,7 @@
<t t-else="">
<strong t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</strong>
</t>
for attendee <t t-out="object.name or ''">Oscar Morgan</t>.
for attendee <t t-out="object.name or 'Guest'"/>.
</div>
<div>
<br />
@@ -282,7 +282,7 @@
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">Your registration</span><br/>
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">Oscar Morgan</span>
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or 'Guest'"/>
</td><td valign="middle" align="right">
<t t-if="is_online">
<a t-attf-href="{{ object.event_id.website_url }}"
@@ -306,7 +306,7 @@
<table width="590" border="0" cellpadding="0" cellspacing="0" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 14px;">
<div>
Hello <t t-out="object.name or ''">Oscar Morgan</t>,<br/>
Hello <t t-out="object.name or 'Guest'"/>,<br/>
We are excited to remind you that the event
<t t-if="is_online">
<a t-att-href="object.event_id.website_url" style="color:#875A7B;text-decoration:none;" t-out="object.event_id.name or ''">OpenWood Collection Online Reveal</a>
+11
View File
@@ -34,6 +34,8 @@ class EventRegistration(models.Model):
email = fields.Char(string='Email', compute='_compute_email', readonly=False, store=True, tracking=11)
phone = fields.Char(string='Phone', compute='_compute_phone', readonly=False, store=True, tracking=12)
mobile = fields.Char(string='Mobile', compute='_compute_mobile', readonly=False, store=True, tracking=13)
company_name = fields.Char(
string='Company Name', compute='_compute_company_name', readonly=False, store=True, tracking=14)
# organization
date_closed = fields.Datetime(
string='Attended Date', compute='_compute_date_closed',
@@ -86,6 +88,15 @@ class EventRegistration(models.Model):
fnames=['mobile']
).get('mobile') or False
@api.depends('partner_id')
def _compute_company_name(self):
for registration in self:
if not registration.company_name and registration.partner_id:
registration.company_name = registration._synchronize_partner_values(
registration.partner_id,
fnames=['company_name']
).get('company_name') or False
@api.depends('state')
def _compute_date_closed(self):
for registration in self:
@@ -16,6 +16,7 @@
<field name="email" optional="show"/>
<field name="phone" optional="show"/>
<field name="mobile" optional="hide"/>
<field name="company_name" optional="hide"/>
<field name="event_id" invisible="context.get('default_event_id')"/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]"/>
<field name="activity_ids" widget="list_activity"/>
@@ -64,6 +65,7 @@
<field name="email"/>
<field name="phone" class="o_force_ltr" widget="phone" options="{'enable_sms': false}"/>
<field name="mobile" class="o_force_ltr" widget="phone"/>
<field name="company_name" placeholder='e.g. "Azure Interior"'/>
</group>
<group string="Event Information" name="event">
<field class="o_text_overflow" name="event_id" attrs="{'readonly': [('state', '!=', 'draft')]}"
@@ -16,29 +16,29 @@ var registerSteps = [{
run: 'click',
}, {
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
trigger: 'form[id="attendee_registration"] .btn[type=submit]',
run: function () {
$("input[name='1-name']").val("Raoulette Poiluchette");
$("input[name='1-phone']").val("0456112233");
$("input[name='1-email']").val("raoulette@example.com");
$("div[name*='Question1'] select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Q2-Answer1')").val());
$("input[name='2-name']").val("Michel Tractopelle");
$("input[name='2-phone']").val("0456332211");
$("input[name='2-email']").val("michel@example.com");
$("div[name*='Question1'] select[name*='question_answer-2']").val($("select[name*='question_answer-2'] option:contains('Q1-Answer1')").val());
$("div[name*='Question2'] select[name*='question_answer-2']").val($("select[name*='question_answer-2'] option:contains('Q2-Answer2')").val());
$("input[name='3-name']").val("Hubert Boitaclous");
$("input[name='3-phone']").val("0456995511");
$("input[name='3-email']").val("hubert@example.com");
$("div[name*='Question1'] select[name*='question_answer-3']").val($("select[name*='question_answer-3'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='question_answer-3']").val($("select[name*='question_answer-3'] option:contains('Q2-Answer2')").val());
$("input[name*='1-name']").val("Raoulette Poiluchette");
$("input[name*='1-phone']").val("0456112233");
$("input[name*='1-email']").val("raoulette@example.com");
$("div[name*='Question1'] select[name*='1-simple_choice']").val($("select[name*='1-simple_choice'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='1-simple_choice']").val($("select[name*='1-simple_choice'] option:contains('Q2-Answer1')").val());
$("input[name*='2-name']").val("Michel Tractopelle");
$("input[name*='2-phone']").val("0456332211");
$("input[name*='2-email']").val("michel@example.com");
$("div[name*='Question1'] select[name*='2-simple_choice']").val($("select[name*='2-simple_choice'] option:contains('Q1-Answer1')").val());
$("div[name*='Question2'] select[name*='2-simple_choice']").val($("select[name*='2-simple_choice'] option:contains('Q2-Answer2')").val());
$("input[name*='3-name']").val("Hubert Boitaclous");
$("input[name*='3-phone']").val("0456995511");
$("input[name*='3-email']").val("hubert@example.com");
$("div[name*='Question1'] select[name*='3-simple_choice']").val($("select[name*='3-simple_choice'] option:contains('Q1-Answer2')").val());
$("div[name*='Question2'] select[name*='3-simple_choice']").val($("select[name*='3-simple_choice'] option:contains('Q2-Answer2')").val());
$("textarea[name*='question_answer']").text("Random answer from random guy");
},
}, {
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']",
trigger: 'button:contains("Continue")',
extra_trigger: "input[name*='1-name'], input[name*='2-name'], input[name*='3-name']",
trigger: 'button[type=submit]',
run: 'click',
}, {
content: "Address filling",
@@ -91,22 +91,22 @@ var registerSteps = [{
run: 'click',
}, {
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
trigger: 'form[id="attendee_registration"] .btn[type=submit]',
run: function () {
$("input[name='1-name']").val("Raoulette Poiluchette");
$("input[name='1-phone']").val("0456112233");
$("input[name='1-email']").val("raoulette@example.com");
$("select[name*='question_answer-1']").val($("select[name*='question_answer-1'] option:contains('Consumers')").val());
$("input[name='2-name']").val("Michel Tractopelle");
$("input[name='2-phone']").val("0456332211");
$("input[name='2-email']").val("michel@example.com");
$("select[name*='question_answer-2']").val($("select[name*='question_answer-1'] option:contains('Research')").val());
$("textarea[name*='question_answer']").text("An unicorn told me about you. I ate it afterwards.");
$("input[name*='1-name']").val("Raoulette Poiluchette");
$("input[name*='1-phone']").val("0456112233");
$("input[name*='1-email']").val("raoulette@example.com");
$("select[name*='1-simple_choice']").val($("select[name*='1-simple_choice'] option:contains('Consumers')").val());
$("input[name*='2-name']").val("Michel Tractopelle");
$("input[name*='2-phone']").val("0456332211");
$("input[name*='2-email']").val("michel@example.com");
$("select[name*='2-simple_choice']").val($("select[name*='1-simple_choice'] option:contains('Research')").val());
$("textarea[name*='text_box']").text("An unicorn told me about you. I ate it afterwards.");
},
}, {
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']",
trigger: 'button:contains("Continue")',
extra_trigger: "input[name*='1-name'], input[name*='2-name'], input[name*='3-name']",
trigger: 'button[type=submit]',
run: 'click',
}, {
trigger: 'div.o_wereg_confirmed_attendees span:contains("Raoulette Poiluchette")',
@@ -128,7 +128,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# no type, website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=449): # tef 398 / com 399
with freeze_time(self.reference_now), self.assertQueryCount(event_user=429): # tef 379 / com 380
self.env.cr._now = self.reference_now # force create_date to check schedulers
# Require for `website_menu` to be visible
# <div name="event_menu_configuration" groups="base.group_no_one">
+47 -39
View File
@@ -1,20 +1,14 @@
# -*- coding: utf-8 -*-
import babel.dates
import pytz
import re
import werkzeug
from ast import literal_eval
from collections import defaultdict
from datetime import datetime, timedelta
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from werkzeug.datastructures import OrderedMultiDict
from werkzeug.exceptions import NotFound
from odoo import fields, http, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website.controllers.main import QueryURL
from odoo.http import request
from odoo.osv import expression
@@ -270,51 +264,65 @@ class WebsiteEventController(http.Controller):
if int(ticket_id) not in event.event_ticket_ids.ids and len(event.event_ticket_ids.ids) > 0:
raise UserError(_("This ticket is not available for sale for this event"))
registrations = {}
global_values = {}
general_answer_ids = []
general_identification_answers = {}
# as we may have several questions populating the same field (e.g: the phone)
# we use this to hold the fields that have already been handled
# goal is to use the answer to the first question of every 'type' (aka name / phone / email / company name)
already_handled_fields_data = {}
for key, value in form_details.items():
if 'question_answer' in key and value:
dummy, registration_index, question_id = key.split('-')
question_sudo = request.env['event.question'].browse(int(question_id))
answer_values = None
if question_sudo.question_type == 'simple_choice':
answer_values = {
'question_id': int(question_id),
'value_answer_id': int(value)
}
elif question_sudo.question_type == 'text_box':
answer_values = {
'question_id': int(question_id),
'value_text_box': value
}
if not value:
continue
if answer_values and not int(registration_index):
general_answer_ids.append((0, 0, answer_values))
elif answer_values:
registrations.setdefault(registration_index, dict())\
.setdefault('registration_answer_ids', list()).append((0, 0, answer_values))
else:
counter, attr_name = key.split('-', 1)
field_name = attr_name.split('-')[0]
key_values = key.split('-')
# Special case for handling event_ticket_id data that holds only 2 values
if len(key_values) == 2:
registration_index, field_name = key_values
if field_name not in registration_fields:
continue
elif isinstance(registration_fields[field_name], (fields.Many2one, fields.Integer)):
value = int(value) or False # 0 is considered as a void many2one aka False
else:
value = value
registrations.setdefault(registration_index, dict())[field_name] = int(value)
continue
if counter == '0':
global_values[attr_name] = value
registration_index, question_type, question_id = key_values
answer_values = None
if question_type == 'simple_choice':
answer_values = {
'question_id': int(question_id),
'value_answer_id': int(value)
}
else:
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.setdefault(registration_index, dict())\
.setdefault('registration_answer_ids', list()).append((0, 0, answer_values))
if question_type in ('name', 'email', 'phone', 'company_name')\
and question_type not in already_handled_fields_data.get(registration_index, []):
if question_type not in registration_fields:
continue
field_name = question_type
already_handled_fields_data.setdefault(registration_index, list()).append(field_name)
if not int(registration_index):
general_identification_answers[field_name] = value
else:
registrations.setdefault(counter, dict())[attr_name] = value
for key, value in global_values.items():
for registration in registrations.values():
registration[key] = value
registrations.setdefault(registration_index, dict())[field_name] = value
if general_answer_ids:
for registration in registrations.values():
registration.setdefault('registration_answer_ids', list()).extend(general_answer_ids)
if general_identification_answers:
for registration in registrations.values():
registration.update(general_identification_answers)
return list(registrations.values())
def _create_attendees_from_registration_post(self, event, registration_data):
+56
View File
@@ -1,5 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Event Type -->
<record id="event.event_type_0" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_type_1" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_type_2" model="event.type">
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<!-- Event Event -->
<record id="event.event_0" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Get Inspired • Stay Connected • Have Fun</field>
@@ -36,6 +60,10 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_1" model="event.event">
@@ -57,6 +85,10 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_2" model="event.event">
@@ -99,6 +131,10 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_3" model="event.event">
@@ -117,12 +153,20 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_4" model="event.event">
<field name="website_published" eval="True"/>
<field name="subtitle">Discover how to grow a sustainable business with our experts.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_4.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_5" model="event.event">
@@ -142,11 +186,19 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_6" model="event.event">
<field name="website_published" eval="False"/>
<field name="cover_properties">{"background-image": "none", "background-color": "secondary", "opacity": ""}</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
<record id="event.event_7" model="event.event">
@@ -169,6 +221,10 @@
</div>
</div>
</field>
<field name="question_ids" eval="[(5, 0, 0),
(0, 0, {'title': 'Name', 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Email', 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': 'Phone', 'question_type': 'phone'})]"/>
</record>
</odoo>
+4 -2
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from collections import defaultdict
from dateutil.relativedelta import relativedelta
import json
import werkzeug.urls
@@ -36,6 +35,9 @@ class Event(models.Model):
})
return res
def _default_question_ids(self):
return self.env['event.type']._default_question_ids()
# description
subtitle = fields.Char('Event Subtitle', translate=True)
# registration
@@ -219,7 +221,7 @@ class Event(models.Model):
questions_tokeep_ids = []
for event in self:
if not event.event_type_id and not event.question_ids:
event.question_ids = False
event.question_ids = self._default_question_ids()
continue
if questions_tokeep_ids:
@@ -14,7 +14,12 @@ class EventQuestion(models.Model):
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)
('text_box', 'Text Input'),
('name', 'Name'),
('email', 'Email'),
('phone', 'Phone'),
('company_name', 'Company'),
], 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.question.answer', 'question_id', "Answers", copy=True)
@@ -12,4 +12,4 @@ class EventRegistration(models.Model):
registration_answer_ids = fields.One2many('event.registration.answer', 'registration_id', string='Attendee Answers')
def _get_website_registration_allowed_fields(self):
return {'name', 'phone', 'email', 'mobile', 'event_id', 'partner_id', 'event_ticket_id'}
return {'name', 'phone', 'email', 'mobile', 'company_name', 'event_id', 'partner_id', 'event_ticket_id'}
+9 -2
View File
@@ -1,13 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
class EventType(models.Model):
_name = 'event.type'
_inherit = ['event.type']
def _default_question_ids(self):
return [
(0, 0, {'title': _('Name'), 'question_type': 'name', 'is_mandatory_answer': True}),
(0, 0, {'title': _('Email'), 'question_type': 'email', 'is_mandatory_answer': True}),
(0, 0, {'title': _('Phone'), 'question_type': 'phone'}),
]
website_menu = fields.Boolean('Display a dedicated menu on Website')
community_menu = fields.Boolean(
"Community Menu", compute="_compute_community_menu",
@@ -17,7 +24,7 @@ class EventType(models.Model):
'Extra Register Button', compute='_compute_menu_register_cta',
readonly=False, store=True)
question_ids = fields.One2many(
'event.question', 'event_type_id',
'event.question', 'event_type_id', default=_default_question_ids,
string='Questions', copy=True)
@api.depends('website_menu')
@@ -25,10 +25,10 @@ registry.category("web_tour.tours").add('test_tickets_questions', {
trigger: 'div:contains("Ticket #1").modal-body input[name*="phone"]',
run: 'text +32499123456'
}, {
trigger: 'div:contains("Ticket #1").modal-body select[name*="question_answer"]',
trigger: 'div:contains("Ticket #1").modal-body select[name*="1-simple_choice"]',
run: 'text Vegetarian'
}, {
trigger: 'div:contains("Ticket #1").modal-body textarea[name*="question_answer"]',
trigger: 'div:contains("Ticket #1").modal-body textarea[name*="1-text_box"]',
run: 'text Fish and Nuts'
}, {
trigger: 'div:contains("Ticket #2").modal-body input[name*="name"]',
@@ -37,13 +37,16 @@ registry.category("web_tour.tours").add('test_tickets_questions', {
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"]',
trigger: 'div:contains("Ticket #2").modal-body input[name*="company_name"]',
run: 'text My Company'
}, {
trigger: 'div:contains("Ticket #2").modal-body select[name*="2-simple_choice"]',
run: 'text Pastafarian'
}, {
trigger: 'div.o_wevent_registration_question_global select[name*="question_answer"]',
trigger: 'div.o_wevent_registration_question_global select[name*="0-simple_choice"]',
run: 'text A friend'
}, {
trigger: 'button:contains("Continue")',
trigger: 'button[type=submit]',
run: 'click'
}, {
// The tour stops too early and the registration fails if we don't wait the confirmation.
-3
View File
@@ -2,12 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta, time
from unittest.mock import patch
from odoo.addons.event.tests.common import EventCase
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate
from odoo.tests.common import TransactionCase
class OnlineEventCase(EventCase):
@@ -25,10 +25,15 @@ class TestEventData(TestEventQuestionCommon):
self.assertEqual(
event.question_ids.mapped('question_type'),
['simple_choice', 'simple_choice', 'text_box'])
self.assertEqual(event.specific_question_ids.title, 'Question1')
['name', 'email', 'phone', 'simple_choice', 'simple_choice', 'text_box'])
self.assertEqual(event.specific_question_ids.filtered(
lambda q: q.question_type in ['simple_choice', 'text_box']).title, 'Question1')
self.assertEqual(event.specific_question_ids.filtered(
lambda q: q.question_type in ['name', 'email', 'phone', 'company_name'])
.mapped('title'), ['Name', 'Email', 'Phone'])
self.assertEqual(
set(event.specific_question_ids.mapped('answer_ids.name')),
set(event.specific_question_ids.filtered(
lambda q: q.question_type in ['simple_choice', 'text_box']).mapped('answer_ids.name')),
set(['Q1-Answer1', 'Q1-Answer2']))
self.assertEqual(len(event.general_question_ids), 2)
self.assertEqual(event.general_question_ids[0].title, 'Question2')
@@ -55,19 +60,34 @@ class TestEventData(TestEventQuestionCommon):
'seats_max': 200,
}])
[name_question, email_question, phone_question] = event.question_ids.filtered(
lambda q: q.question_type in ('name', 'email', 'phone', 'company_name'))
[second_phone_question, company_name_question] = self.env['event.question'].create([{
'title': 'Second Phone',
'question_type': 'phone',
'event_id': event.id,
}, {
'title': 'Company Name',
'question_type': 'company_name',
'event_id': event.id,
}])
form_details = {
'1-name': 'Pixis',
'1-email': 'pixis@gmail.com',
'1-phone': '+32444444444',
'1-name-%s' % name_question.id: 'Pixis',
'1-email-%s' % email_question.id: 'pixis@gmail.com',
'1-phone-%s' % phone_question.id: '+32444444444',
'1-phone-%s' % second_phone_question.id: '+32555555555',
'1-event_ticket_id': ticket_id_1.id,
'2-name': 'Geluchat',
'2-email': 'geluchat@gmail.com',
'2-phone': '+32777777777',
'2-name-%s' % name_question.id: 'Geluchat',
'2-email-%s' % email_question.id: 'geluchat@gmail.com',
'2-phone-%s' % phone_question.id: '+32777777777',
'2-company_name-%s' % company_name_question.id: 'My Company',
'2-event_ticket_id': ticket_id_2.id,
'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',
'1-simple_choice-%s' % self.event_question_1.id: '5',
'2-simple_choice-%s' % self.event_question_1.id: '9',
'0-simple_choice-%s' % self.event_question_2.id: '7',
'0-text_box-%s' % self.event_question_3.id: 'Free Text',
}
with MockRequest(self.env):
@@ -76,11 +96,20 @@ class TestEventData(TestEventQuestionCommon):
self.assertEqual(registrations, [
{'name': 'Pixis', 'email': 'pixis@gmail.com', 'phone': '+32444444444', 'event_ticket_id': ticket_id_1.id,
'registration_answer_ids': [
(0, 0, {'question_id': name_question.id, 'value_text_box': 'Pixis'}),
(0, 0, {'question_id': email_question.id, 'value_text_box': 'pixis@gmail.com'}),
(0, 0, {'question_id': phone_question.id, 'value_text_box': '+32444444444'}),
(0, 0, {'question_id': second_phone_question.id, 'value_text_box': '+32555555555'}),
(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': ticket_id_2.id,
{'name': 'Geluchat', 'email': 'geluchat@gmail.com', 'phone': '+32777777777', 'company_name': 'My Company',
'event_ticket_id': ticket_id_2.id,
'registration_answer_ids': [
(0, 0, {'question_id': name_question.id, 'value_text_box': 'Geluchat'}),
(0, 0, {'question_id': email_question.id, 'value_text_box': 'geluchat@gmail.com'}),
(0, 0, {'question_id': phone_question.id, 'value_text_box': '+32777777777'}),
(0, 0, {'question_id': company_name_question.id, 'value_text_box': 'My Company'}),
(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'})]}
@@ -48,6 +48,18 @@ class TestUi(HttpCaseWithUserDemo):
})],
'website_published': True,
'question_ids': [(0, 0, {
'title': 'Name',
'question_type': 'name',
}), (0, 0, {
'title': 'Email',
'question_type': 'email',
}), (0, 0, {
'title': 'Phone',
'question_type': 'phone',
}), (0, 0, {
'title': 'Company Name',
'question_type': 'company_name',
}), (0, 0, {
'title': 'Meal Type',
'question_type': 'simple_choice',
'answer_ids': [
@@ -81,12 +93,13 @@ class TestUi(HttpCaseWithUserDemo):
self.assertEqual(first_registration.name, 'Attendee A')
self.assertEqual(first_registration.phone, '+32499123456')
self.assertEqual(second_registration.name, 'Attendee B')
self.assertEqual(second_registration.company_name, 'My Company')
event_questions = registrations.mapped('event_id.question_ids')
self.assertEqual(len(event_questions), 3)
self.assertEqual(len(event_questions), 7)
first_registration_answers = first_registration.registration_answer_ids
self.assertEqual(len(first_registration_answers), 3)
self.assertEqual(len(first_registration_answers), 6)
self.assertEqual(first_registration_answers.filtered(
lambda answer: answer.question_id.title == 'Meal Type'
@@ -101,7 +114,7 @@ class TestUi(HttpCaseWithUserDemo):
).value_answer_id.name, 'A friend')
second_registration_answers = second_registration.registration_answer_ids
self.assertEqual(len(second_registration_answers), 2)
self.assertEqual(len(second_registration_answers), 5)
self.assertEqual(second_registration_answers.filtered(
lambda answer: answer.question_id.title == 'Meal Type'
@@ -39,7 +39,8 @@
<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="p-0" icon="fa-bar-chart pe-1" string="Stats" title="Answer Breakdown"/>
<button name="action_view_question_answers" type="object" class="p-0" icon="fa-bar-chart pe-1" string="Stats"
title="Answer Breakdown" attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'text_box'])]}"/>
</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.
@@ -50,7 +51,7 @@
<group>
<group>
<field name="is_mandatory_answer"/>
<field name="question_type" widget="radio" options="{'horizontal': true}"/>
<field name="question_type" widget="radio"/>
</group>
<group>
<field name="once_per_order"/>
@@ -10,7 +10,7 @@
<group>
<group>
<field name="is_mandatory_answer"/>
<field name="question_type" widget="radio" options="{'horizontal': true}"/>
<field name="question_type" widget="radio"/>
</group>
<group>
<field name="once_per_order"/>
@@ -31,7 +31,7 @@
<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')]}" />
<field name="value_text_box" attrs="{'invisible': [('question_type', '=', 'simple_choice')]}" />
</tree>
</field>
</page>
@@ -281,65 +281,47 @@
<div class="modal-content">
<div class="modal-header align-items-center">
<h4 class="modal-title">Attendees</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
</div>
<t t-set="counter_type" t-value="1"/>
<t t-set="counter" t-value="0"/>
<t t-foreach="tickets" t-as="ticket" t-if="availability_check">
<t t-set="input_type_by_question_type" t-value="{'name': 'text', 'email': 'email', 'phone': 'tel', 'company_name': 'text'}"/>
<t t-if="availability_check" t-foreach="tickets" t-as="ticket">
<t t-foreach="range(1, ticket['quantity'] + 1)" t-as="att_counter" name="attendee_loop">
<t t-set="counter" t-value="counter + 1"/>
<div class="modal-body border-bottom">
<h5 class="mt-1 pb-2 border-bottom">Ticket #<span t-out="counter"/> <small class="text-muted">- <span t-out="ticket['name']"/></small></h5>
<div class="row">
<div class="col-lg my-2">
<label>Name *</label>
<input class="form-control" type="text" t-attf-name="#{counter}-name" required="This field is required"
t-att-value="default_first_attendee.get('name', '') if counter == 1 else ''"/>
</div>
<div class="col-lg my-2">
<label>Email *</label>
<input class="form-control" type="email" t-attf-name="#{counter}-email" required="This field is required"
t-att-value="default_first_attendee.get('email', '') if counter == 1 else ''"/>
</div>
<div class="col-lg my-2">
<label>Phone <small>(Optional)</small></label>
<input class="form-control" type="tel" t-attf-name="#{counter}-phone"
t-att-value="default_first_attendee.get('phone', '') if counter == 1 else ''"/>
</div>
<input class="d-none" type="text" t-attf-name="#{counter}-event_ticket_id" t-attf-value="#{ticket['id']}"/>
</div>
<t t-if="event.specific_question_ids">
<div t-foreach="event.specific_question_ids" t-as="question" class="row" t-att-name="question.title">
<div class="col-lg-6 mt-2">
<div class="modal-body">
<h5 t-attf-class="mt-1 pb-2 #{'border-bottom' if event.question_ids else ''}">Ticket #<span t-out="counter"/> <small class="text-muted">- <span t-out="ticket['name']"/></small></h5>
<div t-if="event.specific_question_ids" class="row">
<t t-foreach="event.specific_question_ids" t-as="question">
<div class="col-lg-12 mt-2">
<t t-call="website_event.registration_event_question">
<t t-set="registration_index" t-value="counter"/>
</t>
</div>
</div>
</t>
</t>
</div>
<input class="d-none" type="text" t-attf-name="#{counter}-event_ticket_id" t-attf-value="#{ticket['id']}"/>
</div>
</t>
<t t-set="counter_type" t-value="counter_type + 1"/>
</t>
<t t-if="not availability_check">
<div class="modal-body border-bottom">
<strong> You ordered more tickets than available seats</strong>
</div>
</t>
<t t-if="event.general_question_ids">
<div class="modal-body border-bottom o_wevent_registration_question_global">
<div t-foreach="event.general_question_ids" t-as="question" class="row" t-att-name="question.title">
<div class="col-lg-6 mt-2">
<div t-if="availability_check and event.general_question_ids" class="modal-body border-top o_wevent_registration_question_global">
<div class="row">
<t t-foreach="event.general_question_ids" t-as="question">
<div class="col-lg-12 mt-2">
<t t-call="website_event.registration_event_question">
<t t-set="registration_index" t-value="0"/>
</t>
</div>
</div>
</t>
</div>
</div>
<t t-elif="not availability_check">
<div class="modal-body border-bottom">
<strong> You ordered more tickets than available seats</strong>
</div>
</t>
<div class="modal-footer border-0 justify-content-between">
<div class="modal-footer border-top justify-content-between">
<button type="button" class="btn btn-secondary js_goto_event" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" t-if="availability_check">Continue</button>
<button type="submit" class="btn btn-primary" t-if="availability_check">Confirm Registration</button>
</div>
</div>
</form>
@@ -350,18 +332,23 @@
<template id="registration_event_question" name="Registration Event Question">
<label t-out="question.title"/>
<span t-if="question.is_mandatory_answer">*</span>
<t t-if="question.question_type == 'simple_choice'">
<select t-attf-name="question_answer-#{registration_index}-#{question.id}" class="form-select"
t-att-required="question.is_mandatory_answer">
<t t-if="question.question_type in ['name', 'email', 'phone', 'company_name']">
<input class="form-control" t-att-type="input_type_by_question_type[question.question_type]"
t-attf-name="#{registration_index}-#{question.question_type}-#{question.id}"
t-att-required="question.is_mandatory_answer" t-att-value="default_first_attendee.get(question.question_type, '') if counter in (0,1) else ''"/>
</t>
<t t-elif="question.question_type == 'simple_choice'">
<select t-attf-name="#{registration_index}-#{question.question_type}-#{question.id}"
class="form-select" t-att-required="question.is_mandatory_answer">
<option value=""/>
<t t-foreach="question.answer_ids" t-as="answer">
<option t-out="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-att-required="question.is_mandatory_answer"/>
<t t-else="">
<textarea t-attf-name="#{registration_index}-#{question.question_type}-#{question.id}"
class="col-lg-12 form-control" t-att-required="question.is_mandatory_answer"/>
</t>
</template>
@@ -377,21 +364,19 @@
<div class="row mb-3 o_wereg_confirmed_attendees">
<div class="col-md-4 col-xs-12 mt-3" t-foreach="attendees" t-as="attendee">
<div class="d-flex flex-column">
<span class="fw-bold text-truncate">
<t t-if="attendee.name" t-out="attendee.name"/>
<t t-else="">N/A</t>
<span t-if="attendee.name" class="fw-bold text-truncate">
<t t-out="attendee.name"/>
</span>
<span class="text-truncate">
<i class="fa fa-envelope me-2 "></i>
<t t-if="attendee.email" t-out="attendee.email"/>
<t t-else="">N/A</t>
<span t-if="attendee.email" class="text-truncate">
<i class="fa fa-envelope me-2"/>
<t t-out="attendee.email"/>
</span>
<span t-if="attendee.phone">
<i class="fa fa-phone me-2"></i>
<i class="fa fa-phone me-2"/>
<t t-out="attendee.phone"/>
</span>
<span>
<i class="fa fa-ticket me-2"></i>
<i class="fa fa-ticket me-2"/>
<t t-if="attendee.event_ticket_id">
<t t-out="attendee.event_ticket_id.name"/> (Ref: <t t-out="attendee.id"/>)
</t>
@@ -29,23 +29,23 @@ registry.category("web_tour.tours").add('event_buy_tickets', {
},
{
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
trigger: 'form[id="attendee_registration"] .btn[type=submit]',
run: function () {
$("input[name='1-name']").val("Att1");
$("input[name='1-phone']").val("111 111");
$("input[name='1-email']").val("att1@example.com");
$("input[name='2-name']").val("Att2");
$("input[name='2-phone']").val("222 222");
$("input[name='2-email']").val("att2@example.com");
$("input[name='3-name']").val("Att3");
$("input[name='3-phone']").val("333 333");
$("input[name='3-email']").val("att3@example.com");
$("input[name*='1-name']").val("Att1");
$("input[name*='1-phone']").val("111 111");
$("input[name*='1-email']").val("att1@example.com");
$("input[name*='2-name']").val("Att2");
$("input[name*='2-phone']").val("222 222");
$("input[name*='2-email']").val("att2@example.com");
$("input[name*='3-name']").val("Att3");
$("input[name*='3-phone']").val("333 333");
$("input[name*='3-email']").val("att3@example.com");
},
},
{
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']",
trigger: 'button:contains("Continue")',
extra_trigger: "input[name*='1-name'], input[name*='2-name'], input[name*='3-name']",
trigger: 'button[type=submit]',
},
{
content: "Check that the cart contains exactly 3 triggers",
@@ -26,20 +26,20 @@ registry.category("web_tour.tours").add('event_buy_last_ticket', {
},
{
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
trigger: 'form[id="attendee_registration"] .btn[type=submit]',
run: function () {
$("input[name='1-name']").val("Att1");
$("input[name='1-phone']").val("111 111");
$("input[name='1-email']").val("att1@example.com");
$("input[name='2-name']").val("Att2");
$("input[name='2-phone']").val("222 222");
$("input[name='2-email']").val("att2@example.com");
$("input[name*='1-name']").val("Att1");
$("input[name*='1-phone']").val("111 111");
$("input[name*='1-email']").val("att1@example.com");
$("input[name*='2-name']").val("Att2");
$("input[name*='2-phone']").val("222 222");
$("input[name*='2-email']").val("att2@example.com");
},
},
{
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name']",
trigger: 'button:contains("Continue")',
extra_trigger: "input[name*='1-name'], input[name*='2-name']",
trigger: 'button[type=submit]',
},
{
content: "Fill address",
@@ -18,17 +18,17 @@
},
{
content: "Fill attendees details",
trigger: 'form[id="attendee_registration"] .btn:contains("Continue")',
trigger: 'form[id="attendee_registration"] .btn[type=submit]',
run: function () {
$("input[name='1-name']").val("Great Name");
$("input[name='1-phone']").val("111 111");
$("input[name='1-email']").val("great@name.com");
$("input[name*='1-name']").val("Great Name");
$("input[name*='1-phone']").val("111 111");
$("input[name*='1-email']").val("great@name.com");
},
},
{
content: "Validate attendees details",
extra_trigger: "input[name='1-name'], input[name='2-name']",
trigger: 'button:contains("Continue")',
extra_trigger: "input[name*='1-name'], input[name*='2-name']",
trigger: 'button[type=submit]',
},
...getPriceListChecksSteps({
pricelistName: "EUR With Discount Included",
@@ -88,7 +88,13 @@
<span class="fa fa-sign-in"/>
</a>
</t>
<t t-else="">$0</t>
<t t-else="">
<t t-set="has_paying_ticket" t-value="any(ticket.get('price', 0) > 0 for ticket in tickets)"/>
<button t-if="availability_check" type="submit" class="btn btn-primary">
<span t-if="has_paying_ticket">Go to Payment</span>
<span t-else="">Confirm Registration</span>
</button>
</t>
</xpath>
</template>