From a2872cb98106e6fd0c217d1c59140ea50c8c23c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 17 May 2023 15:29:26 +0200 Subject: [PATCH 01/14] [IMP] mass_mailing: add tests for unsubscribe mailing portal Purpose of this test is to add tours and tests about the portal in mass mailing that allows to unsubscribe from mailing list and to use the exclusion list. This is done using small tours called from unit tests. Two type of tours are added. The first one targets mailing done on mailing lists. As it targets contact model unsubscription click opts-out the contact from the mailing list. The second one targets mailing done on other documents. When clicking on unsubscription link the document's primary email is directly added into the exclusion list. Some other additional tests are added to cover various use cases and features of mailing controllers. Task-2150462 (Mass Mailing: Improve subscription management) Part-of: odoo/odoo#86084 --- ...ailing_portal_unsubscribe_from_document.js | 67 +++ .../mailing_portal_unsubscribe_from_list.js | 105 +++++ addons/mass_mailing/tests/common.py | 44 +- .../tests/test_mailing_controllers.py | 412 +++++++++++++++++- 4 files changed, 598 insertions(+), 30 deletions(-) create mode 100644 addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js create mode 100644 addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js 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 new file mode 100644 index 00000000000..1387b264018 --- /dev/null +++ b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_document.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +/* + * Tour: unsubscribe from a mailing done on documents (aka not on contacts or + * mailing lists). We assume email is not member of any mailing list in this test. + */ +registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document', { + test: true, + steps: () => [ + { + content: "Confirmation unsubscribe is done", + trigger: "div#subscription_info strong:contains('successfully unsubscribed')", + }, { + content: "No warning should be displayed", + trigger: "div#div_blacklist:not(:has(p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')))", + }, { + content: "Revert exclusion list", + trigger: "div#button_remove_blacklist", + }, { + content: "Confirmation exclusion list is removed", + trigger: "div#subscription_info strong:contains('removed from our blacklist')", + }, { + content: "Now exclude me (again)", + trigger: "div#button_add_blacklist", + }, { + content: "Confirmation exclusion is done", + trigger: "div#subscription_info strong:contains('added to our blacklist')", + isCheck: true, + }, + ], +}); + + +/* + * Tour: unsubscribe from a mailing done on documents (aka not on contacts or + * mailing lists). We assume email is member of mailing lists in this test. + */ +registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document_with_lists', { + test: true, + steps: () => [ + { + content: "Confirmation unsubscribe is done", + trigger: "div#subscription_info strong:contains('successfully unsubscribed')", + }, { + content: "Display warning about mailing lists", + trigger: "div#div_blacklist p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')", + }, { + content: "Warning should contain reference to memberships", + trigger: "div#div_blacklist li strong:contains('List1')", + }, { + content: "Revert exclusion list", + trigger: "div#button_remove_blacklist", + }, { + content: "Confirmation exclusion list is removed", + trigger: "div#subscription_info strong:contains('removed from our blacklist')", + }, { + content: "Now exclude me (again)", + trigger: "div#button_add_blacklist", + }, { + content: "Confirmation exclusion is done", + trigger: "div#subscription_info strong:contains('added to our blacklist')", + 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 new file mode 100644 index 00000000000..3ae73876e86 --- /dev/null +++ b/addons/mass_mailing/static/tests/tours/mailing_portal_unsubscribe_from_list.js @@ -0,0 +1,105 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; + +/* + * Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag + * instead of directly blocking emails). + */ +registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list', { + test: true, + steps: () => [ + { + content: "Confirmation unsubscribe is done", + trigger: "div#subscription_info strong:contains('unsubscribed from List1, List2')", + }, { + content: "Write feedback reason", + trigger: "textarea[name='opt_out_feedback']", + run: "text My feedback", + }, { + content: "Hit Send", + trigger: "div#button_feedback", + }, { + content: "Confirmation feedback is sent", + trigger: "div#subscription_info:contains('Your feedback has been sent successfully')", + }, { + content: "Now exclude me", + trigger: "div#button_add_blacklist", + }, { + content: "Confirmation exclusion is done", + trigger: "div#subscription_info strong:contains('added to our blacklist')", + isCheck: true, + }, + ], +}); + + +/* + * Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag + * instead of directly blocking emails), then play with list subscriptions and + * blocklist addition / removal. This is mainly an extended version of the tour + * hereabove, easing debug and splitting checks. + */ +registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_with_update', { + test: true, + steps: () => [ + { + content: "Confirmation unsubscribe is done", + trigger: "div#subscription_info strong:contains('unsubscribed from List1, List2')", + }, { + content: "List1 is present, just opt-outed", + trigger: "li.list-group-item:contains('List1') span:contains('Unsubscribed')", + }, { + content: "List3 is present, opt-outed (test starting data)", + trigger: "li.list-group-item:contains('List3') span:contains('Unsubscribed')", + }, { + content: "List2 is not present (not member -> not displayed)", + trigger: "ul:not(:has(li.list-group-item:contains('List2') span:contains('Unsubscribed')))", + }, { + content: "Write feedback reason", + trigger: "textarea[name='opt_out_feedback']", + run: "text My feedback", + }, { + content: "Hit Send", + trigger: "div#button_feedback", + }, { + content: "Confirmation feedback is sent", + trigger: "div#subscription_info:contains('Your feedback has been sent successfully')", + }, { + content: "Now exclude me", + trigger: "div#button_add_blacklist", + }, { + content: "Confirmation exclusion is done", + trigger: "div#subscription_info strong:contains('added to our blacklist')", + }, { + content: "This should disable the 'Update my subscriptions' button", + trigger: "button#send_form[disabled]", + isCheck: true, + }, { + content: "Revert exclusion list", + trigger: "div#button_remove_blacklist", + }, { + content: "Confirmation exclusion list is removed", + trigger: "div#subscription_info strong:contains('removed from our blacklist')", + }, { + content: "'Update my subscriptions' button usable again", + trigger: "button#send_form:not([disabled])", + }, { + content: "Choose the mailing list 3 to come back", + trigger: "li.list-group-item:contains('List3') input", + }, { + content: "Update subscription", + trigger: "button#send_form", + }, { + content: "Confirmation changes are done", + trigger: "div#subscription_info:contains('Your changes have been saved')", + }, { + content: "Now exclude me (again)", + trigger: "div#button_add_blacklist", + }, { + content: "Confirmation exclusion is done", + trigger: "div#subscription_info strong:contains('added to our blacklist')", + isCheck: true, + } + ], +}); diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index a43ccde80ef..b6857d8c10d 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -291,23 +291,33 @@ class MassMailCommon(MailCommon, MassMailCase): def _create_mailing_list(cls): """ Shortcut to create mailing lists. Currently hardcoded, maybe evolve in a near future. """ - cls.mailing_list_1 = cls.env['mailing.list'].with_context(cls._test_context).create({ - 'name': 'List1', - 'contact_ids': [ - (0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}), - (0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}), - (0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}), - ] - }) - cls.mailing_list_2 = cls.env['mailing.list'].with_context(cls._test_context).create({ - 'name': 'List2', - 'contact_ids': [ - (0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}), - (0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}), - (0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}), - (0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}), - ] - }) + cls.mailing_list_1, cls.mailing_list_2, cls.mailing_list_3, cls.mailing_list_4 = cls.env['mailing.list'].with_context(cls._test_context).create([ + { + 'contact_ids': [ + (0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}), + (0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}), + (0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}), + ], + 'name': 'List1', + }, { + 'contact_ids': [ + (0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}), + (0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}), + (0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}), + (0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}), + ], + 'name': 'List2', + }, { + 'contact_ids': [ + (0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}), + ], + 'name': 'List3', + }, { + 'is_public': False, + 'name': 'List4', + } + ]) + cls.mailing_list_3.subscription_ids[0].opt_out = True @classmethod def _create_mailing_list_of_x_contacts(cls, contacts_nbr): diff --git a/addons/mass_mailing/tests/test_mailing_controllers.py b/addons/mass_mailing/tests/test_mailing_controllers.py index 49ad26ef02b..7c620d4216c 100644 --- a/addons/mass_mailing/tests/test_mailing_controllers.py +++ b/addons/mass_mailing/tests/test_mailing_controllers.py @@ -2,32 +2,52 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from freezegun import freeze_time +from markupsafe import Markup +from requests import Session, PreparedRequest, Response import datetime import werkzeug +from odoo import tools from odoo.addons.mass_mailing.tests.common import MassMailCommon -from requests import Session, PreparedRequest, Response - -from odoo.tests.common import HttpCase -from odoo.tests import tagged +from odoo.tests import HttpCase, tagged +from odoo.tools import mute_logger -@tagged('link_tracker') -class TestMailingControllers(MassMailCommon, HttpCase): +class TestMailingControllersCommon(MassMailCommon, HttpCase): @classmethod def setUpClass(cls): - super(TestMailingControllers, cls).setUpClass() + super(TestMailingControllersCommon, cls).setUpClass() + + # cleanup lists + cls.env['mailing.list'].search([]).unlink() + cls._create_mailing_list() - cls.test_mailing = cls.env['mailing.mailing'].create({ + cls.test_mailing_on_contacts = cls.env['mailing.mailing'].create({ + 'body_html': '

Hello
Go to this link

', + 'mailing_domain': [], + 'mailing_model_id': cls.env['ir.model']._get_id('mailing.contact'), + 'mailing_type': 'mail', + 'name': 'TestMailing on Contacts', + 'subject': 'TestMailing on Contacts', + }) + cls.test_mailing_on_documents = cls.env['mailing.mailing'].create({ + 'body_html': '

Hello
Go to this link

