[IMP] mail, mass_mailing : apply blacklist, opt_out per mailing list

Purpose
=======
- Apply the blacklist implementation to Improve mailing subscription to be more
  compliant with the European GDPR law. Keep a list of people who does not want
  to receive promotional emails (or mass mailing in general) anymore.
- Allows the recipient to update himself his mailing preferences

Specifications
===========
This commit is regrouping some main changes on mass mailing.
  Apply Blacklist for following models through blacklist.mixin :
    - crm.lead
    - res.partner
    - mail.mass_mailing.contact
    - mail.channel.partner
- Opt_out per mailing list instead of per mailing contact.
- Replace opt_out by blacklist in crm.lead + res.partner models
- Added 'ignored' state for mass_mailing. Ignored = blacklisted, opted-out
  Ignored email are not included into final statistics to avoid confusion.
- Unsubscribe(d) pages migrated from website_mass_mailing to mass_mailing module
  as thoses pages should work without having the website module installed

Detailed implementation
===================
Mass-mailing :
	- A blacklisted email is notified by the ban icon next to the email field.
	  (at the left of the email field for display purpose)
	- Renaming the '_get_blacklist' method that was actually
	  searching opt_out list into 'get_opt_out_list'
	- Opt out per mailing list : Add opt_out + related fields (for display and
	  ergonomy reasons) on the relational model
	- Display relational model tree view instead of mass_mailing.contact tree
	  view when clicking on mass_mailing_list in kanban view
	  -> In order to be align between contact_nbr displayed in kanban tile and
	     the content of the tree view
	  -> mass_mailing contact is accesible via the user icon in this tree view
	- Remove custom filters 'filter_contact_subscription' and
	  'filter_contact_unsubscription' as opt_out is not on mass_mailing.contact
	  model anymore
	- Add 'is_public' to mailing list
	    The name of this mailing list can be seen (or not) by recipient in
	    the unsubscription page
	    If the mailing lists used in the mass mailing are not public,
	    the user in only informed that he has been unsubscribed.
	    If the mailing lists are public, the user is informed that he has been
	    unsubscribed from the mailing lists
	    and he has the choice to modify his subscription to all the public
	    mailing list he is or was subscribed to.
	- Unsubscription Page :
		- mass_mailing_contact :
		  Opt_out per mailing list, done by email and not by id, as multiple
		  contact can have the same email
		  The recipient can add/remove himself to/from the blacklist
		  The recipient can send a feedback about why he unsubscribed
		- crm.lead + res.partner :
		  Once the recipient unsubscribe, he is automatically blacklisted
		  The recipient can 'Come back' and remove himself from the blacklist
		  if he changes his mind
		- Show blacklist button parameter added in config :
		  The idea is to enable/disable the fact that the recipient can add
		  himself to the blacklist by showing or not the 'blacklist me' button
		  Only applies for mass_mailing.contact. The recipient, once
		  blacklisted, can always, no matter the value of this parameter,
		  'come back' and unblacklist himself

crm.lead + res.partner :
	- Replace opt_out by blacklist in crm.lead + res.partner models.
	  As when a res.partner or crm.lead unsubcribe, we assume that the recipient
	  does not want to receive mass mailing anymore, at all, even if we adds him
	  to a mailing list afterwards. He is then blacklisted to avoid this.
	  With this behaviour, if a new lead is created with the same email address,
	  he won't be able to receive mail in mass_mode.
	  But he will still be able to receive '1 to 1' direct email.

Task ID 33224
Closes #25966
This commit is contained in:
David Beguin
2018-08-10 17:46:08 +02:00
parent 98ce81cac5
commit 59b4836e8d
33 changed files with 1171 additions and 252 deletions
+2 -4
View File
@@ -50,7 +50,8 @@ class Lead(models.Model):
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority desc,activity_date_deadline,id desc"
_inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin']
_inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin', 'mail.blacklist.mixin']
_primary_email = ['email_from']
def _default_probability(self):
stage_id = self._default_stage_id()
@@ -80,9 +81,6 @@ class Lead(models.Model):
tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', string='Tags', help="Classify and analyze your lead/opportunity categories like: Training, Service")
contact_name = fields.Char('Contact Name', track_visibility='onchange', track_sequence=3)
partner_name = fields.Char("Customer Name", track_visibility='onchange', track_sequence=2, index=True, help='The name of the future partner company that will be created while converting the lead into opportunity')
opt_out = fields.Boolean(string='Opt-Out', oldname='optout',
help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
"Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing.")
type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], index=True, required=True,
default=lambda self: 'lead' if self.env['res.users'].has_group('crm.group_use_lead') else 'opportunity',
help="Type is used to separate Leads and Opportunities")
+11 -6
View File
@@ -80,7 +80,13 @@
<field name="contact_name"/>
<field name="title" placeholder="Title" domain="[]" options='{"no_open": True}'/>
</div>
<field name="email_from" widget="email"/>
<field name="is_blacklisted" invisible="1"/>
<label for="email_from" class="oe_inline"/>
<div class="o_row o_row_readonly">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}"></i>
<field name="email_from" widget="email"/>
</div>
<field name="function"/>
<field name="phone" widget="phone"/>
<field name="mobile"/>
@@ -103,7 +109,6 @@
<page name="extra" string="Extra Info">
<group>
<group string="Email">
<field name="opt_out"/>
<field name="message_bounce" readonly="1" groups="base.group_no_one"/>
</group>
<group string="Tracking" groups="base.group_multi_company,base.group_no_one" name="categorization">
@@ -395,9 +400,10 @@
<separator />
<filter string="Current" name="current" domain="[('active', '=', True)]"/>
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
<filter string="Exclude Opt Out"
name='not_opt_out' domain="[('opt_out', '=', False)]"
help="Leads that did not ask not to be included in mass mailing campaigns"/>
<separator />
<filter string="Exclude blacklisted"
name='not_blacklisted' domain="[('is_blacklisted', '=', False)]"
help="Leads that did ask to not receive mass mailing anymore"/>
<separator/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -562,7 +568,6 @@
</div>
<field name="function"/>
<field name="mobile"/>
<field name="opt_out"/>
</group>
<group string="Marketing">
<field name="campaign_id" />
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="mail.res_partner_view_form_inherit_mail" />
<field name="arch" type="xml">
<field name="opt_out" position="after">
<field name="message_bounce" position="before">
<field name="property_delivery_carrier_id"/>
</field>
</field>
+1
View File
@@ -37,6 +37,7 @@
'views/ir_model_views.xml',
'views/res_partner_views.xml',
'views/mail_blacklist_views.xml',
'views/mail_channel_partner_views.xml',
],
'demo': [
'data/mail_demo.xml',
+6 -2
View File
@@ -20,9 +20,11 @@ _logger = logging.getLogger(__name__)
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_inherit = ['mail.blacklist.mixin']
_description = 'Listeners of a Channel'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
_primary_email = ['partner_email']
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email')
@@ -337,13 +339,15 @@ class Channel(models.Model):
@api.multi
def _notify_email_recipients(self, message, recipient_ids):
# Excluded Blacklisted
whitelist = self.env['res.partner'].sudo().search([('id', 'in', recipient_ids), ('is_blacklisted', '=', False)])
# real mailing list: multiple recipients (hidden by X-Forge-To)
if self.alias_domain and self.alias_name:
return {
'email_to': ','.join(formataddr((partner.name, partner.email)) for partner in self.env['res.partner'].sudo().browse(recipient_ids)),
'email_to': ','.join(formataddr((partner.name, partner.email)) for partner in whitelist),
'recipient_ids': [],
}
return super(Channel, self)._notify_email_recipients(message, recipient_ids)
return super(Channel, self)._notify_email_recipients(message, whitelist.ids)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
+1 -4
View File
@@ -16,13 +16,10 @@ class Partner(models.Model):
""" Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
to restrict usage of automatic email templates. """
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin']
_inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin', 'mail.blacklist.mixin']
_mail_flat_thread = False
message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
opt_out = fields.Boolean(
'Opt-Out', help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
"Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing.")
channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False)
@api.multi
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<odoo>
<data>
<record model="ir.ui.view" id="res_partner_view_form_short">
<field name="name">res.partner.view.form.short.inherit.mail</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_short_form" />
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email']" position="replace">
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}"></i>
<field name="email" widget="email"/>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
+9 -1
View File
@@ -7,8 +7,16 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email']" position="replace">
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}"></i>
<field name="email" widget="email"/>
</div>
</xpath>
<xpath expr="//page[@name='sales_purchases']//field[@name='user_id']" position="after">
<field name="opt_out" string="Mailing Opt-Out" groups="base.group_no_one"/>
<field name="message_bounce" groups="base.group_no_one"/>
</xpath>
<xpath expr="//sheet" position="after">
+5 -6
View File
@@ -279,7 +279,7 @@ class MailComposer(models.TransientModel):
records = self.env[self.model].browse(res_ids)
reply_to_value = self.env['mail.thread']._notify_get_reply_to_on_records(default=self.email_from, records=records)
blacklist = []
blacklisted_rec_ids = []
if mass_mail_mode and hasattr(self.env[self.model], "_primary_email"):
blacklist = self.env['mail.blacklist'].sudo().search([]).mapped('email')
if blacklist:
@@ -339,11 +339,10 @@ class MailComposer(models.TransientModel):
{'model': 'mail.message', 'res_id': 0}
)
# Filter out the blacklisted records by setting the mail state to cancel -> Used for Mass Mailing stats
if blacklist:
if res_id in blacklisted_rec_ids:
mail_values['state'] = 'cancel'
# Do not post the mail into the recipient's chatter
mail_values['notification'] = False
if res_id in blacklisted_rec_ids:
mail_values['state'] = 'cancel'
# Do not post the mail into the recipient's chatter
mail_values['notification'] = False
results[res_id] = mail_values
return results
+1
View File
@@ -38,6 +38,7 @@
'views/snippets_themes_options.xml',
'views/res_users_views.xml',
'views/mass_mailing_blacklist_views.xml',
'views/unsubscribe_templates.xml',
],
'demo': [
'data/mass_mailing_demo.xml',
+99 -12
View File
@@ -21,21 +21,59 @@ class MassMailController(http.Controller):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
res_id = res_id and int(res_id)
res_ids = []
if mailing.mailing_model_name == 'mail.mass_mailing.list':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([
('email', '=', email),
('list_ids', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
])
res_ids = contacts.ids
else:
res_ids = [res_id]
right_token = mailing._unsubscribe_token(res_id, email)
if not consteq(str(token), right_token):
raise exceptions.AccessDenied()
mailing.update_opt_out(email, res_ids, True)
return _('You have been unsubscribed successfully')
if mailing.mailing_model_real == 'mail.mass_mailing.contact':
# Unsubscribe directly + Let the user choose his subscriptions
mailing.update_opt_out(email, mailing.contact_list_ids.ids, True)
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', '=', 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])
unique_list_ids = set([list.list_id.id for list in subscription_list_ids])
list_ids = request.env['mail.mass_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_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:
blacklist_rec = request.env['mail.blacklist'].sudo()._add(email)
blacklist_rec._message_log(_("""The %s asked to not be contacted anymore
using an unsubscribe link.""" % request.env['ir.model']._get(mailing.mailing_model_real).display_name))
return request.render('mass_mailing.page_unsubscribed', {
'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'),
})
return request.redirect('/web')
@http.route('/mail/mailing/unsubscribe', type='json', auth='none')
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing._unsubscribe_token(res_id, email) != token:
return 'unauthorized'
if mailing.exists():
mailing.update_opt_out(email, opt_in_ids, False)
mailing.update_opt_out(email, opt_out_ids, True)
return True
return 'error'
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='none')
def track_mail_open(self, mail_id, **post):
@@ -55,3 +93,52 @@ class MassMailController(http.Controller):
request.env['link.tracker.click'].add_click(code, request.httprequest.remote_addr, country_code, stat_id=stat_id)
return werkzeug.utils.redirect(request.env['link.tracker'].get_url_from_code(code), 301)
@http.route('/mailing/blacklist/check', type='json', auth='none')
def blacklist_check(self, mailing_id, res_id, email, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing._unsubscribe_token(res_id, email) != token:
return 'unauthorized'
if email:
record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=ilike', email)])
if record['active']:
return True
return False
return 'error'
@http.route('/mailing/blacklist/add', type='json', auth='none')
def blacklist_add(self, mailing_id, res_id, email, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing._unsubscribe_token(res_id, email) != token:
return 'unauthorized'
if email:
blacklist_rec = request.env['mail.blacklist'].sudo()._add(email)
blacklist_rec._message_log(_("""The %s asked to not be contacted anymore
using the unsubscription page.""" % request.env['ir.model']._get(mailing.mailing_model_real).display_name))
return True
return 'error'
@http.route('/mailing/blacklist/remove', type='json', auth='none')
def blacklist_remove(self, mailing_id, res_id, email, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing._unsubscribe_token(res_id, email) != token:
return 'unauthorized'
if email:
request.env['mail.blacklist'].sudo()._remove(email)
return True
return 'error'
@http.route('/mailing/feedback', type='json', auth='none')
def send_feedback(self, mailing_id, res_id, email, feedback, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing._unsubscribe_token(res_id, email) != token:
return 'unauthorized'
if mailing.exists() and email:
model = request.env[mailing.mailing_model_real]
[email_field] = model._primary_email
record = model.sudo().search([(email_field, '=ilike', email)])
if record:
record.sudo().message_post(body=_("Feedback from %s: %s" % (email, feedback)))
return True
return False
return 'error'
@@ -28,7 +28,6 @@
<field name="name">Carol Cartridge</field>
<field name="email">cc@example.com</field>
<field name="list_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_list_2')])]"/>
<field name="opt_out" eval="True"/>
</record>
<!-- Create campaign and mailings -->
+100 -44
View File
@@ -38,6 +38,53 @@ class MassMailingTag(models.Model):
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
class MassMailingContactListRel(models.Model):
""" Intermediate model between mass mailing list and mass mailing contact
Indicates if a contact is opted out for a particular list
"""
_name = 'mail.mass_mailing.list_contact_rel'
_description = 'Mass Mailing Subscription Information'
_table = 'mail_mass_mailing_contact_list_rel'
_rec_name = 'contact_id'
contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True)
list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True)
opt_out = fields.Boolean(string='Opt Out',
help='The contact has chosen not to receive mails anymore from this list', default=False)
unsubscription_date = fields.Datetime(string='Unsubscription Date')
contact_count = fields.Integer(related='list_id.contact_nbr', store=False)
message_bounce = fields.Integer(related='contact_id.message_bounce', store=False)
is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False)
_sql_constraints = [
('unique_contact_list', 'unique (contact_id, list_id)',
'A contact cannot be subscribed multiple times to the same list!')
]
@api.model
def create(self, vals):
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContactListRel, self).create(vals)
@api.multi
def write(self, vals):
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContactListRel, self).write(vals)
def action_open_mailing_list_contact(self):
contact_id = self.contact_id
action = {
'name': _(contact_id.name),
'type': 'ir.actions.act_window',
'res_model': 'mail.mass_mailing.contact',
'view_mode': 'form',
'target': 'current',
'res_id': contact_id.id
}
return action
class MassMailingList(models.Model):
"""Model of a contact list. """
@@ -52,8 +99,12 @@ class MassMailingList(models.Model):
contact_ids = fields.Many2many(
'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id',
string='Mailing Lists')
subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id',
string='Subscription Information')
is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription"
" page to allows him to update his subscription preferences.")
# Compute number of contacts non opt-out for a mailing list
# Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list
def _compute_contact_nbr(self):
self.env.cr.execute('''
select
@@ -62,7 +113,9 @@ class MassMailingList(models.Model):
mail_mass_mailing_contact_list_rel r
left join mail_mass_mailing_contact c on (r.contact_id=c.id)
where
c.opt_out <> true
COALESCE(r.opt_out,FALSE) = FALSE
AND c.email NOT IN (select email from mail_blacklist where active = TRUE)
AND substring(c.email, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
group by
list_id
''')
@@ -70,6 +123,10 @@ class MassMailingList(models.Model):
for mailing_list in self:
mailing_list.contact_nbr = data.get(mailing_list.id, 0)
@api.multi
def name_get(self):
return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self]
@api.multi
def action_merge(self, src_lists, archive):
"""
@@ -113,7 +170,8 @@ class MassMailingList(models.Model):
mail_mass_mailing_contact_list_rel contact_list_rel,
mail_mass_mailing_list mailing_list
WHERE contact.id=contact_list_rel.contact_id
AND contact.opt_out = FALSE
AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE
AND contact.email NOT IN (select email from mail_blacklist where active = TRUE)
AND mailing_list.id=contact_list_rel.list_id
AND mailing_list.id IN %s
AND NOT EXISTS
@@ -143,7 +201,7 @@ class MassMailingContact(models.Model):
be able to deal with large contact list to email without bloating the partner
base."""
_name = 'mail.mass_mailing.contact'
_inherit = 'mail.thread'
_inherit = ['mail.thread', 'mail.blacklist.mixin']
_description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
@@ -156,25 +214,12 @@ class MassMailingContact(models.Model):
list_ids = fields.Many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel',
'contact_id', 'list_id', string='Mailing Lists')
opt_out = fields.Boolean(string='Opt Out', help='The contact has chosen not to receive mails anymore from this list')
unsubscription_date = fields.Datetime(string='Unsubscription Date')
subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel',
'contact_id', string='Subscription Information')
message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.', default=0)
country_id = fields.Many2one('res.country', string='Country')
tag_ids = fields.Many2many('res.partner.category', string='Tags')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContact, self).create(vals_list)
@api.multi
def write(self, vals):
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContact, self).write(vals)
def get_name_email(self, name):
name, email = self.env['res.partner']._parse_partner_name(name)
if name and not email:
@@ -246,6 +291,7 @@ class MassMailingCampaign(models.Model):
total = fields.Integer(compute="_compute_statistics")
scheduled = fields.Integer(compute="_compute_statistics")
failed = fields.Integer(compute="_compute_statistics")
ignored = fields.Integer(compute="_compute_statistics")
sent = fields.Integer(compute="_compute_statistics", string="Sent Emails")
delivered = fields.Integer(compute="_compute_statistics")
opened = fields.Integer(compute="_compute_statistics")
@@ -278,8 +324,9 @@ class MassMailingCampaign(models.Model):
c.id as campaign_id,
COUNT(s.id) AS total,
COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied ,
@@ -296,7 +343,7 @@ class MassMailingCampaign(models.Model):
""", (tuple(self.ids), ))
for row in self.env.cr.dictfetchall():
total = row['total'] or 1
total = (row['total'] - row['ignored']) or 1
row['delivered'] = row['sent'] - row['bounced']
row['received_ratio'] = 100.0 * row['delivered'] / total
row['opened_ratio'] = 100.0 * row['opened'] / total
@@ -408,6 +455,7 @@ class MassMailing(models.Model):
total = fields.Integer(compute="_compute_total")
scheduled = fields.Integer(compute="_compute_statistics")
expected = fields.Integer(compute="_compute_statistics")
ignored = fields.Integer(compute="_compute_statistics")
sent = fields.Integer(compute="_compute_statistics")
delivered = fields.Integer(compute="_compute_statistics")
opened = fields.Integer(compute="_compute_statistics")
@@ -451,8 +499,9 @@ class MassMailing(models.Model):
m.id as mailing_id,
COUNT(s.id) AS expected,
COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
@@ -470,7 +519,7 @@ class MassMailing(models.Model):
m.id
""", (tuple(self.ids), ))
for row in self.env.cr.dictfetchall():
total = row['expected'] or 1
total = row['expected'] = (row['expected'] - row['ignored']) or 1
row['received_ratio'] = 100.0 * row['delivered'] / total
row['opened_ratio'] = 100.0 * row['opened'] / total
row['clicks_ratio'] = 100.0 * row['clicked'] / total
@@ -555,14 +604,24 @@ class MassMailing(models.Model):
def _group_expand_states(self, states, domain, order):
return [key for key, val in type(self).state.selection]
def update_opt_out(self, email, res_ids, value):
model = self.env[self.mailing_model_real].with_context(active_test=False)
if 'opt_out' in model._fields:
email_fname = 'email_from'
if 'email' in model._fields:
email_fname = 'email'
records = model.search([('id', 'in', res_ids), (email_fname, 'ilike', email)])
records.write({'opt_out': value})
def update_opt_out(self, email, list_ids, value):
if len(list_ids) > 0:
model = self.env['mail.mass_mailing.contact'].with_context(active_test=False)
records = model.search([('email', '=ilike', email)])
opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([
('contact_id', 'in', records.ids),
('list_id', 'in', list_ids),
('opt_out', '!=', value)
])
opt_out_records.write({'opt_out': value})
message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
if value else _('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'))))
#------------------------------------------------------
# Views & Actions
@@ -629,19 +688,16 @@ class MassMailing(models.Model):
self.ensure_one()
opt_out = {}
target = self.env[self.mailing_model_real]
mail_field = 'email' if 'email' in target._fields else 'email_from'
if 'opt_out' in target._fields:
# avoid loading a large number of records in memory
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM %(target)s
WHERE opt_out AND
substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL;
"""
query = query % {'target': target._table, 'mail_field': mail_field}
self._cr.execute(query)
opt_out = set(m[0] for m in self._cr.fetchall())
if self.mailing_model_real == "mail.mass_mailing.contact":
# if user is opt_out on One list but not on another
# or if two user with same email address, one opted in and the other one opted out, send the mail anyway
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search(
[('list_id', 'in', self.contact_list_ids.ids)])
opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email')
opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email')
opt_out = set(c for c in opt_out_contacts if c not in opt_in_contacts)
_logger.info(
"Mass-mailing %s targets %s, blacklist: %s emails",
self, target._name, len(opt_out))
@@ -33,6 +33,8 @@ class MailMailStats(models.Model):
string='Mass Mailing Campaign',
store=True, readonly=True, index=True)
# Bounce and tracking
ignored = fields.Datetime(help='Date when the email has been invalidated. '
'Invalid emails are blacklisted, opted-out or invalid email format')
scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now)
sent = fields.Datetime(help='Date when the email has been sent')
exception = fields.Datetime(help='Date of technical error leading to the email not being sent')
@@ -49,17 +51,20 @@ class MailMailStats(models.Model):
('sent', 'Sent'),
('opened', 'Opened'),
('replied', 'Replied'),
('bounced', 'Bounced')], store=True)
('bounced', 'Bounced'),
('ignored', 'Ignored')], store=True)
state_update = fields.Datetime(compute="_compute_state", string='State Update',
help='Last state update of the mail',
store=True)
recipient = fields.Char(compute="_compute_recipient")
@api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception')
@api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored')
def _compute_state(self):
self.update({'state_update': fields.Datetime.now()})
for stat in self:
if stat.exception:
if stat.ignored:
stat.state = 'ignored'
elif stat.exception:
stat.state = 'exception'
elif stat.sent:
stat.state = 'sent'
@@ -11,6 +11,9 @@ class ResConfigSettings(models.TransientModel):
mass_mailing_outgoing_mail_server = fields.Boolean(string="Specific Mail Server", config_parameter='mass_mailing.outgoing_mail_server',
help='Use a specific mail server in priority. Otherwise Odoo relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.')
mass_mailing_mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server', config_parameter='mass_mailing.mail_server_id')
show_blacklist_buttons = fields.Boolean(string="Show blacklist buttons on unsubscribe page",
config_parameter='mass_mailing.show_blacklist_buttons',
help="""Allow the recipient to manage himself his state in the blacklist via the unsubscription page.""")
@api.onchange('mass_mailing_outgoing_mail_server')
def _onchange_mass_mailing_outgoing_mail_server(self):
@@ -13,3 +13,4 @@ access_mail_statistics_report,mail.statistics.report,model_mail_statistics_repor
access_utm_source,access_utm_source,utm.model_utm_source,mass_mailing.group_mass_mailing_user,1,1,1,0
access_ir_mail_server,access_ir_mail_server,base.model_ir_mail_server,mass_mailing.group_mass_mailing_user,1,0,0,0
access_mail_blacklist_mass_mailing_user,access.mail.blacklist.mass_mailing_user,mail.model_mail_blacklist,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mail_mass_mailing_list_contact_rel,access.mail.mass_mailing_list_contact_rel,model_mail_mass_mailing_list_contact_rel,mass_mailing.group_mass_mailing_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
13 access_utm_source access_utm_source utm.model_utm_source mass_mailing.group_mass_mailing_user 1 1 1 0
14 access_ir_mail_server access_ir_mail_server base.model_ir_mail_server mass_mailing.group_mass_mailing_user 1 0 0 0
15 access_mail_blacklist_mass_mailing_user access.mail.blacklist.mass_mailing_user mail.model_mail_blacklist mass_mailing.group_mass_mailing_user 1 1 1 1
16 access_mail_mass_mailing_list_contact_rel access.mail.mass_mailing_list_contact_rel model_mail_mass_mailing_list_contact_rel mass_mailing.group_mass_mailing_user 1 1 1 1
@@ -0,0 +1,192 @@
odoo.define('mass_mailing.unsubscribe', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
require('web.dom_ready');
var _t = core._t;
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) {
return $.Deferred().reject("DOM doesn't contain '.o_unsubscribe_form'");
}
if (email != '' && email != undefined){
ajax.jsonRpc('/mailing/blacklist/check', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
.then(function (result) {
if (result == 'unauthorized'){
$('#button_add_blacklist').hide();
$('#button_remove_blacklist').hide();
}
else if (result == true) {
$('#button_remove_blacklist').show();
toggle_opt_out_section(false);
}
else if (result == false) {
$('#button_add_blacklist').show();
toggle_opt_out_section(true);
}
else {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
}
else {
$('#div_blacklist').hide();
}
var unsubscribed_list = $("input[name='unsubscribed_list']").val();
if (unsubscribed_list){
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed from ' + unsubscribed_list + "</strong>."));
}
else{
$('#subscription_info').html(_t('You have been <strong>successfully unsubscribed</strong>.'));
}
$('#unsubscribe_form').on('submit', function (e) {
e.preventDefault();
var checked_ids = [];
$("input[type='checkbox']:checked").each(function (i){
checked_ids[i] = parseInt($(this).val());
});
var unchecked_ids = [];
$("input[type='checkbox']:not(:checked)").each(function (i){
unchecked_ids[i] = parseInt($(this).val());
});
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', {'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').html(_t('You are not authorized to do this!'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else if (result == true) {
$('#subscription_info').html(_t('Your changes have been saved.'));
$('#info_state').removeClass('alert-info').addClass('alert-success');
}
else {
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.'));
$('#info_state').removeClass('alert-info').addClass('alert-warning');
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.'));
$('#info_state').removeClass('alert-info').addClass('alert-warning');
});
});
// ==================
// Blacklist
// ==================
$('#button_add_blacklist').click(function (e) {
e.preventDefault();
ajax.jsonRpc('/mailing/blacklist/add', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
.then(function (result) {
if (result == 'unauthorized'){
$('#subscription_info').html(_t('You are not authorized to do this!'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else
{
if (result) {
$('#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').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
$('#button_add_blacklist').hide();
$('#button_remove_blacklist').show();
$('#unsubscribed_info').hide();
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
});
$('#button_remove_blacklist').click(function (e) {
e.preventDefault();
ajax.jsonRpc('/mailing/blacklist/remove', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token})
.then(function (result) {
if (result == 'unauthorized'){
$('#subscription_info').html(_t('You are not authorized to do this!'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else
{
if (result) {
$('#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').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
}
$('#button_add_blacklist').show();
$('#button_remove_blacklist').hide();
$('#unsubscribed_info').hide();
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error');
});
});
// ==================
// Feedback
// ==================
$('#button_feedback').click(function (e) {
var feedback = $("textarea[name='opt_out_feedback']").val();
e.preventDefault();
ajax.jsonRpc('/mailing/feedback', 'call', {'mailing_id': mailing_id, 'res_id': res_id, 'email': email, 'feedback': feedback, 'token': token})
.then(function (result) {
if (result == 'unauthorized'){
$('#subscription_info').html(_t('You are not authorized to do this!'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
else if (result == true){
$('#subscription_info').html(_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').html(_t('An error occured. Please try again later or contact us.'));
$('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning');
}
})
.fail(function () {
$('#subscription_info').html(_t('An error occured. 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'); }
}
@@ -14,3 +14,7 @@
}
}
}
.o_white_body {
background-color: white;
}
+4
View File
@@ -3,3 +3,7 @@
from . import test_mass_mailing_list_merge
from . import test_mass_mailing_content
from . import test_mail_auto_blacklist
from . import test_blacklist_access_rights
from . import test_mass_mailing
from . import test_blacklisted_channel_partner
@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import AccessError
from odoo.tests import common
class TestAccessRights(common.TransactionCase):
def setUp(self):
super(TestAccessRights, self).setUp()
users = self.env['res.users'].with_context(no_reset_password=True)
partners = self.env['res.partner'].with_context(no_reset_password=True)
mass_mailing_contacts = self.env['mail.mass_mailing.contact'].with_context(no_reset_password=True)
group_user = self.env.ref('mass_mailing.group_mass_mailing_user')
# Clear blacklist
blacklist = self.env['mail.blacklist'].sudo().search([])
i = len(blacklist)
while i != 0:
blacklist[i-1].sudo().unlink()
i -= 1
# Create users
self.user_mm_user = users.create({
'name': 'Andrew User',
'login': 'user',
'email': 'a.m@example.com',
'groups_id': [(6, 0, [group_user.id])]
})
self.user_employee = users.create({
'name': 'Bert Tartignole',
'login': 'bert',
'email': 'b.t@example.com',
'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]
})
self.user_portal = users.create({
'name': 'Chell Gladys',
'login': 'chell',
'email': 'chell@gladys.portal',
'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])]
})
self.contact_blacklisted = mass_mailing_contacts.create({
'name': 'Jack Black',
'email': 'contact@black.list'
})
self.partner_blacklisted = partners.create({
'name': 'Joe Black',
'email': 'test@black.list'
})
# Create a blacklist entry
self.blacklist_entry = self.env['mail.blacklist'].create({
'email': 'test@black.list',
})
self.blacklist_contact_entry = self.env['mail.blacklist'].create({
'email': 'contact@black.list',
})
def test_access_mass_mailing_user(self):
""" Test Mass Mailing user's access rights """
# ===================
# Test on res_partner
# ===================
# mass mailing user can see a blacklist entry
self.blacklist_entry.sudo(self.user_mm_user).read()
# mass mailing user can edit a blacklist entry
self.blacklist_entry.sudo(self.user_mm_user).write({'email': 'edit@black.list'})
# mass mailing user can create a blacklist entry
self.env['mail.blacklist'].sudo(self.user_mm_user).create({
'email': 'another@black.list',
})
self.assertTrue(len(self.env['mail.blacklist'].search([])) == 3,
'Number of blacklisted address incorrect, should be equals to 3')
# mass mailing user can delete a blacklist entry
self.blacklist_entry.sudo(self.user_mm_user).unlink()
self.assertTrue(len(self.env['mail.blacklist'].search([])) == 2,
'Number of blacklisted address incorrect, should be equals to 2')
# mass mailing user can see if user is balcklisted using mixin
self.assertFalse(self.partner_blacklisted.sudo(self.user_mm_user).is_blacklisted,
'Blacklist state incorrect. Should not be blacklisted.')
# ============================
# Test on mass mailing contact
# ============================
self.blacklist_contact_entry.sudo(self.user_mm_user).read()
self.blacklist_contact_entry.sudo(self.user_mm_user).write({'email': 'edit@black.list'})
self.env['mail.blacklist'].sudo(self.user_mm_user).create({
'email': 'again_another@black.list',
})
self.assertTrue(len(self.env['mail.blacklist'].search([])) == 3,
'Number of blacklisted address incorrect, should be equals to 3')
self.blacklist_contact_entry.sudo(self.user_mm_user).unlink()
self.assertTrue(len(self.env['mail.blacklist'].search([])) == 2,
'Number of blacklisted address incorrect, should be equals to 2')
def test_access_portal_user(self):
""" Test portal user's access rights """
# Portal user can't see blacklist entries
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_portal).read()
# Portal user can't edit a blacklist entry
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_portal).write({'email': 'test@edit.be'})
# Portal user can't create a blacklist entry
with self.assertRaises(AccessError):
self.env['mail.blacklist'].sudo(self.user_portal).create({
'email': 'another@black.list',
})
# Portal user can't delete a blacklist entry
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_portal).unlink()
# Portal user can't see if user is balcklisted using mixin
with self.assertRaises(AccessError):
self.partner_blacklisted.sudo(self.user_portal).is_blacklisted()
self.assertTrue(len(self.env['mail.blacklist'].search([])) == 2,
'Number of blacklisted address incorrect, should be equals to 2')
def test_access_employee(self):
""" Test classic employee's access rights """
# Employee can't see blacklist entries
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_employee).read()
# Employee can't edit a blacklist entry
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_employee).write({'email': 'test@edit.be'})
# Employee can't create a blacklist entry
with self.assertRaises(AccessError):
self.env['mail.blacklist'].sudo(self.user_employee).create({
'email': 'another@black.list',
})
# Employee can't delete a blacklist entry
with self.assertRaises(AccessError):
self.blacklist_entry.sudo(self.user_employee).unlink()
# Employee can see if user is balcklisted using mixin
self.assertTrue(self.partner_blacklisted.sudo(self.user_employee).is_blacklisted,
'Blacklist state incorrect. Should be blacklisted.')
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from email.utils import formataddr
from odoo.addons.test_mail.tests import common
class TestChannelPartnersNotification(common.MockEmails):
def _join_channel(self, channel, partners):
for partner in partners:
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner.id})]})
channel.invalidate_cache()
def test_channel_blacklisted_recipients(self):
""" Posting a message on a channel should send one email to all recipients, except the blacklisted ones """
self.test_channel = self.env['mail.channel'].create({
'name': 'Test',
'description': 'Description',
'alias_name': 'test',
'public': 'public',
})
self.test_partner = self.env['res.partner'].create({
'name': 'Test Partner',
'email': 'test@example.com',
})
self.blacklisted_partner = self.env['res.partner'].create({
'name': 'Blacklisted Partner',
'email': 'test@black.list',
})
# Set Blacklist
self.env['mail.blacklist'].create({
'email': 'test@black.list',
})
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
self.test_channel.write({'email_send': True})
self._join_channel(self.test_channel, self.test_partner)
self.test_channel.message_post(body="Test", message_type='comment', subtype='mt_comment')
self.assertEqual(len(self._mails), 1, 'Number of mail incorrect. Should be equal to 1.')
for email in self._mails:
self.assertEqual(
set(email['email_to']),
set([formataddr((self.test_partner.name, self.test_partner.email))]),
'email_to incorrect. Should be equal to "%s"' % (
formataddr((self.test_partner.name, self.test_partner.email))))
@@ -0,0 +1,251 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
class TestAccessRights(common.TransactionCase):
def test_01_mass_mail_blacklist(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing = self.env['mail.mass_mailing']
mail_blacklist = self.env['mail.blacklist']
# create mailing contact record
self.mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'})
self.mailing_contact_2 = mass_mailing_contacts.create({'name': 'test email 2', 'email': 'test2@email.com'})
self.mailing_contact_3 = mass_mailing_contacts.create({'name': 'test email 3', 'email': 'test3@email.com'})
self.mailing_contact_4 = mass_mailing_contacts.create({'name': 'test email 4', 'email': 'test4@email.com'})
self.mailing_contact_5 = mass_mailing_contacts.create({'name': 'test email 5', 'email': 'test5@email.com'})
# create blacklist record
mail_blacklist.create({'email': self.mailing_contact_3.email})
mail_blacklist.create({'email': self.mailing_contact_4.email})
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'test',
'mailing_domain': [('id', 'in',
[self.mailing_contact_1.id, self.mailing_contact_2.id, self.mailing_contact_3.id,
self.mailing_contact_4.id, self.mailing_contact_5.id])],
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
'model': self.mass_mailing.mailing_model_real,
'email_from': self.mass_mailing.email_from,
'composition_mode': 'mass_mail',
'mass_mailing_id': self.mass_mailing.id,
'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids],
}
composer = self.env['mail.compose.message'].with_context(active_ids=res_ids,
mass_mailing_seen_list=self.mass_mailing._get_seen_list()).create(
composer_values)
composer.send_mail()
self.assertEqual(self.mass_mailing.ignored, 2,
'blacklist ignored email number incorrect, should be equals to 2')
def test_02_mass_mail_simple_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'})
mailing_contact_2 = mass_mailing_contacts.create({'name': 'test email 2', 'email': 'test2@email.com'})
# create mailing list record
mailing_list_1 = mass_mailing_lists.create({
'name': 'A',
'contact_ids': [
(4, mailing_contact_1.id),
(4, mailing_contact_2.id)
]
})
# Set Opt out
mass_mailing_optout.search([('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id)]).write({
'opt_out': True,
})
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'One',
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id]
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
'model': self.mass_mailing.mailing_model_real,
'email_from': self.mass_mailing.email_from,
'composition_mode': 'mass_mail',
'mass_mailing_id': self.mass_mailing.id,
'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids],
}
composer = self.env['mail.compose.message'].with_context(
active_ids=res_ids,
mass_mailing_seen_list=self.mass_mailing._get_seen_list(),
mass_mailing_opt_out_list=self.mass_mailing._get_opt_out_list()).create(composer_values)
composer.send_mail()
self.assertEqual(self.mass_mailing.ignored, 1,
'Opt Out ignored email number incorrect, should be equals to 1')
def test_03_mass_mail_multi_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'})
mailing_contact_2 = mass_mailing_contacts.create({'name': 'test email 2', 'email': 'test2@email.com'})
# create mailing list record
mailing_list_1 = mass_mailing_lists.create({
'name': 'A',
'contact_ids': [
(4, mailing_contact_1.id),
(4, mailing_contact_2.id)
]
})
mailing_list_2 = mass_mailing_lists.create({
'name': 'B',
'contact_ids': [
(4, mailing_contact_1.id),
(4, mailing_contact_2.id)
]
})
# Set Opt out
mass_mailing_optout.search([('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id)]).write({
'opt_out': True,
})
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'One',
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id, mailing_list_2.id]
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
'model': self.mass_mailing.mailing_model_real,
'email_from': self.mass_mailing.email_from,
'composition_mode': 'mass_mail',
'mass_mailing_id': self.mass_mailing.id,
'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids],
}
composer = self.env['mail.compose.message'].with_context(
active_ids=res_ids,
mass_mailing_seen_list=self.mass_mailing._get_seen_list(),
mass_mailing_opt_out_list=self.mass_mailing._get_opt_out_list()).create(composer_values)
composer.send_mail()
# if user is opt_out on One list but not on another, send the mail anyway
self.assertEqual(self.mass_mailing.ignored, 0,
'Opt Out ignored email number incorrect, should be equals to 0')
def test_04_mass_mail_multi_users_different_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email (A)', 'email': 'test@email.com'})
mailing_contact_2 = mass_mailing_contacts.create({'name': 'test email (B)', 'email': 'test@email.com'})
# create mailing list record
mailing_list_1 = mass_mailing_lists.create({
'name': 'A',
'contact_ids': [
(4, mailing_contact_1.id),
(4, mailing_contact_2.id)
]
})
# Set Opt out
mass_mailing_optout.search([('contact_id', '=', mailing_contact_1.id), ('list_id', '=', mailing_list_1.id)]).write({
'opt_out': True,
})
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'One',
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id]
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
'model': self.mass_mailing.mailing_model_real,
'email_from': self.mass_mailing.email_from,
'composition_mode': 'mass_mail',
'mass_mailing_id': self.mass_mailing.id,
'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids],
}
composer = self.env['mail.compose.message'].with_context(
active_ids=res_ids,
mass_mailing_seen_list=self.mass_mailing._get_seen_list(),
mass_mailing_opt_out_list=self.mass_mailing._get_opt_out_list()).create(composer_values)
composer.send_mail()
# if user A is opt_out on One list but user B is not opt_out on same list,
# send the mail anyway to the opt-in contact. Ignore (as normal behaviour) opted-out contact.
self.assertEqual(self.mass_mailing.ignored, 1,
'Opt Out ignored email number incorrect, should be equals to 1')
class TestOnResPartner(common.TransactionCase):
def test_mass_mail_on_res_partner(self):
partners = self.env['res.partner']
mass_mailing = self.env['mail.mass_mailing']
# create mailing contact record
partner_a = partners.create({
'name': 'test email 1',
'email': 'test1@email.com',
})
partner_b = partners.create({
'name': 'test email 2',
'email': 'test2@email.com',
})
# Set Blacklist
self.blacklist_contact_entry = self.env['mail.blacklist'].create({
'email': 'test2@email.com',
})
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'One',
'mailing_domain': [('id', 'in', [partner_a.id, partner_b.id])],
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.mailing_model_real = 'res.partner'
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
'model': self.mass_mailing.mailing_model_real,
'email_from': self.mass_mailing.email_from,
'composition_mode': 'mass_mail',
'mass_mailing_id': self.mass_mailing.id,
'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids],
}
composer = self.env['mail.compose.message'].with_context(
active_ids=res_ids,
mass_mailing_seen_list=self.mass_mailing._get_seen_list()).create(composer_values)
composer.send_mail()
# if user is opt_out on One list but not on another, send the mail anyway
self.assertEqual(self.mass_mailing.ignored, 1,
'Blacklisted ignored email number incorrect, should be equals to 1')
@@ -13,6 +13,7 @@
<filter string="Received" name="filter_received" domain="[('sent', '!=', False), ('bounced', '=', False)]"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False)]"/>
<filter string="Failed" name="filter_failed" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '!=', False)]"/>
<filter string="Ignored" name="filter_ignored" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False), ('ignored', '!=', False)]"/>
<separator/>
<filter string="Opened" name="filter_opened" domain="[('opened', '!=', False)]"/>
<filter string="Clicked" name="filter_clicked" domain="[('clicked', '!=', False)]"/>
@@ -5,6 +5,7 @@
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.scss"/>
<link rel="stylesheet" href="/mass_mailing/static/src/css/email_template.css"/>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/unsubscribe.js"></script>
</xpath>
</template>
+109 -26
View File
@@ -12,20 +12,68 @@
parent="mass_mailing_menu_root" sequence="100"/>
<!-- MAILING CONTACT -->
<!-- Custom reports (aka filters) -->
<record id="filter_contact_subscription" model="ir.filters">
<field name="name">List Subscription</field>
<field name="model_id">mail.mass_mailing.contact</field>
<field name="user_id" eval="False"/>
<field name="context">{'group_by': ['create_date:month']}</field>
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_rel_view_form">
<field name="name">mail.mass_mailing.list_contact_rel.view.form</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List contacts">
<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" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}"></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 id="filter_contact_unsubscription" model="ir.filters">
<field name="name">List Unsubscription</field>
<field name="model_id">mail.mass_mailing.contact</field>
<field name="user_id" eval="False"/>
<field name="domain">[('opt_out','=',True)]</field>
<field name="context">{'group_by': ['unsubscription_date:month']}</field>
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_rel_list_contact_view_tree">
<field name="name">mail.mass_mailing.list_contact_rel.list_contact.view.tree</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<field name="contact_id"/>
<field name="unsubscription_date"/>
<field name="opt_out"/>
<field name="message_bounce"/>
<field name="is_blacklisted"/>
<button name="action_open_mailing_list_contact" string="Open Contact" type="object" icon="fa-user"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_view_search">
<field name="name">mail.mass_mailing.list_contact_rel.view.search</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="arch" type="xml">
<search string="Mailing List Contacts">
<field name="contact_id"/>
<field name="opt_out"/>
<field name="list_id"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"/>
<separator/>
<filter string="Exclude Blacklisted" name="not_blacklisted" domain="[('contact_id.is_blacklisted', '=', False)]"/>
<separator/>
<filter string="Valid Recipents" name="valid_recipients" domain="[('opt_out', '=', False), ('contact_id.is_blacklisted', '=', False)]"/>
<separator/>
<filter string="Exclude Bounced" name="not_bounced" domain="[('message_bounce', '&lt;', 3)]"/>
<group expand="0" string="Group By">
<filter string="Creation Date" name="group_create_date"
context="{'group_by': 'create_date'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
@@ -38,7 +86,8 @@
<field name="tag_ids"/>
<field name="list_ids"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"/>
<filter string="Exclude Blacklisted" name="not_blacklisted" domain="[('is_blacklisted', '=', False)]"/>
<separator/>
<separator/>
<filter string="Exclude Bounced" name="not_bounced" domain="[('message_bounce', '&lt;', 3)]"/>
<group expand="0" string="Group By">
@@ -60,7 +109,7 @@
<field name="company_name"/>
<field name="email"/>
<field name="message_bounce"/>
<field name="opt_out"/>
<field name="is_blacklisted"/>
</tree>
</field>
</record>
@@ -126,24 +175,38 @@
</div>
<group>
<group>
<field name="email"/>
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}"></i>
<field name="email" widget="email"/>
</div>
<field name="title_id"/>
<field name="company_name"/>
<field name="country_id"/>
</group>
<group>
<field name="create_date" readonly="1"/>
<field name="unsubscription_date"/>
<label for="message_bounce"/>
<div class="o_row">
<field name="message_bounce"/>
<span> times</span>
</div>
<field name="opt_out"/>
</group>
</group>
<field name="list_ids"/>
<field name="subscription_list_ids">
<tree editable="bottom">
<field name="list_id"/>
<field name="unsubscription_date"/>
<field name="opt_out"/>
</tree>
</field>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
@@ -175,7 +238,7 @@
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="context">{'search_default_not_opt_out': 1}</field>
<field name="context">{'search_default_not_blacklisted': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a contact in your address book
@@ -187,11 +250,11 @@
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts_from_list">
<field name="name">Recipients</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="res_model">mail.mass_mailing.list_contact_rel</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="domain">[('list_ids', 'in', [active_id])]</field>
<field name="context">{'search_default_not_opt_out': 1, 'default_list_ids': [(4,active_id)]}</field>
<field name="domain">[('list_id', 'in', [active_id])]</field>
<field name="context">{'search_default_valid_recipients': 1, 'default_list_id': active_id}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new recipient
@@ -211,6 +274,7 @@
<search string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Creation Month" name="group_create_date"
@@ -228,6 +292,7 @@
<tree string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<field name="contact_nbr"/>
</tree>
</field>
@@ -256,6 +321,9 @@
<field name="name" placeholder="e.g. Consumer Newsletter"/>
</h1>
</div>
<group>
<field name="is_public"/>
</group>
</sheet>
</form>
</field>
@@ -276,6 +344,9 @@
</div>
</group>
</group>
<group>
<field name="is_public"/>
</group>
<footer>
<button string="Create" name="close_dialog" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
@@ -416,7 +487,20 @@
<field name="state" readonly="1" widget="statusbar"/>
</header>
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': [('scheduled', '=', 0)]}">
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': [('ignored', '=', 0)]}">
<p>
<button class="btn-link"
name="%(action_view_mail_mail_statistics_mailing_list)d"
type="action"
context="{'search_default_filter_ignored': True}">
<strong>
<field name="ignored" class="oe_inline"/>
email(s) have been ignored and will not be sent.
</strong>
</button>
</p>
</div>
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': ['|', ('scheduled', '=', 0), ('ignored','!=',0)]}">
<p>
<button class="btn-link"
name="%(action_view_mail_mail_statistics_mailing_list)d"
@@ -505,8 +589,7 @@
<field name="mailing_model_real" invisible="1"/>
<field name="mailing_domain" widget="domain"
attrs="{'invisible': [('mailing_model_name', '=', 'mail.mass_mailing.list')]}"
options="{'model': 'mailing_model_real'}"
context="{'search_default_not_opt_out':1}">
options="{'model': 'mailing_model_real'}">
</field>
<div attrs="{'invisible': [('mailing_model_name', '!=', 'mail.mass_mailing.list')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
@@ -44,6 +44,19 @@
</div>
</div>
</div>
<div class="col-md-6 o_setting_box col-xs-12">
<div class="o_setting_left_pane" title="Allow the recipient to manage himself his state in the blacklist via the unsubscription page.
If the option is active, the 'Blacklist Me' button is hidden on the unsubscription page.
The 'come Back' button will always be visible in any case to allow leads and partners to re-subscribe.">
<field name="show_blacklist_buttons"/>
</div>
<div class="o_setting_right_pane">
<label for="show_blacklist_buttons"/>
<div class="text-muted">
Allow the recipient to manage himself his state in the blacklist via the unsubscription page.
</div>
</div>
</div>
</div>
<h2>Mailing Lists</h2>
<div id="mailing_list" class="row mt16 o_settings_container"/>
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="unsubscribe">
<div class="container o_unsubscribe_form">
<div class="row">
<form action="/mail/mailing/unsubscribe" method="POST" id="unsubscribe_form">
<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 class="col-lg-6 offset-lg-3 mt32">
<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 unsubscribed</p>
<textarea class="form-control" name="opt_out_feedback" cols="60" rows="3"></textarea>
<br/>
<div class="btn btn-primary text-left" id="button_feedback">Send</div>
</div>
</div>
<h1 class="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" class="mail_list_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"/>
</li>
</t>
</t>
</ul>
<div class="mb64">
<div t-if="show_blacklist_button">
<div class="btn btn-default pull-right" id="button_add_blacklist" style="display:none">Blacklist Me</div>
</div>
<div class="btn btn-default pull-right" id="button_remove_blacklist" style="display:none">Come Back</div>
<input type="submit" id="send_form" class="btn btn-primary" value="Update my subscriptions"/>
</div>
</div>
</t>
<t t-if="contacts == False">
<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="unsubscribed">
<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="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="show_blacklist_button" class="mb64">
<div class="btn btn-default pull-right" id="button_add_blacklist" style="display:none">Blacklist Me</div>
<div class="btn btn-default pull-right" id="button_remove_blacklist" style="display:none">Unblacklist Me</div>
</div>
</div>
</div>
</div>
</template>
<template id="page_unsubscribe" name="Unsubscribe">
<t t-call="mass_mailing.layout">
<t t-call="mass_mailing.unsubscribe"/>
</t>
</template>
<template id="page_unsubscribed" name="Unsubscribed">
<t t-call="mass_mailing.layout">
<t t-call="mass_mailing.unsubscribed"/>
</t>
</template>
<!-- new layout for mass_mailing -->
<template id="mass_mailing.layout" name="Mass Mailing Layout">
<t t-call="web.layout">
<t t-set="head">
<t t-call-assets="web.assets_common"/>
<t t-call-assets="mass_mailing.assets_backend"/>
</t>
<body class="o_white_body">
<header>
<div><title>Odoo</title></div>
<div class="text-center">
<img t-attf-src="/web/binary/company_logo"/>
</div>
</header>
<div id="wrap" class="oe_structure oe_empty"/>
<main>
<t t-raw="0"/>
</main>
</body>
<xpath expr="//footer" position="replace">
<div class="container mt16 mb8">
<div class="pull-right" t-ignore="true" t-if="not editable">
Create a <a target="_blank" href="https://www.odoo.com/page/website-builder">free website</a> with
<a target="_blank" class="label label-danger" href="https://www.odoo.com/page/website-builder">Odoo</a>
</div>
<div class="pull-left text-muted" itemscope="itemscope" itemtype="https://schema.org/Organization">
Copyright &amp;copy; <span t-field="res_company.name" itemprop="name">Company name</span>
</div>
</div>
</xpath>
</t>
</template>
</odoo>
@@ -77,9 +77,9 @@ class MailComposeMessage(models.TransientModel):
}
if mail_values.get('body_html') and mass_mail_layout:
mail_values['body_html'] = mass_mail_layout.render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True)
# propagate exception state to stat when still-born
# propagate ignored state to stat when still-born
if mail_values.get('state') == 'cancel':
stat_vals['exception'] = fields.Datetime.now()
stat_vals['ignored'] = fields.Datetime.now()
mail_values.update({
'mailing_id': mass_mailing.id,
'statistics_ids': [(0, 0, stat_vals)],
+4 -3
View File
@@ -72,10 +72,11 @@
display: none!important;
}
.o_row, .o_row > div {
display: inline-block;
.o_row:not(.o_row_readonly) {
&, & > div {
display: inline-block;
}
}
}
.o_form_uri {
@@ -14,7 +14,6 @@ On a simple click, your visitors can subscribe to mailing lists managed in the E
'data': [
'security/mass_mailing_security.xml',
'views/website_mass_mailing_templates.xml',
'views/unsubscribe_templates.xml',
'views/snippets_templates.xml',
'views/mass_mailing_view.xml',
'views/res_config_settings_views.xml',
@@ -7,31 +7,10 @@ from odoo.addons.mass_mailing.controllers.main import MassMailController
class MassMailController(MassMailController):
@route('/mail/mailing/<int:mailing_id>/unsubscribe', type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
if mailing.mailing_model_real == 'mail.mass_mailing.contact':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email', '=', email)])
return request.render('website_mass_mailing.page_unsubscribe', {
'contacts': contacts,
'email': email,
'mailing_id': mailing_id})
else:
super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)
return request.render('website_mass_mailing.page_unsubscribed')
@route('/mail/mailing/unsubscribe', type='json', auth='none')
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
mailing.update_opt_out(email, opt_in_ids, False)
mailing.update_opt_out(email, opt_out_ids, True)
@route('/website_mass_mailing/is_subscriber', type='json', website=True, auth="public")
def is_subscriber(self, list_id, **post):
email = None
if request.uid != request.website.user_id.id:
if not request.env.user._is_public():
email = request.env.user.email
elif request.session.get('mass_mailing_email'):
email = request.session['mass_mailing_email']
@@ -131,44 +131,3 @@ sAnimation.registry.newsletter_popup = sAnimation.Class.extend({
}
});
});
//==============================================================================
odoo.define('mass_mailing.unsubscribe', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
require('web.dom_ready');
var _t = core._t;
if (!$('.o_unsubscribe_form').length) {
return $.Deferred().reject("DOM doesn't contain '.o_unsubscribe_form'");
}
$('#unsubscribe_form').on('submit', function (e) {
e.preventDefault();
var email = $("input[name='email']").val();
var mailing_id = parseInt($("input[name='mailing_id']").val());
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());
});
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', {'opt_in_ids': checked_ids, 'opt_out_ids': unchecked_ids, 'email': email, 'mailing_id': mailing_id})
.then(function (result) {
$('.alert-info').html(_t('Your changes have been saved.')).removeClass('alert-info').addClass('alert-success');
})
.fail(function () {
$('.alert-info').html(_t('Your changes have not been saved, try again later.')).removeClass('alert-info').addClass('alert-warning');
});
});
});
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="unsubscribe">
<div class="container o_unsubscribe_form">
<div class="row">
<form action="/mail/mailing/unsubscribe" method="POST" id="unsubscribe_form">
<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"/>
<div class="col-lg-6 offset-lg-3">
<h1 class="page-header">Mailing Subscriptions</h1>
<t t-if="contacts">
<div class="alert alert-info text-center" role="status">
<p>Choose your mailing subscriptions.</p>
</div>
<ul class="list-group">
<t t-foreach="contacts" t-as="contact">
<li class="list-group-item">
<input type="checkbox" class="mail_list_checkbox" name="contact_ids"
t-att-value="contact['id']" t-att-checked="'checked' if contact['opt_out'] == False else None"/>
<t t-foreach="contact.list_ids" t-as="contact_list">
<t t-esc="contact_list.name"/>
</t>
</li>
</t>
</ul>
<div class="text-center mb16">
<input type="submit" id="send_form" class="btn btn-primary" value="Update my subscriptions"/>
</div>
</t>
<t t-if="contacts == False">
<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="unsubscribed">
<div class="container mt8">
<div class="row">
<div class="col-lg-6 offset-lg-3">
<h1 class="page-header">Mailing Subscriptions</h1>
<div class="alert alert-success text-center" role="status">
<p>You have been successfully <strong>unsubscribed</strong>!</p>
</div>
</div>
</div>
</div>
</template>
<template id="page_unsubscribe" name="Unsubscribe">
<t t-call="website.layout">
<t t-call="website_mass_mailing.unsubscribe"/>
</t>
</template>
<template id="page_unsubscribed" name="Unsubscribed">
<t t-call="website.layout">
<t t-call="website_mass_mailing.unsubscribed"/>
</t>
</template>
</odoo>