From be3c92d805ea6f699fe5ccf74cd5f64dabf1b35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 23 May 2022 15:24:49 +0200 Subject: [PATCH] [REF] mass_mailing: improve portal subscription management page PURPOSE Globally improve usability and features given by mailing portal about exclusion list and opt-out management. SPECIFICATIONS Purpose of this commit is to cleanup and improve the portal subscription page that allows to manage subscription to mailing lists and blacklist status. Main features updated or added * allow to give a feedback when unsubscribing from mailing not related to mailing lists. It was previously limited to mailings done on mailing lists. Now the feedback is allowed in all cases and posts it on the related document; * clean display of opt-in and opt-out lists; * display all public lists, even if not already join. This allows to opt-in to new lists, which was not possible before; * switch on a neutral name for non public lists (as they may contain marketing hints); * give UI feedback to customer when using buttons: add confirmation of block list addition / removal, of updated subscriptions, ... * globally improve wording; Unsubscribe from a document now uses the same layout as unsubscribe from mailing lists. Indeed the first one blacklists the email while the second one opt-outs from mailing lists. But overall form is the same and options are also the same. After all previous cleaning we can now keep a single page for everything. Task-2150462 (Mass Mailing: Improve subscription management) Part-of: odoo/odoo#86084 --- addons/mass_mailing/__manifest__.py | 3 + addons/mass_mailing/controllers/main.py | 78 ++++--- .../src/js/mailing_portal_subscription.js | 57 +---- .../mailing_portal_subscription_blocklist.js | 35 ++- .../mailing_portal_subscription_feedback.js | 65 +++++- .../js/mailing_portal_subscription_form.js | 113 +++++++++- .../mailing_portal_subscription_blocklist.xml | 20 ++ .../mailing_portal_subscription_feedback.xml | 16 ++ .../xml/mailing_portal_subscription_form.xml | 86 ++++++++ ...ailing_portal_unsubscribe_from_document.js | 41 +++- .../mailing_portal_unsubscribe_from_list.js | 64 ++++-- .../tests/test_mailing_controllers.py | 46 +++- .../mailing_templates_portal_unsubscribe.xml | 207 +++++++++--------- 13 files changed, 608 insertions(+), 223 deletions(-) create mode 100644 addons/mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml create mode 100644 addons/mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml create mode 100644 addons/mass_mailing/static/src/xml/mailing_portal_subscription_form.xml diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index 547941ac0e1..83b2f46f09f 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -107,6 +107,9 @@ 'mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js', 'mass_mailing/static/src/js/mailing_portal_subscription_feedback.js', 'mass_mailing/static/src/js/mailing_portal_subscription_form.js', + 'mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml', + 'mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml', + 'mass_mailing/static/src/xml/mailing_portal_subscription_form.xml', ], 'web_editor.assets_wysiwyg': [ 'mass_mailing/static/src/js/mass_mailing_wysiwyg.js', diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 71513f06b61..1ceedd860f8 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -60,37 +60,29 @@ class MassMailController(http.Controller): # Unsubscribe directly + Let the user choose their subscriptions mailing.contact_list_ids._update_subscription_from_email(email, opt_out=True) - contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))]) - subscription_list_ids = contacts.mapped('subscription_list_ids') - # In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2, - # assume that the user is not opt_out on both - # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in - opt_out_list_ids = subscription_list_ids.filtered(lambda rel: rel.opt_out).mapped('list_id') - opt_in_list_ids = subscription_list_ids.filtered(lambda rel: not rel.opt_out).mapped('list_id') - opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids]) + # compute name of unsubscribed list: hide non public lists + if all(not mlist.is_public for mlist in mailing.contact_list_ids): + lists_unsubscribed_name = _('You are no longer part of our mailing list(s).') + elif len(mailing.contact_list_ids) == 1: + lists_unsubscribed_name = _('You are no longer part of the %(mailing_name)s mailing list.', + mailing_name=mailing.contact_list_ids.name) + else: + lists_unsubscribed_name = _( + 'You are no longer part of the %(mailing_names)s mailing list.', + mailing_names=', '.join(mlist.name for mlist in mailing.contact_list_ids if mlist.is_public) + ) - unique_list_ids = set([list.list_id.id for list in subscription_list_ids]) - list_ids = request.env['mailing.list'].sudo().browse(unique_list_ids) - unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public) - - render_values = self._prepare_mailing_subscription_values(mailing, document_id, email, hash_token) return request.render( 'mass_mailing.page_mailing_unsubscribe', dict( - **render_values, - contacts=contacts, - list_ids=list_ids, - opt_out_list_ids=opt_out_list_ids, - unsubscribed_list=unsubscribed_list, + self._prepare_mailing_subscription_values( + mailing, document_id, email, hash_token + ), + unsubscribed_name=lists_unsubscribed_name, ) ) def _mailing_unsubscribe_from_document(self, mailing, document_id, email, hash_token): - opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([ - ('contact_id.email_normalized', '=', email), - ('opt_out', '=', False) - ]).mapped('list_id') - message = Markup( _( 'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)', @@ -99,20 +91,39 @@ class MassMailController(http.Controller): ) _blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) - render_values = self._prepare_mailing_subscription_values(mailing, document_id, email, hash_token) return request.render( - 'mass_mailing.page_mailing_unsubscribe_done', + 'mass_mailing.page_mailing_unsubscribe', dict( - **render_values, - list_ids=opt_in_lists, + self._prepare_mailing_subscription_values( + mailing, document_id, email, hash_token + ), + unsubscribed_name=_('You are no longer part of our services and will not be contacted again.'), ) ) def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token): - """ Prepare common values used in various subscription management or + """ Prepare common values used in various subscription management or blocklist flows done in portal. """ mail_blocklist = self._fetch_blocklist_record(email) email_normalized = tools.email_normalize(email) + + # as there may be several contacts / email -> consider any opt-in overrides + # opt-out + contacts = self._fetch_contacts(email) + lists_optin = contacts.subscription_list_ids.filtered( + lambda sub: not sub.opt_out + ).list_id + lists_optout = contacts.subscription_list_ids.filtered( + lambda sub: sub.opt_out and sub.list_id not in lists_optin + ).list_id + lists_public = request.env['mailing.list'].sudo().search( + [('is_public', '=', True), + ('id', 'not in', (lists_optin + lists_optout).ids) + ], + limit=10, + order='create_date DESC, id DESC', + ) + return { # customer 'document_id': document_id, @@ -121,6 +132,9 @@ class MassMailController(http.Controller): 'hash_token': hash_token, 'mailing_id': mailing.id, 'res_id': document_id, + # feedback + 'feedback_enabled': True, + 'feedback_readonly': False, # blocklist 'blocklist_enabled': bool( request.env['ir.config_parameter'].sudo().get_param( @@ -130,6 +144,12 @@ class MassMailController(http.Controller): ), 'blocklist_possible': mail_blocklist is not None, 'is_blocklisted': mail_blocklist.active if mail_blocklist else False, + # mailing lists + 'contacts': contacts, + 'lists_contacts': contacts.subscription_list_ids.list_id, + 'lists_optin': lists_optin, + 'lists_optout': lists_optout, + 'lists_public': lists_public, } @http.route('/mailing/list/update', type='json', auth='public', csrf=True) @@ -152,7 +172,7 @@ class MassMailController(http.Controller): # opt-in in either already member, either public (to avoid trying to opt-in # in private lists) lists_to_optin = lists_optin.filtered( - lambda mlist: mlist.is_public or mlist in contacts.lists_ids + lambda mlist: mlist.is_public or mlist in contacts.list_ids ) lists_to_optout._update_subscription_from_email(email, opt_out=True) lists_to_optin._update_subscription_from_email(email, opt_out=False) diff --git a/addons/mass_mailing/static/src/js/mailing_portal_subscription.js b/addons/mass_mailing/static/src/js/mailing_portal_subscription.js index 646f42906e7..cb148e9a312 100644 --- a/addons/mass_mailing/static/src/js/mailing_portal_subscription.js +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription.js @@ -1,8 +1,6 @@ /** @odoo-module alias=mailing.PortalSubscription **/ import publicWidget from "@web/legacy/js/public/public_widget"; -import { markup } from "@odoo/owl"; -import { _t } from '@web/core/l10n/translation'; publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ @@ -21,15 +19,12 @@ publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ this.customerData = {...document.getElementById('o_mailing_portal_subscription').dataset}; this.customerData.documentId = parseInt(this.customerData.documentId || 0); this.customerData.mailingId = parseInt(this.customerData.mailingId || 0); - this.customerData.feedbackEnabled = true; + this.lastAction = this.customerData.lastAction; // nodes for widgets (jquery due to widget implementation) this.$bl_elem = this.$('#o_mailing_subscription_blocklist'); this.$feedback_elem = this.$('#o_mailing_subscription_feedback'); this.$form_elem = this.$('#o_mailing_subscription_form'); - // nodes for text / ui update - this.subscriptionInfoNode = document.getElementById('o_mailing_subscription_info'); - this.subscriptionInfoStateNode = document.getElementById('o_mailing_subscription_info_state'); this._attachBlocklist(); this._attachFeedback(); @@ -55,6 +50,7 @@ publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ {customerData: this.customerData} ); this.feedbackWidget.attachTo(this.$feedback_elem); + this.feedbackWidget._setLastAction(this.lastAction); } }, @@ -70,8 +66,7 @@ publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ _onActionDone: function (callKey) { this.lastAction = callKey; - this._updateDisplay(callKey); - this._updateSubscriptionInfo(callKey); + this._updateDisplay(); }, _onBlocklistAdd: function (event) { @@ -87,17 +82,14 @@ publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ const callKey = event.data.callKey; this.customerData.isBlocklisted = false; if (callKey === 'blocklist_remove') { - this.customerData.feedbackEnabled = true; + this.customerData.feedbackEnabled = false; } this._onActionDone(callKey); }, _onFeedbackSent: function (event) { const callKey = event.data.callKey; - if (callKey === 'feedback_sent') { - this.customerData.feedbackEnabled = true; - } - this._onActionDone(callKey); + this.lastAction = callKey; }, _onSubscriptionUpdated: function (event) { @@ -108,46 +100,19 @@ publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({ this._onActionDone(callKey); }, - _updateDisplay: function (callKey) { + _updateDisplay: function () { if (! this.customerData.feedbackEnabled && this.$feedback_elem.length) { - this.$feedback_elem.hide(); + this.$feedback_elem.addClass('d-none'); } else if (this.$feedback_elem.length) { - this.$feedback_elem.show(); + this.$feedback_elem.removeClass('d-none'); } if (this.formWidget) { + this.formWidget._setBlocklisted(this.customerData.isBlocklisted); this.formWidget._setReadonly(this.customerData.isBlocklisted); } if (this.feedbackWidget) { - this.feedbackWidget._updateDisplay(true); - } - }, - - _updateSubscriptionInfo: function (callKey) { - if (callKey === 'blocklist_add') { - this.subscriptionInfoStateNode.innerHTML = markup( - _t('You have been successfully added to our blocklist. You will not be contacted anymore by our services.') - ); - this.subscriptionInfoNode.setAttribute('class', 'alert alert-success'); - } else if (callKey === 'blocklist_remove') { - this.subscriptionInfoStateNode.innerHTML = markup( - _t('You have been successfully removed from our blocklist. You are now able to be contacted by our services.') - ); - this.subscriptionInfoNode.setAttribute('class', 'alert alert-success'); - } else if (callKey == 'feedback_sent') { - this.subscriptionInfoStateNode.innerHTML = _t('Thanks for your feedback.'); - } else if (callKey === 'subscription_updated') { - this.subscriptionInfoStateNode.innerHTML = markup( - _t('You have successfully updated your memberships.') - ); - this.subscriptionInfoNode.setAttribute('class', 'alert alert-success'); - } else if (callKey === 'unauthorized') { - this.subscriptionInfoStateNode.innerHTML = _t('You are not authorized to do this.'); - this.subscriptionInfoNode.setAttribute('class', 'alert alert-error'); - } else if (callKey === 'error') { - this.subscriptionInfoStateNode.innerHTML = _t('An error occurred. Please try again later or contact us.'); - this.subscriptionInfoNode.setAttribute('class', 'alert alert-error'); - } else { - this.subscriptionInfoStateNode.setAttribute('class', 'd-none'); + this.feedbackWidget._updateDisplay(true, false); + this.feedbackWidget._setLastAction(this.lastAction); } }, }); diff --git a/addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js b/addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js index 07582644a80..c9a4b6ed65f 100644 --- a/addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js @@ -1,6 +1,7 @@ /** @odoo-module alias=mailing.PortalSubscriptionBlocklist **/ import { jsonrpc } from "@web/core/network/rpc_service"; +import { renderToElement } from "@web/core/utils/render"; import publicWidget from "@web/legacy/js/public/public_widget"; @@ -46,6 +47,7 @@ publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.e this.customerData.isBlocklisted = true; } this._updateDisplay(); + this._updateInfo(result === true ? 'blocklist_add' : 'error'); this.trigger_up( 'blocklist_add', {'callKey': result === true ? 'blocklist_add' : result, @@ -75,6 +77,7 @@ publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.e this.customerData.isBlocklisted = false; } this._updateDisplay(); + this._updateInfo(result === true ? 'blocklist_remove' : 'error'); this.trigger_up( 'blocklist_remove', {'callKey': result === true ? 'blocklist_remove' : result, @@ -85,8 +88,8 @@ publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.e }, /* - * Display buttons according to current state. Removing from blocklist is - * always available when being blocklisted. Adding in blocklist is available + * Display buttons and info according to current state. Removing from blocklist + * is always available when being blocklisted. Adding in blocklist is available * when not being blocklisted, if the action is possible (valid email mainly) * and if the option is activated. */ @@ -104,6 +107,34 @@ publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.e buttonRemoveNode.classList.add('d-none'); } }, + + /* + * Display feedback (visual tips) to the user concerning the last done action. + */ + _updateInfo: function (infoKey) { + const updateInfo = document.getElementById('o_mailing_subscription_update_info'); + if (infoKey !== undefined) { + const infoContent = renderToElement( + "mass_mailing.portal.blocklist_update_info", + { + infoKey: infoKey, + } + ); + updateInfo.innerHTML = infoContent.innerHTML; + if (['blocklist_add', 'blocklist_remove'].includes(infoKey)) { + updateInfo.classList.add('text-success'); + updateInfo.classList.remove('text-danger'); + } + else { + updateInfo.classList.add('text-danger'); + updateInfo.classList.remove('text-success'); + } + updateInfo.classList.remove('d-none'); + } + else { + updateInfo.classList.add('d-none'); + } + }, }); export default publicWidget.registry.MailingPortalSubscriptionBlocklist; diff --git a/addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js b/addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js index f6ac32a04b2..3682117e472 100644 --- a/addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js @@ -1,7 +1,9 @@ /** @odoo-module alias=mailing.PortalSubscriptionFeedback **/ +import { _t } from "@web/core/l10n/translation"; import { jsonrpc } from "@web/core/network/rpc_service"; import publicWidget from "@web/legacy/js/public/public_widget"; +import { renderToElement } from "@web/core/utils/render"; publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.extend({ @@ -22,7 +24,7 @@ publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.ex * @override */ start: function () { - this._updateDisplay(); + this._updateDisplay(true, false); return this._super.apply(this, arguments); }, @@ -45,7 +47,12 @@ publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.ex } ).then((result) => { if (result === true) { - this._updateDisplay(true); + this._updateDisplay(false, true); + this._updateInfo('feedback_sent'); + } + else { + this._updateDisplay(false, false); + this._updateInfo(result); } this.trigger_up( 'feedback_sent', @@ -54,19 +61,69 @@ publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.ex }); }, + /* + * Set last done action, which triggers some update in the feedback form + * allowing to contextualize the explanation given to the customer. + */ + _setLastAction: function (lastAction) { + this.lastAction = lastAction; + if (this.lastAction === 'blocklist_add') { + document.querySelector('div#o_mailing_subscription_feedback p').innerHTML = _t( + 'Please let us know why you want to be in our block list.' + ); + } + else { + document.querySelector('div#o_mailing_subscription_feedback p').innerHTML = _t( + 'Please let us know why you updated your subscription.' + ); + } + }, + /* * Update display after option changes, notably feedback textarea not being * always accessible. */ - _updateDisplay: function (cleanFeedback) { + _updateDisplay: function (cleanFeedback, setReadonly) { const feedbackArea = document.querySelector('div#o_mailing_subscription_feedback textarea'); + const feedbackButton = document.getElementById('button_feedback'); + const feedbackInfo = document.getElementById('o_mailing_subscription_feedback_info'); if (this.allowFeedback) { feedbackArea.classList.remove('d-none'); } else { feedbackArea.classList.add('d-none'); } + if (setReadonly) { + feedbackArea.setAttribute('disabled', 'disabled'); + feedbackButton.setAttribute('disabled', 'disabled'); + } + else { + feedbackArea.removeAttribute('disabled'); + feedbackButton.removeAttribute('disabled'); + } if (cleanFeedback) { - feedbackArea.values = ''; + feedbackArea.value = ''; + feedbackInfo.innerHTML = ""; + } + }, + + /* + * Display feedback (visual tips) to the user concerning the last done action. + */ + _updateInfo: function (infoKey) { + const feedbackInfo = document.getElementById('o_mailing_subscription_feedback_info'); + if (infoKey !== undefined) { + const infoContent = renderToElement( + "mass_mailing.portal.feedback_update_info", + { + infoKey: infoKey, + } + ); + feedbackInfo.innerHTML = infoContent.innerHTML; + feedbackInfo.classList.add(infoKey === 'feedback_sent' ? 'text-success' : 'text-danger'); + feedbackInfo.classList.remove('d-none', infoKey === 'feedback_sent' ? 'text-danger': 'text-success'); + } + else { + feedbackInfo.classList.add('d-none'); } }, }); diff --git a/addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js b/addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js index f09f4f6473d..e4ea60a7b64 100644 --- a/addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js @@ -1,6 +1,7 @@ /** @odoo-module alias=mailing.PortalSubscriptionForm **/ import { jsonrpc } from "@web/core/network/rpc_service"; +import { renderToFragment } from "@web/core/utils/render"; import publicWidget from "@web/legacy/js/public/public_widget"; @@ -17,6 +18,28 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend return this._super.apply(this, arguments); }, + /** + * @override + * Parse start values of mailing lists subscriptions based on generated DOM + * from server. Done here to avoid having to generate it server-side and + * propagating it through various layers. + */ + start: function () { + const def = this._super.apply(this, arguments); + this.listInfo = [...document.querySelectorAll('#o_mailing_subscription_form_manage input')].map( + node => { + const listInfo = { + id: parseInt(node.getAttribute('value')), + member: node.dataset.member === '1', + name: node.getAttribute('title'), + opt_out: node.getAttribute('checked') !== 'checked', + }; + return listInfo; + } + ); + return def; + }, + /* * Triggers call to update list subscriptions. Bubble up to let parent * handle returned result if necessary. @@ -24,7 +47,7 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend _onFormSend: async function (event) { event.preventDefault(); const formData = new FormData(document.querySelector('div#o_mailing_subscription_form form')); - const mailingListIds = formData.getAll('mailing_list_ids').map(id_str => parseInt(id_str)); + const mailingListOptinIds = formData.getAll('mailing_list_ids').map(id_str => parseInt(id_str)); return await jsonrpc( '/mailing/list/update', { @@ -32,10 +55,14 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend document_id: this.customerData.documentId, email: this.customerData.email, hash_token: this.customerData.hashToken, - lists_optin_ids: mailingListIds, + lists_optin_ids: mailingListOptinIds, mailing_id: this.customerData.mailingId, } ).then((result) => { + if (result === true) { + this._updateDisplay(mailingListOptinIds); + } + this._updateInfo(result === true ? 'subscription_updated' : 'error'); this.trigger_up( 'subscription_updated', {'callKey': result === true ? 'subscription_updated' : result}, @@ -44,11 +71,29 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend }, /** - * Set form elements as readonly, e.g. because blocklisted email take precedence + * Set form elements as hidden / displayed, as this form contains either an + * informational text when being blocklisted, either the complete form to + * manage their subscription. + * @private + */ + _setBlocklisted: function (isBlocklisted) { + if (isBlocklisted) { + document.getElementById('o_mailing_subscription_form_blocklisted').classList.remove('d-none'); + document.getElementById('o_mailing_subscription_form_manage').classList.add('d-none'); + } + else { + document.getElementById('o_mailing_subscription_form_blocklisted').classList.add('d-none'); + document.getElementById('o_mailing_subscription_form_manage').classList.remove('d-none'); + } + }, + + /** + * Set form elements as readonly, e.g. when blocklisted email take precedence + * over subscription update. * @private */ _setReadonly: function (isReadonly) { - const formInputNodes = document.querySelectorAll('#o_mailing_subscription_form form input'); + const formInputNodes = document.querySelectorAll('#o_mailing_subscription_form_manage input'); const formButtonNode = document.getElementById('button_form_send'); if (isReadonly) { formInputNodes.forEach(node => {node.setAttribute('disabled', 'disabled')}); @@ -58,6 +103,66 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend formButtonNode.removeAttribute('disabled'); } }, + + /* + * Update display after subscription, notably to update mailing list subscription + * status. We simply update opt_out status based on the ID being present in the + * newly-selected opt-in mailing lists, then rerender the inputs. + */ + _updateDisplay: function (listOptinIds) { + /* update internal status*/ + this.listInfo.forEach( + (listItem) => { + listItem.member = listItem.member || listOptinIds.includes(listItem.id); + listItem.opt_out = !listOptinIds.includes(listItem.id); + } + ); + + /* update form of lists for update */ + const formContent = renderToFragment( + "mass_mailing.portal.list_form_content", + { + listsMember: this.listInfo.filter(item => item.member === true), + listsProposal: this.listInfo.filter(item => item.member === false), + } + ); + const manageForm = document.getElementById('o_mailing_subscription_form_manage'); + /*manageForm.innerHTML = formContent.innerHTML;*/ + manageForm.replaceChildren(formContent); + + /* update readonly display of customer's lists */ + const formReadonlyContent = renderToFragment( + "mass_mailing.portal.list_form_content_readonly", + { + listsOptin: this.listInfo.filter(item => item.opt_out === false), + } + ); + const readonlyForm = document.getElementById('o_mailing_subscription_form_blocklisted'); + /*readonlyForm.innerHTML = formReadonlyContent.innerHTML;*/ + readonlyForm.replaceChildren(formReadonlyContent); + }, + + /* + * Display feedback (visual tips) to the user concerning the last done action. + */ + _updateInfo: function (infoKey) { + const updateInfo = document.getElementById('o_mailing_subscription_update_info'); + if (infoKey !== undefined) { + const infoContent = renderToFragment( + "mass_mailing.portal.list_form_update_info", + { + infoKey: infoKey, + } + ); + /*updateInfo.innerHTML = infoContent.innerHTML;*/ + updateInfo.replaceChildren(infoContent); + updateInfo.classList.add(infoKey === 'error' ? 'text-danger': 'text-success'); + updateInfo.classList.remove('d-none'); + } + else { + updateInfo.classList.add('d-none'); + } + }, }); export default publicWidget.registry.MailingPortalSubscriptionForm; diff --git a/addons/mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml new file mode 100644 index 00000000000..631c5f8bcbc --- /dev/null +++ b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml @@ -0,0 +1,20 @@ + + + + + + + + Email added to our blocklist + + + + Email removed from our blocklist + + + + An error occurred. Please retry later. + + + + diff --git a/addons/mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml new file mode 100644 index 00000000000..66f2a411cd9 --- /dev/null +++ b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml @@ -0,0 +1,16 @@ + + + + + + + + Sent. Thanks you for your feedback! + + + + An error occurred. Please retry later or contact us. + + + + diff --git a/addons/mass_mailing/static/src/xml/mailing_portal_subscription_form.xml b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_form.xml new file mode 100644 index 00000000000..d3c00e014c4 --- /dev/null +++ b/addons/mass_mailing/static/src/xml/mailing_portal_subscription_form.xml @@ -0,0 +1,86 @@ + + + + + +