', + 'mailing_domain': [], + 'mailing_model_id': cls.env['ir.model']._get_id('res.partner'), + 'mailing_type': 'mail', + 'name': 'TestMailing on Documents', + 'subject': 'TestMailing on Documents', + }) + cls.test_mailing_on_lists = cls.env['mailing.mailing'].create({ 'body_html': '

Hello
Go to this link

', 'contact_list_ids': [(4, cls.mailing_list_1.id), (4, cls.mailing_list_2.id)], - 'mailing_model_id': cls.env['ir.model']._get('mailing.list').id, + 'mailing_model_id': cls.env['ir.model']._get_id('mailing.list'), 'mailing_type': 'mail', - 'name': 'TestMailing', + 'name': 'TestMailing on Lists', 'reply_to': cls.email_reply_to, - 'subject': 'Test', + 'subject': 'TestMailing on Lists', }) cls.test_contact = cls.mailing_list_1.contact_ids[0] @@ -43,10 +63,375 @@ class TestMailingControllers(MassMailCommon, HttpCase): return r return super()._request_handler(s, r, **kw) + +@tagged('mailing_portal', 'post_install', '-at_install') +class TestMailingControllers(TestMailingControllersCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.test_email = tools.formataddr(('Déboulonneur', '')) + cls.test_email_normalized = 'fleurus@example.com' + + def test_assert_initial_values(self): + """ Ensure test base data to ease test understanding. Globally test_email + is member of 2 mailing public lists. """ + memberships = self.env['mailing.contact.subscription'].search([ + ('contact_id.email_normalized', '=', self.test_email_normalized)] + ) + self.assertEqual(memberships.list_id, self.mailing_list_1 + self.mailing_list_3) + self.assertEqual(memberships.mapped('opt_out'), [False, True]) + + contact_l1 = self.mailing_list_1.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + self.assertTrue(contact_l1) + self.assertFalse(contact_l1.is_blacklisted) + self.assertFalse(contact_l1.message_ids) + subscription_l1 = self.mailing_list_1.subscription_ids.filtered( + lambda subscription: subscription.contact_id == contact_l1 + ) + self.assertTrue(subscription_l1) + self.assertFalse(subscription_l1.is_blacklisted) + self.assertFalse(subscription_l1.opt_out) + self.assertFalse(subscription_l1.unsubscription_date) + + contact_l2 = self.mailing_list_2.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + self.assertFalse(contact_l2) + + contact_l3 = self.mailing_list_3.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + self.assertTrue(contact_l3) + self.assertTrue(contact_l3 != contact_l1) + self.assertFalse(contact_l3.is_blacklisted) + subscription_l3 = self.mailing_list_3.subscription_ids.filtered( + lambda subscription: subscription.contact_id == contact_l3 + ) + self.assertFalse(subscription_l3.is_blacklisted) + self.assertTrue(subscription_l3.opt_out) + + contact_l4 = self.mailing_list_4.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + self.assertFalse(contact_l4) + + self.assertFalse(self.env['mail.blacklist'].search([('email', '=', self.test_email_normalized)])) + + @mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view') + def test_mailing_report_unsubscribe(self): + """ Test deactivation of mailing report sending. It requires usage of + a hash token. """ + test_mailing = self.test_mailing_on_lists.with_env(self.env) + self.env['ir.config_parameter'].sudo().set_param( + 'mass_mailing.mass_mailing_reports', True + ) + hash_token = test_mailing._generate_mailing_report_token(self.user_marketing.id) + self.authenticate('user_marketing', 'user_marketing') + + # TEST: various invalid cases + for test_user_id, test_token in [ + (self.user_marketing.id, ''), # no token + (self.user_marketing.id, 'zboobs'), # invalid token + (self.env.uid, hash_token), # invalid credentials + ]: + with self.subTest(test_user_id=test_user_id, test_token=test_token): + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/report/unsubscribe?user_id={test_user_id}&token={test_token}', + ) + ) + self.assertEqual(res.status_code, 404) + self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports')) + + # TEST: not mailing user + self.user_marketing.write({ + 'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)], + }) + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}', + ) + ) + self.assertEqual(res.status_code, 404) + self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports')) + + # TEST: finally valid call + self.user_marketing.write({ + 'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)], + }) + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}', + ) + ) + self.assertEqual(res.status_code, 200) + self.assertFalse(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports')) + + def test_mailing_unsubscribe_from_document_tour(self): + """ Test portal unsubscribe on mailings performed on documents (not + mailing lists or contacts). Primary effect is to automatically exclude + the email (see tour). + + Two tests are performed (with and without existing list subscriptions) + as it triggers the display of the mailing list part of the UI. + + Tour effects + * unsubscribe from mailing based on a document = blocklist; + * remove email from exclusion list; + * re-add email to exclusion list; + """ + test_mailing = self.test_mailing_on_documents.with_env(self.env) + for test_email, tour_name in [ + ('"Not Déboulonneur" ', 'mailing_portal_unsubscribe_from_document'), + (self.test_email, 'mailing_portal_unsubscribe_from_document_with_lists'), + ]: + with self.subTest(test_email=test_email, tour_name=tour_name): + test_partner = self.env['res.partner'].create({ + 'email': test_email, + 'name': 'Test Déboulonneur' + }) + self.assertFalse(test_partner.is_blacklisted) + previous_messages = test_partner.message_ids + + # launch unsubscription tour + hash_token = test_mailing._generate_mailing_recipient_token(test_partner.id, test_partner.email_normalized) + with freeze_time(self._reference_now): + self.start_tour( + f"/mailing/{test_mailing.id}/unsubscribe?email={test_partner.email_normalized}&res_id={test_partner.id}&token={hash_token}", + tour_name, + login=None, + ) + + # status update check + self.assertTrue(test_partner.is_blacklisted) + + # partner (document): no messages added + self.assertEqual(test_partner.message_ids, previous_messages) + # 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) + msg_bl2, msg_unbl, msg_bl, msg_create = bl_record.message_ids + self.assertEqual( + msg_bl2.body, + Markup(f'

Blocklist request from portal of mailing {test_mailing.subject} (document Contact)

') + ) + self.assertEqual( + msg_unbl.body, + Markup(f'

Blocklist removal request from portal of mailing {test_mailing.subject} (document Contact)

') + ) + self.assertEqual( + msg_bl.body, + Markup(f'

Blocklist request from unsubscribe link of mailing {test_mailing.subject} (document Contact)

') + ) + self.assertEqual(msg_create.body, Markup('

Mail Blacklist created

')) + + def test_mailing_unsubscribe_from_list_tour(self): + """ Test portal unsubscribe on mailings performed on mailing lists. Their + effect is to opt-out from the mailing list. + + Tour effects + * unsubscribe from mailing based on lists = opt-out from lists; + * add feedback (opt-out) 'My feedback';' + * add email to exclusion list; + """ + test_mailing = self.test_mailing_on_lists.with_env(self.env) + test_feedback = "My feedback" + + # fetch contact and its subscription and blacklist status, to see the tour effects + contact_l1 = self.mailing_list_1.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + subscription_l1 = self.mailing_list_1.subscription_ids.filtered( + lambda subscription: subscription.contact_id == contact_l1 + ) + + # launch unsubscribe tour + hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email) + with freeze_time(self._reference_now): + self.start_tour( + f"/mailing/{test_mailing.id}/unsubscribe?email={self.test_email_normalized}&res_id={contact_l1.id}&token={hash_token}", + "mailing_portal_unsubscribe_from_list", + login=None, + ) + + # status update check on list 1 + self.assertTrue(subscription_l1.opt_out) + self.assertEqual(subscription_l1.unsubscription_date, self._reference_now) + # status update check on list 2: unmodified (was not member, still not member) + contact_l2 = self.mailing_list_2.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + self.assertFalse(contact_l2) + + # posted messages on contact record for mailing list 1: feedback, unsubscription + message_feedback = contact_l1.message_ids[0] + self.assertEqual( + message_feedback.body, + Markup(f'

Feedback from {self.test_email_normalized}: {test_feedback}

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

The recipient unsubscribed from {self.mailing_list_1.name} mailing list(s)

') + ) + + # posted messages on exclusion list record: activated, deactivated, activated again + bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)]) + self.assertEqual(len(bl_record.message_ids), 2) + msg_bl, msg_create = bl_record.message_ids + self.assertEqual( + msg_bl.body, + Markup(f'

Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)

') + ) + self.assertEqual(msg_create.body, Markup('

Mail Blacklist created

')) + + def test_mailing_unsubscribe_from_list_with_update_tour(self): + """ Test portal unsubscribe on mailings performed on mailing lists. Their + effect is to opt-out from the mailing list. Optional exclusion list can + be done through interface (see tour). + + Tour effects + * unsubscribe from mailing based on lists = opt-out from lists; + * add feedback (opt-out) 'My feedback';' + * add email to exclusion list; + * remove email from exclusion list; + * come back to List3; + * re-add email to exclusion list; + """ + test_mailing = self.test_mailing_on_lists.with_env(self.env) + test_feedback = "My feedback" + + # fetch contact and its subscription and blacklist status, to see the tour effects + contact_l1 = self.mailing_list_1.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + subscription_l1 = self.mailing_list_1.subscription_ids.filtered( + lambda subscription: subscription.contact_id == contact_l1 + ) + contact_l3 = self.mailing_list_3.contact_ids.filtered( + lambda contact: contact.email == self.test_email_normalized + ) + subscription_l3 = self.mailing_list_3.subscription_ids.filtered( + lambda subscription: subscription.contact_id == contact_l3 + ) + + # launch unsubscription tour + hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email) + with freeze_time(self._reference_now): + self.start_tour( + f"/mailing/{test_mailing.id}/unsubscribe?email={contact_l1.email}&res_id={contact_l1.id}&token={hash_token}", + "mailing_portal_unsubscribe_from_list_with_update", + login=None, + ) + + # status update check on list 1 + self.assertTrue(subscription_l1.opt_out) + self.assertEqual(subscription_l1.unsubscription_date, self._reference_now) + # status update check on list 3 (opt-in during test) + 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] + self.assertEqual( + message_feedback.body, + Markup(f'

