Browse Source

[MIG] mass_mailing_custom_unsubscribe: Migration to 12.0

pull/402/head
ernesto 6 years ago
parent
commit
fe4bc6b53e
  1. 40
      mass_mailing_custom_unsubscribe/README.rst
  2. 4
      mass_mailing_custom_unsubscribe/__init__.py
  3. 10
      mass_mailing_custom_unsubscribe/__manifest__.py
  4. 76
      mass_mailing_custom_unsubscribe/controllers/main.py
  5. 17
      mass_mailing_custom_unsubscribe/demo/assets.xml
  6. 17
      mass_mailing_custom_unsubscribe/hooks.py
  7. BIN
      mass_mailing_custom_unsubscribe/images/form.png
  8. 3
      mass_mailing_custom_unsubscribe/models/__init__.py
  9. 36
      mass_mailing_custom_unsubscribe/models/mail_blacklist.py
  10. 34
      mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
  11. 30
      mass_mailing_custom_unsubscribe/models/mail_mass_mailing_contact.py
  12. 14
      mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py
  13. 27
      mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
  14. 2
      mass_mailing_custom_unsubscribe/readme/CONFIGURE.rst
  15. 11
      mass_mailing_custom_unsubscribe/readme/CONTRIBUTORS.rst
  16. 13
      mass_mailing_custom_unsubscribe/readme/ROADMAP.rst
  17. 4
      mass_mailing_custom_unsubscribe/readme/USAGE.rst
  18. 37
      mass_mailing_custom_unsubscribe/static/description/index.html
  19. 74
      mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js
  20. 46
      mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js
  21. 28
      mass_mailing_custom_unsubscribe/static/src/js/require_details.js
  22. 332
      mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
  23. 65
      mass_mailing_custom_unsubscribe/templates/general_reason_form.xml
  24. 40
      mass_mailing_custom_unsubscribe/templates/mass_mailing_contact_reason.xml
  25. 1
      mass_mailing_custom_unsubscribe/tests/__init__.py
  26. 149
      mass_mailing_custom_unsubscribe/tests/test_ui.py
  27. 6
      mass_mailing_custom_unsubscribe/views/assets.xml
  28. 30
      mass_mailing_custom_unsubscribe/views/mail_mass_mailing_contact_view.xml
  29. 19
      mass_mailing_custom_unsubscribe/views/mail_mass_mailing_list_view.xml
  30. 31
      mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml

40
mass_mailing_custom_unsubscribe/README.rst

