[IMP] mass_mailing_(sms): Add unblacklisting button

Previously a blacklisted phone number or email address could only be
removed from their respective blacklist by going to the corresponding
blacklist view [in mass_mailing_(sms)] and manually
archiving/deactiviting the entry. All users could see that an email was
blacklisted via an fa-ban icon added next to their corresponding field
in the modules: crm, mass_mailing, mass_mailing_sms, and contacts (via
extension).

This commit adds additional fa-ban icons next to blacklisted phone
numbers and makes all instances of these icons clickable to remove them
from their corresponding blacklist using a wizard. There are several
limitations to this implementation including:

1. If both a mobile and phone number field appear within a crm lead
instance, then only the mobile field will indicate if it is blacklisted
(due to current implementation of PhoneMixin which only checks 1 phone
value against the blacklist and "sms" which returns mobile numbers first).
I.e. if a phone field value is blacklisted, but a value is typed into the
mobile field, then the phone field will never indicate that it is blacklisted.

2. If someone clicks on a unblacklisting icon after changing the
corresponding field value without clicking off the field input, then the
latest typed in value will be the one submitted for unblacklisting,
which may not actually be blacklisted (due to "is_blacklisted" flag
being a computed field and it not having a chance to re-compute to hide
the button).

3. If someone changes values in the blacklist, then someone who already
has a form open will not see icon disappear until something triggers a
re-compute of the "phone_sanitized_blacklisted" flag (this was already the
case with the icon, but now a user may try to unblacklist a value that is
already unblacklisted.)

4. Since the icon needs to be visible by everyone to indicate whether or
not a phone/email is blacklisted, users without corresponding unblacklist
permissions will be able to click on the icon. When they click on it, they
will be informed they do not have permission to unblacklist. A wizard was
determined to be the best way to implement this unblacklisting for the
following reasons:
  - A "Unblacklisting Reason" is needed and will not be stored as a field.
  - A field widget is unable to do this due to security settings +
    inability to refresh view after unblacklisting with a
    "Unblacklisting Reason" without wiping unsaved changes.

Additionally, to better align with GDPR, the corresponding form view for
the phone/email blacklist has been updated to use the same wizard to
track "Reason for unblacklisting". Unfortunately there is no
straightforward way to prevent direct "Archive" action, so users are
still able to bypass unblacklisting without being asked for a "reason
for unblacklisting".

