Skip to content

Commit

Permalink
[MIG] mass_mailing_custom_unsubscribe: Migration to 17.0
Browse files Browse the repository at this point in the history
- Refactor to adapt to the new core ubsubscription methods

TT49917
  • Loading branch information
chienandalu committed Aug 12, 2024
1 parent de837d9 commit 95b47b6
Show file tree
Hide file tree
Showing 30 changed files with 352 additions and 1,109 deletions.
38 changes: 14 additions & 24 deletions mass_mailing_custom_unsubscribe/README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
==========================================================
Customizable unsubscription process on mass mailing emails
==========================================================
====================================
Mass mailing unsubscription tracking
====================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Expand Down Expand Up @@ -30,10 +30,6 @@ Customizable unsubscription process on mass mailing emails

This addon extends the unsubscription form to let you:

- Choose which mailing lists are not cross-unsubscriptable when
unsubscribing from a different one.
- Know why and when a contact has been subscribed or unsubscribed from
a mass mailing.
- Provide proof on why you are sending mass mailings to a given
contact, as required by the GDPR in Europe.
- Handle discrete unsubscriptions from other recipients that are not a
Expand All @@ -48,22 +44,14 @@ This addon extends the unsubscription form to let you:
Configuration
=============

You can customize what reasons will be displayed to your unsubscriptors
when they are going to unsubscribe. To do it:

1. Go to *Email Marketing > Configuration > Unsubscription Reasons*.
2. Create / edit / remove / sort as usual.
3. If *Details required* is enabled, they will have to fill a text area
to continue.

For having discrete unsubscriptions from other recipients than the
mailing lists, you need to add a glue module that adds 2 fields in the
mailing lists, you need to add a glue module that adds two fields in the
associated model:

- opt_out.
- Either email or email_from.
- ``opt_out``.
- Either ``email`` or ``email_from``.

See mass_mailing_custom_unsubscribe_event for an example.
See ``mass_mailing_custom_unsubscribe_event`` for an example.

Usage
=====
Expand All @@ -80,11 +68,13 @@ Once configured:
Known issues / Roadmap
======================

- This module replaces AJAX submission core implementation from the
mailing list management form, because it is impossible to extend it.
When this is fixed, this addon will need a refactoring (mostly
removing duplicated functionality and depending on it instead of
replacing it).
- We use an alternative model to the core ``mailing.subscription`` as
that one is constrained to mailing/lists and we aim to register other
models opt outs as well. Maybe we could merge ``mail.unsubscription``
with it but it requires such a transformation that is like replacing
it completely with our own logic, so we'd be just avoiding to have
these two sources of information while we'd be probably dealing with
the potential side-effects of changing core's logic...

Bug Tracker
===========
Expand Down
32 changes: 7 additions & 25 deletions mass_mailing_custom_unsubscribe/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
# Copyright 2016 Jairo Llopis <[email protected]>
# Copyright 2018 David Vidal <[email protected]>
# Copyright 2016 Tecnativa - Jairo Llopis
# Copyright 2018 Tecnativa - David Vidal
# Copyright 2020 Tecnativa - Pedro M. Baeza
# Copyright 2024 Tecnativa - Carolina Fernandez
# Copyright 2024 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Customizable unsubscription process on mass mailing emails",
"summary": "Know and track (un)subscription reasons, GDPR compliant",
"name": "Mass mailing unsubscription tracking",
"summary": "Track (un)subscription reasons, GDPR compliant",
"category": "Marketing",
"version": "16.0.1.0.1",
"version": "17.0.1.0.0",
"depends": ["mass_mailing"],
"data": [
"security/ir.model.access.csv",
"data/mail_unsubscription_reason.xml",
"templates/general_reason_form.xml",
"templates/mass_mailing_contact_reason.xml",
"views/mail_unsubscription_reason_view.xml",
"views/mail_mass_mailing_list_view.xml",
"views/mail_unsubscription_view.xml",
"views/mailing_list_view.xml",
],
"assets": {
"mass_mailing.mailing_assets": [
(
"replace",
"mass_mailing/static/src/js/mailing_portal.js",
"mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js",
),
],
"web.assets_tests": [
"mass_mailing_custom_unsubscribe/static/src/js/contact.tour.esm.js",
"mass_mailing_custom_unsubscribe/static/src/js/partner.tour.esm.js",
],
},
"images": ["images/form.png"],
"author": "Tecnativa, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
"license": "AGPL-3",
"installable": True,
}
196 changes: 76 additions & 120 deletions mass_mailing_custom_unsubscribe/controllers/main.py
Original file line number Diff line number Diff line change
@@ -1,159 +1,115 @@
# Copyright 2015 Antiun Ingeniería S.L. (http://www.antiun.com)
# Copyright 2016 Jairo Llopis <[email protected]>
# Copyright 2016 Tecnativa - Jairo Llopis
# Copyright 2020 Tecnativa - Pedro M. Baeza
# Copyright 2024 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging

from odoo.http import request, route

from odoo.addons.mass_mailing.controllers.main import MassMailController

_logger = logging.getLogger(__name__)


class CustomUnsubscribe(MassMailController):
def reason_form(self, mailing_id, email, res_id, reasons, token):
"""Get the unsubscription reason form.
:param mailing.mailing mailing:
Mailing where the unsubscription is being processed.
def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token):
self._add_extra_context(mailing, document_id)
return super()._mailing_unsubscribe_from_list(
mailing, document_id, email, hash_token
)

:param str email:
Email to be unsubscribed.
def _mailing_unsubscribe_from_document(
self, mailing, document_id, email, hash_token
):
self._add_extra_context(mailing, document_id)
return super()._mailing_unsubscribe_from_document(
mailing, document_id, email, hash_token
)

:param int res_id:
ID of the unsubscriber.
def _prepare_mailing_subscription_values(
self, mailing, document_id, email, hash_token
):
def show_list(lst):
return not lst.not_cross_unsubscriptable or lst in mailing.contact_list_ids

:param str token:
Security token for unsubscriptions.
"""
return request.render(
"mass_mailing_custom_unsubscribe.reason_form",
{
"email": email,
"mailing_id": mailing_id,
"reasons": reasons,
"res_id": res_id,
"token": token,
},
values = super()._prepare_mailing_subscription_values(
mailing, document_id, email, hash_token
)

@route()
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
"""Ask/save unsubscription reason."""
_logger.debug(
"Called `mailing()` with: %r", (mailing_id, email, res_id, token, post)
# Manage not_cross_unsubscriptable
values["lists_contacts"] = values["lists_optin"].filtered(
lambda x: show_list(x)
)
reasons = request.env["mail.unsubscription.reason"].search([])
if not res_id:
res_id = "0"
res_id = res_id and int(res_id)
try:
# Check if we already have a reason for unsubscription
reason_id = int(post["reason_id"])
except (KeyError, ValueError):
# No reasons? Ask for them
return self.reason_form(mailing_id, email, res_id, reasons, token)
else:
# Unsubscribe, saving reason and details by context
details = post.get("details", False)
self._add_extra_context(mailing_id, res_id, reason_id, details)
mailing_obj = request.env["mailing.mailing"]
mass_mailing = mailing_obj.sudo().browse(mailing_id)
model = mass_mailing.mailing_model_real
if "opt_out" in request.env[model]._fields and model != "mailing.contact":
mass_mailing.update_opt_out_other(email, [res_id], True)
result = request.render(
"mass_mailing.page_unsubscribed",
{
"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"),
},
)
result.qcontext.update({"reasons": reasons})
else:
# You could get a DetailsRequiredError here, but only if HTML5
# validation fails, which should not happen in modern browsers
result = super().mailing(mailing_id, email, res_id, token=token, **post)
if model == "mailing.contact":
# update list_ids taking into account
# not_cross_unsubscriptable field
result.qcontext.update(
{
"reasons": reasons,
"list_ids": result.qcontext["list_ids"].filtered(
lambda m_list: not m_list.not_cross_unsubscriptable
or m_list in mass_mailing.contact_list_ids
),
}
)
return result
values["lists_optin"] = values["lists_optin"].filtered(lambda x: show_list(x))
values["lists_optout"] = values["lists_optin"].filtered(lambda x: show_list(x))
values["lists_public"] = values["lists_optin"].filtered(lambda x: show_list(x))
return values

@route()
def unsubscribe(
def mailing_update_list_subscription(
self,
mailing_id,
opt_in_ids,
opt_out_ids,
email,
res_id,
token,
reason_id=None,
details=None,
mailing_id=None,
document_id=None,
email=None,
hash_token=None,
lists_optin_ids=None,
**post,
):
"""Store unsubscription reasons when unsubscribing from RPC."""
# Update request context
self._add_extra_context(mailing_id, res_id, reason_id, details)
_logger.debug(
"Called `unsubscribe()` with: %r",
(
mailing_id,
opt_in_ids,
opt_out_ids,
email,
res_id,
token,
reason_id,
details,
),
)
return super().unsubscribe(
mailing_id, opt_in_ids, opt_out_ids, email, res_id, token
self._add_extra_context(mailing_id, document_id)
return super().mailing_update_list_subscription(
mailing_id, document_id, email, hash_token, lists_optin_ids, **post
)

@route()
def blacklist_add(
self, mailing_id, res_id, email, token, reason_id=None, details=None
def mail_blocklist_add(
self, mailing_id=None, document_id=None, email=None, hash_token=None
):
self._add_extra_context(mailing_id, res_id, reason_id, details)
return super().blacklist_add(mailing_id, res_id, email, token)
self._add_extra_context(mailing_id, document_id)
return super().mail_blocklist_add(mailing_id, document_id, email, hash_token)

@route()
def blacklist_remove(
self, mailing_id, res_id, email, token, reason_id=None, details=None
def mail_blocklist_remove(
self, mailing_id=None, document_id=None, email=None, hash_token=None
):
self._add_extra_context(mailing_id, res_id, reason_id, details)
return super().blacklist_remove(mailing_id, res_id, email, token)
self._add_extra_context(mailing_id, document_id)
return super().mail_blocklist_remove(mailing_id, document_id, email, hash_token)

def _add_extra_context(self, mailing_id, res_id, reason_id, details):
@route()
def mailing_send_feedback(
self,
mailing_id=None,
document_id=None,
email=None,
hash_token=None,
last_action=None,
opt_out_reason_id=False,
feedback=None,
**post,
):
self._add_extra_context(mailing_id, document_id, opt_out_reason_id, feedback)
return super().mailing_send_feedback(
mailing_id,
document_id,
email,
hash_token,
last_action,
opt_out_reason_id,
feedback,
**post,
)

def _add_extra_context(self, mailing_id, res_id, reason_id=None, details=None):
if not res_id:
return
environ = request.httprequest.headers.environ
# Add mailing_id and res_id to request.context to be used in the
# redefinition of _add and _remove methods of the mail.blacklist class
extra_context = {
"default_metadata": "\n".join(
"metadata": "\n".join(
f"{val}: {environ.get(val)}"
for val in ("REMOTE_ADDR", "HTTP_USER_AGENT", "HTTP_ACCEPT_LANGUAGE")
),
"mailing_id": mailing_id,
"unsubscription_res_id": int(res_id),
}
if reason_id:
extra_context["default_reason_id"] = int(reason_id)
extra_context["reason_id"] = int(reason_id)
if details:
extra_context["default_details"] = details
extra_context["details"] = details
request.update_context(**extra_context)

This file was deleted.

Loading

0 comments on commit 95b47b6

Please sign in to comment.