[MERGE] mail: reorganize channel model and views

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;

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

Some tools methods do not necessarily require to be public.

Cleanup a bit mail addon by moving menu definitions in their own file
according to guidelines.

No functional change should be involved in this PR as only some code move
and light renaming has been performed.

LINKS

Prepares Task ID-2070632 (Discuss channel task)
Prepares Task ID-2419762 (SM channel task)
COM PR #64862

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2021-01-21 16:50:36 +01:00
committed by GitHub
22 changed files with 408 additions and 375 deletions
+2 -2
View File
@@ -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:
+2 -1
View File
@@ -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',
@@ -25,6 +24,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',
@@ -45,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',
+2
View File
@@ -15,6 +15,8 @@ from . import mail_thread
from . import mail_thread_blacklist
from . import mail_thread_cc
from . import mail_blacklist
from . import mail_moderation
from . import mail_channel_partner
from . import mail_channel
from . import mail_template
from . import mail_shortcode
+191 -223
View File
@@ -10,68 +10,11 @@ from odoo import _, api, fields, models, modules, tools, Command
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import ormcache, formataddr
from odoo.exceptions import AccessError
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg']
_logger = logging.getLogger(__name__)
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_description = 'Listeners of a Channel'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
custom_channel_name = fields.Char('Custom channel name')
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimized")
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
@api.model_create_multi
def create(self, vals_list):
"""Similar access rule as the access rule of the mail channel.
It can not be implemented in XML, because when the record will be created, the
partner will be added in the channel and the security rule will always authorize
the creation.
"""
if not self.env.is_admin():
for vals in vals_list:
if 'channel_id' in vals and not self.env.is_admin():
channel_id = self.env['mail.channel'].browse(vals['channel_id'])
if not channel_id._can_invite(vals.get('partner_id')):
raise AccessError(_('This user can not be added in this channel'))
return super(ChannelPartner, self).create(vals_list)
def write(self, vals):
if not self.env.is_admin():
if {'channel_id', 'partner_id', 'partner_email'} & set(vals):
raise AccessError(_('You can not write on this field'))
return super(ChannelPartner, self).write(vals)
class Moderation(models.Model):
_name = 'mail.moderation'
_description = 'Channel black/white list'
email = fields.Char(string="Email", index=True, required=True)
status = fields.Selection([
('allow', 'Always Allow'),
('ban', 'Permanent Ban')],
string="Status", required=True)
channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
_sql_constraints = [
('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
]
class Channel(models.Model):
""" A mail.channel is a discussion group that may behave like a listener
on documents. """
@@ -94,39 +37,39 @@ class Channel(models.Model):
image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
return base64.b64encode(open(image_path, 'rb').read())
# description
name = fields.Char('Name', required=True, translate=True)
active = fields.Boolean(default=True, help="Set active to false to hide the channel without removing it.")
channel_type = fields.Selection([
('chat', 'Chat Discussion'),
('channel', 'Channel')],
'Channel Type', default='channel')
string='Channel Type', default='channel')
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat')
description = fields.Text('Description')
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel
# depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
# members (depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`)
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen', depends=['channel_partner_ids'])
channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners', depends=['channel_last_seen_partner_ids'])
channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel')
is_member = fields.Boolean('Is a member', compute='_compute_is_member')
# access
public = fields.Selection([
('public', 'Everyone'),
('private', 'Invited people only'),
('groups', 'Selected group of users')],
'Privacy', required=True, default='groups',
help='This group is visible by non members. Invisible groups can add members through the invite button.')
group_public_id = fields.Many2one('res.groups', string='Authorized Group',
default=lambda self: self.env.ref('base.group_user'))
is_subscribed = fields.Boolean(
'Is Subscribed', compute='_compute_is_subscribed')
group_ids = fields.Many2many(
'res.groups', string='Auto Subscription',
help="Members of those groups will automatically added as followers. "
"Note that they will be able to manage their subscription manually "
"if necessary.")
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
is_subscribed = fields.Boolean(
'Is Subscribed', compute='_compute_is_subscribed')
# access
uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False)
public = fields.Selection([
('public', 'Everyone'),
('private', 'Invited people only'),
('groups', 'Selected group of users')], string='Privacy',
required=True, default='groups',
help='This group is visible by non members. Invisible groups can add members through the invite button.')
group_public_id = fields.Many2one('res.groups', string='Authorized Group',
default=lambda self: self.env.ref('base.group_user'))
# moderation
moderation = fields.Boolean(string='Moderate this channel')
moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators')
@@ -142,6 +85,22 @@ class Channel(models.Model):
moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.")
moderation_guidelines_msg = fields.Text(string="Guidelines")
# COMPUTE / INVERSE
@api.depends('channel_type')
def _compute_is_chat(self):
for record in self:
record.is_chat = record.channel_type == 'chat'
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
])
membership_ids = memberships.mapped('channel_id')
for record in self:
record.is_member = record in membership_ids
@api.depends('channel_partner_ids')
def _compute_is_subscribed(self):
for channel in self:
@@ -159,6 +118,8 @@ class Channel(models.Model):
for channel in self:
channel.moderation_count = data.get(channel.id, 0)
# CONSTRAINTS
@api.constrains('moderator_ids')
def _check_moderator_email(self):
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
@@ -184,19 +145,7 @@ class Channel(models.Model):
if any(not channel.moderator_ids for channel in self if channel.moderation):
raise ValidationError(_('Moderated channels must have moderators.'))
def _compute_is_member(self):
memberships = self.env['mail.channel.partner'].sudo().search([
('channel_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
])
membership_ids = memberships.mapped('channel_id')
for record in self:
record.is_member = record in membership_ids
@api.depends('channel_type')
def _compute_is_chat(self):
for record in self:
record.is_chat = record.channel_type == 'chat'
# ONCHANGE
@api.onchange('public')
def _onchange_public(self):
@@ -226,6 +175,10 @@ class Channel(models.Model):
else:
self.moderator_ids |= self.env.user
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
defaults = self.default_get(['image_128'])
@@ -305,12 +258,14 @@ class Channel(models.Model):
return result
def _alias_get_creation_values(self):
values = super(Channel, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id
if self.id:
values['alias_force_thread_id'] = self.id
return values
def init(self):
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
# ------------------------------------------------------------
# MEMBERS MANAGEMENT
# ------------------------------------------------------------
def _subscribe_users(self):
to_create = []
@@ -371,6 +326,85 @@ class Channel(models.Model):
]).unlink()
self.invalidate_cache(fnames=['channel_partner_ids', 'channel_last_seen_partner_ids'])
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
:param partner_ids : list of partner id to add
"""
partners = self.env['res.partner'].browse(partner_ids)
self._invite_check_access(partners)
# add the partner
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add:
if partner.id != self.env.user.partner_id.id:
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
'author': self.env.user.display_name,
'new_partner': partner.display_name,
'channel_id': channel.id,
'channel_name': channel.name,
}
else:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False)
# broadcast the channel header to the added partner
self._broadcast(partner_ids)
def _invite_check_access(self, partners):
""" Check invited partners could match channel access """
failed = []
if any(channel.public == 'groups' for channel in self):
for channel in self.filtered(lambda c: c.public == 'groups'):
invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')]
failed += [(channel, partner) for partner in invalid_partners]
if failed:
raise UserError(
_('Following invites are invalid as user groups do not match: %s') %
', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed)
)
def _can_invite(self, partner_id):
"""Return True if the current user can invite the partner to the channel.
* public: ok;
* private: must be member;
* group: both current user and target must have group;
:return boolean: whether inviting is ok"""
partner = self.env['res.partner'].browse(partner_id)
for channel in self.sudo():
if channel.public == 'private' and not channel.is_member:
return False
if channel.public == 'groups':
if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id:
return False
if channel.group_public_id not in self.env.user.groups_id:
return False
return True
# ------------------------------------------------------------
# MAILING
# ------------------------------------------------------------
def _alias_get_creation_values(self):
values = super(Channel, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id
if self.id:
values['alias_force_thread_id'] = self.id
return values
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return _('restricted to channel members')
return False
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
def _notify_get_groups(self, msg_vals=None):
""" All recipients of a message on a channel are considered as partners.
This means they will receive a minimal email, without a link to access
@@ -397,13 +431,6 @@ class Channel(models.Model):
headers['X-Forge-To'] = list_to
return headers
def _message_receive_bounce(self, email, partner):
""" Override bounce management to unsubscribe bouncing addresses """
for p in partner:
if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
self._action_unfollow(p)
return super(Channel, self)._message_receive_bounce(email, partner)
def _notify_email_recipient_values(self, recipient_ids):
# Excluded Blacklisted
whitelist = self.env['res.partner'].sudo().browse(recipient_ids).filtered(lambda p: not p.is_blacklisted)
@@ -415,32 +442,19 @@ class Channel(models.Model):
}
return super(Channel, self)._notify_email_recipient_values(whitelist.ids)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
of a message. For this operation the message's author email address is required.
This address is returned with status for other computations. """
moderation_status = 'accepted'
email = ''
if self.moderation and message_type in ['email', 'comment']:
author_id = kwargs.get('author_id')
if author_id and isinstance(author_id, int):
email = self.env['res.partner'].browse([author_id]).email
elif author_id:
email = author_id.email
elif kwargs.get('email_from'):
email = tools.email_split(kwargs['email_from'])[0]
else:
email = self.env.user.email
if email in self.mapped('moderator_ids.email'):
return moderation_status, email
status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
if status and status[0] == 'allow':
moderation_status = 'accepted'
elif status and status[0] == 'ban':
moderation_status = 'rejected'
else:
moderation_status = 'pending_moderation'
return moderation_status, email
def _notify_thread(self, message, msg_vals=False, **kwargs):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
else:
message._notify_pending_by_chat()
def _message_receive_bounce(self, email, partner):
""" Override bounce management to unsubscribe bouncing addresses """
for p in partner:
if p.message_bounce >= self.MAX_BOUNCE_LIMIT:
self._action_unfollow(p)
return super(Channel, self)._message_receive_bounce(email, partner)
@api.returns('mail.message', lambda value: value.id)
def message_post(self, *, message_type='notification', **kwargs):
@@ -473,22 +487,36 @@ class Channel(models.Model):
self._set_last_seen_message(message)
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return _('restricted to channel members')
return False
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
def _extract_moderation_values(self, message_type, **kwargs):
""" This method is used to compute moderation status before the creation
of a message. For this operation the message's author email address is required.
This address is returned with status for other computations. """
moderation_status = 'accepted'
email = ''
if self.moderation and message_type in ['email', 'comment']:
author_id = kwargs.get('author_id')
if author_id and isinstance(author_id, int):
email = self.env['res.partner'].browse([author_id]).email
elif author_id:
email = author_id.email
elif kwargs.get('email_from'):
email = tools.email_split(kwargs['email_from'])[0]
else:
email = self.env.user.email
if email in self.mapped('moderator_ids.email'):
return moderation_status, email
status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status')
if status and status[0] == 'allow':
moderation_status = 'accepted'
elif status and status[0] == 'ban':
moderation_status = 'rejected'
else:
moderation_status = 'pending_moderation'
return moderation_status, email
def init(self):
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
if not self._cr.fetchone():
self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)')
# --------------------------------------------------
# Moderation
# --------------------------------------------------
# ------------------------------------------------------------
# MODERATION
# ------------------------------------------------------------
def send_guidelines(self):
""" Send guidelines to all channel members. """
@@ -540,14 +568,9 @@ class Channel(models.Model):
cmds += [Command.create({'email': email, 'status': status}) for email in not_moderated]
return self.write({'moderation_ids': cmds})
#------------------------------------------------------
# Instant Messaging API
#------------------------------------------------------
# A channel header should be broadcasted:
# - when adding user to channel (only to the new added partners)
# - when folding/minimizing a channel (only to the user making the action)
# A message should be broadcasted:
# - when a message is posted on a channel (to the channel, using _notify() method)
# ------------------------------------------------------------
# BROADCAST
# ------------------------------------------------------------
# Anonymous method
def _broadcast(self, partner_ids):
@@ -573,13 +596,6 @@ class Channel(models.Model):
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
return notifications
def _notify_thread(self, message, msg_vals=False, **kwargs):
# When posting a message on a mail channel, manage moderation and postpone notify users
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
else:
message._notify_pending_by_chat()
def _channel_message_notifications(self, message, message_format=False):
""" Generate the bus notifications for the given message
:param message : the mail.message to sent
@@ -594,8 +610,18 @@ 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):
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():
@@ -628,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)
@@ -893,66 +919,6 @@ class Channel(models.Model):
}
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', channel.id), data]])
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
:param partner_ids : list of partner id to add
"""
partners = self.env['res.partner'].browse(partner_ids)
self._invite_check_access(partners)
# add the partner
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
for partner in partners_to_add:
if partner.id != self.env.user.partner_id.id:
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
'author': self.env.user.display_name,
'new_partner': partner.display_name,
'channel_id': channel.id,
'channel_name': channel.name,
}
else:
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False)
# broadcast the channel header to the added partner
self._broadcast(partner_ids)
def _invite_check_access(self, partners):
""" Check invited partners could match channel access """
failed = []
if any(channel.public == 'groups' for channel in self):
for channel in self.filtered(lambda c: c.public == 'groups'):
invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')]
failed += [(channel, partner) for partner in invalid_partners]
if failed:
raise UserError(
_('Following invites are invalid as user groups do not match: %s') %
', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed)
)
def _can_invite(self, partner_id):
"""Return True if the current user can invite the partner to the channel.
* public: ok;
* private: must be member;
* group: both current user and target must have group;
:return boolean: whether inviting is ok"""
partner = self.env['res.partner'].browse(partner_id)
for channel in self.sudo():
if channel.public == 'private' and not channel.is_member:
return False
if channel.public == 'groups':
if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id:
return False
if channel.group_public_id not in self.env.user.groups_id:
return False
return True
@api.model
def channel_set_custom_name(self, channel_id, name=False):
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.id', '=', channel_id)]
@@ -977,9 +943,10 @@ class Channel(models.Model):
notifications.append([channel.uuid, data]) # notify frontend users
self.env['bus.bus'].sendmany(notifications)
#------------------------------------------------------
# Instant Messaging View Specific (Slack Client Action)
#------------------------------------------------------
# ------------------------------------------------------------
# IM VIEW SPECIFIC (Slack Client Action)
# ------------------------------------------------------------
@api.model
def channel_fetch_slot(self):
""" Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
@@ -1106,9 +1073,10 @@ class Channel(models.Model):
""", (tuple(self.ids),))
return self.env.cr.dictfetchall()
#------------------------------------------------------
# Commands
#------------------------------------------------------
# ------------------------------------------------------------
# COMMANDS
# ------------------------------------------------------------
@api.model
@ormcache()
def get_mention_commands(self):
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import AccessError
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_description = 'Listeners of a Channel'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
custom_channel_name = fields.Char('Custom channel name')
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimized")
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
@api.model_create_multi
def create(self, vals_list):
"""Similar access rule as the access rule of the mail channel.
It can not be implemented in XML, because when the record will be created, the
partner will be added in the channel and the security rule will always authorize
the creation.
"""
if not self.env.is_admin():
for vals in vals_list:
if 'channel_id' in vals:
channel_id = self.env['mail.channel'].browse(vals['channel_id'])
if not channel_id._can_invite(vals.get('partner_id')):
raise AccessError(_('This user can not be added in this channel'))
return super(ChannelPartner, self).create(vals_list)
def write(self, vals):
if not self.env.is_admin():
if {'channel_id', 'partner_id', 'partner_email'} & set(vals):
raise AccessError(_('You can not write on this field'))
return super(ChannelPartner, self).write(vals)
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class Moderation(models.Model):
_name = 'mail.moderation'
_description = 'Channel black/white list'
email = fields.Char(string="Email", index=True, required=True)
status = fields.Selection([
('allow', 'Always Allow'),
('ban', 'Permanent Ban')],
string="Status", required=True)
channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True)
_sql_constraints = [
('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !')
]
@@ -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
-13
View File
@@ -85,12 +85,6 @@
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_mail_activity_type"
action="mail_activity_type_action"
parent="mail.mail_menu_technical"
sequence="10"
/>
<record id="mail_activity_action" model="ir.actions.act_window">
<field name="name">Activities</field>
@@ -98,13 +92,6 @@
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="menu_mail_activities"
action="mail_activity_action"
parent="mail.mail_menu_technical"
sequence="11"
/>
<record id="mail_activity_view_form_popup" model="ir.ui.view">
<field name="name">mail.activity.view.form.popup</field>
<field name="model">mail.activity</field>
-6
View File
@@ -78,11 +78,5 @@
</field>
</record>
<menuitem id="mail_alias_menu"
parent="base.menu_email"
action="action_view_mail_alias"
sequence="11"
groups="base.group_no_one"/>
</data>
</odoo>
@@ -67,11 +67,4 @@
</field>
</record>
<!-- Technical Menu -->
<menuitem id="mail_blacklist_menu"
name="Email Blacklist"
action="mail_blacklist_action"
parent="mail.mail_menu_technical"
sequence="22"/>
</odoo>
@@ -0,0 +1,39 @@
<?xml version="1.0"?>
<odoo><data>
<!-- mail.channel.partner -->
<record id="mail_channel_partner_view_tree" model="ir.ui.view">
<field name="name">mail.channel.partner.tree</field>
<field name="model">mail.channel.partner</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Channels">
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</tree>
</field>
</record>
<record id="mail_channel_partner_view_form" model="ir.ui.view">
<field name="name">mail.channel.partner.form</field>
<field name="model">mail.channel.partner</field>
<field name="arch" type="xml">
<form string="Channel">
<sheet>
<group>
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mail_channel_partner_action" model="ir.actions.act_window">
<field name="name">Channels/Partner</field>
<field name="res_model">mail.channel.partner</field>
<field name="view_mode">tree,form</field>
</record>
</data></odoo>
-68
View File
@@ -1,50 +1,6 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- mail.channel.partner -->
<record id="mail_channel_partner_view_tree" model="ir.ui.view">
<field name="name">mail.channel.partner.tree</field>
<field name="model">mail.channel.partner</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Channels">
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</tree>
</field>
</record>
<record id="mail_channel_partner_view_form" model="ir.ui.view">
<field name="name">mail.channel.partner.form</field>
<field name="model">mail.channel.partner</field>
<field name="arch" type="xml">
<form string="Channel">
<sheet>
<group>
<field name="partner_id"/>
<field name="channel_id"/>
<field name="seen_message_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mail_channel_partner_action" model="ir.actions.act_window">
<field name="name">Channels/Partner</field>
<field name="res_model">mail.channel.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Channels/Partner"
id="mail_channel_partner_menu"
parent="base.menu_email"
action="mail_channel_partner_action"
sequence="21"
groups="base.group_no_one"/>
<!-- mail.channel -->
<record id="mail_channel_view_kanban" model="ir.ui.view">
<field name="name">mail.channel.kanban</field>
@@ -192,11 +148,6 @@
<field name="search_view_id" ref="mail_channel_view_search"/>
</record>
<!-- settings !-->
<menuitem id="mail_channel_menu_settings" parent="base.menu_email" sequence="20"
name="Channels" action="mail_channel_action_view"
groups="base.group_no_one"/>
<record id="action_discuss" model="ir.actions.client">
<field name="name">Discuss</field>
<field name="tag">mail.widgets.discuss</field>
@@ -204,24 +155,5 @@
<field name="params" eval="&quot;{ 'default_active_id': 'mail.box_inbox' }&quot;"/>
</record>
<menuitem
id="mail.menu_root_discuss"
name="Discuss"
action="action_discuss"
web_icon="mail,static/description/icon.png"
groups="base.group_user"
sequence="1"
/>
<!--
This menuitem will be activated by integrations modules (like github, twitter, ...). It
is a hook to ease other modules to plug into mail.
-->
<record id="mail.mail_channel_integrations_menu" model="ir.ui.menu">
<field name="name">Integrations</field>
<field name="parent_id" ref="mail.menu_root_discuss"></field>
<field name="active" eval="False"></field>
</record>
</data>
</odoo>
@@ -45,13 +45,5 @@
<field name="view_mode">tree,form</field>
</record>
<!-- Add followers related menu entries in Settings/Email -->
<menuitem name="Followers"
id="menu_email_followers"
parent="mail.mail_menu_technical"
action="action_view_followers"
sequence="21"
groups="base.group_no_one"/>
</data>
</odoo>
-6
View File
@@ -123,11 +123,5 @@
<field name="search_view_id" ref="view_mail_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Emails"
id="menu_mail_mail"
parent="base.menu_email"
action="action_view_mail_mail"
sequence="1"/>
</data>
</odoo>
+100
View File
@@ -1,11 +1,111 @@
<?xml version="1.0"?>
<odoo><data>
<menuitem name="Discuss"
id="mail.menu_root_discuss"
action="action_discuss"
web_icon="mail,static/description/icon.png"
groups="base.group_user"
sequence="1"
/>
<record id="base.menu_email" model="ir.ui.menu">
<field name="sequence">3</field>
</record>
<!-- Under Technical/Email -->
<menuitem name="Emails"
id="menu_mail_mail"
parent="base.menu_email"
action="action_view_mail_mail"
sequence="1"/>
<menuitem id="menu_email_templates"
parent="base.menu_email"
action="action_email_template_tree_all"
sequence="10"/>
<menuitem id="mail_alias_menu"
parent="base.menu_email"
action="action_view_mail_alias"
sequence="11"
groups="base.group_no_one"/>
<menuitem id="mail_channel_menu_settings"
name="Channels"
parent="base.menu_email"
action="mail_channel_action_view"
sequence="20"
groups="base.group_no_one"/>
<menuitem name="Channels/Partner"
id="mail_channel_partner_menu"
parent="base.menu_email"
action="mail_channel_partner_action"
sequence="21"
groups="base.group_no_one"/>
<menuitem name="Channel Moderation"
id="mail_moderation_menu"
parent="base.menu_email"
sequence="22"
action="mail_moderation_action"/>
<!-- Under Technical/Discuss -->
<menuitem name="Discuss"
id="mail_menu_technical"
parent="base.menu_custom"
sequence="1"/>
<menuitem name="Messages"
id="menu_mail_message"
parent="mail.mail_menu_technical"
action="action_view_mail_message"
sequence="1"/>
<menuitem name="Subtypes"
id="menu_message_subtype"
parent="mail.mail_menu_technical"
action="action_view_message_subtype"
sequence="4"/>
<menuitem name="Tracking Values"
id="menu_mail_tracking_value"
parent="mail.mail_menu_technical"
action="action_view_mail_tracking_value"
sequence="5"/>
<menuitem
id="menu_mail_activity_type"
action="mail_activity_type_action"
parent="mail.mail_menu_technical"
sequence="10"
/>
<menuitem
id="menu_mail_activities"
action="mail_activity_action"
parent="mail.mail_menu_technical"
sequence="11"
/>
<menuitem name="Notifications"
id="mail_notification_menu"
parent="mail.mail_menu_technical"
action="mail_notification_action"
sequence="20"
groups="base.group_no_one"/>
<menuitem name="Followers"
id="menu_email_followers"
parent="mail.mail_menu_technical"
action="action_view_followers"
sequence="21"
groups="base.group_no_one"/>
<menuitem id="mail_blacklist_menu"
name="Email Blacklist"
action="mail_blacklist_action"
parent="mail.mail_menu_technical"
sequence="22"/>
<!--
This menuitem will be activated by integrations modules (like github, twitter, ...). It
is a hook to ease other modules to plug into mail.
-->
<record id="mail.mail_channel_integrations_menu" model="ir.ui.menu">
<field name="name">Integrations</field>
<field name="parent_id" ref="mail.menu_root_discuss"></field>
<field name="active" eval="False"></field>
</record>
</data></odoo>
@@ -47,11 +47,6 @@
<field name="res_model">mail.message.subtype</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Subtypes"
id="menu_message_subtype"
parent="mail.mail_menu_technical"
action="action_view_message_subtype"
sequence="4"/>
</data>
</odoo>
-7
View File
@@ -125,13 +125,6 @@
<field name="search_view_id" ref="view_message_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Messages"
id="menu_mail_message"
parent="mail.mail_menu_technical"
action="action_view_mail_message"
sequence="1"/>
<record model="ir.ui.view" id="view_document_file_kanban">
<field name="name">ir.attachment kanban</field>
<field name="model">ir.attachment</field>
@@ -42,11 +42,5 @@
<field name="context">{'search_default_status_ban': 1}</field>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Channel Moderation"
id="mail_moderation_menu"
parent="base.menu_email"
sequence="22"
action="mail_moderation_action"/>
</data>
</odoo>
@@ -48,11 +48,4 @@
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Notifications"
id="mail_notification_menu"
parent="mail.mail_menu_technical"
action="mail_notification_action"
sequence="20"
groups="base.group_no_one"/>
</data></odoo>
@@ -131,8 +131,5 @@
<field name="search_view_id" ref="view_email_template_search"/>
</record>
<menuitem id="menu_email_templates" parent="base.menu_email" action="action_email_template_tree_all"
sequence="10"/>
</data>
</odoo>
@@ -65,11 +65,6 @@
<field name="res_model">mail.tracking.value</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Tracking Values"
id="menu_mail_tracking_value"
parent="mail.mail_menu_technical"
action="action_view_mail_tracking_value"
sequence="5"/>
</data>
</odoo>
+7 -7
View File
@@ -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='<p>Test message_post as log</p>',
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='<p>Test Post Performances basic</p>',
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='<p>Test Post Performances with an email ping</p>',
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='<p>Test Post Performances with an inbox ping</p>',
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='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',