ernesto
6 years ago
30 changed files with 502 additions and 780 deletions
-
40mass_mailing_custom_unsubscribe/README.rst
-
4mass_mailing_custom_unsubscribe/__init__.py
-
10mass_mailing_custom_unsubscribe/__manifest__.py
-
76mass_mailing_custom_unsubscribe/controllers/main.py
-
17mass_mailing_custom_unsubscribe/demo/assets.xml
-
17mass_mailing_custom_unsubscribe/hooks.py
-
BINmass_mailing_custom_unsubscribe/images/form.png
-
3mass_mailing_custom_unsubscribe/models/__init__.py
-
36mass_mailing_custom_unsubscribe/models/mail_blacklist.py
-
34mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py
-
30mass_mailing_custom_unsubscribe/models/mail_mass_mailing_contact.py
-
14mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py
-
27mass_mailing_custom_unsubscribe/models/mail_unsubscription.py
-
2mass_mailing_custom_unsubscribe/readme/CONFIGURE.rst
-
11mass_mailing_custom_unsubscribe/readme/CONTRIBUTORS.rst
-
13mass_mailing_custom_unsubscribe/readme/ROADMAP.rst
-
4mass_mailing_custom_unsubscribe/readme/USAGE.rst
-
37mass_mailing_custom_unsubscribe/static/description/index.html
-
74mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js
-
46mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js
-
28mass_mailing_custom_unsubscribe/static/src/js/require_details.js
-
332mass_mailing_custom_unsubscribe/static/src/js/unsubscribe.js
-
65mass_mailing_custom_unsubscribe/templates/general_reason_form.xml
-
40mass_mailing_custom_unsubscribe/templates/mass_mailing_contact_reason.xml
-
1mass_mailing_custom_unsubscribe/tests/__init__.py
-
149mass_mailing_custom_unsubscribe/tests/test_ui.py
-
6mass_mailing_custom_unsubscribe/views/assets.xml
-
30mass_mailing_custom_unsubscribe/views/mail_mass_mailing_contact_view.xml
-
19mass_mailing_custom_unsubscribe/views/mail_mass_mailing_list_view.xml
-
31mass_mailing_custom_unsubscribe/views/mail_unsubscription_view.xml
@ -1,4 +1,4 @@ |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from . import controllers, models |
|||
from .hooks import post_init_hook |
|||
from . import controllers |
|||
from . import models |
@ -1,17 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<odoo> |
|||
|
|||
<template id="assets_frontend_demo" |
|||
inherit_id="website.assets_frontend"> |
|||
<xpath expr="."> |
|||
<script type="text/javascript" |
|||
src="/mass_mailing_custom_unsubscribe/static/src/js/contact.tour.js"/> |
|||
<script type="text/javascript" |
|||
src="/mass_mailing_custom_unsubscribe/static/src/js/partner.tour.js"/> |
|||
</xpath> |
|||
</template> |
|||
|
|||
</odoo> |
@ -1,17 +0,0 @@ |
|||
# Copyright 2018 David Vidal <david.vidal@tecnativa.com> |
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo import api, SUPERUSER_ID |
|||
|
|||
|
|||
def post_init_hook(cr, registry): |
|||
"""Ensure all existing contacts are going to work as v10""" |
|||
env = api.Environment(cr, SUPERUSER_ID, {}) |
|||
contacts = env['mail.mass_mailing.contact'].search([]) |
|||
for contact in contacts: |
|||
if len(contact.list_ids) <= 1: |
|||
continue |
|||
list_1 = contact.list_ids[0] |
|||
for list_ in contact.list_ids - list_1: |
|||
contact.copy({"list_ids": [(6, 0, list_.ids)]}) |
|||
contact.list_ids = list_1 |
Before Width: 1004 | Height: 506 | Size: 36 KiB |
@ -1,6 +1,5 @@ |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
|||
|
|||
from . import mail_blacklist |
|||
from . import mail_mass_mailing |
|||
from . import mail_mass_mailing_contact |
|||
from . import mail_mass_mailing_list |
|||
from . import mail_unsubscription |
@ -0,0 +1,36 @@ |
|||
# Copyright 2019 Tecnativa - Ernesto Tejeda |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo import models |
|||
|
|||
|
|||
class MailBlackList(models.Model): |
|||
_inherit = 'mail.blacklist' |
|||
|
|||
def _add(self, email): |
|||
mailing_id = self.env.context.get('mailing_id') |
|||
if mailing_id: |
|||
mailing = self.env['mail.mass_mailing'].browse(mailing_id) |
|||
model_name = mailing.mailing_model_real |
|||
res_id = self.env.context.get('res_id') |
|||
self.env["mail.unsubscription"].create({ |
|||
"email": email, |
|||
"mass_mailing_id": mailing_id, |
|||
"unsubscriber_id": "%s,%d" % (model_name, res_id), |
|||
"action": "blacklisting", |
|||
}) |
|||
return super(MailBlackList, self)._add(email) |
|||
|
|||
def _remove(self, email): |
|||
mailing_id = self.env.context.get('mailing_id') |
|||
if mailing_id: |
|||
mailing = self.env['mail.mass_mailing'].browse(mailing_id) |
|||
model_name = mailing.mailing_model_real |
|||
res_id = self.env.context.get('res_id') |
|||
self.env["mail.unsubscription"].create({ |
|||
"email": email, |
|||
"mass_mailing_id": mailing_id, |
|||
"unsubscriber_id": "%s,%d" % (model_name, res_id), |
|||
"action": "de_blacklisting", |
|||
}) |
|||
return super(MailBlackList, self)._remove(email) |
@ -1,30 +0,0 @@ |
|||
# Copyright 2018 David Vidal <david.vidal@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class MailMassMailing(models.Model): |
|||
_inherit = "mail.mass_mailing.contact" |
|||
|
|||
# Recover the old Many2one field so we can set a contact by list |
|||
mailing_list_id = fields.Many2one( |
|||
'mail.mass_mailing.list', |
|||
string='Mailing List', |
|||
ondelete='cascade', |
|||
compute="_compute_mailing_list_id", |
|||
inverse="_inverse_mailing_list_id", |
|||
search="_search_mailing_list_id", |
|||
) |
|||
|
|||
@api.depends('list_ids') |
|||
def _compute_mailing_list_id(self): |
|||
for contact in self: |
|||
contact.mailing_list_id = contact.list_ids[:1] |
|||
|
|||
def _inverse_mailing_list_id(self): |
|||
for contact in self: |
|||
contact.list_ids = contact.mailing_list_id |
|||
|
|||
def _search_mailing_list_id(self, operator, value): |
|||
return [('list_ids', operator, value)] |
@ -1,14 +0,0 @@ |
|||
# Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from odoo 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?'") |
@ -1,7 +1,7 @@ |
|||
You can customize what reasons will be displayed to your unsubscriptors when |
|||
they are going to unsubscribe. To do it: |
|||
|
|||
#. Go to *Mass Mailing > Configuration > Unsubscription Reasons*. |
|||
#. Go to *Email Marketing > Configuration > Unsubscription Reasons*. |
|||
#. Create / edit / remove / sort as usual. |
|||
#. If *Details required* is enabled, they will have to fill a text area to |
|||
continue. |
@ -1,4 +1,7 @@ |
|||
* Rafael Blasco <rafael.blasco@tecnativa.com> |
|||
* Antonio Espinosa <antonio.espinosa@tecnativa.com> |
|||
* Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
* David Vidal <david.vidal@tecnativa.com> |
|||
* `Tecnativa <https://www.tecnativa.com>`_: |
|||
|
|||
* Rafael Blasco |
|||
* Antonio Espinosa |
|||
* Jairo Llopis |
|||
* David Vidal |
|||
* Ernesto Tejeda |
@ -1,11 +1,4 @@ |
|||
* As version 11 has introduced a new relation type between mailing lists and |
|||
contacts that has multiple usability issues that are being reworked by Odoo |
|||
to land in version 12, this module falls back to the version 10 behaviour in |
|||
which one contact belonged to just one list. |
|||
* This module replaces AJAX submission core implementation from the mailing |
|||
list management form, because it is impossible to extend it. When |
|||
https://github.com/odoo/odoo/pull/14386 gets merged (which upstreams most |
|||
needed changes), this addon will need a refactoring (mostly removing |
|||
duplicated functionality and depending on it instead of replacing it). In the |
|||
mean time, there is a little chance that this introduces some |
|||
incompatibilities with other addons that depend on ``website_mass_mailing``. |
|||
list management form, because it is impossible to extend it. When this is |
|||
fixed, this addon will need a refactoring (mostly removing |
|||
duplicated functionality and depending on it instead of replacing it). |
@ -1,8 +1,8 @@ |
|||
Once configured: |
|||
|
|||
#. Go to *Mass Mailing > Mailings > Mass Mailings > Create*. |
|||
#. Go to *Email Marketing > Mailings > Create*. |
|||
#. Edit your mass mailing at wish, but remember to add a snippet from |
|||
*Footers*, so people have an *Unsubscribe* link. |
|||
#. Send it. |
|||
#. If somebody gets unsubscribed, you will see logs about that under |
|||
*Mass Mailing > Mailings > Unsubscriptions*. |
|||
*Email Marketing > Unsubscriptions*. |
@ -1,74 +0,0 @@ |
|||
/* 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 base = require("web_editor.base"); |
|||
var tour = require("web_tour.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( |
|||
"mass_mailing_custom_unsubscribe_tour_contact", |
|||
{ |
|||
tour: true, |
|||
wait_for: base.ready(), |
|||
}, |
|||
[ |
|||
{ |
|||
content: "Unsubscription reasons are invisible", |
|||
trigger: "#unsubscribe_form:has(.js_unsubscription_reason:hidden)", |
|||
}, |
|||
{ |
|||
content: "Uncheck list 0", |
|||
trigger: "li:contains('test list 0') input", |
|||
// List 2 is not cross unsubscriptable
|
|||
extra_trigger: "body:not(:has(li:contains('test list 2'))) li:contains('test list 0') input:checked", |
|||
}, |
|||
{ |
|||
content: "Uncheck list 1", |
|||
trigger: "li:contains('test list 1') input:checked", |
|||
extra_trigger: ".js_unsubscription_reason:visible", |
|||
}, |
|||
{ |
|||
content: "Choose other reason", |
|||
trigger: ".radio:contains('Other reason') :radio", |
|||
extra_trigger: ".radio:contains('Other reason') " + |
|||
":radio:not(: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", |
|||
}, |
|||
{ |
|||
content: "Update subscriptions 1st time", |
|||
trigger: "#unsubscribe_form :submit", |
|||
}, |
|||
{ |
|||
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
|
|||
$(".alert-success").removeClass("alert-success"); |
|||
}, |
|||
}, |
|||
{ |
|||
content: "Update subscriptions 2nd time", |
|||
trigger: "#unsubscribe_form:not(:has(.js_unsubscription_reason:visible)) :submit", |
|||
}, |
|||
{ |
|||
content: "Resuscription was OK", |
|||
trigger: ".alert-success", |
|||
} |
|||
] |
|||
); |
|||
}); |
@ -1,46 +0,0 @@ |
|||
/* 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 base = require("web_editor.base"); |
|||
var tour = require("web_tour.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( |
|||
"mass_mailing_custom_unsubscribe_tour_partner", |
|||
{ |
|||
tour: true, |
|||
wait_for: base.ready(), |
|||
}, |
|||
[ |
|||
{ |
|||
content: "Choose other reason", |
|||
trigger: ".radio:contains('Other reason') :radio:not(:checked)", |
|||
extra_trigger: "#reason_form .js_unsubscription_reason", |
|||
}, |
|||
{ |
|||
content: "Switch to not interested reason", |
|||
trigger: ".radio:contains(\"I'm not interested\") :radio:not(:checked)", |
|||
extra_trigger: "[name='details']:propRequired", |
|||
}, |
|||
{ |
|||
content: "Unsubscribe", |
|||
trigger: "#reason_form :submit", |
|||
extra_trigger: "body:not(:has([name='details']:propRequired))", |
|||
}, |
|||
{ |
|||
content: "Successfully unsubscribed", |
|||
trigger: "body:not(:has(#reason_form)) .alert-success:contains('You have been successfully unsubscribed!')", |
|||
}, |
|||
] |
|||
); |
|||
}); |
@ -1,28 +0,0 @@ |
|||
/* 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.require_details", |
|||
function (require) { |
|||
"use strict"; |
|||
var animation = require("website.content.snippets.animation"); |
|||
|
|||
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", |
|||
$(event.target).is("[data-details-required]") && |
|||
$(event.target).is(":visible")); |
|||
}, |
|||
}); |
|||
|
|||
return animation.registry.mass_mailing_custom_unsubscribe_require_details; |
|||
}); |
@ -1,121 +1,279 @@ |
|||
/* 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"); |
|||
var ajax = require("web.ajax"); |
|||
var animation = require("website.content.snippets.animation"); |
|||
/* This JS module replaces core AJAX submission because it is impossible |
|||
* to extend it as it is currently designed. */ |
|||
odoo.define('mass_mailing_custom_unsubscribe.unsubscribe', function (require) { |
|||
'use strict'; |
|||
|
|||
var ajax = require('web.ajax'); |
|||
var core = require('web.core'); |
|||
require('web.dom_ready'); |
|||
|
|||
var _t = core._t; |
|||
|
|||
animation.registry.mass_mailing_unsubscribe = |
|||
animation.Class.extend({ |
|||
selector: "#unsubscribe_form", |
|||
start: function () { |
|||
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(), 10); |
|||
}, |
|||
|
|||
// 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(); |
|||
}, |
|||
var email = $("input[name='email']").val(); |
|||
var mailing_id = parseInt($("input[name='mailing_id']").val()); |
|||
var res_id = parseInt($("input[name='res_id']").val()); |
|||
var token = (location.search.split('token' + '=')[1] || '').split('&')[0]; |
|||
var $mailing_lists = $("input[name='contact_ids']"); |
|||
var $reasons = $("#custom_div_feedback"); |
|||
var $details = $("textarea[name='details']"); |
|||
var $radio = $(":radio"); |
|||
var $info_state = $("#info_state, #custom_div_feedback"); |
|||
|
|||
$radio.on('change click', function (e) { |
|||
$details.prop( |
|||
"required", |
|||
$(event.target).is("[data-details-required]") && $(event.target).is(":visible") |
|||
); |
|||
}); |
|||
|
|||
// Display reasons form only if there are unsubscriptions
|
|||
toggle_reasons: function () { |
|||
var toggle_reasons = function () { |
|||
// Find contacts that were checked and now are unchecked
|
|||
var $disabled = this.$contacts.filter(function () { |
|||
var $disabled = $mailing_lists.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); |
|||
var $radios = this.$reasons.find(":radio"); |
|||
if (this.$reasons.is(":hidden")) { |
|||
$reasons.toggleClass("d-none", !$disabled.length); |
|||
var $radios = $reasons.find(":radio"); |
|||
if ($reasons.is(":hidden")) { |
|||
// Uncheck chosen reason
|
|||
$radios.prop("checked", false) |
|||
// Unrequire specifying a reason
|
|||
.prop("required", false) |
|||
// Remove possible constraints for details
|
|||
.trigger("change"); |
|||
// Clear textarea
|
|||
$details.val(""); |
|||
} else { |
|||
// Require specifying a reason
|
|||
$radios.prop("required", true); |
|||
} |
|||
}, |
|||
|
|||
// Get values to send
|
|||
values: function () { |
|||
var result = { |
|||
email: this.$email.val(), |
|||
mailing_id: parseInt(this.$mailing_id.val(), 10), |
|||
opt_in_ids: this.contact_ids(true), |
|||
opt_out_ids: this.contact_ids(false), |
|||
res_id: parseInt(this.$res_id.val(), 10), |
|||
token: this.$token.val(), |
|||
}; |
|||
|
|||
$mailing_lists.change(function (e) { |
|||
toggle_reasons(); |
|||
$('#info_state').addClass('invisible'); |
|||
}); |
|||
|
|||
if (email != '' && email != undefined) { |
|||
ajax.jsonRpc('/mailing/blacklist/check', 'call', { |
|||
'email': email, |
|||
'mailing_id': mailing_id, |
|||
'res_id': res_id, |
|||
'token': token |
|||
}) |
|||
.then(function (result) { |
|||
if (result == 'unauthorized') { |
|||
$('#button_add_blacklist').hide(); |
|||
$('#button_remove_blacklist').hide(); |
|||
} |
|||
else if (result == true) { |
|||
$('#button_remove_blacklist').show(); |
|||
toggle_opt_out_section(false); |
|||
} |
|||
else if (result == false) { |
|||
$('#button_add_blacklist').show(); |
|||
toggle_opt_out_section(true); |
|||
} |
|||
else { |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
} |
|||
}) |
|||
.fail(function () { |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
}); |
|||
} |
|||
else { |
|||
$('#div_blacklist').hide(); |
|||
} |
|||
|
|||
var unsubscribed_list = $("input[name='unsubscribed_list']").val(); |
|||
if (unsubscribed_list) { |
|||
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed from ' + unsubscribed_list + "</strong>.")); |
|||
} |
|||
else { |
|||
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed</strong>.')); |
|||
} |
|||
|
|||
$('#unsubscribe_form').on('submit', function (e) { |
|||
e.preventDefault(); |
|||
|
|||
var checked_ids = []; |
|||
$("input[type='checkbox']:checked").each(function (i) { |
|||
checked_ids[i] = parseInt($(this).val()); |
|||
}); |
|||
|
|||
var unchecked_ids = []; |
|||
$("input[type='checkbox']:not(:checked)").each(function (i) { |
|||
unchecked_ids[i] = parseInt($(this).val()); |
|||
}); |
|||
|
|||
var values = { |
|||
'opt_in_ids': checked_ids, |
|||
'opt_out_ids': unchecked_ids, |
|||
'email': email, |
|||
'mailing_id': mailing_id, |
|||
'res_id': res_id, |
|||
'token': token |
|||
}; |
|||
// 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(), |
|||
if ($reasons.is(":visible")) { |
|||
values.reason_id = parseInt( |
|||
$reasons.find("[name='reason_id']:checked").val(), |
|||
10 |
|||
); |
|||
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"); |
|||
values.details = $details.val(); |
|||
} |
|||
|
|||
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', values) |
|||
.then(function (result) { |
|||
if (result == 'unauthorized') { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('You are not authorized to do this!')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); |
|||
} |
|||
else if (result == true) { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('Your changes have been saved.')); |
|||
$info_state.removeClass('alert-info').addClass('alert-success'); |
|||
// Store checked status, to enable further changes
|
|||
this.$contacts.each(function () { |
|||
$mailing_lists.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"); |
|||
}, |
|||
toggle_reasons(); |
|||
} |
|||
else { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.')); |
|||
$info_state.removeClass('alert-info').addClass('alert-warning'); |
|||
} |
|||
}) |
|||
.fail(function () { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.')); |
|||
$info_state.removeClass('alert-info').addClass('alert-warning'); |
|||
}); |
|||
}); |
|||
|
|||
// ==================
|
|||
// Blacklist
|
|||
// ==================
|
|||
$('#button_add_blacklist').click(function (e) { |
|||
e.preventDefault(); |
|||
|
|||
if ($reasons.is(":hidden")) { |
|||
$reasons.toggleClass("d-none", false); |
|||
$reasons.find(":radio").prop("required", true); |
|||
} |
|||
if (!$("#unsubscribe_form")[0].reportValidity()) |
|||
return; |
|||
|
|||
ajax.jsonRpc('/mailing/blacklist/add', 'call', { |
|||
'email': email, |
|||
'mailing_id': mailing_id, |
|||
'res_id': res_id, |
|||
'token': token, |
|||
'reason_id': parseInt( |
|||
$reasons.find("[name='reason_id']:checked").val()), |
|||
'details': $details.val(), |
|||
}) |
|||
.then(function (result) { |
|||
if (result == 'unauthorized') { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('You are not authorized to do this!')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); |
|||
} |
|||
else { |
|||
if (result) { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('You have been successfully <strong>added to our blacklist</strong>. ' |
|||
+ 'You will not be contacted anymore by our services.')); |
|||
$info_state.removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success'); |
|||
toggle_opt_out_section(false); |
|||
// set mailing lists checkboxes to previous state
|
|||
$mailing_lists.each(function () { |
|||
var $this = $(this); |
|||
$this.prop("checked", $(this)[0].hasAttribute("checked")); |
|||
}); |
|||
// Hide reasons and reset reason fields
|
|||
$reasons.toggleClass("d-none", true) |
|||
.find(":radio").prop("checked", false); |
|||
$details.val("").prop("required", false); |
|||
} |
|||
else { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
} |
|||
$('#button_add_blacklist').hide(); |
|||
$('#button_remove_blacklist').show(); |
|||
$('#unsubscribed_info').hide(); |
|||
} |
|||
}) |
|||
.fail(function () { |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
}); |
|||
}); |
|||
|
|||
return animation.registry.mass_mailing_unsubscribe; |
|||
$('#button_remove_blacklist').click(function (e) { |
|||
e.preventDefault(); |
|||
|
|||
ajax.jsonRpc('/mailing/blacklist/remove', 'call', { |
|||
'email': email, |
|||
'mailing_id': mailing_id, |
|||
'res_id': res_id, |
|||
'token': token |
|||
}) |
|||
.then(function (result) { |
|||
if (result == 'unauthorized') { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('You are not authorized to do this!')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); |
|||
} |
|||
else { |
|||
if (result) { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t("You have been successfully <strong>removed from our blacklist</strong>. " |
|||
+ "You are now able to be contacted by our services.")); |
|||
$info_state.removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success'); |
|||
toggle_opt_out_section(true); |
|||
} |
|||
else { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
} |
|||
$('#button_add_blacklist').show(); |
|||
$('#button_remove_blacklist').hide(); |
|||
$('#unsubscribed_info').hide(); |
|||
} |
|||
}) |
|||
.fail(function () { |
|||
$('#info_state').removeClass('invisible'); |
|||
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); |
|||
$info_state.removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function toggle_opt_out_section(value) { |
|||
var result = !value; |
|||
$("#div_opt_out").find('*').attr('disabled', result); |
|||
$("#button_add_blacklist").attr('disabled', false); |
|||
$("#button_remove_blacklist").attr('disabled', false); |
|||
$("#custom_div_feedback").find('*').attr('disabled', false); |
|||
if (value) { |
|||
$('[name="button_subscription"]').addClass('clickable'); |
|||
} |
|||
else { |
|||
$('[name="button_subscription"]').removeClass('clickable'); |
|||
} |
|||
} |
@ -1,149 +0,0 @@ |
|||
# 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 odoo.tests.common import HttpCase |
|||
from werkzeug import urls |
|||
|
|||
|
|||
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): |
|||
url = mail._get_unsubscribe_url(self.email) |
|||
self.assertTrue(urls.url_parse(url).decode_query().get('token')) |
|||
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", |
|||
autospec=True, |
|||
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_id": self.env["mail.mass_mailing.contact"], |
|||
"contact_list_ids": [(6, 0, self.lists.ids)], |
|||
"reply_to_mode": "thread", |
|||
}) |
|||
self.mailings[n]._onchange_model_and_list() |
|||
# 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, |
|||
"mailing_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=self._tour_run % tour, |
|||
ready=self._tour_ready % tour) |
|||
|
|||
# Check results from running tour |
|||
with self.tempenv() as env: |
|||
self.assertTrue(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_id = self.env.ref( |
|||
"base.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=self._tour_run % tour, |
|||
ready=self._tour_ready % 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,30 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2018 David Vidal <david.vidal@tecnativa.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<odoo> |
|||
|
|||
<record id="view_mail_mass_mailing_contact_form" model="ir.ui.view"> |
|||
<field name="model">mail.mass_mailing.contact</field> |
|||
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_form"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="list_ids" position="attributes"> |
|||
<attribute name="invisible">1</attribute> |
|||
</field> |
|||
<field name="email" position="after"> |
|||
<field name="mailing_list_id"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="view_mail_mass_mailing_contact_tree" model="ir.ui.view"> |
|||
<field name="model">mail.mass_mailing.contact</field> |
|||
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_tree"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="email" position="before"> |
|||
<field name="mailing_list_id"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -1,19 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
|||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> |
|||
|
|||
<odoo> |
|||
|
|||
<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> |
|||
|
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue