mirror of https://github.com/OCA/social.git
[9.0][MIG][mass_mailing_custom_unsubscribe] Migrate.
- Imported last updates from v8. - Adapted to v9. - Added a saner default to `mass_mailing.salt` configuration parameter by reusing `database.secret` if available, hoping that some day https://github.com/odoo/odoo/pull/12040 gets merged. - Updated README. - Increase security, drop backwards compatibility. Security got improved upstream, which would again break compatibility among current addon and future master upstream. I choose to break it now and keep it secured future-wise, so I drop the backwards compatibility features. - Includes tour tests. - Removes outdated tests. - Extends the mailing list management form when unsubscriber is a contact. - Adds a reason form even if he is not. - Avoids all methods that were not model-agnostic. [FIX][mass_mailing_custom_unsubscribe] Reasons noupdate After this fix, when you update the addon, you will not lose your customized reasons. [FIX] Compatibilize with mass_mailing_partner Current test code was based on the assumption that the `@api.model` decorator on `create()` ensured an empty recordset when running the method, but that's not true. This was causing an incompatibility betwee these tests and the `mass_mailing_partner` addon, which works assuming 0-1 recordsets. Now records are created from an empty recordset, and thus tests work everywhere. Update instructions If the user does not add the unsubscribe snippet, nothing will happen, so it's added to README to avoid confusion when testing/using the addon. [FIX] Use the right operator to preserve recordsets order Using `|=` sorts records at will each time (treating them as Python's `set`). Using `+=` always appends a record to the end of the set. Since we are using the record position in the set, this caused the test to work sometimes and fail other times. Now it works always.pull/418/head
parent
d615c7d7fc
commit
f4ec126a1a
|
@ -1,40 +1,20 @@
|
||||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||||
:alt: License: AGPL-3
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
|
||||||
==========================================================
|
==========================================================
|
||||||
Customizable unsubscription process on mass mailing emails
|
Customizable unsubscription process on mass mailing emails
|
||||||
==========================================================
|
==========================================================
|
||||||
|
|
||||||
With this module you can set a custom unsubscribe link appended at the bottom
|
This addon extends the unsubscription form to let you:
|
||||||
of mass mailing emails.
|
|
||||||
|
|
||||||
It also displays a beautiful and simple unsubscription form when somebody
|
- Choose which mailing lists are not cross-unsubscriptable when unsubscribing
|
||||||
unsubscribes, to let you know why and let the user unsubscribe form another
|
from a different one.
|
||||||
mailing lists at the same time; and then displays a beautiful and customizable
|
- Know why and when a contact as been unsubscribed from a mass mailing.
|
||||||
goodbye message.
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Unsubscription Message In Mail Footer
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
To configure unsubscribe label go to *Settings > Technical > Parameters >
|
|
||||||
System parameters* and add a ``mass_mailing.unsubscribe.label`` parameter
|
|
||||||
with HTML to set at the bottom of mass emailing emails. Including ``%(url)s``
|
|
||||||
variable where unsubscribe link.
|
|
||||||
|
|
||||||
For example::
|
|
||||||
|
|
||||||
<small>You can unsubscribe <a href="%(url)s">here</a></small>
|
|
||||||
|
|
||||||
Additionally, you can disable this link if you set this parameter to ``False``.
|
|
||||||
|
|
||||||
If this parameter (``mass_mailing.unsubscribe.label``) does not exist, the
|
|
||||||
default 'Click to unsubscribe' link will appear, with the advantage that it is
|
|
||||||
translatable via *Settings > Translations > Application Terms > Translated
|
|
||||||
terms*.
|
|
||||||
|
|
||||||
Unsubscription Reasons
|
Unsubscription Reasons
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
@ -46,63 +26,44 @@ they are going to unsubscribe. To do it:
|
||||||
#. If *Details required* is enabled, they will have to fill a text area to
|
#. If *Details required* is enabled, they will have to fill a text area to
|
||||||
continue.
|
continue.
|
||||||
|
|
||||||
Unsubscription Goodbye Message
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
Your unsubscriptors will receive a beautier goodbye page. You can customize it
|
|
||||||
with these links **after installing the module**:
|
|
||||||
|
|
||||||
* `Unsubscription successful </page/mass_mailing_custom_unsubscribe.successs>`_.
|
|
||||||
* `Unsubscription failed </page/mass_mailing_custom_unsubscribe.failure>`_.
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Once configured, just send mass mailings as usual.
|
Once configured:
|
||||||
|
|
||||||
If somebody gets unsubscribed, you will see logs about that under
|
#. Go to *Mass Mailing > Mailings > Mass Mailings > Create*.
|
||||||
*Marketing > Mass Mailing > Unsubscriptions*.
|
#. 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*.
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
:alt: Try me on Runbot
|
:alt: Try me on Runbot
|
||||||
:target: https://runbot.odoo-community.org/runbot/205/8.0
|
:target: https://runbot.odoo-community.org/runbot/205/9.0
|
||||||
|
|
||||||
Known issues / Roadmap
|
Known issues / Roadmap
|
||||||
======================
|
======================
|
||||||
|
|
||||||
* This needs tests.
|
|
||||||
* This custom HTML is not translatable, so as a suggestion, you can define
|
|
||||||
the same text in several languages in several lines.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code:: html
|
|
||||||
|
|
||||||
<small>[EN] You can unsubscribe <a href="%(url)s">here</a></small><br/>
|
|
||||||
<small>[ES] Puedes darte de baja <a href="%(url)s">aquí</a></small>
|
|
||||||
|
|
||||||
* If you use the ``website_multi`` module, you will probably find that the
|
|
||||||
views are not visible by default.
|
|
||||||
* This module adds a security hash for mass mailing unsubscription URLs, which
|
* This module adds a security hash for mass mailing unsubscription URLs, which
|
||||||
makes to not work anymore URLs of mass mailing messages sent before its
|
disables insecure URLs from mass mailing messages sent before its
|
||||||
installation. If you need backwards compatibility, disable this security
|
installation. This can be a problem, but anyway you'd get that problem in
|
||||||
feature by removing the ``mass_mailing.salt`` system parameter. To avoid
|
Odoo 11.0, so at least this addon will be forward-compatible with it.
|
||||||
breaking current installations, you will not get a salt if you are upgrading
|
* This module replaces AJAX submission core implementation from the mailing
|
||||||
the addon. If you want a salt, create the above system parameter and assign a
|
list management form, because it is impossible to extend it. When
|
||||||
random value to it.
|
https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most
|
||||||
* Security should be patched upstream. Remove security features in the version
|
needed changes), this addon will need a refactoring (mostly removing
|
||||||
where https://github.com/odoo/odoo/pull/12040 gets merged (if it does).
|
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``.
|
||||||
|
|
||||||
Bug Tracker
|
Bug Tracker
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/issues>`_.
|
Bugs are tracked on `GitHub Issues
|
||||||
In case of trouble, please check there if your issue has already been reported.
|
<https://github.com/OCA/social/issues>`_. In case of trouble, please
|
||||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
|
check there if your issue has already been reported. If you spotted it first,
|
||||||
`here <https://github.com/OCA/
|
help us smashing it by providing a detailed and welcomed feedback.
|
||||||
social/issues/new?body=module:%20
|
|
||||||
mass_mailing_custom_unsubscribe%0Aversion:%20
|
|
||||||
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
=======
|
=======
|
||||||
|
@ -110,9 +71,9 @@ Credits
|
||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* Rafael Blasco <rafabn@antiun.com>
|
* Rafael Blasco <rafael.blasco@tecnativa.com>
|
||||||
* Antonio Espinosa <antonioea@antiun.com>
|
* Antonio Espinosa <antonio.espinosa@tecnativa.com>
|
||||||
* Jairo Llopis <yajo.sk8@gmail.com>
|
* Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
|
|
||||||
Maintainer
|
Maintainer
|
||||||
----------
|
----------
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Python source code encoding : https://www.python.org/dev/peps/pep-0263/
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
##############################################################################
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
# For copyright and license notices, see __openerp__.py file in root directory
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
from . import controllers, models
|
from . import controllers, models
|
||||||
|
|
|
@ -1,52 +1,34 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Python source code encoding : https://www.python.org/dev/peps/pep-0263/
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
##############################################################################
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
#
|
|
||||||
# OpenERP, Odoo Source Management Solution
|
|
||||||
# Copyright (c) 2015 Antiun Ingeniería S.L. (http://www.antiun.com)
|
|
||||||
# Antonio Espinosa <antonioea@antiun.com>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published
|
|
||||||
# by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
{
|
{
|
||||||
'name': "Customizable unsubscription process on mass mailing emails",
|
'name': "Customizable unsubscription process on mass mailing emails",
|
||||||
|
"summary": "Know unsubscription reasons, track them",
|
||||||
'category': 'Marketing',
|
'category': 'Marketing',
|
||||||
'version': '8.0.2.0.0',
|
'version': '9.0.2.0.0',
|
||||||
'depends': [
|
'depends': [
|
||||||
'mass_mailing',
|
'website_mass_mailing',
|
||||||
'website_crm',
|
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/install_salt.xml',
|
'data/mail_unsubscription_reason.xml',
|
||||||
'data/mail.unsubscription.reason.csv',
|
'templates/general_reason_form.xml',
|
||||||
|
'templates/mass_mailing_contact_reason.xml',
|
||||||
'views/assets.xml',
|
'views/assets.xml',
|
||||||
'views/mail_unsubscription_reason_view.xml',
|
'views/mail_unsubscription_reason_view.xml',
|
||||||
'views/mail_mass_mailing_list_view.xml',
|
'views/mail_mass_mailing_list_view.xml',
|
||||||
'views/mail_unsubscription_view.xml',
|
'views/mail_unsubscription_view.xml',
|
||||||
'views/pages.xml',
|
],
|
||||||
|
"demo": [
|
||||||
|
'demo/assets.xml',
|
||||||
],
|
],
|
||||||
'images': [
|
'images': [
|
||||||
'images/failure.png',
|
|
||||||
'images/form.png',
|
'images/form.png',
|
||||||
'images/success.png',
|
|
||||||
],
|
],
|
||||||
'author': 'Antiun Ingeniería S.L., '
|
'author': 'Antiun Ingeniería S.L., '
|
||||||
'Tecnativa,'
|
'Tecnativa,'
|
||||||
'Odoo Community Association (OCA)',
|
'Odoo Community Association (OCA)',
|
||||||
'website': 'http://www.antiun.com',
|
'website': 'https://www.tecnativa.com',
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
'installable': False,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
|
|
@ -1,32 +1,22 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com)
|
# Copyright 2015 Antiun Ingeniería S.L. (http://www.antiun.com)
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
from openerp import exceptions
|
import logging
|
||||||
from openerp.http import local_redirect, request, route
|
|
||||||
from openerp.addons.mass_mailing.controllers.main import MassMailController
|
from openerp.http import request, route
|
||||||
from .. import exceptions as _ex
|
from openerp.addons.website_mass_mailing.controllers.main \
|
||||||
|
import MassMailController
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CustomUnsubscribe(MassMailController):
|
class CustomUnsubscribe(MassMailController):
|
||||||
def _mailing_list_contacts_by_email(self, email):
|
def reason_form(self, mailing, email, res_id, token):
|
||||||
"""Gets the mailing list contacts by email.
|
|
||||||
|
|
||||||
This should not be displayed to the final user if security validations
|
|
||||||
have not been matched.
|
|
||||||
"""
|
|
||||||
return request.env["mail.mass_mailing.contact"].sudo().search([
|
|
||||||
("email", "=", email),
|
|
||||||
("opt_out", "=", False),
|
|
||||||
("list_id.not_cross_unsubscriptable", "=", False),
|
|
||||||
])
|
|
||||||
|
|
||||||
def unsubscription_reason(self, mailing_id, email, res_id, token,
|
|
||||||
qcontext_extra=None):
|
|
||||||
"""Get the unsubscription reason form.
|
"""Get the unsubscription reason form.
|
||||||
|
|
||||||
:param mail.mass_mailing mailing_id:
|
:param mail.mass_mailing mailing:
|
||||||
Mailing where the unsubscription is being processed.
|
Mailing where the unsubscription is being processed.
|
||||||
|
|
||||||
:param str email:
|
:param str email:
|
||||||
|
@ -35,203 +25,81 @@ class CustomUnsubscribe(MassMailController):
|
||||||
:param int res_id:
|
:param int res_id:
|
||||||
ID of the unsubscriber.
|
ID of the unsubscriber.
|
||||||
|
|
||||||
:param dict qcontext_extra:
|
:param str token:
|
||||||
Additional dictionary to pass to the view.
|
Security token for unsubscriptions.
|
||||||
"""
|
"""
|
||||||
values = self.unsubscription_qcontext(mailing_id, email, res_id, token)
|
reasons = request.env["mail.unsubscription.reason"].search([])
|
||||||
values.update(qcontext_extra or dict())
|
|
||||||
return request.website.render(
|
return request.website.render(
|
||||||
"mass_mailing_custom_unsubscribe.reason_form",
|
"mass_mailing_custom_unsubscribe.reason_form",
|
||||||
values)
|
{
|
||||||
|
"email": email,
|
||||||
|
"mailing": mailing,
|
||||||
|
"reasons": reasons,
|
||||||
|
"res_id": res_id,
|
||||||
|
"token": token,
|
||||||
|
})
|
||||||
|
|
||||||
def unsubscription_qcontext(self, mailing_id, email, res_id, token):
|
@route()
|
||||||
"""Get rendering context for unsubscription form.
|
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
|
||||||
|
"""Ask/save unsubscription reason."""
|
||||||
:param mail.mass_mailing mailing_id:
|
_logger.debug(
|
||||||
Mailing where the unsubscription is being processed.
|
"Called `mailing()` with: %r",
|
||||||
|
(mailing_id, email, res_id, token, post))
|
||||||
:param str email:
|
mailing = request.env["mail.mass_mailing"].sudo().browse(mailing_id)
|
||||||
Email to be unsubscribed.
|
mailing._unsubscribe_token(res_id, token)
|
||||||
|
# Mass mailing list contacts are a special case because they have a
|
||||||
:param int res_id:
|
# subscription management form
|
||||||
ID of the unsubscriber.
|
if mailing.mailing_model == 'mail.mass_mailing.contact':
|
||||||
"""
|
result = super(CustomUnsubscribe, self).mailing(
|
||||||
email_fname = origin_name = None
|
mailing_id, email, res_id, **post)
|
||||||
domain = [("id", "=", res_id)]
|
# FIXME Remove res_id and token in version where this is merged:
|
||||||
record_ids = request.env[mailing_id.mailing_model].sudo()
|
# https://github.com/odoo/odoo/pull/14385
|
||||||
|
result.qcontext.update({
|
||||||
if "email_from" in record_ids._fields:
|
"token": token,
|
||||||
email_fname = "email_from"
|
"res_id": res_id,
|
||||||
elif "email" in record_ids._fields:
|
"contacts": result.qcontext["contacts"].filtered(
|
||||||
email_fname = "email"
|
lambda contact:
|
||||||
|
not contact.list_id.not_cross_unsubscriptable or
|
||||||
if not (email_fname and email):
|
contact.list_id <= mailing.contact_list_ids
|
||||||
# Trying to unsubscribe without email? Bad boy...
|
),
|
||||||
raise exceptions.AccessDenied()
|
"reasons":
|
||||||
|
request.env["mail.unsubscription.reason"].search([]),
|
||||||
domain.append((email_fname, "ilike", email))
|
})
|
||||||
|
return result
|
||||||
# Search additional mailing lists for the unsubscriber
|
# Any other record type gets a simplified form
|
||||||
additional_contacts = self._mailing_list_contacts_by_email(email)
|
|
||||||
|
|
||||||
if record_ids._name == "mail.mass_mailing.contact":
|
|
||||||
domain.append(
|
|
||||||
("list_id", "in", mailing_id.contact_list_ids.ids))
|
|
||||||
|
|
||||||
# Unsubscription targets
|
|
||||||
record_ids = record_ids.search(domain)
|
|
||||||
|
|
||||||
if record_ids._name == "mail.mass_mailing.contact":
|
|
||||||
additional_contacts -= record_ids
|
|
||||||
|
|
||||||
if not record_ids:
|
|
||||||
# Trying to unsubscribe with fake criteria? Bad boy...
|
|
||||||
raise exceptions.AccessDenied()
|
|
||||||
|
|
||||||
# Get data to identify the source of the unsubscription
|
|
||||||
fnames = self.unsubscription_special_fnames(record_ids._name)
|
|
||||||
first = record_ids[:1]
|
|
||||||
contact_name = first[fnames.get("contact", "name")]
|
|
||||||
origin_model_name = request.env["ir.model"].search(
|
|
||||||
[("model", "=", first._name)]).name
|
|
||||||
try:
|
try:
|
||||||
first = first[fnames["related"]]
|
# Check if we already have a reason for unsubscription
|
||||||
except KeyError:
|
reason_id = int(post["reason_id"])
|
||||||
pass
|
except (KeyError, ValueError):
|
||||||
try:
|
# No reasons? Ask for them
|
||||||
origin_name = first[fnames["origin"]]
|
return self.reason_form(mailing, email, res_id, token)
|
||||||
except KeyError:
|
else:
|
||||||
pass
|
# Unsubscribe, saving reason and details by context
|
||||||
|
request.context.update({
|
||||||
|
"default_reason_id": reason_id,
|
||||||
|
"default_details": post.get("details") or False,
|
||||||
|
})
|
||||||
|
del request.env
|
||||||
|
# You could get a DetailsRequiredError here, but only if HTML5
|
||||||
|
# validation fails, which should not happen in modern browsers
|
||||||
|
return super(CustomUnsubscribe, self).mailing(
|
||||||
|
mailing_id, email, res_id, **post)
|
||||||
|
|
||||||
# Get available reasons
|
@route()
|
||||||
reason_ids = (
|
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id,
|
||||||
request.env["mail.unsubscription.reason"].search([]))
|
token, reason_id=None, details=None):
|
||||||
|
"""Store unsubscription reasons when unsubscribing from RPC."""
|
||||||
return {
|
# Update request context and reset environment
|
||||||
"additional_contact_ids": additional_contacts,
|
if reason_id:
|
||||||
"contact_name": contact_name,
|
request.context["default_reason_id"] = int(reason_id)
|
||||||
"email": email,
|
request.context["default_details"] = details or False
|
||||||
"mailing_id": mailing_id,
|
# FIXME Remove token check in version where this is merged:
|
||||||
"origin_model_name": origin_model_name,
|
# https://github.com/odoo/odoo/pull/14385
|
||||||
"origin_name": origin_name,
|
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
|
||||||
"reason_ids": reason_ids,
|
mailing._unsubscribe_token(res_id, token)
|
||||||
"record_ids": record_ids,
|
_logger.debug(
|
||||||
"res_id": res_id,
|
"Called `unsubscribe()` with: %r",
|
||||||
"token": token,
|
(mailing_id, opt_in_ids, opt_out_ids, email, res_id, token,
|
||||||
}
|
reason_id, details))
|
||||||
|
return super(CustomUnsubscribe, self).unsubscribe(
|
||||||
def unsubscription_special_fnames(self, model):
|
mailing_id, opt_in_ids, opt_out_ids, email)
|
||||||
"""Define special field names to generate the unsubscription qcontext.
|
|
||||||
|
|
||||||
:return dict:
|
|
||||||
Special fields will depend on the model, so this method should
|
|
||||||
return something like::
|
|
||||||
|
|
||||||
{
|
|
||||||
"related": "parent_id",
|
|
||||||
"origin": "display_name",
|
|
||||||
"contact": "contact_name",
|
|
||||||
}
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
- ``model.name`` is the technical name of the model.
|
|
||||||
- ``related`` indicates the name of a field in ``model.name`` that
|
|
||||||
contains a :class:`openerp.fields.Many2one` field which is
|
|
||||||
considered what the user is unsubscribing from.
|
|
||||||
- ``origin``: is the name of the field that contains the name of
|
|
||||||
what the user is unsubscribing from.
|
|
||||||
- ``contact`` is the name of the field that contains the name of
|
|
||||||
the user that is unsubscribing.
|
|
||||||
|
|
||||||
Missing keys will mean that nothing special is required for that
|
|
||||||
model and it will use the default values.
|
|
||||||
"""
|
|
||||||
specials = {
|
|
||||||
"mail.mass_mailing.contact": {
|
|
||||||
"related": "list_id",
|
|
||||||
"origin": "display_name",
|
|
||||||
},
|
|
||||||
"crm.lead": {
|
|
||||||
"origin": "name",
|
|
||||||
"contact": "contact_name",
|
|
||||||
},
|
|
||||||
"hr.applicant": {
|
|
||||||
"related": "job_id",
|
|
||||||
"origin": "name",
|
|
||||||
},
|
|
||||||
# In case you install OCA's event_registration_mass_mailing
|
|
||||||
"event.registration": {
|
|
||||||
"related": "event_id",
|
|
||||||
"origin": "name",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return specials.get(model, dict())
|
|
||||||
|
|
||||||
@route(auth="public", website=True)
|
|
||||||
def mailing(self, mailing_id, email=None, res_id=None, **post):
|
|
||||||
"""Display a confirmation form to get the unsubscription reason."""
|
|
||||||
mailing = request.env["mail.mass_mailing"]
|
|
||||||
path = "/page/mass_mailing_custom_unsubscribe.%s"
|
|
||||||
good_token = mailing.hash_create(mailing_id, res_id, email)
|
|
||||||
|
|
||||||
# Trying to unsubscribe with fake hash? Bad boy...
|
|
||||||
if good_token and post.get("token") != good_token:
|
|
||||||
return local_redirect(path % "failure")
|
|
||||||
|
|
||||||
mailing = mailing.sudo().browse(mailing_id)
|
|
||||||
contact = request.env["mail.mass_mailing.contact"].sudo()
|
|
||||||
unsubscription = request.env["mail.unsubscription"].sudo()
|
|
||||||
|
|
||||||
if not post.get("reason_id"):
|
|
||||||
# We need to know why you leave, get to the form
|
|
||||||
return self.unsubscription_reason(
|
|
||||||
mailing, email, res_id, post.get("token"))
|
|
||||||
|
|
||||||
# Save reason and details
|
|
||||||
try:
|
|
||||||
with request.env.cr.savepoint():
|
|
||||||
records = unsubscription.create({
|
|
||||||
"email": email,
|
|
||||||
"unsubscriber_id": ",".join(
|
|
||||||
(mailing.mailing_model, res_id)),
|
|
||||||
"reason_id": int(post["reason_id"]),
|
|
||||||
"details": post.get("details", False),
|
|
||||||
"mass_mailing_id": mailing_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Should provide details, go back to form
|
|
||||||
except _ex.DetailsRequiredError:
|
|
||||||
return self.unsubscription_reason(
|
|
||||||
mailing, email, res_id, post.get("token"),
|
|
||||||
{"error_details_required": True})
|
|
||||||
|
|
||||||
# Unsubscribe from additional lists
|
|
||||||
for key, value in post.iteritems():
|
|
||||||
try:
|
|
||||||
label, list_id = key.split(",")
|
|
||||||
if label != "list_id":
|
|
||||||
raise ValueError
|
|
||||||
list_id = int(list_id)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
contact_id = contact.browse(int(value))
|
|
||||||
if contact_id.list_id.id == list_id:
|
|
||||||
contact_id.opt_out = True
|
|
||||||
records += unsubscription.create({
|
|
||||||
"email": email,
|
|
||||||
"unsubscriber_id": ",".join((contact._name, value)),
|
|
||||||
"reason_id": int(post["reason_id"]),
|
|
||||||
"details": post.get("details", False),
|
|
||||||
"mass_mailing_id": mailing_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
# All is OK, unsubscribe
|
|
||||||
result = super(CustomUnsubscribe, self).mailing(
|
|
||||||
mailing_id, email, res_id, **post)
|
|
||||||
records.write({"success": result.data == "OK"})
|
|
||||||
|
|
||||||
# Redirect to the result
|
|
||||||
return local_redirect(path % ("success" if result.data == "OK"
|
|
||||||
else "failure"))
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
|
||||||
|
|
||||||
<openerp>
|
|
||||||
<data noupdate="1">
|
|
||||||
|
|
||||||
<function model="mail.mass_mailing" name="_init_salt_create"/>
|
|
||||||
|
|
||||||
</data>
|
|
||||||
</openerp>
|
|
|
@ -1,5 +0,0 @@
|
||||||
"id","name","sequence","details_required"
|
|
||||||
"reason_not_interested","I'm not interested",10,"False"
|
|
||||||
"reason_not_requested","I did not request this",20,"False"
|
|
||||||
"reason_too_many","I get too many emails",30,"False"
|
|
||||||
"reason_other","Other reason",100,"True"
|
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
|
<openerp>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="reason_not_interested"
|
||||||
|
model="mail.unsubscription.reason"
|
||||||
|
forcecreate="False">
|
||||||
|
<field name="name">I'm not interested</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="details_required" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="reason_not_requested"
|
||||||
|
model="mail.unsubscription.reason"
|
||||||
|
forcecreate="False">
|
||||||
|
<field name="name">I did not request this</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="details_required" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="reason_too_many"
|
||||||
|
model="mail.unsubscription.reason"
|
||||||
|
forcecreate="False">
|
||||||
|
<field name="name">I get too many emails</field>
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="details_required" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="reason_other"
|
||||||
|
model="mail.unsubscription.reason"
|
||||||
|
forcecreate="False">
|
||||||
|
<field name="name">Other reason</field>
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="details_required" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</openerp>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?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>
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from openerp import exceptions
|
from openerp import exceptions
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
|
@ -1,27 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
try:
|
|
||||||
from openupgradelib.openupgrade import rename_xmlids
|
|
||||||
except ImportError:
|
|
||||||
# Simplified version mostly copied from openupgradelib
|
|
||||||
def rename_xmlids(cr, xmlids_spec):
|
|
||||||
for (old, new) in xmlids_spec:
|
|
||||||
if '.' not in old or '.' not in new:
|
|
||||||
raise Exception(
|
|
||||||
'Cannot rename XMLID %s to %s: need the module '
|
|
||||||
'reference to be specified in the IDs' % (old, new))
|
|
||||||
else:
|
|
||||||
query = ("UPDATE ir_model_data SET module = %s, name = %s "
|
|
||||||
"WHERE module = %s and name = %s")
|
|
||||||
cr.execute(query, tuple(new.split('.') + old.split('.')))
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
|
||||||
"""Update database from previous versions, before updating module."""
|
|
||||||
rename_xmlids(
|
|
||||||
cr,
|
|
||||||
(("website.mass_mail_unsubscription_" + r,
|
|
||||||
"mass_mailing_custom_unsubscribe." + r)
|
|
||||||
for r in ("success", "failure")))
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
from . import mail_mail
|
from . import mail_mail
|
||||||
from . import mail_mass_mailing
|
from . import mail_mass_mailing
|
||||||
|
|
|
@ -1,52 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Python source code encoding : https://www.python.org/dev/peps/pep-0263/
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
##############################################################################
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
# For copyright and license notices, see __openerp__.py file in root directory
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
import urlparse
|
|
||||||
import urllib
|
|
||||||
from openerp import api, models
|
from openerp import api, models
|
||||||
from openerp.tools.translate import _
|
|
||||||
|
|
||||||
|
|
||||||
class MailMail(models.Model):
|
class MailMail(models.Model):
|
||||||
_inherit = 'mail.mail'
|
_inherit = 'mail.mail'
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_unsubscribe_url(self, mail, email_to, msg=None):
|
def _get_unsubscribe_url(self, mail, email_to):
|
||||||
m_config = self.env['ir.config_parameter']
|
result = super(MailMail, self)._get_unsubscribe_url(mail, email_to)
|
||||||
base_url = m_config.get_param('web.base.url')
|
token = mail.mailing_id._unsubscribe_token(mail.res_id)
|
||||||
config_msg = m_config.get_param('mass_mailing.unsubscribe.label')
|
return "%s&token=%s" % (result, token)
|
||||||
params = {
|
|
||||||
'db': self.env.cr.dbname,
|
|
||||||
'res_id': mail.res_id,
|
|
||||||
'email': email_to,
|
|
||||||
'token': self.env["mail.mass_mailing"].hash_create(
|
|
||||||
mail.mailing_id.id,
|
|
||||||
mail.res_id,
|
|
||||||
email_to),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Avoid `token=None` in URL
|
|
||||||
if not params["token"]:
|
|
||||||
del params["token"]
|
|
||||||
|
|
||||||
# Generate URL
|
|
||||||
url = urlparse.urljoin(
|
|
||||||
base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
|
|
||||||
'mailing_id': mail.mailing_id.id,
|
|
||||||
'params': urllib.urlencode(params),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
html = ''
|
|
||||||
if config_msg is False:
|
|
||||||
html = '<small><a href="%(url)s">%(label)s</a></small>' % {
|
|
||||||
'url': url,
|
|
||||||
'label': msg or _('Click to unsubscribe'),
|
|
||||||
}
|
|
||||||
elif config_msg.lower() != 'false':
|
|
||||||
html = config_msg % {
|
|
||||||
'url': url,
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
|
|
|
@ -1,35 +1,54 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from hashlib import sha256
|
import hmac
|
||||||
from uuid import uuid4
|
import hashlib
|
||||||
from openerp import api, models
|
from openerp import api, models
|
||||||
|
from openerp.exceptions import AccessDenied
|
||||||
|
from openerp.tools import consteq
|
||||||
|
|
||||||
|
|
||||||
class MailMassMailing(models.Model):
|
class MailMassMailing(models.Model):
|
||||||
_inherit = "mail.mass_mailing"
|
_inherit = "mail.mass_mailing"
|
||||||
|
|
||||||
@api.model
|
@api.multi
|
||||||
def _init_salt_create(self):
|
def _unsubscribe_token(self, res_id, compare=None):
|
||||||
"""Create a salt to secure the unsubscription URLs."""
|
"""Generate a secure hash for this mailing list and parameters.
|
||||||
icp = self.env["ir.config_parameter"]
|
This is appended to the unsubscription URL and then checked at
|
||||||
key = "mass_mailing.salt"
|
unsubscription time to ensure no malicious unsubscriptions are
|
||||||
salt = icp.get_param(key)
|
performed.
|
||||||
if salt is False:
|
|
||||||
salt = str(uuid4())
|
|
||||||
icp.set_param(key, salt, ["base.group_erp_manager"])
|
|
||||||
|
|
||||||
@api.model
|
:param int res_id:
|
||||||
def hash_create(self, mailing_id, res_id, email):
|
ID of the resource that will be unsubscribed.
|
||||||
"""Create a secure hash to know if the unsubscription is trusted.
|
|
||||||
|
|
||||||
:return None/str:
|
:param str compare:
|
||||||
Secure hash, or ``None`` if the system parameter is empty.
|
Received token to be compared with the good one.
|
||||||
|
|
||||||
|
:raise AccessDenied:
|
||||||
|
Will happen if you provide :param:`compare` and it does not match
|
||||||
|
the good token.
|
||||||
"""
|
"""
|
||||||
salt = self.env["ir.config_parameter"].sudo().get_param(
|
secret = self.env["ir.config_parameter"].sudo().get_param(
|
||||||
"mass_mailing.salt")
|
"database.secret")
|
||||||
if not salt:
|
key = (self.env.cr.dbname, self.id, int(res_id))
|
||||||
return None
|
token = hmac.new(str(secret), repr(key), hashlib.sha512).hexdigest()
|
||||||
source = (self.env.cr.dbname, mailing_id, res_id, email, salt)
|
if compare is not None and not consteq(token, str(compare)):
|
||||||
return sha256(",".join(map(unicode, source))).hexdigest()
|
raise AccessDenied()
|
||||||
|
return token
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def update_opt_out(self, mailing_id, email, res_ids, value):
|
||||||
|
"""Save unsubscription reason when opting out from mailing."""
|
||||||
|
mailing = self.browse(mailing_id)
|
||||||
|
if value and self.env.context.get("default_reason_id"):
|
||||||
|
for res_id in res_ids:
|
||||||
|
# reason_id and details are expected from the context
|
||||||
|
self.env["mail.unsubscription"].create({
|
||||||
|
"email": email,
|
||||||
|
"mass_mailing_id": mailing.id,
|
||||||
|
"unsubscriber_id": "%s,%d" % (
|
||||||
|
mailing.mailing_model, int(res_id)),
|
||||||
|
})
|
||||||
|
return super(MailMassMailing, self).update_opt_out(
|
||||||
|
mailing_id, email, res_ids, value)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from openerp import fields, models
|
from openerp import fields, models
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from openerp import _, api, fields, models
|
from openerp import _, api, fields, models
|
||||||
|
@ -36,9 +36,6 @@ class MailUnsubscription(models.Model):
|
||||||
help="More details on why the unsubscription was made.")
|
help="More details on why the unsubscription was made.")
|
||||||
details_required = fields.Boolean(
|
details_required = fields.Boolean(
|
||||||
related="reason_id.details_required")
|
related="reason_id.details_required")
|
||||||
success = fields.Boolean(
|
|
||||||
help="If this is unchecked, it indicates some failure happened in the "
|
|
||||||
"unsubscription process.")
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_date(self):
|
def _default_date(self):
|
||||||
|
@ -53,10 +50,10 @@ class MailUnsubscription(models.Model):
|
||||||
@api.constrains("details", "reason_id")
|
@api.constrains("details", "reason_id")
|
||||||
def _check_details_needed(self):
|
def _check_details_needed(self):
|
||||||
"""Ensure details are given if required."""
|
"""Ensure details are given if required."""
|
||||||
for s in self:
|
for one in self:
|
||||||
if not s.details and s.details_required:
|
if not one.details and one.details_required:
|
||||||
raise exceptions.DetailsRequiredError(
|
raise exceptions.DetailsRequiredError(
|
||||||
_("This reason requires an explanation."))
|
_("Please provide details on why you are unsubscribing."))
|
||||||
|
|
||||||
|
|
||||||
class MailUnsubscriptionReason(models.Model):
|
class MailUnsubscriptionReason(models.Model):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
"read_unsubscription_reason_public","Public users can read unsubscription reasons","model_mail_unsubscription_reason","base.group_public",1,0,0,0
|
read_unsubscription_reason_public,Public users can read unsubscription reasons,model_mail_unsubscription_reason,base.group_public,1,0,0,0
|
||||||
"read_unsubscription_reason_employee","Employee users can read unsubscription reasons","model_mail_unsubscription_reason","base.group_user",1,0,0,0
|
read_unsubscription_reason_employee,Employee users can read unsubscription reasons,model_mail_unsubscription_reason,base.group_user,1,0,0,0
|
||||||
"write_unsubscription_reason","Mass mailing managers can manage unsubscription reasons","model_mail_unsubscription_reason","mass_mailing.group_mass_mailing_campaign",1,1,1,1
|
write_unsubscription_reason,Mass mailing managers can manage unsubscription reasons,model_mail_unsubscription_reason,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||||
"read_unsubscription","Marketing users can read unsubscriptions","model_mail_unsubscription","marketing.group_marketing_user",1,0,0,0
|
read_unsubscription,Marketing users can read unsubscriptions,model_mail_unsubscription,mass_mailing.group_mass_mailing_user,1,0,0,0
|
||||||
"write_unsubscription","Mass mailing managers can manage unsubscriptions","model_mail_unsubscription","mass_mailing.group_mass_mailing_campaign",1,1,1,1
|
write_unsubscription,Mass mailing managers can manage unsubscriptions,model_mail_unsubscription,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||||
|
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/* 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 Tour = require("web.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({
|
||||||
|
id: "mass_mailing_custom_unsubscribe_tour_contact",
|
||||||
|
name: "Mass mailing contact unsubscribes",
|
||||||
|
mode: "test",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "Unsubscription reasons are invisible",
|
||||||
|
waitFor: "#unsubscribe_form .js_unsubscription_reason:hidden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Uncheck list 0",
|
||||||
|
element: "li:contains('test list 0') input",
|
||||||
|
waitFor: "li:contains('test list 0') input:checked",
|
||||||
|
// List 2 is not cross unsubscriptable
|
||||||
|
waitNot: "li:contains('test list 2')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Uncheck list 1",
|
||||||
|
element: "li:contains('test list 1') input:checked",
|
||||||
|
waitFor: ".js_unsubscription_reason:visible",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Choose other reason",
|
||||||
|
element: ".radio:contains('Other reason') :radio",
|
||||||
|
waitFor: ".radio:contains('Other reason') " +
|
||||||
|
":radio:not(:checked)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Add details to reason",
|
||||||
|
element: "[name='details']:visible:propRequired",
|
||||||
|
sampleText: "I want to unsubscribe because I want. Period.",
|
||||||
|
waitFor: ".radio:contains('Other reason') :radio:checked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Update subscriptions 1st time",
|
||||||
|
element: "#unsubscribe_form :submit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Subscribe again to list 0",
|
||||||
|
element: "li:contains('test list 0') input:not(:checked)",
|
||||||
|
waitFor: ".alert-success",
|
||||||
|
waitNot: "#unsubscribe_form .js_unsubscription_reason:visible",
|
||||||
|
onend: function () {
|
||||||
|
// This one will get the success again after next step
|
||||||
|
$(".alert-success").removeClass("alert-success");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Update subscriptions 2nd time",
|
||||||
|
element: "#unsubscribe_form :submit",
|
||||||
|
waitNot: "#unsubscribe_form .js_unsubscription_reason:visible",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Resuscription was OK",
|
||||||
|
waitFor: ".alert-success",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return Tour.tours.mass_mailing_custom_unsubscribe_tour_contact;
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
/* 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 Tour = require("web.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({
|
||||||
|
id: "mass_mailing_custom_unsubscribe_tour_partner",
|
||||||
|
name: "Mass mailing partner unsubscribes",
|
||||||
|
mode: "test",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "Choose other reason",
|
||||||
|
element: ".radio:contains('Other reason') " +
|
||||||
|
":radio:not(:checked)",
|
||||||
|
waitFor: "#reason_form .js_unsubscription_reason",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Switch to not interested reason",
|
||||||
|
element: ".radio:contains(\"I'm not interested\") " +
|
||||||
|
":radio:not(:checked)",
|
||||||
|
waitFor: "[name='details']:propRequired",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Unsubscribe",
|
||||||
|
element: "#reason_form :submit",
|
||||||
|
waitNot: "[name='details']:propRequired",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Successfully unsubscribed",
|
||||||
|
waitFor: ".alert-success:contains(" +
|
||||||
|
"'Your changes have been saved.')",
|
||||||
|
waitNot: "#reason_form",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return Tour.tours.mass_mailing_custom_unsubscribe_tour_partner;
|
||||||
|
});
|
|
@ -1,13 +1,25 @@
|
||||||
/* © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
/* Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
|
* 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("web_editor.snippets.animation");
|
||||||
|
|
||||||
"use strict";
|
return animation.registry.mass_mailing_custom_unsubscribe_require_details =
|
||||||
(function ($) {
|
animation.Class.extend({
|
||||||
$("#reason_form :radio").change(function(event) {
|
selector: ".js_unsubscription_reason",
|
||||||
$("textarea[name=details]").attr(
|
|
||||||
"required",
|
start: function () {
|
||||||
$(event.target).is("[data-details-required]")
|
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]"));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
$("#reason_form :radio:checked").change();
|
});
|
||||||
})(jQuery);
|
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
/* 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"),
|
||||||
|
ajax = require("web.ajax"),
|
||||||
|
animation = require("web_editor.snippets.animation"),
|
||||||
|
_t = core._t;
|
||||||
|
|
||||||
|
return animation.registry.mass_mailing_unsubscribe =
|
||||||
|
animation.Class.extend({
|
||||||
|
selector: "#unsubscribe_form",
|
||||||
|
start: function (editable_mode) {
|
||||||
|
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());
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Display reasons form only if there are unsubscriptions
|
||||||
|
toggle_reasons: function () {
|
||||||
|
// Find contacts that were checked and now are unchecked
|
||||||
|
var $disabled = this.$contacts.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);
|
||||||
|
if (this.$reasons.is(":hidden")) {
|
||||||
|
// Uncheck chosen reason
|
||||||
|
this.$reasons.find(":radio").prop("checked", false)
|
||||||
|
// Remove possible constraints for details
|
||||||
|
.trigger("change");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get values to send
|
||||||
|
values: function () {
|
||||||
|
var result = {
|
||||||
|
email: this.$email.val(),
|
||||||
|
mailing_id: parseInt(this.$mailing_id.val()),
|
||||||
|
opt_in_ids: this.contact_ids(true),
|
||||||
|
opt_out_ids: this.contact_ids(false),
|
||||||
|
res_id: parseInt(this.$res_id.val()),
|
||||||
|
token: this.$token.val(),
|
||||||
|
};
|
||||||
|
// 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());
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Store checked status, to enable further changes
|
||||||
|
this.$contacts.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");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?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="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 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"/>
|
||||||
|
<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>
|
||||||
|
</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()"/>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 text-center mt16 mb32">
|
||||||
|
<h2>
|
||||||
|
Mailing Unsubscription
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?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="unsubscribe"
|
||||||
|
inherit_id="website_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[@class='container o_unsubscribe_form']"
|
||||||
|
position="attributes">
|
||||||
|
<attribute name="class" value="container o_unsubscribe_form_custom"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Add reasons to mass mailing list manager -->
|
||||||
|
<xpath expr="//t[@t-as='contact']/.." position="after">
|
||||||
|
<t t-call="mass_mailing_custom_unsubscribe.reason">
|
||||||
|
<t t-set="extra_class" t-value="'hidden'"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from . import test_unsubscription
|
from . import test_unsubscription
|
||||||
from . import test_mail_mail
|
from . import test_ui
|
||||||
from . import test_controller
|
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# © 2016 LasLabs Inc.
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
import mock
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
from openerp.tests.common import TransactionCase
|
|
||||||
|
|
||||||
from openerp.addons.mass_mailing_custom_unsubscribe.controllers.main import (
|
|
||||||
CustomUnsubscribe
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
model = 'openerp.addons.mass_mailing_custom_unsubscribe.controllers.main'
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def mock_assets():
|
|
||||||
""" Mock & yield controller assets """
|
|
||||||
with mock.patch('%s.request' % model) as request:
|
|
||||||
yield {
|
|
||||||
'request': request,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EndTestException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestController(TransactionCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestController, self).setUp()
|
|
||||||
self.controller = CustomUnsubscribe()
|
|
||||||
|
|
||||||
def _default_domain(self):
|
|
||||||
return [
|
|
||||||
('opt_out', '=', False),
|
|
||||||
('list_id.not_cross_unsubscriptable', '=', False),
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_mailing_list_contacts_by_email_search(self):
|
|
||||||
""" It should search for contacts """
|
|
||||||
expect = 'email'
|
|
||||||
with mock_assets() as mk:
|
|
||||||
self.controller._mailing_list_contacts_by_email(expect)
|
|
||||||
model_obj = mk['request'].env['mail.mass_mailing.contact'].sudo()
|
|
||||||
model_obj.search.assert_called_once_with(
|
|
||||||
[('email', '=', expect)] + self._default_domain()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_mailing_list_contacts_by_email_return(self):
|
|
||||||
""" It should return result of search """
|
|
||||||
expect = 'email'
|
|
||||||
with mock_assets() as mk:
|
|
||||||
res = self.controller._mailing_list_contacts_by_email(expect)
|
|
||||||
model_obj = mk['request'].env['mail.mass_mailing.contact'].sudo()
|
|
||||||
self.assertEqual(
|
|
||||||
model_obj.search(), res,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_unsubscription_reason_gets_context(self):
|
|
||||||
""" It should retrieve unsub qcontext """
|
|
||||||
expect = 'mailing_id', 'email', 'res_id', 'token'
|
|
||||||
with mock_assets():
|
|
||||||
with mock.patch.object(
|
|
||||||
self.controller, 'unsubscription_qcontext'
|
|
||||||
) as unsub:
|
|
||||||
unsub.side_effect = EndTestException
|
|
||||||
with self.assertRaises(EndTestException):
|
|
||||||
self.controller.unsubscription_reason(*expect)
|
|
||||||
unsub.assert_called_once_with(*expect)
|
|
||||||
|
|
||||||
def test_unsubscription_updates_with_extra_context(self):
|
|
||||||
""" It should update qcontext with provided vals """
|
|
||||||
expect = 'mailing_id', 'email', 'res_id', 'token'
|
|
||||||
qcontext = {'context': 'test'}
|
|
||||||
with mock_assets():
|
|
||||||
with mock.patch.object(
|
|
||||||
self.controller, 'unsubscription_qcontext'
|
|
||||||
) as unsub:
|
|
||||||
self.controller.unsubscription_reason(
|
|
||||||
*expect, qcontext_extra=qcontext
|
|
||||||
)
|
|
||||||
unsub().update.assert_called_once_with(qcontext)
|
|
||||||
|
|
||||||
def test_unsubscription_updates_rendered_correctly(self):
|
|
||||||
""" It should correctly render website """
|
|
||||||
expect = 'mailing_id', 'email', 'res_id', 'token'
|
|
||||||
with mock_assets() as mk:
|
|
||||||
with mock.patch.object(
|
|
||||||
self.controller, 'unsubscription_qcontext'
|
|
||||||
) as unsub:
|
|
||||||
self.controller.unsubscription_reason(*expect)
|
|
||||||
mk['request'].website.render.assert_called_once_with(
|
|
||||||
"mass_mailing_custom_unsubscribe.reason_form",
|
|
||||||
unsub(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_unsubscription_updates_returns_site(self):
|
|
||||||
""" It should return website """
|
|
||||||
expect = 'mailing_id', 'email', 'res_id', 'token'
|
|
||||||
with mock_assets() as mk:
|
|
||||||
with mock.patch.object(
|
|
||||||
self.controller, 'unsubscription_qcontext'
|
|
||||||
):
|
|
||||||
res = self.controller.unsubscription_reason(*expect)
|
|
||||||
self.assertEqual(
|
|
||||||
mk['request'].website.render(), res
|
|
||||||
)
|
|
|
@ -1,97 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# © 2016 LasLabs Inc.
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from openerp.tests.common import TransactionCase
|
|
||||||
|
|
||||||
|
|
||||||
model = 'openerp.addons.mass_mailing_custom_unsubscribe.models.mail_mail'
|
|
||||||
|
|
||||||
|
|
||||||
class EndTestException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestMailMail(TransactionCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestMailMail, self).setUp()
|
|
||||||
self.Model = self.env['mail.mail']
|
|
||||||
param_obj = self.env['ir.config_parameter']
|
|
||||||
self.base_url = param_obj.get_param('web.base.url')
|
|
||||||
self.config_msg = param_obj.get_param(
|
|
||||||
'mass_mailing.unsubscribe.label'
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch('%s.urlparse' % model)
|
|
||||||
@mock.patch('%s.urllib' % model)
|
|
||||||
def test_get_unsubscribe_url_proper_url(self, urllib, urlparse):
|
|
||||||
""" It should join the URL w/ proper args """
|
|
||||||
urlparse.urljoin.side_effect = EndTestException
|
|
||||||
expect = mock.MagicMock(), 'email', 'msg'
|
|
||||||
with self.assertRaises(EndTestException):
|
|
||||||
self.Model._get_unsubscribe_url(*expect)
|
|
||||||
urlparse.urljoin.assert_called_once_with(
|
|
||||||
self.base_url,
|
|
||||||
'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
|
|
||||||
'mailing_id': expect[0].mailing_id.id,
|
|
||||||
'params': urllib.urlencode(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch('%s.urlparse' % model)
|
|
||||||
@mock.patch('%s.urllib' % model)
|
|
||||||
def test_get_unsubscribe_url_correct_params(self, urllib, urlparse):
|
|
||||||
""" It should create URL params w/ proper data """
|
|
||||||
urlparse.urljoin.side_effect = EndTestException
|
|
||||||
expect = mock.MagicMock(), 'email', 'msg'
|
|
||||||
with self.assertRaises(EndTestException):
|
|
||||||
self.Model._get_unsubscribe_url(*expect)
|
|
||||||
urllib.urlencode.assert_called_once_with(dict(
|
|
||||||
db=self.env.cr.dbname,
|
|
||||||
res_id=expect[0].res_id,
|
|
||||||
email=expect[1],
|
|
||||||
token=self.env['mail.mass_mailing'].hash_create(
|
|
||||||
expect[0].mailing_id.id,
|
|
||||||
expect[0].res_id,
|
|
||||||
expect[1],
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
@mock.patch('%s.urlparse' % model)
|
|
||||||
@mock.patch('%s.urllib' % model)
|
|
||||||
def test_get_unsubscribe_url_false_config_msg(self, urllib, urlparse):
|
|
||||||
""" It should return default config msg when none supplied """
|
|
||||||
expects = ['uri', False]
|
|
||||||
urlparse.urljoin.return_value = expects[0]
|
|
||||||
with mock.patch.object(self.Model, 'env') as env:
|
|
||||||
env['ir.config_paramater'].get_param.side_effect = expects
|
|
||||||
res = self.Model._get_unsubscribe_url(
|
|
||||||
mock.MagicMock(), 'email', 'msg'
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
expects[0], res,
|
|
||||||
'Did not include URI in default message'
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
'msg', res,
|
|
||||||
'Did not include input msg in default message'
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch('%s.urlparse' % model)
|
|
||||||
@mock.patch('%s.urllib' % model)
|
|
||||||
def test_get_unsubscribe_url_with_config_msg(self, urllib, urlparse):
|
|
||||||
""" It should return config message w/ URL formatted """
|
|
||||||
expects = ['uri', 'test %(url)s']
|
|
||||||
urlparse.urljoin.return_value = expects[0]
|
|
||||||
with mock.patch.object(self.Model, 'env') as env:
|
|
||||||
env['ir.config_paramater'].get_param.side_effect = expects
|
|
||||||
res = self.Model._get_unsubscribe_url(
|
|
||||||
mock.MagicMock(), 'email', 'msg'
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
expects[1] % {'url': expects[0]}, res,
|
|
||||||
'Did not return proper config message'
|
|
||||||
)
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 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 openerp.tests.common import HttpCase
|
||||||
|
|
||||||
|
|
||||||
|
class UICase(HttpCase):
|
||||||
|
def extract_url(self, mail, *args, **kwargs):
|
||||||
|
url = mail._get_unsubscribe_url(mail, self.email)
|
||||||
|
self.assertIn("&token=", url)
|
||||||
|
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",
|
||||||
|
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": "mail.mass_mailing.contact",
|
||||||
|
"contact_list_ids": [(6, 0, self.lists.ids)],
|
||||||
|
"reply_to_mode": "thread",
|
||||||
|
})
|
||||||
|
self.mailings[n].write(
|
||||||
|
self.mailings[n].on_change_model_and_list(
|
||||||
|
self.mailings[n].mailing_model,
|
||||||
|
self.mailings[n].contact_list_ids.ids,
|
||||||
|
)["value"])
|
||||||
|
# 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,
|
||||||
|
"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=("odoo.__DEBUG__.services['web.Tour']"
|
||||||
|
".run('%s', 'test')") % tour,
|
||||||
|
ready="odoo.__DEBUG__.services['web.Tour'].tours.%s" % tour)
|
||||||
|
|
||||||
|
# Check results from running tour
|
||||||
|
with self.tempenv() as env:
|
||||||
|
self.assertFalse(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 = "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=("odoo.__DEBUG__.services['web.Tour']"
|
||||||
|
".run('%s', 'test')") % tour,
|
||||||
|
ready="odoo.__DEBUG__.services['web.Tour'].tours.%s" % 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))
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from openerp.tests.common import TransactionCase
|
from openerp.tests.common import TransactionCase
|
||||||
|
@ -14,7 +14,7 @@ class UnsubscriptionCase(TransactionCase):
|
||||||
"email": "axelor@yourcompany.example.com",
|
"email": "axelor@yourcompany.example.com",
|
||||||
"mass_mailing_id": self.env.ref("mass_mailing.mass_mail_1").id,
|
"mass_mailing_id": self.env.ref("mass_mailing.mass_mail_1").id,
|
||||||
"unsubscriber_id":
|
"unsubscriber_id":
|
||||||
"res.partner,%d" % self.env.ref("base.res_partner_13").id,
|
"res.partner,%d" % self.env.ref("base.res_partner_2").id,
|
||||||
"reason_id":
|
"reason_id":
|
||||||
self.env.ref(
|
self.env.ref(
|
||||||
"mass_mailing_custom_unsubscribe.reason_other").id,
|
"mass_mailing_custom_unsubscribe.reason_other").id,
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
<openerp>
|
<odoo>
|
||||||
<data>
|
|
||||||
|
|
||||||
<template id="assets_frontend"
|
<template id="assets_frontend"
|
||||||
inherit_id="website.assets_frontend">
|
inherit_id="website.assets_frontend">
|
||||||
<xpath expr=".">
|
<xpath expr=".">
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="/mass_mailing_custom_unsubscribe/static/src/js/require_details.js"/>
|
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>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</data>
|
</odoo>
|
||||||
</openerp>
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- © 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
<!-- Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
<openerp>
|
<odoo>
|
||||||
<data>
|
|
||||||
|
|
||||||
<record id="view_mail_mass_mailing_list_form" model="ir.ui.view">
|
<record id="view_mail_mass_mailing_list_form" model="ir.ui.view">
|
||||||
<field name="model">mail.mass_mailing.list</field>
|
<field name="model">mail.mass_mailing.list</field>
|
||||||
|
@ -17,5 +16,4 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</data>
|
</odoo>
|
||||||
</openerp>
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
<openerp>
|
<odoo>
|
||||||
<data>
|
|
||||||
|
|
||||||
<record id="mail_unsubscription_reason_view_form" model="ir.ui.view">
|
<record id="mail_unsubscription_reason_view_form" model="ir.ui.view">
|
||||||
<field name="name">Mail Unsubscription Reason Form</field>
|
<field name="name">Mail Unsubscription Reason Form</field>
|
||||||
|
@ -52,9 +51,7 @@
|
||||||
|
|
||||||
<menuitem
|
<menuitem
|
||||||
id="mail_unsubscription_reason_menu"
|
id="mail_unsubscription_reason_menu"
|
||||||
parent="mass_mailing.marketing_configuration"
|
parent="mass_mailing.menu_mass_mailing_configuration"
|
||||||
groups="mass_mailing.group_mass_mailing_campaign"
|
|
||||||
action="mail_unsubscription_reason_action"/>
|
action="mail_unsubscription_reason_action"/>
|
||||||
|
|
||||||
</data>
|
</odoo>
|
||||||
</openerp>
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
|
||||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||||
|
|
||||||
<openerp>
|
<odoo>
|
||||||
<data>
|
|
||||||
|
|
||||||
<record id="mail_unsubscription_view_form" model="ir.ui.view">
|
<record id="mail_unsubscription_view_form" model="ir.ui.view">
|
||||||
<field name="name">Mail Unsubscription Form</field>
|
<field name="name">Mail Unsubscription Form</field>
|
||||||
|
@ -16,7 +15,6 @@
|
||||||
<field name="mass_mailing_id"/>
|
<field name="mass_mailing_id"/>
|
||||||
<field name="unsubscriber_id"/>
|
<field name="unsubscriber_id"/>
|
||||||
<field name="email"/>
|
<field name="email"/>
|
||||||
<field name="success"/>
|
|
||||||
<field name="reason_id"/>
|
<field name="reason_id"/>
|
||||||
<field name="details"
|
<field name="details"
|
||||||
attrs="{'required': [('details_required', '=', True)]}"/>
|
attrs="{'required': [('details_required', '=', True)]}"/>
|
||||||
|
@ -57,7 +55,6 @@
|
||||||
<field name="mass_mailing_id"/>
|
<field name="mass_mailing_id"/>
|
||||||
<field name="unsubscriber_id"/>
|
<field name="unsubscriber_id"/>
|
||||||
<field name="email"/>
|
<field name="email"/>
|
||||||
<field name="success"/>
|
|
||||||
<field name="reason_id"/>
|
<field name="reason_id"/>
|
||||||
<field name="details"/>
|
<field name="details"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
|
@ -70,8 +67,6 @@
|
||||||
context="{'group_by': 'reason_id'}"/>
|
context="{'group_by': 'reason_id'}"/>
|
||||||
<filter string="Mass mailing"
|
<filter string="Mass mailing"
|
||||||
context="{'group_by': 'mass_mailing_id'}"/>
|
context="{'group_by': 'mass_mailing_id'}"/>
|
||||||
<filter string="Success"
|
|
||||||
context="{'group_by': 'success'}"/>
|
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
|
@ -82,8 +77,7 @@
|
||||||
res_model="mail.unsubscription"/>
|
res_model="mail.unsubscription"/>
|
||||||
|
|
||||||
<menuitem id="mail_unsubscription_menu"
|
<menuitem id="mail_unsubscription_menu"
|
||||||
parent="mass_mailing.mass_mailing_campaign"
|
parent="mass_mailing.mass_mailing_menu"
|
||||||
action="mail_unsubscription_action"/>
|
action="mail_unsubscription_action"/>
|
||||||
|
|
||||||
</data>
|
</odoo>
|
||||||
</openerp>
|
|
||||||
|
|
|
@ -1,155 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<openerp>
|
|
||||||
<data>
|
|
||||||
|
|
||||||
<template name="Unsubscription worked"
|
|
||||||
id="success"
|
|
||||||
page="True">
|
|
||||||
<t t-call="website.layout">
|
|
||||||
<div id="wrap" class="oe_structure oe_empty">
|
|
||||||
<section class="jumbotron mt16 mb16">
|
|
||||||
<div class="container">
|
|
||||||
<h1>
|
|
||||||
You were successfully unsubscribed from our
|
|
||||||
mailing list.
|
|
||||||
</h1>
|
|
||||||
<h3 class="text-muted">
|
|
||||||
It's sad to see you go, but if you love
|
|
||||||
something, let it go.
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
Is there anything else you want to tell us?
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a class="btn btn-primary btn-lg"
|
|
||||||
href="/page/contactus">Contact us</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template name="Unsubscription failed"
|
|
||||||
id="failure"
|
|
||||||
page="True">
|
|
||||||
<t t-call="website.layout">
|
|
||||||
<div id="wrap" class="oe_structure oe_empty">
|
|
||||||
<section class="jumbotron mt16 mb16">
|
|
||||||
<div class="container">
|
|
||||||
<h1>
|
|
||||||
There was an error processing your unsubscription
|
|
||||||
request.
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
We apologize for the inconvenience. You can contact us
|
|
||||||
and we will handle your unsubscription manually.
|
|
||||||
</p>
|
|
||||||
<p>Thanks for your patience.</p>
|
|
||||||
<p>
|
|
||||||
<a class="btn btn-primary btn-lg"
|
|
||||||
href="/page/contactus">Contact us</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</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.id}/unsubscribe"
|
|
||||||
method="post">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 text-center mt16 mb32">
|
|
||||||
<h2>
|
|
||||||
Hello,
|
|
||||||
<t t-esc="contact_name"/>
|
|
||||||
</h2>
|
|
||||||
<h3 class="text-muted">
|
|
||||||
You are trying to unsubscribe from all massive mailings
|
|
||||||
<t t-if="origin_name">
|
|
||||||
sent to followers of
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<i><span>"</span><t t-esc="origin_name"/><span>"</span></i>
|
|
||||||
</t>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div t-if="additional_contact_ids"
|
|
||||||
class="col-md-12 mt16">
|
|
||||||
Is there any other mailing list you want to leave?
|
|
||||||
<t t-foreach="additional_contact_ids"
|
|
||||||
t-as="contact">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
t-attf-name="list_id,#{contact.list_id.id}"
|
|
||||||
type="checkbox"
|
|
||||||
t-att-value="contact.id"/>
|
|
||||||
<t t-esc="contact.list_id.display_name"/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 mt16">
|
|
||||||
But before continuing, could you please tell us why do you want to unsubscribe?
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 mb16">
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="db"
|
|
||||||
t-att-value="env.cr.dbname"/>
|
|
||||||
<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="reason_ids" 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"/>
|
|
||||||
<t t-esc="reason.display_name"/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
<div t-attf-class="form-group #{error_details_required and 'has-error' or ''}">
|
|
||||||
<textarea
|
|
||||||
name="details"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="Anything else you want to say before you leave?"
|
|
||||||
rows="3"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb16 mt16">
|
|
||||||
<button type="submit" class="btn btn-danger">
|
|
||||||
Unsubscribe now
|
|
||||||
</button>
|
|
||||||
<p class="help-block">Thank you!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</data>
|
|
||||||
</openerp>
|
|
Loading…
Reference in New Issue