[REF] website_event_track: Track Proposal Form Revamp (GDPR)
Distinguish and display to the speaker which info is collected by the form
for internal use vs external dissemination. The contact details one wants to
share with the event manager (Private details : contact_phone, contact_email)
could indeed be different from the ones shared to attendees (partner_phone,
partner_email,...). The form in both back-end and front-end is made more
detailed, including more fields. Previously the form could have several
speakers, now only one.
Front-End
- Track proposal form changed, now includes tags, contact / speaker info...
- Track name and description are mandatory fields
- User can select (but not create) several track tags from those existing.
This allows categorizing the track in pre-defined categories with format
"tag category : tag name". It requires a select2 widget,
implemented in new file website_event_track_proposal_add_tag.js.
- New tickable section "contact me through different contact" not displayed
if the checkbox is not ticked. If the section is ticked, then the info set
there (contact_name, contact_phone, contact_email) will be added on a new
contact with id partner_id set on the track.
- In both cases (checkbox on/off), if the contact/partner email is the
same as logged user's, uses its partner on form. No contact creation needed.
- Improved / dynamic error display on form submission. If the form is valid,
it resets after submission.
- New widget in website_event_track_proposal.js:
- The partner_name is propagated on contact_name on input but editable.
- When the optional section is checked:
- The contact_name is made required.
- The user must at least enter a contact phone or an email. The email
is normalized but any phone format is accepted, since the choice of
country is not resolved (e.g. geoip not always relevant). Could be
improved with international phone number widget, see COM PR #34725
Back-end: following changes are done to ease contact creation and form completion
from back-end, as well as reaching the speaker from chatter
- On the form:
- If created on the fly from M2O (entering a name and using "create ..."),
the new partner will use default values contact_phone and contact_email.
- Once the contact is set (partner_id), contact_phone and contact_email
are set to readonly since they change according to the partner.
- When setting or changing the partner: this will fill all the form fields
with new available partner data to ease the flow, but only the empty ones
(this prevents losing previously entered information).
If partner is a company, company name is set to the name of partner.
- All fields are editable in the speaker section.
- Exception thrown if no contact mean available when supposed to.
- On the chatter, about contact creation and message subscription:
- From contact creation through suggested contacts.
- If the partner is set but is not in the followers, it is suggested
- If no partner is set, then there is at maximum one suggested contact
from track data: using contact_email if there is one, partner_email
otherwise. This priority serves main commit purpose. The user can
create and edit a corresponding contact if the address is not linked
to a partner. It will then be searched for and set on track.
Modifies tests in test_track_partner_sync. Before, partner fields on track
would be erased by customer's ones. Now, they are updated only if empty.
Contact fields are erased by customer's ones if set. tests changed accordingly.
Task ID - 2329406
COM PR odoo/odoo#60847
UPG PR odoo/upgrade#2346
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
committed by
Thibault Delavallée
parent
e8a5af2e28
commit
d47d64963e
@@ -5,7 +5,7 @@
|
||||
'name': 'Advanced Events',
|
||||
'category': 'Marketing',
|
||||
'summary': 'Sponsors, Tracks, Agenda, Event News',
|
||||
'version': '1.2',
|
||||
'version': '1.3',
|
||||
'description': "",
|
||||
'depends': ['website_event'],
|
||||
'data': [
|
||||
@@ -48,6 +48,8 @@
|
||||
'website_event_track/static/src/scss/event_track_templates_online.scss',
|
||||
'website_event_track/static/src/scss/pwa_frontend.scss',
|
||||
'website_event_track/static/src/js/website_event_track.js',
|
||||
'website_event_track/static/src/js/website_event_track_proposal_form.js',
|
||||
'website_event_track/static/src/js/website_event_track_proposal_form_tags.js',
|
||||
'website_event_track/static/src/js/event_track_reminder.js',
|
||||
'website_event_track/static/src/js/website_event_pwa_widget.js',
|
||||
'website_event_track/static/lib/idb-keyval/idb-keyval.js',
|
||||
|
||||
@@ -11,7 +11,7 @@ import babel.dates
|
||||
import base64
|
||||
import pytz
|
||||
|
||||
from odoo import exceptions, http, fields, _
|
||||
from odoo import exceptions, http, fields, tools, _
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import is_html_empty, plaintext2html
|
||||
@@ -392,30 +392,67 @@ class EventTrackController(http.Controller):
|
||||
|
||||
@http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True)
|
||||
def event_track_proposal_post(self, event, **post):
|
||||
tags = []
|
||||
for tag in event.allowed_track_tag_ids:
|
||||
if post.get('tag_' + str(tag.id)):
|
||||
tags.append(tag.id)
|
||||
if not event.can_access_from_current_website():
|
||||
raise NotFound()
|
||||
|
||||
track = request.env['event.track'].sudo().create({
|
||||
# Only accept existing tag indices. Use search instead of browse + exists:
|
||||
# this prevents users to register colorless tags if not allowed to (ACL).
|
||||
input_tag_indices = [int(tag_id) for tag_id in post['tags'].split(',') if tag_id]
|
||||
valid_tag_indices = request.env['event.track.tag'].search([('id', 'in', input_tag_indices)]).ids
|
||||
|
||||
contact = request.env['res.partner']
|
||||
visitor_partner = request.env['website.visitor']._get_visitor_from_request().partner_id
|
||||
# Contact name is required. Therefore, empty contacts are not considered here. At least one of contact_phone
|
||||
# and contact_email must be filled. Email is verified. If the post tries to create contact with no valid entry,
|
||||
# raise exception. If normalized email is the same as logged partner, use its partner_id on track instead.
|
||||
# This prevents contact duplication. Otherwise, create new contact with contact additional info of post.
|
||||
if post.get('add_contact_information'):
|
||||
valid_contact_email = tools.email_normalize(post.get('contact_email'))
|
||||
# Here, the phone is not formatted. To format it, one needs a country. Based on a country, from geoip for instance.
|
||||
# The problem is that one could propose a track in country A with phone number of country B. Validity is therefore
|
||||
# quite tricky. We accept any format of contact_phone. Could be improved with select country phone widget.
|
||||
if valid_contact_email or post.get('contact_phone'):
|
||||
if visitor_partner and valid_contact_email == visitor_partner.email_normalized:
|
||||
contact = visitor_partner
|
||||
else:
|
||||
contact = request.env['res.partner'].sudo().create({
|
||||
'email': valid_contact_email,
|
||||
'name': post.get('contact_name'),
|
||||
'phone': post.get('contact_phone'),
|
||||
})
|
||||
else:
|
||||
raise exceptions.ValidationError(_("Format Error : please enter a valid contact phone or contact email."))
|
||||
# If the speaker email is the same as logged user's, then also uses its partner on track, same as above.
|
||||
else:
|
||||
valid_speaker_email = tools.email_normalize(post['partner_email'])
|
||||
if visitor_partner and valid_speaker_email == visitor_partner.email_normalized:
|
||||
contact = visitor_partner
|
||||
|
||||
track = request.env['event.track'].with_context({'mail_create_nosubscribe': True}).sudo().create({
|
||||
'name': post['track_name'],
|
||||
'partner_id': contact.id,
|
||||
'partner_name': post['partner_name'],
|
||||
'partner_email': post['email_from'],
|
||||
'partner_phone': post['phone'],
|
||||
'partner_biography': plaintext2html(post['biography']),
|
||||
'partner_email': post['partner_email'],
|
||||
'partner_phone': post['partner_phone'],
|
||||
'partner_function': post['partner_function'],
|
||||
'contact_phone': contact.phone,
|
||||
'contact_email': contact.email,
|
||||
'event_id': event.id,
|
||||
'tag_ids': [(6, 0, tags)],
|
||||
'user_id': False,
|
||||
'tag_ids': [(6, 0, valid_tag_indices)],
|
||||
'description': plaintext2html(post['description']),
|
||||
'image': base64.b64encode(post['image'].read()) if post.get('image') else False
|
||||
'user_id': False,
|
||||
'image': base64.b64encode(post['image'].read()) if post.get('image') else False,
|
||||
})
|
||||
|
||||
if request.env.user != request.website.user_id:
|
||||
track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids)
|
||||
else:
|
||||
partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])])
|
||||
if partner:
|
||||
track.sudo().message_subscribe(partner_ids=partner.ids)
|
||||
return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event})
|
||||
|
||||
return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event, 'main_object': event})
|
||||
|
||||
# ACL : This route is necessary since rpc search_read method in js is not accessible to all users (e.g. public user).
|
||||
@http.route(['''/event/track_tag/search_read'''], type='json', auth="public", website=True)
|
||||
def website_event_track_fetch_tags(self, domain, fields):
|
||||
return request.env['event.track.tag'].search_read(domain, fields)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLS
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="body_html" type="xml">
|
||||
<div>
|
||||
Dear ${object.partner_name or ''}<br/>
|
||||
Dear ${object.partner_id.name or object.partner_name or ''}<br/>
|
||||
We are pleased to inform you that your proposal ${object.name} has been accepted and confirmed for the event ${object.event_id.name}.
|
||||
<br/>
|
||||
You will find more details here:
|
||||
|
||||
@@ -51,30 +51,42 @@ class Track(models.Model):
|
||||
" * Grey is the default situation\n"
|
||||
" * Red indicates something is preventing the progress of this track\n"
|
||||
" * Green indicates the track is ready to be pulled to the next stage")
|
||||
# speaker
|
||||
partner_id = fields.Many2one('res.partner', 'Speaker')
|
||||
partner_id = fields.Many2one('res.partner', 'Contact', help="Contact of the track, may be different from speaker.")
|
||||
# speaker information
|
||||
partner_name = fields.Char(
|
||||
string='Name', compute='_compute_partner_name',
|
||||
readonly=False, store=True, tracking=10)
|
||||
readonly=False, store=True, tracking=10,
|
||||
help='Speaker name is used for public display and may vary from contact name')
|
||||
partner_email = fields.Char(
|
||||
string='Email', compute='_compute_partner_email',
|
||||
readonly=False, store=True, tracking=20)
|
||||
readonly=False, store=True, tracking=20,
|
||||
help='Speaker email is used for public display and may vary from contact email')
|
||||
partner_phone = fields.Char(
|
||||
string='Phone', compute='_compute_partner_phone',
|
||||
readonly=False, store=True, tracking=30)
|
||||
readonly=False, store=True, tracking=30,
|
||||
help='Speaker phone is used for public display and may vary from contact phone')
|
||||
partner_biography = fields.Html(
|
||||
string='Biography', compute='_compute_partner_biography',
|
||||
readonly=False, store=True)
|
||||
partner_function = fields.Char(
|
||||
'Job Position', related='partner_id.function',
|
||||
compute_sudo=True, readonly=True)
|
||||
'Job Position', compute='_compute_partner_function',
|
||||
store=True, readonly=False)
|
||||
partner_company_name = fields.Char(
|
||||
'Company Name', related='partner_id.parent_name',
|
||||
compute_sudo=True, readonly=True)
|
||||
'Company Name', compute='_compute_partner_company_name',
|
||||
readonly=False, store=True)
|
||||
image = fields.Image(
|
||||
string="Speaker Photo", compute="_compute_speaker_image",
|
||||
readonly=False, store=True,
|
||||
max_width=256, max_height=256)
|
||||
# contact information
|
||||
contact_email = fields.Char(
|
||||
string='Contact Email', compute='_compute_contact_email',
|
||||
readonly=False, store=True, tracking=20,
|
||||
help="Contact email is private and used internally")
|
||||
contact_phone = fields.Char(
|
||||
string='Contact Phone', compute='_compute_contact_phone',
|
||||
readonly=False, store=True, tracking=30,
|
||||
help="Contact phone is private and used internally")
|
||||
location_id = fields.Many2one('event.track.location', 'Location')
|
||||
# time information
|
||||
date = fields.Datetime('Track Date')
|
||||
@@ -149,19 +161,19 @@ class Track(models.Model):
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_name(self):
|
||||
for track in self:
|
||||
if not track.partner_name or track.partner_id:
|
||||
if track.partner_id and not track.partner_name:
|
||||
track.partner_name = track.partner_id.name
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_email(self):
|
||||
for track in self:
|
||||
if not track.partner_email or track.partner_id:
|
||||
if track.partner_id and not track.partner_email:
|
||||
track.partner_email = track.partner_id.email
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_phone(self):
|
||||
for track in self:
|
||||
if not track.partner_phone or track.partner_id:
|
||||
if track.partner_id and not track.partner_phone:
|
||||
track.partner_phone = track.partner_id.phone
|
||||
|
||||
@api.depends('partner_id')
|
||||
@@ -173,12 +185,40 @@ class Track(models.Model):
|
||||
not is_html_empty(track.partner_id.website_description):
|
||||
track.partner_biography = track.partner_id.website_description
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_function(self):
|
||||
for track in self:
|
||||
if track.partner_id and not track.partner_function:
|
||||
track.partner_function = track.partner_id.function
|
||||
|
||||
@api.depends('partner_id', 'partner_id.company_type')
|
||||
def _compute_partner_company_name(self):
|
||||
for track in self:
|
||||
if track.partner_id.company_type == 'company':
|
||||
track.partner_company_name = track.partner_id.name
|
||||
elif not track.partner_company_name:
|
||||
track.partner_company_name = track.partner_id.parent_id.name
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_speaker_image(self):
|
||||
for track in self:
|
||||
if not track.image:
|
||||
track.image = track.partner_id.image_256
|
||||
|
||||
# CONTACT
|
||||
|
||||
@api.depends('partner_id', 'partner_id.email')
|
||||
def _compute_contact_email(self):
|
||||
for track in self:
|
||||
if track.partner_id:
|
||||
track.contact_email = track.partner_id.email
|
||||
|
||||
@api.depends('partner_id', 'partner_id.phone')
|
||||
def _compute_contact_phone(self):
|
||||
for track in self:
|
||||
if track.partner_id:
|
||||
track.contact_phone = track.partner_id.phone
|
||||
|
||||
# TIME
|
||||
|
||||
@api.depends('date', 'duration')
|
||||
@@ -342,8 +382,6 @@ class Track(models.Model):
|
||||
stage = self.env['event.track.stage'].browse(vals['stage_id'])
|
||||
self._synchronize_with_stage(stage)
|
||||
res = super(Track, self).write(vals)
|
||||
if vals.get('partner_id'):
|
||||
self.message_subscribe([vals['partner_id']])
|
||||
return res
|
||||
|
||||
@api.model
|
||||
@@ -361,6 +399,48 @@ class Track(models.Model):
|
||||
# MESSAGING
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
return {
|
||||
track.id: {
|
||||
'partner_ids': [],
|
||||
'email_to': track.contact_email or track.partner_email,
|
||||
'email_cc': False
|
||||
} for track in self
|
||||
}
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(Track, self)._message_get_suggested_recipients()
|
||||
for track in self:
|
||||
if track.partner_id:
|
||||
if track.partner_id not in recipients:
|
||||
track._message_add_suggested_recipient(recipients, partner=track.partner_id, reason=_('Contact'))
|
||||
else:
|
||||
# Priority: contact information then speaker information
|
||||
if track.contact_email and track.contact_email != track.partner_id.email:
|
||||
track._message_add_suggested_recipient(recipients, email=track.contact_email, reason=_('Contact Email'))
|
||||
if not track.contact_email and track.partner_email and track.partner_email != track.partner_id.email:
|
||||
track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email'))
|
||||
return recipients
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
# OVERRIDE
|
||||
# If no partner is set on track when sending a message, then we create one from suggested contact selected.
|
||||
# If one or more have been created from chatter (Suggested Recipients) we search for the expected one and write the partner_id on track.
|
||||
if msg_vals.get('partner_ids') and not self.partner_id:
|
||||
# Contact(s) created from chatter set on track : we verify if at least one is the expected contact
|
||||
# linked to the track. (created from contact_email if any, then partner_email if any)
|
||||
main_email = self.contact_email or self.partner_email
|
||||
if main_email:
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == main_email)
|
||||
if new_partner:
|
||||
main_email_string = 'contact_email' if self.contact_email else 'partner_email'
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
(main_email_string, '=', new_partner.email),
|
||||
('stage_id.is_cancel', '=', False),
|
||||
]).write({'partner_id': new_partner.id})
|
||||
return super(Track, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super(Track, self)._track_template(changes)
|
||||
track = self[0]
|
||||
@@ -381,27 +461,6 @@ class Track(models.Model):
|
||||
return self.env.ref('website_event_track.mt_track_ready')
|
||||
return super(Track, self)._track_subtype(init_values)
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(Track, self)._message_get_suggested_recipients()
|
||||
for track in self:
|
||||
if track.partner_email and track.partner_email != track.partner_id.email:
|
||||
track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email'))
|
||||
return recipients
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.partner_email and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email)
|
||||
if new_partner:
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
('partner_email', '=', new_partner.email),
|
||||
('stage_id.is_cancel', '=', False),
|
||||
]).write({'partner_id': new_partner.id})
|
||||
return super(Track, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTION
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
odoo.define('website_event_track.website_event_track_proposal_form', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
publicWidget.registry.websiteEventTrackProposalForm = publicWidget.Widget.extend({
|
||||
|
||||
selector: '.o_website_event_track_proposal_form',
|
||||
events: {
|
||||
'click .o_wetrack_add_contact_information_checkbox': '_onAdvancedContactToggle',
|
||||
'input input[name="partner_name"]': '_onPartnerNameInput',
|
||||
'click .o_wetrack_proposal_submit_button': '_onProposalFormSubmit',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
this.useAdvancedContact = false;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Evaluate and return validity of form input fields:
|
||||
* - 1) error 'invalidFormInputs' : Invalid ones are marked as is-invalid and o_wetrack_input_error.
|
||||
* - 2) error 'noContactMean' : Contact mean fields are marked as is-invalid and contact
|
||||
* section as o_wetrack_no_contact_mean_error if none of them is filled.
|
||||
*
|
||||
* @private
|
||||
* @returns {Boolean} - True if no error remain, false otherwise
|
||||
*/
|
||||
_isFormValid: function () {
|
||||
var formErrors = [];
|
||||
|
||||
// 1) Valid Form Inputs
|
||||
this.$('.form-group').each(function (index, field) {
|
||||
var $field = $(field);
|
||||
// Validate current input, if not select2 field.
|
||||
var inputs = $field.find('.form-control').not('.o_wetrack_select2_tags');
|
||||
var invalidInputs = inputs.toArray().filter(function (input) {
|
||||
return !input.checkValidity();
|
||||
});
|
||||
|
||||
$field.find('.form-control').removeClass('o_wetrack_input_error is-invalid');
|
||||
if (invalidInputs.length) {
|
||||
$field.find('.form-control').addClass('o_wetrack_input_error is-invalid');
|
||||
formErrors.push('invalidFormInputs');
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Advanced Contact Must Have a Contact Mean
|
||||
if (this.useAdvancedContact) {
|
||||
var hasContactMean = this.$('.o_wetrack_contact_phone_input').val() ||
|
||||
this.$('.o_wetrack_contact_email_input').val();
|
||||
if (!hasContactMean) {
|
||||
this.$('.o_wetrack_contact_information').addClass('o_wetrack_no_contact_mean_error');
|
||||
this.$('.o_wetrack_contact_mean').addClass('is-invalid');
|
||||
formErrors.push('noContactMean');
|
||||
} else {
|
||||
this.$('.o_wetrack_contact_information').removeClass('o_wetrack_no_contact_mean_error');
|
||||
this.$('.o_wetrack_contact_mean:not(".o_wetrack_input_error")').removeClass('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Form Validity and Error Display
|
||||
this._updateErrorDisplay(formErrors);
|
||||
return formErrors.length === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* If there are still errors in form, display the error section and
|
||||
* compose the error message accordingly.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} errors - Names of errors still present in form.
|
||||
*/
|
||||
_updateErrorDisplay: function(errors) {
|
||||
|
||||
this.$('.o_wetrack_proposal_error_section').toggleClass('d-none', !errors.length);
|
||||
|
||||
var errorMessage = '';
|
||||
var $errorElement = this.$('.o_wetrack_proposal_error_message');
|
||||
|
||||
if (errors.includes('invalidFormInputs')) { errorMessage += _t(' Please fill out the form correctly.'); }
|
||||
if (errors.includes('noContactMean')) { errorMessage += _t(' Please enter either a contact email address or a contact phone number.'); }
|
||||
|
||||
$errorElement.text(errorMessage).change();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Display / Hide Additional Contact Information section when toggling
|
||||
* the checkbox on the form o_wetrack_add_contact_information_checkbox.
|
||||
* Also empty the email to prevent hidden email format error.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onAdvancedContactToggle: function(ev) {
|
||||
this.useAdvancedContact = !this.useAdvancedContact;
|
||||
var $contactName = this.$(".o_wetrack_contact_name_input")[0];
|
||||
var $advancedInformation = this.$('.o_wetrack_contact_information');
|
||||
|
||||
if (this.useAdvancedContact) {
|
||||
$advancedInformation.removeClass('d-none');
|
||||
$contactName.setAttribute("required", "True");
|
||||
} else {
|
||||
this.$('.o_wetrack_contact_email_input').val('').change();
|
||||
$advancedInformation.addClass('d-none');
|
||||
$contactName.removeAttribute("required");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Propagates the new input on speaker name to contact name, as long as the latter
|
||||
* is the start of partner name. Otherwise, do not modify existing contact name.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onPartnerNameInput: function(ev) {
|
||||
var partnerNameText = $(ev.currentTarget).val();
|
||||
var contactNameText = this.$(".o_wetrack_contact_name_input").val();
|
||||
if (partnerNameText.startsWith(contactNameText)) {
|
||||
this.$(".o_wetrack_contact_name_input").val(partnerNameText).change();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Submits the form if no errors are present in the form after validation.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onProposalFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Prevent further clicking
|
||||
this.$target.find('.o_wetrack_proposal_submit_button')
|
||||
.addClass('disabled')
|
||||
.attr('disabled', 'disabled');
|
||||
|
||||
// Submission of the form if no errors remain
|
||||
if (this._isFormValid()) {
|
||||
this.$el.submit();
|
||||
this.$target[0].reset();
|
||||
}
|
||||
|
||||
// Restore button
|
||||
this.$target.find('.o_wetrack_proposal_submit_button')
|
||||
.removeAttr('disabled')
|
||||
.removeClass('disabled');
|
||||
},
|
||||
});
|
||||
|
||||
return publicWidget.registry.websiteEventTrackProposalForm;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
odoo.define('website_event_track.website_event_track_proposal_form_tags', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
publicWidget.registry.websiteEventTrackProposalFormTags = publicWidget.Widget.extend({
|
||||
|
||||
selector: '.o_website_event_track_proposal_form_tags',
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._bindSelect2Dropdown();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler for select2 on tags added to the proposal track form.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_bindSelect2Dropdown: function () {
|
||||
var self = this;
|
||||
this.$('.o_wetrack_select2_tags').select2(this._select2Wrapper(_t('Select categories'),
|
||||
function () {
|
||||
return self._rpc({
|
||||
route: "/event/track_tag/search_read",
|
||||
params: {
|
||||
fields: ['name', 'category_id'],
|
||||
domain: [],
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for select2. Load data from server once and store it.
|
||||
* Tags are sorted in alphabetical order and have format "tag.category.name : tag.name"
|
||||
* Or "tag.name" if tag does not belong to any category.
|
||||
*
|
||||
* @private
|
||||
* @param {String} tag - Placeholder for element.
|
||||
* @param {Function} fetchFNC - Fetch data from remote location. Should return a Promise.
|
||||
* Resolved data should be array of objects with id and name. eg. [{'id': id, 'name': 'text'}, ...]
|
||||
* @param {String} nameKey - (optional) the name key of the returned record
|
||||
* ('name' if not provided)
|
||||
* @returns {Object} select2 wrapper object
|
||||
*/
|
||||
_select2Wrapper: function (tag, fetchFNC, nameKey) {
|
||||
nameKey = nameKey || 'name';
|
||||
|
||||
var values = {
|
||||
placeholder: tag,
|
||||
allowClear: true,
|
||||
formatNoMatches: _t('No results found'),
|
||||
selection_data: false,
|
||||
fetch_rpc_fnc: fetchFNC,
|
||||
multiple: 'multiple',
|
||||
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
|
||||
|
||||
// category_id structure : [id, tag category name]
|
||||
fill_data: function (query, data) {
|
||||
var that = this,
|
||||
tags = {results: []};
|
||||
_.each(data, function (obj) {
|
||||
// select tags matching either category or tag name
|
||||
if (that.matcher(query.term, obj[nameKey]) || that.matcher(query.term, obj.category_id[1])) {
|
||||
if (obj.category_id[1]) {
|
||||
tags.results.push({id: obj.id, text: obj.category_id[1] + " : " + obj[nameKey]});
|
||||
} else {
|
||||
tags.results.push({id: obj.id, text: obj[nameKey]});
|
||||
}
|
||||
}
|
||||
});
|
||||
query.callback(tags);
|
||||
},
|
||||
|
||||
query: function (query) {
|
||||
var that = this;
|
||||
// fetch data only once and store it
|
||||
if (!this.selection_data) {
|
||||
this.fetch_rpc_fnc().then(function (data) {
|
||||
that.fill_data(query, data);
|
||||
that.selection_data = data;
|
||||
});
|
||||
} else {
|
||||
this.fill_data(query, this.selection_data);
|
||||
}
|
||||
}
|
||||
};
|
||||
return values;
|
||||
},
|
||||
});
|
||||
|
||||
return publicWidget.registry.websiteEventTrackProposalFormTags;
|
||||
|
||||
});
|
||||
@@ -9,7 +9,6 @@ from odoo.addons.website.models.website_visitor import WebsiteVisitor
|
||||
from odoo.addons.website_event.tests.common import TestEventOnlineCommon
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
class TestTrackData(TestEventOnlineCommon):
|
||||
|
||||
@users('user_eventmanager')
|
||||
@@ -70,15 +69,31 @@ class TestTrackData(TestEventOnlineCommon):
|
||||
new_track.write({'partner_id': customer.id})
|
||||
self.assertEqual(new_track.partner_id, customer)
|
||||
self.assertEqual(
|
||||
new_track.partner_name, customer.name,
|
||||
'Track customer should take over existing value')
|
||||
new_track.partner_name, 'Nibbler In Space',
|
||||
'Track customer should not take over existing value')
|
||||
self.assertEqual(
|
||||
new_track.partner_email, customer.email,
|
||||
'Track customer should take over existing value')
|
||||
'Track customer should take over empty value')
|
||||
self.assertEqual(
|
||||
new_track.partner_phone, customer.phone,
|
||||
'Track customer should take over existing value')
|
||||
new_track.partner_phone, test_phone,
|
||||
'Track customer should not take over existing value')
|
||||
|
||||
# contacts fields should be updated with track customer
|
||||
new_track = self.env['event.track'].create({
|
||||
'event_id': event.id,
|
||||
'name': 'Mega Track',
|
||||
'contact_phone': test_phone,
|
||||
})
|
||||
self.assertEqual(new_track.contact_email, False)
|
||||
self.assertEqual(new_track.contact_phone, test_phone)
|
||||
new_track.write({'partner_id': customer.id})
|
||||
self.assertEqual(new_track.partner_id, customer)
|
||||
self.assertEqual(
|
||||
new_track.contact_email, customer.email,
|
||||
'Track customer should take over empty contact email value')
|
||||
self.assertEqual(
|
||||
new_track.contact_phone, customer.phone,
|
||||
'Track customer should take over existing contact phone value')
|
||||
|
||||
class TestTrackSuggestions(TestEventOnlineCommon):
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
<tree string="Event Track Tag" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="color" widget="color_picker"/>
|
||||
<field name="category_id"/>
|
||||
<field name="color" widget="color_picker"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -15,96 +15,91 @@
|
||||
This event does not accept proposals.
|
||||
</p>
|
||||
</section>
|
||||
<section class="row">
|
||||
<section class="row mb32">
|
||||
<div class="oe_structure col-lg-12">
|
||||
<section>
|
||||
<h3 class="o_page_header mt8">
|
||||
Introduction
|
||||
</h3>
|
||||
<p>
|
||||
We will accept a broad range of
|
||||
presentations, from reports on academic and
|
||||
commercial projects to tutorials and case
|
||||
studies. As long as the presentation is
|
||||
interesting and potentially useful to the
|
||||
audience, it will be considered for
|
||||
inclusion in the programme.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="oe_structure">
|
||||
<section>
|
||||
<h3 class="o_page_header mt16">
|
||||
Introduction
|
||||
</h3>
|
||||
<p>
|
||||
We will accept a broad range of
|
||||
presentations, from reports on academic and
|
||||
commercial projects to tutorials and case
|
||||
studies. As long as the presentation is
|
||||
interesting and potentially useful to the
|
||||
audience, it will be considered for
|
||||
inclusion in the programme.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div t-if="not track">
|
||||
<section class="mt-5">
|
||||
<h3 class="o_page_header">Application</h3>
|
||||
<p>
|
||||
Fill this form to propose your talk.
|
||||
</p>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fa fa-info-circle"/>
|
||||
You can add multiple speakers by separating names, emails and phones with commas.
|
||||
</div>
|
||||
</section>
|
||||
<section id="forms" class="mb-5"
|
||||
t-if="event.website_track_proposal">
|
||||
<form class="mt32 js_website_submit_form" t-attf-action="/event/#{event.id}/track_proposal/post" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="track_name">Talk Title</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input type="text" class="form-control" name="track_name" required="True"/>
|
||||
<section id="forms" t-if="event.website_track_proposal">
|
||||
<form class="mt32 js_website_submit_form o_website_event_track_proposal_form" t-attf-action="/event/#{event.id}/track_proposal/post" method="post" enctype="multipart/form-data">
|
||||
<input name="csrf_token" type="hidden" t-att-value="request.csrf_token()"/>
|
||||
<div id="track_intro">
|
||||
<h4 class="mt32"><b>Talk Intro</b></h4>
|
||||
<div class="form-text text-muted mb16">
|
||||
What is your talk about?
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="description">Talk Introduction</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<textarea class="form-control" name="description"/>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="track_name" style="width: 200px" >
|
||||
<span class="o_wetrack_proposal_label_content">Talk Title</span>
|
||||
<span>*</span>
|
||||
</label>
|
||||
<div class="col-sm">
|
||||
<input name="track_name" type="text" class="form-control form-field" required="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" t-if="len(event.allowed_track_tag_ids)">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="phone">Tags</label>
|
||||
<div class="col-lg-9 col-md-8">
|
||||
<div class="row">
|
||||
<label class="col-md-4" t-foreach="event.allowed_track_tag_ids" t-as="tag">
|
||||
<input type="checkbox" value="1" t-attf-name="tag_#{str(tag.id)}"/>
|
||||
<t t-esc="tag.name"/>
|
||||
</label>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="description" style="width: 200px" >
|
||||
<span class="o_wetrack_proposal_label_content">Talk Introduction</span>
|
||||
<span>*</span>
|
||||
</label>
|
||||
<div class="col-sm">
|
||||
<textarea name="description" class="form-control" required="True"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="tags" style="width: 200px">Categories</label>
|
||||
<div class="col-sm o_website_event_track_proposal_form_tags">
|
||||
<input name="tags" type="text" class="form-control o_wetrack_select2_tags"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="partner_name">Speaker(s) Name</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input type="text" class="form-control" name="partner_name" required="True"/>
|
||||
<div id="speaker_profile">
|
||||
<h4 class="mt32"><b>Speaker Profile</b></h4>
|
||||
<div class="form-text text-muted mb16">
|
||||
Who will give this talk? We will show this to attendees to showcase your talk.
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="partner_name" style="width: 200px">Name</label>
|
||||
<div class="col-sm"><input name="partner_name" type="text" class="form-control"/></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="partner_email" style="width: 200px">Email</label>
|
||||
<div class="col-sm"><input name="partner_email" type="email" class="form-control" /></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="partner_phone" style="width: 200px">Phone</label>
|
||||
<div class="col-sm"><input name="partner_phone" type="text" class="form-control"/></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="image" style="width: 200px">Picture</label><br/>
|
||||
<div class="col-sm"><input name="image" type="file" accept="image/*" style="width: 100%"/></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="partner_function" style="width: 200px">Job Title</label>
|
||||
<div class="col-sm"><input name="partner_function" type="text" class="form-control"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="email_from">Speaker(s) Email</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input type="email" class="form-control" name="email_from" required="True" multiple="multiple"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="phone">Speaker(s) Phone</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input type="text" class="form-control" name="phone" required="True"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="biography">Speaker(s) Biography</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<textarea class="form-control" name="biography" rows="5"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-md-4 col-form-label" for="image">Speaker(s) Picture</label>
|
||||
<div class="col-lg-7 col-md-8">
|
||||
<input name="image" type="file" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row o_form_buttons">
|
||||
<div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7">
|
||||
<button type="submit" class="btn btn-primary">Submit Proposal</button>
|
||||
</div>
|
||||
<t t-call="website_event_track.event_track_proposal_contact_details"/>
|
||||
<div class="form-group o_form_buttons">
|
||||
<button type="submit" class="btn btn-primary o_wetrack_proposal_submit_button">Submit Proposal</button>
|
||||
<span class="o_wetrack_proposal_error_section text-danger d-none ml8">
|
||||
<i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>
|
||||
<span class="o_wetrack_proposal_error_message"/>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -153,12 +148,43 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="event_track_proposal_contact_details">
|
||||
<div id="event_track_proposal_additional_contact_section">
|
||||
<div class="form-group">
|
||||
<input name="add_contact_information" type= "checkbox" class="o_wetrack_add_contact_information_checkbox"/>
|
||||
<label class="font-weight-normal" for="add_contact_information">Contact me through a different email/phone</label>
|
||||
</div>
|
||||
<div class="o_wetrack_contact_information d-none">
|
||||
<h4><b>Contact Information</b></h4>
|
||||
<div class="form-text text-muted mb16">
|
||||
How can our team get in touch with you?
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" style="width: 200px">
|
||||
<span for="contact_name">Name</span>
|
||||
<span>*</span>
|
||||
</label>
|
||||
<div class="col-sm"><input name="contact_name" type="text" class="form-control o_wetrack_contact_name_input"/></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="contact_email" style="width: 200px" >Email</label>
|
||||
<div class="col-sm"><input name="contact_email" type="email" class="form-control o_wetrack_contact_mean o_wetrack_contact_email_input"/></div>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label class="col-form-label col-sm-auto" for="contact_phone" style="width: 200px" >Phone</label>
|
||||
<div class="col-sm"><input name="contact_phone" type="text" class="form-control o_wetrack_contact_mean o_wetrack_contact_phone_input"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="event_track_proposal_success">
|
||||
<section class="mt-5">
|
||||
<section class="my-5">
|
||||
<h3 class="o_page_header">Application</h3>
|
||||
<p>
|
||||
Thank you for your proposal.
|
||||
</p><p>
|
||||
</p>
|
||||
<p>
|
||||
We will evaluate your proposition and get back to you shortly.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -169,22 +169,28 @@
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Speakers" name="speakers">
|
||||
<group class="display-flex justify-content-between">
|
||||
<page string="Speaker" name="speaker">
|
||||
<group string="Contact Details" class="display-flex justify-content-between">
|
||||
<group>
|
||||
<field name="partner_id" context="{'default_phone': contact_phone, 'default_email': contact_email}"/>
|
||||
<field name="contact_email" attrs="{'readonly': [('partner_id', '!=', False)]}"/>
|
||||
<field name="contact_phone" class="o_force_ltr" attrs="{'readonly': [('partner_id', '!=', False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Speaker Bio" class="display-flex justify-content-between">
|
||||
<group>
|
||||
<field name="partner_id"
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
|
||||
context="{'default_phone': partner_phone, 'default_email': partner_email}"/>
|
||||
<field name="partner_name"/>
|
||||
<field name="partner_email"/>
|
||||
<field name="partner_phone" class="o_force_ltr"/>
|
||||
<field name="partner_function"/>
|
||||
<field name="partner_company_name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="image" nolabel="1" widget="image" class="oe_avatar"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="partner_biography"/>
|
||||
<field name="partner_biography" string="Biography"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Description" name="description">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<li><b>Proposed By</b>: <t t-esc="track.partner_name or track.partner_email"/></li>
|
||||
<li><b>Mail</b>: <a t-attf-href="mailto:#{track.partner_email}"><t t-esc="track.partner_email"/></a></li>
|
||||
<li><b>Phone</b>: <t t-esc="track.partner_phone"/></li>
|
||||
<li><b>Speakers Biography</b>: <div t-field="track.partner_biography"/></li>
|
||||
<li><b>Speaker Biography</b>: <div t-field="track.partner_biography"/></li>
|
||||
<li><b>Talk Introduction</b>: <div t-field="track.description"/></li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user