[MERGE][REF] mass_mailing: improve subscription portal
PURPOSE
Overall cleaning of subscription and exclusion management code from portal.
This code comes mainly from v12 and can now benefit from cleaning and update.
Globally improve usability and features given by mailing portal about exclusion
list and opt-out management.
SPECIFICATIONS
Cleanup access control (hash usage, returned / raised errors). Globally check
security and usage of controllers.
Refactor JS to be finally using frontend widgets. Split it into sub-widgets to model
main parts of the unsubscribe portal: feedback sending, email blocklist management
and opt-out from mailing lists.
Improve usability of unsubscribe portal, notably
* allow to give a feedback when unsubscribing from mailing not related to
mailing lists. It was previously limited to mailings done on mailing lists.
Now the feedback is allowed in all cases and posts it on the related
document;
* clean display of opt-in and opt-out lists;
* display all public lists, even if not already join. This allows to opt-in
to new lists, which was not possible before;
* switch on a neutral name for non public lists (as they may contain
marketing hints);
* give UI feedback to customer when using buttons: add confirmation of
block list addition / removal, of updated subscriptions, ...
* globally improve wording;
Add a generic 'my' page for mailing, allowing logged users to control their
blocklist status and mailing lists.
Improve feedback sending. Messages and notes are improved to have a
better wording and links to contextual records when possible (mailing,
mailed records, contacts, ...). Add opt out reasons when updating
subscriptions or block list status. This allows to better report on common
causes.
LINKS
Task-2669037 (Mass Mailing: Refactor js/portal for subscription)
Task-2150462 (Mass Mailing: Improve subscription management)
closes odoo/odoo#86084
Related: odoo/upgrade#3327
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -28,7 +28,8 @@
|
||||
'data/ir_module_data.xml',
|
||||
'data/mailing_data_templates.xml',
|
||||
'data/mailing_list_contact.xml',
|
||||
'data/mailing_contact_subscription.xml',
|
||||
'data/mailing_subscription_optout.xml',
|
||||
'data/mailing_subscription.xml',
|
||||
'data/res_users_data.xml',
|
||||
'wizard/mail_compose_message_views.xml',
|
||||
'wizard/mailing_contact_import_views.xml',
|
||||
@@ -37,13 +38,15 @@
|
||||
'wizard/mailing_mailing_test_views.xml',
|
||||
'wizard/mailing_mailing_schedule_date_views.xml',
|
||||
'report/mailing_trace_report_views.xml',
|
||||
'views/mail_blacklist_views.xml',
|
||||
'views/mailing_filter_views.xml',
|
||||
'views/mailing_trace_views.xml',
|
||||
'views/link_tracker_views.xml',
|
||||
'views/mailing_contact_views.xml',
|
||||
'views/mailing_contact_subscription_views.xml',
|
||||
'views/mailing_list_views.xml',
|
||||
'views/mailing_mailing_views.xml',
|
||||
'views/mailing_subscription_views.xml',
|
||||
'views/mailing_subscription_optout_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/utm_campaign_views.xml',
|
||||
'views/mailing_menus.xml',
|
||||
@@ -82,7 +85,7 @@
|
||||
'demo': [
|
||||
'demo/utm.xml',
|
||||
'demo/mailing_list_contact.xml',
|
||||
'demo/mailing_contact_subscription.xml',
|
||||
'demo/mailing_subscription.xml',
|
||||
'demo/mailing_mailing.xml',
|
||||
'demo/mailing_trace.xml',
|
||||
],
|
||||
@@ -103,7 +106,13 @@
|
||||
],
|
||||
'mass_mailing.mailing_assets': [
|
||||
'mass_mailing/static/src/scss/mailing_portal.scss',
|
||||
'mass_mailing/static/src/js/mailing_portal.js',
|
||||
'mass_mailing/static/src/js/mailing_portal_subscription.js',
|
||||
'mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js',
|
||||
'mass_mailing/static/src/js/mailing_portal_subscription_feedback.js',
|
||||
'mass_mailing/static/src/js/mailing_portal_subscription_form.js',
|
||||
'mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml',
|
||||
'mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml',
|
||||
'mass_mailing/static/src/xml/mailing_portal_subscription_form.xml',
|
||||
],
|
||||
'web_editor.assets_wysiwyg': [
|
||||
'mass_mailing/static/src/js/mass_mailing_wysiwyg.js',
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.base.models.ir_qweb import keep_query
|
||||
|
||||
|
||||
class MailingLegacy(http.Controller):
|
||||
""" Retro compatibility layer for legacy endpoint"""
|
||||
|
||||
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
|
||||
def mailing_unsubscribe(self, mailing_id, **post):
|
||||
return request.redirect(
|
||||
f'/mailing/{mailing_id}/unsubscribe?{keep_query("*")}'
|
||||
def mailing_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post):
|
||||
""" Old route, using mail/mailing prefix, and outdated parameter names """
|
||||
params = werkzeug.urls.url_encode(
|
||||
dict(**post, document_id=res_id, email=email, hash_token=token)
|
||||
)
|
||||
return request.redirect(
|
||||
f'/mailing/{mailing_id}/unsubscribe?{params}'
|
||||
)
|
||||
|
||||
@@ -2,113 +2,325 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import werkzeug
|
||||
|
||||
from odoo import _, exceptions, http, tools
|
||||
from datetime import timedelta
|
||||
from markupsafe import Markup, escape
|
||||
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
||||
|
||||
from odoo import _, fields, http, tools
|
||||
from odoo.http import request, Response
|
||||
from odoo.tools import consteq
|
||||
|
||||
from markupsafe import Markup, escape
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
class MassMailController(http.Controller):
|
||||
|
||||
def _check_mailing_email_token(self, mailing_id, res_id, email, token):
|
||||
if not (mailing_id and res_id and email and token):
|
||||
return False
|
||||
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
return consteq(mailing._generate_mailing_recipient_token(res_id, email), token)
|
||||
def _check_mailing_email_token(self, mailing_id, document_id, email, hash_token,
|
||||
required_mailing_id=False):
|
||||
""" Return the mailing based on given credentials, sudo-ed. Raises if
|
||||
there is an issue fetching it.
|
||||
|
||||
Specific use case
|
||||
* hash_token is always required for public users, no generic page is
|
||||
available for them;
|
||||
* hash_token is not required for generic page for logged user, aka
|
||||
if no mailing_id is given;
|
||||
* hash_token is not required for mailing specific page if the user
|
||||
is a mailing user;
|
||||
* hash_token is not required for generic page for logged user, aka
|
||||
if no mailing_id is given and if mailing_id is not required;
|
||||
* hash_token always requires the triplet mailing_id, email and
|
||||
document_id, as it indicates it comes from a mailing email and
|
||||
is used when comparing hashes;
|
||||
"""
|
||||
if not hash_token:
|
||||
if request.env.user._is_public():
|
||||
raise BadRequest()
|
||||
if mailing_id and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
|
||||
raise BadRequest()
|
||||
if hash_token and (not mailing_id or not email or not document_id):
|
||||
raise BadRequest()
|
||||
if mailing_id:
|
||||
mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
if not mailing_sudo.exists():
|
||||
raise NotFound()
|
||||
if hash_token and not consteq(mailing_sudo._generate_mailing_recipient_token(document_id, email), hash_token):
|
||||
raise Unauthorized()
|
||||
else:
|
||||
if required_mailing_id:
|
||||
raise BadRequest()
|
||||
mailing_sudo = request.env['mailing.mailing'].sudo()
|
||||
return mailing_sudo
|
||||
|
||||
def _fetch_blocklist_record(self, email):
|
||||
if not email or not tools.email_normalize(email):
|
||||
return None
|
||||
return request.env['mail.blacklist'].sudo().with_context(
|
||||
active_test=False
|
||||
).search(
|
||||
[('email', '=', tools.email_normalize(email))]
|
||||
)
|
||||
|
||||
def _fetch_contacts(self, email):
|
||||
if not email or not tools.email_normalize(email):
|
||||
return request.env['mailing.contact']
|
||||
return request.env['mailing.contact'].sudo().search(
|
||||
[('email_normalized', '=', tools.email_normalize(email))]
|
||||
)
|
||||
|
||||
def _fetch_subscription_optouts(self):
|
||||
return request.env['mailing.subscription.optout'].sudo().search([])
|
||||
|
||||
def _fetch_user_information(self, email, hash_token):
|
||||
if hash_token or request.env.user._is_public():
|
||||
return email, hash_token
|
||||
return request.env.user.email_normalized, None
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SUBSCRIPTION MANAGEMENT
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/mailing/my', type='http', website=True, auth='user')
|
||||
def mailing_my(self):
|
||||
email, _hash_token = self._fetch_user_information(None, None)
|
||||
if not email:
|
||||
raise Unauthorized()
|
||||
|
||||
render_values = self._prepare_mailing_subscription_values(
|
||||
request.env['mailing.mailing'], False, email, None
|
||||
)
|
||||
render_values.update(feedback_enabled=False)
|
||||
return request.render(
|
||||
'mass_mailing.page_mailing_unsubscribe',
|
||||
render_values
|
||||
)
|
||||
|
||||
@http.route(['/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
|
||||
def mailing_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post):
|
||||
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
if mailing.exists():
|
||||
res_id = res_id and int(res_id)
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, str(token)):
|
||||
raise exceptions.AccessDenied()
|
||||
def mailing_unsubscribe(self, mailing_id, document_id=None, email=None, hash_token=None):
|
||||
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||
try:
|
||||
mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email_found, hash_token_found,
|
||||
required_mailing_id=True
|
||||
)
|
||||
except NotFound as e: # avoid leaking ID existence
|
||||
raise Unauthorized() from e
|
||||
|
||||
if mailing.mailing_model_real == 'mailing.contact':
|
||||
# Unsubscribe directly + Let the user choose their subscriptions
|
||||
mailing.update_opt_out(email, mailing.contact_list_ids.ids, True)
|
||||
if mailing_sudo.mailing_on_mailing_list:
|
||||
return self._mailing_unsubscribe_from_list(mailing_sudo, document_id, email_found, hash_token_found)
|
||||
return self._mailing_unsubscribe_from_document(mailing_sudo, document_id, email_found, hash_token_found)
|
||||
|
||||
contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
|
||||
subscription_list_ids = contacts.mapped('subscription_list_ids')
|
||||
# In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2,
|
||||
# assume that the user is not opt_out on both
|
||||
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
|
||||
opt_out_list_ids = subscription_list_ids.filtered(lambda rel: rel.opt_out).mapped('list_id')
|
||||
opt_in_list_ids = subscription_list_ids.filtered(lambda rel: not rel.opt_out).mapped('list_id')
|
||||
opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids])
|
||||
def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token):
|
||||
# Unsubscribe directly + Let the user choose their subscriptions
|
||||
mailing.contact_list_ids._update_subscription_from_email(email, opt_out=True)
|
||||
|
||||
unique_list_ids = set([list.list_id.id for list in subscription_list_ids])
|
||||
list_ids = request.env['mailing.list'].sudo().browse(unique_list_ids)
|
||||
unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public)
|
||||
return request.render('mass_mailing.page_mailing_unsubscribe', {
|
||||
'contacts': contacts,
|
||||
'list_ids': list_ids,
|
||||
'opt_out_list_ids': opt_out_list_ids,
|
||||
'unsubscribed_list': unsubscribed_list,
|
||||
'email': email,
|
||||
'mailing_id': mailing_id,
|
||||
'res_id': res_id,
|
||||
'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'),
|
||||
})
|
||||
else:
|
||||
opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([
|
||||
('contact_id.email_normalized', '=', email),
|
||||
('opt_out', '=', False)
|
||||
]).mapped('list_id')
|
||||
# compute name of unsubscribed list: hide non public lists
|
||||
if all(not mlist.is_public for mlist in mailing.contact_list_ids):
|
||||
lists_unsubscribed_name = _('You are no longer part of our mailing list(s).')
|
||||
elif len(mailing.contact_list_ids) == 1:
|
||||
lists_unsubscribed_name = _('You are no longer part of the %(mailing_name)s mailing list.',
|
||||
mailing_name=mailing.contact_list_ids.name)
|
||||
else:
|
||||
lists_unsubscribed_name = _(
|
||||
'You are no longer part of the %(mailing_names)s mailing list.',
|
||||
mailing_names=', '.join(mlist.name for mlist in mailing.contact_list_ids if mlist.is_public)
|
||||
)
|
||||
|
||||
message = Markup('<p>%s</p>') % Markup(
|
||||
_(
|
||||
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing, res_id)
|
||||
)
|
||||
return request.render(
|
||||
'mass_mailing.page_mailing_unsubscribe',
|
||||
dict(
|
||||
self._prepare_mailing_subscription_values(
|
||||
mailing, document_id, email, hash_token
|
||||
),
|
||||
last_action='subscription_updated',
|
||||
unsubscribed_name=lists_unsubscribed_name,
|
||||
)
|
||||
)
|
||||
|
||||
def _mailing_unsubscribe_from_document(self, mailing, document_id, email, hash_token):
|
||||
if document_id:
|
||||
message = Markup(_(
|
||||
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing, document_id)
|
||||
))
|
||||
else:
|
||||
message = Markup(_(
|
||||
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (direct link usage)',
|
||||
**self._format_bl_request(mailing, document_id)
|
||||
))
|
||||
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=Markup('<p>%s</p>') % message)
|
||||
|
||||
return request.render(
|
||||
'mass_mailing.page_mailing_unsubscribe',
|
||||
dict(
|
||||
self._prepare_mailing_subscription_values(
|
||||
mailing, document_id, email, hash_token
|
||||
),
|
||||
last_action='blocklist_add',
|
||||
unsubscribed_name=_('You are no longer part of our services and will not be contacted again.'),
|
||||
)
|
||||
)
|
||||
|
||||
def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token):
|
||||
""" Prepare common values used in various subscription management or
|
||||
blocklist flows done in portal. """
|
||||
mail_blocklist = self._fetch_blocklist_record(email)
|
||||
email_normalized = tools.email_normalize(email)
|
||||
|
||||
# fetch optout/blacklist reasons
|
||||
opt_out_reasons = self._fetch_subscription_optouts()
|
||||
|
||||
# as there may be several contacts / email -> consider any opt-in overrides
|
||||
# opt-out
|
||||
contacts = self._fetch_contacts(email)
|
||||
lists_optin = contacts.subscription_ids.filtered(
|
||||
lambda sub: not sub.opt_out
|
||||
).list_id
|
||||
lists_optout = contacts.subscription_ids.filtered(
|
||||
lambda sub: sub.opt_out and sub.list_id not in lists_optin
|
||||
).list_id
|
||||
lists_public = request.env['mailing.list'].sudo().search(
|
||||
[('is_public', '=', True),
|
||||
('id', 'not in', (lists_optin + lists_optout).ids)
|
||||
],
|
||||
limit=10,
|
||||
order='create_date DESC, id DESC',
|
||||
)
|
||||
|
||||
return {
|
||||
# customer
|
||||
'document_id': document_id,
|
||||
'email': email,
|
||||
'email_valid': bool(email_normalized),
|
||||
'hash_token': hash_token,
|
||||
'mailing_id': mailing.id,
|
||||
'res_id': document_id,
|
||||
# feedback
|
||||
'feedback_enabled': True,
|
||||
'feedback_readonly': False,
|
||||
'opt_out_reasons': opt_out_reasons,
|
||||
# blocklist
|
||||
'blocklist_enabled': bool(
|
||||
request.env['ir.config_parameter'].sudo().get_param(
|
||||
'mass_mailing.show_blacklist_buttons',
|
||||
default=True,
|
||||
)
|
||||
_blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message)
|
||||
),
|
||||
'blocklist_possible': mail_blocklist is not None,
|
||||
'is_blocklisted': mail_blocklist.active if mail_blocklist else False,
|
||||
# mailing lists
|
||||
'contacts': contacts,
|
||||
'lists_contacts': contacts.subscription_ids.list_id,
|
||||
'lists_optin': lists_optin,
|
||||
'lists_optout': lists_optout,
|
||||
'lists_public': lists_public,
|
||||
}
|
||||
|
||||
return request.render('mass_mailing.page_mailing_unsubscribe_done', {
|
||||
'email': email,
|
||||
'mailing_id': mailing_id,
|
||||
'res_id': res_id,
|
||||
'list_ids': opt_in_lists,
|
||||
'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param(
|
||||
'mass_mailing.show_blacklist_buttons'),
|
||||
})
|
||||
return request.redirect('/web')
|
||||
@http.route('/mailing/list/update', type='json', auth='public', csrf=True)
|
||||
def mailing_update_list_subscription(self, mailing_id=None, document_id=None,
|
||||
email=None, hash_token=None,
|
||||
lists_optin_ids=None, **post):
|
||||
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||
try:
|
||||
_mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email_found, hash_token_found,
|
||||
required_mailing_id=False
|
||||
)
|
||||
except BadRequest:
|
||||
return 'error'
|
||||
except (NotFound, Unauthorized):
|
||||
return 'unauthorized'
|
||||
|
||||
@http.route('/mailing/list/update', type='json', auth='public')
|
||||
def mailing_update_list_subscription(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token):
|
||||
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
if mailing.exists():
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, token):
|
||||
return 'unauthorized'
|
||||
mailing.update_opt_out(email, opt_in_ids, False)
|
||||
mailing.update_opt_out(email, opt_out_ids, True)
|
||||
return True
|
||||
return 'error'
|
||||
contacts = self._fetch_contacts(email_found)
|
||||
lists_optin = request.env['mailing.list'].sudo().browse(lists_optin_ids or []).exists()
|
||||
# opt-out all not chosen lists
|
||||
lists_to_optout = contacts.subscription_ids.filtered(
|
||||
lambda sub: not sub.opt_out and sub.list_id not in lists_optin
|
||||
).list_id
|
||||
# opt-in in either already member, either public (to avoid trying to opt-in
|
||||
# in private lists)
|
||||
lists_to_optin = lists_optin.filtered(
|
||||
lambda mlist: mlist.is_public or mlist in contacts.list_ids
|
||||
)
|
||||
lists_to_optout._update_subscription_from_email(email_found, opt_out=True)
|
||||
lists_to_optin._update_subscription_from_email(email_found, opt_out=False)
|
||||
|
||||
@http.route('/mailing/feedback', type='json', auth='public')
|
||||
def mailing_send_feedback(self, mailing_id, res_id, email, feedback, token):
|
||||
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
if mailing.exists() and email:
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, token):
|
||||
return 'unauthorized'
|
||||
model = request.env[mailing.mailing_model_real]
|
||||
records = model.sudo().search([('email_normalized', '=', tools.email_normalize(email))])
|
||||
for record in records:
|
||||
record.sudo().message_post(body=_("Feedback from %(email)s: %(feedback)s", email=email, feedback=feedback))
|
||||
return bool(records)
|
||||
return 'error'
|
||||
return len(lists_to_optout)
|
||||
|
||||
@http.route('/mailing/feedback', type='json', auth='public', csrf=True)
|
||||
def mailing_send_feedback(self, mailing_id=None, document_id=None,
|
||||
email=None, hash_token=None,
|
||||
last_action=None,
|
||||
opt_out_reason_id=False, feedback=None,
|
||||
**post):
|
||||
""" Feedback can be given after some actions, notably after opt-outing
|
||||
from mailing lists or adding an email in the blocklist.
|
||||
|
||||
This controller tries to write the customer feedback in the most relevant
|
||||
record. Feedback consists in two parts, the opt-out reason (based on data
|
||||
in 'mailing.subscription.optout' model) and the feedback itself (which
|
||||
is triggered by the optout reason 'is_feedback' fields).
|
||||
"""
|
||||
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||
try:
|
||||
mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email_found, hash_token_found,
|
||||
required_mailing_id=False,
|
||||
)
|
||||
except BadRequest:
|
||||
return 'error'
|
||||
except (NotFound, Unauthorized):
|
||||
return 'unauthorized'
|
||||
|
||||
if not opt_out_reason_id:
|
||||
return 'error'
|
||||
feedback = feedback.strip() if feedback else ''
|
||||
message = ''
|
||||
if feedback:
|
||||
if not request.env.user._is_public():
|
||||
author_name = f'{request.env.user.name} ({email_found})'
|
||||
else:
|
||||
author_name = email_found
|
||||
message = Markup("<p>%s<br />%s</p>") % (
|
||||
_('Feedback from %(author_name)s', author_name=author_name),
|
||||
feedback
|
||||
)
|
||||
|
||||
# blocklist addition: opt-out and feedback linked to the mail.blacklist records
|
||||
if last_action == 'blocklist_add':
|
||||
mail_blocklist = self._fetch_blocklist_record(email)
|
||||
if mail_blocklist:
|
||||
if message:
|
||||
mail_blocklist._track_set_log_message(message)
|
||||
mail_blocklist.opt_out_reason_id = opt_out_reason_id
|
||||
|
||||
# opt-outed from mailing lists (either from a mailing or directly from 'my')
|
||||
# -> in that case, update recently-updated subscription records and log on
|
||||
# contacts
|
||||
documents_for_post = []
|
||||
if (last_action in {'subscription_updated', 'subscription_updated_optout'} or
|
||||
(not last_action and (not mailing_sudo or mailing_sudo.mailing_on_mailing_list))):
|
||||
contacts = self._fetch_contacts(email_found)
|
||||
contacts.subscription_ids.filtered(
|
||||
lambda sub: sub.opt_out and sub.opt_out_datetime >= (fields.Datetime.now() - timedelta(minutes=10))
|
||||
).opt_out_reason_id = opt_out_reason_id
|
||||
if message:
|
||||
documents_for_post = contacts
|
||||
# feedback coming from a mailing, without additional context information: log
|
||||
elif mailing_sudo and message:
|
||||
documents_for_post = request.env[mailing_sudo.mailing_model_real].sudo().search(
|
||||
[('id', '=', document_id)
|
||||
])
|
||||
|
||||
for document_sudo in documents_for_post:
|
||||
document_sudo.message_post(body=message)
|
||||
|
||||
return True
|
||||
|
||||
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public', sitemap=False)
|
||||
def mailing_unsubscribe_placeholder_link(self, **post):
|
||||
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
|
||||
raise NotFound()
|
||||
return request.redirect('/mailing/my', code=301, local=True)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TRACKING
|
||||
@@ -119,7 +331,7 @@ class MassMailController(http.Controller):
|
||||
""" Email tracking. """
|
||||
expected_token = request.env['mail.mail']._generate_mail_recipient_token(mail_id)
|
||||
if not consteq(token, expected_token):
|
||||
raise BadRequest()
|
||||
raise Unauthorized()
|
||||
|
||||
request.env['mailing.trace'].sudo().set_opened(domain=[('mail_mail_id_int', 'in', [mail_id])])
|
||||
response = Response()
|
||||
@@ -148,101 +360,124 @@ class MassMailController(http.Controller):
|
||||
@http.route('/mailing/report/unsubscribe', type='http', website=True, auth='public')
|
||||
def mailing_report_deactivate(self, token, user_id):
|
||||
if not token or not user_id:
|
||||
raise NotFound()
|
||||
user_id = int(user_id)
|
||||
correct_token = consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user_id))
|
||||
user = request.env['res.users'].sudo().browse(user_id)
|
||||
if correct_token and user.has_group('mass_mailing.group_mass_mailing_user'):
|
||||
request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False)
|
||||
if user.has_group('base.group_system'):
|
||||
menu_id = request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id
|
||||
return request.render('mass_mailing.mailing_report_deactivated', {'menu_id': menu_id})
|
||||
return request.render('mass_mailing.mailing_report_deactivated')
|
||||
raise NotFound()
|
||||
raise BadRequest()
|
||||
user = request.env['res.users'].sudo().browse(int(user_id)).exists()
|
||||
if not user or not user.has_group('mass_mailing.group_mass_mailing_user') or \
|
||||
not consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user.id)):
|
||||
raise Unauthorized()
|
||||
|
||||
request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False)
|
||||
render_vals = {}
|
||||
if user.has_group('base.group_system'):
|
||||
render_vals = {'menu_id': request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id}
|
||||
return request.render('mass_mailing.mailing_report_deactivated', render_vals)
|
||||
|
||||
@http.route(['/mailing/<int:mailing_id>/view'], type='http', website=True, auth='public')
|
||||
def mailing_view_in_browser(self, mailing_id, email=None, res_id=None, token=""):
|
||||
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
if mailing.exists():
|
||||
res_id = int(res_id) if res_id else False
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, str(token)) and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
|
||||
raise exceptions.AccessDenied()
|
||||
def mailing_view_in_browser(self, mailing_id, email=None, document_id=None, hash_token=None, **kwargs):
|
||||
# backward compatibility: temporary for mailings sent before migation to 17
|
||||
document_id = document_id or kwargs.get('res_id')
|
||||
hash_token = hash_token or kwargs.get('token')
|
||||
try:
|
||||
mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email, hash_token,
|
||||
required_mailing_id=True,
|
||||
)
|
||||
except NotFound as e:
|
||||
raise Unauthorized() from e
|
||||
|
||||
# do not force lang, will simply use user context
|
||||
html_markupsafe = mailing._render_field(
|
||||
'body_html',
|
||||
[res_id],
|
||||
compute_lang=False,
|
||||
options={'post_process': False}
|
||||
)[res_id]
|
||||
# Update generic URLs (without parameters) to final ones
|
||||
html_markupsafe = html_markupsafe.replace('/unsubscribe_from_list',
|
||||
mailing._get_unsubscribe_url(email, res_id))
|
||||
# do not force lang, will simply use user context
|
||||
document_id = int(document_id) if document_id and document_id.isdigit() else 0
|
||||
html_markupsafe = mailing_sudo._render_field(
|
||||
'body_html',
|
||||
[document_id],
|
||||
compute_lang=False,
|
||||
options={'post_process': False}
|
||||
)[document_id]
|
||||
# Update generic URLs (without parameters) to final ones
|
||||
if document_id:
|
||||
html_markupsafe = html_markupsafe.replace(
|
||||
'/unsubscribe_from_list',
|
||||
mailing_sudo._get_unsubscribe_url(email, document_id)
|
||||
)
|
||||
else: # when manually trying a /view on a mailing, not through email link
|
||||
html_markupsafe = html_markupsafe.replace(
|
||||
'/unsubscribe_from_list',
|
||||
werkzeug.urls.url_join(
|
||||
mailing_sudo.get_base_url(),
|
||||
f'/mailing/{mailing_sudo.id}/unsubscribe',
|
||||
)
|
||||
)
|
||||
|
||||
return request.render('mass_mailing.mailing_view', {
|
||||
'body': html_markupsafe,
|
||||
})
|
||||
|
||||
return request.redirect('/web')
|
||||
return request.render(
|
||||
'mass_mailing.mailing_view',
|
||||
{
|
||||
'body': html_markupsafe,
|
||||
},
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# BLACKLIST
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@http.route('/mailing/blacklist/check', type='json', auth='public')
|
||||
def mail_blacklist_check(self, mailing_id, res_id, email, token):
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, token):
|
||||
@http.route('/mailing/blocklist/add', type='json', auth='public')
|
||||
def mail_blocklist_add(self, mailing_id=None, document_id=None,
|
||||
email=None, hash_token=None):
|
||||
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||
try:
|
||||
mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email_found, hash_token_found,
|
||||
required_mailing_id=False,
|
||||
)
|
||||
except BadRequest:
|
||||
return 'error'
|
||||
except (NotFound, Unauthorized):
|
||||
return 'unauthorized'
|
||||
if email:
|
||||
record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))])
|
||||
if record['active']:
|
||||
return True
|
||||
return False
|
||||
return 'error'
|
||||
|
||||
@http.route('/mailing/blacklist/add', type='json', auth='public')
|
||||
def mail_blacklist_add(self, mailing_id, res_id, email, token):
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, token):
|
||||
return 'unauthorized'
|
||||
if email:
|
||||
if mailing_id and res_id:
|
||||
mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
message = Markup('<p>%s</p>') % Markup(
|
||||
_(
|
||||
'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing_sudo, res_id)
|
||||
)
|
||||
if mailing_sudo:
|
||||
message = Markup(
|
||||
_(
|
||||
'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing_sudo, document_id)
|
||||
)
|
||||
else:
|
||||
message = Markup('<p>%s</p>') % _('Blocklist request from portal')
|
||||
)
|
||||
else:
|
||||
message = Markup('<p>%s</p>') % _('Blocklist request from portal')
|
||||
|
||||
_blacklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=message)
|
||||
return True
|
||||
return 'error'
|
||||
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email_found, message=message)
|
||||
return True
|
||||
|
||||
@http.route('/mailing/blacklist/remove', type='json', auth='public')
|
||||
def mail_blacklist_remove(self, mailing_id, res_id, email, token):
|
||||
if not self._check_mailing_email_token(mailing_id, res_id, email, token):
|
||||
@http.route('/mailing/blocklist/remove', type='json', auth='public')
|
||||
def mail_blocklist_remove(self, mailing_id=None, document_id=None,
|
||||
email=None, hash_token=None):
|
||||
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||
try:
|
||||
mailing_sudo = self._check_mailing_email_token(
|
||||
mailing_id, document_id, email_found, hash_token_found,
|
||||
required_mailing_id=False,
|
||||
)
|
||||
except BadRequest:
|
||||
return 'error'
|
||||
except (NotFound, Unauthorized):
|
||||
return 'unauthorized'
|
||||
if email:
|
||||
if mailing_id and res_id:
|
||||
mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||
message = Markup('<p>%s</p>') % Markup(
|
||||
_(
|
||||
'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing_sudo, res_id)
|
||||
)
|
||||
)
|
||||
else:
|
||||
message = Markup('<p>%s</p>') % _('Blocklist removal request from portal')
|
||||
|
||||
_blacklist_rec = request.env['mail.blacklist'].sudo()._remove(email, message=message)
|
||||
return True
|
||||
return 'error'
|
||||
if mailing_sudo and document_id:
|
||||
message = Markup(
|
||||
_(
|
||||
'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||
**self._format_bl_request(mailing_sudo, document_id)
|
||||
)
|
||||
)
|
||||
else:
|
||||
message = Markup('<p>%s</p>') % _('Blocklist removal request from portal')
|
||||
|
||||
_blocklist_rec = request.env['mail.blacklist'].sudo()._remove(email_found, message=message)
|
||||
return True
|
||||
|
||||
def _format_bl_request(self, mailing, document_id):
|
||||
mailing_model_name = request.env['ir.model']._get(mailing.mailing_model_real).display_name
|
||||
return {
|
||||
'mailing_link': Markup(f'<a href="#" data-oe-model="mailing.mailing" data-oe-id="{mailing.id}">{escape(mailing.subject)}</a>'),
|
||||
'record_link': Markup(f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'),
|
||||
'record_link': Markup(
|
||||
f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'
|
||||
) if document_id else '',
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mailing_list_data_sub_contact_0" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_0" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_0"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
</record>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
<record id="mailing_subscription_optout_data_0" model="mailing.subscription.optout">
|
||||
<field name="name">I never subscribed to this list</field>
|
||||
<field name="sequence">1</field>
|
||||
</record>
|
||||
<record id="mailing_subscription_optout_data_1" model="mailing.subscription.optout">
|
||||
<field name="name">I changed my mind</field>
|
||||
<field name="sequence">2</field>
|
||||
</record>
|
||||
<record id="mailing_subscription_optout_data_2" model="mailing.subscription.optout">
|
||||
<field name="name">I receive too many emails from this list</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
<record id="mailing_subscription_optout_data_3" model="mailing.subscription.optout">
|
||||
<field name="name">The content of these emails is not relevant to me</field>
|
||||
<field name="sequence">4</field>
|
||||
</record>
|
||||
<record id="mailing_subscription_optout_data_4" model="mailing.subscription.optout">
|
||||
<field name="name">Other</field>
|
||||
<field name="sequence">99</field>
|
||||
<field name="is_feedback" eval="True"/>
|
||||
</record>
|
||||
</data></odoo>
|
||||
+8
-8
@@ -2,38 +2,38 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Subscription to 'mailing_list_data' -->
|
||||
<record id="mailing_list_data_sub_contact_1" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_1" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_1"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
</record>
|
||||
<record id="mailing_list_data_sub_contact_2" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_2" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_2"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
</record>
|
||||
<record id="mailing_list_data_sub_contact_3" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_3" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_3"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
</record>
|
||||
<record id="mailing_list_data_sub_contact_4" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_4" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_4"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
<field name="opt_out">True</field>
|
||||
</record>
|
||||
<record id="mailing_list_data_sub_contact_5" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_5" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_5"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
</record>
|
||||
<record id="mailing_list_data_sub_contact_6" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_data_sub_contact_6" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_6"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||
<field name="opt_out">True</field>
|
||||
</record>
|
||||
<!-- Subscription to 'mailing_list_1' -->
|
||||
<record id="mailing_list_1_sub_contact_3" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_1_sub_contact_3" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_3"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_1"/>
|
||||
</record>
|
||||
<record id="mailing_list_1_sub_contact_6" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_1_sub_contact_6" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing.mass_mail_contact_6"/>
|
||||
<field name="list_id" ref="mass_mailing.mailing_list_1"/>
|
||||
</record>
|
||||
@@ -4,9 +4,11 @@
|
||||
from . import ir_mail_server
|
||||
from . import ir_model
|
||||
from . import link_tracker
|
||||
from . import mailing_contact_subscription
|
||||
from . import mail_blacklist
|
||||
from . import mailing_subscription # keep before due to decorated m2m
|
||||
from . import mailing_contact
|
||||
from . import mailing_list
|
||||
from . import mailing_subscription_optout
|
||||
from . import mailing_trace
|
||||
from . import mailing
|
||||
from . import mailing_filter
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailBlackList(models.Model):
|
||||
""" Model of blacklisted email addresses to stop sending emails."""
|
||||
_inherit = ['mail.blacklist']
|
||||
|
||||
opt_out_reason_id = fields.Many2one(
|
||||
'mailing.subscription.optout', string='Reason',
|
||||
ondelete='restrict',
|
||||
tracking=10)
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'opt_out_reason_id' in init_values and self.opt_out_reason_id:
|
||||
return self.env.ref('mail.mt_comment')
|
||||
return super()._track_subtype(init_values)
|
||||
@@ -63,28 +63,43 @@ class MailMail(models.Model):
|
||||
return body
|
||||
|
||||
def _prepare_outgoing_list(self, recipients_follower_status=None):
|
||||
""" Update mailing specific links to add tracking based on res_id """
|
||||
""" Update mailing specific links to replace generic unsubscribe and
|
||||
view links by email-specific links. Also add headers to allow
|
||||
unsubscribe from email managers. """
|
||||
email_list = super()._prepare_outgoing_list(recipients_follower_status)
|
||||
if not self.res_id or not self.mailing_id:
|
||||
return email_list
|
||||
|
||||
base_url = self.mailing_id.get_base_url()
|
||||
for email_values in email_list:
|
||||
if tools.is_html_empty(email_values['body']) or not email_values['email_to']:
|
||||
if not email_values['email_to']:
|
||||
continue
|
||||
|
||||
emails = tools.email_split(email_values['email_to'][0])
|
||||
email_to = emails[0] if emails else False
|
||||
unsubscribe_url = self.mailing_id._get_unsubscribe_url(email_to, self.res_id)
|
||||
view_url = self.mailing_id._get_view_url(email_to, self.res_id)
|
||||
|
||||
if f'{base_url}/unsubscribe_from_list' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/unsubscribe_from_list',
|
||||
self.mailing_id._get_unsubscribe_url(email_to, self.res_id),
|
||||
)
|
||||
if f'{base_url}/view' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/view',
|
||||
self.mailing_id._get_view_url(email_to, self.res_id),
|
||||
)
|
||||
# replace links in body
|
||||
if not tools.is_html_empty(email_values['body']):
|
||||
if f'{base_url}/unsubscribe_from_list' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/unsubscribe_from_list',
|
||||
unsubscribe_url,
|
||||
)
|
||||
if f'{base_url}/view' in email_values['body']:
|
||||
email_values['body'] = email_values['body'].replace(
|
||||
f'{base_url}/view',
|
||||
view_url,
|
||||
)
|
||||
|
||||
# add headers
|
||||
email_values['headers'].update({
|
||||
'List-Unsubscribe': f'<{unsubscribe_url}>',
|
||||
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
||||
'Precedence': 'list',
|
||||
'X-Auto-Response-Suppress': 'OOF', # avoid out-of-office replies from MS Exchange
|
||||
})
|
||||
return email_list
|
||||
|
||||
def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
|
||||
|
||||
@@ -749,7 +749,7 @@ class MassMailing(models.Model):
|
||||
if self.contact_list_ids:
|
||||
action['context'] = {
|
||||
'default_mailing_list_ids': self.contact_list_ids[0].ids,
|
||||
'default_subscription_list_ids': [(0, 0, {'list_id': self.contact_list_ids[0].id})],
|
||||
'default_subscription_ids': [(0, 0, {'list_id': self.contact_list_ids[0].id})],
|
||||
}
|
||||
action['domain'] = [('list_ids', 'in', self.contact_list_ids.ids)]
|
||||
return action
|
||||
@@ -785,25 +785,6 @@ class MassMailing(models.Model):
|
||||
|
||||
return values_list
|
||||
|
||||
def update_opt_out(self, email, list_ids, value):
|
||||
if len(list_ids) > 0:
|
||||
model = self.env['mailing.contact'].with_context(active_test=False)
|
||||
records = model.search([('email_normalized', '=', tools.email_normalize(email))])
|
||||
opt_out_records = self.env['mailing.contact.subscription'].search([
|
||||
('contact_id', 'in', records.ids),
|
||||
('list_id', 'in', list_ids),
|
||||
('opt_out', '!=', value)
|
||||
])
|
||||
|
||||
opt_out_records.write({'opt_out': value})
|
||||
message = Markup(_('The recipient <strong>unsubscribed from %s</strong> mailing list(s)')) \
|
||||
if value else Markup(_('The recipient <strong>subscribed to %s</strong> mailing list(s)'))
|
||||
for record in records:
|
||||
# filter the list_id by record
|
||||
record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
|
||||
if len(record_lists) > 0:
|
||||
record.sudo().message_post(body=message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# A/B Test
|
||||
# ------------------------------------------------------
|
||||
@@ -1019,9 +1000,9 @@ class MassMailing(models.Model):
|
||||
self.get_base_url(), 'mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
|
||||
'mailing_id': self.id,
|
||||
'params': werkzeug.urls.url_encode({
|
||||
'res_id': res_id,
|
||||
'document_id': res_id,
|
||||
'email': email_to,
|
||||
'token': self._generate_mailing_recipient_token(res_id, email_to),
|
||||
'hash_token': self._generate_mailing_recipient_token(res_id, email_to),
|
||||
}),
|
||||
}
|
||||
)
|
||||
@@ -1032,9 +1013,9 @@ class MassMailing(models.Model):
|
||||
self.get_base_url(), 'mailing/%(mailing_id)s/view?%(params)s' % {
|
||||
'mailing_id': self.id,
|
||||
'params': werkzeug.urls.url_encode({
|
||||
'res_id': res_id,
|
||||
'document_id': res_id,
|
||||
'email': email_to,
|
||||
'token': self._generate_mailing_recipient_token(res_id, email_to),
|
||||
'hash_token': self._generate_mailing_recipient_token(res_id, email_to),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -14,18 +14,18 @@ class MassMailingContact(models.Model):
|
||||
_name = 'mailing.contact'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_description = 'Mailing Contact'
|
||||
_order = 'email'
|
||||
_order = 'name ASC, id DESC'
|
||||
_mailing_enabled = True
|
||||
|
||||
def default_get(self, fields_list):
|
||||
""" When coming from a mailing list we may have a default_list_ids context
|
||||
key. We should use it to create subscription_list_ids default value that
|
||||
key. We should use it to create subscription_ids default value that
|
||||
are displayed to the user as list_ids is not displayed on form view. """
|
||||
res = super(MassMailingContact, self).default_get(fields_list)
|
||||
if 'subscription_list_ids' in fields_list and not res.get('subscription_list_ids'):
|
||||
if 'subscription_ids' in fields_list and not res.get('subscription_ids'):
|
||||
list_ids = self.env.context.get('default_list_ids')
|
||||
if 'default_list_ids' not in res and list_ids and isinstance(list_ids, (list, tuple)):
|
||||
res['subscription_list_ids'] = [
|
||||
res['subscription_ids'] = [
|
||||
(0, 0, {'list_id': list_id}) for list_id in list_ids]
|
||||
return res
|
||||
|
||||
@@ -34,10 +34,10 @@ class MassMailingContact(models.Model):
|
||||
title_id = fields.Many2one('res.partner.title', string='Title')
|
||||
email = fields.Char('Email')
|
||||
list_ids = fields.Many2many(
|
||||
'mailing.list', 'mailing_contact_list_rel',
|
||||
'mailing.list', 'mailing_subscription',
|
||||
'contact_id', 'list_id', string='Mailing Lists')
|
||||
subscription_list_ids = fields.One2many(
|
||||
'mailing.contact.subscription', 'contact_id', string='Subscription Information')
|
||||
subscription_ids = fields.One2many(
|
||||
'mailing.subscription', 'contact_id', string='Subscription Information')
|
||||
country_id = fields.Many2one('res.country', string='Country')
|
||||
tag_ids = fields.Many2many('res.partner.category', string='Tags')
|
||||
opt_out = fields.Boolean(
|
||||
@@ -57,17 +57,17 @@ class MassMailingContact(models.Model):
|
||||
|
||||
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
|
||||
[active_list_id] = self._context['default_list_ids']
|
||||
contacts = self.env['mailing.contact.subscription'].search([('list_id', '=', active_list_id)])
|
||||
contacts = self.env['mailing.subscription'].search([('list_id', '=', active_list_id)])
|
||||
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
|
||||
return expression.FALSE_DOMAIN if value else expression.TRUE_DOMAIN
|
||||
|
||||
@api.depends('subscription_list_ids')
|
||||
@api.depends('subscription_ids')
|
||||
@api.depends_context('default_list_ids')
|
||||
def _compute_opt_out(self):
|
||||
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
|
||||
[active_list_id] = self._context['default_list_ids']
|
||||
for record in self:
|
||||
active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id)
|
||||
active_subscription_list = record.subscription_ids.filtered(lambda l: l.list_id.id == active_list_id)
|
||||
record.opt_out = active_subscription_list.opt_out
|
||||
else:
|
||||
for record in self:
|
||||
@@ -76,7 +76,7 @@ class MassMailingContact(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Synchronize default_list_ids (currently used notably for computed
|
||||
fields) default key with subscription_list_ids given by user when creating
|
||||
fields) default key with subscription_ids given by user when creating
|
||||
contacts.
|
||||
|
||||
Those two values have the same purpose, adding a list to to the contact
|
||||
@@ -90,21 +90,21 @@ class MassMailingContact(models.Model):
|
||||
default_list_ids = default_list_ids if isinstance(default_list_ids, (list, tuple)) else []
|
||||
|
||||
for vals in vals_list:
|
||||
if vals.get('list_ids') and vals.get('subscription_list_ids'):
|
||||
raise UserError(_('You should give either list_ids, either subscription_list_ids to create new contacts.'))
|
||||
if vals.get('list_ids') and vals.get('subscription_ids'):
|
||||
raise UserError(_('You should give either list_ids, either subscription_ids to create new contacts.'))
|
||||
|
||||
if default_list_ids:
|
||||
for vals in vals_list:
|
||||
if vals.get('list_ids'):
|
||||
continue
|
||||
current_list_ids = []
|
||||
subscription_ids = vals.get('subscription_list_ids') or []
|
||||
subscription_ids = vals.get('subscription_ids') or []
|
||||
for subscription in subscription_ids:
|
||||
if len(subscription) == 3:
|
||||
current_list_ids.append(subscription[2]['list_id'])
|
||||
for list_id in set(default_list_ids) - set(current_list_ids):
|
||||
subscription_ids.append((0, 0, {'list_id': list_id}))
|
||||
vals['subscription_list_ids'] = subscription_ids
|
||||
vals['subscription_ids'] = subscription_ids
|
||||
|
||||
return super(MassMailingContact, self.with_context(default_list_ids=False)).create(vals_list)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, Command, fields, models
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import _, api, Command, fields, models, tools
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
@@ -25,14 +27,14 @@ class MassMailingList(models.Model):
|
||||
contact_pct_blacklisted = fields.Float(compute="_compute_mailing_list_statistics", string="Percentage of Blacklisted")
|
||||
contact_pct_bounce = fields.Float(compute="_compute_mailing_list_statistics", string="Percentage of Bouncing")
|
||||
contact_ids = fields.Many2many(
|
||||
'mailing.contact', 'mailing_contact_list_rel', 'list_id', 'contact_id',
|
||||
'mailing.contact', 'mailing_subscription', 'list_id', 'contact_id',
|
||||
string='Mailing Lists', copy=False)
|
||||
mailing_count = fields.Integer(compute="_compute_mailing_count", string="Number of Mailing")
|
||||
mailing_ids = fields.Many2many(
|
||||
'mailing.mailing', 'mail_mass_mailing_list_rel',
|
||||
string='Mass Mailings', copy=False)
|
||||
subscription_ids = fields.One2many(
|
||||
'mailing.contact.subscription', 'list_id',
|
||||
'mailing.subscription', 'list_id',
|
||||
string='Subscription Information',
|
||||
copy=True, depends=['contact_ids'])
|
||||
is_public = fields.Boolean(
|
||||
@@ -76,13 +78,13 @@ class MassMailingList(models.Model):
|
||||
bounce_per_mailing = {}
|
||||
if self.ids:
|
||||
sql = '''
|
||||
SELECT mclr.list_id, COUNT(DISTINCT mc.id)
|
||||
SELECT list_sub.list_id, COUNT(DISTINCT mc.id)
|
||||
FROM mailing_contact mc
|
||||
LEFT OUTER JOIN mailing_contact_list_rel mclr
|
||||
ON mc.id = mclr.contact_id
|
||||
LEFT OUTER JOIN mailing_subscription list_sub
|
||||
ON mc.id = list_sub.contact_id
|
||||
WHERE mc.message_bounce > 0
|
||||
AND mclr.list_id in %s
|
||||
GROUP BY mclr.list_id
|
||||
AND list_sub.list_id in %s
|
||||
GROUP BY list_sub.list_id
|
||||
'''
|
||||
self.env.cr.execute(sql, (tuple(self.ids),))
|
||||
bounce_per_mailing = dict(self.env.cr.fetchall())
|
||||
@@ -142,7 +144,7 @@ class MassMailingList(models.Model):
|
||||
action['context'] = {
|
||||
**self.env.context,
|
||||
'default_mailing_list_ids': self.ids,
|
||||
'default_subscription_list_ids': [
|
||||
'default_subscription_ids': [
|
||||
Command.create({'list_id': mailing_list.id})
|
||||
for mailing_list in self
|
||||
],
|
||||
@@ -230,7 +232,7 @@ class MassMailingList(models.Model):
|
||||
src_lists |= self
|
||||
self.env.flush_all()
|
||||
self.env.cr.execute("""
|
||||
INSERT INTO mailing_contact_list_rel (contact_id, list_id)
|
||||
INSERT INTO mailing_subscription (contact_id, list_id)
|
||||
SELECT st.contact_id AS contact_id, %s AS list_id
|
||||
FROM
|
||||
(
|
||||
@@ -241,7 +243,7 @@ class MassMailingList(models.Model):
|
||||
row_number() OVER (PARTITION BY email ORDER BY email) AS rn
|
||||
FROM
|
||||
mailing_contact contact,
|
||||
mailing_contact_list_rel contact_list_rel,
|
||||
mailing_subscription contact_list_rel,
|
||||
mailing_list list
|
||||
WHERE contact.id=contact_list_rel.contact_id
|
||||
AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE
|
||||
@@ -253,7 +255,7 @@ class MassMailingList(models.Model):
|
||||
SELECT 1
|
||||
FROM
|
||||
mailing_contact contact2,
|
||||
mailing_contact_list_rel contact_list_rel2
|
||||
mailing_subscription contact_list_rel2
|
||||
WHERE contact2.email = contact.email
|
||||
AND contact_list_rel2.contact_id = contact2.id
|
||||
AND contact_list_rel2.list_id = %s
|
||||
@@ -267,6 +269,91 @@ class MassMailingList(models.Model):
|
||||
def close_dialog(self):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
# ------------------------------------------------------
|
||||
# SUBSCRIPTION MANAGEMENT
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _update_subscription_from_email(self, email, opt_out=True, force_message=None):
|
||||
""" When opting-out: we have to switch opted-in subscriptions. We don't
|
||||
need to create subscription for other lists as opt-out = not being a
|
||||
member.
|
||||
|
||||
When opting-in: we have to switch opted-out subscriptions and create
|
||||
subscription for other mailing lists id they are public. Indeed a
|
||||
contact is opted-in when being subscribed in a mailing list.
|
||||
|
||||
:param str email: email address that should opt-in or opt-out from
|
||||
mailing lists;
|
||||
:param boolean opt_out: if True, opt-out from lists given by self if
|
||||
'email' is member of it. If False, opt-in in lists givben by self
|
||||
and create membership if not already member;
|
||||
:param str force_message: if given, post a note using that body on
|
||||
contact instead of generated update message. Give False to entirely
|
||||
skip the note step;
|
||||
"""
|
||||
email_normalized = tools.email_normalize(email)
|
||||
if not self or not email_normalized:
|
||||
return
|
||||
|
||||
contacts = self.env['mailing.contact'].with_context(active_test=False).search(
|
||||
[('email_normalized', '=', email_normalized)]
|
||||
)
|
||||
if not contacts:
|
||||
return
|
||||
|
||||
# switch opted-in subscriptions
|
||||
if opt_out:
|
||||
current_opt_in = contacts.subscription_ids.filtered(
|
||||
lambda sub: not sub.opt_out and sub.list_id in self
|
||||
)
|
||||
if current_opt_in:
|
||||
current_opt_in.write({'opt_out': True})
|
||||
# switch opted-out subscription and create missing subscriptions
|
||||
else:
|
||||
subscriptions = contacts.subscription_ids.filtered(lambda sub: sub.list_id in self)
|
||||
current_opt_out = subscriptions.filtered('opt_out')
|
||||
if current_opt_out:
|
||||
current_opt_out.write({'opt_out': False})
|
||||
|
||||
# create a subscription (for a single contact) for missing lists
|
||||
missing_lists = self - subscriptions.list_id
|
||||
if missing_lists:
|
||||
self.env['mailing.subscription'].create([
|
||||
{'contact_id': contacts[0].id,
|
||||
'list_id': mailing_list.id}
|
||||
for mailing_list in missing_lists
|
||||
])
|
||||
|
||||
for contact in contacts:
|
||||
# do not log if no opt-out / opt-in was actually done
|
||||
if opt_out:
|
||||
updated = current_opt_in.filtered(lambda sub: sub.contact_id == contact).list_id
|
||||
else:
|
||||
updated = current_opt_out.filtered(lambda sub: sub.contact_id == contact).list_id + missing_lists
|
||||
if not updated:
|
||||
continue
|
||||
|
||||
if force_message is False:
|
||||
continue
|
||||
if force_message:
|
||||
body = force_message
|
||||
elif opt_out:
|
||||
body = Markup('<p>%s</p>') % _(
|
||||
'%(contact_name)s unsubscribed from the following mailing list(s)',
|
||||
contact_name=contact.display_name
|
||||
)
|
||||
body += Markup('<ul>%s</ul>' % ''.join(Markup('<li>%s</li>') % name for name in updated.mapped('name')))
|
||||
else:
|
||||
body = Markup('<p>%s</p>') % _(
|
||||
'%(contact_name)s subscribed to the following mailing list(s)',
|
||||
contact_name=contact.display_name
|
||||
)
|
||||
body += Markup('<ul>%s</ul>' % ''.join(Markup('<li>%s</li>') % name for name in updated.mapped('name')))
|
||||
contact.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=body,
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# MAILING
|
||||
# ------------------------------------------------------
|
||||
@@ -310,7 +397,7 @@ class MassMailingList(models.Model):
|
||||
SELECT
|
||||
{','.join(self._get_contact_statistics_fields().values())}
|
||||
FROM
|
||||
mailing_contact_list_rel r
|
||||
mailing_subscription r
|
||||
{self._get_contact_statistics_joins()}
|
||||
WHERE list_id IN %s
|
||||
GROUP BY
|
||||
|
||||
+14
-11
@@ -4,13 +4,13 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MassMailingContactListRel(models.Model):
|
||||
class MailingSubscription(models.Model):
|
||||
""" Intermediate model between mass mailing list and mass mailing contact
|
||||
Indicates if a contact is opted out for a particular list
|
||||
"""
|
||||
_name = 'mailing.contact.subscription'
|
||||
_description = 'Mass Mailing Subscription Information'
|
||||
_table = 'mailing_contact_list_rel'
|
||||
_name = 'mailing.subscription'
|
||||
_description = 'Mailing List Subscription'
|
||||
_table = 'mailing_subscription'
|
||||
_rec_name = 'contact_id'
|
||||
_order = 'list_id DESC, contact_id DESC'
|
||||
|
||||
@@ -20,9 +20,12 @@ class MassMailingContactListRel(models.Model):
|
||||
string='Opt Out',
|
||||
default=False,
|
||||
help='The contact has chosen not to receive mails anymore from this list')
|
||||
unsubscription_date = fields.Datetime(
|
||||
opt_out_reason_id = fields.Many2one(
|
||||
'mailing.subscription.optout', string='Reason',
|
||||
ondelete='restrict')
|
||||
opt_out_datetime = fields.Datetime(
|
||||
string='Unsubscription Date',
|
||||
compute='_compute_unsubscription_date', readonly=False, store=True)
|
||||
compute='_compute_opt_out_datetime', readonly=False, store=True)
|
||||
message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False)
|
||||
is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False)
|
||||
|
||||
@@ -32,19 +35,19 @@ class MassMailingContactListRel(models.Model):
|
||||
]
|
||||
|
||||
@api.depends('opt_out')
|
||||
def _compute_unsubscription_date(self):
|
||||
self.filtered(lambda sub: not sub.opt_out).unsubscription_date = False
|
||||
def _compute_opt_out_datetime(self):
|
||||
self.filtered(lambda sub: not sub.opt_out).opt_out_datetime = False
|
||||
for subscription in self.filtered('opt_out'):
|
||||
subscription.unsubscription_date = self.env.cr.now()
|
||||
subscription.opt_out_datetime = self.env.cr.now()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('unsubscription_date'):
|
||||
if vals.get('opt_out_datetime') or vals.get('opt_out_reason_id'):
|
||||
vals['opt_out'] = True
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('unsubscription_date'):
|
||||
if vals.get('opt_out_datetime') or vals.get('opt_out_reason_id'):
|
||||
vals['opt_out'] = True
|
||||
return super().write(vals)
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailingSubscriptionReason(models.Model):
|
||||
""" Reason for opting out of mailing lists or for blacklisting. """
|
||||
_name = 'mailing.subscription.optout'
|
||||
_description = 'Mailing Subscription Reason'
|
||||
_order = 'sequence ASC, create_date DESC, id DESC'
|
||||
|
||||
name = fields.Char(string='Reason', translate=True)
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
is_feedback = fields.Boolean(string='Allow Feedback')
|
||||
@@ -2,7 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_utm_tag_mass_mailing_campaign,utm.tag,utm.model_utm_tag,mass_mailing.group_mass_mailing_campaign,1,1,1,1
|
||||
access_mailing_contact_mm_user,access.mailing.contact.mm.user,model_mailing_contact,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_contact_import_mailing_user,access.mailing.contact.import.mailing.user,model_mailing_contact_import,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_contact_subscription_mm_user,access.mailing.contact.subscription.mm.user,model_mailing_contact_subscription,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_subscription_mm_user,access.mailing.subscription.mm.user,model_mailing_subscription,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_subscription_optout_mm_user,access.mailing.subscription.optout.mm.user,model_mailing_subscription_optout,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_list_mm_user,access.mailing.list.mm.user,model_mailing_list,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_utm_stage,utm.stage,utm.model_utm_stage,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
access_mailing_mailing_mm_user,access.mailing.mailing.mm.user,model_mailing_mailing,mass_mailing.group_mass_mailing_user,1,1,1,1
|
||||
|
||||
|
@@ -1,195 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import { whenReady } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
|
||||
whenReady(() => {
|
||||
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];
|
||||
|
||||
if ($('.o_unsubscribe_form').length) {
|
||||
if (email != '' && email != undefined){
|
||||
jsonrpc('/mailing/blacklist/check', {'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').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. 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 %s</strong>.",
|
||||
escape(unsubscribed_list)
|
||||
)
|
||||
)
|
||||
}
|
||||
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());
|
||||
});
|
||||
|
||||
jsonrpc('/mailing/list/update', {'opt_in_ids': checked_ids, 'opt_out_ids': unchecked_ids, 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_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) {
|
||||
$('#subscription_info').text(_t('Your changes have been saved.'));
|
||||
$('#info_state').removeClass('alert-info').addClass('alert-success');
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Your changes have not been saved, try again later.'));
|
||||
$('#info_state').removeClass('alert-info').addClass('alert-warning');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_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();
|
||||
|
||||
jsonrpc('/mailing/blacklist/add', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_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) {
|
||||
$('#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);
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. 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();
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
});
|
||||
});
|
||||
|
||||
$('#button_remove_blacklist').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
jsonrpc('/mailing/blacklist/remove', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_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) {
|
||||
$('#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 {
|
||||
$('#subscription_info').text(_t('An error occurred. 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();
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
|
||||
});
|
||||
});
|
||||
|
||||
// ==================
|
||||
// Feedback
|
||||
// ==================
|
||||
$('#button_feedback').click(function (e) {
|
||||
var feedback = $("textarea[name='opt_out_feedback']").val();
|
||||
e.preventDefault();
|
||||
jsonrpc('/mailing/feedback', {'mailing_id': mailing_id, 'res_id': res_id, 'email': email, 'feedback': feedback, 'token': token})
|
||||
.then(function (result) {
|
||||
if (result == 'unauthorized'){
|
||||
$('#subscription_info').text(_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){
|
||||
$('#subscription_info').text(_t('Thank you! Your feedback has been sent successfully!'));
|
||||
$('#info_state').removeClass('alert-warning').removeClass('alert-info').removeClass('alert-error').addClass('alert-success');
|
||||
$("#div_feedback").hide();
|
||||
}
|
||||
else {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
|
||||
}
|
||||
})
|
||||
.guardedCatch(function () {
|
||||
$('#subscription_info').text(_t('An error occurred. Please try again later or contact us.'));
|
||||
$('#info_state').removeClass('alert-info').removeClass('alert-success').removeClass('alert-error').addClass('alert-warning');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
if (value) { $('[name="button_subscription"]').addClass('clickable'); }
|
||||
else { $('[name="button_subscription"]').removeClass('clickable'); }
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/** @odoo-module alias=mailing.PortalSubscription **/
|
||||
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
|
||||
|
||||
publicWidget.registry.MailingPortalSubscription = publicWidget.Widget.extend({
|
||||
custom_events: {
|
||||
'blocklist_add': '_onBlocklistAdd',
|
||||
'blocklist_remove': '_onBlocklistRemove',
|
||||
'feedback_sent': '_onFeedbackSent',
|
||||
'subscription_updated': '_onSubscriptionUpdated',
|
||||
},
|
||||
selector: '#o_mailing_portal_subscription',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.customerData = {...document.getElementById('o_mailing_portal_subscription').dataset};
|
||||
this.customerData.documentId = parseInt(this.customerData.documentId || 0);
|
||||
this.customerData.mailingId = parseInt(this.customerData.mailingId || 0);
|
||||
this.lastAction = this.customerData.lastAction;
|
||||
|
||||
// nodes for widgets (jquery due to widget implementation)
|
||||
this.$bl_elem = this.$('#o_mailing_subscription_blocklist');
|
||||
this.$feedback_elem = this.$('#o_mailing_subscription_feedback');
|
||||
this.$form_elem = this.$('#o_mailing_subscription_form');
|
||||
|
||||
this._attachBlocklist();
|
||||
this._attachFeedback();
|
||||
this._attachForm();
|
||||
this._updateDisplay();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_attachBlocklist: function () {
|
||||
if (this.$bl_elem.length) {
|
||||
this.blocklistWidget = new publicWidget.registry.MailingPortalSubscriptionBlocklist(
|
||||
this,
|
||||
{customerData: this.customerData}
|
||||
);
|
||||
this.blocklistWidget.attachTo(this.$bl_elem);
|
||||
}
|
||||
},
|
||||
|
||||
_attachFeedback: function () {
|
||||
if (this.$feedback_elem.length) {
|
||||
this.feedbackWidget = new publicWidget.registry.MailingPortalSubscriptionFeedback(
|
||||
this,
|
||||
{customerData: this.customerData}
|
||||
);
|
||||
this.feedbackWidget.attachTo(this.$feedback_elem);
|
||||
this.feedbackWidget._setLastAction(this.lastAction);
|
||||
}
|
||||
},
|
||||
|
||||
_attachForm: function () {
|
||||
if (this.$form_elem.length) {
|
||||
this.formWidget = new publicWidget.registry.MailingPortalSubscriptionForm(
|
||||
this,
|
||||
{customerData: this.customerData}
|
||||
);
|
||||
this.formWidget.attachTo(this.$form_elem);
|
||||
}
|
||||
},
|
||||
|
||||
_onActionDone: function (callKey) {
|
||||
this.lastAction = callKey;
|
||||
this._updateDisplay();
|
||||
},
|
||||
|
||||
_onBlocklistAdd: function (event) {
|
||||
const callKey = event.data.callKey;
|
||||
this.customerData.isBlocklisted = event.data.isBlocklisted;
|
||||
if (callKey === 'blocklist_add') {
|
||||
this.customerData.feedbackEnabled = true;
|
||||
}
|
||||
this._onActionDone(callKey);
|
||||
},
|
||||
|
||||
_onBlocklistRemove: function (event) {
|
||||
const callKey = event.data.callKey;
|
||||
this.customerData.isBlocklisted = false;
|
||||
if (callKey === 'blocklist_remove') {
|
||||
this.customerData.feedbackEnabled = false;
|
||||
}
|
||||
this._onActionDone(callKey);
|
||||
},
|
||||
|
||||
_onFeedbackSent: function (event) {
|
||||
const callKey = event.data.callKey;
|
||||
this.lastAction = callKey;
|
||||
},
|
||||
|
||||
_onSubscriptionUpdated: function (event) {
|
||||
const callKey = event.data.callKey;
|
||||
if (callKey === 'subscription_updated_optout') {
|
||||
this.customerData.feedbackEnabled = true;
|
||||
}
|
||||
else if (callKey === 'subscription_updated') {
|
||||
this.customerData.feedbackEnabled = false;
|
||||
}
|
||||
this._onActionDone(callKey);
|
||||
},
|
||||
|
||||
_updateDisplay: function () {
|
||||
if (! this.customerData.feedbackEnabled && this.$feedback_elem.length) {
|
||||
this.$feedback_elem.addClass('d-none');
|
||||
} else if (this.$feedback_elem.length) {
|
||||
this.$feedback_elem.removeClass('d-none');
|
||||
}
|
||||
if (this.formWidget) {
|
||||
this.formWidget._setBlocklisted(this.customerData.isBlocklisted);
|
||||
this.formWidget._setReadonly(this.customerData.isBlocklisted);
|
||||
}
|
||||
if (this.feedbackWidget) {
|
||||
this.feedbackWidget._setLastAction(this.lastAction);
|
||||
this.feedbackWidget._updateDisplay(true, false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default publicWidget.registry.MailingPortalSubscription;
|
||||
@@ -0,0 +1,140 @@
|
||||
/** @odoo-module alias=mailing.PortalSubscriptionBlocklist **/
|
||||
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
|
||||
|
||||
publicWidget.registry.MailingPortalSubscriptionBlocklist = publicWidget.Widget.extend({
|
||||
events: {
|
||||
'click #button_blocklist_add': '_onBlocklistAddClick',
|
||||
'click #button_blocklist_remove': '_onBlocklistRemoveClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this.customerData = options.customerData;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._updateDisplay();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/*
|
||||
* Triggers call to add current email in blocklist. Update widget internals
|
||||
* and DOM accordingly (buttons display mainly). Bubble up to let parent
|
||||
* handle returned result if necessary.
|
||||
*/
|
||||
_onBlocklistAddClick: async function (event) {
|
||||
event.preventDefault();
|
||||
return await jsonrpc(
|
||||
'/mailing/blocklist/add',
|
||||
{
|
||||
document_id: this.customerData.documentId,
|
||||
email: this.customerData.email,
|
||||
hash_token: this.customerData.hashToken,
|
||||
mailing_id: this.customerData.mailingId,
|
||||
}
|
||||
).then((result) => {
|
||||
if (result === true) {
|
||||
this.customerData.isBlocklisted = true;
|
||||
}
|
||||
this._updateDisplay();
|
||||
this._updateInfo(result === true ? 'blocklist_add' : 'error');
|
||||
this.trigger_up(
|
||||
'blocklist_add',
|
||||
{'callKey': result === true ? 'blocklist_add' : result,
|
||||
'isBlocklisted': result === true ? true: this.customerData.isBlocklisted,
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Triggers call to remove current email from blocklist. Update widget
|
||||
* internals and DOM accordingly (buttons display mainly). Bubble up to let
|
||||
* parent handle returned result if necessary.
|
||||
*/
|
||||
_onBlocklistRemoveClick: async function (event) {
|
||||
event.preventDefault();
|
||||
return await jsonrpc(
|
||||
'/mailing/blocklist/remove',
|
||||
{
|
||||
document_id: this.customerData.documentId,
|
||||
email: this.customerData.email,
|
||||
hash_token: this.customerData.hashToken,
|
||||
mailing_id: this.customerData.mailingId,
|
||||
}
|
||||
).then((result) => {
|
||||
if (result === true) {
|
||||
this.customerData.isBlocklisted = false;
|
||||
}
|
||||
this._updateDisplay();
|
||||
this._updateInfo(result === true ? 'blocklist_remove' : 'error');
|
||||
this.trigger_up(
|
||||
'blocklist_remove',
|
||||
{'callKey': result === true ? 'blocklist_remove' : result,
|
||||
'isBlocklisted': result === true ? false: this.customerData.isBlocklisted,
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Display buttons and info according to current state. Removing from blocklist
|
||||
* is always available when being blocklisted. Adding in blocklist is available
|
||||
* when not being blocklisted, if the action is possible (valid email mainly)
|
||||
* and if the option is activated.
|
||||
*/
|
||||
_updateDisplay: function () {
|
||||
const buttonAddNode = document.getElementById('button_blocklist_add');
|
||||
const buttonRemoveNode = document.getElementById('button_blocklist_remove');
|
||||
if (this.customerData.blocklistEnabled && this.customerData.blocklistPossible && !this.customerData.isBlocklisted) {
|
||||
buttonAddNode.classList.remove('d-none');
|
||||
} else {
|
||||
buttonAddNode.classList.add('d-none');
|
||||
}
|
||||
if (this.customerData.isBlocklisted) {
|
||||
buttonRemoveNode.classList.remove('d-none');
|
||||
} else {
|
||||
buttonRemoveNode.classList.add('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Display feedback (visual tips) to the user concerning the last done action.
|
||||
*/
|
||||
_updateInfo: function (infoKey) {
|
||||
const updateInfo = document.getElementById('o_mailing_subscription_update_info');
|
||||
if (infoKey !== undefined) {
|
||||
const infoContent = renderToElement(
|
||||
"mass_mailing.portal.blocklist_update_info",
|
||||
{
|
||||
infoKey: infoKey,
|
||||
}
|
||||
);
|
||||
updateInfo.innerHTML = infoContent.innerHTML;
|
||||
if (['blocklist_add', 'blocklist_remove'].includes(infoKey)) {
|
||||
updateInfo.classList.add('text-success');
|
||||
updateInfo.classList.remove('text-danger');
|
||||
}
|
||||
else {
|
||||
updateInfo.classList.add('text-danger');
|
||||
updateInfo.classList.remove('text-success');
|
||||
}
|
||||
updateInfo.classList.remove('d-none');
|
||||
}
|
||||
else {
|
||||
updateInfo.classList.add('d-none');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default publicWidget.registry.MailingPortalSubscriptionBlocklist;
|
||||
@@ -0,0 +1,149 @@
|
||||
/** @odoo-module alias=mailing.PortalSubscriptionFeedback **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
|
||||
|
||||
publicWidget.registry.MailingPortalSubscriptionFeedback = publicWidget.Widget.extend({
|
||||
events: {
|
||||
'click #button_feedback': '_onFeedbackClick',
|
||||
'click .o_mailing_subscription_opt_out_reason': '_onOptOutReasonClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this.customerData = options.customerData;
|
||||
this.allowFeedback = false;
|
||||
this.lastAction = false;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this._updateDisplay(true, false);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/*
|
||||
* Triggers call to give a feedback about current subscription update.
|
||||
* Bubble up to let parent handle returned result if necessary.
|
||||
*/
|
||||
_onFeedbackClick: async function (event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(document.querySelector('div#o_mailing_subscription_feedback form'));
|
||||
const optoutReasonId = parseInt(formData.get('opt_out_reason_id'));
|
||||
return await jsonrpc(
|
||||
'/mailing/feedback',
|
||||
{
|
||||
csrf_token: formData.get('csrf_token'),
|
||||
document_id: this.customerData.documentId,
|
||||
email: this.customerData.email,
|
||||
feedback: formData.get('feedback'),
|
||||
hash_token: this.customerData.hashToken,
|
||||
last_action: this.lastAction,
|
||||
mailing_id: this.customerData.mailingId,
|
||||
opt_out_reason_id: optoutReasonId,
|
||||
}
|
||||
).then((result) => {
|
||||
if (result === true) {
|
||||
this._updateDisplay(false, true);
|
||||
this._updateInfo('feedback_sent');
|
||||
}
|
||||
else {
|
||||
this._updateDisplay(false, false);
|
||||
this._updateInfo(result);
|
||||
}
|
||||
this.trigger_up(
|
||||
'feedback_sent',
|
||||
{'callKey': result === true ? 'feedback_sent' : result},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Toggle feedback textarea display based on reason configuration
|
||||
*/
|
||||
_onOptOutReasonClick: function (event) {
|
||||
this.allowFeedback = $(event.currentTarget).data('isFeedback');
|
||||
this._updateDisplay()
|
||||
},
|
||||
|
||||
/*
|
||||
* Set last done action, which triggers some update in the feedback form
|
||||
* allowing to contextualize the explanation given to the customer.
|
||||
*/
|
||||
_setLastAction: function (lastAction) {
|
||||
this.lastAction = lastAction;
|
||||
if (this.lastAction === 'blocklist_add') {
|
||||
document.querySelector('div#o_mailing_subscription_feedback p').innerHTML = _t(
|
||||
'Please let us know why you want to be in our block list.'
|
||||
);
|
||||
}
|
||||
else {
|
||||
document.querySelector('div#o_mailing_subscription_feedback p').innerHTML = _t(
|
||||
'Please let us know why you updated your subscription.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Update display after option changes, notably feedback textarea not being
|
||||
* always accessible. Opt-out reasons are displayed only if user opt-outed
|
||||
* from some mailing lists or added their email in the blocklist.
|
||||
*/
|
||||
_updateDisplay: function (cleanFeedback, setReadonly) {
|
||||
const feedbackArea = document.querySelector('div#o_mailing_subscription_feedback textarea');
|
||||
const feedbackButton = document.getElementById('button_feedback');
|
||||
const feedbackReasons = document.querySelectorAll('div#o_mailing_subscription_feedback input');
|
||||
const feedbackInfo = document.getElementById('o_mailing_subscription_feedback_info');
|
||||
if (this.allowFeedback) {
|
||||
feedbackArea.classList.remove('d-none');
|
||||
} else {
|
||||
feedbackArea.classList.add('d-none');
|
||||
}
|
||||
if (setReadonly) {
|
||||
feedbackArea.setAttribute('disabled', 'disabled');
|
||||
feedbackButton.setAttribute('disabled', 'disabled');
|
||||
feedbackReasons.forEach(node => node.setAttribute('disabled', 'disabled'));
|
||||
}
|
||||
else {
|
||||
feedbackArea.removeAttribute('disabled');
|
||||
feedbackButton.removeAttribute('disabled');
|
||||
feedbackReasons.forEach(node => node.removeAttribute('disabled'));
|
||||
}
|
||||
if (cleanFeedback) {
|
||||
feedbackArea.value = '';
|
||||
feedbackInfo.innerHTML = "";
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Display feedback (visual tips) to the user concerning the last done action.
|
||||
*/
|
||||
_updateInfo: function (infoKey) {
|
||||
const feedbackInfo = document.getElementById('o_mailing_subscription_feedback_info');
|
||||
if (infoKey !== undefined) {
|
||||
const infoContent = renderToElement(
|
||||
"mass_mailing.portal.feedback_update_info",
|
||||
{
|
||||
infoKey: infoKey,
|
||||
}
|
||||
);
|
||||
feedbackInfo.innerHTML = infoContent.innerHTML;
|
||||
feedbackInfo.classList.add(infoKey === 'feedback_sent' ? 'text-success' : 'text-danger');
|
||||
feedbackInfo.classList.remove('d-none', infoKey === 'feedback_sent' ? 'text-danger': 'text-success');
|
||||
}
|
||||
else {
|
||||
feedbackInfo.classList.add('d-none');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default publicWidget.registry.MailingPortalSubscriptionFeedback;
|
||||
@@ -0,0 +1,177 @@
|
||||
/** @odoo-module alias=mailing.PortalSubscriptionForm **/
|
||||
|
||||
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||
import { renderToFragment } from "@web/core/utils/render";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
|
||||
|
||||
publicWidget.registry.MailingPortalSubscriptionForm = publicWidget.Widget.extend({
|
||||
events: {
|
||||
'click #button_form_send': '_onFormSend',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this.customerData = options.customerData;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Parse start values of mailing lists subscriptions based on generated DOM
|
||||
* from server. Done here to avoid having to generate it server-side and
|
||||
* propagating it through various layers.
|
||||
*/
|
||||
start: function () {
|
||||
const def = this._super.apply(this, arguments);
|
||||
this.listInfo = [...document.querySelectorAll('#o_mailing_subscription_form_manage input')].map(
|
||||
node => {
|
||||
const listInfo = {
|
||||
id: parseInt(node.getAttribute('value')),
|
||||
member: node.dataset.member === '1',
|
||||
name: node.getAttribute('title'),
|
||||
opt_out: node.getAttribute('checked') !== 'checked',
|
||||
};
|
||||
return listInfo;
|
||||
}
|
||||
);
|
||||
return def;
|
||||
},
|
||||
|
||||
/*
|
||||
* Triggers call to update list subscriptions. Bubble up to let parent
|
||||
* handle returned result if necessary. RPC call returns number of optouted
|
||||
* lists, used by parent widget notably to know which feedback to ask.
|
||||
*/
|
||||
_onFormSend: async function (event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(document.querySelector('div#o_mailing_subscription_form form'));
|
||||
const mailingListOptinIds = formData.getAll('mailing_list_ids').map(id_str => parseInt(id_str));
|
||||
return await jsonrpc(
|
||||
'/mailing/list/update',
|
||||
{
|
||||
csrf_token: formData.get('csrf_token'),
|
||||
document_id: this.customerData.documentId,
|
||||
email: this.customerData.email,
|
||||
hash_token: this.customerData.hashToken,
|
||||
lists_optin_ids: mailingListOptinIds,
|
||||
mailing_id: this.customerData.mailingId,
|
||||
}
|
||||
).then((result) => {
|
||||
const has_error = ['error', 'unauthorized'].includes(result);
|
||||
let callKey;
|
||||
if (has_error) {
|
||||
callKey = 'error';
|
||||
}
|
||||
else {
|
||||
callKey = (parseInt(result) > 0) ? 'subscription_updated_optout': 'subscription_updated';
|
||||
this._updateDisplay(mailingListOptinIds);
|
||||
}
|
||||
this._updateInfo(has_error ? 'error' : 'subscription_updated');
|
||||
this.trigger_up(
|
||||
'subscription_updated',
|
||||
{'callKey': callKey},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set form elements as hidden / displayed, as this form contains either an
|
||||
* informational text when being blocklisted, either the complete form to
|
||||
* manage their subscription.
|
||||
* @private
|
||||
*/
|
||||
_setBlocklisted: function (isBlocklisted) {
|
||||
if (isBlocklisted) {
|
||||
document.getElementById('o_mailing_subscription_form_blocklisted').classList.remove('d-none');
|
||||
document.getElementById('o_mailing_subscription_form_manage').classList.add('d-none');
|
||||
}
|
||||
else {
|
||||
document.getElementById('o_mailing_subscription_form_blocklisted').classList.add('d-none');
|
||||
document.getElementById('o_mailing_subscription_form_manage').classList.remove('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set form elements as readonly, e.g. when blocklisted email take precedence
|
||||
* over subscription update.
|
||||
* @private
|
||||
*/
|
||||
_setReadonly: function (isReadonly) {
|
||||
const formInputNodes = document.querySelectorAll('#o_mailing_subscription_form_manage input');
|
||||
const formButtonNode = document.getElementById('button_form_send');
|
||||
if (isReadonly) {
|
||||
formInputNodes.forEach(node => {node.setAttribute('disabled', 'disabled')});
|
||||
formButtonNode.setAttribute('disabled', 'disabled');
|
||||
formButtonNode.classList.add('d-none');
|
||||
} else {
|
||||
formInputNodes.forEach(node => {node.removeAttribute('disabled')});
|
||||
formButtonNode.removeAttribute('disabled');
|
||||
formButtonNode.classList.remove('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Update display after subscription, notably to update mailing list subscription
|
||||
* status. We simply update opt_out status based on the ID being present in the
|
||||
* newly-selected opt-in mailing lists, then rerender the inputs.
|
||||
*/
|
||||
_updateDisplay: function (listOptinIds) {
|
||||
/* update internal status*/
|
||||
this.listInfo.forEach(
|
||||
(listItem) => {
|
||||
listItem.member = listItem.member || listOptinIds.includes(listItem.id);
|
||||
listItem.opt_out = !listOptinIds.includes(listItem.id);
|
||||
}
|
||||
);
|
||||
|
||||
/* update form of lists for update */
|
||||
const formContent = renderToFragment(
|
||||
"mass_mailing.portal.list_form_content",
|
||||
{
|
||||
listsMember: this.listInfo.filter(item => item.member === true),
|
||||
listsProposal: this.listInfo.filter(item => item.member === false),
|
||||
}
|
||||
);
|
||||
const manageForm = document.getElementById('o_mailing_subscription_form_manage');
|
||||
/*manageForm.innerHTML = formContent.innerHTML;*/
|
||||
manageForm.replaceChildren(formContent);
|
||||
|
||||
/* update readonly display of customer's lists */
|
||||
const formReadonlyContent = renderToFragment(
|
||||
"mass_mailing.portal.list_form_content_readonly",
|
||||
{
|
||||
listsOptin: this.listInfo.filter(item => item.opt_out === false),
|
||||
}
|
||||
);
|
||||
const readonlyForm = document.getElementById('o_mailing_subscription_form_blocklisted');
|
||||
/*readonlyForm.innerHTML = formReadonlyContent.innerHTML;*/
|
||||
readonlyForm.replaceChildren(formReadonlyContent);
|
||||
},
|
||||
|
||||
/*
|
||||
* Display feedback (visual tips) to the user concerning the last done action.
|
||||
*/
|
||||
_updateInfo: function (infoKey) {
|
||||
const updateInfo = document.getElementById('o_mailing_subscription_update_info');
|
||||
if (infoKey !== undefined) {
|
||||
const infoContent = renderToFragment(
|
||||
"mass_mailing.portal.list_form_update_info",
|
||||
{
|
||||
infoKey: infoKey,
|
||||
}
|
||||
);
|
||||
/*updateInfo.innerHTML = infoContent.innerHTML;*/
|
||||
updateInfo.replaceChildren(infoContent);
|
||||
updateInfo.classList.add(infoKey === 'error' ? 'text-danger': 'text-success');
|
||||
updateInfo.classList.remove('d-none');
|
||||
}
|
||||
else {
|
||||
updateInfo.classList.add('d-none');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default publicWidget.registry.MailingPortalSubscriptionForm;
|
||||
@@ -8,7 +8,7 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.o_mailing_portal_list_unsubscribed {
|
||||
.o_mailing_list_unsubscribed {
|
||||
margin-left: 20px;
|
||||
color: #005326;
|
||||
font-size: 90%;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mass_mailing.portal.blocklist_update_info">
|
||||
<span>
|
||||
<t t-if="infoKey == 'blocklist_add'">
|
||||
<i class="fa fa-check me-1"/>
|
||||
<span>Email added to our blocklist</span>
|
||||
</t>
|
||||
<t t-elif="infoKey == 'blocklist_remove'">
|
||||
<i class="fa fa-check me-1"/>
|
||||
<span>Email removed from our blocklist</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
<span>An error occurred. Please retry later.</span>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mass_mailing.portal.feedback_update_info">
|
||||
<span>
|
||||
<t t-if="infoKey == 'feedback_sent'">
|
||||
<i class="fa fa-check me-1"/>
|
||||
<span>Sent. Thanks you for your feedback!</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
<span>An error occurred. Please retry later or contact us.</span>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<!-- Re-render lists form as no reload is done on portal page -->
|
||||
<t t-name="mass_mailing.portal.list_form_content">
|
||||
<p t-if="listsMember.length">
|
||||
Choose your mailing subscriptions
|
||||
</p>
|
||||
<ul id="o_mailing_subscription_form_lists"
|
||||
t-if="listsMember.length"
|
||||
class="list-group">
|
||||
<li t-foreach="listsMember"
|
||||
t-as="mailing_list"
|
||||
t-key="mailing_list_key"
|
||||
class="list-group-item d-flex align-items-center">
|
||||
<input type="checkbox"
|
||||
name="mailing_list_ids"
|
||||
t-att-data-member="1"
|
||||
t-att-value="mailing_list['id']"
|
||||
t-att-checked="mailing_list['opt_out'] ? undefined : 'checked'"
|
||||
t-att-title="mailing_list['name']"/>
|
||||
<span class="ms-1" t-out="mailing_list['name']"/>
|
||||
<span class="o_mailing_list_unsubscribed ms-auto">
|
||||
<t t-if="mailing_list['opt_out']">
|
||||
Not subscribed
|
||||
</t>
|
||||
<t t-else="">
|
||||
Subscribed
|
||||
</t>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p t-if="listsProposal.length">
|
||||
You may also be interested in
|
||||
</p>
|
||||
<ul id="o_mailing_subscription_form_lists_additional"
|
||||
t-if="listsProposal.length"
|
||||
class="list-group">
|
||||
<li t-foreach="listsProposal"
|
||||
t-as="mailing_list"
|
||||
t-key="mailing_list_key"
|
||||
class="list-group-item">
|
||||
<input type="checkbox"
|
||||
name="mailing_list_ids"
|
||||
t-att-value="mailing_list['id']"
|
||||
t-att-title="mailing_list['name']"/>
|
||||
<span class="ms-1" t-out="mailing_list['name']"/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<!-- Re-render lists (in readonly mode) as no reload is done on portal page -->
|
||||
<t t-name="mass_mailing.portal.list_form_content_readonly">
|
||||
<p>
|
||||
Your email is currently <strong>in our block list</strong>.
|
||||
<t t-if="listsOptin.length">
|
||||
You will not receive any news from those mailing lists you are a member of:
|
||||
</t>
|
||||
<t t-else="">
|
||||
You will not hear from us anymore.
|
||||
</t>
|
||||
</p>
|
||||
<ul class="list-group mb-4"
|
||||
t-if="listsOptin.length">
|
||||
<t t-foreach="listsOptin"
|
||||
t-as="mailing_list"
|
||||
t-key="mailing_list_key">
|
||||
<li class="list-group-item d-flex align-items-center">
|
||||
<strong t-out="mailing_list['name']"/>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
<!-- Post action informative span -->
|
||||
<t t-name="mass_mailing.portal.list_form_update_info">
|
||||
<t t-if="infoKey == 'subscription_updated'">
|
||||
<i class="fa fa-check me-1"/>
|
||||
<span>Membership updated</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
<span>An error occurred. Please retry later.</span>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,110 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/*
|
||||
* Tour: unsubscribe from a mailing done on documents (aka not on contacts or
|
||||
* mailing lists). We assume email is not member of any mailing list in this test.
|
||||
*/
|
||||
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document', {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
content: "Confirmation unsubscribe is done",
|
||||
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')",
|
||||
}, {
|
||||
content: "No warning should be displayed",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted:not(:has(p:contains('You will not receive any news from those mailing lists you are a member of')))",
|
||||
}, {
|
||||
contnet: "Warning will not receive anything anymore",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not hear from us anymore.')",
|
||||
}, {
|
||||
content: "Feedback textarea not displayed (see data)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose 'Other' reason",
|
||||
trigger: "fieldset label:contains('Other')",
|
||||
}, {
|
||||
content: "This should display the Feedback area",
|
||||
trigger: "div#o_mailing_portal_subscription textarea",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Write feedback reason",
|
||||
trigger: "textarea[name='feedback']",
|
||||
run: "text My feedback",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
}, {
|
||||
content: "Revert exclusion list",
|
||||
trigger: "div#button_blocklist_remove",
|
||||
}, {
|
||||
content: "Confirmation exclusion list is removed",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
|
||||
}, {
|
||||
content: "Now exclude me (again)",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
isCheck: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Tour: unsubscribe from a mailing done on documents (aka not on contacts or
|
||||
* mailing lists). We assume email is member of mailing lists in this test.
|
||||
*/
|
||||
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document_with_lists', {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
content: "Confirmation unsubscribe is done",
|
||||
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')",
|
||||
}, {
|
||||
content: "Display warning about mailing lists",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
|
||||
}, {
|
||||
content: "Warning should contain reference to memberships",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List1')",
|
||||
}, {
|
||||
content: "Feedback textarea not displayed (see data)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose 'Other' reason",
|
||||
trigger: "fieldset label:contains('Other')",
|
||||
}, {
|
||||
content: "This should display the Feedback area",
|
||||
trigger: "div#o_mailing_portal_subscription textarea",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Write feedback reason",
|
||||
trigger: "textarea[name='feedback']",
|
||||
run: "text My feedback",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
}, {
|
||||
content: "Revert exclusion list",
|
||||
trigger: "div#button_blocklist_remove",
|
||||
}, {
|
||||
content: "Confirmation exclusion list is removed",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
|
||||
}, {
|
||||
content: "Now exclude me (again)",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
isCheck: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/*
|
||||
* Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag
|
||||
* instead of directly blocking emails).
|
||||
*/
|
||||
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list', {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
content: "Confirmation unsubscribe is done",
|
||||
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')",
|
||||
}, {
|
||||
content: "Feedback textarea not displayed (see data)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose 'Other' reason",
|
||||
trigger: "fieldset label:contains('Other')",
|
||||
}, {
|
||||
content: "Write feedback reason",
|
||||
trigger: "textarea[name='feedback']",
|
||||
run: "text My feedback",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
}, {
|
||||
content: "Now exclude me",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
isCheck: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag
|
||||
* instead of directly blocking emails), then play with list subscriptions and
|
||||
* blocklist addition / removal. This is mainly an extended version of the tour
|
||||
* hereabove, easing debug and splitting checks.
|
||||
*/
|
||||
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_list_with_update', {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
content: "Confirmation unsubscribe is done",
|
||||
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')",
|
||||
}, {
|
||||
content: "List1 is present, just opt-outed",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List1') span:contains('Not subscribed')",
|
||||
}, {
|
||||
content: "List3 is present, opt-outed (test starting data)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Not subscribed')",
|
||||
}, {
|
||||
content: "List2 is proposed (not member -> proposal to join)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional li.list-group-item:contains('List2')",
|
||||
}, {
|
||||
content: "List4 is not proposed (not member but not private)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional:not(:has(li.list-group-item:contains('List4')))",
|
||||
}, {
|
||||
content: "Feedback textarea not displayed (see data)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose 'Other' reason",
|
||||
trigger: "fieldset label:contains('Other')",
|
||||
}, {
|
||||
content: "Write feedback reason",
|
||||
trigger: "textarea[name='feedback']",
|
||||
run: "text My feedback",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
}, {
|
||||
content: "Now exclude me",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
}, {
|
||||
content: "This should disable the 'Update my subscriptions' (Apply changes) button",
|
||||
trigger: "div#o_mailing_subscription_blocklist:not(button#button_form_send)",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Revert exclusion list",
|
||||
trigger: "div#button_blocklist_remove",
|
||||
}, {
|
||||
content: "Confirmation exclusion list is removed",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
|
||||
}, {
|
||||
content: "'Update my subscriptions' button usable again",
|
||||
trigger: "button#button_form_send:not([disabled])",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Choose the mailing list 3 to come back",
|
||||
trigger: "ul#o_mailing_subscription_form_lists input[title='List3']",
|
||||
}, {
|
||||
content: "Add list 2",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional input[title='List2']",
|
||||
}, {
|
||||
content: "Update subscription",
|
||||
trigger: "button#button_form_send",
|
||||
}, {
|
||||
content: "Confirmation changes are done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Membership updated')",
|
||||
}, {
|
||||
content: "List 3 is noted as subscribed again",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Subscribed')",
|
||||
}, {
|
||||
content: "List 2 has joined the subscriptions",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List2') span:contains('Subscribed')",
|
||||
}, {
|
||||
content: "No list in proposals",
|
||||
trigger: "div#o_mailing_subscription_form_manage:not(:has(ul#o_mailing_subscription_form_lists_additional))",
|
||||
}, {
|
||||
content: "Feedback area is not displayed (nothing opt-out or no blocklist done, no feedback required)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
extra_trigger: "div#o_mailing_portal_subscription:not(fieldset)",
|
||||
}, {
|
||||
content: "Now exclude me (again)",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
}, {
|
||||
content: "Should display warning about mailing lists",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
|
||||
}, {
|
||||
content: "Warning should contain reference to memberships",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List2')",
|
||||
extra_trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List3')",
|
||||
}, {
|
||||
content: "Give a reason for blocklist (first one)",
|
||||
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
isCheck: true,
|
||||
}
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/*
|
||||
* Tour: use 'my' portal page of mailing to manage mailing lists subscription
|
||||
* as well as manage blocklist (add / remove my own email from block list).
|
||||
*/
|
||||
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_my', {
|
||||
test: true,
|
||||
steps: () => [
|
||||
{
|
||||
content: "List1 is present, opt-in member",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List1') span:contains('Subscribed')",
|
||||
}, {
|
||||
content: "List3 is present, opt-outed (test starting data)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Not subscribed')",
|
||||
}, {
|
||||
content: "List2 is proposed (not member -> proposal to join)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional li.list-group-item:contains('List2')",
|
||||
}, {
|
||||
content: "List4 is not proposed (not member but not private)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional:not(:has(li.list-group-item:contains('List4')))",
|
||||
}, {
|
||||
content: "Feedback area is not displayed (nothing done, no feedback required)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
extra_trigger: "div#o_mailing_portal_subscription:not(fieldset)",
|
||||
}, {
|
||||
content: "List3: come back (choose to opt-in instead of opt-out)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists input[title='List3']",
|
||||
}, {
|
||||
content: "List2: join (opt-in, not already member)",
|
||||
trigger: "ul#o_mailing_subscription_form_lists_additional input[title='List2']",
|
||||
}, {
|
||||
content: "List1: opt-out",
|
||||
trigger: "ul#o_mailing_subscription_form_lists input[title='List1']",
|
||||
}, {
|
||||
content: "Update subscription",
|
||||
trigger: "button#button_form_send",
|
||||
}, {
|
||||
content: "Confirmation changes are done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Membership updated')",
|
||||
}, {
|
||||
content: "Should make feedback reasons choice appear (feedback still not displayed, linked to reasons)",
|
||||
trigger: "div#o_mailing_portal_subscription fieldset",
|
||||
extra_trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose first reason, which should not display feedback (see data)",
|
||||
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
|
||||
}, {
|
||||
content: "Feedback textarea not displayed (see data)",
|
||||
trigger: "div#o_mailing_portal_subscription:not(textarea)",
|
||||
}, {
|
||||
content: "Choose 'Other' reason",
|
||||
trigger: "fieldset label:contains('Other')",
|
||||
}, {
|
||||
content: "This should display the Feedback area",
|
||||
trigger: "div#o_mailing_portal_subscription textarea",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Write feedback reason",
|
||||
trigger: "textarea[name='feedback']",
|
||||
run: "text My feedback",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
}, {
|
||||
content: "Once sent feedback area is readonly",
|
||||
trigger: "fieldset input.o_mailing_subscription_opt_out_reason[disabled]",
|
||||
extra_trigger: "textarea[disabled]",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Now exclude me",
|
||||
trigger: "div#button_blocklist_add",
|
||||
}, {
|
||||
content: "Confirmation exclusion is done",
|
||||
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
|
||||
}, {
|
||||
content: "This should disable the 'Update my subscriptions' (Apply changes) button",
|
||||
trigger: "div#o_mailing_subscription_blocklist:not(button#button_form_send)",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "This should enabled Feedback again",
|
||||
trigger: "div#o_mailing_portal_subscription textarea",
|
||||
isCheck: true,
|
||||
}, {
|
||||
content: "Display warning about mailing lists",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
|
||||
}, {
|
||||
content: "Warning should contain reference to memberships",
|
||||
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List2')",
|
||||
extra_trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List3')",
|
||||
}, {
|
||||
content: "Give a reason for blocklist (first one)",
|
||||
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
|
||||
}, {
|
||||
content: "Hit Send",
|
||||
trigger: "button#button_feedback",
|
||||
}, {
|
||||
content: "Confirmation feedback is sent",
|
||||
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
|
||||
isCheck: true,
|
||||
}
|
||||
],
|
||||
});
|
||||
@@ -291,23 +291,33 @@ class MassMailCommon(MailCommon, MassMailCase):
|
||||
def _create_mailing_list(cls):
|
||||
""" Shortcut to create mailing lists. Currently hardcoded, maybe evolve
|
||||
in a near future. """
|
||||
cls.mailing_list_1 = cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'List1',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
|
||||
(0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
]
|
||||
})
|
||||
cls.mailing_list_2 = cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'List2',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
]
|
||||
})
|
||||
cls.mailing_list_1, cls.mailing_list_2, cls.mailing_list_3, cls.mailing_list_4 = cls.env['mailing.list'].with_context(cls._test_context).create([
|
||||
{
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
|
||||
(0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
],
|
||||
'name': 'List1',
|
||||
}, {
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
],
|
||||
'name': 'List2',
|
||||
}, {
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
|
||||
],
|
||||
'name': 'List3',
|
||||
}, {
|
||||
'is_public': False,
|
||||
'name': 'List4',
|
||||
}
|
||||
])
|
||||
cls.mailing_list_3.subscription_ids[0].opt_out = True
|
||||
|
||||
@classmethod
|
||||
def _create_mailing_list_of_x_contacts(cls, contacts_nbr):
|
||||
@@ -316,7 +326,10 @@ class MassMailCommon(MailCommon, MassMailCase):
|
||||
return cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'Test List',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Contact %s' % i, 'email': 'contact%s@example.com' % i})
|
||||
for i in range(contacts_nbr)
|
||||
(0, 0, {
|
||||
'name': f'Contact %{idx}',
|
||||
'email': f'contact%{idx}@example.com'
|
||||
})
|
||||
for idx in range(contacts_nbr)
|
||||
],
|
||||
})
|
||||
|
||||
@@ -2,32 +2,53 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from freezegun import freeze_time
|
||||
from markupsafe import Markup
|
||||
from requests import Session, PreparedRequest, Response
|
||||
|
||||
import datetime
|
||||
import werkzeug
|
||||
|
||||
from odoo import tools
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from requests import Session, PreparedRequest, Response
|
||||
|
||||
from odoo.tests.common import HttpCase
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('link_tracker')
|
||||
class TestMailingControllers(MassMailCommon, HttpCase):
|
||||
class TestMailingControllersCommon(MassMailCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingControllers, cls).setUpClass()
|
||||
super(TestMailingControllersCommon, cls).setUpClass()
|
||||
|
||||
# cleanup lists
|
||||
cls.env['mailing.list'].search([]).unlink()
|
||||
|
||||
cls._create_mailing_list()
|
||||
cls.test_mailing = cls.env['mailing.mailing'].create({
|
||||
cls.test_mailing_on_contacts = cls.env['mailing.mailing'].create({
|
||||
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
|
||||
'mailing_domain': [],
|
||||
'mailing_model_id': cls.env['ir.model']._get_id('mailing.contact'),
|
||||
'mailing_type': 'mail',
|
||||
'name': 'TestMailing on Contacts',
|
||||
'subject': 'TestMailing on Contacts',
|
||||
})
|
||||
cls.test_mailing_on_documents = cls.env['mailing.mailing'].create({
|
||||
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
|
||||
'mailing_domain': [],
|
||||
'mailing_model_id': cls.env['ir.model']._get_id('res.partner'),
|
||||
'mailing_type': 'mail',
|
||||
'name': 'TestMailing on Documents',
|
||||
'subject': 'TestMailing on Documents',
|
||||
})
|
||||
cls.test_mailing_on_lists = cls.env['mailing.mailing'].create({
|
||||
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
|
||||
'contact_list_ids': [(4, cls.mailing_list_1.id), (4, cls.mailing_list_2.id)],
|
||||
'mailing_model_id': cls.env['ir.model']._get('mailing.list').id,
|
||||
'mailing_model_id': cls.env['ir.model']._get_id('mailing.list'),
|
||||
'mailing_type': 'mail',
|
||||
'name': 'TestMailing',
|
||||
'name': 'TestMailing on Lists',
|
||||
'reply_to': cls.email_reply_to,
|
||||
'subject': 'Test',
|
||||
'subject': 'TestMailing on Lists',
|
||||
})
|
||||
|
||||
cls.test_contact = cls.mailing_list_1.contact_ids[0]
|
||||
@@ -43,10 +64,569 @@ class TestMailingControllers(MassMailCommon, HttpCase):
|
||||
return r
|
||||
return super()._request_handler(s, r, **kw)
|
||||
|
||||
|
||||
@tagged('mailing_portal', 'post_install', '-at_install')
|
||||
class TestMailingControllers(TestMailingControllersCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_email = tools.formataddr(('Déboulonneur', '<fleurus@example.com>'))
|
||||
cls.test_email_normalized = 'fleurus@example.com'
|
||||
|
||||
def test_assert_initial_values(self):
|
||||
""" Ensure test base data to ease test understanding. Globally test_email
|
||||
is member of 2 mailing public lists. """
|
||||
memberships = self.env['mailing.subscription'].search([
|
||||
('contact_id.email_normalized', '=', self.test_email_normalized)]
|
||||
)
|
||||
self.assertEqual(memberships.list_id, self.mailing_list_1 + self.mailing_list_3)
|
||||
self.assertEqual(memberships.mapped('opt_out'), [False, True])
|
||||
|
||||
contact_l1 = self.mailing_list_1.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
self.assertTrue(contact_l1)
|
||||
self.assertFalse(contact_l1.is_blacklisted)
|
||||
self.assertFalse(contact_l1.message_ids)
|
||||
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l1
|
||||
)
|
||||
self.assertTrue(subscription_l1)
|
||||
self.assertFalse(subscription_l1.is_blacklisted)
|
||||
self.assertFalse(subscription_l1.opt_out)
|
||||
self.assertFalse(subscription_l1.opt_out_datetime)
|
||||
|
||||
contact_l2 = self.mailing_list_2.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
self.assertFalse(contact_l2)
|
||||
|
||||
contact_l3 = self.mailing_list_3.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
self.assertTrue(contact_l3)
|
||||
self.assertTrue(contact_l3 != contact_l1)
|
||||
self.assertFalse(contact_l3.is_blacklisted)
|
||||
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l3
|
||||
)
|
||||
self.assertFalse(subscription_l3.is_blacklisted)
|
||||
self.assertTrue(subscription_l3.opt_out)
|
||||
|
||||
contact_l4 = self.mailing_list_4.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
self.assertFalse(contact_l4)
|
||||
|
||||
self.assertFalse(self.env['mail.blacklist'].search([('email', '=', self.test_email_normalized)]))
|
||||
|
||||
@mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
|
||||
def test_mailing_report_unsubscribe(self):
|
||||
""" Test deactivation of mailing report sending. It requires usage of
|
||||
a hash token. """
|
||||
test_mailing = self.test_mailing_on_lists.with_env(self.env)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'mass_mailing.mass_mailing_reports', True
|
||||
)
|
||||
hash_token = test_mailing._generate_mailing_report_token(self.user_marketing.id)
|
||||
self.authenticate('user_marketing', 'user_marketing')
|
||||
|
||||
# TEST: various invalid cases
|
||||
for test_user_id, test_token, error_code in [
|
||||
(self.user_marketing.id, '', 400), # no token
|
||||
(self.user_marketing.id, 'zboobs', 418), # invalid token
|
||||
(self.env.uid, hash_token, 418), # invalid credentials
|
||||
]:
|
||||
with self.subTest(test_user_id=test_user_id, test_token=test_token):
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/report/unsubscribe?user_id={test_user_id}&token={test_token}',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, error_code)
|
||||
self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
|
||||
|
||||
# TEST: not mailing user
|
||||
self.user_marketing.write({
|
||||
'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 418)
|
||||
self.assertTrue(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
|
||||
|
||||
# TEST: finally valid call
|
||||
self.user_marketing.write({
|
||||
'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/report/unsubscribe?user_id={self.user_marketing.id}&token={hash_token}',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertFalse(self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mass_mailing_reports'))
|
||||
|
||||
def test_mailing_unsubscribe_from_document_tour(self):
|
||||
""" Test portal unsubscribe on mailings performed on documents (not
|
||||
mailing lists or contacts). Primary effect is to automatically exclude
|
||||
the email (see tour).
|
||||
|
||||
Two tests are performed (with and without existing list subscriptions)
|
||||
as it triggers the display of the mailing list part of the UI.
|
||||
|
||||
Tour effects
|
||||
* unsubscribe from mailing based on a document = blocklist;
|
||||
* add feedback (block list): Other reason, with 'My feedback' feedback;
|
||||
* remove email from exclusion list;
|
||||
* re-add email to exclusion list;
|
||||
"""
|
||||
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
|
||||
test_mailing = self.test_mailing_on_documents.with_env(self.env)
|
||||
test_feedback = "My feedback"
|
||||
|
||||
for test_email, tour_name in [
|
||||
('"Not Déboulonneur" <not.fleurus@example.com>', 'mailing_portal_unsubscribe_from_document'),
|
||||
(self.test_email, 'mailing_portal_unsubscribe_from_document_with_lists'),
|
||||
]:
|
||||
with self.subTest(test_email=test_email, tour_name=tour_name):
|
||||
test_partner = self.env['res.partner'].create({
|
||||
'email': test_email,
|
||||
'name': 'Test Déboulonneur'
|
||||
})
|
||||
self.assertFalse(test_partner.is_blacklisted)
|
||||
previous_messages = test_partner.message_ids
|
||||
test_email_normalized = tools.email_normalize(test_email)
|
||||
|
||||
# launch unsubscription tour
|
||||
hash_token = test_mailing._generate_mailing_recipient_token(test_partner.id, test_partner.email_normalized)
|
||||
with freeze_time(self._reference_now):
|
||||
self.start_tour(
|
||||
f"/mailing/{test_mailing.id}/unsubscribe?email={test_partner.email_normalized}&document_id={test_partner.id}&hash_token={hash_token}",
|
||||
tour_name,
|
||||
login=None,
|
||||
)
|
||||
|
||||
# status update check
|
||||
self.assertTrue(test_partner.is_blacklisted)
|
||||
|
||||
# partner (document): new message for blocklist addition with feedback
|
||||
self.assertEqual(len(test_partner.message_ids), len(previous_messages) + 1)
|
||||
msg_fb = test_partner.message_ids[0]
|
||||
self.assertEqual(
|
||||
msg_fb.body,
|
||||
Markup(f'<p>Feedback from {test_email_normalized}<br>{test_feedback}</p>')
|
||||
)
|
||||
|
||||
# posted messages on exclusion list record: activated, feedback, deactivated, activated again
|
||||
bl_record = self.env['mail.blacklist'].search([('email', '=', test_partner.email_normalized)])
|
||||
self.assertEqual(len(bl_record.message_ids), 5)
|
||||
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[-1])
|
||||
msg_bl2, msg_unbl, msg_fb, msg_bl, msg_create = bl_record.message_ids
|
||||
self.assertEqual(
|
||||
msg_bl2.body,
|
||||
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_unbl.body,
|
||||
Markup(f'<p>Blocklist removal request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_fb.body,
|
||||
Markup(f'<p>Feedback from {test_email_normalized}<br>{test_feedback}</p>')
|
||||
)
|
||||
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[-1])])
|
||||
self.assertEqual(
|
||||
msg_bl.body,
|
||||
Markup(f'<p>Blocklist request from unsubscribe link of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{test_partner._name}" data-oe-id="{test_partner.id}">Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
|
||||
|
||||
def test_mailing_unsubscribe_from_document_tour_mailing_user(self):
|
||||
""" Test portal unsubscribe on mailings performed on documents (not
|
||||
mailing lists or contacts) using a generic '/unsubscribe' link allowing
|
||||
mailing users to see and edit unsubcribe page.
|
||||
|
||||
Tour effects
|
||||
* unsubscribe from mailing based on a document = blocklist;
|
||||
* add feedback (block list): Other reason, with 'My feedback' feedback;
|
||||
* remove email from exclusion list;
|
||||
* re-add email to exclusion list;
|
||||
"""
|
||||
# update user to link it with existing mailing contacts and allow the tour
|
||||
# to run; test without and with mailing group
|
||||
self.user_marketing.write({
|
||||
'email': tools.formataddr(("Déboulonneur", "fleurus@example.com")),
|
||||
'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
test_mailing = self.test_mailing_on_documents.with_env(self.env)
|
||||
self.authenticate('user_marketing', 'user_marketing')
|
||||
|
||||
# no group -> no direct access to /unsubscribe
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/{test_mailing.id}/unsubscribe',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
# group -> direct access to /unsubscribe should wokr
|
||||
self.user_marketing.write({
|
||||
'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
# launch unsubscription tour
|
||||
with freeze_time(self._reference_now):
|
||||
self.start_tour(
|
||||
f"/mailing/{test_mailing.id}/unsubscribe",
|
||||
"mailing_portal_unsubscribe_from_document_with_lists",
|
||||
login=self.user_marketing.login,
|
||||
)
|
||||
|
||||
def test_mailing_unsubscribe_from_list_tour(self):
|
||||
""" Test portal unsubscribe on mailings performed on mailing lists. Their
|
||||
effect is to opt-out from the mailing list.
|
||||
|
||||
Tour effects
|
||||
* unsubscribe from mailing based on lists = opt-out from lists;
|
||||
* add feedback (opt-out): Other reason, with 'My feedback' feedback;
|
||||
* add email to exclusion list;
|
||||
"""
|
||||
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
|
||||
test_mailing = self.test_mailing_on_lists.with_env(self.env)
|
||||
test_feedback = "My feedback"
|
||||
|
||||
# fetch contact and its subscription and blacklist status, to see the tour effects
|
||||
contact_l1 = self.mailing_list_1.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l1
|
||||
)
|
||||
|
||||
# launch unsubscribe tour
|
||||
hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email)
|
||||
with freeze_time(self._reference_now):
|
||||
self.start_tour(
|
||||
f"/mailing/{test_mailing.id}/unsubscribe?email={self.test_email_normalized}&document_id={contact_l1.id}&hash_token={hash_token}",
|
||||
"mailing_portal_unsubscribe_from_list",
|
||||
login=None,
|
||||
)
|
||||
|
||||
# status update check on list 1
|
||||
self.assertTrue(subscription_l1.opt_out)
|
||||
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now)
|
||||
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
|
||||
# status update check on list 2: unmodified (was not member, still not member)
|
||||
contact_l2 = self.mailing_list_2.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
self.assertFalse(contact_l2)
|
||||
|
||||
# posted messages on contact record for mailing list 1: feedback, unsubscription
|
||||
message_feedback = contact_l1.message_ids[0]
|
||||
self.assertEqual(
|
||||
message_feedback.body,
|
||||
Markup(f'<p>Feedback from {contact_l1.email_normalized}<br>{test_feedback}</p>')
|
||||
)
|
||||
message_unsub = contact_l1.message_ids[1]
|
||||
self.assertEqual(
|
||||
message_unsub.body,
|
||||
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p><ul><li>{self.mailing_list_1.name}</li></ul>')
|
||||
)
|
||||
|
||||
# posted messages on exclusion list record: activated, deactivated, activated again
|
||||
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
|
||||
self.assertEqual(len(bl_record.message_ids), 2)
|
||||
self.assertFalse(bl_record.opt_out_reason_id)
|
||||
msg_bl, msg_create = bl_record.message_ids
|
||||
self.assertEqual(
|
||||
msg_bl.body,
|
||||
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
|
||||
|
||||
def test_mailing_unsubscribe_from_list_with_update_tour(self):
|
||||
""" Test portal unsubscribe on mailings performed on mailing lists. Their
|
||||
effect is to opt-out from the mailing list. Optional exclusion list can
|
||||
be done through interface (see tour).
|
||||
|
||||
Tour effects
|
||||
* unsubscribe from mailing based on lists = opt-out from lists;
|
||||
* add feedback (opt-out): Other reason, with 'My feedback' feedback;
|
||||
* add email to exclusion list;
|
||||
* remove email from exclusion list;
|
||||
* come back to List3;
|
||||
* join List2 (with no feedback, as no opt-out / block list was done);
|
||||
* re-add email to exclusion list;
|
||||
"""
|
||||
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
|
||||
test_mailing = self.test_mailing_on_lists.with_env(self.env)
|
||||
test_feedback = "My feedback"
|
||||
|
||||
# fetch contact and its subscription and blacklist status, to see the tour effects
|
||||
contact_l1 = self.mailing_list_1.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l1
|
||||
)
|
||||
contact_l3 = self.mailing_list_3.contact_ids.filtered(
|
||||
lambda contact: contact.email == self.test_email_normalized
|
||||
)
|
||||
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l3
|
||||
)
|
||||
|
||||
# launch unsubscription tour
|
||||
hash_token = test_mailing._generate_mailing_recipient_token(contact_l1.id, contact_l1.email)
|
||||
with freeze_time(self._reference_now):
|
||||
self.start_tour(
|
||||
f"/mailing/{test_mailing.id}/unsubscribe?email={contact_l1.email}&document_id={contact_l1.id}&hash_token={hash_token}",
|
||||
"mailing_portal_unsubscribe_from_list_with_update",
|
||||
login=None,
|
||||
)
|
||||
|
||||
# status update check on list 1
|
||||
self.assertTrue(subscription_l1.opt_out)
|
||||
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now)
|
||||
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
|
||||
# status update check on list 3 (opt-in during test)
|
||||
self.assertFalse(subscription_l3.opt_out)
|
||||
self.assertFalse(subscription_l3.opt_out_datetime)
|
||||
|
||||
# posted messages on contact record for mailing list 1: subscription update, feedback, unsubscription
|
||||
message_update = contact_l1.message_ids[0]
|
||||
self.assertEqual(
|
||||
message_update.body,
|
||||
Markup(f'<p>{contact_l1.display_name} subscribed to the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_2.name}</li></ul>')
|
||||
)
|
||||
message_feedback = contact_l1.message_ids[1]
|
||||
self.assertEqual(
|
||||
message_feedback.body,
|
||||
Markup(f'<p>Feedback from {contact_l1.email_normalized}<br>{test_feedback}</p>')
|
||||
)
|
||||
message_unsub = contact_l1.message_ids[2]
|
||||
self.assertEqual(
|
||||
message_unsub.body,
|
||||
Markup(f'<p>{contact_l1.display_name} unsubscribed from the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_1.name}</li></ul>')
|
||||
)
|
||||
|
||||
# posted messages on contact record for mailing list 3: subscription
|
||||
message_sub = contact_l3.message_ids[0]
|
||||
self.assertEqual(
|
||||
message_sub.body,
|
||||
Markup(f'<p>{contact_l3.display_name} subscribed to the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_3.name}</li><li>{self.mailing_list_2.name}</li></ul>')
|
||||
)
|
||||
|
||||
# posted messages on exclusion list record: activated, deactivated, activated again, feedback
|
||||
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
|
||||
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[0])
|
||||
self.assertEqual(len(bl_record.message_ids), 5)
|
||||
msg_fb, msg_bl2, msg_unbl, msg_bl, msg_create = bl_record.message_ids
|
||||
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[0])])
|
||||
self.assertFalse(msg_fb.body)
|
||||
self.assertEqual(
|
||||
msg_bl2.body,
|
||||
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_unbl.body,
|
||||
Markup(f'<p>Blocklist removal request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_bl.body,
|
||||
Markup(f'<p>Blocklist request from portal of mailing <a href="#" data-oe-model="{test_mailing._name}" '
|
||||
f'data-oe-id="{test_mailing.id}">{test_mailing.subject}</a> (document <a href="#" '
|
||||
f'data-oe-model="{contact_l1._name}" data-oe-id="{contact_l1.id}">Mailing Contact</a>)</p>')
|
||||
)
|
||||
self.assertEqual(msg_create.body, Markup('<p>Mail Blacklist created</p>'))
|
||||
|
||||
def test_mailing_unsubscribe_from_my(self):
|
||||
""" Test portal unsubscribe using the 'my' mailing-specific portal page.
|
||||
It allows to opt-in / opt-out from mailing lists as well as to manage
|
||||
blocklist (see tour).
|
||||
|
||||
Tour effects
|
||||
* opt-in List3 from opt-out, opt-in List2, opt-out List1;
|
||||
* add feedback (as new opt-out): Other reason, with 'My feedback' feedback;
|
||||
* add email in block list;
|
||||
* add feedback (as block list addition): First reason (hence no feedback);
|
||||
"""
|
||||
test_feedback = "My feedback"
|
||||
portal_user = mail_new_test_user(
|
||||
self.env,
|
||||
email=tools.formataddr(("Déboulonneur", "fleurus@example.com")),
|
||||
groups='base.group_portal',
|
||||
login='user_portal_fleurus',
|
||||
name='Déboulonneur User',
|
||||
signature='--\nDéboulonneur',
|
||||
)
|
||||
_test_email, test_email_normalized = portal_user.email, portal_user.email_normalized
|
||||
opt_out_reasons = self.env['mailing.subscription.optout'].search([])
|
||||
|
||||
# launch 'my' mailing' tour
|
||||
self.authenticate(portal_user.login, portal_user.login)
|
||||
with freeze_time(self._reference_now):
|
||||
self.start_tour(
|
||||
"/mailing/my",
|
||||
"mailing_portal_unsubscribe_from_my",
|
||||
login=portal_user.login,
|
||||
)
|
||||
|
||||
# fetch contact and its subscription and blacklist status, to see the tour effects
|
||||
contact_l1 = self.mailing_list_1.contact_ids.filtered(
|
||||
lambda contact: contact.email == test_email_normalized
|
||||
)
|
||||
subscription_l1 = self.mailing_list_1.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l1
|
||||
)
|
||||
contact_l2 = self.mailing_list_2.contact_ids.filtered(
|
||||
lambda contact: contact.email == test_email_normalized
|
||||
)
|
||||
subscription_l2 = self.mailing_list_2.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l2
|
||||
)
|
||||
contact_l3 = self.mailing_list_3.contact_ids.filtered(
|
||||
lambda contact: contact.email == test_email_normalized
|
||||
)
|
||||
subscription_l3 = self.mailing_list_3.subscription_ids.filtered(
|
||||
lambda subscription: subscription.contact_id == contact_l3
|
||||
)
|
||||
self.assertEqual(contact_l2, contact_l3,
|
||||
'When creating new membership, should link with first found existing contact')
|
||||
self.assertTrue(contact_l1.is_blacklisted)
|
||||
self.assertTrue(contact_l3.is_blacklisted)
|
||||
self.assertTrue(subscription_l1.opt_out)
|
||||
self.assertEqual(subscription_l1.opt_out_datetime, self._reference_now,
|
||||
'Subscription: opt-outed during test, datetime should have been set')
|
||||
self.assertEqual(subscription_l1.opt_out_reason_id, opt_out_reasons[-1])
|
||||
self.assertFalse(subscription_l2.opt_out)
|
||||
self.assertFalse(subscription_l2.opt_out_datetime)
|
||||
self.assertFalse(subscription_l2.opt_out_reason_id)
|
||||
self.assertFalse(subscription_l3.opt_out)
|
||||
self.assertFalse(subscription_l3.opt_out_datetime,
|
||||
'Subscription: opt-in during test, datetime should have been reset')
|
||||
self.assertFalse(subscription_l3.opt_out_reason_id)
|
||||
# message on contact for list 1: opt-out L1, join L2
|
||||
msg_fb, msg_sub, msg_uns = contact_l1.message_ids
|
||||
self.assertEqual(
|
||||
msg_fb.body,
|
||||
Markup(f'<p>Feedback from {portal_user.name} ({test_email_normalized})<br>{test_feedback}</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_sub.body,
|
||||
Markup(f'<p>{contact_l1.name} subscribed to the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_2.name}</li></ul>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_uns.body,
|
||||
Markup(f'<p>{contact_l1.name} unsubscribed from the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_1.name}</li></ul>')
|
||||
)
|
||||
# message on contact for list 2: opt-in L3 and L2
|
||||
msg_fb, msg_sub = contact_l3.message_ids
|
||||
self.assertEqual(
|
||||
msg_fb.body,
|
||||
Markup(f'<p>Feedback from {portal_user.name} ({test_email_normalized})<br>{test_feedback}</p>')
|
||||
)
|
||||
self.assertEqual(
|
||||
msg_sub.body,
|
||||
Markup(f'<p>{contact_l3.name} subscribed to the following mailing list(s)</p>'
|
||||
f'<ul><li>{self.mailing_list_3.name}</li><li>{self.mailing_list_2.name}</li></ul>')
|
||||
)
|
||||
|
||||
# block list record created, feedback logged
|
||||
bl_record = self.env['mail.blacklist'].search([('email', '=', contact_l1.email_normalized)])
|
||||
self.assertEqual(bl_record.opt_out_reason_id, opt_out_reasons[0])
|
||||
self.assertEqual(len(bl_record.message_ids), 3)
|
||||
msg_fb, msg_bl, _msg_create = bl_record.message_ids
|
||||
self.assertTracking(msg_fb, [('opt_out_reason_id', 'many2one', False, opt_out_reasons[0])])
|
||||
self.assertEqual(msg_bl.body, Markup('<p>Blocklist request from portal</p>'))
|
||||
|
||||
@mute_logger('odoo.http', 'odoo.addons.website.models.ir_ui_view')
|
||||
def test_mailing_view(self):
|
||||
""" Test preview of mailing. It requires either a token, either being
|
||||
mailing user. """
|
||||
test_mailing = self.test_mailing_on_documents.with_env(self.env)
|
||||
shadow_mailing = test_mailing.copy()
|
||||
doc_id, email_normalized = self.user_marketing.partner_id.id, self.user_marketing.email_normalized
|
||||
hash_token = test_mailing._generate_mailing_recipient_token(doc_id, email_normalized)
|
||||
self.user_marketing.write({
|
||||
'groups_id': [(3, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
self.authenticate('user_marketing', 'user_marketing')
|
||||
|
||||
# TEST: various invalid cases
|
||||
for test_mid, test_doc_id, test_email, test_token, error_code in [
|
||||
(test_mailing.id, doc_id, email_normalized, '', 400), # no token
|
||||
(test_mailing.id, doc_id, email_normalized, 'zboobs', 418), # wrong token
|
||||
(test_mailing.id, self.env.user.partner_id.id, email_normalized, hash_token, 418), # mismatch
|
||||
(test_mailing.id, doc_id, 'not.email@example.com', hash_token, 418), # mismatch
|
||||
(shadow_mailing.id, doc_id, email_normalized, hash_token, 418), # valid credentials but wrong mailing_id
|
||||
(0, doc_id, email_normalized, hash_token, 400), # valid credentials but missing mailing_id
|
||||
]:
|
||||
with self.subTest(test_mid=test_mid, test_email=test_email, test_doc_id=test_doc_id, test_token=test_token):
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/{test_mid}/view?email={test_email}&document_id={test_doc_id}&hash_token={test_token}',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, error_code)
|
||||
|
||||
# TEST: valid call using credentials
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/{test_mailing.id}/view?email={email_normalized}&document_id={doc_id}&hash_token={hash_token}',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
# TEST: invalid credentials but mailing user
|
||||
self.user_marketing.write({
|
||||
'groups_id': [(4, self.env.ref('mass_mailing.group_mass_mailing_user').id)],
|
||||
})
|
||||
res = self.url_open(
|
||||
werkzeug.urls.url_join(
|
||||
test_mailing.get_base_url(),
|
||||
f'mailing/{test_mailing.id}/view',
|
||||
)
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
|
||||
@tagged('link_tracker', 'mailing_portal')
|
||||
class TestMailingTracking(TestMailingControllersCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing')
|
||||
def test_tracking_short_code(self):
|
||||
""" Test opening short code linked to a mailing trace: should set the
|
||||
trace as opened and clicked, create a click record. """
|
||||
mailing = self.test_mailing.with_env(self.env)
|
||||
mailing = self.test_mailing_on_lists.with_env(self.env)
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
@@ -75,10 +655,11 @@ class TestMailingControllers(MassMailCommon, HttpCase):
|
||||
self.assertEqual(mailing_trace.open_datetime, self._reference_now)
|
||||
self.assertEqual(mailing_trace.trace_status, 'open')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mass_mailing.models.mailing')
|
||||
def test_tracking_url_token(self):
|
||||
""" Test tracking of mails linked to a mailing trace: should set the
|
||||
trace as opened. """
|
||||
mailing = self.test_mailing.with_env(self.env)
|
||||
mailing = self.test_mailing_on_lists.with_env(self.env)
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
@@ -104,4 +685,4 @@ class TestMailingControllers(MassMailCommon, HttpCase):
|
||||
f'mail/track/{mail_id_int}/fake_token/blank.gif'
|
||||
)
|
||||
response = self.url_open(track_url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestMailingListMerge(MassMailCommon):
|
||||
new = self.env['mailing.contact'].with_context(default_list_ids=default_list_ids).create([{
|
||||
'name': 'Contact_%d' % x,
|
||||
'email': 'contact_%d@test.example.com' % x,
|
||||
'subscription_list_ids': [(0, 0, {
|
||||
'subscription_ids': [(0, 0, {
|
||||
'list_id': self.mailing_list_1.id,
|
||||
'opt_out': True,
|
||||
}), (0, 0, {
|
||||
@@ -109,15 +109,15 @@ class TestMailingListMerge(MassMailCommon):
|
||||
patch.object(self.env.cr, 'now', lambda: datetime(2022, 1, 1, 12, 0, 0)):
|
||||
contact_form = Form(self.env['mailing.contact'])
|
||||
contact_form.name = 'Contact_test'
|
||||
with contact_form.subscription_list_ids.new() as subscription:
|
||||
with contact_form.subscription_ids.new() as subscription:
|
||||
subscription.list_id = self.mailing_list_1
|
||||
subscription.opt_out = True
|
||||
with contact_form.subscription_list_ids.new() as subscription:
|
||||
with contact_form.subscription_ids.new() as subscription:
|
||||
subscription.list_id = self.mailing_list_2
|
||||
subscription.opt_out = False
|
||||
contact = contact_form.save()
|
||||
self.assertEqual(contact.subscription_list_ids[0].unsubscription_date, datetime(2022, 1, 1, 12, 0, 0))
|
||||
self.assertFalse(contact.subscription_list_ids[1].unsubscription_date)
|
||||
self.assertEqual(contact.subscription_ids[0].opt_out_datetime, datetime(2022, 1, 1, 12, 0, 0))
|
||||
self.assertFalse(contact.subscription_ids[1].opt_out_datetime)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_contact_copy_in_context_of_mailing_list(self):
|
||||
@@ -125,7 +125,7 @@ class TestMailingListMerge(MassMailCommon):
|
||||
contact_1 = MailingContact.create({
|
||||
'name': 'Sam',
|
||||
'email': 'gamgee@shire.com',
|
||||
'subscription_list_ids': [(0, 0, {'list_id': self.mailing_list_3.id})],
|
||||
'subscription_ids': [(0, 0, {'list_id': self.mailing_list_3.id})],
|
||||
})
|
||||
# Copy the contact with default_list_ids in context, which should not raise anything
|
||||
contact_2 = contact_1.with_context(default_list_ids=self.mailing_list_3.ids).copy()
|
||||
@@ -274,3 +274,49 @@ class TestMailingContactImport(MassMailCommon):
|
||||
|
||||
contact = self.env['mailing.contact'].search([('email', '=', 'already_exists_list_1@example.com')])
|
||||
self.assertEqual(len(contact), 1, 'Should have updated the existing contact instead of creating a new one')
|
||||
|
||||
|
||||
@tagged('mailing_list')
|
||||
class TestSubscriptionManagement(MassMailCommon):
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_update_optout(self):
|
||||
_email_formatted = '"Mireille Labeille" <mireille@test.example.com>'
|
||||
_email_formatted_upd = '"Mireille Oreille-Labeille" <mireille@test.example.com>'
|
||||
_email_normalized = 'mireille@test.example.com'
|
||||
self._create_mailing_list()
|
||||
ml_1, ml_2 = self.mailing_list_1.with_env(self.env), self.mailing_list_2.with_env(self.env)
|
||||
ml_3 = self._create_mailing_list_of_x_contacts(3)
|
||||
self.assertEqual(ml_1.contact_count, 3)
|
||||
self.assertEqual(ml_1.contact_count_blacklisted, 0)
|
||||
self.assertEqual(ml_1.contact_count_email, 3)
|
||||
self.assertEqual(ml_1.contact_count_opt_out, 0)
|
||||
self.assertEqual(ml_2.contact_count, 4)
|
||||
self.assertEqual(ml_2.contact_count_blacklisted, 0)
|
||||
self.assertEqual(ml_2.contact_count_email, 4)
|
||||
self.assertEqual(ml_2.contact_count_opt_out, 0)
|
||||
self.assertEqual(ml_3.contact_count, 3)
|
||||
self.assertEqual(ml_3.contact_count_blacklisted, 0)
|
||||
self.assertEqual(ml_3.contact_count_email, 3)
|
||||
self.assertEqual(ml_3.contact_count_opt_out, 0)
|
||||
|
||||
# create a new test contact
|
||||
contact = self.env['mailing.contact'].browse(
|
||||
self.env['mailing.contact'].name_create(_email_formatted)[0]
|
||||
)
|
||||
self.assertEqual(contact.email, _email_normalized)
|
||||
self.assertEqual(contact.name, 'Mireille Labeille')
|
||||
|
||||
# add new subscriptions (and ensure email_normalized is used)
|
||||
(ml_1 + ml_2)._update_subscription_from_email(_email_formatted_upd, opt_out=False)
|
||||
subs = self.env['mailing.subscription'].search(
|
||||
[('contact_id', '=', contact.id)]
|
||||
)
|
||||
self.assertEqual(subs.list_id, ml_1 + ml_2)
|
||||
|
||||
# opt-out from opted-in mailing list + 1 non opted-in mailing list
|
||||
(ml_2 + ml_3)._update_subscription_from_email(_email_formatted_upd, opt_out=True)
|
||||
subs = self.env['mailing.subscription'].search(
|
||||
[('contact_id', '=', contact.id)]
|
||||
)
|
||||
self.assertEqual(subs.list_id, ml_1 + ml_2)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="mail_blacklist_view_tree" model="ir.ui.view">
|
||||
<field name="name">mail.blacklist.view.tree.inherit.mailing</field>
|
||||
<field name="model">mail.blacklist</field>
|
||||
<field name="inherit_id" ref="mail.mail_blacklist_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='email']" position="after">
|
||||
<field name="opt_out_reason_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_blacklist_view_form" model="ir.ui.view">
|
||||
<field name="name">mail.blacklist.view.form.inherit.mailing</field>
|
||||
<field name="model">mail.blacklist</field>
|
||||
<field name="inherit_id" ref="mail.mail_blacklist_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='email']" position="after">
|
||||
<field name="opt_out_reason_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_blacklist_view_search" model="ir.ui.view">
|
||||
<field name="name">mail.blacklist.view.search</field>
|
||||
<field name="model">mail.blacklist</field>
|
||||
<field name="inherit_id" ref="mail.mail_blacklist_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='inactive']" position="after">
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Reason"
|
||||
name="group_by_opt_out_reason_id"
|
||||
context="{'group_by': 'opt_out_reason_id'}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="mailing_contact_subscription_view_form">
|
||||
<field name="name">mailing.contact.subscription.view.form</field>
|
||||
<field name="model">mailing.contact.subscription</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Mailing List Subscription">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="list_id"/>
|
||||
<field name="is_blacklisted" invisible="1"/>
|
||||
<label for="contact_id" class="oe_inline"/>
|
||||
<div class="o_row o_row_readonly">
|
||||
<i class="fa fa-ban text-danger" role="img" title="This email is blacklisted for mass mailings"
|
||||
aria-label="Blacklisted" invisible="not is_blacklisted" groups="base.group_user"></i>
|
||||
<field name="contact_id"/>
|
||||
</div>
|
||||
<field name="unsubscription_date" readonly="1"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="message_bounce" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mailing_contact_subscription_view_tree">
|
||||
<field name="name">mailing.contact.subscription.view.tree</field>
|
||||
<field name="model">mailing.contact.subscription</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Mailing List Subscriptions">
|
||||
<field name="contact_id"/>
|
||||
<field name="unsubscription_date"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="message_bounce"/>
|
||||
<field name="is_blacklisted"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mailing_contact_subscription_view_search">
|
||||
<field name="name">mailing.contact.subscription.view.search</field>
|
||||
<field name="model">mailing.contact.subscription</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Mailing List Subscriptions">
|
||||
<field name="contact_id"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="list_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -144,11 +144,12 @@
|
||||
<field name="message_bounce" readonly="1" invisible="not id"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="subscription_list_ids">
|
||||
<field name="subscription_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="list_id"/>
|
||||
<field name="unsubscription_date"/>
|
||||
<field name="opt_out_datetime" readonly="1"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
|
||||
@@ -42,8 +42,17 @@
|
||||
<menuitem name="Reporting"
|
||||
id="menu_mass_mailing_report"
|
||||
sequence="90"
|
||||
parent="mass_mailing_menu_root"
|
||||
parent="mass_mailing_menu_root"/>
|
||||
<menuitem name="Mass Mailing Analysis"
|
||||
id="mailing_menu_report_mailing"
|
||||
sequence="1"
|
||||
parent="menu_mass_mailing_report"
|
||||
action="mailing_trace_report_action_mail"/>
|
||||
<menuitem name="Optout"
|
||||
id="mailing_menu_report_subscribe_reason"
|
||||
sequence="2"
|
||||
parent="menu_mass_mailing_report"
|
||||
action="mailing_subscription_action_report_optout"/>
|
||||
|
||||
<!-- Configuration -->
|
||||
<menuitem name="Configuration"
|
||||
@@ -82,6 +91,12 @@
|
||||
action="mail.mail_blacklist_action"
|
||||
parent="mass_mailing_configuration"
|
||||
sequence="20"/>
|
||||
<!-- Configuration / Optout reasons -->
|
||||
<menuitem id="mailing_subscription_optout_menu"
|
||||
name="Optout Reasons"
|
||||
action="mailing_subscription_optout_action"
|
||||
sequence="21"
|
||||
parent="mass_mailing_configuration"/>
|
||||
<!-- Configuration / Favorite Filters -->
|
||||
<menuitem id="mailing_filter_menu_action"
|
||||
action="mailing_filter_action"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mailing_subscription_optout_view_form" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.optout.view.form</field>
|
||||
<field name="model">mailing.subscription.optout</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="is_feedback"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_optout_view_tree" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.optout.view.tree</field>
|
||||
<field name="model">mailing.subscription.optout</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_optout_view_search" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.optout.view.search</field>
|
||||
<field name="model">mailing.subscription.optout</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_optout_action" model="ir.actions.act_window">
|
||||
<field name="name">Optout Reasons</field>
|
||||
<field name="res_model">mailing.subscription.optout</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No data yet.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mailing_subscription_view_form" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.view.form</field>
|
||||
<field name="model">mailing.subscription</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Mailing List Subscription">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="list_id"/>
|
||||
<field name="is_blacklisted" invisible="1"/>
|
||||
<label for="contact_id" class="oe_inline"/>
|
||||
<div class="o_row o_row_readonly">
|
||||
<i class="fa fa-ban text-danger" role="img" title="This email is blacklisted for mass mailings"
|
||||
aria-label="Blacklisted" invisible="not is_blacklisted" groups="base.group_user"></i>
|
||||
<field name="contact_id"/>
|
||||
</div>
|
||||
<field name="opt_out_datetime" readonly="1"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
<field name="message_bounce" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_view_tree" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.view.tree</field>
|
||||
<field name="model">mailing.subscription</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Mailing List Subscriptions">
|
||||
<field name="contact_id"/>
|
||||
<field name="list_id"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
<field name="opt_out_datetime" readonly="1"/>
|
||||
<field name="message_bounce" optional="hide"/>
|
||||
<field name="is_blacklisted" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_view_search" model="ir.ui.view">
|
||||
<field name="name">mailing.subscription.view.search</field>
|
||||
<field name="model">mailing.subscription</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Mailing List Subscriptions">
|
||||
<field name="contact_id"/>
|
||||
<field name="list_id"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
<field name="opt_out_datetime"/>
|
||||
<filter string="Optout" name="filter_optout" domain="[('opt_out', '=', True)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Mailing List" name="group_by_list_id" context="{'group_by': 'list_id'}"/>
|
||||
<filter string="Optout Reason" name="group_by_opt_out_reason_id" context="{'group_by': 'opt_out_reason_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_subscription_action_report_optout" model="ir.actions.act_window">
|
||||
<field name="name">Subscriptions</field>
|
||||
<field name="res_model">mailing.subscription</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{
|
||||
'search_default_group_by_opt_out_reason_id': 1,
|
||||
'search_default_filter_optout': 1,
|
||||
}</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No data yet.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -7,113 +7,159 @@
|
||||
</template>
|
||||
|
||||
<template id="unsubscribe_form">
|
||||
<div class="container o_unsubscribe_form">
|
||||
<div id="o_mailing_portal_subscription"
|
||||
class="container o_mailing_portal_body"
|
||||
t-att-data-blocklist-enabled="blocklist_enabled"
|
||||
t-att-data-blocklist-possible="blocklist_possible"
|
||||
t-att-data-document-id="document_id"
|
||||
t-att-data-feedback-enabled="feedback_enabled"
|
||||
t-att-data-feedback-readonly="feedback_readonly"
|
||||
t-att-data-hash-token="hash_token"
|
||||
t-att-data-last-action="last_action"
|
||||
t-att-data-email="email"
|
||||
t-att-data-email-valid="email_valid"
|
||||
t-att-data-is-blocklisted="is_blocklisted"
|
||||
t-att-data-mailing-id="mailing_id">
|
||||
<div class="row">
|
||||
<form action="/mailing/list/update" method="POST" id="unsubscribe_form" class="col-lg-6 offset-lg-3 mt-4">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
|
||||
<input type="hidden" name="res_id" t-att-value="res_id"/>
|
||||
<input type="hidden" name="unsubscribed_list" t-att-value="unsubscribed_list"/>
|
||||
|
||||
<div>
|
||||
<t t-if="contacts">
|
||||
<div id="info_state" class="alert alert-success" role="status">
|
||||
<div id="subscription_info"></div>
|
||||
<div id="div_feedback">
|
||||
<p>We would appreciate if you provide feedback about why you updated<br/>your subscriptions</p>
|
||||
<textarea class="form-control" name="opt_out_feedback" cols="60" rows="3"></textarea>
|
||||
<br/>
|
||||
<div class="btn btn-primary text-start" id="button_feedback">Send</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 offset-lg-3 mt-4">
|
||||
<div id="o_mailing_subscription_form">
|
||||
<form action="/mailing/list/update" method="POST">
|
||||
<h1 class="o_page_header">Mailing Subscriptions</h1>
|
||||
<p>Choose your mailing subscriptions</p>
|
||||
<div id="div_opt_out">
|
||||
<ul class="list-group">
|
||||
<t t-foreach="list_ids" t-as="list_id">
|
||||
<t t-if="list_id.is_public == True">
|
||||
<li class="list-group-item">
|
||||
<input type="checkbox" name="contact_ids"
|
||||
t-att-value="list_id['id']" t-att-checked="None if list_id['id'] in opt_out_list_ids else 'checked'"/>
|
||||
<t t-esc="list_id.name"/>
|
||||
<span t-if="list_id['id'] in opt_out_list_ids"
|
||||
class="o_mailing_portal_list_unsubscribed">
|
||||
Unsubscribed
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<div id="o_mailing_subscription_form_blocklisted"
|
||||
t-att-class="'' if is_blocklisted else 'd-none'">
|
||||
<p>
|
||||
Your email is currently <strong>in our block list</strong>.
|
||||
<t t-if="lists_optin">
|
||||
You will not receive any news from those mailing lists you are a member of:
|
||||
</t>
|
||||
<t t-else="">
|
||||
You will not hear from us anymore.
|
||||
</t>
|
||||
</p>
|
||||
<ul class="list-group mb-4"
|
||||
t-if="lists_optin">
|
||||
<t t-foreach="lists_optin" t-as="mailing_list">
|
||||
<li class="list-group-item d-flex align-items-center">
|
||||
<strong>
|
||||
<t t-if="mailing_list.is_public" t-esc="mailing_list.name"/>
|
||||
<t t-else="">Mailing List #<t t-esc="mailing_list.id"/></t>
|
||||
</strong>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
<div class="mb64 pt-3">
|
||||
<div class="btn btn-link float-end pe-0 text-uppercase"
|
||||
t-if="show_blacklist_button"
|
||||
id="button_add_blacklist"
|
||||
style="display:none">Blacklist Me</div>
|
||||
<div class="btn btn-link float-end pe-0 text-uppercase"
|
||||
id="button_remove_blacklist"
|
||||
style="display:none">Come Back</div>
|
||||
<button type="submit" id="send_form"
|
||||
class="btn btn-primary">Update my subscriptions</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info text-center" role="status">
|
||||
<p>You are not subscribed to any of our mailing list.</p>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="page_mailing_unsubscribe_done" name="Unsubscribed">
|
||||
<t t-call="mass_mailing.layout">
|
||||
<t t-call="mass_mailing.unsubscribe_done"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="unsubscribe_done">
|
||||
<div class="container o_unsubscribe_form">
|
||||
<div class="row">
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
|
||||
<input type="hidden" name="res_id" t-att-value="res_id"/>
|
||||
<div id="div_blacklist" class="col-lg-6 offset-lg-3">
|
||||
<h1 class="o_page_header">Mailing Subscriptions</h1>
|
||||
|
||||
<div id="subscription_info" class="alert alert-success text-center" role="status">
|
||||
<p>You have been successfully <strong>unsubscribed</strong>!</p>
|
||||
</div>
|
||||
|
||||
<div t-if="list_ids" class="alert alert-warning">
|
||||
<p class="text-center">You were still subscribed to those newsletters. You will not receive any news from them anymore:</p>
|
||||
<ul class="list-group mb-4">
|
||||
<t t-foreach="list_ids" t-as="list_id">
|
||||
<t t-if="list_id.is_public == True">
|
||||
<li class="list-group-item bg-transparent">
|
||||
<strong><t t-esc="list_id.name"/></strong>
|
||||
<div id="o_mailing_subscription_form_manage"
|
||||
t-att-class="'d-none' if is_blocklisted else ''">
|
||||
<p t-if="lists_optin + lists_optout">
|
||||
Choose your mailing subscriptions
|
||||
</p>
|
||||
<ul id="o_mailing_subscription_form_lists"
|
||||
t-if="lists_optin + lists_optout"
|
||||
class="list-group">
|
||||
<li t-foreach="(lists_optin + lists_optout)"
|
||||
t-as="mailing_list"
|
||||
class="list-group-item d-flex align-items-center">
|
||||
<t t-set="title" t-value="mailing_list.name if mailing_list.is_public else 'Mailing List #%s' % mailing_list.id"/>
|
||||
<input type="checkbox"
|
||||
name="mailing_list_ids"
|
||||
t-att-data-member="1"
|
||||
t-att-disabled="'disabled' if is_blocklisted else None"
|
||||
t-att-value="mailing_list.id"
|
||||
t-att-checked="None if mailing_list in lists_optout else 'checked'"
|
||||
t-att-title="title"/>
|
||||
<span class="ms-1" t-out="title"/>
|
||||
<span class="o_mailing_list_unsubscribed ms-auto">
|
||||
<t t-if="mailing_list in lists_optout">
|
||||
Not subscribed
|
||||
</t>
|
||||
<t t-else="">
|
||||
Subscribed
|
||||
</t>
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
</ul>
|
||||
</ul>
|
||||
<p t-if="lists_public">
|
||||
You may also be interested in
|
||||
</p>
|
||||
<ul id="o_mailing_subscription_form_lists_additional"
|
||||
t-if="lists_public"
|
||||
class="list-group">
|
||||
<li t-foreach="lists_public"
|
||||
t-as="mailing_list"
|
||||
class="list-group-item">
|
||||
<input type="checkbox"
|
||||
name="mailing_list_ids"
|
||||
t-att-disabled="'disabled' if is_blocklisted else None"
|
||||
t-att-value="mailing_list.id"
|
||||
t-att-title="mailing_list.name"/>
|
||||
<span class="ms-1" t-out="mailing_list.name"/>
|
||||
</li>
|
||||
</ul>
|
||||
<p t-if="not lists_optin and not lists_optout and not lists_public">
|
||||
You are not subscribed to any of our mailing list.
|
||||
</p>
|
||||
</div>
|
||||
<div id="o_mailing_subscription_blocklist"
|
||||
class="d-flex align-items-center mb-4 pt-3">
|
||||
<button type="submit" id="button_form_send"
|
||||
t-att-disabled="'disabled' if is_blocklisted else None"
|
||||
t-att-class="'btn btn-primary ' + ('' if lists_optin or lists_optout or lists_public else 'd-none')">
|
||||
Apply changes
|
||||
</button>
|
||||
<div class="btn btn-link me-2 text-uppercase d-none"
|
||||
t-if="blocklist_enabled and blocklist_possible"
|
||||
id="button_blocklist_add">Exclude Me</div>
|
||||
<div class="btn btn-link text-uppercase d-none"
|
||||
id="button_blocklist_remove">Come Back</div>
|
||||
<div id="o_mailing_subscription_update_info" class="d-none"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div t-if="show_blacklist_button" class="mb64">
|
||||
<div class="btn btn-link float-end"
|
||||
id="button_add_blacklist"
|
||||
style="display:none">Blacklist Me</div>
|
||||
<div class="btn btn-link float-end"
|
||||
id="button_remove_blacklist"
|
||||
style="display:none">Come Back</div>
|
||||
<div id="o_mailing_subscription_info"
|
||||
class="mb-4"
|
||||
role="status">
|
||||
<t t-if="unsubscribed_name">
|
||||
<h1 class="o_page_header">Successfully Unsubscribed</h1>
|
||||
<span t-out="unsubscribed_name"/>
|
||||
</t>
|
||||
<div id="o_mailing_subscription_feedback"
|
||||
t-att-class="'' if feedback_enabled else 'd-none'">
|
||||
<form action="/mail/mailing/feedback" method="POST"
|
||||
class="mt-3">
|
||||
<p>
|
||||
Please let us know why you updated your subscription.
|
||||
</p>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<fieldset>
|
||||
<div class="form-check"
|
||||
t-foreach="opt_out_reasons"
|
||||
t-as="opt_out_reason">
|
||||
<input class="form-check-input o_mailing_subscription_opt_out_reason"
|
||||
type="radio"
|
||||
name="opt_out_reason_id"
|
||||
t-att-id="'opt_out_reason_id' + str(opt_out_reason.id)"
|
||||
t-att-data-is-feedback="opt_out_reason.is_feedback"
|
||||
t-att-value="opt_out_reason.id"/>
|
||||
<label class="form-check-label"
|
||||
t-att-for="'opt_out_reason_id' + str(opt_out_reason.id)"
|
||||
t-out="opt_out_reason.name"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<textarea class="form-control d-none" name="feedback" cols="60" rows="3"></textarea>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<button type="submit" id="button_feedback"
|
||||
class="btn btn-primary text-start me-2">Send</button>
|
||||
<div id="o_mailing_subscription_feedback_info" class="d-none"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="o_mailing_portal_subscription_editor" class="oe_structure oe_empty"/>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
'demo': [
|
||||
'demo/utm.xml',
|
||||
'demo/mailing_list_contact.xml',
|
||||
'demo/mailing_contact_subscription.xml',
|
||||
'demo/mailing_subscription.xml',
|
||||
'demo/mailing_mailing.xml',
|
||||
'demo/mailing_trace.xml',
|
||||
],
|
||||
|
||||
@@ -59,7 +59,7 @@ class MailingSMSController(http.Controller):
|
||||
if tocheck_number and trace:
|
||||
mailing_list_ids = trace.mass_mailing_id.contact_list_ids
|
||||
if mailing_list_ids:
|
||||
subscriptions = request.env['mailing.contact.subscription'].sudo().search([
|
||||
subscriptions = request.env['mailing.subscription'].sudo().search([
|
||||
('list_id', 'in', mailing_list_ids.ids),
|
||||
('contact_id.phone_sanitized', '=', tocheck_number),
|
||||
])
|
||||
@@ -70,7 +70,7 @@ class MailingSMSController(http.Controller):
|
||||
blacklist_rec._message_log(
|
||||
body=_('Blacklist through SMS Marketing unsubscribe (mailing ID: %s - model: %s)') %
|
||||
(trace.mass_mailing_id.id, trace.mass_mailing_id.mailing_model_id.display_name))
|
||||
lists_optin = request.env['mailing.contact.subscription'].sudo().search([
|
||||
lists_optin = request.env['mailing.subscription'].sudo().search([
|
||||
('contact_id.phone_sanitized', '=', tocheck_number),
|
||||
('list_id', 'not in', mailing_list_ids.ids),
|
||||
('opt_out', '=', False),
|
||||
|
||||
+5
-5
@@ -1,22 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="0">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_0" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_0" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing_sms.mailing_contact_0_0"/>
|
||||
<field name="list_id" ref="mass_mailing_sms.mailing_list_sms_0"/>
|
||||
</record>
|
||||
<record id="mailing_list_sms_0_sub_contact_0_1" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_1" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing_sms.mailing_contact_0_1"/>
|
||||
<field name="list_id" ref="mass_mailing_sms.mailing_list_sms_0"/>
|
||||
</record>
|
||||
<record id="mailing_list_sms_0_sub_contact_0_2" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_2" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing_sms.mailing_contact_0_2"/>
|
||||
<field name="list_id" ref="mass_mailing_sms.mailing_list_sms_0"/>
|
||||
</record>
|
||||
<record id="mailing_list_sms_0_sub_contact_0_3" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_3" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing_sms.mailing_contact_0_3"/>
|
||||
<field name="list_id" ref="mass_mailing_sms.mailing_list_sms_0"/>
|
||||
</record>
|
||||
<record id="mailing_list_sms_0_sub_contact_0_4" model="mailing.contact.subscription">
|
||||
<record id="mailing_list_sms_0_sub_contact_0_4" model="mailing.subscription">
|
||||
<field name="contact_id" ref="mass_mailing_sms.mailing_contact_0_4"/>
|
||||
<field name="list_id" ref="mass_mailing_sms.mailing_list_sms_0"/>
|
||||
</record>
|
||||
@@ -79,11 +79,11 @@ class TestMassMailing(TestMailFullCommon):
|
||||
unsubscribe_href = self._get_href_from_anchor_id(email['body'], "url6")
|
||||
unsubscribe_url = werkzeug.urls.url_parse(unsubscribe_href)
|
||||
unsubscribe_params = unsubscribe_url.decode_query().to_dict(flat=True)
|
||||
self.assertEqual(int(unsubscribe_params['res_id']), recipient.id)
|
||||
self.assertEqual(int(unsubscribe_params['document_id']), recipient.id)
|
||||
self.assertEqual(unsubscribe_params['email'], recipient.email_normalized)
|
||||
self.assertEqual(
|
||||
mailing._generate_mailing_recipient_token(unsubscribe_params['res_id'], (unsubscribe_params['email'])),
|
||||
unsubscribe_params['token']
|
||||
mailing._generate_mailing_recipient_token(unsubscribe_params['document_id'], (unsubscribe_params['email'])),
|
||||
unsubscribe_params['hash_token']
|
||||
)
|
||||
# rendered view
|
||||
self.assertIn(
|
||||
@@ -92,11 +92,11 @@ class TestMassMailing(TestMailFullCommon):
|
||||
view_href = self._get_href_from_anchor_id(email['body'], "url6")
|
||||
view_url = werkzeug.urls.url_parse(view_href)
|
||||
view_params = view_url.decode_query().to_dict(flat=True)
|
||||
self.assertEqual(int(view_params['res_id']), recipient.id)
|
||||
self.assertEqual(int(view_params['document_id']), recipient.id)
|
||||
self.assertEqual(view_params['email'], recipient.email_normalized)
|
||||
self.assertEqual(
|
||||
mailing._generate_mailing_recipient_token(view_params['res_id'], (view_params['email'])),
|
||||
view_params['token']
|
||||
mailing._generate_mailing_recipient_token(view_params['document_id'], (view_params['email'])),
|
||||
view_params['hash_token']
|
||||
)
|
||||
|
||||
self.assertMailTraces(
|
||||
|
||||
@@ -61,6 +61,7 @@ class MailingBLacklist(models.Model):
|
||||
_description = 'Mailing Blacklist Enabled'
|
||||
_name = 'mailing.test.blacklist'
|
||||
_inherit = ['mail.thread.blacklist']
|
||||
_order = 'name ASC, id DESC'
|
||||
_primary_email = 'email_from'
|
||||
|
||||
name = fields.Char()
|
||||
|
||||
@@ -565,7 +565,7 @@ class TestMassMailing(TestMassMailCommon):
|
||||
# contact_1 is optout but same email is not optout from the same list
|
||||
# contact 3 is optout in list 1 but not in list 2
|
||||
# contact 5 is optout
|
||||
subs = self.env['mailing.contact.subscription'].search([
|
||||
subs = self.env['mailing.subscription'].search([
|
||||
'|', '|',
|
||||
'&', ('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id),
|
||||
'&', ('contact_id', '=', mailing_contact_3.id), ('list_id', '=', mailing_list_1.id),
|
||||
@@ -591,7 +591,8 @@ class TestMassMailing(TestMassMailCommon):
|
||||
{'email': 'test4@test.example.com'},
|
||||
{'email': 'test5@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'}],
|
||||
mailing,
|
||||
mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
|
||||
# mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
|
||||
mailing_contact_2 + mailing_contact_1 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
|
||||
check_mail=True
|
||||
)
|
||||
self.assertEqual(mailing.canceled, 2)
|
||||
|
||||
@@ -11,18 +11,31 @@ from odoo.tools import mute_logger
|
||||
@tagged('mailing_manage')
|
||||
class TestMailingTest(TestMassMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_records = cls.env['mailing.test.blacklist'].create([
|
||||
{
|
||||
'email_from': f'test.mailing.{idx}@test.example.com',
|
||||
'name': f'Test Mailing {idx}',
|
||||
'user_id': cls.user_marketing.id,
|
||||
}
|
||||
for idx in range(5)
|
||||
])
|
||||
cls.test_mailing_bl = cls.env['mailing.mailing'].create({
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'mailing_domain': [('id', 'in', cls.test_records.ids)],
|
||||
'mailing_model_id': cls.env['ir.model']._get_id('mailing.test.blacklist'),
|
||||
'mailing_type': 'mail',
|
||||
'name': 'TestButton',
|
||||
'preview': 'Preview {{ object.name }}',
|
||||
'subject': 'Subject {{ object.name }}',
|
||||
})
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
|
||||
def test_mailing_test_button(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestButton',
|
||||
'subject': 'Subject {{ object.name }}',
|
||||
'preview': 'Preview {{ object.name }}',
|
||||
'state': 'draft',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
mailing = self.test_mailing_bl.with_env(self.env)
|
||||
mailing_test = self.env['mailing.mailing.test'].create({
|
||||
'email_to': 'test@test.com',
|
||||
'mass_mailing_id': mailing.id,
|
||||
@@ -63,25 +76,12 @@ class TestMailingTest(TestMassMailCommon):
|
||||
mailing_test.send_mail_test()
|
||||
|
||||
def test_mailing_test_equals_reality(self):
|
||||
"""
|
||||
Check that both test and real emails will format the qweb and inline placeholders correctly in body and subject.
|
||||
"""
|
||||
contact_list = self.env['mailing.list'].create({
|
||||
'name': 'Testers',
|
||||
'contact_ids': [Command.create({
|
||||
'name': 'Mitchell Admin',
|
||||
'email': 'real@real.com',
|
||||
})],
|
||||
})
|
||||
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestButton',
|
||||
'subject': 'Subject {{ object.name }} <t t-out="object.name"/>',
|
||||
'state': 'draft',
|
||||
'mailing_type': 'mail',
|
||||
""" Check that both test and real emails will format the qweb and inline
|
||||
placeholders correctly in body and subject. """
|
||||
mailing = self.test_mailing_bl.with_env(self.env)
|
||||
mailing.write({
|
||||
'body_html': '<p>Hello {{ object.name }} <t t-out="object.name"/></p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
|
||||
'contact_list_ids': [contact_list.id],
|
||||
'subject': 'Subject {{ object.name }} <t t-out="object.name"/>',
|
||||
})
|
||||
mailing_test = self.env['mailing.mailing.test'].create({
|
||||
'email_to': 'test@test.com',
|
||||
@@ -91,8 +91,10 @@ class TestMailingTest(TestMassMailCommon):
|
||||
with self.mock_mail_gateway():
|
||||
mailing_test.send_mail_test()
|
||||
|
||||
expected_subject = 'Subject Mitchell Admin <t t-out="object.name"/>'
|
||||
expected_body = 'Hello {{ object.name }} Mitchell Admin'
|
||||
expected_test_record = self.env[mailing.mailing_model_real].search([], limit=1)
|
||||
self.assertEqual(expected_test_record, self.test_records[0], 'Should take first found one')
|
||||
expected_subject = f'Subject {expected_test_record.name} <t t-out="object.name"/>'
|
||||
expected_body = 'Hello {{ object.name }}' + f' {expected_test_record.name}'
|
||||
|
||||
self.assertSentEmail(self.env.user.partner_id, ['test@test.com'],
|
||||
subject=expected_subject,
|
||||
@@ -103,6 +105,9 @@ class TestMailingTest(TestMassMailCommon):
|
||||
mailing.action_launch()
|
||||
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').method_direct_trigger()
|
||||
|
||||
self.assertSentEmail(self.env.user.partner_id, ['real@real.com'],
|
||||
self.assertSentEmail(
|
||||
self.env.user.partner_id,
|
||||
[expected_test_record.email_from],
|
||||
subject=expected_subject,
|
||||
body_content=expected_body)
|
||||
body_content=expected_body,
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ class MassMailController(main.MassMailController):
|
||||
fname = self._get_fname(subscription_type)
|
||||
is_subscriber = False
|
||||
if value and fname:
|
||||
contacts_count = request.env['mailing.contact.subscription'].sudo().search_count(
|
||||
contacts_count = request.env['mailing.subscription'].sudo().search_count(
|
||||
[('list_id', 'in', [int(list_id)]), (f'contact_id.{fname}', '=', value), ('opt_out', '=', False)])
|
||||
is_subscriber = contacts_count > 0
|
||||
|
||||
@@ -40,7 +40,7 @@ class MassMailController(main.MassMailController):
|
||||
'toast_content': _("Suspicious activity detected by Google reCaptcha."),
|
||||
}
|
||||
|
||||
ContactSubscription = request.env['mailing.contact.subscription'].sudo()
|
||||
ContactSubscription = request.env['mailing.subscription'].sudo()
|
||||
Contacts = request.env['mailing.contact'].sudo()
|
||||
if subscription_type == 'email':
|
||||
name, value = tools.parse_contact_from_email(value)
|
||||
|
||||
Reference in New Issue
Block a user