From 11b8735d8a68bfe44ba4c3aaedada85983e1b337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 21 Dec 2020 08:35:14 +0000 Subject: [PATCH] [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 --- addons/mail/__manifest__.py | 1 + addons/mail/models/__init__.py | 2 + addons/mail/models/mail_channel.py | 410 ++++++++---------- addons/mail/models/mail_channel_partner.py | 44 ++ addons/mail/models/mail_moderation.py | 20 + .../mail/views/mail_channel_partner_views.xml | 46 ++ addons/mail/views/mail_channel_views.xml | 44 -- 7 files changed, 302 insertions(+), 265 deletions(-) create mode 100644 addons/mail/models/mail_channel_partner.py create mode 100644 addons/mail/models/mail_moderation.py create mode 100644 addons/mail/views/mail_channel_partner_views.xml 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)s invited %(new_partner)s to #%(channel_name)s
') % { + 'author': self.env.user.display_name, + 'new_partner': partner.display_name, + 'channel_id': channel.id, + 'channel_name': channel.name, + } + else: + notification = _('
joined #%s
') % (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)s invited %(new_partner)s to #%(channel_name)s
') % { - 'author': self.env.user.display_name, - 'new_partner': partner.display_name, - 'channel_id': channel.id, - 'channel_name': channel.name, - } - else: - notification = _('
joined #%s
') % (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