[IMP] mass_mailing: ease unsubscribe page edition for mailing users

PURPOSE

Globally improve usability and features given by mailing portal about exclusion
list and opt-out management.

SPECIFICATIONS

Ease unsubscribe page preview and edition. 'unsubscribe_from_list' generic
unsubscribe link now redirects to '/mailing/my' page, allowing to see what
an unsubscription page looks like. Empty sections are also added to ease
editor usage.

No real unsubscription preview can be done from 'unsubscribe_from_list' link
displayed in a mailing body as the mailing could not exist when clicking on it.
Moreover it would require custom code to redirect from this generic link to
the real unsubscription page, for few real added value.

Tweak access to allow mailing users to access a mailing given its non-protected
'mailing/<id>/unsubscribe' link. It is not linked to any backend document
(like specific document mailed, ...) but allows to display the page, edit
it, ...

Tweak 'mailing/<id>/view' to allow to generate the non-protected unsubscribe
link. It allows a mailing user to "view" a mailing, then click on "unsubscribe"
and land on the unsubscribe page, without having to deal with document_id
and tokens.

Task-2150462 (Mass Mailing: Improve subscription management)

Part-of: odoo/odoo#86084
This commit is contained in:
Thibault Delavallée
2023-10-06 06:13:53 +00:00
parent c0775de8d0
commit 85f76cd077
3 changed files with 78 additions and 19 deletions
+36 -19
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import werkzeug
from datetime import timedelta
from markupsafe import Markup, escape
@@ -24,12 +25,19 @@ class MassMailController(http.Controller):
available for them;
* hash_token is not required for generic page for logged user, aka
if no mailing_id is given;
* hash_token is not required for mailing specific page if the user
is a mailing user;
* hash_token is not required for generic page for logged user, aka
if no mailing_id is given and if mailing_id is not required;
* hash_token always requires the triplet mailing_id, email and
document_id, as it indicates it comes from a mailing email and
is used when comparing hashes;
"""
if not hash_token and (mailing_id or request.env.user._is_public()):
raise BadRequest()
if not hash_token:
if request.env.user._is_public():
raise BadRequest()
if mailing_id and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
raise BadRequest()
if hash_token and (not mailing_id or not email or not document_id):
raise BadRequest()
if mailing_id:
@@ -130,13 +138,17 @@ class MassMailController(http.Controller):
)
def _mailing_unsubscribe_from_document(self, mailing, document_id, email, hash_token):
message = Markup(
_(
if document_id:
message = Markup(_(
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)',
**self._format_bl_request(mailing, document_id)
)
)
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message)
))
else:
message = Markup(_(
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (direct link usage)',
**self._format_bl_request(mailing, document_id)
))
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=Markup('<p>%s</p>') % message)
return request.render(
'mass_mailing.page_mailing_unsubscribe',
@@ -308,7 +320,7 @@ class MassMailController(http.Controller):
@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 BadRequest()
return request.redirect('/mailing/my', code=301, local=True)
# ------------------------------------------------------------
# TRACKING
@@ -372,12 +384,6 @@ class MassMailController(http.Controller):
)
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
document_id = int(document_id) if document_id and document_id.isdigit() else 0
@@ -388,10 +394,19 @@ class MassMailController(http.Controller):
options={'post_process': False}
)[document_id]
# Update generic URLs (without parameters) to final ones
html_markupsafe = html_markupsafe.replace(
'/unsubscribe_from_list',
mailing_sudo._get_unsubscribe_url(email, document_id)
)
if document_id:
html_markupsafe = html_markupsafe.replace(
'/unsubscribe_from_list',
mailing_sudo._get_unsubscribe_url(email, document_id)
)
else: # when manually trying a /view on a mailing, not through email link
html_markupsafe = html_markupsafe.replace(
'/unsubscribe_from_list',
werkzeug.urls.url_join(
mailing_sudo.get_base_url(),
f'/mailing/{mailing_sudo.id}/unsubscribe',
)
)
return request.render(
'mass_mailing.mailing_view',
@@ -462,5 +477,7 @@ class MassMailController(http.Controller):
mailing_model_name = request.env['ir.model']._get(mailing.mailing_model_real).display_name
return {
'mailing_link': Markup(f'<a href="#" data-oe-model="mailing.mailing" data-oe-id="{mailing.id}">{escape(mailing.subject)}</a>'),
'record_link': Markup(f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'),
'record_link': Markup(
f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'
) if document_id else '',
}
@@ -255,6 +255,47 @@ class TestMailingControllers(TestMailingControllersCommon):
)
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
def test_mailing_unsubscribe_from_document_tour_mailing_user(self):
""" Test portal unsubscribe on mailings performed on documents (not
mailing lists or contacts) using a generic '/unsubscribe' link allowing
mailing users to see and edit unsubcribe page.
Tour effects
* unsubscribe from mailing based on a document = blocklist;
* add feedback (block list): Other reason, with 'My feedback' feedback;
* remove email from exclusion list;
* re-add email to exclusion list;
"""
# update user to link it with existing mailing contacts and allow the tour
# to run; test without and with mailing group
self.user_marketing.write({
'email': tools.formataddr(("Déboulonneur", "fleurus@example.com")),
'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
test_mailing = self.test_mailing_on_documents.with_env(self.env)
self.authenticate('user_marketing', 'user_marketing')
# no group -> no direct access to /unsubscribe
res = self.url_open(
werkzeug.urls.url_join(
test_mailing.get_base_url(),
f'mailing/{test_mailing.id}/unsubscribe',
)
)
self.assertEqual(res.status_code, 400)
# group -> direct access to /unsubscribe should wokr
self.user_marketing.write({
'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
})
# launch unsubscription tour
with freeze_time(self._reference_now):
self.start_tour(
f"/mailing/{test_mailing.id}/unsubscribe",
"mailing_portal_unsubscribe_from_document_with_lists",
login=self.user_marketing.login,
)
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.
@@ -159,6 +159,7 @@
</div>
</div>
</div>
<div id="o_mailing_portal_subscription_editor" class="oe_structure oe_empty"/>
</div>
</template>
</odoo>