[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+4
-5
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user