[MOV] mail: reorganize channel code

PURPOSE

Reorganize and lint mail.channel code. Purpose is to ease future modifications
linked to groups and channels in mail / Discuss.

SPECIFICATIONS

Move mail.channel.partner and mail.moderation models into their own file.
This makes some noise in diff but as channel is quite an heavy model let us
do this once for all.

Move mail.channel.partner views in their own file, as for python models.

Also containing

  * separate and reorganize fields by main definition;
  * separate and reorganize methods by categories, notably CRUD + orm, members
    management, moderation, mailing, IM, commands;
  * reorganize compute section: compute (by field order), constraints then
    onchanges;

LINKS

Prepares Task ID-2070632 (Discuss channel task)
Prepares Task ID-2419762 (SM channel task)
COM PR odoo/odoo#64862
This commit is contained in:
Thibault Delavallée
2021-01-21 14:01:57 +00:00
parent cd885ad2cd
commit 11b8735d8a
7 changed files with 302 additions and 265 deletions
+1
View File
@@ -25,6 +25,7 @@
'views/mail_mail_views.xml',
'views/mail_followers_views.xml',
'views/mail_moderation_views.xml',
'views/mail_channel_partner_views.xml',
'views/mail_channel_views.xml',
'views/mail_shortcode_views.xml',
'views/mail_activity_views.xml',
+2
View File
@@ -15,6 +15,8 @@ from . import mail_thread
from . import mail_thread_blacklist
from . import mail_thread_cc
from . import mail_blacklist
from . import mail_moderation
from . import mail_channel_partner
from . import mail_channel
from . import mail_template
from . import mail_shortcode
+189 -221
View File
@@ -10,68 +10,11 @@ from odoo import _, api, fields, models, modules, tools, Command
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import ormcache, formataddr
from odoo.exceptions import AccessError
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
_logger = logging.getLogger(__name__)
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_description = 'Listeners of a Channel'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
custom_channel_name = fields.Char('Custom channel name')
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimized")
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
@api.model_create_multi
def create(self, vals_list):
"""Similar access rule as the access rule of the mail channel.
It can not be implemented in XML, because when the record will be created, the
partner will be added in the channel and the security rule will always authorize
the creation.
"""
if not self.env.is_admin():
for vals in vals_list:
if 'channel_id' in vals and not self.env.is_admin():
channel_id = self.env['mail.channel'].browse(vals['channel_id'])
if not channel_id._can_invite(vals.get('partner_id')):
raise AccessError(_('This user can not be added in this channel'))
return super(ChannelPartner, self).create(vals_list)
def write(self, vals):
if not self.env.is_admin():
if {'channel_id', 'partner_id', 'partner_email'} & set(vals):
raise AccessError(_('You can not write on this field'))
return super(ChannelPartner, self).write(vals)
class Moderation(models.Model):
_name = 'mail.moderation'
_description = 'Channel black/white list'
email = fields.Char(string="Email", index=True, required=True)
status = fields.Selection([
('allow', 'Always Allow'),
('ban', 'Permanent Ban')],
string="Status", required=True)
channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
_sql_constraints = [
('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
]
class Channel(models.Model):
""" A mail.channel is a discussion group that may behave like a listener
on documents. """
@@ -94,39 +37,39 @@ class Channel(models.Model):
image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
return base64.b64encode(open(image_path, 'rb').read())
# description
name = fields.Char('Name', required=True, translate=True)
active = fields.Boolean(default=True, help="Set active to false to hide the channel without removing it.")
channel_type = fields.Selection([
('chat', 'Chat Discussion'),
('channel', 'Channel')],
'Channel Type', default='channel')
string='Channel Type', default='channel')
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat')
description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel
# depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
# members (depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`)
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen', depends=['channel_partner_ids'])
channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners', depends=['channel_last_seen_partner_ids'])
channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel')
is_member = fields.Boolean('Is a member', compute='_compute_is_member')
# access
public = fields.Selection([
('public', 'Everyone'),
('private', 'Invited people only'),
('groups', 'Selected group of users')],
'Privacy', required=True, default='groups',
help='This group is visible by non members. Invisible groups can add members through the invite button.')
group_public_id = fields.Many2one('res.groups', string='Authorized Group',
default=lambda self: self.env.ref('base.group_user'))
is_subscribed = fields.Boolean(
'Is Subscribed', compute='_compute_is_subscribed')
group_ids = fields.Many2many(
'res.groups', string='Auto Subscription',
help="Members of those groups will automatically added as followers. "
"Note that they will be able to manage their subscription manually "
"if necessary.")
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
is_subscribed = fields.Boolean(
'Is Subscribed', compute='_compute_is_subscribed')
# access
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
public = fields.Selection([
('public', 'Everyone'),
('private', 'Invited people only'),
('groups', 'Selected group of users')], string='Privacy',
required=True, default='groups',
help='This group is visible by non members. Invisible groups can add members through the invite button.')
group_public_id = fields.Many2one('res.groups', string='Authorized Group',
default=lambda self: self.env.ref('base.group_user'))
# moderation
moderation = fields.Boolean(string='Moderate this channel')
moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
@@ -142,6 +85,22 @@ class Channel(models.Model):
moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
moderation_guidelines_msg = fields.Text(string="Guidelines")
# COMPUTE / INVERSE
@api.depends('channel_type')
def _compute_is_chat(self):
for record in self:
record.is_chat = record.channel_type == 'chat'
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
])
membership_ids = memberships.mapped('channel_id')
for record in self:
record.is_member = record in membership_ids
@api.depends('channel_partner_ids')
def _compute_is_subscribed(self):
for channel in self:
@@ -159,6 +118,8 @@ class Channel(models.Model):
for channel in self:
channel.moderation_count = data.get(channel.id, 0)
# CONSTRAINTS
@api.constrains('moderator_ids')
def _check_moderator_email(self):
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
@@ -184,19 +145,7 @@ class Channel(models.Model):
if any(not channel.moderator_ids for channel in self if channel.moderation):
raise ValidationError(_('Moderated channels must have moderators.'))
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
])
membership_ids = memberships.mapped('channel_id')
for record in self:
record.is_member = record in membership_ids
@api.depends('channel_type')
def _compute_is_chat(self):
for record in self:
record.is_chat = record.channel_type == 'chat'
# ONCHANGE
@api.onchange('public')
def _onchange_public(self):
@@ -226,6 +175,10 @@ class Channel(models.Model):
else:
self.moderator_ids |= self.env.user
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
defaults = self.default_get(['image_128'])
@@ -305,12 +258,14 @@ class Channel(models.Model):
return result
def _alias_get_creation_values(self):
values = super(Channel, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id
if self.id:
values['alias_force_thread_id'] = self.id
return values
def init(self):
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
# ------------------------------------------------------------
# MEMBERS MANAGEMENT
# ------------------------------------------------------------
def _subscribe_users(self):
to_create = []
@@ -371,6 +326,85 @@ class Channel(models.Model):
]).unlink()
self.invalidate_cache(fnames=['channel_partner_ids', 'channel_last_seen_partner_ids'])
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
:param partner_ids : list of partner id to add
"""
partners = self.env['res.partner'].browse(partner_ids)
self._invite_check_access(partners)
# add the partner
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add:
if partner.id != self.env.user.partner_id.id:
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
'author': self.env.user.display_name,
'new_partner': partner.display_name,
'channel_id': channel.id,
'channel_name': channel.name,
}
else:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False)
# broadcast the channel header to the added partner
self._broadcast(partner_ids)
def _invite_check_access(self, partners):
""" Check invited partners could match channel access """
failed = []
if any(channel.public == 'groups' for channel in self):
for channel in self.filtered(lambda c: c.public == 'groups'):
invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')]
failed += [(channel, partner) for partner in invalid_partners]
if failed:
raise UserError(
_('Following invites are invalid as user groups do not match: %s') %
', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed)
)
def _can_invite(self, partner_id):
"""Return True if the current user can invite the partner to the channel.
* public: ok;
* private: must be member;
* group: both current user and target must have group;
:return boolean: whether inviting is ok"""
partner = self.env['res.partner'].browse(partner_id)
for channel in self.sudo():
if channel.public == 'private' and not channel.is_member:
return False
if channel.public == 'groups':
if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id:
return False
if channel.group_public_id not in self.env.user.groups_id:
return False
return True
# ------------------------------------------------------------
# MAILING
# ------------------------------------------------------------
def _alias_get_creation_values(self):
values = super(Channel, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id
if self.id:
values['alias_force_thread_id'] = self.id
return values
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return _('restricted to channel members')
return False
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
def _notify_get_groups(self, msg_vals=None):
""" All recipients of a message on a channel are considered as partners.
This means they will receive a minimal email, without a link to access
@@ -397,13 +431,6 @@ class Channel(models.Model):
headers['X-Forge-To'] = list_to
return headers
def _message_receive_bounce(self, email, partner):
""" Override bounce management to unsubscribe bouncing addresses """
for p in partner:
if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
self._action_unfollow(p)
return super(Channel, self)._message_receive_bounce(email, partner)
def _notify_email_recipient_values(self, recipient_ids):
# Excluded Blacklisted
whitelist = self.env['res.partner'].sudo().browse(recipient_ids).filtered(lambda p: not p.is_blacklisted)
@@ -415,32 +442,19 @@ class Channel(models.Model):
}
return super(Channel, self)._notify_email_recipient_values(whitelist.ids)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
of a message. For this operation the message's author email address is required.
This address is returned with status for other computations. """
moderation_status = 'accepted'
email = ''
if self.moderation and message_type in ['email', 'comment']:
author_id = kwargs.get('author_id')
if author_id and isinstance(author_id, int):
email = self.env['res.partner'].browse([author_id]).email
elif author_id:
email = author_id.email
elif kwargs.get('email_from'):
email = tools.email_split(kwargs['email_from'])[0]
else:
email = self.env.user.email
if email in self.mapped('moderator_ids.email'):
return moderation_status, email
status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
if status and status[0] == 'allow':
moderation_status = 'accepted'
elif status and status[0] == 'ban':
moderation_status = 'rejected'
else:
moderation_status = 'pending_moderation'
return moderation_status, email
def _notify_thread(self, message, msg_vals=False, **kwargs):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
else:
message._notify_pending_by_chat()
def _message_receive_bounce(self, email, partner):
""" Override bounce management to unsubscribe bouncing addresses """
for p in partner:
if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
self._action_unfollow(p)
return super(Channel, self)._message_receive_bounce(email, partner)
@api.returns('mail.message', lambda value: value.id)
def message_post(self, *, message_type='notification', **kwargs):
@@ -473,22 +487,36 @@ class Channel(models.Model):
self._set_last_seen_message(message)
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return _('restricted to channel members')
return False
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
of a message. For this operation the message's author email address is required.
This address is returned with status for other computations. """
moderation_status = 'accepted'
email = ''
if self.moderation and message_type in ['email', 'comment']:
author_id = kwargs.get('author_id')
if author_id and isinstance(author_id, int):
email = self.env['res.partner'].browse([author_id]).email
elif author_id:
email = author_id.email
elif kwargs.get('email_from'):
email = tools.email_split(kwargs['email_from'])[0]
else:
email = self.env.user.email
if email in self.mapped('moderator_ids.email'):
return moderation_status, email
status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
if status and status[0] == 'allow':
moderation_status = 'accepted'
elif status and status[0] == 'ban':
moderation_status = 'rejected'
else:
moderation_status = 'pending_moderation'
return moderation_status, email
def init(self):
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
# --------------------------------------------------
# Moderation
# --------------------------------------------------
# ------------------------------------------------------------
# MODERATION
# ------------------------------------------------------------
def send_guidelines(self):
""" Send guidelines to all channel members. """
@@ -540,14 +568,9 @@ class Channel(models.Model):
cmds += [Command.create({'email': email, 'status': status}) for email in not_moderated]
return self.write({'moderation_ids': cmds})
#------------------------------------------------------
# Instant Messaging API
#------------------------------------------------------
# A channel header should be broadcasted:
# - when adding user to channel (only to the new added partners)
# - when folding/minimizing a channel (only to the user making the action)
# A message should be broadcasted:
# - when a message is posted on a channel (to the channel, using _notify() method)
# ------------------------------------------------------------
# BROADCAST
# ------------------------------------------------------------
# Anonymous method
def _broadcast(self, partner_ids):
@@ -573,13 +596,6 @@ class Channel(models.Model):
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
return notifications
def _notify_thread(self, message, msg_vals=False, **kwargs):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
else:
message._notify_pending_by_chat()
def _channel_message_notifications(self, message, message_format=False):
""" Generate the bus notifications for the given message
:param message : the mail.message to sent
@@ -594,6 +610,16 @@ class Channel(models.Model):
notifications.append([channel.uuid, dict(message_format)])
return notifications
# ------------------------------------------------------------
# INSTANT MESSAGING API
# ------------------------------------------------------------
# A channel header should be broadcasted:
# - when adding user to channel (only to the new added partners)
# - when folding/minimizing a channel (only to the user making the action)
# A message should be broadcasted:
# - when a message is posted on a channel (to the channel, using _notify() method)
# ------------------------------------------------------------
@api.model
def partner_info(self, all_partners, direct_partners):
"""
@@ -893,66 +919,6 @@ class Channel(models.Model):
}
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', channel.id), data]])
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
:param partner_ids : list of partner id to add
"""
partners = self.env['res.partner'].browse(partner_ids)
self._invite_check_access(partners)
# add the partner
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add:
if partner.id != self.env.user.partner_id.id:
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
'author': self.env.user.display_name,
'new_partner': partner.display_name,
'channel_id': channel.id,
'channel_name': channel.name,
}
else:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False)
# broadcast the channel header to the added partner
self._broadcast(partner_ids)
def _invite_check_access(self, partners):
""" Check invited partners could match channel access """
failed = []
if any(channel.public == 'groups' for channel in self):
for channel in self.filtered(lambda c: c.public == 'groups'):
invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')]
failed += [(channel, partner) for partner in invalid_partners]
if failed:
raise UserError(
_('Following invites are invalid as user groups do not match: %s') %
', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed)
)
def _can_invite(self, partner_id):
"""Return True if the current user can invite the partner to the channel.
* public: ok;
* private: must be member;
* group: both current user and target must have group;
:return boolean: whether inviting is ok"""
partner = self.env['res.partner'].browse(partner_id)
for channel in self.sudo():
if channel.public == 'private' and not channel.is_member:
return False
if channel.public == 'groups':
if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id:
return False
if channel.group_public_id not in self.env.user.groups_id:
return False
return True
@api.model
def channel_set_custom_name(self, channel_id, name=False):
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.id', '=', channel_id)]
@@ -977,9 +943,10 @@ class Channel(models.Model):
notifications.append([channel.uuid, data]) # notify frontend users
self.env['bus.bus'].sendmany(notifications)
#------------------------------------------------------
# Instant Messaging View Specific (Slack Client Action)
#------------------------------------------------------
# ------------------------------------------------------------
# IM VIEW SPECIFIC (Slack Client Action)
# ------------------------------------------------------------
@api.model
def channel_fetch_slot(self):
""" Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
@@ -1106,9 +1073,10 @@ class Channel(models.Model):
""", (tuple(self.ids),))
return self.env.cr.dictfetchall()
#------------------------------------------------------
# Commands
#------------------------------------------------------
# ------------------------------------------------------------
# COMMANDS
# ------------------------------------------------------------
@api.model
@ormcache()
def get_mention_commands(self):
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import AccessError
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_description = 'Listeners of a Channel'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
custom_channel_name = fields.Char('Custom channel name')
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimized")
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
@api.model_create_multi
def create(self, vals_list):
"""Similar access rule as the access rule of the mail channel.
It can not be implemented in XML, because when the record will be created, the
partner will be added in the channel and the security rule will always authorize
the creation.
"""
if not self.env.is_admin():
for vals in vals_list:
if 'channel_id' in vals:
channel_id = self.env['mail.channel'].browse(vals['channel_id'])
if not channel_id._can_invite(vals.get('partner_id')):
raise AccessError(_('This user can not be added in this channel'))
return super(ChannelPartner, self).create(vals_list)
def write(self, vals):
if not self.env.is_admin():
if {'channel_id', 'partner_id', 'partner_email'} & set(vals):
raise AccessError(_('You can not write on this field'))
return super(ChannelPartner, self).write(vals)
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Moderation(models.Model):
_name = 'mail.moderation'
_description = 'Channel black/white list'
email = fields.Char(string="Email", index=True, required=True)
status = fields.Selection([
('allow', 'Always Allow'),
('ban', 'Permanent Ban')],
string="Status", required=True)
channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
_sql_constraints = [
('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
]
@@ -0,0 +1,46 @@
<?xml version="1.0"?>
<odoo><data>
<!-- mail.channel.partner -->
<record id="mail_channel_partner_view_tree" model="ir.ui.view">
<field name="name">mail.channel.partner.tree</field>
<field name="model">mail.channel.partner</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Channels">
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</tree>
</field>
</record>
<record id="mail_channel_partner_view_form" model="ir.ui.view">
<field name="name">mail.channel.partner.form</field>
<field name="model">mail.channel.partner</field>
<field name="arch" type="xml">
<form string="Channel">
<sheet>
<group>
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mail_channel_partner_action" model="ir.actions.act_window">
<field name="name">Channels/Partner</field>
<field name="res_model">mail.channel.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Channels/Partner"
id="mail_channel_partner_menu"
parent="base.menu_email"
action="mail_channel_partner_action"
sequence="21"
groups="base.group_no_one"/>
</data></odoo>
-44
View File
@@ -1,50 +1,6 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- mail.channel.partner -->
<record id="mail_channel_partner_view_tree" model="ir.ui.view">
<field name="name">mail.channel.partner.tree</field>
<field name="model">mail.channel.partner</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Channels">
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</tree>
</field>
</record>
<record id="mail_channel_partner_view_form" model="ir.ui.view">
<field name="name">mail.channel.partner.form</field>
<field name="model">mail.channel.partner</field>
<field name="arch" type="xml">
<form string="Channel">
<sheet>
<group>
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mail_channel_partner_action" model="ir.actions.act_window">
<field name="name">Channels/Partner</field>
<field name="res_model">mail.channel.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Channels/Partner"
id="mail_channel_partner_menu"
parent="base.menu_email"
action="mail_channel_partner_action"
sequence="21"
groups="base.group_no_one"/>
<!-- mail.channel -->
<record id="mail_channel_view_kanban" model="ir.ui.view">
<field name="name">mail.channel.kanban</field>