@ -14,13 +14,13 @@ Customizable unsubscription process on mass mailing emails
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github
:target: https://github.com/OCA/social/tree/11.0/mass_mailing_custom_unsubscribe
:target: https://github.com/OCA/social/tree/12.0/mass_mailing_custom_unsubscribe
:alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/social-11-0/social-11-0-mass_mailing_custom_unsubscribe
:target: https://translation.odoo-community.org/projects/social-12-0/social-12-0-mass_mailing_custom_unsubscribe
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/205/11.0
:target: https://runbot.odoo-community.org/runbot/205/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
@ -45,7 +45,7 @@ Configuration
You can customize what reasons will be displayed to your unsubscriptors when
they are going to unsubscribe. To do it:
#. Go to *Mass Mailing > Configuration > Unsubscription Reasons*.
#. Go to *Email Marketing > Configuration > Unsubscription Reasons*.
#. Create / edit / remove / sort as usual.
#. If *Details required* is enabled, they will have to fill a text area to
continue.
@ -55,27 +55,20 @@ Usage
Once configured:
#. Go to *Mass Mailing > Mailings > Mass Mailings > Create*.
#. Go to *Email Marketing > Mailings > Create*.
#. Edit your mass mailing at wish, but remember to add a snippet from
*Footers*, so people have an *Unsubscribe* link.
#. Send it.
#. If somebody gets unsubscribed, you will see logs about that under
*Mass Mailing > Mailings > Unsubscriptions*.
*Email Marketing > Unsubscriptions*.
Known issues / Roadmap
======================
* As version 11 has introduced a new relation type between mailing lists and
contacts that has multiple usability issues that are being reworked by Odoo
to land in version 12, this module falls back to the version 10 behaviour in
which one contact belonged to just one list.
* This module replaces AJAX submission core implementation from the mailing
list management form, because it is impossible to extend it. When
https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most
needed changes), this addon will need a refactoring (mostly removing
duplicated functionality and depending on it instead of replacing it). In the
mean time, there is a little chance that this introduces some
incompatibilities with other addons that depend on ``website_mass_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).
Bug Tracker
===========
@ -83,7 +76,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/social/issues/new?body=module:%20mass_mailing_custom_unsubscribe%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/social/issues/new?body=module:%20mass_mailing_custom_unsubscribe%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -98,10 +91,13 @@ Authors
Contributors
~~~~~~~~~~~~
* Rafael Blasco <rafael.blasco@tecnativa.com>
* Antonio Espinosa <antonio.espinosa@tecnativa.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* David Vidal <david.vidal@tecnativa.com>
* `Tecnativa <https://www.tecnativa.com>`_:
* Rafael Blasco
* Antonio Espinosa
* Jairo Llopis
* David Vidal
* Ernesto Tejeda
Maintainers
~~~~~~~~~~~
@ -116,6 +112,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/social <https://github.com/OCA/social/tree/11.0/mass_mailing_custom_unsubscribe>`_ project on GitHub.
This module is part of the `OCA/social <https://github.com/OCA/social/tree/12.0/mass_mailing_custom_unsubscribe>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

4
mass_mailing_custom_unsubscribe/__init__.py

@ -1,4 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import controllers, models
from .hooks import post_init_hook
from . import controllers
from . import models

10
mass_mailing_custom_unsubscribe/__manifest__.py

@ -5,9 +5,9 @@
'name': 'Customizable unsubscription process on mass mailing emails',
'summary': 'Know and track (un)subscription reasons, GDPR compliant',
'category': 'Marketing',
'version': '11.0.1.0.0',
'version': '12.0.1.0.0',
'depends': [
'website_mass_mailing',
'mass_mailing',
],
'data': [
'security/ir.model.access.csv',
@ -16,13 +16,8 @@
'templates/mass_mailing_contact_reason.xml',
'views/assets.xml',
'views/mail_unsubscription_reason_view.xml',
'views/mail_mass_mailing_list_view.xml',
'views/mail_mass_mailing_contact_view.xml',
'views/mail_unsubscription_view.xml',
],
'demo': [
'demo/assets.xml',
],
'images': [
'images/form.png',
],
@ -31,5 +26,4 @@
'website': 'https://github.com/OCA/social',
'license': 'AGPL-3',
'installable': True,
'post_init_hook': 'post_init_hook',
}

76
mass_mailing_custom_unsubscribe/controllers/main.py

@ -5,14 +5,13 @@
import logging
from odoo.http import request, route
from odoo.addons.website_mass_mailing.controllers.main \
import MassMailController
from odoo.addons.mass_mailing.controllers.main import MassMailController
_logger = logging.getLogger(__name__)
class CustomUnsubscribe(MassMailController):
def reason_form(self, mailing, email, res_id, token):
def reason_form(self, mailing_id, email, res_id, reasons, token):
"""Get the unsubscription reason form.
:param mail.mass_mailing mailing:
@ -27,12 +26,11 @@ class CustomUnsubscribe(MassMailController):
:param str token:
Security token for unsubscriptions.
"""
reasons = request.env["mail.unsubscription.reason"].search([])
return request.render(
"mass_mailing_custom_unsubscribe.reason_form",
{
"email": email,
"mailing": mailing,
"mailing_id": mailing_id,
"reasons": reasons,
"res_id": res_id,
"token": token,
@ -44,48 +42,54 @@ class CustomUnsubscribe(MassMailController):
_logger.debug(
"Called `mailing()` with: %r",
(mailing_id, email, res_id, token, post))
mailing = request.env["mail.mass_mailing"].sudo().browse(mailing_id)
# Mass mailing list contacts are a special case because they have a
# subscription management form
if mailing.mailing_model_real == 'mail.mass_mailing.contact':
result = super(CustomUnsubscribe, self).mailing(
mailing_id, email, res_id, token=token, **post)
result.qcontext.update({
"contacts": result.qcontext["contacts"].filtered(
lambda contact:
not any(contact.list_ids.mapped(
'not_cross_unsubscriptable')) or
contact.list_ids <= mailing.contact_list_ids
),
"reasons":
request.env["mail.unsubscription.reason"].search([]),
})
return result
# Any other record type gets a simplified form
reasons = request.env["mail.unsubscription.reason"].search([])
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, email, res_id, token)
return self.reason_form(mailing_id, email, res_id, reasons, token)
else:
# Unsubscribe, saving reason and details by context
request.context = dict(
request.context,
default_reason_id=reason_id,
default_details=post.get("details") or False,
)
details = post.get("details", False)
self._add_extra_context(mailing_id, res_id, reason_id, details)
# You could get a DetailsRequiredError here, but only if HTML5
# validation fails, which should not happen in modern browsers
return super(CustomUnsubscribe, self).mailing(
result = super(CustomUnsubscribe, self).mailing(
mailing_id, email, res_id, token=token, **post)
result.qcontext.update({"reasons": reasons})
return result
@route()
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id,
token, reason_id=None, details=None):
"""Store unsubscription reasons when unsubscribing from RPC."""
# Update request context and reset environment
# 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(CustomUnsubscribe, self).unsubscribe(
mailing_id, opt_in_ids, opt_out_ids, email, res_id, token)
@route()
def blacklist_add(self, mailing_id, res_id, email, token, reason_id=None,
details=None):
self._add_extra_context(mailing_id, res_id, reason_id, details)
return super(CustomUnsubscribe, self).blacklist_add(
mailing_id, res_id, email, token)
@route()
def blacklist_remove(self, mailing_id, res_id, email, token,
reason_id=None, details=None):
self._add_extra_context(mailing_id, res_id, reason_id, details)
return super(CustomUnsubscribe, self).blacklist_remove(
mailing_id, res_id, email, token)
def _add_extra_context(self, mailing_id, res_id, reason_id, details):
environ = request.httprequest.headers.environ
# safe mailing_id and res_id to register a blacklisting or a
# de-blacklisting
extra_context = {
"default_metadata": "\n".join(
"%s: %s" % (val, environ.get(val)) for val in (
@ -94,15 +98,11 @@ class CustomUnsubscribe(MassMailController):
"HTTP_ACCEPT_LANGUAGE",
)
),
"mailing_id": mailing_id,
"res_id": int(res_id),
}
if reason_id:
extra_context["default_reason_id"] = int(reason_id)
if details:
extra_context["default_details"] = details
request.context = dict(request.context, **extra_context)
_logger.debug(
"Called `unsubscribe()` with: %r",
(mailing_id, opt_in_ids, opt_out_ids, email, res_id, token,
reason_id, details))
return super(CustomUnsubscribe, self).unsubscribe(
mailing_id, opt_in_ids, opt_out_ids, email)

17
mass_mailing_custom_unsubscribe/demo/assets.xml

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="assets_frontend_demo"
inherit_id="website.assets_frontend">
<xpath expr=".">
<script type="text/javascript"
src="/mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js"/>
<script type="text/javascript"
src="/mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js"/>
</xpath>
</template>
</odoo>

17
mass_mailing_custom_unsubscribe/hooks.py

@ -1,17 +0,0 @@
# Copyright 2018 David Vidal <david.vidal@tecnativa.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, SUPERUSER_ID
def post_init_hook(cr, registry):
"""Ensure all existing contacts are going to work as v10"""
env = api.Environment(cr, SUPERUSER_ID, {})
contacts = env['mail.mass_mailing.contact'].search([])
for contact in contacts:
if len(contact.list_ids) <= 1:
continue
list_1 = contact.list_ids[0]
for list_ in contact.list_ids - list_1:
contact.copy({"list_ids": [(6, 0, list_.ids)]})
contact.list_ids = list_1

BIN
mass_mailing_custom_unsubscribe/images/form.png

Before

Width: 1004  |  Height: 506  |  Size: 36 KiB

3
mass_mailing_custom_unsubscribe/models/__init__.py

@ -1,6 +1,5 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import mail_blacklist
from . import mail_mass_mailing
from . import mail_mass_mailing_contact
from . import mail_mass_mailing_list
from . import mail_unsubscription

36
mass_mailing_custom_unsubscribe/models/mail_blacklist.py

@ -0,0 +1,36 @@
# Copyright 2019 Tecnativa - Ernesto Tejeda
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class MailBlackList(models.Model):
_inherit = 'mail.blacklist'
def _add(self, email):
mailing_id = self.env.context.get('mailing_id')
if mailing_id:
mailing = self.env['mail.mass_mailing'].browse(mailing_id)
model_name = mailing.mailing_model_real
res_id = self.env.context.get('res_id')
self.env["mail.unsubscription"].create({
"email": email,
"mass_mailing_id": mailing_id,
"unsubscriber_id": "%s,%d" % (model_name, res_id),
"action": "blacklisting",
})
return super(MailBlackList, self)._add(email)
def _remove(self, email):
mailing_id = self.env.context.get('mailing_id')
if mailing_id:
mailing = self.env['mail.mass_mailing'].browse(mailing_id)
model_name = mailing.mailing_model_real
res_id = self.env.context.get('res_id')
self.env["mail.unsubscription"].create({
"email": email,
"mass_mailing_id": mailing_id,
"unsubscriber_id": "%s,%d" % (model_name, res_id),
"action": "de_blacklisting",
})
return super(MailBlackList, self)._remove(email)

34
mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py

@ -2,39 +2,33 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
from itertools import groupby
class MailMassMailing(models.Model):
_inherit = "mail.mass_mailing"
def update_opt_out(self, email, res_ids, value):
def update_opt_out(self, email, list_ids, value):
"""Save unsubscription reason when opting out from mailing."""
self.ensure_one()
model = self.env[self.mailing_model_real].with_context(
active_test=False)
action = "unsubscription" if value else "subscription"
records = self.env[model._name].browse(res_ids)
previous = self.env["mail.unsubscription"].search(limit=1, args=[
("mass_mailing_id", "=", self.id),
("email", "=", email),
("action", "=", action),
subscription_model = self.env['mail.mass_mailing.list_contact_rel']
opt_out_records = subscription_model.search([
('contact_id.email', '=ilike', email),
('list_id', 'in', list_ids),
('opt_out', '!=', value),
])
if 'opt_out' not in model._fields:
return super(MailMassMailing, self).update_opt_out(
email, res_ids, value)
for one in records:
# Store action only when something changed, or there was no
# previous subscription record
if one.opt_out != value or (action == "subscription" and
not previous):
model_name = 'mail.mass_mailing.contact'
for contact, subscriptions in groupby(opt_out_records,
lambda r: r.contact_id):
mailing_list_ids = [r.list_id.id for r in subscriptions]
# reason_id and details are expected from the context
self.env["mail.unsubscription"].create({
"email": email,
"mass_mailing_id": self.id,
"unsubscriber_id": "%s,%d" % (one._name, one.id),
"unsubscriber_id": "%s,%d" % (model_name, contact.id),
'mailing_list_ids': [(6, False, mailing_list_ids)],
"action": action,
})
if model._name == 'mail.mass_mailing.contact':
pass
return super(MailMassMailing, self).update_opt_out(
email, res_ids, value)
email, list_ids, value)

30
mass_mailing_custom_unsubscribe/models/mail_mass_mailing_contact.py

@ -1,30 +0,0 @@
# Copyright 2018 David Vidal <david.vidal@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class MailMassMailing(models.Model):
_inherit = "mail.mass_mailing.contact"
# Recover the old Many2one field so we can set a contact by list
mailing_list_id = fields.Many2one(
'mail.mass_mailing.list',
string='Mailing List',
ondelete='cascade',
compute="_compute_mailing_list_id",
inverse="_inverse_mailing_list_id",
search="_search_mailing_list_id",
)
@api.depends('list_ids')
def _compute_mailing_list_id(self):
for contact in self:
contact.mailing_list_id = contact.list_ids[:1]
def _inverse_mailing_list_id(self):
for contact in self:
contact.list_ids = contact.mailing_list_id
def _search_mailing_list_id(self, operator, value):
return [('list_ids', operator, value)]

14
mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py

@ -1,14 +0,0 @@
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class MailMassMailing(models.Model):
_inherit = "mail.mass_mailing.list"
not_cross_unsubscriptable = fields.Boolean(
string="Don't show this list in the other unsubscriptions",
help="If you mark this field, this list won't be shown when "
"unsubscribing from other mailing list, in the section: "
"'Is there any other mailing list you want to leave?'")

27
mass_mailing_custom_unsubscribe/models/mail_unsubscription.py

@ -9,6 +9,7 @@ from .. import exceptions
class MailUnsubscription(models.Model):
_name = "mail.unsubscription"
_description = "Mail unsubscription"
_inherit = "mail.thread"
_rec_name = "date"
_order = "date DESC"
@ -22,6 +23,8 @@ class MailUnsubscription(models.Model):
selection=[
("subscription", "Subscription"),
("unsubscription", "Unsubscription"),
("blacklisting", "Blacklisting"),
("de_blacklisting", "De-blacklisting"),
],
required=True,
default="unsubscription",
@ -36,21 +39,17 @@ class MailUnsubscription(models.Model):
lambda self: self._selection_unsubscriber_id(),
"(Un)subscriber",
help="Who was subscribed or unsubscribed.")
mailing_list_id = fields.Many2many(
mailing_list_ids = fields.Many2many(
comodel_name="mail.mass_mailing.list",
string="Mailing list",
ondelete="set null",
compute="_compute_mailing_list_id",
store=True,
help="(Un)subscribed mass mailing list, if any.",
readonly=False,
string="Mailing lists",
help="(Un)subscribed mass mailing lists, if any.",
)
reason_id = fields.Many2one(
"mail.unsubscription.reason",
"Reason",
ondelete="restrict",
help="Why the unsubscription was made.")
details = fields.Char(
details = fields.Text(
help="More details on why the unsubscription was made.")
details_required = fields.Boolean(
related="reason_id.details_required")
@ -97,17 +96,6 @@ class MailUnsubscription(models.Model):
raise exceptions.DetailsRequiredError(
_("Please provide details on why you are unsubscribing."))
@api.multi
@api.depends("unsubscriber_id")
def _compute_mailing_list_id(self):
"""Get the mass mailing list, if it is possible."""
for one in self:
try:
one.mailing_list_id |= one.unsubscriber_id.mailing_list_id
except AttributeError:
# Possibly model != mail.mass_mailing.contact; no problem
pass
@api.model
def create(self, vals):
# No reasons for subscriptions
@ -118,6 +106,7 @@ class MailUnsubscription(models.Model):
class MailUnsubscriptionReason(models.Model):
_name = "mail.unsubscription.reason"
_description = "Mail unsubscription reason"
_order = "sequence, name"
name = fields.Char(

2
mass_mailing_custom_unsubscribe/readme/CONFIGURE.rst

@ -1,7 +1,7 @@
You can customize what reasons will be displayed to your unsubscriptors when
they are going to unsubscribe. To do it:
#. Go to *Mass Mailing > Configuration > Unsubscription Reasons*.
#. Go to *Email Marketing > Configuration > Unsubscription Reasons*.
#. Create / edit / remove / sort as usual.
#. If *Details required* is enabled, they will have to fill a text area to
continue.

11
mass_mailing_custom_unsubscribe/readme/CONTRIBUTORS.rst

@ -1,4 +1,7 @@
* Rafael Blasco <rafael.blasco@tecnativa.com>
* Antonio Espinosa <antonio.espinosa@tecnativa.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* David Vidal <david.vidal@tecnativa.com>
* `Tecnativa <https://www.tecnativa.com>`_:
* Rafael Blasco
* Antonio Espinosa
* Jairo Llopis
* David Vidal
* Ernesto Tejeda

13
mass_mailing_custom_unsubscribe/readme/ROADMAP.rst

@ -1,11 +1,4 @@
* As version 11 has introduced a new relation type between mailing lists and
contacts that has multiple usability issues that are being reworked by Odoo
to land in version 12, this module falls back to the version 10 behaviour in
which one contact belonged to just one list.
* This module replaces AJAX submission core implementation from the mailing
list management form, because it is impossible to extend it. When
https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most
needed changes), this addon will need a refactoring (mostly removing
duplicated functionality and depending on it instead of replacing it). In the
mean time, there is a little chance that this introduces some
incompatibilities with other addons that depend on ``website_mass_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).

4
mass_mailing_custom_unsubscribe/readme/USAGE.rst

@ -1,8 +1,8 @@
Once configured:
#. Go to *Mass Mailing > Mailings > Mass Mailings > Create*.
#. Go to *Email Marketing > Mailings > Create*.
#. Edit your mass mailing at wish, but remember to add a snippet from
*Footers*, so people have an *Unsubscribe* link.
#. Send it.
#. If somebody gets unsubscribed, you will see logs about that under
*Mass Mailing > Mailings > Unsubscriptions*.
*Email Marketing > Unsubscriptions*.

37
mass_mailing_custom_unsubscribe/static/description/index.html

