[FIX] digest: ease unsubscribe

Purpose of this commit is to ease usage of unsubscribe button for periodic
digests for non-admins. Users land on page that says "You have unsubscribed
from ..." and not on digest form view anymore.

It is done using a public route with a token, allowing to unsubscribe even
when being not logged, for example when receiving digest emails that are
unwanted.

Task-2641394 (Digest emails sending improvement)
Task-2582128 (Digest onboarding and usage improvement)

X-original-commit: a4b98a82d04b398406958d82f466c09f6021a989
Part-of: odoo/odoo#79877
Co-authored-by: Thibault Delavallée <tde@odoo.com>
This commit is contained in:
Fabio Barbero
2021-11-16 16:58:39 +00:00
committed by Thibault Delavallée
co-authored by Thibault Delavallée
parent d27a6f7276
commit 0ac3de16a9
4 changed files with 46 additions and 9 deletions
+19 -6
View File
@@ -1,21 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import Forbidden
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.urls import url_encode
from odoo import _
from odoo.http import Controller, request, route
from odoo.tools import consteq
class DigestController(Controller):
@route('/digest/<int:digest_id>/unsubscribe', type='http', website=True, auth='user')
def digest_unsubscribe(self, digest_id):
digest = request.env['digest.digest'].browse(digest_id).exists()
digest.action_unsubcribe()
@route('/digest/<int:digest_id>/unsubscribe', type='http', website=True, auth='public')
def digest_unsubscribe(self, digest_id, token=None, user_id=None):
digest_sudo = request.env['digest.digest'].sudo().browse(digest_id).exists()
# new route parameters
if digest_sudo and token and user_id:
correct_token = digest_sudo._get_unsubscribe_token(int(user_id))
if not consteq(correct_token, token):
raise NotFound()
digest_sudo._action_unsubscribe_users(request.env['res.users'].sudo().browse(int(user_id)))
# old route was given without any token or user_id but only for auth users
elif digest_sudo and not token and not user_id and not request.env.user.share:
digest_sudo.action_unsubcribe()
else:
raise NotFound()
return request.render('digest.portal_digest_unsubscribed', {
'digest': digest,
'digest': digest_sudo,
})
@route('/digest/<int:digest_id>/set_periodicity', type='http', website=True, auth='user')
+8 -1
View File
@@ -402,7 +402,14 @@
</div>
<div class="by_odoo">
Sent by <a href="https://www.odoo.com" target="_blank" class="odoo_link"><span class="odoo_link_text">Odoo</span></a>
<t t-if="object and object._name == 'digest.digest'">
<t t-if="unsubscribe_token">
–
<a t-attf-href="/digest/#{object.id}/unsubscribe?token=#{unsubscribe_token}&amp;user_id=#{user.id}"
target="_blank" style="text-decoration: none;">
<span style="color: #8f8f8f;">Unsubscribe</span>
</a>
</t>
<t t-elif="object and object._name == 'digest.digest'">
–
<a t-att-href="'/web#view_type=form&amp;model=digest.digest&amp;id=%s' % object.id"
target="_blank" style="text-decoration: none;">
+9 -1
View File
@@ -7,7 +7,6 @@ import pytz
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from markupsafe import Markup
from werkzeug.urls import url_join
from odoo import api, fields, models, tools, _
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
@@ -137,6 +136,7 @@ class Digest(models.Model):
'top_button_url': self.get_base_url(),
'company': user.company_id,
'user': user,
'unsubscribe_token': self._get_unsubscribe_token(user.id),
'tips_count': tips_count,
'formatted_date': datetime.today().strftime('%B %d, %Y'),
'display_mobile_banner': True,
@@ -176,6 +176,14 @@ class Digest(models.Model):
except MailDeliveryException as e:
_logger.warning('MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.', digest.id)
def _get_unsubscribe_token(self, user_id):
"""Generate a secure hash for this digest and user. It allows to
unsubscribe from a digest while keeping some security in that process.
:param int user_id: ID of the user to unsubscribe
"""
return tools.hmac(self.env(su=True), 'digest-unsubscribe', (self.id, user_id))
# ------------------------------------------------------------
# KPIS
# ------------------------------------------------------------
+10 -1
View File
@@ -160,6 +160,7 @@ class TestUnsubscribe(HttpCaseWithUserDemo):
})
self.test_digest._action_subscribe_users(self.user_demo)
self.base_url = self.test_digest.get_base_url()
self.user_demo_unsubscribe_token = self.test_digest._get_unsubscribe_token(self.user_demo.id)
@users('demo')
def test_unsubscribe_classic(self):
@@ -182,13 +183,21 @@ class TestUnsubscribe(HttpCaseWithUserDemo):
self.assertEqual(response.status_code, 200)
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
def test_unsubscribe_token(self):
self.assertIn(self.user_demo, self.test_digest.user_ids)
self.authenticate(None, None)
response = self._url_unsubscribe(token=self.user_demo_unsubscribe_token, user_id=self.user_demo.id)
self.assertEqual(response.status_code, 200)
self.test_digest.invalidate_cache()
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
def test_unsubscribe_public(self):
""" Check public users are redirected when trying to catch unsubscribe
route. """
self.authenticate(None, None)
response = self._url_unsubscribe()
self.assertIn('web/login?redirect', response.url)
self.assertEqual(response.status_code, 404)
def _url_unsubscribe(self, token=None, user_id=None):
url_params = {}