diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 90f11c63257..81185de0e4b 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -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',
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index 38b80e6dc12..03c7e597fa2 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -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
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 9f2b2fb97a0..db1b39de91e 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -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 = _('
') % {
+ 'author': self.env.user.display_name,
+ 'new_partner': partner.display_name,
+ 'channel_id': channel.id,
+ 'channel_name': channel.name,
+ }
+ else:
+ notification = _('') % (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 = _('') % {
- 'author': self.env.user.display_name,
- 'new_partner': partner.display_name,
- 'channel_id': channel.id,
- 'channel_name': channel.name,
- }
- else:
- notification = _('') % (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):
diff --git a/addons/mail/models/mail_channel_partner.py b/addons/mail/models/mail_channel_partner.py
new file mode 100644
index 00000000000..aec60edf971
--- /dev/null
+++ b/addons/mail/models/mail_channel_partner.py
@@ -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)
diff --git a/addons/mail/models/mail_moderation.py b/addons/mail/models/mail_moderation.py
new file mode 100644
index 00000000000..7e4982a946a
--- /dev/null
+++ b/addons/mail/models/mail_moderation.py
@@ -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 !')
+ ]
diff --git a/addons/mail/views/mail_channel_partner_views.xml b/addons/mail/views/mail_channel_partner_views.xml
new file mode 100644
index 00000000000..b89ec9c61b3
--- /dev/null
+++ b/addons/mail/views/mail_channel_partner_views.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ mail.channel.partner.tree
+ mail.channel.partner
+ 10
+
+
+
+
+
+
+
+
+
+
+ mail.channel.partner.form
+ mail.channel.partner
+
+
+
+
+
+
+ Channels/Partner
+ mail.channel.partner
+ tree,form
+
+
+
+
+
diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml
index 78853166aca..16d279803c1 100644
--- a/addons/mail/views/mail_channel_views.xml
+++ b/addons/mail/views/mail_channel_views.xml
@@ -1,50 +1,6 @@
-
-
-
- mail.channel.partner.tree
- mail.channel.partner
- 10
-
-
-
-
-
-
-
-
-
-
- mail.channel.partner.form
- mail.channel.partner
-
-
-
-
-
-
- Channels/Partner
- mail.channel.partner
- tree,form
-
-
-
-
mail.channel.kanban