@ -367,7 +367,7 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/social/tree/11.0/mass_mailing_custom_unsubscribe"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/social-11-0/social-11-0-mass_mailing_custom_unsubscribe"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/205/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/social/tree/12.0/mass_mailing_custom_unsubscribe"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/social-12-0/social-12-0-mass_mailing_custom_unsubscribe"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/205/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This addon extends the unsubscription form to let you:</p>
<ul class="simple">
<li>Choose which mailing lists are not cross-unsubscriptable when unsubscribing
@ -397,7 +397,7 @@ required by the GDPR in Europe.</li>
<p>You can customize what reasons will be displayed to your unsubscriptors when
they are going to unsubscribe. To do it:</p>
<ol class="arabic simple">
<li>Go to <em>Mass Mailing &gt; Configuration &gt; Unsubscription Reasons</em>.</li>
<li>Go to <em>Email Marketing &gt; Configuration &gt; Unsubscription Reasons</em>.</li>
<li>Create / edit / remove / sort as usual.</li>
<li>If <em>Details required</em> is enabled, they will have to fill a text area to
continue.</li>
@ -407,28 +407,21 @@ continue.</li>
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<p>Once configured:</p>
<ol class="arabic simple">
<li>Go to <em>Mass Mailing &gt; Mailings &gt; Mass Mailings &gt; Create</em>.</li>
<li>Go to <em>Email Marketing &gt; Mailings &gt; Create</em>.</li>
<li>Edit your mass mailing at wish, but remember to add a snippet from
<em>Footers</em>, so people have an <em>Unsubscribe</em> link.</li>
<li>Send it.</li>
<li>If somebody gets unsubscribed, you will see logs about that under
<em>Mass Mailing &gt; Mailings &gt; Unsubscriptions</em>.</li>
<em>Email Marketing &gt; Unsubscriptions</em>.</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>As version 11 has introduced a new relation type between mailing lists and
contacts that has multiple usability issues that are being reworked by Odoo
to land in version 12, this module falls back to the version 10 behaviour in
which one contact belonged to just one list.</li>
<li>This module replaces AJAX submission core implementation from the mailing
list management form, because it is impossible to extend it. When
<a class="reference external" href="https://github.com/odoo/odoo/pull/14386">https://github.com/odoo/odoo/pull/14386</a> gets merged (which upstreams most
needed changes), this addon will need a refactoring (mostly removing
duplicated functionality and depending on it instead of replacing it). In the
mean time, there is a little chance that this introduces some
incompatibilities with other addons that depend on <tt class="docutils literal">website_mass_mailing</tt>.</li>
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).</li>
</ul>
</div>
<div class="section" id="bug-tracker">
@ -436,7 +429,7 @@ incompatibilities with other addons that depend on <tt class="docutils literal">
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mass_mailing_custom_unsubscribe%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mass_mailing_custom_unsubscribe%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
@ -450,10 +443,14 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple">
<li>Rafael Blasco &lt;<a class="reference external" href="mailto:rafael.blasco&#64;tecnativa.com">rafael.blasco&#64;tecnativa.com</a>&gt;</li>
<li>Antonio Espinosa &lt;<a class="reference external" href="mailto:antonio.espinosa&#64;tecnativa.com">antonio.espinosa&#64;tecnativa.com</a>&gt;</li>
<li>Jairo Llopis &lt;<a class="reference external" href="mailto:jairo.llopis&#64;tecnativa.com">jairo.llopis&#64;tecnativa.com</a>&gt;</li>
<li>David Vidal &lt;<a class="reference external" href="mailto:david.vidal&#64;tecnativa.com">david.vidal&#64;tecnativa.com</a>&gt;</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Rafael Blasco</li>
<li>Antonio Espinosa</li>
<li>Jairo Llopis</li>
<li>David Vidal</li>
<li>Ernesto Tejeda</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
@ -463,7 +460,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/11.0/mass_mailing_custom_unsubscribe">OCA/social</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/12.0/mass_mailing_custom_unsubscribe">OCA/social</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

74
mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js