+ Choose your mailing subscriptions +

+
    +
  • + + + + + Not subscribed + + + Subscribed + + +
  • +
+

+ You may also be interested in +

+
    +
  • + + +
  • +
+
+ + + +

+ Your email is currently in our block list. + + You will not receive any news from those mailing lists you are a member of: + + + You will not hear from us anymore. + +

+
    + +
  • + +
  • +
    +
+
+ + + + + + Membership updated + + + + An error occurred. Please retry later. + + +
diff --git a/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js index e6518a61c24..fc3659501d3 100644 --- a/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js +++ b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js @@ -11,22 +11,35 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_documen steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed')", + trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')", }, { content: "No warning should be displayed", - trigger: "div#o_mailing_portal_subscription:not(:has(p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')))", + trigger: "div#o_mailing_subscription_form_blocklisted:not(:has(p:contains('You will not receive any news from those mailing lists you are a member of')))", + }, { + contnet: "Warning will not receive anything anymore", + trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not hear from us anymore.')", + }, { + content: "Write feedback reason", + trigger: "textarea[name='feedback']", + run: "text My feedback", + }, { + content: "Hit Send", + trigger: "button#button_feedback", + }, { + content: "Confirmation feedback is sent", + trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')", }, { content: "Revert exclusion list", trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')", }, { content: "Now exclude me (again)", trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')", isCheck: true, }, ], @@ -42,25 +55,35 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_documen steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed')", + trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')", }, { content: "Display warning about mailing lists", - trigger: "div#o_mailing_portal_subscription p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')", + trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')", }, { content: "Warning should contain reference to memberships", - trigger: "div#o_mailing_portal_subscription li strong:contains('List1')", + trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List1')", + }, { + content: "Write feedback reason", + trigger: "textarea[name='feedback']", + run: "text My feedback", + }, { + content: "Hit Send", + trigger: "button#button_feedback", + }, { + content: "Confirmation feedback is sent", + trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')", }, { content: "Revert exclusion list", trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')", }, { content: "Now exclude me (again)", trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')", isCheck: true, }, ], diff --git a/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js index b11f7c74087..bbef17d5007 100644 --- a/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js +++ b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js @@ -11,23 +11,23 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list', steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed from List1, List2')", + trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')", }, { content: "Write feedback reason", trigger: "textarea[name='feedback']", run: "text My feedback", }, { content: "Hit Send", - trigger: "div#button_feedback", + trigger: "button#button_feedback", }, { content: "Confirmation feedback is sent", - trigger: "div#o_mailing_subscription_info_state:contains('Thanks for your feedback')", + trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')", }, { content: "Now exclude me", trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')", isCheck: true, }, ], @@ -45,32 +45,35 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_wi steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed from List1, List2')", + trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')", }, { content: "List1 is present, just opt-outed", - trigger: "li.list-group-item:contains('List1') span:contains('Unsubscribed')", + trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List1') span:contains('Not subscribed')", }, { content: "List3 is present, opt-outed (test starting data)", - trigger: "li.list-group-item:contains('List3') span:contains('Unsubscribed')", + trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Not subscribed')", }, { - content: "List2 is not present (not member -> not displayed)", - trigger: "ul:not(:has(li.list-group-item:contains('List2') span:contains('Unsubscribed')))", + content: "List2 is proposed (not member -> proposal to join)", + trigger: "ul#o_mailing_subscription_form_lists_additional li.list-group-item:contains('List2')", + }, { + content: "List4 is not proposed (not member but not private)", + trigger: "ul#o_mailing_subscription_form_lists_additional:not(:has(li.list-group-item:contains('List4')))", }, { content: "Write feedback reason", trigger: "textarea[name='feedback']", run: "text My feedback", }, { content: "Hit Send", - trigger: "div#button_feedback", + trigger: "button#button_feedback", }, { content: "Confirmation feedback is sent", - trigger: "div#o_mailing_subscription_info_state:contains('Thanks for your feedback')", + trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')", }, { content: "Now exclude me", trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')", }, { content: "This should disable the 'Update my subscriptions' button", trigger: "button#button_form_send[disabled]", @@ -80,26 +83,53 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_wi trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')", }, { content: "'Update my subscriptions' button usable again", trigger: "button#button_form_send:not([disabled])", + isCheck: true, }, { content: "Choose the mailing list 3 to come back", - trigger: "li.list-group-item:contains('List3') input", + trigger: "ul#o_mailing_subscription_form_lists input[title='List3']", + }, { + content: "Add list 2", + trigger: "ul#o_mailing_subscription_form_lists_additional input[title='List2']", }, { content: "Update subscription", trigger: "button#button_form_send", }, { content: "Confirmation changes are done", - trigger: "div#o_mailing_subscription_info_state strong:contains('updated your memberships')", + trigger: "div#o_mailing_subscription_update_info span:contains('Membership updated')", + }, { + content: "List 3 is noted as subscribed again", + trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Subscribed')", + }, { + content: "List 2 has joined the subscriptions", + trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List2') span:contains('Subscribed')", + }, { + content: "No list in proposals", + trigger: "div#o_mailing_subscription_form_manage:not(:has(ul#o_mailing_subscription_form_lists_additional))", + }, { + content: "Write feedback reason", + trigger: "textarea[name='feedback']", + run: "text Another feedback", + }, { + content: "Hit Send", + trigger: "button#button_feedback", }, { content: "Now exclude me (again)", trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", + trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')", + }, { + content: "Should display warning about mailing lists", + trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')", + }, { + content: "Warning should contain reference to memberships", + trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List2')", + extra_trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List3')", isCheck: true, - } + }, ], }); diff --git a/addons/mass_mailing/tests/test_mailing_controllers.py b/addons/mass_mailing/tests/test_mailing_controllers.py index e3d2483c33b..3578e3cac25 100644 --- a/addons/mass_mailing/tests/test_mailing_controllers.py +++ b/addons/mass_mailing/tests/test_mailing_controllers.py @@ -183,10 +183,13 @@ class TestMailingControllers(TestMailingControllersCommon): Tour effects * unsubscribe from mailing based on a document = blocklist; + * add feedback (block list) 'My feedback'; * remove email from exclusion list; * re-add email to exclusion list; """ test_mailing = self.test_mailing_on_documents.with_env(self.env) + test_feedback = "My feedback" + for test_email, tour_name in [ ('"Not Déboulonneur" ', 'mailing_portal_unsubscribe_from_document'), (self.test_email, 'mailing_portal_unsubscribe_from_document_with_lists'), @@ -198,6 +201,7 @@ class TestMailingControllers(TestMailingControllersCommon): }) self.assertFalse(test_partner.is_blacklisted) previous_messages = test_partner.message_ids + test_email_normalized = tools.email_normalize(test_email) # launch unsubscription tour hash_token = test_mailing._generate_mailing_recipient_token(test_partner.id, test_partner.email_normalized) @@ -211,8 +215,14 @@ class TestMailingControllers(TestMailingControllersCommon): # status update check self.assertTrue(test_partner.is_blacklisted) - # partner (document): no messages added - self.assertEqual(test_partner.message_ids, previous_messages) + # partner (document): feedback message added + self.assertEqual(len(test_partner.message_ids), len(previous_messages) + 1) + message_feedback = test_partner.message_ids[0] + self.assertEqual( + message_feedback.body, + Markup(f'