Feedback from {self.test_email_normalized}: {test_feedback}

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

The recipient unsubscribed from {self.mailing_list_1.name} mailing list(s)

') + ) + + # posted messages on contact record for mailing list 3: subscription + message_unsub = contact_l3.message_ids[0] + self.assertEqual( + message_unsub.body, + Markup(f'

The recipient subscribed to {self.mailing_list_3.name} mailing list(s)

') + ) + + # posted messages on exclusion list record: activated, deactivated, activated again + bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)]) + self.assertEqual(len(bl_record.message_ids), 4) + msg_bl2, msg_unbl, msg_bl, msg_create = bl_record.message_ids + self.assertEqual( + msg_bl2.body, + Markup(f'

Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)

') + ) + self.assertEqual( + msg_unbl.body, + Markup(f'

Blocklist removal request from portal of mailing {test_mailing.subject} (document Mailing Contact)

') + ) + self.assertEqual( + msg_bl.body, + Markup(f'

Blocklist request from portal of mailing {test_mailing.subject} (document Mailing Contact)

') + ) + self.assertEqual(msg_create.body, Markup('

Mail Blacklist created

')) + + @mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view') + def test_mailing_view(self): + """ Test preview of mailing. It requires either a token, either being + mailing user. """ + test_mailing = self.test_mailing_on_documents.with_env(self.env) + res_id, email_normalized = self.user_marketing.partner_id.id, self.user_marketing.email_normalized + hash_token = test_mailing._generate_mailing_recipient_token(res_id, email_normalized) + self.user_marketing.write({ + 'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)], + }) + self.authenticate('user_marketing', 'user_marketing') + + # TEST: various invalid cases + for test_res_id, test_email, test_token in [ + (res_id, email_normalized, ''), # no token + (res_id, email_normalized, 'zboobs'), # wrong token + (self.env.user.partner_id.id, email_normalized, hash_token), # mismatch + (res_id, 'not.email@example.com', hash_token), # mismatch + ]: + with self.subTest(test_email=test_email, test_res_id=test_res_id, test_token=test_token): + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/{test_mailing.id}/view?email={test_email}&res_id={test_res_id}&token={test_token}', + ) + ) + self.assertEqual(res.status_code, 403) + + # TEST: valid call using credentials + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/{test_mailing.id}/view?email={email_normalized}&res_id={res_id}&token={hash_token}', + ) + ) + self.assertEqual(res.status_code, 200) + + # TEST: invalid credentials but mailing user + self.user_marketing.write({ + 'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)], + }) + res = self.url_open( + werkzeug.urls.url_join( + test_mailing.get_base_url(), + f'mailing/{test_mailing.id}/view', + ) + ) + self.assertEqual(res.status_code, 200) + + +@tagged('link_tracker', 'mailing_portal') +class TestMailingTracking(TestMailingControllersCommon): + + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing') def test_tracking_short_code(self): """ Test opening short code linked to a mailing trace: should set the trace as opened and clicked, create a click record. """ - mailing = self.test_mailing.with_env(self.env) + mailing = self.test_mailing_on_lists.with_env(self.env) with self.mock_mail_gateway(mail_unlink_sent=False): mailing.action_send_mail() @@ -75,10 +460,11 @@ class TestMailingControllers(MassMailCommon, HttpCase): self.assertEqual(mailing_trace.open_datetime, self._reference_now) self.assertEqual(mailing_trace.trace_status, 'open') + @mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing') def test_tracking_url_token(self): """ Test tracking of mails linked to a mailing trace: should set the trace as opened. """ - mailing = self.test_mailing.with_env(self.env) + mailing = self.test_mailing_on_lists.with_env(self.env) with self.mock_mail_gateway(mail_unlink_sent=False): mailing.action_send_mail() From 786c2c9e3c1df72017324d69d4309bee425ac62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 30 May 2023 13:57:27 +0200 Subject: [PATCH 02/14] [IMP] mass_mailing: improve contact order Use name instead of email, as contact is notably used in sms marketing application with mainly phone numbers. Better use the name as primary ordering field. Then use ID to avoid non deterministic behavior. This requires to fix some tests in 'test_mass_mailing' so that they use test models instead of existing models. That way they are not dependent on existing data and existing models definition anymore. An issue with ordering rose as we modified the ordering of contact model. Task-2150462 (Mass Mailing: Improve subscription management) Part-of: odoo/odoo#86084 --- addons/mass_mailing/models/mailing_contact.py | 2 +- .../models/mailing_models.py | 1 + .../tests/test_mailing_test.py | 67 ++++++++++--------- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/addons/mass_mailing/models/mailing_contact.py b/addons/mass_mailing/models/mailing_contact.py index bde7c2c057c..404fa4de43d 100644 --- a/addons/mass_mailing/models/mailing_contact.py +++ b/addons/mass_mailing/models/mailing_contact.py @@ -14,7 +14,7 @@ class MassMailingContact(models.Model): _name = 'mailing.contact' _inherit = ['mail.thread.blacklist'] _description = 'Mailing Contact' - _order = 'email' + _order = 'name ASC, id DESC' _mailing_enabled = True def default_get(self, fields_list): diff --git a/addons/test_mass_mailing/models/mailing_models.py b/addons/test_mass_mailing/models/mailing_models.py index e6ed803c4ba..eca2ecb18f5 100644 --- a/addons/test_mass_mailing/models/mailing_models.py +++ b/addons/test_mass_mailing/models/mailing_models.py @@ -61,6 +61,7 @@ class MailingBLacklist(models.Model): _description = 'Mailing Blacklist Enabled' _name = 'mailing.test.blacklist' _inherit = ['mail.thread.blacklist'] + _order = 'name ASC, id DESC' _primary_email = 'email_from' name = fields.Char() diff --git a/addons/test_mass_mailing/tests/test_mailing_test.py b/addons/test_mass_mailing/tests/test_mailing_test.py index 5e0d8ac149a..7aa6e6ed7d4 100644 --- a/addons/test_mass_mailing/tests/test_mailing_test.py +++ b/addons/test_mass_mailing/tests/test_mailing_test.py @@ -11,18 +11,31 @@ from odoo.tools import mute_logger @tagged('mailing_manage') class TestMailingTest(TestMassMailCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.test_records = cls.env['mailing.test.blacklist'].create([ + { + 'email_from': f'test.mailing.{idx}@test.example.com', + 'name': f'Test Mailing {idx}', + 'user_id': cls.user_marketing.id, + } + for idx in range(5) + ]) + cls.test_mailing_bl = cls.env['mailing.mailing'].create({ + 'body_html': '

Hello

', + 'mailing_domain': [('id', 'in', cls.test_records.ids)], + 'mailing_model_id': cls.env['ir.model']._get_id('mailing.test.blacklist'), + 'mailing_type': 'mail', + 'name': 'TestButton', + 'preview': 'Preview {{ object.name }}', + 'subject': 'Subject {{ object.name }}', + }) + @users('user_marketing') @mute_logger('odoo.addons.mail.models.mail_render_mixin') def test_mailing_test_button(self): - mailing = self.env['mailing.mailing'].create({ - 'name': 'TestButton', - 'subject': 'Subject {{ object.name }}', - 'preview': 'Preview {{ object.name }}', - 'state': 'draft', - 'mailing_type': 'mail', - 'body_html': '

Hello

', - 'mailing_model_id': self.env['ir.model']._get('res.partner').id, - }) + mailing = self.test_mailing_bl.with_env(self.env) mailing_test = self.env['mailing.mailing.test'].create({ 'email_to': 'test@test.com', 'mass_mailing_id': mailing.id, @@ -63,25 +76,12 @@ class TestMailingTest(TestMassMailCommon): mailing_test.send_mail_test() def test_mailing_test_equals_reality(self): - """ - Check that both test and real emails will format the qweb and inline placeholders correctly in body and subject. - """ - contact_list = self.env['mailing.list'].create({ - 'name': 'Testers', - 'contact_ids': [Command.create({ - 'name': 'Mitchell Admin', - 'email': 'real@real.com', - })], - }) - - mailing = self.env['mailing.mailing'].create({ - 'name': 'TestButton', - 'subject': 'Subject {{ object.name }} ', - 'state': 'draft', - 'mailing_type': 'mail', + """ Check that both test and real emails will format the qweb and inline + placeholders correctly in body and subject. """ + mailing = self.test_mailing_bl.with_env(self.env) + mailing.write({ 'body_html': '

Hello {{ object.name }}

', - 'mailing_model_id': self.env['ir.model']._get('mailing.list').id, - 'contact_list_ids': [contact_list.id], + 'subject': 'Subject {{ object.name }} ', }) mailing_test = self.env['mailing.mailing.test'].create({ 'email_to': 'test@test.com', @@ -91,8 +91,10 @@ class TestMailingTest(TestMassMailCommon): with self.mock_mail_gateway(): mailing_test.send_mail_test() - expected_subject = 'Subject Mitchell Admin ' - expected_body = 'Hello {{ object.name }} Mitchell Admin' + expected_test_record = self.env[mailing.mailing_model_real].search([], limit=1) + self.assertEqual(expected_test_record, self.test_records[0], 'Should take first found one') + expected_subject = f'Subject {expected_test_record.name} ' + expected_body = 'Hello {{ object.name }}' + f' {expected_test_record.name}' self.assertSentEmail(self.env.user.partner_id, ['test@test.com'], subject=expected_subject, @@ -103,6 +105,9 @@ class TestMailingTest(TestMassMailCommon): mailing.action_launch() self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').method_direct_trigger() - self.assertSentEmail(self.env.user.partner_id, ['real@real.com'], + self.assertSentEmail( + self.env.user.partner_id, + [expected_test_record.email_from], subject=expected_subject, - body_content=expected_body) + body_content=expected_body, + ) From c559eef741c30e1a8cfc86b45c420b26f61fdbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 May 2022 10:58:59 +0200 Subject: [PATCH 03/14] [REF] mass_mailing: cleanup controller access code PURPOSE Overall cleaning of subscription and exclusion management code from portal. This code comes mainly from v12 and can now benefit from cleaning and update. SPECIFICATIONS Purpose of this commit is to better organize controllers about access control. First do access control in a clean check method, then have business code. We also use more frontend oriented errors like BadRequest or Unauthorized. A main tool method now correctly raises depending on given input. Controllers have the responsibility to let it raise, return a keyword of even skip error if the flow allows it. If something is wrong, simply raise (http) or return (json) generic errors in main cases to avoid leaking information. Task-2669037 (Mass Mailing: Refactor js/portal for subscription) Part-of: odoo/odoo#86084 --- addons/mass_mailing/controllers/main.py | 316 ++++++++++-------- .../tests/test_mailing_controllers.py | 26 +- 2 files changed, 182 insertions(+), 160 deletions(-) diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index df3089ea121..d640fc21078 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -3,21 +3,27 @@ import base64 -from odoo import _, exceptions, http, tools +from markupsafe import Markup, escape +from werkzeug.exceptions import BadRequest, NotFound, Unauthorized + +from odoo import _, http, tools from odoo.http import request, Response from odoo.tools import consteq -from markupsafe import Markup, escape -from werkzeug.exceptions import BadRequest, NotFound - class MassMailController(http.Controller): - def _check_mailing_email_token(self, mailing_id, res_id, email, token): - if not (mailing_id and res_id and email and token): - return False - mailing = request.env['mailing.mailing'].sudo().browse(mailing_id) - return consteq(mailing._generate_mailing_recipient_token(res_id, email), token) + def _check_mailing_email_token(self, mailing_id, document_id, email, hash_token): + """ Return the mailing based on given credentials, sudo-ed. Raises if + there is an issue fetching it. """ + if not mailing_id or not email or not document_id or not hash_token: + raise BadRequest() + mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id) + if not mailing_sudo.exists(): + raise NotFound() + if not consteq(mailing_sudo._generate_mailing_recipient_token(document_id, email), hash_token): + raise Unauthorized() + return mailing_sudo # ------------------------------------------------------------ # SUBSCRIPTION MANAGEMENT @@ -25,90 +31,92 @@ class MassMailController(http.Controller): @http.route(['/mailing//unsubscribe'], type='http', website=True, auth='public') def mailing_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post): - mailing = request.env['mailing.mailing'].sudo().browse(mailing_id) - if mailing.exists(): - res_id = res_id and int(res_id) - if not self._check_mailing_email_token(mailing_id, res_id, email, str(token)): - raise exceptions.AccessDenied() + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except NotFound as e: # avoid leaking ID existence + raise Unauthorized() from e - if mailing.mailing_model_real == 'mailing.contact': - # Unsubscribe directly + Let the user choose their subscriptions - mailing.update_opt_out(email, mailing.contact_list_ids.ids, True) + if mailing_sudo.mailing_model_real == 'mailing.contact': + # Unsubscribe directly + Let the user choose their subscriptions + mailing_sudo.update_opt_out(email, mailing_sudo.contact_list_ids.ids, 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]) + 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]) - 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) - return request.render('mass_mailing.page_mailing_unsubscribe', { - 'contacts': contacts, - 'list_ids': list_ids, - 'opt_out_list_ids': opt_out_list_ids, - 'unsubscribed_list': unsubscribed_list, - 'email': email, - 'mailing_id': mailing_id, - 'res_id': res_id, - 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), - }) - else: - opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([ - ('contact_id.email_normalized', '=', email), - ('opt_out', '=', False) - ]).mapped('list_id') + 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_sudo.contact_list_ids if list.is_public) + return request.render('mass_mailing.page_mailing_unsubscribe', { + 'contacts': contacts, + 'list_ids': list_ids, + 'opt_out_list_ids': opt_out_list_ids, + 'unsubscribed_list': unsubscribed_list, + 'email': email, + 'mailing_id': mailing_id, + 'res_id': res_id, + 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), + }) - message = Markup('

%s

') % Markup( - _( - 'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing, res_id) - ) - ) - _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) + opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([ + ('contact_id.email_normalized', '=', email), + ('opt_out', '=', False) + ]).mapped('list_id') - return request.render('mass_mailing.page_mailing_unsubscribe_done', { - 'email': email, - 'mailing_id': mailing_id, - 'res_id': res_id, - 'list_ids': opt_in_lists, - 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( - 'mass_mailing.show_blacklist_buttons'), - }) - return request.redirect('/web') + message = Markup('

%s

') % Markup( + _( + 'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)', + **self._format_bl_request(mailing_sudo, res_id) + ) + ) + _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) + + return request.render('mass_mailing.page_mailing_unsubscribe_done', { + 'email': email, + 'mailing_id': mailing_id, + 'res_id': res_id, + 'list_ids': opt_in_lists, + 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( + 'mass_mailing.show_blacklist_buttons'), + }) @http.route('/mailing/list/update', type='json', auth='public') def mailing_update_list_subscription(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token): - mailing = request.env['mailing.mailing'].sudo().browse(mailing_id) - if mailing.exists(): - if not self._check_mailing_email_token(mailing_id, res_id, email, token): - return 'unauthorized' - mailing.update_opt_out(email, opt_in_ids, False) - mailing.update_opt_out(email, opt_out_ids, True) - return True - return 'error' + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except BadRequest: + return 'error' + except (NotFound, Unauthorized): + return 'unauthorized' + + mailing_sudo.update_opt_out(email, opt_in_ids, False) + mailing_sudo.update_opt_out(email, opt_out_ids, True) + return True @http.route('/mailing/feedback', type='json', auth='public') def mailing_send_feedback(self, mailing_id, res_id, email, feedback, token): - mailing = request.env['mailing.mailing'].sudo().browse(mailing_id) - if mailing.exists() and email: - if not self._check_mailing_email_token(mailing_id, res_id, email, token): - return 'unauthorized' - model = request.env[mailing.mailing_model_real] - records = model.sudo().search([('email_normalized', '=', tools.email_normalize(email))]) - for record in records: - record.sudo().message_post(body=_("Feedback from %(email)s: %(feedback)s", email=email, feedback=feedback)) - return bool(records) - return 'error' + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except BadRequest: + return 'error' + except (NotFound, Unauthorized): + return 'unauthorized' + + model = request.env[mailing_sudo.mailing_model_real] + records = model.sudo().search([('email_normalized', '=', tools.email_normalize(email))]) + for record in records: + record.sudo().message_post(body=_("Feedback from %(email)s: %(feedback)s", email=email, feedback=feedback)) + return bool(records) @http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public', sitemap=False) def mailing_unsubscribe_placeholder_link(self, **post): """Dummy route so placeholder is not prefixed by language, MUST have multilang=False""" - raise NotFound() + raise BadRequest() # ------------------------------------------------------------ # TRACKING @@ -119,7 +127,7 @@ class MassMailController(http.Controller): """ Email tracking. """ expected_token = request.env['mail.mail']._generate_mail_recipient_token(mail_id) if not consteq(token, expected_token): - raise BadRequest() + raise Unauthorized() request.env['mailing.trace'].sudo().set_opened(domain=[('mail_mail_id_int', 'in', [mail_id])]) response = Response() @@ -148,42 +156,51 @@ class MassMailController(http.Controller): @http.route('/mailing/report/unsubscribe', type='http', website=True, auth='public') def mailing_report_deactivate(self, token, user_id): if not token or not user_id: - raise NotFound() - user_id = int(user_id) - correct_token = consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user_id)) - user = request.env['res.users'].sudo().browse(user_id) - if correct_token and user.has_group('mass_mailing.group_mass_mailing_user'): - request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False) - if user.has_group('base.group_system'): - menu_id = request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id - return request.render('mass_mailing.mailing_report_deactivated', {'menu_id': menu_id}) - return request.render('mass_mailing.mailing_report_deactivated') - raise NotFound() + raise BadRequest() + user = request.env['res.users'].sudo().browse(int(user_id)).exists() + if not user or not user.has_group('mass_mailing.group_mass_mailing_user') or \ + not consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user.id)): + raise Unauthorized() + + request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False) + render_vals = {} + if user.has_group('base.group_system'): + render_vals = {'menu_id': request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id} + return request.render('mass_mailing.mailing_report_deactivated', render_vals) @http.route(['/mailing//view'], type='http', website=True, auth='public') def mailing_view_in_browser(self, mailing_id, email=None, res_id=None, token=""): - mailing = request.env['mailing.mailing'].sudo().browse(mailing_id) - if mailing.exists(): - res_id = int(res_id) if res_id else False - if not self._check_mailing_email_token(mailing_id, res_id, email, str(token)) and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'): - raise exceptions.AccessDenied() + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except NotFound as e: + raise Unauthorized() from e + except (BadRequest, Unauthorized): + if not request.env.user.has_group('mass_mailing.group_mass_mailing_user'): + raise + mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id).exists() + if not mailing_sudo: + raise - # do not force lang, will simply use user context - html_markupsafe = mailing._render_field( - 'body_html', - [res_id], - compute_lang=False, - options={'post_process': False} - )[res_id] - # Update generic URLs (without parameters) to final ones - html_markupsafe = html_markupsafe.replace('/unsubscribe_from_list', - mailing._get_unsubscribe_url(email, res_id)) + # do not force lang, will simply use user context + res_id = int(res_id) if res_id and res_id.isdigit() else 0 + html_markupsafe = mailing_sudo._render_field( + 'body_html', + [res_id], + compute_lang=False, + options={'post_process': False} + )[res_id] + # Update generic URLs (without parameters) to final ones + html_markupsafe = html_markupsafe.replace( + '/unsubscribe_from_list', + mailing_sudo._get_unsubscribe_url(email, res_id) + ) - return request.render('mass_mailing.mailing_view', { - 'body': html_markupsafe, - }) - - return request.redirect('/web') + return request.render( + 'mass_mailing.mailing_view', + { + 'body': html_markupsafe, + }, + ) # ------------------------------------------------------------ # BLACKLIST @@ -191,54 +208,59 @@ class MassMailController(http.Controller): @http.route('/mailing/blacklist/check', type='json', auth='public') def mail_blacklist_check(self, mailing_id, res_id, email, token): - if not self._check_mailing_email_token(mailing_id, res_id, email, token): + try: + _mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except BadRequest: + return 'error' + except (NotFound, Unauthorized): return 'unauthorized' - if email: - record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))]) - if record['active']: - return True - return False - return 'error' + + record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))]) + return record['active'] @http.route('/mailing/blacklist/add', type='json', auth='public') def mail_blacklist_add(self, mailing_id, res_id, email, token): - if not self._check_mailing_email_token(mailing_id, res_id, email, token): + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except BadRequest: + return 'error' + except (NotFound, Unauthorized): return 'unauthorized' - if email: - if mailing_id and res_id: - mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id) - message = Markup('

%s

') % Markup( - _( - 'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing_sudo, res_id) - ) - ) - else: - message = Markup('

%s

') % _('Blocklist request from portal') - _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) - return True - return 'error' + if mailing_sudo: + message = Markup('

%s

') % Markup( + _( + 'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)', + **self._format_bl_request(mailing_sudo, res_id) + ) + ) + else: + message = Markup('

%s

') % _('Blocklist request from portal') + + _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) + return True @http.route('/mailing/blacklist/remove', type='json', auth='public') def mail_blacklist_remove(self, mailing_id, res_id, email, token): - if not self._check_mailing_email_token(mailing_id, res_id, email, token): + try: + mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + except BadRequest: + return 'error' + except (NotFound, Unauthorized): return 'unauthorized' - if email: - if mailing_id and res_id: - mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id) - message = Markup('

%s

') % Markup( - _( - 'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing_sudo, res_id) - ) - ) - else: - message = Markup('

%s

') % _('Blocklist removal request from portal') - _blacklist_rec = request.env['mail.blacklist'].sudo()._remove(email, message=message) - return True - return 'error' + if mailing_sudo and res_id: + message = Markup('

%s

') % Markup( + _( + 'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)', + **self._format_bl_request(mailing_sudo, res_id) + ) + ) + else: + message = Markup('

%s

') % _('Blocklist removal request from portal') + + _blacklist_rec = request.env['mail.blacklist'].sudo()._remove(email, message=message) + return True def _format_bl_request(self, mailing, document_id): mailing_model_name = request.env['ir.model']._get(mailing.mailing_model_real).display_name diff --git a/addons/mass_mailing/tests/test_mailing_controllers.py b/addons/mass_mailing/tests/test_mailing_controllers.py index 7c620d4216c..f8789c1139f 100644 --- a/addons/mass_mailing/tests/test_mailing_controllers.py +++ b/addons/mass_mailing/tests/test_mailing_controllers.py @@ -132,10 +132,10 @@ class TestMailingControllers(TestMailingControllersCommon): self.authenticate('user_marketing', 'user_marketing') # TEST: various invalid cases - for test_user_id, test_token in [ - (self.user_marketing.id, ''), # no token - (self.user_marketing.id, 'zboobs'), # invalid token - (self.env.uid, hash_token), # invalid credentials + for test_user_id, test_token, error_code in [ + (self.user_marketing.id, '', 400), # no token + (self.user_marketing.id, 'zboobs', 418), # invalid token + (self.env.uid, hash_token, 418), # invalid credentials ]: with self.subTest(test_user_id=test_user_id, test_token=test_token): res = self.url_open( @@ -144,7 +144,7 @@ class TestMailingControllers(TestMailingControllersCommon): f'mailing/report/unsubscribe?user_id={test_user_id}&token={test_token}', ) ) - self.assertEqual(res.status_code, 404) + self.assertEqual(res.status_code, error_code) self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports')) # TEST: not mailing user @@ -157,7 +157,7 @@ class TestMailingControllers(TestMailingControllersCommon): f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}', ) ) - self.assertEqual(res.status_code, 404) + self.assertEqual(res.status_code, 418) self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports')) # TEST: finally valid call @@ -387,11 +387,11 @@ class TestMailingControllers(TestMailingControllersCommon): self.authenticate('user_marketing', 'user_marketing') # TEST: various invalid cases - for test_res_id, test_email, test_token in [ - (res_id, email_normalized, ''), # no token - (res_id, email_normalized, 'zboobs'), # wrong token - (self.env.user.partner_id.id, email_normalized, hash_token), # mismatch - (res_id, 'not.email@example.com', hash_token), # mismatch + for test_res_id, test_email, test_token, error_code in [ + (res_id, email_normalized, '', 400), # no token + (res_id, email_normalized, 'zboobs', 418), # wrong token + (self.env.user.partner_id.id, email_normalized, hash_token, 418), # mismatch + (res_id, 'not.email@example.com', hash_token, 418), # mismatch ]: with self.subTest(test_email=test_email, test_res_id=test_res_id, test_token=test_token): res = self.url_open( @@ -400,7 +400,7 @@ class TestMailingControllers(TestMailingControllersCommon): f'mailing/{test_mailing.id}/view?email={test_email}&res_id={test_res_id}&token={test_token}', ) ) - self.assertEqual(res.status_code, 403) + self.assertEqual(res.status_code, error_code) # TEST: valid call using credentials res = self.url_open( @@ -490,4 +490,4 @@ class TestMailingTracking(TestMailingControllersCommon): f'mail/track/{mail_id_int}/fake_token/blank.gif' ) response = self.url_open(track_url) - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 401) From f6aea2b047bd721c085fa2c6a3de5f7e323b1960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 9 Mar 2022 13:10:07 +0100 Subject: [PATCH 04/14] [MOV] mass_mailing: split main unsubscribe controller PURPOSE Overall cleaning of subscription and exclusion management code from portal. This code comes mainly from v12 and can now benefit from cleaning and update. SPECIFICATIONS Main portal controller of mass mailing is currently the unsubscribe controller. This controller has two main behavior: * one is used when un-subscribing from a mailing based on mailing lists (opt-out from lists); * one is used when un-subscribing from a mailing based on documents (like registrations or applicants). This one directly sets emails in exclusion list; Better split it into two sub methods so that it is easier to understand and modify in future commits. Task-2669037 (Mass Mailing: Refactor js/portal for subscription) Part-of: odoo/odoo#86084 --- addons/mass_mailing/controllers/main.py | 80 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index d640fc21078..4c5d18e52fe 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -37,32 +37,39 @@ class MassMailController(http.Controller): raise Unauthorized() from e if mailing_sudo.mailing_model_real == 'mailing.contact': - # Unsubscribe directly + Let the user choose their subscriptions - mailing_sudo.update_opt_out(email, mailing_sudo.contact_list_ids.ids, True) + return self._mailing_unsubscribe_from_list(mailing_sudo, res_id, email, token) + return self._mailing_unsubscribe_from_document(mailing_sudo, res_id, email, token) - 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]) + def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token): + # Unsubscribe directly + Let the user choose their subscriptions + mailing.update_opt_out(email, mailing.contact_list_ids.ids, True) - 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_sudo.contact_list_ids if list.is_public) - return request.render('mass_mailing.page_mailing_unsubscribe', { - 'contacts': contacts, - 'list_ids': list_ids, - 'opt_out_list_ids': opt_out_list_ids, - 'unsubscribed_list': unsubscribed_list, - 'email': email, - 'mailing_id': mailing_id, - 'res_id': res_id, - 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), - }) + 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]) + 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, + ) + ) + + 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) @@ -71,19 +78,34 @@ class MassMailController(http.Controller): message = Markup('

%s

') % Markup( _( 'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing_sudo, res_id) + **self._format_bl_request(mailing, document_id) ) ) _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) - return request.render('mass_mailing.page_mailing_unsubscribe_done', { + render_values = self._prepare_mailing_subscription_values(mailing, document_id, email, hash_token) + return request.render( + 'mass_mailing.page_mailing_unsubscribe_done', + dict( + **render_values, + list_ids=opt_in_lists, + ) + ) + + def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token): + """ Prepare common values used in various subscription management or + blacklist flows done in portal. """ + return { + # customer + 'document_id': document_id, 'email': email, - 'mailing_id': mailing_id, - 'res_id': res_id, - 'list_ids': opt_in_lists, + 'mailing_id': mailing.id, + 'res_id': document_id, + 'token': hash_token, + # blacklist 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( 'mass_mailing.show_blacklist_buttons'), - }) + } @http.route('/mailing/list/update', type='json', auth='public') def mailing_update_list_subscription(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token): From 32e3917b3503970b8f1fa4de506a81c66b00f668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 May 2022 12:05:05 +0200 Subject: [PATCH 05/14] [REF] mass_mailing: rewrite subscription management JS PURPOSE Overall cleaning of subscription and exclusion management code from portal. This code comes mainly from v12 and can now benefit from cleaning and update. SPECIFICATIONS Purpose of this commit is to get rid of old declarative javascript used in mass mailing. It is still using old fashioned JS, not using widget or any standard way of doing JS-based behavior in Odoo. In this commit we introduce a main widget that manages the subscription page. It has 3 sub widgets to manage parts of its features * blocklist management: add or remove current email from exclusion list. Adding an email in exclusion list is doable only if activated in settings; * feedback: allow user to give their feedback. It currently logs on target model, based on a hardcoded search on 'email_normalized' field. This is strange as not all models have a normalized email field, but who am I to judge anyway ? * subscription management: allow to opt-in and opt-out from mailing lists; This commit mainly keeps the current behavior, just rewrites the JS part. Some further feature or UI update will move into sub-widgets in subsequent commits. Some Markup issues are also fixed in this commit, as code is already updated. Task-2669037 (Mass Mailing: Refactor js/portal for subscription) Part-of: odoo/odoo#86084 --- addons/mass_mailing/__manifest__.py | 5 +- addons/mass_mailing/controllers/main.py | 94 +++++---- .../static/src/js/mailing_portal.js | 195 ------------------ .../src/js/mailing_portal_subscription.js | 155 ++++++++++++++ .../mailing_portal_subscription_blocklist.js | 109 ++++++++++ .../mailing_portal_subscription_feedback.js | 74 +++++++ .../js/mailing_portal_subscription_form.js | 63 ++++++ .../static/src/scss/mailing_portal.scss | 2 +- ...ailing_portal_unsubscribe_from_document.js | 26 +-- .../mailing_portal_unsubscribe_from_list.js | 36 ++-- .../mailing_templates_portal_unsubscribe.xml | 168 ++++++++------- 11 files changed, 589 insertions(+), 338 deletions(-) delete mode 100644 addons/mass_mailing/static/src/js/mailing_portal.js create mode 100644 addons/mass_mailing/static/src/js/mailing_portal_subscription.js create mode 100644 addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js create mode 100644 addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js create mode 100644 addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index 69465997bcc..547941ac0e1 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -103,7 +103,10 @@ ], 'mass_mailing.mailing_assets': [ 'mass_mailing/static/src/scss/mailing_portal.scss', - 'mass_mailing/static/src/js/mailing_portal.js', + 'mass_mailing/static/src/js/mailing_portal_subscription.js', + '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', ], '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 4c5d18e52fe..b95074ee3e8 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -25,6 +25,15 @@ class MassMailController(http.Controller): raise Unauthorized() return mailing_sudo + def _fetch_blocklist_record(self, email): + if not email or not tools.email_normalize(email): + return None + return request.env['mail.blacklist'].sudo().with_context( + active_test=False + ).search( + [('email', '=', tools.email_normalize(email))] + ) + # ------------------------------------------------------------ # SUBSCRIPTION MANAGEMENT # ------------------------------------------------------------ @@ -75,13 +84,13 @@ class MassMailController(http.Controller): ('opt_out', '=', False) ]).mapped('list_id') - message = Markup('

%s

') % Markup( + message = Markup( _( 'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)', **self._format_bl_request(mailing, document_id) ) ) - _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) + _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( @@ -94,36 +103,51 @@ class MassMailController(http.Controller): def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token): """ Prepare common values used in various subscription management or - blacklist flows done in portal. """ + blocklist flows done in portal. """ + mail_blocklist = self._fetch_blocklist_record(email) + email_normalized = tools.email_normalize(email) return { # customer 'document_id': document_id, 'email': email, + 'email_valid': bool(email_normalized), + 'hash_token': hash_token, 'mailing_id': mailing.id, 'res_id': document_id, - 'token': hash_token, - # blacklist - 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( - 'mass_mailing.show_blacklist_buttons'), + # blocklist + 'blocklist_enabled': bool( + request.env['ir.config_parameter'].sudo().get_param( + 'mass_mailing.show_blacklist_buttons', + default=True, + ) + ), + 'blocklist_possible': mail_blocklist is not None, + 'is_blocklisted': mail_blocklist.active if mail_blocklist else False, } - @http.route('/mailing/list/update', type='json', auth='public') - def mailing_update_list_subscription(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token): + @http.route('/mailing/list/update', type='json', auth='public', csrf=True) + def mailing_update_list_subscription(self, mailing_id=None, document_id=None, + email=None, hash_token=None, + opt_in_ids=None, opt_out_ids=None, **post): try: - mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + mailing_sudo = self._check_mailing_email_token(mailing_id, document_id, email, hash_token) except BadRequest: return 'error' except (NotFound, Unauthorized): return 'unauthorized' - mailing_sudo.update_opt_out(email, opt_in_ids, False) - mailing_sudo.update_opt_out(email, opt_out_ids, True) + if opt_in_ids: + mailing_sudo.update_opt_out(email, opt_in_ids, False) + if opt_out_ids: + mailing_sudo.update_opt_out(email, opt_out_ids, True) return True - @http.route('/mailing/feedback', type='json', auth='public') - def mailing_send_feedback(self, mailing_id, res_id, email, feedback, token): + @http.route('/mailing/feedback', type='json', auth='public', csrf=True) + def mailing_send_feedback(self, mailing_id=None, document_id=None, + email=None, hash_token=None, + feedback=None, **post): try: - mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + mailing_sudo = self._check_mailing_email_token(mailing_id, document_id, email, hash_token) except BadRequest: return 'error' except (NotFound, Unauthorized): @@ -228,60 +252,50 @@ class MassMailController(http.Controller): # BLACKLIST # ------------------------------------------------------------ - @http.route('/mailing/blacklist/check', type='json', auth='public') - def mail_blacklist_check(self, mailing_id, res_id, email, token): + @http.route('/mailing/blocklist/add', type='json', auth='public') + def mail_blocklist_add(self, mailing_id=None, document_id=None, + email=None, hash_token=None): try: - _mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) - except BadRequest: - return 'error' - except (NotFound, Unauthorized): - return 'unauthorized' - - record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))]) - return record['active'] - - @http.route('/mailing/blacklist/add', type='json', auth='public') - def mail_blacklist_add(self, mailing_id, res_id, email, token): - try: - mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + mailing_sudo = self._check_mailing_email_token(mailing_id, document_id, email, hash_token) except BadRequest: return 'error' except (NotFound, Unauthorized): return 'unauthorized' if mailing_sudo: - message = Markup('

%s

') % Markup( + message = Markup( _( 'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing_sudo, res_id) + **self._format_bl_request(mailing_sudo, document_id) ) ) else: message = Markup('

%s

') % _('Blocklist request from portal') - _blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) + _blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message) return True - @http.route('/mailing/blacklist/remove', type='json', auth='public') - def mail_blacklist_remove(self, mailing_id, res_id, email, token): + @http.route('/mailing/blocklist/remove', type='json', auth='public') + def mail_blocklist_remove(self, mailing_id=None, document_id=None, + email=None, hash_token=None): try: - mailing_sudo = self._check_mailing_email_token(mailing_id, res_id, email, token) + mailing_sudo = self._check_mailing_email_token(mailing_id, document_id, email, hash_token) except BadRequest: return 'error' except (NotFound, Unauthorized): return 'unauthorized' - if mailing_sudo and res_id: - message = Markup('

%s

') % Markup( + if mailing_sudo and document_id: + message = Markup( _( 'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)', - **self._format_bl_request(mailing_sudo, res_id) + **self._format_bl_request(mailing_sudo, document_id) ) ) else: message = Markup('

%s

') % _('Blocklist removal request from portal') - _blacklist_rec = request.env['mail.blacklist'].sudo()._remove(email, message=message) + _blocklist_rec = request.env['mail.blacklist'].sudo()._remove(email, message=message) return True def _format_bl_request(self, mailing, document_id): diff --git a/addons/mass_mailing/static/src/js/mailing_portal.js b/addons/mass_mailing/static/src/js/mailing_portal.js deleted file mode 100644 index 7be8aafd2f7..00000000000 --- a/addons/mass_mailing/static/src/js/mailing_portal.js +++ /dev/null @@ -1,195 +0,0 @@ -/** @odoo-module **/ - -import { escape } from "@web/core/utils/strings"; -import { whenReady } from "@odoo/owl"; -import { _t } from "@web/core/l10n/translation"; -import { jsonrpc } from "@web/core/network/rpc_service"; - -whenReady(() => { - var email = $("input[name='email']").val(); - var mailing_id = parseInt($("input[name='mailing_id']").val()); - var res_id = parseInt($("input[name='res_id']").val()); - var token = (location.search.split('token' + '=')[1] || '').split('&')[0]; - - if ($('.o_unsubscribe_form').length) { - if (email != '' && email != undefined){ - jsonrpc('/mailing/blacklist/check', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token}) - .then(function (result) { - if (result == 'unauthorized'){ - $('#button_add_blacklist').hide(); - $('#button_remove_blacklist').hide(); - } - else if (result == true) { - $('#button_remove_blacklist').show(); - toggle_opt_out_section(false); - } - else if (result == false) { - $('#button_add_blacklist').show(); - toggle_opt_out_section(true); - } - else { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - } - }) - .guardedCatch(function () { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - }); - } - else { - $('#div_blacklist').hide(); - } - - var unsubscribed_list = $("input[name='unsubscribed_list']").val(); - if (unsubscribed_list){ - $("#subscription_info").html( - _t( - "You have been successfully unsubscribed from %s.", - escape(unsubscribed_list) - ) - ) - } - else{ - $('#subscription_info').html(_t('You have been successfully unsubscribed.')); - } - - $('#unsubscribe_form').on('submit', function (e) { - e.preventDefault(); - - var checked_ids = []; - $("input[type='checkbox']:checked").each(function (i){ - checked_ids[i] = parseInt($(this).val()); - }); - - var unchecked_ids = []; - $("input[type='checkbox']:not(:checked)").each(function (i){ - unchecked_ids[i] = parseInt($(this).val()); - }); - - jsonrpc('/mailing/list/update', {'opt_in_ids': checked_ids, 'opt_out_ids': unchecked_ids, 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token}) - .then(function (result) { - if (result == 'unauthorized'){ - $('#subscription_info').text(_t('You are not authorized to do this!')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); - } - else if (result == true) { - $('#subscription_info').text(_t('Your changes have been saved.')); - $('#info_state').removeClass('alert-info').addClass('alert-success'); - } - else { - $('#subscription_info').text(_t('An error occurred. Your changes have not been saved, try again later.')); - $('#info_state').removeClass('alert-info').addClass('alert-warning'); - } - }) - .guardedCatch(function () { - $('#subscription_info').text(_t('An error occurred. Your changes have not been saved, try again later.')); - $('#info_state').removeClass('alert-info').addClass('alert-warning'); - }); - }); - - // ================== - // Blacklist - // ================== - $('#button_add_blacklist').click(function (e) { - e.preventDefault(); - - jsonrpc('/mailing/blacklist/add', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token}) - .then(function (result) { - if (result == 'unauthorized'){ - $('#subscription_info').text(_t('You are not authorized to do this!')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); - } - else - { - if (result) { - $('#subscription_info').html(_t('You have been successfully added to our blacklist. ' - + 'You will not be contacted anymore by our services.')); - $('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success'); - toggle_opt_out_section(false); - } - else { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - } - $('#button_add_blacklist').hide(); - $('#button_remove_blacklist').show(); - $('#unsubscribed_info').hide(); - } - }) - .guardedCatch(function () { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - }); - }); - - $('#button_remove_blacklist').click(function (e) { - e.preventDefault(); - - jsonrpc('/mailing/blacklist/remove', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token}) - .then(function (result) { - if (result == 'unauthorized'){ - $('#subscription_info').text(_t('You are not authorized to do this!')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); - } - else - { - if (result) { - $('#subscription_info').html(_t("You have been successfully removed from our blacklist. " - + "You are now able to be contacted by our services.")); - $('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success'); - toggle_opt_out_section(true); - } - else { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - } - $('#button_add_blacklist').show(); - $('#button_remove_blacklist').hide(); - $('#unsubscribed_info').hide(); - } - }) - .guardedCatch(function () { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); - }); - }); - - // ================== - // Feedback - // ================== - $('#button_feedback').click(function (e) { - var feedback = $("textarea[name='opt_out_feedback']").val(); - e.preventDefault(); - jsonrpc('/mailing/feedback', {'mailing_id': mailing_id, 'res_id': res_id, 'email': email, 'feedback': feedback, 'token': token}) - .then(function (result) { - if (result == 'unauthorized'){ - $('#subscription_info').text(_t('You are not authorized to do this!')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); - } - else if (result == true){ - $('#subscription_info').text(_t('Thank you! Your feedback has been sent successfully!')); - $('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success'); - $("#div_feedback").hide(); - } - else { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); - } - }) - .guardedCatch(function () { - $('#subscription_info').text(_t('An error occurred. Please try again later or contact us.')); - $('#info_state').removeClass('alert-info').removeClass('alert-success').removeClass('alert-error').addClass('alert-warning'); - }); - }); - } -}); - -function toggle_opt_out_section(value) { - var result = !value; - $("#div_opt_out").find('*').attr('disabled',result); - $("#button_add_blacklist").attr('disabled', false); - $("#button_remove_blacklist").attr('disabled', false); - if (value) { $('[name="button_subscription"]').addClass('clickable'); } - else { $('[name="button_subscription"]').removeClass('clickable'); } -} diff --git a/addons/mass_mailing/static/src/js/mailing_portal_subscription.js b/addons/mass_mailing/static/src/js/mailing_portal_subscription.js new file mode 100644 index 00000000000..646f42906e7 --- /dev/null +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription.js @@ -0,0 +1,155 @@ +/** @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({ + custom_events: { + 'blocklist_add': '_onBlocklistAdd', + 'blocklist_remove': '_onBlocklistRemove', + 'feedback_sent': '_onFeedbackSent', + 'subscription_updated': '_onSubscriptionUpdated', + }, + selector: '#o_mailing_portal_subscription', + + /** + * @override + */ + start: function () { + 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; + + // 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(); + this._attachForm(); + this._updateDisplay(); + return this._super.apply(this, arguments); + }, + + _attachBlocklist: function () { + if (this.$bl_elem.length) { + this.blocklistWidget = new publicWidget.registry.MailingPortalSubscriptionBlocklist( + this, + {customerData: this.customerData} + ); + this.blocklistWidget.attachTo(this.$bl_elem); + } + }, + + _attachFeedback: function () { + if (this.$feedback_elem.length) { + this.feedbackWidget = new publicWidget.registry.MailingPortalSubscriptionFeedback( + this, + {customerData: this.customerData} + ); + this.feedbackWidget.attachTo(this.$feedback_elem); + } + }, + + _attachForm: function () { + if (this.$form_elem.length) { + this.formWidget = new publicWidget.registry.MailingPortalSubscriptionForm( + this, + {customerData: this.customerData} + ); + this.formWidget.attachTo(this.$form_elem); + } + }, + + _onActionDone: function (callKey) { + this.lastAction = callKey; + this._updateDisplay(callKey); + this._updateSubscriptionInfo(callKey); + }, + + _onBlocklistAdd: function (event) { + const callKey = event.data.callKey; + this.customerData.isBlocklisted = event.data.isBlocklisted; + if (callKey === 'blocklist_add') { + this.customerData.feedbackEnabled = true; + } + this._onActionDone(callKey); + }, + + _onBlocklistRemove: function (event) { + const callKey = event.data.callKey; + this.customerData.isBlocklisted = false; + if (callKey === 'blocklist_remove') { + this.customerData.feedbackEnabled = true; + } + this._onActionDone(callKey); + }, + + _onFeedbackSent: function (event) { + const callKey = event.data.callKey; + if (callKey === 'feedback_sent') { + this.customerData.feedbackEnabled = true; + } + this._onActionDone(callKey); + }, + + _onSubscriptionUpdated: function (event) { + const callKey = event.data.callKey; + if (callKey === 'subscription_updated') { + this.customerData.feedbackEnabled = true; + } + this._onActionDone(callKey); + }, + + _updateDisplay: function (callKey) { + if (! this.customerData.feedbackEnabled && this.$feedback_elem.length) { + this.$feedback_elem.hide(); + } else if (this.$feedback_elem.length) { + this.$feedback_elem.show(); + } + if (this.formWidget) { + 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'); + } + }, +}); + +export default publicWidget.registry.MailingPortalSubscription; 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 new file mode 100644 index 00000000000..07582644a80 --- /dev/null +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js @@ -0,0 +1,109 @@ +/** @odoo-module alias=mailing.PortalSubscriptionBlocklist **/ + +import { jsonrpc } from "@web/core/network/rpc_service"; +import publicWidget from "@web/legacy/js/public/public_widget"; + + +publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.extend({ + events: { + 'click #button_blocklist_add': '_onBlocklistAddClick', + 'click #button_blocklist_remove': '_onBlocklistRemoveClick', + }, + + /** + * @override + */ + init: function (parent, options) { + this.customerData = options.customerData; + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + start: function () { + this._updateDisplay(); + return this._super.apply(this, arguments); + }, + + /* + * Triggers call to add current email in blocklist. Update widget internals + * and DOM accordingly (buttons display mainly). Bubble up to let parent + * handle returned result if necessary. + */ + _onBlocklistAddClick: async function (event) { + event.preventDefault(); + return await jsonrpc( + '/mailing/blocklist/add', + { + document_id: this.customerData.documentId, + email: this.customerData.email, + hash_token: this.customerData.hashToken, + mailing_id: this.customerData.mailingId, + } + ).then((result) => { + if (result === true) { + this.customerData.isBlocklisted = true; + } + this._updateDisplay(); + this.trigger_up( + 'blocklist_add', + {'callKey': result === true ? 'blocklist_add' : result, + 'isBlocklisted': result === true ? true: this.customerData.isBlocklisted, + }, + ); + }); + }, + + /* + * Triggers call to remove current email from blocklist. Update widget + * internals and DOM accordingly (buttons display mainly). Bubble up to let + * parent handle returned result if necessary. + */ + _onBlocklistRemoveClick: async function (event) { + event.preventDefault(); + return await jsonrpc( + '/mailing/blocklist/remove', + { + document_id: this.customerData.documentId, + email: this.customerData.email, + hash_token: this.customerData.hashToken, + mailing_id: this.customerData.mailingId, + } + ).then((result) => { + if (result === true) { + this.customerData.isBlocklisted = false; + } + this._updateDisplay(); + this.trigger_up( + 'blocklist_remove', + {'callKey': result === true ? 'blocklist_remove' : result, + 'isBlocklisted': result === true ? false: this.customerData.isBlocklisted, + }, + ); + }); + }, + + /* + * Display buttons 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. + */ + _updateDisplay: function () { + const buttonAddNode = document.getElementById('button_blocklist_add'); + const buttonRemoveNode = document.getElementById('button_blocklist_remove'); + if (this.customerData.blocklistEnabled && this.customerData.blocklistPossible && !this.customerData.isBlocklisted) { + buttonAddNode.classList.remove('d-none'); + } else { + buttonAddNode.classList.add('d-none'); + } + if (this.customerData.isBlocklisted) { + buttonRemoveNode.classList.remove('d-none'); + } else { + buttonRemoveNode.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 new file mode 100644 index 00000000000..f6ac32a04b2 --- /dev/null +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_feedback.js @@ -0,0 +1,74 @@ +/** @odoo-module alias=mailing.PortalSubscriptionFeedback **/ + +import { jsonrpc } from "@web/core/network/rpc_service"; +import publicWidget from "@web/legacy/js/public/public_widget"; + + +publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.extend({ + events: { + 'click #button_feedback': '_onFeedbackClick', + }, + + /** + * @override + */ + init: function (parent, options) { + this.customerData = options.customerData; + this.allowFeedback = true; + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + start: function () { + this._updateDisplay(); + return this._super.apply(this, arguments); + }, + + /* + * Triggers call to give a feedback about current subscription update. + * Bubble up to let parent handle returned result if necessary. + */ + _onFeedbackClick: async function (event) { + event.preventDefault(); + const formData = new FormData(document.querySelector('div#o_mailing_subscription_feedback form')); + return await jsonrpc( + '/mailing/feedback', + { + csrf_token: formData.get('csrf_token'), + document_id: this.customerData.documentId, + email: this.customerData.email, + feedback: formData.get('feedback'), + hash_token: this.customerData.hashToken, + mailing_id: this.customerData.mailingId, + } + ).then((result) => { + if (result === true) { + this._updateDisplay(true); + } + this.trigger_up( + 'feedback_sent', + {'callKey': result === true ? 'feedback_sent' : result}, + ); + }); + }, + + /* + * Update display after option changes, notably feedback textarea not being + * always accessible. + */ + _updateDisplay: function (cleanFeedback) { + const feedbackArea = document.querySelector('div#o_mailing_subscription_feedback textarea'); + if (this.allowFeedback) { + feedbackArea.classList.remove('d-none'); + } else { + feedbackArea.classList.add('d-none'); + } + if (cleanFeedback) { + feedbackArea.values = ''; + } + }, +}); + +export default publicWidget.registry.MailingPortalSubscriptionFeedback; 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 new file mode 100644 index 00000000000..d056c63e2f3 --- /dev/null +++ b/addons/mass_mailing/static/src/js/mailing_portal_subscription_form.js @@ -0,0 +1,63 @@ +/** @odoo-module alias=mailing.PortalSubscriptionForm **/ + +import { jsonrpc } from "@web/core/network/rpc_service"; +import publicWidget from "@web/legacy/js/public/public_widget"; + + +publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend({ + events: { + 'click #button_form_send': '_onFormSend', + }, + + /** + * @override + */ + init: function (parent, options) { + this.customerData = options.customerData; + return this._super.apply(this, arguments); + }, + + /* + * Triggers call to update list subscriptions. Bubble up to let parent + * handle returned result if necessary. + */ + _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)); + return await jsonrpc( + '/mailing/list/update', + { + csrf_token: formData.get('csrf_token'), + document_id: this.customerData.documentId, + email: this.customerData.email, + hash_token: this.customerData.hashToken, + mailing_id: this.customerData.mailingId, + opt_in_ids: mailingListIds, + } + ).then((result) => { + this.trigger_up( + 'subscription_updated', + {'callKey': result === true ? 'subscription_updated' : result}, + ); + }); + }, + + /** + * Set form elements as readonly, e.g. because blocklisted email take precedence + * @private + */ + _setReadonly: function (isReadonly) { + const formInputNodes = document.querySelectorAll('#o_mailing_subscription_form form input'); + const formButtonNode = document.getElementById('button_form_send'); + if (isReadonly) { + formInputNodes.forEach(node => {node.setAttribute('disabled', 'disabled')}); + formButtonNode.setAttribute('disabled', 'disabled'); + } else { + formInputNodes.forEach(node => {node.removeAttribute('disabled')}); + formButtonNode.removeAttribute('disabled'); + } + }, +}); + +export default publicWidget.registry.MailingPortalSubscriptionForm; diff --git a/addons/mass_mailing/static/src/scss/mailing_portal.scss b/addons/mass_mailing/static/src/scss/mailing_portal.scss index 85ea4a0f20f..0813e65f3e5 100644 --- a/addons/mass_mailing/static/src/scss/mailing_portal.scss +++ b/addons/mass_mailing/static/src/scss/mailing_portal.scss @@ -8,7 +8,7 @@ text-transform: uppercase; } - .o_mailing_portal_list_unsubscribed { + .o_mailing_list_unsubscribed { margin-left: 20px; color: #005326; font-size: 90%; 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 1387b264018..e6518a61c24 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,22 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_documen steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#subscription_info strong:contains('successfully unsubscribed')", + trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed')", }, { content: "No warning should be displayed", - trigger: "div#div_blacklist:not(:has(p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')))", + 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')))", }, { content: "Revert exclusion list", - trigger: "div#button_remove_blacklist", + trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#subscription_info strong:contains('removed from our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", }, { content: "Now exclude me (again)", - trigger: "div#button_add_blacklist", + trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#subscription_info strong:contains('added to our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", isCheck: true, }, ], @@ -42,25 +42,25 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_documen steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#subscription_info strong:contains('successfully unsubscribed')", + trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed')", }, { content: "Display warning about mailing lists", - trigger: "div#div_blacklist p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')", + trigger: "div#o_mailing_portal_subscription p:contains('You were still subscribed to those newsletters. You will not receive any news from them anymore')", }, { content: "Warning should contain reference to memberships", - trigger: "div#div_blacklist li strong:contains('List1')", + trigger: "div#o_mailing_portal_subscription li strong:contains('List1')", }, { content: "Revert exclusion list", - trigger: "div#button_remove_blacklist", + trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#subscription_info strong:contains('removed from our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", }, { content: "Now exclude me (again)", - trigger: "div#button_add_blacklist", + trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#subscription_info strong:contains('added to our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('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 3ae73876e86..b11f7c74087 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#subscription_info strong:contains('unsubscribed from List1, List2')", + trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed from List1, List2')", }, { content: "Write feedback reason", - trigger: "textarea[name='opt_out_feedback']", + trigger: "textarea[name='feedback']", run: "text My feedback", }, { content: "Hit Send", trigger: "div#button_feedback", }, { content: "Confirmation feedback is sent", - trigger: "div#subscription_info:contains('Your feedback has been sent successfully')", + trigger: "div#o_mailing_subscription_info_state:contains('Thanks for your feedback')", }, { content: "Now exclude me", - trigger: "div#button_add_blacklist", + trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#subscription_info strong:contains('added to our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", isCheck: true, }, ], @@ -45,7 +45,7 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_wi steps: () => [ { content: "Confirmation unsubscribe is done", - trigger: "div#subscription_info strong:contains('unsubscribed from List1, List2')", + trigger: "div#o_mailing_subscription_info_state strong:contains('unsubscribed from List1, List2')", }, { content: "List1 is present, just opt-outed", trigger: "li.list-group-item:contains('List1') span:contains('Unsubscribed')", @@ -57,48 +57,48 @@ registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_wi trigger: "ul:not(:has(li.list-group-item:contains('List2') span:contains('Unsubscribed')))", }, { content: "Write feedback reason", - trigger: "textarea[name='opt_out_feedback']", + trigger: "textarea[name='feedback']", run: "text My feedback", }, { content: "Hit Send", trigger: "div#button_feedback", }, { content: "Confirmation feedback is sent", - trigger: "div#subscription_info:contains('Your feedback has been sent successfully')", + trigger: "div#o_mailing_subscription_info_state:contains('Thanks for your feedback')", }, { content: "Now exclude me", - trigger: "div#button_add_blacklist", + trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#subscription_info strong:contains('added to our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", }, { content: "This should disable the 'Update my subscriptions' button", - trigger: "button#send_form[disabled]", + trigger: "button#button_form_send[disabled]", isCheck: true, }, { content: "Revert exclusion list", - trigger: "div#button_remove_blacklist", + trigger: "div#button_blocklist_remove", }, { content: "Confirmation exclusion list is removed", - trigger: "div#subscription_info strong:contains('removed from our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('removed from our blocklist')", }, { content: "'Update my subscriptions' button usable again", - trigger: "button#send_form:not([disabled])", + trigger: "button#button_form_send:not([disabled])", }, { content: "Choose the mailing list 3 to come back", trigger: "li.list-group-item:contains('List3') input", }, { content: "Update subscription", - trigger: "button#send_form", + trigger: "button#button_form_send", }, { content: "Confirmation changes are done", - trigger: "div#subscription_info:contains('Your changes have been saved')", + trigger: "div#o_mailing_subscription_info_state strong:contains('updated your memberships')", }, { content: "Now exclude me (again)", - trigger: "div#button_add_blacklist", + trigger: "div#button_blocklist_add", }, { content: "Confirmation exclusion is done", - trigger: "div#subscription_info strong:contains('added to our blacklist')", + trigger: "div#o_mailing_subscription_info_state strong:contains('added to our blocklist')", isCheck: true, } ], diff --git a/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml b/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml index 8fa8dd490d6..16e39ce7be2 100644 --- a/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml +++ b/addons/mass_mailing/views/mailing_templates_portal_unsubscribe.xml @@ -7,67 +7,83 @@ @@ -79,16 +95,28 @@ - - - - From 42d896f8ef2d15a9a17077afc9bc6c4adbd2b8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 4 Oct 2023 21:58:11 +0200 Subject: [PATCH 14/14] [FIX] mass_mailing: this is not deterministic Part-of: odoo/odoo#86084 --- addons/test_mass_mailing/tests/test_mailing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/test_mass_mailing/tests/test_mailing.py b/addons/test_mass_mailing/tests/test_mailing.py index 1e1f5ce3071..832ef476937 100644 --- a/addons/test_mass_mailing/tests/test_mailing.py +++ b/addons/test_mass_mailing/tests/test_mailing.py @@ -591,7 +591,8 @@ class TestMassMailing(TestMassMailCommon): {'email': 'test4@test.example.com'}, {'email': 'test5@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'}], mailing, - mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5, + # mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5, + mailing_contact_2 + mailing_contact_1 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5, check_mail=True ) self.assertEqual(mailing.canceled, 2)