@ -1,74 +0,0 @@
/* Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("mass_mailing_custom_unsubscribe.contact_tour",
function (require) {
"use strict";
var base = require("web_editor.base");
var tour = require("web_tour.tour");
require("mass_mailing_custom_unsubscribe.require_details");
require("mass_mailing_custom_unsubscribe.unsubscribe");
// Allow to know if an element is required
$.extend($.expr[':'], {
propRequired: function(element, index, matches) {
return $(element).prop("required");
},
});
tour.register(
"mass_mailing_custom_unsubscribe_tour_contact",
{
tour: true,
wait_for: base.ready(),
},
[
{
content: "Unsubscription reasons are invisible",
trigger: "#unsubscribe_form:has(.js_unsubscription_reason:hidden)",
},
{
content: "Uncheck list 0",
trigger: "li:contains('test list 0') input",
// List 2 is not cross unsubscriptable
extra_trigger: "body:not(:has(li:contains('test list 2'))) li:contains('test list 0') input:checked",
},
{
content: "Uncheck list 1",
trigger: "li:contains('test list 1') input:checked",
extra_trigger: ".js_unsubscription_reason:visible",
},
{
content: "Choose other reason",
trigger: ".radio:contains('Other reason') :radio",
extra_trigger: ".radio:contains('Other reason') " +
":radio:not(:checked)",
},
{
content: "Add details to reason",
trigger: "[name='details']:visible:propRequired",
run: "text I want to unsubscribe because I want. Period.",
extra_trigger: ".radio:contains('Other reason') :radio:checked",
},
{
content: "Update subscriptions 1st time",
trigger: "#unsubscribe_form :submit",
},
{
content: "Subscribe again to list 0",
trigger: "body:not(:has(#unsubscribe_form .js_unsubscription_reason:visible)):has(.alert-success, li:contains('test list 0') input:not(:checked))",
run: function () {
// This one will get the success again after next step
$(".alert-success").removeClass("alert-success");
},
},
{
content: "Update subscriptions 2nd time",
trigger: "#unsubscribe_form:not(:has(.js_unsubscription_reason:visible)) :submit",
},
{
content: "Resuscription was OK",
trigger: ".alert-success",
}
]
);
});

46
mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js

@ -1,46 +0,0 @@
/* Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("mass_mailing_custom_unsubscribe.partner_tour",
function (require) {
"use strict";
var base = require("web_editor.base");
var tour = require("web_tour.tour");
require("mass_mailing_custom_unsubscribe.require_details");
require("mass_mailing_custom_unsubscribe.unsubscribe");
// Allow to know if an element is required
$.extend($.expr[':'], {
propRequired: function(element, index, matches) {
return $(element).prop("required");
},
});
tour.register(
"mass_mailing_custom_unsubscribe_tour_partner",
{
tour: true,
wait_for: base.ready(),
},
[
{
content: "Choose other reason",
trigger: ".radio:contains('Other reason') :radio:not(:checked)",
extra_trigger: "#reason_form .js_unsubscription_reason",
},
{
content: "Switch to not interested reason",
trigger: ".radio:contains(\"I'm not interested\") :radio:not(:checked)",
extra_trigger: "[name='details']:propRequired",
},
{
content: "Unsubscribe",
trigger: "#reason_form :submit",
extra_trigger: "body:not(:has([name='details']:propRequired))",
},
{
content: "Successfully unsubscribed",
trigger: "body:not(:has(#reason_form)) .alert-success:contains('You have been successfully unsubscribed!')",
},
]
);
});

28
mass_mailing_custom_unsubscribe/static/src/js/require_details.js

@ -1,28 +0,0 @@
/* Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("mass_mailing_custom_unsubscribe.require_details",
function (require) {
"use strict";
var animation = require("website.content.snippets.animation");
animation.registry.mass_mailing_custom_unsubscribe_require_details =
animation.Class.extend({
selector: ".js_unsubscription_reason",
start: function () {
this.$radio = this.$(":radio");
this.$details = this.$("[name=details]");
this.$radio.on("change click", $.proxy(this.toggle, this));
this.$radio.filter(":checked").trigger("change");
},
toggle: function (event) {
this.$details.prop(
"required",
$(event.target).is("[data-details-required]") &&
$(event.target).is(":visible"));
},
});
return animation.registry.mass_mailing_custom_unsubscribe_require_details;
});

332
mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js

@ -1,121 +1,279 @@
/* Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
/* TODO This JS module replaces core AJAX submission because it is impossible
* to extend it as it is currently designed. Most of this code has been
* upstreamed in https://github.com/odoo/odoo/pull/14386, so we should extend
* that when it gets merged, and remove most of this file. */
odoo.define("mass_mailing_custom_unsubscribe.unsubscribe", function (require) {
"use strict";
var core = require("web.core");
var ajax = require("web.ajax");
var animation = require("website.content.snippets.animation");
/* This JS module replaces core AJAX submission because it is impossible
* to extend it as it is currently designed. */
odoo.define('mass_mailing_custom_unsubscribe.unsubscribe', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
require('web.dom_ready');
var _t = core._t;
animation.registry.mass_mailing_unsubscribe =
animation.Class.extend({
selector: "#unsubscribe_form",
start: function () {
this.controller = '/mail/mailing/unsubscribe';
this.$alert = this.$(".alert");
this.$email = this.$("input[name='email']");
this.$contacts = this.$("input[name='contact_ids']");
this.$mailing_id = this.$("input[name='mailing_id']");
this.$token = this.$("input[name='token']");
this.$res_id = this.$("input[name='res_id']");
this.$reasons = this.$(".js_unsubscription_reason");
this.$details = this.$reasons.find("[name='details']");
this.$el.on("submit", $.proxy(this.submit, this));
this.$contacts.on("change", $.proxy(this.toggle_reasons, this));
this.toggle_reasons();
},
// Helper to get list ids, to use in this.$contacts.map()
int_val: function (index, element) {
return parseInt($(element).val(), 10);
},
// Get a filtered array of integer IDs of matching lists
contact_ids: function (checked) {
var filter = checked ? ":checked" : ":not(:checked)";
return this.$contacts.filter(filter).map(this.int_val).get();
},
var email = $("input[name='email']").val();
var mailing_id = parseInt($("input[name='mailing_id']").val());
var res_id = parseInt($("input[name='res_id']").val());
var token = (location.search.split('token' + '=')[1] || '').split('&')[0];
var $mailing_lists = $("input[name='contact_ids']");
var $reasons = $("#custom_div_feedback");
var $details = $("textarea[name='details']");
var $radio = $(":radio");
var $info_state = $("#info_state, #custom_div_feedback");
$radio.on('change click', function (e) {
$details.prop(
"required",
$(event.target).is("[data-details-required]") && $(event.target).is(":visible")
);
});
// Display reasons form only if there are unsubscriptions
toggle_reasons: function () {
var toggle_reasons = function () {
// Find contacts that were checked and now are unchecked
var $disabled = this.$contacts.filter(function () {
var $disabled = $mailing_lists.filter(function () {
var $this = $(this);
return !$this.prop("checked") && $this.attr("checked");
});
// Hide reasons form if you are only subscribing
this.$reasons.toggleClass("hidden", !$disabled.length);
var $radios = this.$reasons.find(":radio");
if (this.$reasons.is(":hidden")) {
$reasons.toggleClass("d-none", !$disabled.length);
var $radios = $reasons.find(":radio");
if ($reasons.is(":hidden")) {
// Uncheck chosen reason
$radios.prop("checked", false)
// Unrequire specifying a reason
.prop("required", false)
// Remove possible constraints for details
.trigger("change");
// Clear textarea
$details.val("");
} else {
// Require specifying a reason
$radios.prop("required", true);
}
},
// Get values to send
values: function () {
var result = {
email: this.$email.val(),
mailing_id: parseInt(this.$mailing_id.val(), 10),
opt_in_ids: this.contact_ids(true),
opt_out_ids: this.contact_ids(false),
res_id: parseInt(this.$res_id.val(), 10),
token: this.$token.val(),
};
$mailing_lists.change(function (e) {
toggle_reasons();
$('#info_state').addClass('invisible');
});
if (email != '' && email != undefined) {
ajax.jsonRpc('/mailing/blacklist/check', 'call', {
'email': email,
'mailing_id': mailing_id,
'res_id': res_id,
'token': token
})
.then(function (result) {
if (result == 'unauthorized') {
$('#button_add_blacklist').hide();
$('#button_remove_blacklist').hide();
}
else if (result == true) {
$('#button_remove_blacklist').show();
toggle_opt_out_section(false);
}
else if (result == false) {
$('#button_add_blacklist').show();
toggle_opt_out_section(true);
}
else {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
}
else {
$('#div_blacklist').hide();
}
var unsubscribed_list = $("input[name='unsubscribed_list']").val();
if (unsubscribed_list) {
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed from ' + unsubscribed_list + "</strong>."));
}
else {
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed</strong>.'));
}
$('#unsubscribe_form').on('submit', function (e) {
e.preventDefault();
var checked_ids = [];
$("input[type='checkbox']:checked").each(function (i) {
checked_ids[i] = parseInt($(this).val());
});
var unchecked_ids = [];
$("input[type='checkbox']:not(:checked)").each(function (i) {
unchecked_ids[i] = parseInt($(this).val());
});
var values = {
'opt_in_ids': checked_ids,
'opt_out_ids': unchecked_ids,
'email': email,
'mailing_id': mailing_id,
'res_id': res_id,
'token': token
};
// Only send reason and details if an unsubscription was found
if (this.$reasons.is(":visible")) {
result.reason_id = parseInt(
this.$reasons.find("[name='reason_id']:checked").val(),
if ($reasons.is(":visible")) {
values.reason_id = parseInt(
$reasons.find("[name='reason_id']:checked").val(),
10
);
result.details = this.$details.val();
}
return result;
},
// Submit by ajax
submit: function (event) {
event.preventDefault();
return ajax.jsonRpc(this.controller, "call", this.values())
.done($.proxy(this.success, this))
.fail($.proxy(this.failure, this));
},
// When you successfully saved the new subscriptions status
success: function () {
this.$alert
.html(_t('Your changes have been saved.'))
.removeClass("alert-info alert-warning")
.addClass("alert-success");
values.details = $details.val();
}
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', values)
.then(function (result) {
if (result == 'unauthorized') {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('You are not authorized to do this!'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else if (result == true) {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('Your changes have been saved.'));
$info_state.removeClass('alert-info').addClass('alert-success');
// Store checked status, to enable further changes
this.$contacts.each(function () {
$mailing_lists.each(function () {
var $this = $(this);
$this.attr("checked", $this.prop("checked"));
});
this.toggle_reasons();
},
// When you fail to save the new subscriptions status
failure: function () {
this.$alert
.html(_t('Your changes have not been saved, try again later.'))
.removeClass("alert-info alert-success")
.addClass("alert-warning");
},
toggle_reasons();
}
else {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.'));
$info_state.removeClass('alert-info').addClass('alert-warning');
}
})
.fail(function () {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.'));
$info_state.removeClass('alert-info').addClass('alert-warning');
});
});
// ==================
// Blacklist
// ==================
$('#button_add_blacklist').click(function (e) {
e.preventDefault();
if ($reasons.is(":hidden")) {
$reasons.toggleClass("d-none", false);
$reasons.find(":radio").prop("required", true);
}
if (!$("#unsubscribe_form")[0].reportValidity())
return;
ajax.jsonRpc('/mailing/blacklist/add', 'call', {
'email': email,
'mailing_id': mailing_id,
'res_id': res_id,
'token': token,
'reason_id': parseInt(
$reasons.find("[name='reason_id']:checked").val()),
'details': $details.val(),
})
.then(function (result) {
if (result == 'unauthorized') {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('You are not authorized to do this!'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else {
if (result) {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('You have been successfully <strong>added to our blacklist</strong>. '
+ 'You will not be contacted anymore by our services.'));
$info_state.removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
toggle_opt_out_section(false);
// set mailing lists checkboxes to previous state
$mailing_lists.each(function () {
var $this = $(this);
$this.prop("checked", $(this)[0].hasAttribute("checked"));
});
// Hide reasons and reset reason fields
$reasons.toggleClass("d-none", true)
.find(":radio").prop("checked", false);
$details.val("").prop("required", false);
}
else {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
$('#button_add_blacklist').hide();
$('#button_remove_blacklist').show();
$('#unsubscribed_info').hide();
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
});
return animation.registry.mass_mailing_unsubscribe;
$('#button_remove_blacklist').click(function (e) {
e.preventDefault();
ajax.jsonRpc('/mailing/blacklist/remove', 'call', {
'email': email,
'mailing_id': mailing_id,
'res_id': res_id,
'token': token
})
.then(function (result) {
if (result == 'unauthorized') {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('You are not authorized to do this!'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else {
if (result) {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t("You have been successfully <strong>removed from our blacklist</strong>. "
+ "You are now able to be contacted by our services."));
$info_state.removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
toggle_opt_out_section(true);
}
else {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
$('#button_add_blacklist').show();
$('#button_remove_blacklist').hide();
$('#unsubscribed_info').hide();
}
})
.fail(function () {
$('#info_state').removeClass('invisible');
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
});
});
function toggle_opt_out_section(value) {
var result = !value;
$("#div_opt_out").find('*').attr('disabled', result);
$("#button_add_blacklist").attr('disabled', false);
$("#button_remove_blacklist").attr('disabled', false);
$("#custom_div_feedback").find('*').attr('disabled', false);
if (value) {
$('[name="button_subscription"]').addClass('clickable');
}
else {
$('[name="button_subscription"]').removeClass('clickable');
}
}

65
mass_mailing_custom_unsubscribe/templates/general_reason_form.xml

@ -5,76 +5,41 @@
<odoo>
<template id="reason" name="UI for Providing Unsubscription Reasons">
<div t-attf-class="js_unsubscription_reason #{extra_class or ''}">
<div class="col-md-12 mt16">
Before unsubscribing, could you please tell us why do you want to unsubscribe?
</div>
<div id="custom_div_feedback" t-attf-class="alert alert-success mt-4 #{extra_class or ''}" role="status">
<p>We would appreciate if you provide feedback about why you updated<br/>your subscriptions
</p>
<div class="col-md-12 mb16">
<input
type="hidden"
name="res_id"
t-att-value="res_id"/>
<input
type="hidden"
name="email"
t-att-value="email"/>
<input
type="hidden"
name="token"
t-att-value="token"/>
<t t-foreach="reasons" t-as="reason">
<div class="radio">
<label>
<input
type="radio"
name="reason_id"
t-att-data-details-required="reason.details_required"
t-att-value="reason.id"/>
<input type="radio" name="reason_id" t-att-data-details-required="reason.details_required" t-att-value="reason.id" required="required"/>
<t t-esc="reason.display_name"/>
</label>
</div>
</t>
<div t-attf-class="form-group">
<textarea
name="details"
class="form-control"
placeholder="I am unsubscribing because..."
rows="3"/>
</div>
</div>
<textarea class="form-control" name="details" cols="60" rows="3"></textarea>
</div>
</template>
<template id="reason_form"
name="Unsubscription Reason Form">
<t t-call="website.layout">
<div id="wrap" class="oe_structure oe_empty">
<section class="mt16 mb16">
<form
id="reason_form"
class="container"
t-attf-action="/mail/mailing/#{mailing.id}/unsubscribe"
method="post">
<input
type="hidden"
name="csrf_token"
t-att-value="request.csrf_token()"/>
<template id="reason_form" name="Unsubscription Reason Form">
<t t-call="mass_mailing.layout">
<div class="container">
<div class="row">
<div class="col-md-12 text-center mt16 mb32">
<h2>
Mailing Unsubscription
</h2>
</div>
<form id="reason_form" t-attf-action="/mail/mailing/#{mailing_id}/unsubscribe?token=#{token}&amp;debug=assets" method="post" class="col-lg-6 offset-lg-3 mt-4">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="email" t-att-value="email"/>
<input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
<input type="hidden" name="res_id" t-att-value="res_id"/>
<t t-call="mass_mailing_custom_unsubscribe.reason"/>
<div class="form-group mb16 mt16">
<button type="submit" class="btn btn-danger">
Unsubscribe now
</button>
<p class="help-block">Thank you!</p>
</div>
<p class="form-text text-muted">Thank you!</p>
</div>
</form>
</section>
</div>
</div>
</t>
</template>

40
mass_mailing_custom_unsubscribe/templates/mass_mailing_contact_reason.xml

@ -4,23 +4,43 @@
<odoo>
<template id="unsubscribe"
inherit_id="website_mass_mailing.unsubscribe"
name="Add Reasons to Mailing List Management Form">
<template id="unsubscribe" inherit_id="mass_mailing.unsubscribe" name="Add Reasons to Mailing List Management Form">
<!-- Disable core AJAX submission of form, because it is impossible to
extend it as it is designed right now. It is refactored in this addon.
TODO Remove when merged https://github.com/odoo/odoo/pull/14386. -->
<xpath expr="//div[hasclass('container', 'o_unsubscribe_form')]"
position="attributes">
extend it as it is designed right now. -->
<xpath expr="//div[hasclass('container', 'o_unsubscribe_form')]" position="attributes">
<attribute name="class" value="container o_unsubscribe_form_custom"/>
</xpath>
<!-- Hide original feedback textarea to put another one after mailing lists checkboxes -->
<xpath expr="//div[@id='div_feedback']" position="attributes">
<attribute name="class" add="d-none" separator=" "/>
</xpath>
<!-- Add reasons to mass mailing list manager -->
<xpath expr="//t[@t-as='contact']/.." position="after">
<xpath expr="//ul[hasclass('list-group')]" position="after">
<t t-call="mass_mailing_custom_unsubscribe.reason">
<t t-set="extra_class" t-value="'d-none'"/>
</t>
</xpath>
</template>
<template id="unsubscribed"
inherit_id="mass_mailing.unsubscribed"
name="Add Reasons to Blacklist Management Form">
<!-- Disable core AJAX submission of form, because it is impossible to
extend it as it is designed right now. -->
<xpath expr="//div[hasclass('container', 'o_unsubscribe_form')]" position="attributes">
<attribute name="class" value="container o_unsubscribe_form_custom"/>
</xpath>
<!-- Add reasons to blacklist manager -->
<xpath expr="//div[@id='button_add_blacklist']" position="before">
<form id="unsubscribe_form">
<t t-call="mass_mailing_custom_unsubscribe.reason">
<t t-set="extra_class" t-value="'hidden'"/>
<t t-set="extra_class" t-value="'d-none'"/>
</t>
</form>
</xpath>
</template>
</template>
</odoo>

1
mass_mailing_custom_unsubscribe/tests/__init__.py

@ -2,4 +2,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_unsubscription
from . import test_ui

149
mass_mailing_custom_unsubscribe/tests/test_ui.py

@ -1,149 +0,0 @@
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import mock
from contextlib import contextmanager
from odoo.tests.common import HttpCase
from werkzeug import urls
class UICase(HttpCase):
_tour_run = "odoo.__DEBUG__.services['web_tour.tour'].run('%s')"
_tour_ready = "odoo.__DEBUG__.services['web_tour.tour'].tours.%s.ready"
def extract_url(self, mail, *args, **kwargs):
url = mail._get_unsubscribe_url(self.email)
self.assertTrue(urls.url_parse(url).decode_query().get('token'))
self.assertTrue(url.startswith(self.domain))
self.url = url.replace(self.domain, "", 1)
return True
def setUp(self):
super(UICase, self).setUp()
self.email = "test.contact@example.com"
self.mail_postprocess_patch = mock.patch(
"openerp.addons.mass_mailing.models.mail_mail.MailMail."
"_postprocess_sent_message",
autospec=True,
side_effect=self.extract_url,
)
with self.tempenv() as env:
self.domain = env["ir.config_parameter"].get_param('web.base.url')
List = self.lists = env["mail.mass_mailing.list"]
Mailing = self.mailings = env["mail.mass_mailing"]
Contact = self.contacts = env["mail.mass_mailing.contact"]
for n in range(3):
self.lists += List.create({
"name": "test list %d" % n,
})
self.mailings += Mailing.create({
"name": "test mailing %d" % n,
"mailing_model_id": self.env["mail.mass_mailing.contact"],
"contact_list_ids": [(6, 0, self.lists.ids)],
"reply_to_mode": "thread",
})
self.mailings[n]._onchange_model_and_list()
# HACK https://github.com/odoo/odoo/pull/14429
self.mailings[n].body_html = """
<div>
<a href="/unsubscribe_from_list">
This link should get the unsubscription URL
</a>
</div>
"""
self.contacts += Contact.create({
"name": "test contact %d" % n,
"email": self.email,
"mailing_list_id": self.lists[n].id,
})
def tearDown(self):
del self.email, self.lists, self.contacts, self.mailings, self.url
super(UICase, self).tearDown()
@contextmanager
def tempenv(self):
with self.cursor() as cr:
env = self.env(cr)
try:
self.lists = self.lists.with_env(env)
self.contacts = self.contacts.with_env(env)
self.mailings = self.mailings.with_env(env)
except AttributeError:
pass # We are in :meth:`~.setUp`
yield env
def test_contact_unsubscription(self):
"""Test a mass mailing contact that wants to unsubscribe."""
with self.tempenv() as env:
# This list we are unsubscribing from, should appear always in UI
self.lists[0].not_cross_unsubscriptable = True
# This another list should not appear in UI
self.lists[2].not_cross_unsubscriptable = True
# Extract the unsubscription link from the message body
with self.mail_postprocess_patch:
self.mailings[0].send_mail()
tour = "mass_mailing_custom_unsubscribe_tour_contact"
self.phantom_js(
url_path=self.url,
code=self._tour_run % tour,
ready=self._tour_ready % tour)
# Check results from running tour
with self.tempenv() as env:
self.assertTrue(self.contacts[0].opt_out)
self.assertTrue(self.contacts[1].opt_out)
self.assertFalse(self.contacts[2].opt_out)
unsubscriptions = env["mail.unsubscription"].search([
("mass_mailing_id", "=", self.mailings[0].id),
("email", "=", self.email),
("unsubscriber_id", "in",
["%s,%d" % (cnt._name, cnt.id)
for cnt in self.contacts]),
("details", "=",
"I want to unsubscribe because I want. Period."),
("reason_id", "=",
env.ref("mass_mailing_custom_unsubscribe.reason_other").id),
])
try:
self.assertEqual(2, len(unsubscriptions))
except AssertionError:
# HACK This works locally but fails on travis, undo in v10
pass
def test_partner_unsubscription(self):
"""Test a partner that wants to unsubscribe."""
with self.tempenv() as env:
# Change mailing to be sent to partner
partner_id = env["res.partner"].name_create(
"Demo Partner <%s>" % self.email)[0]
self.mailings[0].mailing_model_id = self.env.ref(
"base.model_res_partner")
self.mailings[0].mailing_domain = repr([
('opt_out', '=', False),
('id', '=', partner_id),
])
# Extract the unsubscription link from the message body
with self.mail_postprocess_patch:
self.mailings[0].send_mail()
tour = "mass_mailing_custom_unsubscribe_tour_partner"
self.phantom_js(
url_path=self.url,
code=self._tour_run % tour,
ready=self._tour_ready % tour)
# Check results from running tour
with self.tempenv() as env:
partner = env["res.partner"].browse(partner_id)
self.assertTrue(partner.opt_out)
unsubscriptions = env["mail.unsubscription"].search([
("mass_mailing_id", "=", self.mailings[0].id),
("email", "=", self.email),
("unsubscriber_id", "=", "res.partner,%d" % partner_id),
("details", "=", False),
("reason_id", "=",
env.ref("mass_mailing_custom_unsubscribe"
".reason_not_interested").id),
])
self.assertEqual(1, len(unsubscriptions))

6
mass_mailing_custom_unsubscribe/views/assets.xml

@ -4,11 +4,9 @@
<odoo>
<template id="assets_frontend"
inherit_id="website.assets_frontend">
<template id="assets_backend"
inherit_id="web.assets_backend">
<xpath expr=".">
<script type="text/javascript"
src="/mass_mailing_custom_unsubscribe/static/src/js/require_details.js"/>
<script type="text/javascript"
src="/mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js"/>
</xpath>

30
mass_mailing_custom_unsubscribe/views/mail_mass_mailing_contact_view.xml

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 David Vidal <david.vidal@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_mail_mass_mailing_contact_form" model="ir.ui.view">
<field name="model">mail.mass_mailing.contact</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_form"/>
<field name="arch" type="xml">
<field name="list_ids" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="email" position="after">
<field name="mailing_list_id"/>
</field>
</field>
</record>
<record id="view_mail_mass_mailing_contact_tree" model="ir.ui.view">
<field name="model">mail.mass_mailing.contact</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_tree"/>
<field name="arch" type="xml">
<field name="email" position="before">
<field name="mailing_list_id"/>
</field>
</field>
</record>
</odoo>

19
mass_mailing_custom_unsubscribe/views/mail_mass_mailing_list_view.xml

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_mail_mass_mailing_list_form" model="ir.ui.view">
<field name="model">mail.mass_mailing.list</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_list_form"/>
<field name="arch" type="xml">
<div class="oe_title" position="after">
<group>
<field name="not_cross_unsubscriptable"/>
</group>
</div>
</field>
</record>
</odoo>

31
mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml

@ -14,7 +14,7 @@
<field name="date"/>
<field name="mass_mailing_id"/>
<field name="unsubscriber_id"/>
<field name="mailing_list_id" widget="many2many_tags"/>
<field name="mailing_list_ids" widget="many2many_tags"/>
<field name="email"/>
<field name="action"/>
<field name="reason_id"
@ -40,11 +40,11 @@
<field name="name">Mail Unsubscription Tree</field>
<field name="model">mail.unsubscription</field>
<field name="arch" type="xml">
<tree decoration-warning="action == 'unsubscription'">
<tree decoration-warning="action in ['unsubscription', 'blacklisting']">
<field name="date"/>
<field name="mass_mailing_id"/>
<field name="unsubscriber_id"/>
<field name="mailing_list_id" widget="many2many_tags"/>
<field name="mailing_list_ids" widget="many2many_tags"/>
<field name="email" invisible="True"/>
<field name="action"/>
<field name="reason_id"/>
@ -60,23 +60,29 @@
<search>
<field name="mass_mailing_id"/>
<field name="unsubscriber_id"/>
<field name="mailing_list_id"/>
<field name="mailing_list_ids"/>
<field name="email"/>
<field name="reason_id"/>
<field name="details"/>
<separator/>
<group string="Group by">
<filter string="Month"
<filter name="group_by_month"
string="Month"
context="{'group_by': 'date:month'}"/>
<filter string="Year"
<filter name="group_by_year"
string="Year"
context="{'group_by': 'date:year'}"/>
<filter string="Action"
<filter name="group_by_action"
string="Action"
context="{'group_by': 'action'}"/>
<filter string="Email"
<filter name="group_by_email"
string="Email"
context="{'group_by': 'email'}"/>
<filter string="Reason"
<filter name="group_by_reason"
string="Reason"
context="{'group_by': 'reason_id'}"/>
<filter string="Mass mailing"
<filter name="group_by_mass_mailing"
string="Mass mailing"
context="{'group_by': 'mass_mailing_id'}"/>
</group>
</search>
@ -89,7 +95,7 @@
<field name="arch" type="xml">
<pivot string="(Un)subscriptions">
<field name="reason_id" type="row"/>
<field name="mailing_list_id" type="row"/>
<field name="mailing_list_ids" type="row"/>
<field name="action" type="col"/>
</pivot>
</field>
@ -111,8 +117,7 @@
view_mode="tree,form,pivot,graph"
res_model="mail.unsubscription"/>
<menuitem id="mail_unsubscription_menu"
parent="mass_mailing.mass_mailing_menu"
<menuitem id="mail_unsubscription_menu" parent="mass_mailing.mass_mailing_menu_root"
action="mail_unsubscription_action"/>
</odoo>
Loading…
Cancel
Save