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.

-
-
-
-
- - - - -