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/129/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/__openerp__.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 -*- |
# -*- 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_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