[REF] mass_mailing: cleanup subscriptions update (optin / optout from lists)

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 cleanup code that manages chosen lists from
portal and updates opt-in and opt-out accordingly.

Code is now located on mailing list model. When opting-it, new subscriptions
have to be created for mailing lists so that email is part of the list. When
opting-out we just have to toggle the opt_out flag on subscription model.

Logged messages are also improved. We log on contact model the updated
mailing lists for opt-in or opt-out.

Task-2669037 (Mass Mailing: Refactor js/portal for subscription)

Part-of: odoo/odoo#86084
This commit is contained in:
Thibault Delavallée
2023-10-06 06:13:53 +00:00
parent 32e3917b35
commit 11e813d7f3
7 changed files with 167 additions and 33 deletions
+24 -7
View File
@@ -34,6 +34,13 @@ class MassMailController(http.Controller):
[('email', '=', tools.email_normalize(email))]
)
def _fetch_contacts(self, email):
if not email or not tools.email_normalize(email):
return request.env['mailing.contact']
return request.env['mailing.contact'].sudo().search(
[('email_normalized', '=', tools.email_normalize(email))]
)
# ------------------------------------------------------------
# SUBSCRIPTION MANAGEMENT
# ------------------------------------------------------------
@@ -51,7 +58,7 @@ class MassMailController(http.Controller):
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)
mailing.contact_list_ids._update_subscription_from_email(email, opt_out=True)
contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
subscription_list_ids = contacts.mapped('subscription_list_ids')
@@ -128,18 +135,28 @@ class MassMailController(http.Controller):
@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):
lists_optin_ids=None, **post):
try:
mailing_sudo = self._check_mailing_email_token(mailing_id, document_id, email, hash_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 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)
contacts = self._fetch_contacts(email)
lists_optin = request.env['mailing.list'].sudo().browse(lists_optin_ids or []).exists()
# opt-out all not chosen lists
lists_to_optout = contacts.subscription_list_ids.filtered(
lambda sub: not sub.opt_out and sub.list_id not in lists_optin
).list_id
# opt-in in either already member, either public (to avoid trying to opt-in
# in private lists)
lists_to_optin = lists_optin.filtered(
lambda mlist: mlist.is_public or mlist in contacts.lists_ids
)
lists_to_optout._update_subscription_from_email(email, opt_out=True)
lists_to_optin._update_subscription_from_email(email, opt_out=False)
return True
@http.route('/mailing/feedback', type='json', auth='public', csrf=True)
-19
View File
@@ -785,25 +785,6 @@ class MassMailing(models.Model):
return values_list
def update_opt_out(self, email, list_ids, value):
if len(list_ids) > 0:
model = self.env['mailing.contact'].with_context(active_test=False)
records = model.search([('email_normalized', '=', tools.email_normalize(email))])
opt_out_records = self.env['mailing.contact.subscription'].search([
('contact_id', 'in', records.ids),
('list_id', 'in', list_ids),
('opt_out', '!=', value)
])
opt_out_records.write({'opt_out': value})
message = Markup(_('The recipient <strong>unsubscribed from %s</strong> mailing list(s)')) \
if value else Markup(_('The recipient <strong>subscribed to %s</strong> mailing list(s)'))
for record in records:
# filter the list_id by record
record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
if len(record_lists) > 0:
record.sudo().message_post(body=message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))
# ------------------------------------------------------
# A/B Test
# ------------------------------------------------------
+88 -1
View File
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, Command, fields, models
from markupsafe import Markup
from odoo import _, api, Command, fields, models, tools
from odoo.exceptions import UserError
@@ -267,6 +269,91 @@ class MassMailingList(models.Model):
def close_dialog(self):
return {'type': 'ir.actions.act_window_close'}
# ------------------------------------------------------
# SUBSCRIPTION MANAGEMENT
# ------------------------------------------------------
def _update_subscription_from_email(self, email, opt_out=True, force_message=None):
""" When opting-out: we have to switch opted-in subscriptions. We don't
need to create subscription for other lists as opt-out = not being a
member.
When opting-in: we have to switch opted-out subscriptions and create
subscription for other mailing lists id they are public. Indeed a
contact is opted-in when being subscribed in a mailing list.
:param str email: email address that should opt-in or opt-out from
mailing lists;
:param boolean opt_out: if True, opt-out from lists given by self if
'email' is member of it. If False, opt-in in lists givben by self
and create membership if not already member;
:param str force_message: if given, post a note using that body on
contact instead of generated update message. Give False to entirely
skip the note step;
"""
email_normalized = tools.email_normalize(email)
if not self or not email_normalized:
return
contacts = self.env['mailing.contact'].with_context(active_test=False).search(
[('email_normalized', '=', email_normalized)]
)
if not contacts:
return
# switch opted-in subscriptions
if opt_out:
current_opt_in = contacts.subscription_list_ids.filtered(
lambda sub: not sub.opt_out and sub.list_id in self
)
if current_opt_in:
current_opt_in.write({'opt_out': True})
# switch opted-out subscription and create missing subscriptions
else:
subscriptions = contacts.subscription_list_ids.filtered(lambda sub: sub.list_id in self)
current_opt_out = subscriptions.filtered('opt_out')
if current_opt_out:
current_opt_out.write({'opt_out': False})
# create a subscription (for a single contact) for missing lists
missing_lists = self - subscriptions.list_id
if missing_lists:
self.env['mailing.contact.subscription'].create([
{'contact_id': contacts[0].id,
'list_id': mailing_list.id}
for mailing_list in missing_lists
])
for contact in contacts:
# do not log if no opt-out / opt-in was actually done
if opt_out:
updated = current_opt_in.filtered(lambda sub: sub.contact_id == contact).list_id
else:
updated = current_opt_out.filtered(lambda sub: sub.contact_id == contact).list_id + missing_lists
if not updated:
continue
if force_message is False:
continue
if force_message:
body = force_message
elif opt_out:
body = Markup('<p>%s</p>') % _(
'%(contact_name)s unsubscribed from the following mailing list(s)',
contact_name=contact.display_name
)
body += Markup('<ul>%s</ul>' % ''.join(Markup('<li>%s</li>') % name for name in updated.mapped('name')))
else:
body = Markup('<p>%s</p>') % _(
'%(contact_name)s subscribed to the following mailing list(s)',
contact_name=contact.display_name
)
body += Markup('<ul>%s</ul>' % ''.join(Markup('<li>%s</li>') % name for name in updated.mapped('name')))
contact.with_context(mail_create_nosubscribe=True).message_post(
body=body,
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
)
# ------------------------------------------------------
# MAILING
# ------------------------------------------------------
@@ -32,8 +32,8 @@ publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend
document_id: this.customerData.documentId,
email: this.customerData.email,
hash_token: this.customerData.hashToken,
lists_optin_ids: mailingListIds,
mailing_id: this.customerData.mailingId,
opt_in_ids: mailingListIds,
}
).then((result) => {
this.trigger_up(
+5 -2
View File
@@ -326,7 +326,10 @@ class MassMailCommon(MailCommon, MassMailCase):
return cls.env['mailing.list'].with_context(cls._test_context).create({
'name': 'Test List',
'contact_ids': [
(0, 0, {'name': 'Contact %s' % i, 'email': 'contact%s@example.com' % i})
for i in range(contacts_nbr)
(0, 0, {
'name': f'Contact %{idx}',
'email': f'contact%{idx}@example.com'
})
for idx in range(contacts_nbr)
],
})
@@ -278,7 +278,7 @@ class TestMailingControllers(TestMailingControllersCommon):
message_unsub = contact_l1.message_ids[1]
self.assertEqual(
message_unsub.body,
Markup(f'<p>The recipient <strong>unsubscribed from {self.mailing_list_1.name}</strong> mailing list(s)</p>')
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p><ul><li>{self.mailing_list_1.name}</li></ul>')
)
# posted messages on exclusion list record: activated, deactivated, activated again
@@ -346,14 +346,14 @@ class TestMailingControllers(TestMailingControllersCommon):
message_unsub = contact_l1.message_ids[1]
self.assertEqual(
message_unsub.body,
Markup(f'<p>The recipient <strong>unsubscribed from {self.mailing_list_1.name}</strong> mailing list(s)</p>')
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p><ul><li>{self.mailing_list_1.name}</li></ul>')
)
# posted messages on contact record for mailing list 3: subscription
message_unsub = contact_l3.message_ids[0]
self.assertEqual(
message_unsub.body,
Markup(f'<p>The recipient <strong>subscribed to {self.mailing_list_3.name}</strong> mailing list(s)</p>')
Markup(f'<p>{contact_l1.display_name} subscribed to the following mailing list(s)</p><ul><li>{self.mailing_list_3.name}</li></ul>')
)
# posted messages on exclusion list record: activated, deactivated, activated again
@@ -274,3 +274,49 @@ class TestMailingContactImport(MassMailCommon):
contact = self.env['mailing.contact'].search([('email', '=', 'already_exists_list_1@example.com')])
self.assertEqual(len(contact), 1, 'Should have updated the existing contact instead of creating a new one')
@tagged('mailing_list')
class TestSubscriptionManagement(MassMailCommon):
@users('user_marketing')
def test_mailing_update_optout(self):
_email_formatted = '"Mireille Labeille" <mireille@test.example.com>'
_email_formatted_upd = '"Mireille Oreille-Labeille" <mireille@test.example.com>'
_email_normalized = 'mireille@test.example.com'
self._create_mailing_list()
ml_1, ml_2 = self.mailing_list_1.with_env(self.env), self.mailing_list_2.with_env(self.env)
ml_3 = self._create_mailing_list_of_x_contacts(3)
self.assertEqual(ml_1.contact_count, 3)
self.assertEqual(ml_1.contact_count_blacklisted, 0)
self.assertEqual(ml_1.contact_count_email, 3)
self.assertEqual(ml_1.contact_count_opt_out, 0)
self.assertEqual(ml_2.contact_count, 4)
self.assertEqual(ml_2.contact_count_blacklisted, 0)
self.assertEqual(ml_2.contact_count_email, 4)
self.assertEqual(ml_2.contact_count_opt_out, 0)
self.assertEqual(ml_3.contact_count, 3)
self.assertEqual(ml_3.contact_count_blacklisted, 0)
self.assertEqual(ml_3.contact_count_email, 3)
self.assertEqual(ml_3.contact_count_opt_out, 0)
# create a new test contact
contact = self.env['mailing.contact'].browse(
self.env['mailing.contact'].name_create(_email_formatted)[0]
)
self.assertEqual(contact.email, _email_normalized)
self.assertEqual(contact.name, 'Mireille Labeille')
# add new subscriptions (and ensure email_normalized is used)
(ml_1 + ml_2)._update_subscription_from_email(_email_formatted_upd, opt_out=False)
subs = self.env['mailing.contact.subscription'].search(
[('contact_id', '=', contact.id)]
)
self.assertEqual(subs.list_id, ml_1 + ml_2)
# opt-out from opted-in mailing list + 1 non opted-in mailing list
(ml_2 + ml_3)._update_subscription_from_email(_email_formatted_upd, opt_out=True)
subs = self.env['mailing.contact.subscription'].search(
[('contact_id', '=', contact.id)]
)
self.assertEqual(subs.list_id, ml_1 + ml_2)