From d47d64963e0bb2684694d53b25c7bc912376e814 Mon Sep 17 00:00:00 2001 From: Noe Antoine Date: Thu, 5 Nov 2020 13:21:30 +0000 Subject: [PATCH] [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) --- addons/website_event_track/__manifest__.py | 4 +- .../controllers/event_track.py | 71 +++++-- .../data/mail_template_data.xml | 2 +- .../website_event_track/models/event_track.py | 129 ++++++++---- .../js/website_event_track_proposal_form.js | 169 +++++++++++++++ .../website_event_track_proposal_form_tags.js | 101 +++++++++ .../tests/test_track_internals.py | 27 ++- .../views/event_track_tag_views.xml | 2 +- .../views/event_track_templates_proposal.xml | 192 ++++++++++-------- .../views/event_track_views.xml | 18 +- .../views/mail_templates.xml | 2 +- 11 files changed, 566 insertions(+), 151 deletions(-) create mode 100644 addons/website_event_track/static/src/js/website_event_track_proposal_form.js create mode 100644 addons/website_event_track/static/src/js/website_event_track_proposal_form_tags.js diff --git a/addons/website_event_track/__manifest__.py b/addons/website_event_track/__manifest__.py index 0523f0db3a9..507dbba588c 100644 --- a/addons/website_event_track/__manifest__.py +++ b/addons/website_event_track/__manifest__.py @@ -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', diff --git a/addons/website_event_track/controllers/event_track.py b/addons/website_event_track/controllers/event_track.py index 72ea2f2aa7e..8b3ac031330 100644 --- a/addons/website_event_track/controllers/event_track.py +++ b/addons/website_event_track/controllers/event_track.py @@ -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//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 diff --git a/addons/website_event_track/data/mail_template_data.xml b/addons/website_event_track/data/mail_template_data.xml index 2365d4717ce..3772e6fe60a 100644 --- a/addons/website_event_track/data/mail_template_data.xml +++ b/addons/website_event_track/data/mail_template_data.xml @@ -7,7 +7,7 @@
- Dear ${object.partner_name or ''}
+ Dear ${object.partner_id.name or object.partner_name or ''}
We are pleased to inform you that your proposal ${object.name} has been accepted and confirmed for the event ${object.event_id.name}.
You will find more details here: diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py index 7af875da34d..6c753bcb081 100644 --- a/addons/website_event_track/models/event_track.py +++ b/addons/website_event_track/models/event_track.py @@ -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 # ------------------------------------------------------------ diff --git a/addons/website_event_track/static/src/js/website_event_track_proposal_form.js b/addons/website_event_track/static/src/js/website_event_track_proposal_form.js new file mode 100644 index 00000000000..808517b3682 --- /dev/null +++ b/addons/website_event_track/static/src/js/website_event_track_proposal_form.js @@ -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; + +}); diff --git a/addons/website_event_track/static/src/js/website_event_track_proposal_form_tags.js b/addons/website_event_track/static/src/js/website_event_track_proposal_form_tags.js new file mode 100644 index 00000000000..fa3dc9267c3 --- /dev/null +++ b/addons/website_event_track/static/src/js/website_event_track_proposal_form_tags.js @@ -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; + +}); diff --git a/addons/website_event_track/tests/test_track_internals.py b/addons/website_event_track/tests/test_track_internals.py index d6ebc697659..f882100f525 100644 --- a/addons/website_event_track/tests/test_track_internals.py +++ b/addons/website_event_track/tests/test_track_internals.py @@ -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): diff --git a/addons/website_event_track/views/event_track_tag_views.xml b/addons/website_event_track/views/event_track_tag_views.xml index 780b9814b8b..9a6ae42e23a 100644 --- a/addons/website_event_track/views/event_track_tag_views.xml +++ b/addons/website_event_track/views/event_track_tag_views.xml @@ -62,8 +62,8 @@ - + diff --git a/addons/website_event_track/views/event_track_templates_proposal.xml b/addons/website_event_track/views/event_track_templates_proposal.xml index 794c5e54fe4..b308968dfcd 100644 --- a/addons/website_event_track/views/event_track_templates_proposal.xml +++ b/addons/website_event_track/views/event_track_templates_proposal.xml @@ -15,96 +15,91 @@ This event does not accept proposals.

-
+
+
+
+

+ Introduction +

+

+ 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. +

+
+
-
-
-

- Introduction -

-

- 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. -

-
-
-
-

Application

-

- Fill this form to propose your talk. -

- -
-
-
- -
- -
- +
+ + +
+

Talk Intro

+
+ What is your talk about?
-
-
- -
-