[REF] mail: move discuss.channel code to its own folder (models)

This commit focuses on removing all references to discuss.channel from
code located in /models of mail module.

In preparation of splitting discuss and mail modules.

Part of task-3265211

closes odoo/odoo#119413

Signed-off-by: Debondt Didier (did) <did@odoo.com>
This commit is contained in:
Sébastien Theys
2023-04-26 19:49:08 +02:00
parent b92415023d
commit b1772094b1
22 changed files with 409 additions and 204 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import discuss
from . import mail
# after mail specifically as discuss module depends on mail
from . import discuss
+6 -7
View File
@@ -34,10 +34,6 @@ from . import mail_tracking_value
from . import mail_template
# discuss
from . import discuss_channel_member
from . import discuss_channel_rtc_session
from . import discuss_channel
from . import mail_guest
from . import mail_ice_server
from . import mail_shortcode
from . import res_users_settings
@@ -55,10 +51,13 @@ from . import ir_model
from . import ir_model_fields
from . import ir_ui_view
from . import ir_qweb
from . import ir_websocket
from . import res_company
from . import res_config_settings
from . import res_partner
from . import res_users
from . import res_groups
from . import update
# after mail specifically as discuss module depends on mail
from . import discuss
# discuss_channel_member must be loaded first
from . import res_partner
+17
View File
@@ -0,0 +1,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# mail
from . import mail_message
# discuss
from . import discuss_channel_member
from . import discuss_channel_rtc_session
from . import discuss_channel
from . import mail_guest
# odoo models
from . import ir_attachment
from . import ir_websocket
from . import res_groups
from . import res_partner
from . import res_users
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
@@ -260,7 +259,7 @@ class Channel(models.Model):
failing_channels = self.sudo().filtered(lambda channel: channel.channel_type != vals.get('channel_type'))
if failing_channels:
raise UserError(_('Cannot change the channel type of: %(channel_names)s'), channel_names=', '.join(failing_channels.mapped('name')))
result = super(Channel, self).write(vals)
result = super().write(vals)
if vals.get('group_ids'):
self._subscribe_users_automatically()
if 'image_128' in vals:
@@ -561,7 +560,7 @@ class Channel(models.Model):
def _notify_thread(self, message, msg_vals=False, **kwargs):
# link message to channel
rdata = super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
rdata = super()._notify_thread(message, msg_vals=msg_vals, **kwargs)
message_format_values = message.message_format()[0]
bus_notifications = self._channel_message_notifications(message, message_format_values)
@@ -583,7 +582,7 @@ class Channel(models.Model):
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)
return super()._message_receive_bounce(email, partner)
def _message_compute_author(self, author_id=None, email_from=None, raise_on_email=True):
return super()._message_compute_author(author_id=author_id, email_from=email_from, raise_on_email=False)
@@ -1163,7 +1162,7 @@ class Channel(models.Model):
@api.model
def get_mention_suggestions(self, search, limit=8):
""" Return 'limit'-first channels' id, name, channel_type and authorizedGroupFullName fields such that the
""" Return 'limit'-first channels' id, name, channel_type and authorizedGroupFullName fields such that the
name matches a 'search' string. Exclude channels of type chat (DM) and group.
"""
domain = expression.AND([
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
@@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _bus_notification_target(self):
self.ensure_one()
if self.res_model == "discuss.channel" and self.res_id:
return self.env["discuss.channel"].browse(self.res_id)
guest = self.env["mail.guest"]._get_guest_from_context()
if self.env.user._is_public() and guest:
return guest
return super()._bus_notification_target()
@@ -4,14 +4,16 @@ from odoo.addons.bus.websocket import wsrequest
class IrWebsocket(models.AbstractModel):
_inherit = 'ir.websocket'
_inherit = "ir.websocket"
def _get_im_status(self, data):
im_status = super()._get_im_status(data)
if 'mail.guest' in data:
im_status['Guest'] = self.env['mail.guest'].sudo().with_context(active_test=False).search_read(
[('id', 'in', data['mail.guest'])],
['im_status']
if "mail.guest" in data:
im_status["Guest"] = (
self.env["mail.guest"]
.sudo()
.with_context(active_test=False)
.search_read([("id", "in", data["mail.guest"])], ["im_status"])
)
return im_status
@@ -22,8 +24,8 @@ class IrWebsocket(models.AbstractModel):
# request.
req = request or wsrequest
channels = list(channels) # do not alter original list
guest_sudo = self.env['mail.guest']._get_guest_from_request(req).sudo()
discuss_channels = self.env['discuss.channel']
guest_sudo = self.env["mail.guest"]._get_guest_from_request(req).sudo()
discuss_channels = self.env["discuss.channel"]
if req.session.uid:
discuss_channels = self.env.user.partner_id.channel_ids
elif guest_sudo:
@@ -41,7 +43,11 @@ class IrWebsocket(models.AbstractModel):
# retrieve the current guest. Let's retrieve the proper
# request.
req = request or wsrequest
guest_sudo = self.env['mail.guest']._get_guest_from_request(req).sudo()
guest_sudo = self.env["mail.guest"]._get_guest_from_request(req).sudo()
if not guest_sudo:
return
guest_sudo.env['bus.presence'].update_presence(inactivity_period, identity_field='guest_id', identity_value=guest_sudo.id)
guest_sudo.env["bus.presence"].update_presence(
inactivity_period,
identity_field="guest_id",
identity_value=guest_sudo.id,
)
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
@@ -0,0 +1,23 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MailMessage(models.Model):
_inherit = "mail.message"
def _message_format_extras(self, format_reply):
self.ensure_one()
vals = super()._message_format_extras(format_reply)
if format_reply and self.model == "discuss.channel" and self.parent_id:
vals["parentMessage"] = self.parent_id.message_format(format_reply=False)[0]
return vals
def _bus_notification_target(self):
self.ensure_one()
if self.model == "discuss.channel" and self.res_id:
return self.env["discuss.channel"].browse(self.res_id)
guest = self.env["mail.guest"]._get_guest_from_context()
if self.env.user._is_public() and guest:
return guest
return super()._bus_notification_target()
+16
View File
@@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResGroups(models.Model):
_inherit = "res.groups"
def write(self, vals):
res = super().write(vals)
if vals.get("users"):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_ids = [command[1] for command in vals["users"] if command[0] == 4]
user_ids += [id for command in vals["users"] if command[0] == 6 for id in command[2]]
self.env["discuss.channel"].search([("group_ids", "in", self._ids)])._subscribe_users_automatically()
return res
+128
View File
@@ -0,0 +1,128 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.osv import expression
class ResPartner(models.Model):
_inherit = "res.partner"
channel_ids = fields.Many2many(
"discuss.channel",
"discuss_channel_member",
"partner_id",
"channel_id",
string="Channels",
copy=False,
)
def _get_channels_as_member(self):
"""Returns the channels of the partner."""
self.ensure_one()
channels = self.env["discuss.channel"]
# get the channels and groups
channels |= self.env["discuss.channel"].search(
[
("channel_type", "in", ("channel", "group")),
("channel_partner_ids", "in", [self.id]),
]
)
# get the pinned direct messages
channels |= self.env["discuss.channel"].search(
[
("channel_type", "=", "chat"),
(
"channel_member_ids",
"in",
self.env["discuss.channel.member"]
.sudo()
._search(
[
("partner_id", "=", self.id),
("is_pinned", "=", True),
]
),
),
]
)
return channels
@api.model
def search_for_channel_invite(self, search_term, channel_id=None, limit=30):
"""Returns partners matching search_term that can be invited to a channel.
If the channel_id is specified, only partners that can actually be invited to the channel
are returned (not already members, and in accordance to the channel configuration).
"""
domain = expression.AND(
[
expression.OR(
[
[("name", "ilike", search_term)],
[("email", "ilike", search_term)],
]
),
[("active", "=", True)],
[("type", "!=", "private")],
[("user_ids", "!=", False)],
[("user_ids.active", "=", True)],
[("user_ids.share", "=", False)],
]
)
if channel_id:
channel = self.env["discuss.channel"].search([("id", "=", int(channel_id))])
domain = expression.AND([domain, [("channel_ids", "not in", channel.id)]])
if channel.group_public_id:
domain = expression.AND([domain, [("user_ids.groups_id", "in", channel.group_public_id.id)]])
query = self.env["res.partner"]._search(domain, order="name, id")
query.order = 'LOWER("res_partner"."name"), "res_partner"."id"' # bypass lack of support for case insensitive order in search()
query.limit = int(limit)
return {
"count": self.env["res.partner"].search_count(domain),
"partners": list(self.env["res.partner"].browse(query).mail_partner_format().values()),
}
@api.model
def get_mention_suggestions_from_channel(self, channel_id, search, limit=8):
"""Return 'limit'-first partners' such that the name or email matches a 'search' string.
Prioritize partners that are also (internal) users, and then extend the research to all partners.
Only members of the given channel are returned.
The return format is a list of partner data (as per returned by `mail_partner_format()`).
"""
channel = self.env["discuss.channel"].search([("id", "=", channel_id)])
if not channel:
return []
domain = expression.AND(
[
self._get_mention_suggestions_domain(search),
[("channel_ids", "in", channel.id)],
]
)
partners = self._search_mention_suggestions(domain, limit)
member_by_partner = {
member.partner_id: member
for member in self.env["discuss.channel.member"].search(
[
("channel_id", "=", channel.id),
("partner_id", "in", partners.ids),
]
)
}
partners_format = partners.mail_partner_format()
for partner in partners:
partners_format.get(partner)["persona"] = {
"channelMembers": [
(
"insert",
member_by_partner.get(partner)
._discuss_channel_member_format(
fields={
"id": True,
"channel": {"id"},
"persona": {"partner": {"id"}},
}
)
.get(member_by_partner.get(partner)),
)
],
}
return list(partners_format.values())
+49
View File
@@ -0,0 +1,49 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.base.models.res_users import is_selection_groups
class ResUsers(models.Model):
_inherit = "res.users"
@api.model_create_multi
def create(self, vals_list):
users = super().create(vals_list)
# Auto-subscribe to channels unless skip explicitly requested
if not self.env.context.get("discuss_channel_nosubscribe"):
self.env["discuss.channel"].search(
[("group_ids", "in", users.groups_id.ids)]
)._subscribe_users_automatically()
return users
def write(self, vals):
res = super().write(vals)
if "active" in vals and not vals["active"]:
self._unsubscribe_from_non_public_channels()
sel_groups = [vals[k] for k in vals if is_selection_groups(k) and vals[k]]
if vals.get("groups_id"):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_group_ids = [command[1] for command in vals["groups_id"] if command[0] == 4]
user_group_ids += [id for command in vals["groups_id"] if command[0] == 6 for id in command[2]]
self.env["discuss.channel"].search([("group_ids", "in", user_group_ids)])._subscribe_users_automatically()
elif sel_groups:
self.env["discuss.channel"].search([("group_ids", "in", sel_groups)])._subscribe_users_automatically()
return res
def unlink(self):
self._unsubscribe_from_non_public_channels()
return super().unlink()
def _unsubscribe_from_non_public_channels(self):
"""This method un-subscribes users from group restricted channels. Main purpose
of this method is to prevent sending internal communication to archived / deleted users.
We do not un-subscribes users from public channels because in most common cases,
public channels are mailing list (e-mail based) and so users should always receive
updates from public channels until they manually un-subscribe themselves.
"""
domain = [("partner_id", "in", self.partner_id.ids)]
current_cm = self.env["discuss.channel.member"].sudo().search(domain)
current_cm.filtered(
lambda cm: (cm.channel_id.channel_type == "channel" and cm.channel_id.group_public_id)
).unlink()
+7 -8
View File
@@ -39,16 +39,15 @@ class IrAttachment(models.Model):
related_record.message_main_attachment_id = self
def _delete_and_notify(self):
for attachment in self:
if attachment.res_model == 'discuss.channel' and attachment.res_id:
target = self.env['discuss.channel'].browse(attachment.res_id)
else:
target = self.env.user.partner_id
self.env['bus.bus']._sendone(target, 'ir.attachment/delete', {
'id': attachment.id,
})
self.env['bus.bus']._sendmany((attachment._bus_notification_target(), 'ir.attachment/delete', {
'id': attachment.id,
}) for attachment in self)
self.unlink()
def _bus_notification_target(self):
self.ensure_one()
return self.env.user.partner_id
def _attachment_format(self, legacy=False):
safari = request and request.httprequest.user_agent and request.httprequest.user_agent.browser == 'safari'
res_list = []
+2 -15
View File
@@ -42,14 +42,7 @@ class LinkPreview(models.Model):
link_previews |= self.env['mail.link.preview']._create_link_preview(url, message.id, requests_session)
if not link_previews:
return
guest = self.env['mail.guest']._get_guest_from_context()
if message.model == 'discuss.channel' and message.res_id:
target = self.env['discuss.channel'].browse(message.res_id)
elif self.env.user._is_public() and guest:
target = guest
else:
target = self.env.user.partner_id
self.env['bus.bus']._sendone(target, 'mail.record/insert', {
self.env['bus.bus']._sendone(message._bus_notification_target(), 'mail.record/insert', {
'LinkPreview': link_previews._link_preview_format()
})
@@ -67,13 +60,7 @@ class LinkPreview(models.Model):
notifications = []
guest = self.env['mail.guest']._get_guest_from_context()
for link_preview in self:
if link_preview.message_id.model == 'discuss.channel' and link_preview.message_id.res_id:
target = self.env['discuss.channel'].browse(link_preview.message_id.res_id)
elif self.env.user._is_public() and guest:
target = guest
else:
target = self.env.user.partner_id
notifications.append((target, 'mail.link.preview/delete', {
notifications.append((link_preview.message_id._bus_notification_target(), 'mail.link.preview/delete', {
'id': link_preview.id,
'message_id': link_preview.message_id.id,
}))
+9 -5
View File
@@ -856,12 +856,10 @@ class Message(models.Model):
def _message_format(self, fnames, format_reply=True, legacy=False):
"""Reads values from messages and formats them for the web client."""
vals_list = self._read_format(fnames)
thread_ids_by_model_name = defaultdict(set)
for message in self:
if message.model and message.res_id:
thread_ids_by_model_name[message.model].add(message.res_id)
for vals in vals_list:
message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids)
author = {
@@ -890,9 +888,8 @@ class Message(models.Model):
'message': {'id': message_sudo.id},
'partners': [{'id': partner.id, 'name': partner.name} for partner in reactions.partner_id],
} for content, reactions in reactions_per_content.items()]
if format_reply and message_sudo.model == 'discuss.channel' and message_sudo.parent_id:
vals['parentMessage'] = message_sudo.parent_id.message_format(format_reply=False)[0]
allowed_tracking_ids = message_sudo.tracking_value_ids.filtered(lambda tracking: not tracking.field_groups or self.env.is_superuser() or self.user_has_groups(tracking.field_groups))
vals.update(message_sudo._message_format_extras(format_reply))
vals.update({
'author': author,
'default_subject': default_subject,
@@ -905,9 +902,12 @@ class Message(models.Model):
'pinned_at': message_sudo.pinned_at,
'record_name': record_name,
})
return vals_list
def _message_format_extras(self, format_reply):
self.ensure_one()
return {}
@api.model
def _message_fetch(self, domain, before=None, after=None, around=None, limit=30):
""" Get a limited amount of formatted messages with provided domain.
@@ -1075,6 +1075,10 @@ class Message(models.Model):
]
self.env['bus.bus']._sendmany(updates)
def _bus_notification_target(self):
self.ensure_one()
return self.env.user.partner_id
# ------------------------------------------------------
# TOOLS
# ------------------------------------------------------
-23
View File
@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResGroups(models.Model):
""" Update of res.groups class
- if adding users from a group, check discuss.channels linked to this user
group and subscribe them. This is done by overriding the write method.
"""
_name = 'res.groups'
_inherit = 'res.groups'
_description = 'Access Groups'
def write(self, vals):
res = super(ResGroups, self).write(vals)
if vals.get('users'):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_ids = [command[1] for command in vals['users'] if command[0] == 4]
user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
self.env['discuss.channel'].search([('group_ids', 'in', self._ids)])._subscribe_users_automatically()
return res
+17 -63
View File
@@ -20,8 +20,6 @@ class Partner(models.Model):
parent_id = fields.Many2one(tracking=3)
user_id = fields.Many2one(tracking=4)
vat = fields.Char(tracking=5)
# channels
channel_ids = fields.Many2many('discuss.channel', 'discuss_channel_member', 'partner_id', 'channel_id', string='Channels', copy=False)
# tracked field used for chatter logging purposes
# we need this to be readable inline as tracking messages use inline HTML nodes
contact_address_inline = fields.Char(compute='_compute_contact_address_inline', string='Inlined Complete Address', tracking=True)
@@ -264,71 +262,34 @@ class Partner(models.Model):
], limit=100)
return notifications.mail_message_id._message_notification_format()
def _get_channels_as_member(self):
"""Returns the channels of the partner."""
self.ensure_one()
channels = self.env['discuss.channel']
# get the channels and groups
channels |= self.env['discuss.channel'].search([
('channel_type', 'in', ('channel', 'group')),
('channel_partner_ids', 'in', [self.id]),
])
# get the pinned direct messages
channels |= self.env['discuss.channel'].search([
('channel_type', '=', 'chat'),
('channel_member_ids', 'in', self.env['discuss.channel.member'].sudo()._search([
('partner_id', '=', self.id),
('is_pinned', '=', True),
])),
])
return channels
@api.model
def get_mention_suggestions(self, search, limit=8):
""" Return 'limit'-first partners' such that the name or email matches a 'search' string.
Prioritize partners that are also (internal) users, and then extend the research to all partners.
The return format is a list of partner data (as per returned by `mail_partner_format()`).
"""
domain = self._get_mention_suggestions_domain(search)
partners = self._search_mention_suggestions(domain, limit)
return list(partners.mail_partner_format().values())
@api.model
def search_for_channel_invite(self, search_term, channel_id=None, limit=30):
""" Returns partners matching search_term that can be invited to a channel.
If the channel_id is specified, only partners that can actually be invited to the channel
are returned (not already members, and in accordance to the channel configuration).
"""
domain = expression.AND([
def _get_mention_suggestions_domain(self, search):
return expression.AND([
expression.OR([
[('name', 'ilike', search_term)],
[('email', 'ilike', search_term)],
[('name', 'ilike', search)],
[('email', 'ilike', search)],
]),
[('active', '=', True)],
[('type', '!=', 'private')],
[('user_ids', '!=', False)],
[('user_ids.active', '=', True)],
[('user_ids.share', '=', False)],
])
if channel_id:
channel = self.env['discuss.channel'].search([('id', '=', int(channel_id))])
domain = expression.AND([domain, [('channel_ids', 'not in', channel.id)]])
if channel.group_public_id:
domain = expression.AND([domain, [('user_ids.groups_id', 'in', channel.group_public_id.id)]])
query = self.env['res.partner']._search(domain, order='name, id')
query.order = 'LOWER("res_partner"."name"), "res_partner"."id"' # bypass lack of support for case insensitive order in search()
query.limit = int(limit)
return {
'count': self.env['res.partner'].search_count(domain),
'partners': list(self.env['res.partner'].browse(query).mail_partner_format().values()),
}
@api.model
def get_mention_suggestions(self, search, limit=8, channel_id=None):
""" Return 'limit'-first partners' such that the name or email matches a 'search' string.
Prioritize partners that are also (internal) users, and then extend the research to all partners.
If channel_id is given, only members of this channel are returned.
The return format is a list of partner data (as per returned by `mail_partner_format()`).
"""
search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
search_dom = expression.AND([[('active', '=', True), ('type', '!=', 'private')], search_dom])
if channel_id:
search_dom = expression.AND([[('channel_ids', 'in', channel_id)], search_dom])
domain_is_user = expression.AND([[('user_ids', '!=', False), ('user_ids.active', '=', True)], search_dom])
def _search_mention_suggestions(self, domain, limit):
domain_is_user = expression.AND([[('user_ids', '!=', False)], [('user_ids.active', '=', True)], domain])
priority_conditions = [
expression.AND([domain_is_user, [('partner_share', '=', False)]]), # Search partners that are internal users
domain_is_user, # Search partners that are users
search_dom, # Search partners that are not users
domain, # Search partners that are not users
]
partners = self.env['res.partner']
for domain in priority_conditions:
@@ -340,14 +301,7 @@ class Partner(models.Model):
# really slow.
query = self._search(expression.AND([[('id', 'not in', partners.ids)], domain]), limit=remaining_limit)
partners |= self.browse(query)
partners_format = partners.mail_partner_format()
if channel_id:
member_by_partner = {member.partner_id: member for member in self.env['discuss.channel.member'].search([('channel_id', '=', channel_id), ('partner_id', 'in', partners.ids)])}
for partner in partners:
partners_format.get(partner)['persona'] = {
'channelMembers': [('insert', member_by_partner.get(partner)._discuss_channel_member_format(fields={'id': True, 'channel': {'id'}, 'persona': {'partner': {'id'}}}).get(member_by_partner.get(partner)))],
}
return list(partners_format.values())
return partners
@api.model
def im_search(self, name, limit=20, excluded_ids=None):
-35
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, exceptions, fields, models, modules, tools
from odoo.addons.base.models.res_users import is_selection_groups
from odoo.tools import email_normalize
@@ -12,8 +11,6 @@ class Users(models.Model):
- make a new user follow itself
- add a welcome message
- add suggestion preference
- if adding groups to a user, check discuss.channels linked to this user
group, and the user. This is done by overriding the write method.
"""
_name = 'res.users'
_inherit = ['res.users']
@@ -101,9 +98,6 @@ class Users(models.Model):
message_type='notification',
subtype_xmlid='mail.mt_note'
)
# Auto-subscribe to channels unless skip explicitly requested
if not self.env.context.get('discuss_channel_nosubscribe'):
self.env['discuss.channel'].search([('group_ids', 'in', users.groups_id.ids)])._subscribe_users_automatically()
return users
def write(self, vals):
@@ -159,23 +153,8 @@ class Users(models.Model):
mail_values={'email_to': previous_email},
suggest_password_reset=False,
)
if 'active' in vals and not vals['active']:
self._unsubscribe_from_non_public_channels()
sel_groups = [vals[k] for k in vals if is_selection_groups(k) and vals[k]]
if vals.get('groups_id'):
# form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
self.env['discuss.channel'].search([('group_ids', 'in', user_group_ids)])._subscribe_users_automatically()
elif sel_groups:
self.env['discuss.channel'].search([('group_ids', 'in', sel_groups)])._subscribe_users_automatically()
return write_res
def unlink(self):
self._unsubscribe_from_non_public_channels()
return super().unlink()
def _notify_security_setting_update(self, subject, content, mail_values=None, **kwargs):
""" This method is meant to be called whenever a sensitive update is done on the user's account.
It will send an email to the concerned user warning him about this change and making some security suggestions.
@@ -239,20 +218,6 @@ class Users(models.Model):
'update_datetime': fields.Datetime.now(),
}
def _unsubscribe_from_non_public_channels(self):
""" This method un-subscribes users from group restricted channels. Main purpose
of this method is to prevent sending internal communication to archived / deleted users.
We do not un-subscribes users from public channels because in most common cases,
public channels are mailing list (e-mail based) and so users should always receive
updates from public channels until they manually un-subscribe themselves.
"""
current_cm = self.env['discuss.channel.member'].sudo().search([
('partner_id', 'in', self.partner_id.ids),
])
current_cm.filtered(
lambda cm: (cm.channel_id.channel_type == 'channel' and cm.channel_id.group_public_id)
).unlink()
def _get_portal_access_update_body(self, access_granted):
body = _('Portal Access Granted') if access_granted else _('Portal Access Revoked')
if self.partner_id.email:
@@ -37,19 +37,20 @@ export class SuggestionService {
}
}
/**
* @param {string} term
* @param {import("@mail/core/thread_model").Thread} thread
*/
async fetchPartners(term, thread) {
const kwargs = { search: term };
const isNonPublicChannel =
thread &&
(thread.type === "group" ||
thread.type === "chat" ||
(thread.type === "channel" && thread.group_based_subscription));
if (isNonPublicChannel) {
if (thread.model === "discuss.channel") {
kwargs.channel_id = thread.id;
}
const suggestedPartners = await this.orm.call(
"res.partner",
"get_mention_suggestions",
thread.model === "discuss.channel"
? "get_mention_suggestions_from_channel"
: "get_mention_suggestions",
[],
kwargs
);
@@ -20,6 +20,12 @@ patch(MockServer.prototype, "mail/models/res_partner", {
if (args.model === "res.partner" && args.method === "get_mention_suggestions") {
return this._mockResPartnerGetMentionSuggestions(args);
}
if (
args.model === "res.partner" &&
args.method === "get_mention_suggestions_from_channel"
) {
return this._mockResPartnerGetMentionSuggestionsFromChannel(args);
}
return this._super(route, args);
},
@@ -30,6 +36,69 @@ patch(MockServer.prototype, "mail/models/res_partner", {
* @returns {Array[]}
*/
_mockResPartnerGetMentionSuggestions(args) {
const search = (args.args[0] || args.kwargs.search || "").toLowerCase();
const limit = args.args[1] || args.kwargs.limit || 8;
/**
* Returns the given list of partners after filtering it according to
* the logic of the Python method `get_mention_suggestions` for the
* given search term. The result is truncated to the given limit and
* formatted as expected by the original method.
*
* @param {Object[]} partners
* @param {string} search
* @param {integer} limit
* @returns {Object[]}
*/
const mentionSuggestionsFilter = (partners, search, limit) => {
const matchingPartners = [
...this._mockResPartnerMailPartnerFormat(
partners
.filter((partner) => {
// no search term is considered as return all
if (!search) {
return true;
}
// otherwise name or email must match search term
if (partner.name && partner.name.toLowerCase().includes(search)) {
return true;
}
if (partner.email && partner.email.toLowerCase().includes(search)) {
return true;
}
return false;
})
.map((partner) => partner.id)
).values(),
];
// reduce results to max limit
matchingPartners.length = Math.min(matchingPartners.length, limit);
return matchingPartners;
};
// add main suggestions based on users
const partnersFromUsers = this.getRecords("res.users", [])
.map((user) => this.getRecords("res.partner", [["id", "=", user.partner_id]])[0])
.filter((partner) => partner);
const mainMatchingPartners = mentionSuggestionsFilter(partnersFromUsers, search, limit);
let extraMatchingPartners = [];
// if not enough results add extra suggestions based on partners
const remainingLimit = limit - mainMatchingPartners.length;
if (mainMatchingPartners.length < limit) {
const partners = this.getRecords("res.partner", [
["id", "not in", mainMatchingPartners.map((partner) => partner.id)],
]);
extraMatchingPartners = mentionSuggestionsFilter(partners, search, remainingLimit);
}
return mainMatchingPartners.concat(extraMatchingPartners);
},
/**
* Simulates `get_channel_mention_suggestions` on `res.partner`.
*
* @private
* @returns {Array[]}
*/
_mockResPartnerGetMentionSuggestionsFromChannel(args) {
const search = (args.args[0] || args.kwargs.search || "").toLowerCase();
const limit = args.args[1] || args.kwargs.limit || 8;
const channel_id = args.args[2] || args.kwargs.channel_id;
@@ -50,14 +119,12 @@ patch(MockServer.prototype, "mail/models/res_partner", {
...this._mockResPartnerMailPartnerFormat(
partners
.filter((partner) => {
if (channel_id) {
const [member] = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
["partner_id", "=", partner.id],
]);
if (!member) {
return false;
}
const [member] = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
["partner_id", "=", partner.id],
]);
if (!member) {
return false;
}
// no search term is considered as return all
if (!search) {
@@ -75,22 +142,20 @@ patch(MockServer.prototype, "mail/models/res_partner", {
.map((partner) => partner.id)
).values(),
].map((partnerFormat) => {
if (channel_id) {
const [member] = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
["partner_id", "=", partnerFormat.id],
]);
partnerFormat["persona"] = {
channelMembers: [
[
"insert",
this._mockDiscussChannelMember_DiscussChannelMemberFormat([
member.id,
]),
],
const [member] = this.getRecords("discuss.channel.member", [
["channel_id", "=", channel_id],
["partner_id", "=", partnerFormat.id],
]);
partnerFormat["persona"] = {
channelMembers: [
[
"insert",
this._mockDiscussChannelMember_DiscussChannelMemberFormat([
member.id,
])[0],
],
};
}
],
};
return partnerFormat;
});
// reduce results to max limit
+1 -1
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from unittest.mock import patch
from odoo import Command, fields
from odoo.addons.mail.models.discuss_channel import channel_avatar, group_avatar
from odoo.addons.mail.models.discuss.discuss_channel import channel_avatar, group_avatar
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError, UserError