diff --git a/mass_mailing_custom_unsubscribe/README.rst b/mass_mailing_custom_unsubscribe/README.rst index 293670cc..551b6d17 100644 --- a/mass_mailing_custom_unsubscribe/README.rst +++ b/mass_mailing_custom_unsubscribe/README.rst @@ -8,10 +8,17 @@ Customizable unsubscription process on mass mailing emails With this module you can set a custom unsubscribe link appended at the bottom of mass mailing emails. +It also displays a beautiful and simple unsubscription form when somebody +unsubscribes, to let you know why and let the user unsubscribe form another +mailing lists at the same time; and then displays a beautiful and customizable +goodbye message. Configuration ============= +Unsubscription Message In Mail Footer +------------------------------------- + To configure unsubscribe label go to *Settings > Technical > Parameters > System parameters* and add a ``mass_mailing.unsubscribe.label`` parameter with HTML to set at the bottom of mass emailing emails. Including ``%(url)s`` @@ -28,34 +35,63 @@ default 'Click to unsubscribe' link will appear, with the advantage that it is translatable via *Settings > Translations > Application Terms > Translated terms*. -Also your unsubscriptors will recieve a beautier goodbye page. You can -customize it clicking here **after installing the module**: +Unsubscription Reasons +---------------------- + +You can customize what reasons will be displayed to your unsubscriptors when +they are going to unsubscribe. To do it: + +#. Go to *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. + +Unsubscription Goodbye Message +------------------------------ -* `Unsubscription successful `_. -* `Unsubscription failed `_. +Your unsubscriptors will receive a beautier goodbye page. You can customize it +with these links **after installing the module**: +* `Unsubscription successful `_. +* `Unsubscription failed `_. Usage ===== +Once configured, just send mass mailings as usual. + +If somebody gets unsubscribed, you will see logs about that under +*Marketing > Mass Mailing > Unsubscriptions*. + .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot :target: https://runbot.odoo-community.org/runbot/205/8.0 - Known issues / Roadmap ====================== +* This needs tests. * This custom HTML is not translatable, so as a suggestion, you can define the same text in several languages in several lines. -For example: + For example: .. code:: html [EN] You can unsubscribe here
[ES] Puedes darte de baja aquí +* If you use the ``website_multi`` module, you will probably find that the + views are not visible by default. +* This module adds a security hash for mass mailing unsubscription URLs, which + makes to not work anymore URLs of mass mailing messages sent before its + installation. If you need backwards compatibility, disable this security + feature by removing the ``mass_mailing.salt`` system parameter. To avoid + breaking current installations, you will not get a salt if you are upgrading + the addon. If you want a salt, create the above system parameter and assign a + random value to it. +* Security should be patched upstream. Remove security features in the version + where https://github.com/odoo/odoo/pull/12040 gets merged (if it does). Bug Tracker =========== @@ -63,8 +99,10 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback -`here `_. - +`here `_. Credits ======= @@ -89,4 +127,4 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit http://odoo-community.org. +To contribute to this module, please visit https://odoo-community.org. diff --git a/mass_mailing_custom_unsubscribe/__manifest__.py b/mass_mailing_custom_unsubscribe/__manifest__.py index 5d749a3e..e4d97195 100644 --- a/mass_mailing_custom_unsubscribe/__manifest__.py +++ b/mass_mailing_custom_unsubscribe/__manifest__.py @@ -23,15 +23,28 @@ { 'name': "Customizable unsubscription process on mass mailing emails", 'category': 'Marketing', - 'version': '8.0.1.1.0', + 'version': '8.0.2.0.0', 'depends': [ 'mass_mailing', 'website_crm', ], 'data': [ + 'security/ir.model.access.csv', + 'data/install_salt.xml', + 'data/mail.unsubscription.reason.csv', + 'views/assets.xml', + 'views/mail_unsubscription_reason_view.xml', + 'views/mail_mass_mailing_list_view.xml', + 'views/mail_unsubscription_view.xml', 'views/pages.xml', ], + 'images': [ + 'images/failure.png', + 'images/form.png', + 'images/success.png', + ], 'author': 'Antiun Ingeniería S.L., ' + 'Tecnativa,' 'Odoo Community Association (OCA)', 'website': 'http://www.antiun.com', 'license': 'AGPL-3', diff --git a/mass_mailing_custom_unsubscribe/controllers.py b/mass_mailing_custom_unsubscribe/controllers.py deleted file mode 100644 index b05ceee5..00000000 --- a/mass_mailing_custom_unsubscribe/controllers.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com) -# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). - -from openerp import http -from openerp.addons.mass_mailing.controllers.main import MassMailController - - -class CustomUnsuscribe(MassMailController): - @http.route() - def mailing(self, *args, **kwargs): - path = "/page/mass_mail_unsubscription_%s" - result = super(CustomUnsuscribe, self).mailing(*args, **kwargs) - return http.local_redirect( - path % ("success" if result.data == "OK" else "failure")) diff --git a/mass_mailing_custom_unsubscribe/controllers/__init__.py b/mass_mailing_custom_unsubscribe/controllers/__init__.py new file mode 100644 index 00000000..49478571 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import main diff --git a/mass_mailing_custom_unsubscribe/controllers/main.py b/mass_mailing_custom_unsubscribe/controllers/main.py new file mode 100644 index 00000000..263d39b4 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/controllers/main.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingeniería S.L. (http://www.antiun.com) +# © 2016 Jairo Llopis +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from openerp import exceptions +from openerp.http import local_redirect, request, route +from openerp.addons.mass_mailing.controllers.main import MassMailController +from .. import exceptions as _ex + + +class CustomUnsubscribe(MassMailController): + def _mailing_list_contacts_by_email(self, email): + """Gets the mailing list contacts by email. + + This should not be displayed to the final user if security validations + have not been matched. + """ + return request.env["mail.mass_mailing.contact"].sudo().search([ + ("email", "=", email), + ("opt_out", "=", False), + ("list_id.not_cross_unsubscriptable", "=", False), + ]) + + def unsubscription_reason(self, mailing_id, email, res_id, token, + qcontext_extra=None): + """Get the unsubscription reason form. + + :param mail.mass_mailing mailing_id: + Mailing where the unsubscription is being processed. + + :param str email: + Email to be unsubscribed. + + :param int res_id: + ID of the unsubscriber. + + :param dict qcontext_extra: + Additional dictionary to pass to the view. + """ + values = self.unsubscription_qcontext(mailing_id, email, res_id, token) + values.update(qcontext_extra or dict()) + return request.website.render( + "mass_mailing_custom_unsubscribe.reason_form", + values) + + def unsubscription_qcontext(self, mailing_id, email, res_id, token): + """Get rendering context for unsubscription form. + + :param mail.mass_mailing mailing_id: + Mailing where the unsubscription is being processed. + + :param str email: + Email to be unsubscribed. + + :param int res_id: + ID of the unsubscriber. + """ + email_fname = origin_name = None + domain = [("id", "=", res_id)] + record_ids = request.env[mailing_id.mailing_model].sudo() + + if "email_from" in record_ids._fields: + email_fname = "email_from" + elif "email" in record_ids._fields: + email_fname = "email" + + if not (email_fname and email): + # Trying to unsubscribe without email? Bad boy... + raise exceptions.AccessDenied() + + domain.append((email_fname, "ilike", email)) + + # Search additional mailing lists for the unsubscriber + additional_contacts = self._mailing_list_contacts_by_email(email) + + if record_ids._name == "mail.mass_mailing.contact": + domain.append( + ("list_id", "in", mailing_id.contact_list_ids.ids)) + + # Unsubscription targets + record_ids = record_ids.search(domain) + + if record_ids._name == "mail.mass_mailing.contact": + additional_contacts -= record_ids + + if not record_ids: + # Trying to unsubscribe with fake criteria? Bad boy... + raise exceptions.AccessDenied() + + # Get data to identify the source of the unsubscription + fnames = self.unsubscription_special_fnames(record_ids._name) + first = record_ids[:1] + contact_name = first[fnames.get("contact", "name")] + origin_model_name = request.env["ir.model"].search( + [("model", "=", first._name)]).name + try: + first = first[fnames["related"]] + except KeyError: + pass + try: + origin_name = first[fnames["origin"]] + except KeyError: + pass + + # Get available reasons + reason_ids = ( + request.env["mail.unsubscription.reason"].search([])) + + return { + "additional_contact_ids": additional_contacts, + "contact_name": contact_name, + "email": email, + "mailing_id": mailing_id, + "origin_model_name": origin_model_name, + "origin_name": origin_name, + "reason_ids": reason_ids, + "record_ids": record_ids, + "res_id": res_id, + "token": token, + } + + def unsubscription_special_fnames(self, model): + """Define special field names to generate the unsubscription qcontext. + + :return dict: + Special fields will depend on the model, so this method should + return something like:: + + { + "related": "parent_id", + "origin": "display_name", + "contact": "contact_name", + } + + Where: + + - ``model.name`` is the technical name of the model. + - ``related`` indicates the name of a field in ``model.name`` that + contains a :class:`openerp.fields.Many2one` field which is + considered what the user is unsubscribing from. + - ``origin``: is the name of the field that contains the name of + what the user is unsubscribing from. + - ``contact`` is the name of the field that contains the name of + the user that is unsubscribing. + + Missing keys will mean that nothing special is required for that + model and it will use the default values. + """ + specials = { + "mail.mass_mailing.contact": { + "related": "list_id", + "origin": "display_name", + }, + "crm.lead": { + "origin": "name", + "contact": "contact_name", + }, + "hr.applicant": { + "related": "job_id", + "origin": "name", + }, + # In case you install OCA's event_registration_mass_mailing + "event.registration": { + "related": "event_id", + "origin": "name", + }, + } + return specials.get(model, dict()) + + @route(auth="public", website=True) + def mailing(self, mailing_id, email=None, res_id=None, **post): + """Display a confirmation form to get the unsubscription reason.""" + mailing = request.env["mail.mass_mailing"] + path = "/page/mass_mailing_custom_unsubscribe.%s" + good_token = mailing.hash_create(mailing_id, res_id, email) + + # Trying to unsubscribe with fake hash? Bad boy... + if good_token and post.get("token") != good_token: + return local_redirect(path % "failure") + + mailing = mailing.sudo().browse(mailing_id) + contact = request.env["mail.mass_mailing.contact"].sudo() + unsubscription = request.env["mail.unsubscription"].sudo() + + if not post.get("reason_id"): + # We need to know why you leave, get to the form + return self.unsubscription_reason( + mailing, email, res_id, post.get("token")) + + # Save reason and details + try: + with request.env.cr.savepoint(): + records = unsubscription.create({ + "email": email, + "unsubscriber_id": ",".join( + (mailing.mailing_model, res_id)), + "reason_id": int(post["reason_id"]), + "details": post.get("details", False), + "mass_mailing_id": mailing_id, + }) + + # Should provide details, go back to form + except _ex.DetailsRequiredError: + return self.unsubscription_reason( + mailing, email, res_id, post.get("token"), + {"error_details_required": True}) + + # Unsubscribe from additional lists + for key, value in post.iteritems(): + try: + label, list_id = key.split(",") + if label != "list_id": + raise ValueError + list_id = int(list_id) + except ValueError: + pass + else: + contact_id = contact.browse(int(value)) + if contact_id.list_id.id == list_id: + contact_id.opt_out = True + records += unsubscription.create({ + "email": email, + "unsubscriber_id": ",".join((contact._name, value)), + "reason_id": int(post["reason_id"]), + "details": post.get("details", False), + "mass_mailing_id": mailing_id, + }) + + # All is OK, unsubscribe + result = super(CustomUnsubscribe, self).mailing( + mailing_id, email, res_id, **post) + records.write({"success": result.data == "OK"}) + + # Redirect to the result + return local_redirect(path % ("success" if result.data == "OK" + else "failure")) diff --git a/mass_mailing_custom_unsubscribe/data/install_salt.xml b/mass_mailing_custom_unsubscribe/data/install_salt.xml new file mode 100644 index 00000000..46732d60 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/data/install_salt.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv b/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv new file mode 100644 index 00000000..061d1587 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/data/mail.unsubscription.reason.csv @@ -0,0 +1,5 @@ +"id","name","sequence","details_required" +"reason_not_interested","I'm not interested",10,"False" +"reason_not_requested","I did not request this",20,"False" +"reason_too_many","I get too many emails",30,"False" +"reason_other","Other reason",100,"True" diff --git a/mass_mailing_custom_unsubscribe/exceptions.py b/mass_mailing_custom_unsubscribe/exceptions.py new file mode 100644 index 00000000..efaac908 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/exceptions.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import exceptions + + +class DetailsRequiredError(exceptions.ValidationError): + pass diff --git a/mass_mailing_custom_unsubscribe/i18n/es.po b/mass_mailing_custom_unsubscribe/i18n/es.po index a5a9da56..4405dea3 100644 --- a/mass_mailing_custom_unsubscribe/i18n/es.po +++ b/mass_mailing_custom_unsubscribe/i18n/es.po @@ -1,29 +1,381 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * mass_mailing_custom_unsubscribe -# -# Translators: +# * mass_mailing_custom_unsubscribe +# msgid "" msgstr "" -"Project-Id-Version: social (8.0)\n" +"Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-09-04 14:42+0000\n" -"PO-Revision-Date: 2015-09-04 14:43+0000\n" -"Last-Translator: OCA Transbot \n" -"Language-Team: Spanish (http://www.transifex.com/oca/OCA-social-8-0/language/es/)\n" +"POT-Creation-Date: 2016-05-23 14:21+0000\n" +"PO-Revision-Date: 2016-05-23 14:21+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: \n" #. module: mass_mailing_custom_unsubscribe -#: code:addons/mass_mailing_custom_unsubscribe/models/mail_mail.py:37 +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "Anything else you want to say before you leave?" +msgstr "¿Algo más que quiera decir antes de irse?" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "But before continuing, could you please tell us why do you want to unsubscribe?" +msgstr "Pero antes de continuar, ¿podría decirnos por qué quiere darse de baja?" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,details_required:0 +#: help:mail.unsubscription.reason,details_required:0 +msgid "Check to ask for more details when this reason is selected." +msgstr "Marcar para pedir más detalles cuando está razón se selecciona." + +#. module: mass_mailing_custom_unsubscribe +#: code:addons/mass_mailing_custom_unsubscribe/models/mail_mail.py:39 #, python-format msgid "Click to unsubscribe" -msgstr "Haz click para darte de baja" +msgstr "Haga click para darse de baja" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.failure +#: view:website:mass_mailing_custom_unsubscribe.success +msgid "Contact us" +msgstr "Contáctenos" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,create_uid:0 +#: field:mail.unsubscription.reason,create_uid:0 +msgid "Created by" +msgstr "Creado por" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,create_date:0 +#: field:mail.unsubscription.reason,create_date:0 +msgid "Created on" +msgstr "Creado en" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,date:0 +msgid "Date" +msgstr "Fecha" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,message_last_post:0 +msgid "Date of the last message posted on the record." +msgstr "Fecha del último mensaje publicado en el registro." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,details:0 +msgid "Details" +msgstr "Detalles" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,details_required:0 +#: field:mail.unsubscription.reason,details_required:0 +msgid "Details required" +msgstr "Detalles requeridos" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,display_name:0 +#: field:mail.unsubscription.reason,display_name:0 +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.mass_mailing.list,not_cross_unsubscriptable:0 +msgid "Don't show this list in the other unsubscriptions" +msgstr "No mostrar esta lista en las otras desuscripciones" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,email:0 +msgid "Email" +msgstr "Correo electrónico" + +#. module: mass_mailing_custom_unsubscribe +#: model:ir.model,name:mass_mailing_custom_unsubscribe.model_mail_unsubscription +msgid "Email Thread" +msgstr "Hilo de mensajes" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_follower_ids:0 +msgid "Followers" +msgstr "Seguidores" + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +msgid "Group by" +msgstr "Agrupar por" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "Hello," +msgstr "Hola," + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,message_summary:0 +msgid "Holds the Chatter summary (number of messages, ...). This summary is directly in html format in order to be inserted in kanban views." +msgstr "Contiene el resumen del chatter (nº de mensajes, ...). Este resumen está directamente en formato html para ser insertado en vistas kanban." + +#. module: mass_mailing_custom_unsubscribe +#: model:mail.unsubscription.reason,name:mass_mailing_custom_unsubscribe.reason_not_requested +msgid "I did not request this" +msgstr "No lo solicité" + +#. module: mass_mailing_custom_unsubscribe +#: model:mail.unsubscription.reason,name:mass_mailing_custom_unsubscribe.reason_too_many +msgid "I get too many emails" +msgstr "Tengo demasiados correos electrónicos" + +#. module: mass_mailing_custom_unsubscribe +#: model:mail.unsubscription.reason,name:mass_mailing_custom_unsubscribe.reason_not_interested +msgid "I'm not interested" +msgstr "No estoy interesado" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,id:0 +#: field:mail.unsubscription.reason,id:0 +msgid "ID" +msgstr "ID" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,message_unread:0 +msgid "If checked new messages require your attention." +msgstr "Si está marcado, hay nuevos mensajes que requieren de su atención." + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,success:0 +msgid "If this is unchecked, it indicates some failure happened in the unsubscription process." +msgstr "Si no está marcado, indica que algún fallo ha ocurrido en el proceso de desuscripción." + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.mass_mailing.list,not_cross_unsubscriptable:0 +msgid "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?'" +msgstr "Si marca esta casilla, esta lista no será mostrada cuando se dé de baja de otra lista de correo, en la sección: '¿Hay alguna otra lista de correo que quiera dejar?'" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_is_follower:0 +msgid "Is a Follower" +msgstr "Es un seguidor" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "Is there any other mailing list you want to leave?" +msgstr "¿Hay alguna otra lista de correo que quiera dejar?" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.success +msgid "Is there anything else you want to tell us?" +msgstr "¿Hay algo más que quiera decirnos?" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.success +msgid "It's sad to see you go, but if you love\n" +" something, let it go." +msgstr "Es triste verlo partir, pero si quiere a algo, debe dejarlo marchar." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_last_post:0 +msgid "Last Message Date" +msgstr "Fecha del último mensaje" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,__last_update:0 +#: field:mail.unsubscription.reason,__last_update:0 +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,write_uid:0 +#: field:mail.unsubscription.reason,write_uid:0 +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,write_date:0 +#: field:mail.unsubscription.reason,write_date:0 +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: mass_mailing_custom_unsubscribe +#: model:ir.model,name:mass_mailing_custom_unsubscribe.model_mail_mass_mailing_list +msgid "Mailing List" +msgstr "Lista de correo" + +#. module: mass_mailing_custom_unsubscribe +#: model:ir.model,name:mass_mailing_custom_unsubscribe.model_mail_mass_mailing +msgid "Mass Mailing" +msgstr "Envío masivo" + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +#: field:mail.unsubscription,mass_mailing_id:0 +msgid "Mass mailing" +msgstr "Envío masivo" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,mass_mailing_id:0 +msgid "Mass mailing from which he was unsubscribed." +msgstr "Envío masivo del que ha sido dado de baja." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_ids:0 +msgid "Messages" +msgstr "Mensajes" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,message_ids:0 +msgid "Messages and communication history" +msgstr "Mensajes e historial de comunicación" + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +msgid "Month" +msgstr "Mes" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,details:0 +msgid "More details on why the unsubscription was made." +msgstr "Más detalles de por qué se dio de baja." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription.reason,name:0 +msgid "Name" +msgstr "Nombre" + +#. module: mass_mailing_custom_unsubscribe +#: model:mail.unsubscription.reason,name:mass_mailing_custom_unsubscribe.reason_other +msgid "Other reason" +msgstr "Otra razón" #. module: mass_mailing_custom_unsubscribe #: model:ir.model,name:mass_mailing_custom_unsubscribe.model_mail_mail msgid "Outgoing Mails" msgstr "Correos salientes" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription.reason,sequence:0 +msgid "Position of the reason in the list." +msgstr "Posición de la razón en la lista." + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +#: field:mail.unsubscription,reason_id:0 +msgid "Reason" +msgstr "Razón" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription.reason,sequence:0 +msgid "Sequence" +msgstr "Secuencia" + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +#: field:mail.unsubscription,success:0 +msgid "Success" +msgstr "Éxito" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_summary:0 +msgid "Summary" +msgstr "Resumen" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "Thank you!" +msgstr "¡Gracias!" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.failure +msgid "Thanks for your patience." +msgstr "Gracias por su paciencia." + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.failure +msgid "There was an error processing your unsubscription\n" +" request." +msgstr "There was an error processing your unsubscription\n" +" request." + +#. module: mass_mailing_custom_unsubscribe +#: code:addons/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py:59 +#, python-format +msgid "This reason requires an explanation." +msgstr "Esta razón requiere rellenar la explicación." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,message_unread:0 +msgid "Unread Messages" +msgstr "Mensajes sin leer" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "Unsubscribe now" +msgstr "Darse de baja ahora" + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,unsubscriber_id:0 +msgid "Unsubscriber" +msgstr "Desuscriptor" + +#. module: mass_mailing_custom_unsubscribe +#: model:ir.actions.act_window,name:mass_mailing_custom_unsubscribe.mail_unsubscription_reason_action +#: model:ir.ui.menu,name:mass_mailing_custom_unsubscribe.mail_unsubscription_reason_menu +msgid "Unsubscription Reasons" +msgstr "Razones de desuscripción" + +#. module: mass_mailing_custom_unsubscribe +#: model:ir.actions.act_window,name:mass_mailing_custom_unsubscribe.mail_unsubscription_action +#: model:ir.ui.menu,name:mass_mailing_custom_unsubscribe.mail_unsubscription_menu +msgid "Unsubscriptions" +msgstr "Desuscripciones" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.failure +msgid "We apologize for the inconvenience. You can contact us\n" +" and we will handle your unsubscription manually." +msgstr "Lamentamos los inconvenientes. Puede contactarnos\n" +" y realizaremos la desuscripción manualmente." + +#. module: mass_mailing_custom_unsubscribe +#: field:mail.unsubscription,website_message_ids:0 +msgid "Website Messages" +msgstr "Mensajes del sitio web" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,website_message_ids:0 +msgid "Website communication history" +msgstr "Historial de comunicación del sitio web" + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,unsubscriber_id:0 +msgid "Who was unsubscribed." +msgstr "Quién se dio de baja." + +#. module: mass_mailing_custom_unsubscribe +#: help:mail.unsubscription,reason_id:0 +msgid "Why the unsubscription was made." +msgstr "Por qué se dio de baja." + +#. module: mass_mailing_custom_unsubscribe +#: view:mail.unsubscription:mass_mailing_custom_unsubscribe.mail_unsubscription_view_search +msgid "Year" +msgstr "Año" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "You are trying to unsubscribe from all massive mailings" +msgstr "Está intentando darse de baja de todos los envíos masivos" + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.success +msgid "You were successfully unsubscribed from our\n" +" mailing list." +msgstr "Fue dado de baja correctamente de nuestra\n" +" lista de correo." + +#. module: mass_mailing_custom_unsubscribe +#: view:website:mass_mailing_custom_unsubscribe.reason_form +msgid "sent to followers of" +msgstr "enviados a los seguidores de" + diff --git a/mass_mailing_custom_unsubscribe/images/failure.png b/mass_mailing_custom_unsubscribe/images/failure.png new file mode 100644 index 00000000..ae751bfc Binary files /dev/null and b/mass_mailing_custom_unsubscribe/images/failure.png differ diff --git a/mass_mailing_custom_unsubscribe/images/form.png b/mass_mailing_custom_unsubscribe/images/form.png new file mode 100644 index 00000000..ceb0a541 Binary files /dev/null and b/mass_mailing_custom_unsubscribe/images/form.png differ diff --git a/mass_mailing_custom_unsubscribe/images/success.png b/mass_mailing_custom_unsubscribe/images/success.png new file mode 100644 index 00000000..1d835ee3 Binary files /dev/null and b/mass_mailing_custom_unsubscribe/images/success.png differ diff --git a/mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py b/mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py new file mode 100644 index 00000000..55d2cf46 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/migrations/8.0.2.0.0/pre-migrate.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# 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"))) diff --git a/mass_mailing_custom_unsubscribe/models/__init__.py b/mass_mailing_custom_unsubscribe/models/__init__.py index ac5156be..c81c6cc0 100644 --- a/mass_mailing_custom_unsubscribe/models/__init__.py +++ b/mass_mailing_custom_unsubscribe/models/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -# Python source code encoding : https://www.python.org/dev/peps/pep-0263/ -############################################################################## -# For copyright and license notices, see __openerp__.py file in root directory -############################################################################## +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). from . import mail_mail +from . import mail_mass_mailing +from . import mail_mass_mailing_list +from . import mail_unsubscription diff --git a/mass_mailing_custom_unsubscribe/models/mail_mail.py b/mass_mailing_custom_unsubscribe/models/mail_mail.py index 9ae33586..dde94eca 100644 --- a/mass_mailing_custom_unsubscribe/models/mail_mail.py +++ b/mass_mailing_custom_unsubscribe/models/mail_mail.py @@ -6,28 +6,37 @@ import urlparse import urllib - -from openerp import models +from openerp import api, models from openerp.tools.translate import _ class MailMail(models.Model): _inherit = 'mail.mail' - def _get_unsubscribe_url(self, cr, uid, mail, email_to, - msg=None, context=None): - m_config = self.pool.get('ir.config_parameter') - base_url = m_config.get_param(cr, uid, 'web.base.url') - config_msg = m_config.get_param(cr, uid, - 'mass_mailing.unsubscribe.label') + @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({ - 'db': cr.dbname, - 'res_id': mail.res_id, - 'email': email_to - }) + 'params': urllib.urlencode(params), } ) html = '' diff --git a/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py new file mode 100644 index 00000000..4e17ec85 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from hashlib import sha256 +from uuid import uuid4 +from openerp import api, models + + +class MailMassMailing(models.Model): + _inherit = "mail.mass_mailing" + + @api.model + def _init_salt_create(self): + """Create a salt to secure the unsubscription URLs.""" + icp = self.env["ir.config_parameter"] + key = "mass_mailing.salt" + salt = icp.get_param(key) + if salt is False: + salt = str(uuid4()) + icp.set_param(key, salt, ["base.group_erp_manager"]) + + @api.model + def hash_create(self, mailing_id, res_id, email): + """Create a secure hash to know if the unsubscription is trusted. + + :return None/str: + Secure hash, or ``None`` if the system parameter is empty. + """ + salt = self.env["ir.config_parameter"].sudo().get_param( + "mass_mailing.salt") + if not salt: + return None + source = (self.env.cr.dbname, mailing_id, res_id, email, salt) + return sha256(",".join(map(unicode, source))).hexdigest() diff --git a/mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py new file mode 100644 index 00000000..97101894 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/models/mail_mass_mailing_list.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# © 2016 Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import fields, models + + +class MailMassMailing(models.Model): + _inherit = "mail.mass_mailing.list" + + not_cross_unsubscriptable = fields.Boolean( + string="Don't show this list in the other unsubscriptions", + help="If you mark this field, this list won't be shown when " + "unsubscribing from other mailing list, in the section: " + "'Is there any other mailing list you want to leave?'") diff --git a/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py b/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py new file mode 100644 index 00000000..8686d59e --- /dev/null +++ b/mass_mailing_custom_unsubscribe/models/mail_unsubscription.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import _, api, fields, models +from .. import exceptions + + +class MailUnsubscription(models.Model): + _name = "mail.unsubscription" + _inherit = "mail.thread" + _rec_name = "date" + + date = fields.Datetime( + default=lambda self: self._default_date(), + required=True) + email = fields.Char( + required=True) + mass_mailing_id = fields.Many2one( + "mail.mass_mailing", + "Mass mailing", + required=True, + help="Mass mailing from which he was unsubscribed.") + unsubscriber_id = fields.Reference( + lambda self: self._selection_unsubscriber_id(), + "Unsubscriber", + required=True, + help="Who was unsubscribed.") + reason_id = fields.Many2one( + "mail.unsubscription.reason", + "Reason", + ondelete="restrict", + required=True, + help="Why the unsubscription was made.") + details = fields.Char( + help="More details on why the unsubscription was made.") + details_required = fields.Boolean( + related="reason_id.details_required") + success = fields.Boolean( + help="If this is unchecked, it indicates some failure happened in the " + "unsubscription process.") + + @api.model + def _default_date(self): + return fields.Datetime.now() + + @api.model + def _selection_unsubscriber_id(self): + """Models that can be linked to a ``mail.mass_mailing``.""" + return self.env["mail.mass_mailing"]._get_mailing_model() + + @api.multi + @api.constrains("details", "reason_id") + def _check_details_needed(self): + """Ensure details are given if required.""" + for s in self: + if not s.details and s.details_required: + raise exceptions.DetailsRequiredError( + _("This reason requires an explanation.")) + + +class MailUnsubscriptionReason(models.Model): + _name = "mail.unsubscription.reason" + _order = "sequence, name" + + name = fields.Char( + index=True, + translate=True, + required=True) + details_required = fields.Boolean( + help="Check to ask for more details when this reason is selected.") + sequence = fields.Integer( + index=True, + help="Position of the reason in the list.") diff --git a/mass_mailing_custom_unsubscribe/security/ir.model.access.csv b/mass_mailing_custom_unsubscribe/security/ir.model.access.csv new file mode 100644 index 00000000..4fdadeab --- /dev/null +++ b/mass_mailing_custom_unsubscribe/security/ir.model.access.csv @@ -0,0 +1,6 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"read_unsubscription_reason_public","Public users can read unsubscription reasons","model_mail_unsubscription_reason","base.group_public",1,0,0,0 +"read_unsubscription_reason_employee","Employee users can read unsubscription reasons","model_mail_unsubscription_reason","base.group_user",1,0,0,0 +"write_unsubscription_reason","Mass mailing managers can manage unsubscription reasons","model_mail_unsubscription_reason","mass_mailing.group_mass_mailing_campaign",1,1,1,1 +"read_unsubscription","Marketing users can read unsubscriptions","model_mail_unsubscription","marketing.group_marketing_user",1,0,0,0 +"write_unsubscription","Mass mailing managers can manage unsubscriptions","model_mail_unsubscription","mass_mailing.group_mass_mailing_campaign",1,1,1,1 diff --git a/mass_mailing_custom_unsubscribe/static/src/js/require_details.js b/mass_mailing_custom_unsubscribe/static/src/js/require_details.js new file mode 100644 index 00000000..a8459f53 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/static/src/js/require_details.js @@ -0,0 +1,13 @@ +/* © 2016 Jairo Llopis + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +"use strict"; +(function ($) { + $("#reason_form :radio").change(function(event) { + $("textarea[name=details]").attr( + "required", + $(event.target).is("[data-details-required]") + ); + }); + $("#reason_form :radio:checked").change(); +})(jQuery); diff --git a/mass_mailing_custom_unsubscribe/tests/__init__.py b/mass_mailing_custom_unsubscribe/tests/__init__.py new file mode 100644 index 00000000..033a58c2 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_unsubscription +from . import test_mail_mail +from . import test_controller diff --git a/mass_mailing_custom_unsubscribe/tests/test_controller.py b/mass_mailing_custom_unsubscribe/tests/test_controller.py new file mode 100644 index 00000000..df04d793 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/tests/test_controller.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# © 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock +from contextlib import contextmanager + +from openerp.tests.common import TransactionCase + +from openerp.addons.mass_mailing_custom_unsubscribe.controllers.main import ( + CustomUnsubscribe +) + + +model = 'openerp.addons.mass_mailing_custom_unsubscribe.controllers.main' + + +@contextmanager +def mock_assets(): + """ Mock & yield controller assets """ + with mock.patch('%s.request' % model) as request: + yield { + 'request': request, + } + + +class EndTestException(Exception): + pass + + +class TestController(TransactionCase): + + def setUp(self): + super(TestController, self).setUp() + self.controller = CustomUnsubscribe() + + def _default_domain(self): + return [ + ('opt_out', '=', False), + ('list_id.not_cross_unsubscriptable', '=', False), + ] + + def test_mailing_list_contacts_by_email_search(self): + """ It should search for contacts """ + expect = 'email' + with mock_assets() as mk: + self.controller._mailing_list_contacts_by_email(expect) + model_obj = mk['request'].env['mail.mass_mailing.contact'].sudo() + model_obj.search.assert_called_once_with( + [('email', '=', expect)] + self._default_domain() + ) + + def test_mailing_list_contacts_by_email_return(self): + """ It should return result of search """ + expect = 'email' + with mock_assets() as mk: + res = self.controller._mailing_list_contacts_by_email(expect) + model_obj = mk['request'].env['mail.mass_mailing.contact'].sudo() + self.assertEqual( + model_obj.search(), res, + ) + + def test_unsubscription_reason_gets_context(self): + """ It should retrieve unsub qcontext """ + expect = 'mailing_id', 'email', 'res_id', 'token' + with mock_assets(): + with mock.patch.object( + self.controller, 'unsubscription_qcontext' + ) as unsub: + unsub.side_effect = EndTestException + with self.assertRaises(EndTestException): + self.controller.unsubscription_reason(*expect) + unsub.assert_called_once_with(*expect) + + def test_unsubscription_updates_with_extra_context(self): + """ It should update qcontext with provided vals """ + expect = 'mailing_id', 'email', 'res_id', 'token' + qcontext = {'context': 'test'} + with mock_assets(): + with mock.patch.object( + self.controller, 'unsubscription_qcontext' + ) as unsub: + self.controller.unsubscription_reason( + *expect, qcontext_extra=qcontext + ) + unsub().update.assert_called_once_with(qcontext) + + def test_unsubscription_updates_rendered_correctly(self): + """ It should correctly render website """ + expect = 'mailing_id', 'email', 'res_id', 'token' + with mock_assets() as mk: + with mock.patch.object( + self.controller, 'unsubscription_qcontext' + ) as unsub: + self.controller.unsubscription_reason(*expect) + mk['request'].website.render.assert_called_once_with( + "mass_mailing_custom_unsubscribe.reason_form", + unsub(), + ) + + def test_unsubscription_updates_returns_site(self): + """ It should return website """ + expect = 'mailing_id', 'email', 'res_id', 'token' + with mock_assets() as mk: + with mock.patch.object( + self.controller, 'unsubscription_qcontext' + ): + res = self.controller.unsubscription_reason(*expect) + self.assertEqual( + mk['request'].website.render(), res + ) diff --git a/mass_mailing_custom_unsubscribe/tests/test_mail_mail.py b/mass_mailing_custom_unsubscribe/tests/test_mail_mail.py new file mode 100644 index 00000000..9692267c --- /dev/null +++ b/mass_mailing_custom_unsubscribe/tests/test_mail_mail.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# © 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock + +from openerp.tests.common import TransactionCase + + +model = 'openerp.addons.mass_mailing_custom_unsubscribe.models.mail_mail' + + +class EndTestException(Exception): + pass + + +class TestMailMail(TransactionCase): + + def setUp(self): + super(TestMailMail, self).setUp() + self.Model = self.env['mail.mail'] + param_obj = self.env['ir.config_parameter'] + self.base_url = param_obj.get_param('web.base.url') + self.config_msg = param_obj.get_param( + 'mass_mailing.unsubscribe.label' + ) + + @mock.patch('%s.urlparse' % model) + @mock.patch('%s.urllib' % model) + def test_get_unsubscribe_url_proper_url(self, urllib, urlparse): + """ It should join the URL w/ proper args """ + urlparse.urljoin.side_effect = EndTestException + expect = mock.MagicMock(), 'email', 'msg' + with self.assertRaises(EndTestException): + self.Model._get_unsubscribe_url(*expect) + urlparse.urljoin.assert_called_once_with( + self.base_url, + 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % { + 'mailing_id': expect[0].mailing_id.id, + 'params': urllib.urlencode(), + } + ) + + @mock.patch('%s.urlparse' % model) + @mock.patch('%s.urllib' % model) + def test_get_unsubscribe_url_correct_params(self, urllib, urlparse): + """ It should create URL params w/ proper data """ + urlparse.urljoin.side_effect = EndTestException + expect = mock.MagicMock(), 'email', 'msg' + with self.assertRaises(EndTestException): + self.Model._get_unsubscribe_url(*expect) + urllib.urlencode.assert_called_once_with(dict( + db=self.env.cr.dbname, + res_id=expect[0].res_id, + email=expect[1], + token=self.env['mail.mass_mailing'].hash_create( + expect[0].mailing_id.id, + expect[0].res_id, + expect[1], + ) + )) + + @mock.patch('%s.urlparse' % model) + @mock.patch('%s.urllib' % model) + def test_get_unsubscribe_url_false_config_msg(self, urllib, urlparse): + """ It should return default config msg when none supplied """ + expects = ['uri', False] + urlparse.urljoin.return_value = expects[0] + with mock.patch.object(self.Model, 'env') as env: + env['ir.config_paramater'].get_param.side_effect = expects + res = self.Model._get_unsubscribe_url( + mock.MagicMock(), 'email', 'msg' + ) + self.assertIn( + expects[0], res, + 'Did not include URI in default message' + ) + self.assertIn( + 'msg', res, + 'Did not include input msg in default message' + ) + + @mock.patch('%s.urlparse' % model) + @mock.patch('%s.urllib' % model) + def test_get_unsubscribe_url_with_config_msg(self, urllib, urlparse): + """ It should return config message w/ URL formatted """ + expects = ['uri', 'test %(url)s'] + urlparse.urljoin.return_value = expects[0] + with mock.patch.object(self.Model, 'env') as env: + env['ir.config_paramater'].get_param.side_effect = expects + res = self.Model._get_unsubscribe_url( + mock.MagicMock(), 'email', 'msg' + ) + self.assertEqual( + expects[1] % {'url': expects[0]}, res, + 'Did not return proper config message' + ) diff --git a/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py b/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py new file mode 100644 index 00000000..3c787d46 --- /dev/null +++ b/mass_mailing_custom_unsubscribe/tests/test_unsubscription.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp.tests.common import TransactionCase +from .. import exceptions + + +class UnsubscriptionCase(TransactionCase): + def test_details_required(self): + """Cannot create unsubscription without details when required.""" + with self.assertRaises(exceptions.DetailsRequiredError): + self.env["mail.unsubscription"].create({ + "email": "axelor@yourcompany.example.com", + "mass_mailing_id": self.env.ref("mass_mailing.mass_mail_1").id, + "unsubscriber_id": + "res.partner,%d" % self.env.ref("base.res_partner_13").id, + "reason_id": + self.env.ref( + "mass_mailing_custom_unsubscribe.reason_other").id, + }) diff --git a/mass_mailing_custom_unsubscribe/views/assets.xml b/mass_mailing_custom_unsubscribe/views/assets.xml new file mode 100644 index 00000000..47583e5b --- /dev/null +++ b/mass_mailing_custom_unsubscribe/views/assets.xml @@ -0,0 +1,17 @@ + + + + + + +