Supports task: 2117635
Upgrade PR: odoo/upgrade#939
COM PR: odoo/odoo#45315
This commit is contained in:
Tiffany Chang (tic)
2020-03-26 10:15:18 +00:00
parent 3a112c7b78
commit f9ff21a200
28 changed files with 301 additions and 56 deletions
+1
View File
@@ -54,6 +54,7 @@ class Lead(models.Model):
_order = "priority desc, id desc"
_inherit = ['mail.thread.cc',
'mail.thread.blacklist',
'mail.thread.phone',
'mail.activity.mixin',
'utm.mixin',
'format.address.mixin',
+54 -11
View File
@@ -130,20 +130,33 @@
/>
<field name="is_blacklisted" invisible="1"/>
<field name="partner_is_blacklisted" invisible="1"/>
<field name="phone_blacklisted" invisible="1"/>
<field name="mobile_blacklisted" invisible="1"/>
<field name="email_state" invisible="1"/>
<field name="phone_state" 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 mailings"
aria-label="Blacklisted" attrs="{'invisible': ['|', ('is_blacklisted', '=', False), ('partner_address_email', '!=', False)]}" groups="base.group_user"/>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': email_from}" groups="base.group_user"
attrs="{'invisible': ['|', ('is_blacklisted', '=', False), ('partner_address_email', '!=', False)]}"/>
<field name="email_from"
attrs="{'invisible': [('partner_address_email', '!=', False)]}" string="Email" widget="email"/>
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailings"
aria-label="Blacklisted" attrs="{'invisible': ['|', ('partner_is_blacklisted', '=', False), ('partner_address_email', '=', False)]}" groups="base.group_user"/>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': partner_address_email}" groups="base.group_user"
attrs="{'invisible': ['|', ('partner_is_blacklisted', '=', False), ('partner_address_email', '=', False)]}"/>
<field name="partner_address_email"
attrs="{'invisible': [('partner_address_email', '=', False)]}" widget="email" string="Email"/>
</div>
<field name="phone" widget="phone"/>
<label for="phone" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': phone}" groups="base.group_user"
attrs="{'invisible': [('phone_blacklisted', '=', False)]}"/>
<field name="phone" widget="phone"/>
</div>
</group>
<group name="lead_info" attrs="{'invisible': [('type', '=', 'opportunity')]}">
<label for="contact_name"/>
@@ -156,14 +169,37 @@
<field name="phone_state" 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 mailings"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"/>
<field name="email_from" widget="email"/>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': email_from}" groups="base.group_user"
attrs="{'invisible': ['|', ('is_blacklisted', '=', False), ('partner_address_email', '!=', False)]}"/>
<field name="email_from"
attrs="{'invisible': [('partner_address_email', '!=', False)]}" string="Email" widget="email"/>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': partner_address_email}" groups="base.group_user"
attrs="{'invisible': ['|', ('partner_is_blacklisted', '=', False), ('partner_address_email', '=', False)]}"/>
<field name="partner_address_email"
attrs="{'invisible': [('partner_address_email', '=', False)]}" widget="email" string="Email"/>
</div>
<field name="email_cc" groups="base.group_no_one"/>
<field name="function"/>
<field name="phone" widget="phone"/>
<field name="mobile" widget="phone"/>
<label for="phone" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': phone}" groups="base.group_user"
attrs="{'invisible': [('phone_blacklisted', '=', False)]}"/>
<field name="phone" widget="phone"/>
</div>
<label for="mobile" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': mobile}" groups="base.group_user"
attrs="{'invisible': [('mobile_blacklisted', '=', False)]}"/>
<field name="mobile" widget="phone" string="Mobile"/>
</div>
</group>
<group attrs="{'invisible': [('type', '=', 'lead')]}">
<field name="date_deadline"/>
@@ -234,7 +270,14 @@
<field name="title" placeholder="Title" domain="[]" options='{"no_open": True}'/>
</div>
<field name="function"/>
<field name="mobile" widget="phone"/>
<label for="mobile" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': mobile}" groups="base.group_user"
attrs="{'invisible': [('mobile_blacklisted', '=', False)]}"/>
<field name="mobile" widget="phone"/>
</div>
</group>
<group string="Marketing">
<field name="campaign_id" />
+1
View File
@@ -11,6 +11,7 @@
'data': [
'views/mail_menus.xml',
'wizard/invite_view.xml',
'wizard/mail_blacklist_remove_view.xml',
'wizard/mail_compose_message_view.xml',
'wizard/mail_resend_cancel_views.xml',
'wizard/mail_resend_message_views.xml',
+34 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
class MailBlackList(models.Model):
@@ -78,6 +78,13 @@ class MailBlackList(models.Model):
record = self.create({'email': email})
return record
def action_remove_with_reason(self, email, reason=None):
record = self._remove(email)
if reason:
record.message_post(body=_("Unblacklisting Reason: %s" % (reason)))
return record
def _remove(self, email):
normalized = tools.email_normalize(email)
record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)])
@@ -87,6 +94,17 @@ class MailBlackList(models.Model):
record = record.create({'email': email, 'active': False})
return record
def mail_action_blacklist_remove(self):
return {
'name': 'Are you sure you want to unblacklist this Email Address?',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.blacklist.remove',
'target': 'new',
}
def action_add(self):
self._add(self.email)
class MailBlackListMixin(models.AbstractModel):
""" Mixin that is inherited by all model with opt out. This mixin stores a normalized
@@ -192,3 +210,18 @@ class MailBlackListMixin(models.AbstractModel):
bounce counter of the record. """
super(MailBlackListMixin, self)._message_reset_bounce(email)
self.write({'message_bounce': 0})
def mail_action_blacklist_remove(self):
# wizard access rights currently not working as expected and allows users without access to
# open this wizard, therefore we check to make sure they have access before the wizard opens.
can_access = self.env['mail.blacklist'].check_access_rights('write', raise_exception=False)
if can_access:
return {
'name': 'Are you sure you want to unblacklist this Email Address?',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.blacklist.remove',
'target': 'new',
}
else:
raise AccessError("You do not have the access right to unblacklist emails. Please contact your administrator.")
+1
View File
@@ -45,3 +45,4 @@ access_mail_resend_cancel,access.mail.resend.cancel,model_mail_resend_cancel,bas
access_mail_resend_message,access.mail.resend.message,model_mail_resend_message,base.group_user,1,1,1,0
access_mail_resend_partner,access.mail.resend.partner,model_mail_resend_partner,base.group_user,1,1,1,0
access_mail_template_preview,access.mail.template.preview,model_mail_template_preview,base.group_user,1,1,1,0
access_mail_blacklist_remove_system,acesss.mail.blacklist.remove.system,model_mail_blacklist_remove,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
45 access_mail_resend_message access.mail.resend.message model_mail_resend_message base.group_user 1 1 1 0
46 access_mail_resend_partner access.mail.resend.partner model_mail_resend_partner base.group_user 1 1 1 0
47 access_mail_template_preview access.mail.template.preview model_mail_template_preview base.group_user 1 1 1 0
48 access_mail_blacklist_remove_system acesss.mail.blacklist.remove.system model_mail_blacklist_remove base.group_system 1 1 1 1
+10 -1
View File
@@ -16,12 +16,21 @@
<field name="model">mail.blacklist</field>
<field name="arch" type="xml">
<form string="Add Email Blacklist" duplicate="false" edit="false">
<header>
<button name="mail_action_blacklist_remove" string="Unblacklist"
type="object" class="oe_highlight" context="{'default_email': email}"
attrs="{'invisible': ['|', ('active', '=', False), ('email', '=', False)]}"/>
<button name="action_add" string="Blacklist"
type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('active', '=', True), ('email', '=', False)]}"/>
</header>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<group>
<group>
<field name="email"/>
<field name="active" widget="boolean_toggle"/>
<field name="active" readonly="1"/>
<br/>
</group>
</group>
</sheet>
+4 -2
View File
@@ -11,8 +11,10 @@
<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 mailings"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"></i>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': email}" groups="base.group_user"
attrs="{'invisible': [('is_blacklisted', '=', False)]}"/>
<field name="email" widget="email"/>
</div>
</xpath>
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import invite
from . import mail_blacklist_remove
from . import mail_compose_message
from . import mail_resend_cancel
from . import mail_resend_message
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class MailBlacklistRemove(models.TransientModel):
_name = 'mail.blacklist.remove'
_description = 'Remove email from blacklist wizard'
email = fields.Char(name="Email", readonly=True, required=True)
reason = fields.Char(name="Reason")
def action_unblacklist_apply(self):
return self.env['mail.blacklist'].action_remove_with_reason(self.email, self.reason)
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<odoo>
<record id="mail_blacklist_remove_view_form" model="ir.ui.view">
<field name="name">mail.blacklist.remove.form</field>
<field name="model">mail.blacklist.remove</field>
<field name="arch" type="xml">
<form string="mail_blacklist_removal">
<group class="oe_title">
<field name="email" string="Email Address"/>
<field name="reason" string="Reason"/>
</group>
<footer>
<button name="action_unblacklist_apply" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -13,6 +13,7 @@ access_mailing_trace_report_mm_user,access.mailing.trace.report.mm.user,model_ma
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_blacklist_remove_mass_mailing_user,acesss.mail.blacklist.remove.mass_mailing_user,mail.model_mail_blacklist_remove,mass_mailing.group_mass_mailing_user,1,1,1,1
access_link_tracker_mailing,access.link.tracker.mailing,link_tracker.model_link_tracker,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_list_merge,access.mailing.list.merge,model_mailing_list_merge,mass_mailing.group_mass_mailing_user,1,1,1,0
access_mailing_mailing_schedule_date,access.mailing.mailing.schedule.date,model_mailing_mailing_schedule_date,mass_mailing.group_mass_mailing_user,1,1,1,0
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_blacklist_remove_mass_mailing_user acesss.mail.blacklist.remove.mass_mailing_user mail.model_mail_blacklist_remove mass_mailing.group_mass_mailing_user 1 1 1 1
17 access_link_tracker_mailing access.link.tracker.mailing link_tracker.model_link_tracker mass_mailing.group_mass_mailing_user 1 1 1 1
18 access_mailing_list_merge access.mailing.list.merge model_mailing_list_merge mass_mailing.group_mass_mailing_user 1 1 1 0
19 access_mailing_mailing_schedule_date access.mailing.mailing.schedule.date model_mailing_mailing_schedule_date mass_mailing.group_mass_mailing_user 1 1 1 0
@@ -13,7 +13,7 @@
<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 mailings"
<i class="fa fa-ban text-danger" role="img" title="This email is blacklisted for mass mailings"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"></i>
<field name="contact_id"/>
</div>
@@ -164,8 +164,10 @@
<group>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly" name="email_details">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailings"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"></i>
<button name="mail_action_blacklist_remove" class="fa fa-ban text-danger"
title="This email is blacklisted for mass mailings. Click to unblacklist."
type="object" context="{'default_email': email}" groups="base.group_user"
attrs="{'invisible': [('is_blacklisted', '=', False)]}"/>
<field name="email" widget="email"/>
<field name="is_blacklisted" invisible="1"/>
</div>
@@ -217,7 +217,7 @@ class Mailing(models.Model):
def _get_default_mailing_domain(self):
mailing_domain = super(Mailing, self)._get_default_mailing_domain()
if self.mailing_type == 'sms' and 'phone_blacklisted' in self.env[self.mailing_model_name]._fields:
mailing_domain = expression.AND([mailing_domain, [('phone_blacklisted', '=', False)]])
if self.mailing_type == 'sms' and 'phone_sanitized_blacklisted' in self.env[self.mailing_model_name]._fields:
mailing_domain = expression.AND([mailing_domain, [('phone_sanitized_blacklisted', '=', False)]])
return mailing_domain
@@ -1,2 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mailing_sms_test,access.mailing.sms.test,model_mailing_sms_test,mass_mailing.group_mass_mailing_user,1,1,1,0
access_phone_blacklist_remove_mass_mailing_user,acesss.phone.blacklist.remove.mass_mailing_user,phone_validation.model_phone_blacklist_remove,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
2 access_mailing_sms_test access.mailing.sms.test model_mailing_sms_test mass_mailing.group_mass_mailing_user 1 1 1 0
3 access_phone_blacklist_remove_mass_mailing_user acesss.phone.blacklist.remove.mass_mailing_user phone_validation.model_phone_blacklist_remove mass_mailing.group_mass_mailing_user 1 1 1 1
@@ -36,7 +36,7 @@ class TestMassMailValues(TestMassMailCommon):
self.assertEqual(mailing.mailing_model_name, 'res.partner')
self.assertEqual(mailing.mailing_model_real, 'res.partner')
# default for partner: remove blacklisted
self.assertEqual(literal_eval(mailing.mailing_domain), [('phone_blacklisted', '=', False)])
self.assertEqual(literal_eval(mailing.mailing_domain), [('phone_sanitized_blacklisted', '=', False)])
# update template -> update body
mailing.write({'sms_template_id': self.sms_template_partner.id})
self.assertEqual(mailing.body_plaintext, self.sms_template_partner.body)
@@ -13,12 +13,12 @@
<separator/>
<filter string="Valid SMS Recipients"
name="filter_valid_sms_recipient"
domain="[('opt_out', '=', False), ('phone_blacklisted', '=', False), ('phone_sanitized', '!=', False)]"
domain="[('opt_out', '=', False), ('phone_sanitized_blacklisted', '=', False), ('phone_sanitized', '!=', False)]"
invisible="not context.get('default_list_ids')"/>
<separator/>
<filter string="Exclude Blacklisted Phone"
name="filter_not_phone_bl"
domain="[('phone_blacklisted', '=', False)]"/>
domain="[('phone_sanitized_blacklisted', '=', False)]"/>
</xpath>
</field>
</record>
@@ -32,7 +32,7 @@
<xpath expr="//field[@name='is_blacklisted']" position="after">
<field name="mobile"/>
<field name="phone_sanitized" invisible="1"/>
<field name="phone_blacklisted"/>
<field name="phone_sanitized_blacklisted"/>
</xpath>
</field>
</record>
@@ -45,11 +45,13 @@
<xpath expr="//div[@name='email_details']" position="after">
<label for="mobile" class="oe_inline"/>
<div class="o_row o_row_readonly" name="phone_details">
<i class="fa fa-ban" style="color: red;" role="img" title="This number is blacklisted for SMS Marketing"
aria-label="Phone Blacklisted" attrs="{'invisible': [('phone_blacklisted', '=', False)]}" groups="base.group_user"></i>
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': mobile}" groups="base.group_user"
attrs="{'invisible': [('mobile_blacklisted', '=', False)]}"/>
<field name="mobile" widget="phone" options="{'enable_sms': True}"/>
<field name="phone_sanitized" invisible="1"/>
<field name="phone_blacklisted" invisible="1"/>
<field name="mobile_blacklisted" invisible="1"/>
</div>
</xpath>
</field>
+1
View File
@@ -3,3 +3,4 @@
from . import tools
from . import models
from . import wizard
+1
View File
@@ -26,6 +26,7 @@ It adds two mixins :
'data': [
'security/ir.model.access.csv',
'views/phone_blacklist_views.xml',
'wizard/phone_blacklist_remove_view.xml',
],
'depends': [
'base',
@@ -3,7 +3,7 @@
from odoo import api, fields, models, _
from odoo.addons.phone_validation.tools import phone_validation
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
class PhoneMixin(models.AbstractModel):
@@ -30,10 +30,18 @@ class PhoneMixin(models.AbstractModel):
phone_sanitized = fields.Char(
string='Sanitized Number', compute="_compute_phone_sanitized", compute_sudo=True, store=True,
help="Field used to store sanitized phone number. Helps speeding up searches and comparisons.")
phone_sanitized_blacklisted = fields.Boolean(
string='Phone Blacklisted', compute="_compute_blacklisted", compute_sudo=True, store=False,
search="_search_phone_sanitized_blacklisted", groups="base.group_user",
help="If the sanitized phone number is on the blacklist, the contact won't receive mass mailing sms anymore, from any list")
phone_blacklisted = fields.Boolean(
string='Phone Blacklisted', compute="_compute_phone_blacklisted", compute_sudo=True, store=False,
search="_search_phone_blacklisted", groups="base.group_user",
help="If the phone number is on the blacklist, the contact won't receive mass mailing sms anymore, from any list")
string='Blacklisted Phone is Phone', compute="_compute_blacklisted", compute_sudo=True, store=False, groups="base.group_user",
help="Indicates if a blacklisted sanitized phone number is a phone number. Helps distinguish which number is blacklisted \
when there is both a mobile and phone field in a model.")
mobile_blacklisted = fields.Boolean(
string='Blacklisted Phone Is Mobile', compute="_compute_blacklisted", compute_sudo=True, store=False, groups="base.group_user",
help="Indicates if a blacklisted sanitized phone number is a mobile number. Helps distinguish which number is blacklisted \
when there is both a mobile and phone field in a model.")
@api.depends(lambda self: self._phone_get_number_fields())
def _compute_phone_sanitized(self):
@@ -47,16 +55,26 @@ class PhoneMixin(models.AbstractModel):
record.phone_sanitized = sanitized
@api.depends('phone_sanitized')
def _compute_phone_blacklisted(self):
def _compute_blacklisted(self):
# TODO : Should remove the sudo as compute_sudo defined on methods.
# But if user doesn't have access to mail.blacklist, doen't work without sudo().
blacklist = set(self.env['phone.blacklist'].sudo().search([
('number', 'in', self.mapped('phone_sanitized'))]).mapped('number'))
number_fields = self._phone_get_number_fields()
for record in self:
record.phone_blacklisted = record.phone_sanitized in blacklist
record.phone_sanitized_blacklisted = record.phone_sanitized in blacklist
# This is a bit of a hack. Assume that any "mobile" numbers will have the word 'mobile'
# in them due to varying field names and assume all others are just "phone" numbers.
# Note that the limitation of only having 1 phone_sanitized value means that a phone/mobile number
# may not be calculated as blacklisted even though it is if both field values exist in a model.
for number_field in number_fields:
if 'mobile' in number_field:
record.mobile_blacklisted = record.phone_sanitized_blacklisted and record.phone_get_sanitized_number(number_fname=number_field) == record.phone_sanitized
else:
record.phone_blacklisted = record.phone_sanitized_blacklisted and record.phone_get_sanitized_number(number_fname=number_field) == record.phone_sanitized
@api.model
def _search_phone_blacklisted(self, operator, value):
def _search_phone_sanitized_blacklisted(self, operator, value):
# Assumes operator is '=' or '!=' and value is True or False
self._assert_phone_field()
if operator != '=':
@@ -121,3 +139,18 @@ class PhoneMixin(models.AbstractModel):
def _phone_reset_blacklisted(self):
return self.env['phone.blacklist'].sudo()._remove([r.phone_sanitized for r in self])
def phone_action_blacklist_remove(self):
# wizard access rights currently not working as expected and allows users without access to
# open this wizard, therefore we check to make sure they have access before the wizard opens.
can_access = self.env['phone.blacklist'].check_access_rights('write', raise_exception=False)
if can_access:
return {
'name': 'Are you sure you want to unblacklist this Phone Number?',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'phone.blacklist.remove',
'target': 'new',
}
else:
raise AccessError("You do not have the access right to unblacklist phone numbers. Please contact your administrator.")
@@ -31,9 +31,10 @@ class PhoneBlackList(models.Model):
done = set()
for value in values:
number = value['number']
sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
sanitized_values = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]
sanitized = sanitized_values['sanitized']
if not sanitized:
raise UserError(_('Invalid number %s') % number)
raise UserError(sanitized_values['msg'] + _(" Please correct the number and try again."))
if sanitized in done:
continue
done.add(sanitized)
@@ -52,9 +53,10 @@ class PhoneBlackList(models.Model):
def write(self, values):
if 'number' in values:
number = values['number']
sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
sanitized_values = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]
sanitized = sanitized_values['sanitized']
if not sanitized:
raise UserError(_('Invalid number %s') % number)
raise UserError(sanitized_values['msg'] + _(" Please correct the number and try again."))
values['number'] = sanitized
return super(PhoneBlackList, self).write(values)
@@ -92,6 +94,13 @@ class PhoneBlackList(models.Model):
records += self.create([{'number': n} for n in todo])
return records
def action_remove_with_reason(self, number, reason=None):
records = self.remove(number)
if reason:
for record in records:
record.message_post(body=_("Unblacklisting Reason: %s" % (reason)))
return records
def remove(self, number):
sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized']
return self._remove([sanitized])
@@ -107,3 +116,15 @@ class PhoneBlackList(models.Model):
if todo:
records += self.create([{'number': n, 'active': False} for n in todo])
return records
def phone_action_blacklist_remove(self):
return {
'name': 'Are you sure you want to unblacklist this Phone Number?',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'phone.blacklist.remove',
'target': 'new',
}
def action_add(self):
self.add(self.number)
@@ -1,3 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_phone_blacklist_all,access.phone.blacklist.all,model_phone_blacklist,,0,0,0,0
access_phone_blacklist_system,access.phone.blacklist.system,model_phone_blacklist,base.group_system,1,1,1,1
access_phone_blacklist_remove_system,acesss.phone.blacklist.remove.system,model_phone_blacklist_remove,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_phone_blacklist_all access.phone.blacklist.all model_phone_blacklist 0 0 0 0
3 access_phone_blacklist_system access.phone.blacklist.system model_phone_blacklist base.group_system 1 1 1 1
4 access_phone_blacklist_remove_system acesss.phone.blacklist.remove.system model_phone_blacklist_remove base.group_system 1 1 1 1
@@ -17,12 +17,12 @@ try:
try:
phone_nbr = phonenumbers.parse(number, region=country_code, keep_raw_input=True)
except phonenumbers.phonenumberutil.NumberParseException as e:
raise UserError(_('Unable to parse %s: %s') % (number, str(e)))
raise UserError(_('Unable to parse %s.: %s') % (number, str(e)))
if not phonenumbers.is_possible_number(phone_nbr):
raise UserError(_('Impossible number %s: probably invalid number of digits') % number)
raise UserError(_('Impossible number %s: probably invalid number of digits.') % number)
if not phonenumbers.is_valid_number(phone_nbr):
raise UserError(_('Invalid number %s: probably incorrect prefix') % number)
raise UserError(_('Invalid number %s: probably incorrect prefix.') % number)
return phone_nbr
@@ -16,12 +16,21 @@
<field name="model">phone.blacklist</field>
<field name="arch" type="xml">
<form string="Phone Blacklist" duplicate="false" edit="false">
<header>
<button name="phone_action_blacklist_remove" string="Unblacklist"
type="object" class="oe_highlight" context="{'default_phone': number}"
attrs="{'invisible': ['|', ('active', '=', False), ('number', '=', False)]}"/>
<button name="action_add" string="Blacklist"
type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('active', '=', True), ('number', '=', False)]}"/>
</header>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<group>
<group>
<field name="number"/>
<field name="active" widget="boolean_toggle"/>
<field name="active" readonly="1"/>
<br/>
</group>
</group>
</sheet>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import phone_blacklist_remove
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class PhoneBlacklistRemove(models.TransientModel):
_name = 'phone.blacklist.remove'
_description = 'Remove phone from blacklist'
phone = fields.Char(string="Phone Number", readonly=True, required=True)
reason = fields.Char(name="Reason")
def action_unblacklist_apply(self):
return self.env['phone.blacklist'].action_remove_with_reason(self.phone, self.reason)
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<odoo>
<record id="phone_blacklist_remove_view_form" model="ir.ui.view">
<field name="name">phone.blacklist.remove.form</field>
<field name="model">phone.blacklist.remove</field>
<field name="arch" type="xml">
<form string="phone_blacklist_removal">
<group class="oe_title">
<field name="phone" string="Phone Number"/>
<field name="reason" string="Reason"/>
</group>
<footer>
<button name="action_unblacklist_apply" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
+17 -5
View File
@@ -12,15 +12,27 @@
<field name="phone_sanitized" groups="base.group_no_one" invisible="1"/>
</xpath>
<xpath expr="//field[@name='phone']" position="replace">
<label for="phone"/>
<div class="o_row">
<field name="phone_blacklisted" invisible="1"/>
<field name="mobile_blacklisted" invisible="1"/>
<label for="phone" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': phone}" groups="base.group_user"
attrs="{'invisible': [('phone_blacklisted', '=', False)]}"/>
<field name="phone" widget="phone"/>
</div>
</xpath>
<xpath expr="//field[@name='mobile']" position="replace">
<label for="mobile"/>
<div class="o_row">
<field name="mobile" widget="phone" options="{'enable_sms': True}"/>
<field name="phone_blacklisted" invisible="1"/>
<field name="mobile_blacklisted" invisible="1"/>
<label for="mobile" class="oe_inline"/>
<div class="o_row o_row_readonly">
<button name="phone_action_blacklist_remove" class="fa fa-ban text-danger"
title="This phone number is blacklisted for SMS Marketing. Click to unblacklist."
type="object" context="{'default_phone': mobile}" groups="base.group_user"
attrs="{'invisible': [('mobile_blacklisted', '=', False)]}"/>
<field name="mobile" widget="phone"/>
</div>
</xpath>
</field>
@@ -24,44 +24,44 @@ class TestPhoneBlacklist(test_mail_full_common.TestSMSCommon, test_mail_full_com
with self.with_user('employee'):
test_record = self.env['mail.test.sms.bl'].browse(self.test_record.id)
self.assertEqual(test_record.phone_sanitized, self.test_numbers_san[1])
self.assertFalse(test_record.phone_blacklisted)
self.assertFalse(test_record.phone_sanitized_blacklisted)
bl_record = self.env['phone.blacklist'].sudo().create([{'number': self.test_numbers_san[1]}])
test_record.invalidate_cache()
self.assertTrue(test_record.phone_blacklisted)
self.assertTrue(test_record.phone_sanitized_blacklisted)
self.env['phone.blacklist'].sudo().remove(self.test_numbers_san[1])
self.assertFalse(bl_record.active)
test_record.invalidate_cache()
self.assertFalse(test_record.phone_blacklisted)
self.assertFalse(test_record.phone_sanitized_blacklisted)
self.env['phone.blacklist'].sudo().add(self.test_numbers_san[1])
self.assertTrue(bl_record.active)
test_record.invalidate_cache()
self.assertTrue(test_record.phone_blacklisted)
self.assertTrue(test_record.phone_sanitized_blacklisted)
bl_record_2 = self.env['phone.blacklist'].sudo().create([{'number': self.test_numbers_san[1]}])
self.assertEqual(bl_record, bl_record_2)
rec = self.env['mail.test.sms.bl'].search([('phone_blacklisted', '=', True)])
rec = self.env['mail.test.sms.bl'].search([('phone_sanitized_blacklisted', '=', True)])
self.assertEqual(rec, test_record)
bl_record.unlink()
rec = self.env['mail.test.sms.bl'].search([('phone_blacklisted', '=', True)])
rec = self.env['mail.test.sms.bl'].search([('phone_sanitized_blacklisted', '=', True)])
self.assertEqual(rec, self.env['mail.test.sms.bl'])
def test_phone_sanitize_api(self):
with self.with_user('employee'):
test_record = self.env['mail.test.sms.bl'].browse(self.test_record.id)
self.assertFalse(test_record.phone_blacklisted)
self.assertFalse(test_record.phone_sanitized_blacklisted)
test_record._phone_set_blacklisted()
test_record.invalidate_cache()
self.assertTrue(test_record.phone_blacklisted)
self.assertTrue(test_record.phone_sanitized_blacklisted)
test_record._phone_reset_blacklisted()
test_record.invalidate_cache()
self.assertFalse(test_record.phone_blacklisted)
self.assertFalse(test_record.phone_sanitized_blacklisted)
def test_phone_sanitize_internals(self):
with self.with_user('employee'):