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 1/4] [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 From 57fe16ef617bc1ba6fa71aa45c1a64a2402e5f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 21 Jan 2021 13:56:06 +0000 Subject: [PATCH 2/4] [IMP] mail: make some tools methods private PURPOSE Reorganize and lint mail.channel code. Purpose is to ease future modifications linked to groups and channels in mail / Discuss. SPECIFICATIONS Some tools methods do not necessarily require to be public. LINKS Prepares Task ID-2070632 (Discuss channel task) Prepares Task ID-2419762 (SM channel task) COM PR odoo/odoo#64862 --- addons/hr_holidays/models/mail_channel.py | 4 ++-- addons/mail/models/mail_channel.py | 4 ++-- addons/mail/static/tests/helpers/mock_server.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/hr_holidays/models/mail_channel.py b/addons/hr_holidays/models/mail_channel.py index 166caa37d3e..4a1105d97e7 100644 --- a/addons/hr_holidays/models/mail_channel.py +++ b/addons/hr_holidays/models/mail_channel.py @@ -7,8 +7,8 @@ from odoo import fields, models class Channel(models.Model): _inherit = 'mail.channel' - def partner_info(self, all_partners, direct_partners): - partner_infos = super(Channel, self).partner_info(all_partners, direct_partners) + def _get_channel_partner_info(self, all_partners, direct_partners): + partner_infos = super(Channel, self)._get_channel_partner_info(all_partners, direct_partners) # only search for leave out_of_office_date_end if im_status is on leave partners_on_leave = [partner_id for partner_id in direct_partners.ids if 'leave' in partner_infos[partner_id]['im_status']] if partners_on_leave: diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index db1b39de91e..237edf8ef50 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -621,7 +621,7 @@ class Channel(models.Model): # ------------------------------------------------------------ @api.model - def partner_info(self, all_partners, direct_partners): + def _get_channel_partner_info(self, all_partners, direct_partners): """ Return the information needed by channel to display channel members :param all_partners: list of res.parner(): @@ -654,7 +654,7 @@ class Channel(models.Model): all_partners = all_partner_channel.mapped('partner_id') direct_channel_partners = all_partner_channel.filtered(lambda pc: channel_dict[pc.channel_id.id].channel_type == 'chat') direct_partners = direct_channel_partners.mapped('partner_id') - partner_infos = self.partner_info(all_partners, direct_partners) + partner_infos = self._get_channel_partner_info(all_partners, direct_partners) # add last message preview (only used in mobile) addPreview = self._context.get('isMobile', False) diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 71adf0e043e..11ad476b3e2 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -1028,7 +1028,7 @@ MockServer.include({ this._widget.call('bus_service', 'trigger', 'notification', notifications); }, /** - * Simulates `partner_info` on `mail.channel`. + * Simulates `_get_channel_partner_info` on `mail.channel`. * * @private * @param {integer[]} all_partners From e72dd61c94cb79f03a3708ad56f93537cd4c0415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 20 Jan 2021 08:35:41 +0000 Subject: [PATCH 3/4] [MOV] mail: move menu definitions in their own file PURPOSE Reorganize and lint mail.channel code. Purpose is to ease future modifications linked to groups and channels in mail / Discuss. SPECIFICATIONS Cleanup a bit mail addon by moving menu definitions in their own file according to guidelines. LINKS Prepares Task ID-2070632 (Discuss channel task) Prepares Task ID-2419762 (SM channel task) COM PR odoo/odoo#64862 --- addons/mail/__manifest__.py | 2 +- addons/mail/views/mail_activity_views.xml | 13 --- addons/mail/views/mail_alias_views.xml | 6 -- addons/mail/views/mail_blacklist_views.xml | 7 -- .../mail/views/mail_channel_partner_views.xml | 7 -- addons/mail/views/mail_channel_views.xml | 24 ----- addons/mail/views/mail_followers_views.xml | 8 -- addons/mail/views/mail_mail_views.xml | 6 -- addons/mail/views/mail_menus.xml | 100 ++++++++++++++++++ .../mail/views/mail_message_subtype_views.xml | 5 - addons/mail/views/mail_message_views.xml | 7 -- addons/mail/views/mail_moderation_views.xml | 6 -- addons/mail/views/mail_notification_views.xml | 7 -- addons/mail/views/mail_template_views.xml | 3 - addons/mail/views/mail_tracking_views.xml | 5 - 15 files changed, 101 insertions(+), 105 deletions(-) diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 81185de0e4b..0f3aed77aee 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -11,7 +11,6 @@ 'depends': ['base', 'base_setup', 'bus', 'web_tour'], 'data': [ 'views/assets.xml', - 'views/mail_menus.xml', 'wizard/invite_view.xml', 'wizard/mail_blacklist_remove_view.xml', 'wizard/mail_compose_message_view.xml', @@ -46,6 +45,7 @@ 'views/ir_model_views.xml', 'views/res_partner_views.xml', 'views/mail_blacklist_views.xml', + 'views/mail_menus.xml', ], 'demo': [ 'data/mail_channel_demo.xml', diff --git a/addons/mail/views/mail_activity_views.xml b/addons/mail/views/mail_activity_views.xml index 5245124baba..3103a7f6d69 100644 --- a/addons/mail/views/mail_activity_views.xml +++ b/addons/mail/views/mail_activity_views.xml @@ -85,12 +85,6 @@ tree,form - Activities @@ -98,13 +92,6 @@ tree,form - - mail.activity.view.form.popup mail.activity diff --git a/addons/mail/views/mail_alias_views.xml b/addons/mail/views/mail_alias_views.xml index 3584bac53e7..4fa3255f744 100644 --- a/addons/mail/views/mail_alias_views.xml +++ b/addons/mail/views/mail_alias_views.xml @@ -78,11 +78,5 @@ - -
diff --git a/addons/mail/views/mail_blacklist_views.xml b/addons/mail/views/mail_blacklist_views.xml index 4ad4dc86c87..b0aea4ed67e 100644 --- a/addons/mail/views/mail_blacklist_views.xml +++ b/addons/mail/views/mail_blacklist_views.xml @@ -67,11 +67,4 @@ - - - diff --git a/addons/mail/views/mail_channel_partner_views.xml b/addons/mail/views/mail_channel_partner_views.xml index b89ec9c61b3..adb3a8915cc 100644 --- a/addons/mail/views/mail_channel_partner_views.xml +++ b/addons/mail/views/mail_channel_partner_views.xml @@ -36,11 +36,4 @@ tree,form - - diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml index 16d279803c1..dce184c0a80 100644 --- a/addons/mail/views/mail_channel_views.xml +++ b/addons/mail/views/mail_channel_views.xml @@ -148,11 +148,6 @@ - - - Discuss mail.widgets.discuss @@ -160,24 +155,5 @@ - - - - - Integrations - - - - diff --git a/addons/mail/views/mail_followers_views.xml b/addons/mail/views/mail_followers_views.xml index bb098690b57..d4ba0a0411c 100644 --- a/addons/mail/views/mail_followers_views.xml +++ b/addons/mail/views/mail_followers_views.xml @@ -45,13 +45,5 @@ tree,form - - - diff --git a/addons/mail/views/mail_mail_views.xml b/addons/mail/views/mail_mail_views.xml index bc087e39b6d..1eef28a6253 100644 --- a/addons/mail/views/mail_mail_views.xml +++ b/addons/mail/views/mail_mail_views.xml @@ -123,11 +123,5 @@ - - diff --git a/addons/mail/views/mail_menus.xml b/addons/mail/views/mail_menus.xml index 44df3e561ed..57cbfac23a3 100644 --- a/addons/mail/views/mail_menus.xml +++ b/addons/mail/views/mail_menus.xml @@ -1,11 +1,111 @@ + + 3 + + + + + + + + + + + + + + + + + + + + + + + + Integrations + + + + diff --git a/addons/mail/views/mail_message_subtype_views.xml b/addons/mail/views/mail_message_subtype_views.xml index a9c9171e72f..f13a459c7ca 100644 --- a/addons/mail/views/mail_message_subtype_views.xml +++ b/addons/mail/views/mail_message_subtype_views.xml @@ -47,11 +47,6 @@ mail.message.subtype tree,form - diff --git a/addons/mail/views/mail_message_views.xml b/addons/mail/views/mail_message_views.xml index f0405ea3fec..df65876ee14 100644 --- a/addons/mail/views/mail_message_views.xml +++ b/addons/mail/views/mail_message_views.xml @@ -125,13 +125,6 @@ - - - ir.attachment kanban ir.attachment diff --git a/addons/mail/views/mail_moderation_views.xml b/addons/mail/views/mail_moderation_views.xml index 4bcc602bbcb..73a1bb1263b 100644 --- a/addons/mail/views/mail_moderation_views.xml +++ b/addons/mail/views/mail_moderation_views.xml @@ -42,11 +42,5 @@ {'search_default_status_ban': 1} - - diff --git a/addons/mail/views/mail_notification_views.xml b/addons/mail/views/mail_notification_views.xml index 3167fb4cda4..b96b7f70b71 100644 --- a/addons/mail/views/mail_notification_views.xml +++ b/addons/mail/views/mail_notification_views.xml @@ -48,11 +48,4 @@ tree,form - - diff --git a/addons/mail/views/mail_template_views.xml b/addons/mail/views/mail_template_views.xml index 0f3744027f3..35f488929d6 100644 --- a/addons/mail/views/mail_template_views.xml +++ b/addons/mail/views/mail_template_views.xml @@ -131,8 +131,5 @@ - - diff --git a/addons/mail/views/mail_tracking_views.xml b/addons/mail/views/mail_tracking_views.xml index ea43c5fa418..793023d1092 100644 --- a/addons/mail/views/mail_tracking_views.xml +++ b/addons/mail/views/mail_tracking_views.xml @@ -65,11 +65,6 @@ mail.tracking.value tree,form - \ No newline at end of file From b690b3f45581fb16842c6bd96e2ae46490013952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 20 Jan 2021 10:55:33 +0000 Subject: [PATCH 4/4] [UPD] test_mail: update query counters at current runbot state PURPOSE Reorganize and lint mail.channel code. Purpose is to ease future modifications linked to groups and channels in mail / Discuss. SPECIFICATIONS Purpose is to better spot future query counter updates by ensuring current thresholds effectively match runbot state. Some query counters for admin are currently a bit lower than expected, probably linked to various ORM improvements and cleaning done since last counter update. LINKS Prepares Task ID-2070632 (Discuss channel task) Prepares Task ID-2419762 (SM channel task) COM PR odoo/odoo#64862 --- addons/test_mail/tests/test_performance.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index 1ffea697f4a..424a465cd51 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -270,7 +270,7 @@ class TestMailAPIPerformance(BaseMailPerformance): 'partner_ids': [(4, customer_id)], }) - with self.assertQueryCount(__system__=29, emp=32): + with self.assertQueryCount(__system__=25, emp=32): composer.send_mail() @users('__system__', 'emp') @@ -290,7 +290,7 @@ class TestMailAPIPerformance(BaseMailPerformance): }).create({}) composer.onchange_template_id_wrapper() - with self.assertQueryCount(__system__=37, emp=40): + with self.assertQueryCount(__system__=33, emp=40): composer.send_mail() # remove created partner to ensure tests are the same each run @@ -331,7 +331,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_log_with_post(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=5, emp=6): + with self.assertQueryCount(__system__=3, emp=6): record.message_post( body='

Test message_post as log

', subtype_xmlid='mail.mt_note', @@ -342,7 +342,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_post_no_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=5, emp=6): + with self.assertQueryCount(__system__=3, emp=6): record.message_post( body='

Test Post Performances basic

', partner_ids=[], @@ -355,7 +355,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_post_one_email_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=30, emp=31): + with self.assertQueryCount(__system__=28, emp=31): record.message_post( body='

Test Post Performances with an email ping

', partner_ids=self.customer.ids, @@ -367,7 +367,7 @@ class TestMailAPIPerformance(BaseMailPerformance): def test_message_post_one_inbox_notification(self): record = self.env['mail.test.simple'].create({'name': 'Test'}) - with self.assertQueryCount(__system__=14, emp=17): + with self.assertQueryCount(__system__=12, emp=17): record.message_post( body='

Test Post Performances with an inbox ping

', partner_ids=self.user_test.partner_id.ids, @@ -951,7 +951,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance): ] self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals) attachement_ids = self.attachements.ids - with self.assertQueryCount(emp=81): + with self.assertQueryCount(emp=79): self.cr.sql_log = self.warm and self.cr.sql_log_count record.with_context({}).message_post( body='

Test body

',