Browse Source

[MIG][mass_mailing_custom_unsubscribe] Migrate to v10

pull/279/head
Jairo Llopis 7 years ago
committed by David
parent
commit
7e3310d69e
  1. 10
      mass_mailing_custom_unsubscribe/README.rst
  2. 5
      mass_mailing_custom_unsubscribe/__manifest__.py
  3. 19
      mass_mailing_custom_unsubscribe/controllers/main.py
  4. 9
      mass_mailing_custom_unsubscribe/models/mail_mail.py
  5. 11
      mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
  6. 2
      mass_mailing_custom_unsubscribe/security/ir.model.access.csv
  7. 77
      mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js
  8. 49
      mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js
  9. 24
      mass_mailing_custom_unsubscribe/tests/test_ui.py
  10. 2
      mass_mailing_custom_unsubscribe/views/mail_unsubscription_reason_view.xml

10
mass_mailing_custom_unsubscribe/README.rst

@ -10,7 +10,7 @@ This addon extends the unsubscription form to let you:
- Choose which mailing lists are not cross-unsubscriptable when unsubscribing - Choose which mailing lists are not cross-unsubscriptable when unsubscribing
from a different one. from a different one.
- Know why and when a contact as been unsubscribed from a mass mailing.
- Know why and when a contact has been unsubscribed from a mass mailing.
Configuration Configuration
============= =============
@ -21,7 +21,7 @@ Unsubscription Reasons
You can customize what reasons will be displayed to your unsubscriptors when You can customize what reasons will be displayed to your unsubscriptors when
they are going to unsubscribe. To do it: they are going to unsubscribe. To do it:
#. Go to *Marketing > Configuration > Unsubscription Reasons*.
#. Go to *Mass Mailing > Configuration > Unsubscription Reasons*.
#. Create / edit / remove / sort as usual. #. Create / edit / remove / sort as usual.
#. If *Details required* is enabled, they will have to fill a text area to #. If *Details required* is enabled, they will have to fill a text area to
continue. continue.
@ -40,7 +40,7 @@ Once configured:
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot :alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/9.0
:target: https://runbot.odoo-community.org/runbot/205/10.0
Known issues / Roadmap Known issues / Roadmap
====================== ======================
@ -48,7 +48,9 @@ Known issues / Roadmap
* This module adds a security hash for mass mailing unsubscription URLs, which * This module adds a security hash for mass mailing unsubscription URLs, which
disables insecure URLs from mass mailing messages sent before its disables insecure URLs from mass mailing messages sent before its
installation. This can be a problem, but anyway you'd get that problem in installation. This can be a problem, but anyway you'd get that problem in
Odoo 11.0, so at least this addon will be forward-compatible with it.
Odoo 11.0, where https://github.com/odoo/odoo/pull/12040 was merged, so at
least this addon will be forward-compatible with it. So, **this feature must
be removed from here when migrating to v11**.
* This module replaces AJAX submission core implementation from the mailing * This module replaces AJAX submission core implementation from the mailing
list management form, because it is impossible to extend it. When list management form, because it is impossible to extend it. When
https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most

5
mass_mailing_custom_unsubscribe/__manifest__.py

@ -5,7 +5,7 @@
'name': "Customizable unsubscription process on mass mailing emails", 'name': "Customizable unsubscription process on mass mailing emails",
"summary": "Know unsubscription reasons, track them", "summary": "Know unsubscription reasons, track them",
'category': 'Marketing', 'category': 'Marketing',
'version': '9.0.2.0.0',
'version': '10.0.1.0.0',
'depends': [ 'depends': [
'website_mass_mailing', 'website_mass_mailing',
], ],
@ -25,8 +25,7 @@
'images': [ 'images': [
'images/form.png', 'images/form.png',
], ],
'author': 'Antiun Ingeniería S.L., '
'Tecnativa,'
'author': 'Tecnativa,'
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',
'website': 'https://www.tecnativa.com', 'website': 'https://www.tecnativa.com',
'license': 'AGPL-3', 'license': 'AGPL-3',

19
mass_mailing_custom_unsubscribe/controllers/main.py

@ -29,7 +29,7 @@ class CustomUnsubscribe(MassMailController):
Security token for unsubscriptions. Security token for unsubscriptions.
""" """
reasons = request.env["mail.unsubscription.reason"].search([]) reasons = request.env["mail.unsubscription.reason"].search([])
return request.website.render(
return request.render(
"mass_mailing_custom_unsubscribe.reason_form", "mass_mailing_custom_unsubscribe.reason_form",
{ {
"email": email, "email": email,
@ -75,11 +75,11 @@ class CustomUnsubscribe(MassMailController):
return self.reason_form(mailing, email, res_id, token) return self.reason_form(mailing, email, res_id, token)
else: else:
# Unsubscribe, saving reason and details by context # Unsubscribe, saving reason and details by context
request.context.update({
"default_reason_id": reason_id,
"default_details": post.get("details") or False,
})
del request.env
request.context = dict(
request.context,
default_reason_id=reason_id,
default_details=post.get("details") or False,
)
# You could get a DetailsRequiredError here, but only if HTML5 # You could get a DetailsRequiredError here, but only if HTML5
# validation fails, which should not happen in modern browsers # validation fails, which should not happen in modern browsers
return super(CustomUnsubscribe, self).mailing( return super(CustomUnsubscribe, self).mailing(
@ -91,8 +91,11 @@ class CustomUnsubscribe(MassMailController):
"""Store unsubscription reasons when unsubscribing from RPC.""" """Store unsubscription reasons when unsubscribing from RPC."""
# Update request context and reset environment # Update request context and reset environment
if reason_id: if reason_id:
request.context["default_reason_id"] = int(reason_id)
request.context["default_details"] = details or False
request.context = dict(
request.context,
default_reason_id=int(reason_id),
default_details=details or False,
)
# FIXME Remove token check in version where this is merged: # FIXME Remove token check in version where this is merged:
# https://github.com/odoo/odoo/pull/14385 # https://github.com/odoo/odoo/pull/14385
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)

9
mass_mailing_custom_unsubscribe/models/mail_mail.py

@ -2,14 +2,13 @@
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> # Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, models
from openerp import models
class MailMail(models.Model): class MailMail(models.Model):
_inherit = 'mail.mail' _inherit = 'mail.mail'
@api.model
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)
def _get_unsubscribe_url(self, email_to):
result = super(MailMail, self)._get_unsubscribe_url(email_to)
token = self.mailing_id._unsubscribe_token(self.res_id)
return "%s&token=%s" % (result, token) return "%s&token=%s" % (result, token)

11
mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py

@ -37,18 +37,17 @@ class MailMassMailing(models.Model):
raise AccessDenied() raise AccessDenied()
return token return token
@api.model
def update_opt_out(self, mailing_id, email, res_ids, value):
def update_opt_out(self, email, res_ids, value):
"""Save unsubscription reason when opting out from mailing.""" """Save unsubscription reason when opting out from mailing."""
mailing = self.browse(mailing_id)
self.ensure_one()
if value and self.env.context.get("default_reason_id"): if value and self.env.context.get("default_reason_id"):
for res_id in res_ids: for res_id in res_ids:
# reason_id and details are expected from the context # reason_id and details are expected from the context
self.env["mail.unsubscription"].create({ self.env["mail.unsubscription"].create({
"email": email, "email": email,
"mass_mailing_id": mailing.id,
"mass_mailing_id": self.id,
"unsubscriber_id": "%s,%d" % ( "unsubscriber_id": "%s,%d" % (
mailing.mailing_model, int(res_id)),
self.mailing_model, int(res_id)),
}) })
return super(MailMassMailing, self).update_opt_out( return super(MailMassMailing, self).update_opt_out(
mailing_id, email, res_ids, value)
email, res_ids, value)

2
mass_mailing_custom_unsubscribe/security/ir.model.access.csv

@ -3,4 +3,4 @@ read_unsubscription_reason_public,Public users can read unsubscription reasons,m
read_unsubscription_reason_employee,Employee users can read unsubscription reasons,model_mail_unsubscription_reason,base.group_user,1,0,0,0 read_unsubscription_reason_employee,Employee users can read unsubscription reasons,model_mail_unsubscription_reason,base.group_user,1,0,0,0
write_unsubscription_reason,Mass mailing managers can manage unsubscription reasons,model_mail_unsubscription_reason,mass_mailing.group_mass_mailing_user,1,1,1,1 write_unsubscription_reason,Mass mailing managers can manage unsubscription reasons,model_mail_unsubscription_reason,mass_mailing.group_mass_mailing_user,1,1,1,1
read_unsubscription,Marketing users can read unsubscriptions,model_mail_unsubscription,mass_mailing.group_mass_mailing_user,1,0,0,0 read_unsubscription,Marketing users can read unsubscriptions,model_mail_unsubscription,mass_mailing.group_mass_mailing_user,1,0,0,0
write_unsubscription,Mass mailing managers can manage unsubscriptions,model_mail_unsubscription,mass_mailing.group_mass_mailing_user,1,1,1,1
write_unsubscription,Admins can change unsubscriptions,model_mail_unsubscription,base.group_system,1,1,1,1

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

@ -3,75 +3,72 @@
odoo.define("mass_mailing_custom_unsubscribe.contact_tour", odoo.define("mass_mailing_custom_unsubscribe.contact_tour",
function (require) { function (require) {
"use strict"; "use strict";
var Tour = require("web.Tour");
var base = require("web_editor.base");
var tour = require("web_tour.tour");
require("mass_mailing_custom_unsubscribe.require_details"); require("mass_mailing_custom_unsubscribe.require_details");
require("mass_mailing_custom_unsubscribe.unsubscribe"); require("mass_mailing_custom_unsubscribe.unsubscribe");
// Allow to know if an element is required // Allow to know if an element is required
$.extend($.expr[':'], { $.extend($.expr[':'], {
propRequired: function(element, index, matches) {
propRequired: function(element, index, matches) {
return $(element).prop("required"); return $(element).prop("required");
},
},
}); });
Tour.register({
id: "mass_mailing_custom_unsubscribe_tour_contact",
name: "Mass mailing contact unsubscribes",
mode: "test",
steps: [
tour.register(
"mass_mailing_custom_unsubscribe_tour_contact",
{
tour: true,
wait_for: base.ready(),
},
[
{ {
title: "Unsubscription reasons are invisible",
waitFor: "#unsubscribe_form .js_unsubscription_reason:hidden",
content: "Unsubscription reasons are invisible",
trigger: "#unsubscribe_form:has(.js_unsubscription_reason:hidden)",
}, },
{ {
title: "Uncheck list 0",
element: "li:contains('test list 0') input",
waitFor: "li:contains('test list 0') input:checked",
content: "Uncheck list 0",
trigger: "li:contains('test list 0') input",
// List 2 is not cross unsubscriptable // List 2 is not cross unsubscriptable
waitNot: "li:contains('test list 2')",
extra_trigger: "body:not(:has(li:contains('test list 2'))) li:contains('test list 0') input:checked",
}, },
{ {
title: "Uncheck list 1",
element: "li:contains('test list 1') input:checked",
waitFor: ".js_unsubscription_reason:visible",
content: "Uncheck list 1",
trigger: "li:contains('test list 1') input:checked",
extra_trigger: ".js_unsubscription_reason:visible",
}, },
{ {
title: "Choose other reason",
element: ".radio:contains('Other reason') :radio",
waitFor: ".radio:contains('Other reason') " +
":radio:not(:checked)",
content: "Choose other reason",
trigger: ".radio:contains('Other reason') :radio",
extra_trigger: ".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",
content: "Add details to reason",
trigger: "[name='details']:visible:propRequired",
run: "text I want to unsubscribe because I want. Period.",
extra_trigger: ".radio:contains('Other reason') :radio:checked",
}, },
{ {
title: "Update subscriptions 1st time",
element: "#unsubscribe_form :submit",
content: "Update subscriptions 1st time",
trigger: "#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 () {
content: "Subscribe again to list 0",
trigger: "body:not(:has(#unsubscribe_form .js_unsubscription_reason:visible)):has(.alert-success, li:contains('test list 0') input:not(:checked))",
run: function () {
// This one will get the success again after next step // This one will get the success again after next step
$(".alert-success").removeClass("alert-success"); $(".alert-success").removeClass("alert-success");
}, },
}, },
{ {
title: "Update subscriptions 2nd time",
element: "#unsubscribe_form :submit",
waitNot: "#unsubscribe_form .js_unsubscription_reason:visible",
content: "Update subscriptions 2nd time",
trigger: "#unsubscribe_form:not(:has(.js_unsubscription_reason:visible)) :submit",
}, },
{ {
title: "Resuscription was OK",
waitFor: ".alert-success",
content: "Resuscription was OK",
trigger: ".alert-success",
} }
] ]
});
return Tour.tours.mass_mailing_custom_unsubscribe_tour_contact;
);
}); });

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

@ -3,47 +3,44 @@
odoo.define("mass_mailing_custom_unsubscribe.partner_tour", odoo.define("mass_mailing_custom_unsubscribe.partner_tour",
function (require) { function (require) {
"use strict"; "use strict";
var Tour = require("web.Tour");
var base = require("web_editor.base");
var tour = require("web_tour.tour");
require("mass_mailing_custom_unsubscribe.require_details"); require("mass_mailing_custom_unsubscribe.require_details");
require("mass_mailing_custom_unsubscribe.unsubscribe"); require("mass_mailing_custom_unsubscribe.unsubscribe");
// Allow to know if an element is required // Allow to know if an element is required
$.extend($.expr[':'], { $.extend($.expr[':'], {
propRequired: function(element, index, matches) {
propRequired: function(element, index, matches) {
return $(element).prop("required"); return $(element).prop("required");
},
},
}); });
Tour.register({
id: "mass_mailing_custom_unsubscribe_tour_partner",
name: "Mass mailing partner unsubscribes",
mode: "test",
steps: [
tour.register(
"mass_mailing_custom_unsubscribe_tour_partner",
{
tour: true,
wait_for: base.ready(),
},
[
{ {
title: "Choose other reason",
element: ".radio:contains('Other reason') " +
":radio:not(:checked)",
waitFor: "#reason_form .js_unsubscription_reason",
content: "Choose other reason",
trigger: ".radio:contains('Other reason') :radio:not(:checked)",
extra_trigger: "#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",
content: "Switch to not interested reason",
trigger: ".radio:contains(\"I'm not interested\") :radio:not(:checked)",
extra_trigger: "[name='details']:propRequired",
}, },
{ {
title: "Unsubscribe",
element: "#reason_form :submit",
waitNot: "[name='details']:propRequired",
content: "Unsubscribe",
trigger: "#reason_form :submit",
extra_trigger: "body:not(:has([name='details']:propRequired))",
}, },
{ {
title: "Successfully unsubscribed",
waitFor: ".alert-success:contains(" +
"'Your changes have been saved.')",
waitNot: "#reason_form",
content: "Successfully unsubscribed",
trigger: "body:not(:has(#reason_form)) .alert-success:contains('Your changes have been saved.')",
}, },
] ]
});
return Tour.tours.mass_mailing_custom_unsubscribe_tour_partner;
);
}); });

24
mass_mailing_custom_unsubscribe/tests/test_ui.py

@ -7,8 +7,11 @@ from openerp.tests.common import HttpCase
class UICase(HttpCase): class UICase(HttpCase):
_tour_run = "odoo.__DEBUG__.services['web_tour.tour'].run('%s')"
_tour_ready = "odoo.__DEBUG__.services['web_tour.tour'].tours.%s.ready"
def extract_url(self, mail, *args, **kwargs): def extract_url(self, mail, *args, **kwargs):
url = mail._get_unsubscribe_url(mail, self.email)
url = mail._get_unsubscribe_url(self.email)
self.assertIn("&token=", url) self.assertIn("&token=", url)
self.assertTrue(url.startswith(self.domain)) self.assertTrue(url.startswith(self.domain))
self.url = url.replace(self.domain, "", 1) self.url = url.replace(self.domain, "", 1)
@ -20,6 +23,7 @@ class UICase(HttpCase):
self.mail_postprocess_patch = mock.patch( self.mail_postprocess_patch = mock.patch(
"openerp.addons.mass_mailing.models.mail_mail.MailMail." "openerp.addons.mass_mailing.models.mail_mail.MailMail."
"_postprocess_sent_message", "_postprocess_sent_message",
autospec=True,
side_effect=self.extract_url, side_effect=self.extract_url,
) )
with self.tempenv() as env: with self.tempenv() as env:
@ -37,11 +41,7 @@ class UICase(HttpCase):
"contact_list_ids": [(6, 0, self.lists.ids)], "contact_list_ids": [(6, 0, self.lists.ids)],
"reply_to_mode": "thread", "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"])
self.mailings[n]._onchange_model_and_list()
# HACK https://github.com/odoo/odoo/pull/14429 # HACK https://github.com/odoo/odoo/pull/14429
self.mailings[n].body_html = """ self.mailings[n].body_html = """
<div> <div>
@ -86,13 +86,12 @@ class UICase(HttpCase):
tour = "mass_mailing_custom_unsubscribe_tour_contact" tour = "mass_mailing_custom_unsubscribe_tour_contact"
self.phantom_js( self.phantom_js(
url_path=self.url, url_path=self.url,
code=("odoo.__DEBUG__.services['web.Tour']"
".run('%s', 'test')") % tour,
ready="odoo.__DEBUG__.services['web.Tour'].tours.%s" % tour)
code=self._tour_run % tour,
ready=self._tour_ready % tour)
# Check results from running tour # Check results from running tour
with self.tempenv() as env: with self.tempenv() as env:
self.assertFalse(self.contacts[0].opt_out)
self.assertTrue(self.contacts[0].opt_out)
self.assertTrue(self.contacts[1].opt_out) self.assertTrue(self.contacts[1].opt_out)
self.assertFalse(self.contacts[2].opt_out) self.assertFalse(self.contacts[2].opt_out)
unsubscriptions = env["mail.unsubscription"].search([ unsubscriptions = env["mail.unsubscription"].search([
@ -130,9 +129,8 @@ class UICase(HttpCase):
tour = "mass_mailing_custom_unsubscribe_tour_partner" tour = "mass_mailing_custom_unsubscribe_tour_partner"
self.phantom_js( self.phantom_js(
url_path=self.url, url_path=self.url,
code=("odoo.__DEBUG__.services['web.Tour']"
".run('%s', 'test')") % tour,
ready="odoo.__DEBUG__.services['web.Tour'].tours.%s" % tour)
code=self._tour_run % tour,
ready=self._tour_ready % tour)
# Check results from running tour # Check results from running tour
with self.tempenv() as env: with self.tempenv() as env:

2
mass_mailing_custom_unsubscribe/views/mail_unsubscription_reason_view.xml

@ -26,9 +26,9 @@
<field name="model">mail.unsubscription.reason</field> <field name="model">mail.unsubscription.reason</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree> <tree>
<field name="sequence" widget="handle"/>
<field name="name"/> <field name="name"/>
<field name="details_required"/> <field name="details_required"/>
<field name="sequence" invisible="True"/>
</tree> </tree>
</field> </field>
</record> </record>

Loading…
Cancel
Save