[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:
committed by
Thibault Delavallée
co-authored by
Thibault Delavallée
parent
d27a6f7276
commit
0ac3de16a9
@@ -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')
|
||||
|
||||
@@ -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}&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&model=digest.digest&id=%s' % object.id"
|
||||
target="_blank" style="text-decoration: none;">
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user