Feedback from {test_email_normalized}: {test_feedback}

') + ) + # posted messages on exclusion list record: activated, deactivated, activated again bl_record = self.env['mail.blacklist'].search([('email', '=', test_partner.email_normalized)]) self.assertEqual(len(bl_record.message_ids), 4) @@ -302,10 +312,13 @@ class TestMailingControllers(TestMailingControllersCommon): * add email to exclusion list; * remove email from exclusion list; * come back to List3; + * join List2; + * add feedback (opt-out) 'Another feedback';' * re-add email to exclusion list; """ test_mailing = self.test_mailing_on_lists.with_env(self.env) test_feedback = "My feedback" + test_feedback_2 = "Another feedback" # fetch contact and its subscription and blacklist status, to see the tour effects contact_l1 = self.mailing_list_1.contact_ids.filtered( @@ -337,23 +350,38 @@ class TestMailingControllers(TestMailingControllersCommon): self.assertFalse(subscription_l3.opt_out) self.assertFalse(subscription_l3.unsubscription_date) - # posted messages on contact record for mailing list 1: feedback, unsubscription - message_feedback = contact_l1.message_ids[0] + # posted messages on contact record for mailing list 1: last feedback, subscription update, feedback, unsubscription + message_feedback_2 = contact_l1.message_ids[0] + self.assertEqual( + message_feedback_2.body, + Markup(f'

Feedback from {self.test_email_normalized}: {test_feedback_2}

') + ) + message_update = contact_l1.message_ids[1] + self.assertEqual( + message_update.body, + Markup(f'

{contact_l1.display_name} subscribed to the following mailing list(s)

  • {self.mailing_list_2.name}
') + ) + message_feedback = contact_l1.message_ids[2] self.assertEqual( message_feedback.body, Markup(f'

Feedback from {self.test_email_normalized}: {test_feedback}

') ) - message_unsub = contact_l1.message_ids[1] + message_unsub = contact_l1.message_ids[3] self.assertEqual( message_unsub.body, Markup(f'

{contact_l1.display_name} unsubscribed from the following mailing list(s)

  • {self.mailing_list_1.name}
') ) - # posted messages on contact record for mailing list 3: subscription - message_unsub = contact_l3.message_ids[0] + # posted messages on contact record for mailing list 3: last feedback, subscription + message_feedback = contact_l1.message_ids[0] self.assertEqual( - message_unsub.body, - Markup(f'

{contact_l1.display_name} subscribed to the following mailing list(s)

  • {self.mailing_list_3.name}
') + message_feedback_2.body, + Markup(f'

Feedback from {self.test_email_normalized}: {test_feedback_2}

') + ) + message_sub = contact_l3.message_ids[1] + self.assertEqual( + message_sub.body, + Markup(f'

{contact_l3.display_name} subscribed to the following mailing list(s)

  • {self.mailing_list_3.name}
  • {self.mailing_list_2.name}
') ) # posted messages on exclusion list record: activated, deactivated, activated again diff --git a/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml b/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml index 16e39ce7be2..43b116e01e4 100644 --- a/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml +++ b/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml @@ -12,6 +12,8 @@ t-att-data-blocklist-enabled="blocklist_enabled" t-att-data-blocklist-possible="blocklist_possible" t-att-data-document-id="document_id" + t-att-data-feedback-enabled="feedback_enabled" + t-att-data-feedback-readonly="feedback_readonly" t-att-data-hash-token="hash_token" t-att-data-email="email" t-att-data-email-valid="email_valid" @@ -20,126 +22,125 @@
-
- - You have been successfully unsubscribed from . - - - You have been successfully unsubscribed. - -
-
-
- + class="mb-4" + role="status"> + +

Successfully Unsubscribed

+ +
+
+

- We would appreciate if you provide feedback about why you updated
your subscriptions. + Please let us know why you updated your subscription.

+ -
-
Send
+
+ +
+
-
+

Mailing Subscriptions

- -

Choose your mailing subscriptions

-
    -
  • - - - - Unsubscribed - -
  • -
+
+

+ Your email is currently in our block list. + + You will not receive any news from those mailing lists you are a member of: + + + You will not hear from us anymore. + +

+
    + +
  • + + + Mailing List # + +
  • +
    +
+
+ +
+

+ Choose your mailing subscriptions +

+
    +
  • + + + + + + Not subscribed + + + Subscribed + + +
  • +
+

+ You may also be interested in +

+
    +
  • + + +
  • +
+

+ You are not subscribed to any of our mailing list. +

+
- - + class="d-flex align-items-center mb-4 pt-3"> + + +
-
-

You are not subscribed to any of our mailing list.

-
-
-
-
- - - - -