Browse Source
[8.0][IMP][mass_mailing_custom_unsubscribe] Get reasons for unsubscription (#58)
[8.0][IMP][mass_mailing_custom_unsubscribe] Get reasons for unsubscription (#58)
* [8.0][IMP][mass_mailing_custom_unsubscribe] Get reasons for unsubscription.pull/178/head
Yajo
8 years ago
committed by
Jairo Llopis
29 changed files with 1411 additions and 60 deletions
-
54mass_mailing_custom_unsubscribe/README.rst
-
15mass_mailing_custom_unsubscribe/__manifest__.py
-
15mass_mailing_custom_unsubscribe/controllers.py
-
5mass_mailing_custom_unsubscribe/controllers/__init__.py
-
237mass_mailing_custom_unsubscribe/controllers/main.py
-
11mass_mailing_custom_unsubscribe/data/install_salt.xml
-
5mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv
-
9mass_mailing_custom_unsubscribe/exceptions.py
-
372mass_mailing_custom_unsubscribe/i18n/es.po
-
BINmass_mailing_custom_unsubscribe/images/failure.png
-
BINmass_mailing_custom_unsubscribe/images/form.png
-
BINmass_mailing_custom_unsubscribe/images/success.png
-
27mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py
-
8mass_mailing_custom_unsubscribe/models/__init__.py
-
35mass_mailing_custom_unsubscribe/models/mail_mail.py
-
35mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
-
15mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py
-
74mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
-
6mass_mailing_custom_unsubscribe/security/ir.model.access.csv
-
13mass_mailing_custom_unsubscribe/static/src/js/require_details.js
-
7mass_mailing_custom_unsubscribe/tests/__init__.py
-
111mass_mailing_custom_unsubscribe/tests/test_controller.py
-
97mass_mailing_custom_unsubscribe/tests/test_mail_mail.py
-
21mass_mailing_custom_unsubscribe/tests/test_unsubscription.py
-
17mass_mailing_custom_unsubscribe/views/assets.xml
-
21mass_mailing_custom_unsubscribe/views/mail_mass_mailing_list_view.xml
-
60mass_mailing_custom_unsubscribe/views/mail_unsubscription_reason_view.xml
-
89mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
-
106mass_mailing_custom_unsubscribe/views/pages.xml
@ -1,15 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com) |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from openerp import http |
|||
from openerp.addons.mass_mailing.controllers.main import MassMailController |
|||
|
|||
|
|||
class CustomUnsuscribe(MassMailController): |
|||
@http.route() |
|||
def mailing(self, *args, **kwargs): |
|||
path = "/page/mass_mail_unsubscription_%s" |
|||
result = super(CustomUnsuscribe, self).mailing(*args, **kwargs) |
|||
return http.local_redirect( |
|||
path % ("success" if result.data == "OK" else "failure")) |
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from . import main |
@ -0,0 +1,237 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com) |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from openerp import exceptions |
|||
from openerp.http import local_redirect, request, route |
|||
from openerp.addons.mass_mailing.controllers.main import MassMailController |
|||
from .. import exceptions as _ex |
|||
|
|||
|
|||
class CustomUnsubscribe(MassMailController): |
|||
def _mailing_list_contacts_by_email(self, email): |
|||
"""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. |
|||
|
|||
:param mail.mass_mailing mailing_id: |
|||
Mailing where the unsubscription is being processed. |
|||
|
|||
:param str email: |
|||
Email to be unsubscribed. |
|||
|
|||
:param int res_id: |
|||
ID of the unsubscriber. |
|||
|
|||
:param dict qcontext_extra: |
|||
Additional dictionary to pass to the view. |
|||
""" |
|||
values = self.unsubscription_qcontext(mailing_id, email, res_id, token) |
|||
values.update(qcontext_extra or dict()) |
|||
return request.website.render( |
|||
"mass_mailing_custom_unsubscribe.reason_form", |
|||
values) |
|||
|
|||
def unsubscription_qcontext(self, mailing_id, email, res_id, token): |
|||
"""Get rendering context for unsubscription form. |
|||
|
|||
:param mail.mass_mailing mailing_id: |
|||
Mailing where the unsubscription is being processed. |
|||
|
|||
:param str email: |
|||
Email to be unsubscribed. |
|||
|
|||
:param int res_id: |
|||
ID of the unsubscriber. |
|||
""" |
|||
email_fname = origin_name = None |
|||
domain = [("id", "=", res_id)] |
|||
record_ids = request.env[mailing_id.mailing_model].sudo() |
|||
|
|||
if "email_from" in record_ids._fields: |
|||
email_fname = "email_from" |
|||
elif "email" in record_ids._fields: |
|||
email_fname = "email" |
|||
|
|||
if not (email_fname and email): |
|||
# Trying to unsubscribe without email? Bad boy... |
|||
raise exceptions.AccessDenied() |
|||
|
|||
domain.append((email_fname, "ilike", email)) |
|||
|
|||
# Search additional mailing lists for the unsubscriber |
|||
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: |
|||
first = first[fnames["related"]] |
|||
except KeyError: |
|||
pass |
|||
try: |
|||
origin_name = first[fnames["origin"]] |
|||
except KeyError: |
|||
pass |
|||
|
|||
# Get available reasons |
|||
reason_ids = ( |
|||
request.env["mail.unsubscription.reason"].search([])) |
|||
|
|||
return { |
|||
"additional_contact_ids": additional_contacts, |
|||
"contact_name": contact_name, |
|||
"email": email, |
|||
"mailing_id": mailing_id, |
|||
"origin_model_name": origin_model_name, |
|||
"origin_name": origin_name, |
|||
"reason_ids": reason_ids, |
|||
"record_ids": record_ids, |
|||
"res_id": res_id, |
|||
"token": token, |
|||
} |
|||
|
|||
def unsubscription_special_fnames(self, model): |
|||
"""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")) |
@ -0,0 +1,11 @@ |
|||
<?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> |
@ -0,0 +1,5 @@ |
|||
"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,9 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from openerp import exceptions |
|||
|
|||
|
|||
class DetailsRequiredError(exceptions.ValidationError): |
|||
pass |
After Width: 696 | Height: 411 | Size: 41 KiB |
After Width: 1004 | Height: 506 | Size: 36 KiB |
After Width: 676 | Height: 376 | Size: 35 KiB |
@ -0,0 +1,27 @@ |
|||
# -*- 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,7 +1,7 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Python source code encoding : https://www.python.org/dev/peps/pep-0263/ |
|||
############################################################################## |
|||
# For copyright and license notices, see __openerp__.py file in root directory |
|||
############################################################################## |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from . import mail_mail |
|||
from . import mail_mass_mailing |
|||
from . import mail_mass_mailing_list |
|||
from . import mail_unsubscription |
@ -0,0 +1,35 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from hashlib import sha256 |
|||
from uuid import uuid4 |
|||
from openerp import api, models |
|||
|
|||
|
|||
class MailMassMailing(models.Model): |
|||
_inherit = "mail.mass_mailing" |
|||
|
|||
@api.model |
|||
def _init_salt_create(self): |
|||
"""Create a salt to secure the unsubscription URLs.""" |
|||
icp = self.env["ir.config_parameter"] |
|||
key = "mass_mailing.salt" |
|||
salt = icp.get_param(key) |
|||
if salt is False: |
|||
salt = str(uuid4()) |
|||
icp.set_param(key, salt, ["base.group_erp_manager"]) |
|||
|
|||
@api.model |
|||
def hash_create(self, mailing_id, res_id, email): |
|||
"""Create a secure hash to know if the unsubscription is trusted. |
|||
|
|||
:return None/str: |
|||
Secure hash, or ``None`` if the system parameter is empty. |
|||
""" |
|||
salt = self.env["ir.config_parameter"].sudo().get_param( |
|||
"mass_mailing.salt") |
|||
if not salt: |
|||
return None |
|||
source = (self.env.cr.dbname, mailing_id, res_id, email, salt) |
|||
return sha256(",".join(map(unicode, source))).hexdigest() |
@ -0,0 +1,15 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from openerp 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?'") |
@ -0,0 +1,74 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from openerp import _, api, fields, models |
|||
from .. import exceptions |
|||
|
|||
|
|||
class MailUnsubscription(models.Model): |
|||
_name = "mail.unsubscription" |
|||
_inherit = "mail.thread" |
|||
_rec_name = "date" |
|||
|
|||
date = fields.Datetime( |
|||
default=lambda self: self._default_date(), |
|||
required=True) |
|||
email = fields.Char( |
|||
required=True) |
|||
mass_mailing_id = fields.Many2one( |
|||
"mail.mass_mailing", |
|||
"Mass mailing", |
|||
required=True, |
|||
help="Mass mailing from which he was unsubscribed.") |
|||
unsubscriber_id = fields.Reference( |
|||
lambda self: self._selection_unsubscriber_id(), |
|||
"Unsubscriber", |
|||
required=True, |
|||
help="Who was unsubscribed.") |
|||
reason_id = fields.Many2one( |
|||
"mail.unsubscription.reason", |
|||
"Reason", |
|||
ondelete="restrict", |
|||
required=True, |
|||
help="Why the unsubscription was made.") |
|||
details = fields.Char( |
|||
help="More details on why the unsubscription was made.") |
|||
details_required = fields.Boolean( |
|||
related="reason_id.details_required") |
|||
success = fields.Boolean( |
|||
help="If this is unchecked, it indicates some failure happened in the " |
|||
"unsubscription process.") |
|||
|
|||
@api.model |
|||
def _default_date(self): |
|||
return fields.Datetime.now() |
|||
|
|||
@api.model |
|||
def _selection_unsubscriber_id(self): |
|||
"""Models that can be linked to a ``mail.mass_mailing``.""" |
|||
return self.env["mail.mass_mailing"]._get_mailing_model() |
|||
|
|||
@api.multi |
|||
@api.constrains("details", "reason_id") |
|||
def _check_details_needed(self): |
|||
"""Ensure details are given if required.""" |
|||
for s in self: |
|||
if not s.details and s.details_required: |
|||
raise exceptions.DetailsRequiredError( |
|||
_("This reason requires an explanation.")) |
|||
|
|||
|
|||
class MailUnsubscriptionReason(models.Model): |
|||
_name = "mail.unsubscription.reason" |
|||
_order = "sequence, name" |
|||
|
|||
name = fields.Char( |
|||
index=True, |
|||
translate=True, |
|||
required=True) |
|||
details_required = fields.Boolean( |
|||
help="Check to ask for more details when this reason is selected.") |
|||
sequence = fields.Integer( |
|||
index=True, |
|||
help="Position of the reason in the list.") |
@ -0,0 +1,6 @@ |
|||
"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_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 |
|||
"read_unsubscription","Marketing users can read unsubscriptions","model_mail_unsubscription","marketing.group_marketing_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 |
@ -0,0 +1,13 @@ |
|||
/* © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
|
|||
|
|||
"use strict"; |
|||
(function ($) { |
|||
$("#reason_form :radio").change(function(event) { |
|||
$("textarea[name=details]").attr( |
|||
"required", |
|||
$(event.target).is("[data-details-required]") |
|||
); |
|||
}); |
|||
$("#reason_form :radio:checked").change(); |
|||
})(jQuery); |
@ -0,0 +1,7 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from . import test_unsubscription |
|||
from . import test_mail_mail |
|||
from . import test_controller |
@ -0,0 +1,111 @@ |
|||
# -*- 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 |
|||
) |
@ -0,0 +1,97 @@ |
|||
# -*- 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,21 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from openerp.tests.common import TransactionCase |
|||
from .. import exceptions |
|||
|
|||
|
|||
class UnsubscriptionCase(TransactionCase): |
|||
def test_details_required(self): |
|||
"""Cannot create unsubscription without details when required.""" |
|||
with self.assertRaises(exceptions.DetailsRequiredError): |
|||
self.env["mail.unsubscription"].create({ |
|||
"email": "axelor@yourcompany.example.com", |
|||
"mass_mailing_id": self.env.ref("mass_mailing.mass_mail_1").id, |
|||
"unsubscriber_id": |
|||
"res.partner,%d" % self.env.ref("base.res_partner_13").id, |
|||
"reason_id": |
|||
self.env.ref( |
|||
"mass_mailing_custom_unsubscribe.reason_other").id, |
|||
}) |
@ -0,0 +1,17 @@ |
|||
<?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> |
|||
|
|||
<template id="assets_frontend" |
|||
inherit_id="website.assets_frontend"> |
|||
<xpath expr="."> |
|||
<script type="text/javascript" |
|||
src="/mass_mailing_custom_unsubscribe/static/src/js/require_details.js"/> |
|||
</xpath> |
|||
</template> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,21 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- © 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<openerp> |
|||
<data> |
|||
|
|||
<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> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,60 @@ |
|||
<?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> |
|||
|
|||
<record id="mail_unsubscription_reason_view_form" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Reason Form</field> |
|||
<field name="model">mail.unsubscription.reason</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<sheet> |
|||
<group> |
|||
<field name="name"/> |
|||
<field name="details_required"/> |
|||
<field name="sequence"/> |
|||
</group> |
|||
<div class="oe_chatter"/> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="mail_unsubscription_reason_view_tree" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Reason Tree</field> |
|||
<field name="model">mail.unsubscription.reason</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="name"/> |
|||
<field name="details_required"/> |
|||
<field name="sequence" invisible="True"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="mail_unsubscription_reason_view_search" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Reason Search</field> |
|||
<field name="model">mail.unsubscription.reason</field> |
|||
<field name="arch" type="xml"> |
|||
<search> |
|||
<field name="name"/> |
|||
<field name="details_required"/> |
|||
</search> |
|||
</field> |
|||
</record> |
|||
|
|||
<act_window |
|||
id="mail_unsubscription_reason_action" |
|||
name="Unsubscription Reasons" |
|||
res_model="mail.unsubscription.reason"/> |
|||
|
|||
<menuitem |
|||
id="mail_unsubscription_reason_menu" |
|||
parent="mass_mailing.marketing_configuration" |
|||
groups="mass_mailing.group_mass_mailing_campaign" |
|||
action="mail_unsubscription_reason_action"/> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,89 @@ |
|||
<?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> |
|||
|
|||
<record id="mail_unsubscription_view_form" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Form</field> |
|||
<field name="model">mail.unsubscription</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<sheet> |
|||
<group> |
|||
<field name="date"/> |
|||
<field name="mass_mailing_id"/> |
|||
<field name="unsubscriber_id"/> |
|||
<field name="email"/> |
|||
<field name="success"/> |
|||
<field name="reason_id"/> |
|||
<field name="details" |
|||
attrs="{'required': [('details_required', '=', True)]}"/> |
|||
<field name="details_required" invisible="True"/> |
|||
</group> |
|||
</sheet> |
|||
<div class="oe_chatter"> |
|||
<field name="message_follower_ids" |
|||
widget="mail_followers" |
|||
groups="base.group_user"/> |
|||
<field name="message_ids" |
|||
widget="mail_thread"/> |
|||
</div> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="mail_unsubscription_view_tree" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Tree</field> |
|||
<field name="model">mail.unsubscription</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="date"/> |
|||
<field name="mass_mailing_id"/> |
|||
<field name="unsubscriber_id"/> |
|||
<field name="email" invisible="True"/> |
|||
<field name="reason_id"/> |
|||
<field name="details" invisible="True"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="mail_unsubscription_view_search" model="ir.ui.view"> |
|||
<field name="name">Mail Unsubscription Search</field> |
|||
<field name="model">mail.unsubscription</field> |
|||
<field name="arch" type="xml"> |
|||
<search> |
|||
<field name="mass_mailing_id"/> |
|||
<field name="unsubscriber_id"/> |
|||
<field name="email"/> |
|||
<field name="success"/> |
|||
<field name="reason_id"/> |
|||
<field name="details"/> |
|||
<separator/> |
|||
<group string="Group by"> |
|||
<filter string="Month" |
|||
context="{'group_by': 'date:month'}"/> |
|||
<filter string="Year" |
|||
context="{'group_by': 'date:year'}"/> |
|||
<filter string="Reason" |
|||
context="{'group_by': 'reason_id'}"/> |
|||
<filter string="Mass mailing" |
|||
context="{'group_by': 'mass_mailing_id'}"/> |
|||
<filter string="Success" |
|||
context="{'group_by': 'success'}"/> |
|||
</group> |
|||
</search> |
|||
</field> |
|||
</record> |
|||
|
|||
<act_window id="mail_unsubscription_action" |
|||
name="Unsubscriptions" |
|||
res_model="mail.unsubscription"/> |
|||
|
|||
<menuitem id="mail_unsubscription_menu" |
|||
parent="mass_mailing.mass_mailing_campaign" |
|||
action="mail_unsubscription_action"/> |
|||
|
|||
</data> |
|||
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue