Browse Source
[9.0][MIG][mass_mailing_custom_unsubscribe] Migrate.
[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/178/head
Jairo Llopis
8 years ago
35 changed files with 788 additions and 853 deletions
-
99mass_mailing_custom_unsubscribe/README.rst
-
6mass_mailing_custom_unsubscribe/__init__.py
-
44mass_mailing_custom_unsubscribe/__manifest__.py
-
2mass_mailing_custom_unsubscribe/controllers/__init__.py
-
284mass_mailing_custom_unsubscribe/controllers/main.py
-
11mass_mailing_custom_unsubscribe/data/install_salt.xml
-
5mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv
-
41mass_mailing_custom_unsubscribe/data/mail_unsubscription_reason.xml
-
17mass_mailing_custom_unsubscribe/demo/assets.xml
-
2mass_mailing_custom_unsubscribe/exceptions.py
-
BINmass_mailing_custom_unsubscribe/images/failure.png
-
BINmass_mailing_custom_unsubscribe/images/success.png
-
27mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py
-
2mass_mailing_custom_unsubscribe/models/__init__.py
-
49mass_mailing_custom_unsubscribe/models/mail_mail.py
-
65mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
-
2mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py
-
11mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
-
12mass_mailing_custom_unsubscribe/security/ir.model.access.csv
-
77mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js
-
49mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js
-
30mass_mailing_custom_unsubscribe/static/src/js/require_details.js
-
111mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
-
82mass_mailing_custom_unsubscribe/templates/general_reason_form.xml
-
26mass_mailing_custom_unsubscribe/templates/mass_mailing_contact_reason.xml
-
5mass_mailing_custom_unsubscribe/tests/__init__.py
-
111mass_mailing_custom_unsubscribe/tests/test_controller.py
-
97mass_mailing_custom_unsubscribe/tests/test_mail_mail.py
-
150mass_mailing_custom_unsubscribe/tests/test_ui.py
-
4mass_mailing_custom_unsubscribe/tests/test_unsubscription.py
-
10mass_mailing_custom_unsubscribe/views/assets.xml
-
8mass_mailing_custom_unsubscribe/views/mail_mass_mailing_list_view.xml
-
11mass_mailing_custom_unsubscribe/views/mail_unsubscription_reason_view.xml
-
14mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
-
155mass_mailing_custom_unsubscribe/views/pages.xml
@ -1,7 +1,5 @@ |
|||||
# -*- 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 |
|
||||
############################################################################## |
|
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
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/ |
|
||||
############################################################################## |
|
||||
# |
|
||||
# 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
{ |
{ |
||||
'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_crm', |
|
||||
|
'website_mass_mailing', |
||||
], |
], |
||||
'data': [ |
'data': [ |
||||
'security/ir.model.access.csv', |
'security/ir.model.access.csv', |
||||
'data/install_salt.xml', |
|
||||
'data/mail.unsubscription.reason.csv', |
|
||||
|
'data/mail_unsubscription_reason.xml', |
||||
|
'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,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> |
Before Width: 696 | Height: 411 | Size: 41 KiB |
Before Width: 676 | Height: 376 | 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,52 +1,15 @@ |
|||||
# -*- 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 |
|
||||
############################################################################## |
|
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
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): |
|
||||
m_config = self.env['ir.config_parameter'] |
|
||||
base_url = m_config.get_param('web.base.url') |
|
||||
config_msg = m_config.get_param('mass_mailing.unsubscribe.label') |
|
||||
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 |
|
||||
|
def _get_unsubscribe_url(self, mail, email_to): |
||||
|
result = super(MailMail, self)._get_unsubscribe_url(mail, email_to) |
||||
|
token = mail.mailing_id._unsubscribe_token(mail.res_id) |
||||
|
return "%s&token=%s" % (result, token) |
@ -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 |
|
||||
from uuid import uuid4 |
|
||||
|
import hmac |
||||
|
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 |
|
||||
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.multi |
||||
|
def _unsubscribe_token(self, res_id, compare=None): |
||||
|
"""Generate a secure hash for this mailing list and parameters. |
||||
|
This is appended to the unsubscription URL and then checked at |
||||
|
unsubscription time to ensure no malicious unsubscriptions are |
||||
|
performed. |
||||
|
|
||||
@api.model |
|
||||
def hash_create(self, mailing_id, res_id, email): |
|
||||
"""Create a secure hash to know if the unsubscription is trusted. |
|
||||
|
:param int res_id: |
||||
|
ID of the resource that will be unsubscribed. |
||||
|
|
||||
|
:param str compare: |
||||
|
Received token to be compared with the good one. |
||||
|
|
||||
:return None/str: |
|
||||
Secure hash, or ``None`` if the system parameter is empty. |
|
||||
|
: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( |
|
||||
"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() |
|
||||
|
secret = self.env["ir.config_parameter"].sudo().get_param( |
||||
|
"database.secret") |
||||
|
key = (self.env.cr.dbname, self.id, int(res_id)) |
||||
|
token = hmac.new(str(secret), repr(key), hashlib.sha512).hexdigest() |
||||
|
if compare is not None and not consteq(token, str(compare)): |
||||
|
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,6 +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 |
|
||||
|
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_user,1,1,1,1 |
||||
|
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_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"; |
|
||||
(function ($) { |
|
||||
$("#reason_form :radio").change(function(event) { |
|
||||
$("textarea[name=details]").attr( |
|
||||
|
return animation.registry.mass_mailing_custom_unsubscribe_require_details = |
||||
|
animation.Class.extend({ |
||||
|
selector: ".js_unsubscription_reason", |
||||
|
|
||||
|
start: function () { |
||||
|
this.$radio = this.$(":radio"); |
||||
|
this.$details = this.$("[name=details]"); |
||||
|
this.$radio.on("change click", $.proxy(this.toggle, this)); |
||||
|
this.$radio.filter(":checked").trigger("change"); |
||||
|
}, |
||||
|
|
||||
|
toggle: function (event) { |
||||
|
this.$details.prop( |
||||
"required", |
"required", |
||||
$(event.target).is("[data-details-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_controller |
|
||||
|
from . import test_ui |
@ -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,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> |
|
||||
<data> |
|
||||
|
<odoo> |
||||
|
|
||||
<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> |
|
||||
</openerp> |
|
||||
|
</odoo> |
@ -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> |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue