mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 00:51:58 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -1,7 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import bus
|
||||
from . import bus_presence
|
||||
from . import bus_listener_mixin
|
||||
from . import ir_attachment
|
||||
from . import ir_http
|
||||
from . import ir_model
|
||||
from . import ir_websocket
|
||||
from . import res_groups
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
from . import res_partner
|
||||
|
|
|
|||
|
|
@ -5,25 +5,28 @@ import json
|
|||
import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import selectors
|
||||
import threading
|
||||
import time
|
||||
from psycopg2 import InterfaceError, sql
|
||||
from psycopg2 import InterfaceError
|
||||
from psycopg2.pool import PoolError
|
||||
|
||||
import odoo
|
||||
from ..tools import orjson
|
||||
from odoo import api, fields, models
|
||||
from odoo.service.server import CommonServer
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.tools import date_utils
|
||||
from odoo.tools import json_default, SQL
|
||||
from odoo.tools.constants import GC_UNLINK_LIMIT
|
||||
from odoo.tools.misc import OrderedSet
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# longpolling timeout connection
|
||||
TIMEOUT = 50
|
||||
DEFAULT_GC_RETENTION_SECONDS = 60 * 60 * 24 # 24 hours
|
||||
|
||||
# custom function to call instead of NOTIFY postgresql command (opt-in)
|
||||
ODOO_NOTIFY_FUNCTION = os.environ.get('ODOO_NOTIFY_FUNCTION')
|
||||
# custom function to call instead of default PostgreSQL's `pg_notify`
|
||||
ODOO_NOTIFY_FUNCTION = os.getenv('ODOO_NOTIFY_FUNCTION', 'pg_notify')
|
||||
|
||||
|
||||
def get_notify_payload_max_length(default=8000):
|
||||
|
|
@ -40,11 +43,11 @@ def get_notify_payload_max_length(default=8000):
|
|||
NOTIFY_PAYLOAD_MAX_LENGTH = get_notify_payload_max_length()
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
# Bus
|
||||
#----------------------------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
def json_dump(v):
|
||||
return json.dumps(v, separators=(',', ':'), default=date_utils.json_default)
|
||||
return json.dumps(v, separators=(',', ':'), default=json_default)
|
||||
|
||||
def hashable(key):
|
||||
if isinstance(key, list):
|
||||
|
|
@ -55,6 +58,8 @@ def hashable(key):
|
|||
def channel_with_db(dbname, channel):
|
||||
if isinstance(channel, models.Model):
|
||||
return (dbname, channel._name, channel.id)
|
||||
if isinstance(channel, tuple) and len(channel) == 2 and isinstance(channel[0], models.Model):
|
||||
return (dbname, channel[0]._name, channel[0].id, channel[1])
|
||||
if isinstance(channel, str):
|
||||
return (dbname, channel)
|
||||
return channel
|
||||
|
|
@ -80,9 +85,9 @@ def get_notify_payloads(channels):
|
|||
get_notify_payloads(channels[pivot:]))
|
||||
|
||||
|
||||
class ImBus(models.Model):
|
||||
|
||||
class BusBus(models.Model):
|
||||
_name = 'bus.bus'
|
||||
|
||||
_description = 'Communication Bus'
|
||||
|
||||
channel = fields.Char('Channel')
|
||||
|
|
@ -90,75 +95,107 @@ class ImBus(models.Model):
|
|||
|
||||
@api.autovacuum
|
||||
def _gc_messages(self):
|
||||
timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT*2)
|
||||
domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
|
||||
records = self.search(domain, limit=models.GC_UNLINK_LIMIT)
|
||||
if len(records) >= models.GC_UNLINK_LIMIT:
|
||||
self.env.ref('base.autovacuum_job')._trigger()
|
||||
return records.unlink()
|
||||
gc_retention_seconds = int(
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("bus.gc_retention_seconds", DEFAULT_GC_RETENTION_SECONDS)
|
||||
)
|
||||
timeout_ago = fields.Datetime.now() - datetime.timedelta(seconds=gc_retention_seconds)
|
||||
# Direct SQL to avoid ORM overhead; this way we can delete millions of rows quickly.
|
||||
# This is a low-level table with no expected references, and doing this avoids
|
||||
# the need to split or reschedule this GC job.
|
||||
self.env.cr.execute("DELETE FROM bus_bus WHERE create_date < %s", (timeout_ago,))
|
||||
|
||||
@api.model
|
||||
def _sendmany(self, notifications):
|
||||
channels = set()
|
||||
values = []
|
||||
for target, notification_type, message in notifications:
|
||||
channel = channel_with_db(self.env.cr.dbname, target)
|
||||
channels.add(channel)
|
||||
values.append({
|
||||
'channel': json_dump(channel),
|
||||
'message': json_dump({
|
||||
'type': notification_type,
|
||||
'payload': message,
|
||||
})
|
||||
})
|
||||
self.sudo().create(values)
|
||||
if channels:
|
||||
def _sendone(self, target, notification_type, message):
|
||||
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
|
||||
|
||||
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
|
||||
security.
|
||||
|
||||
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
|
||||
attacker.
|
||||
"""
|
||||
self._ensure_hooks()
|
||||
channel = channel_with_db(self.env.cr.dbname, target)
|
||||
self.env.cr.precommit.data["bus.bus.values"].append(
|
||||
{
|
||||
"channel": json_dump(channel),
|
||||
"message": json_dump(
|
||||
{
|
||||
"type": notification_type,
|
||||
"payload": message,
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
self.env.cr.postcommit.data["bus.bus.channels"].add(channel)
|
||||
|
||||
def _ensure_hooks(self):
|
||||
if "bus.bus.values" not in self.env.cr.precommit.data:
|
||||
self.env.cr.precommit.data["bus.bus.values"] = []
|
||||
|
||||
@self.env.cr.precommit.add
|
||||
def create_bus():
|
||||
self.sudo().create(self.env.cr.precommit.data.pop("bus.bus.values"))
|
||||
|
||||
if "bus.bus.channels" not in self.env.cr.postcommit.data:
|
||||
self.env.cr.postcommit.data["bus.bus.channels"] = OrderedSet()
|
||||
|
||||
# We have to wait until the notifications are commited in database.
|
||||
# When calling `NOTIFY imbus`, notifications will be fetched in the
|
||||
# bus table. If the transaction is not commited yet, there will be
|
||||
# nothing to fetch, and the websocket will return no notification.
|
||||
@self.env.cr.postcommit.add
|
||||
def notify():
|
||||
with odoo.sql_db.db_connect('postgres').cursor() as cr:
|
||||
if ODOO_NOTIFY_FUNCTION:
|
||||
query = sql.SQL("SELECT {}('imbus', %s)").format(sql.Identifier(ODOO_NOTIFY_FUNCTION))
|
||||
else:
|
||||
query = "NOTIFY imbus, %s"
|
||||
payloads = get_notify_payloads(list(channels))
|
||||
if len(payloads) > 1:
|
||||
_logger.info("The imbus notification payload was too large, "
|
||||
"it's been split into %d payloads.", len(payloads))
|
||||
payloads = get_notify_payloads(
|
||||
list(self.env.cr.postcommit.data.pop("bus.bus.channels"))
|
||||
)
|
||||
if len(payloads) > 1:
|
||||
_logger.info(
|
||||
"The imbus notification payload was too large, it's been split into %d payloads.",
|
||||
len(payloads),
|
||||
)
|
||||
with odoo.sql_db.db_connect("postgres").cursor() as cr:
|
||||
for payload in payloads:
|
||||
cr.execute(query, (payload,))
|
||||
cr.execute(
|
||||
SQL(
|
||||
"SELECT %s('imbus', %s)",
|
||||
SQL.identifier(ODOO_NOTIFY_FUNCTION),
|
||||
payload,
|
||||
)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _sendone(self, channel, notification_type, message):
|
||||
self._sendmany([[channel, notification_type, message]])
|
||||
|
||||
@api.model
|
||||
def _poll(self, channels, last=0):
|
||||
def _poll(self, channels, last=0, ignore_ids=None):
|
||||
# first poll return the notification in the 'buffer'
|
||||
if last == 0:
|
||||
timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT)
|
||||
domain = [('create_date', '>', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
|
||||
timeout_ago = fields.Datetime.now() - datetime.timedelta(seconds=TIMEOUT)
|
||||
domain = [('create_date', '>', timeout_ago)]
|
||||
else: # else returns the unread notifications
|
||||
domain = [('id', '>', last)]
|
||||
if ignore_ids:
|
||||
domain.append(("id", "not in", ignore_ids))
|
||||
channels = [json_dump(channel_with_db(self.env.cr.dbname, c)) for c in channels]
|
||||
domain.append(('channel', 'in', channels))
|
||||
notifications = self.sudo().search_read(domain)
|
||||
notifications = self.sudo().search_read(domain, ["message"])
|
||||
# list of notification to return
|
||||
result = []
|
||||
for notif in notifications:
|
||||
result.append({
|
||||
'id': notif['id'],
|
||||
'message': json.loads(notif['message']),
|
||||
'message': orjson.loads(notif['message']),
|
||||
})
|
||||
return result
|
||||
|
||||
def _bus_last_id(self):
|
||||
last = self.env['bus.bus'].search([], order='id desc', limit=1)
|
||||
return last.id if last else 0
|
||||
|
||||
#----------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Dispatcher
|
||||
#----------------------------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
|
||||
class BusSubscription:
|
||||
def __init__(self, channels, last):
|
||||
|
|
@ -211,7 +248,7 @@ class ImDispatch(threading.Thread):
|
|||
conn.poll()
|
||||
channels = []
|
||||
while conn.notifies:
|
||||
channels.extend(json.loads(conn.notifies.pop().payload))
|
||||
channels.extend(orjson.loads(conn.notifies.pop().payload))
|
||||
# relay notifications to websockets that have
|
||||
# subscribed to the corresponding channels.
|
||||
websockets = set()
|
||||
|
|
@ -225,7 +262,7 @@ class ImDispatch(threading.Thread):
|
|||
try:
|
||||
self.loop()
|
||||
except Exception as exc:
|
||||
if isinstance(exc, InterfaceError) and stop_event.is_set():
|
||||
if isinstance(exc, (InterfaceError, PoolError)) and stop_event.is_set():
|
||||
continue
|
||||
_logger.exception("Bus.loop error, sleep and retry")
|
||||
time.sleep(TIMEOUT)
|
||||
|
|
|
|||
30
odoo-bringout-oca-ocb-bus/bus/models/bus_listener_mixin.py
Normal file
30
odoo-bringout-oca-ocb-bus/bus/models/bus_listener_mixin.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class BusListenerMixin(models.AbstractModel):
|
||||
"""Allow sending messages related to the current model via as a bus.bus channel.
|
||||
|
||||
The model needs to be allowed as a valid channel for the bus in `_build_bus_channel_list`.
|
||||
"""
|
||||
|
||||
_name = 'bus.listener.mixin'
|
||||
_description = "Can send messages via bus.bus"
|
||||
|
||||
def _bus_send(self, notification_type, message, /, *, subchannel=None):
|
||||
"""Send a notification to the webclient."""
|
||||
for record in self:
|
||||
main_channel = record
|
||||
while (new_main_channel := main_channel._bus_channel()) != main_channel:
|
||||
main_channel = new_main_channel
|
||||
assert isinstance(main_channel, models.Model)
|
||||
if not main_channel:
|
||||
continue
|
||||
main_channel.ensure_one()
|
||||
channel = main_channel if subchannel is None else (main_channel, subchannel)
|
||||
# _sendone: channel is safe (record or tuple with record)
|
||||
self.env["bus.bus"]._sendone(channel, notification_type, message)
|
||||
|
||||
def _bus_channel(self):
|
||||
return self
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from psycopg2 import OperationalError
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import tools
|
||||
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
UPDATE_PRESENCE_DELAY = 60
|
||||
DISCONNECTION_TIMER = UPDATE_PRESENCE_DELAY + 5
|
||||
AWAY_TIMER = 1800 # 30 minutes
|
||||
|
||||
|
||||
class BusPresence(models.Model):
|
||||
""" User Presence
|
||||
Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not
|
||||
attached to res_users to avoid database concurrence errors. Since the 'update' method is executed
|
||||
at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'.
|
||||
"""
|
||||
|
||||
_name = 'bus.presence'
|
||||
_description = 'User Presence'
|
||||
_log_access = False
|
||||
|
||||
user_id = fields.Many2one('res.users', 'Users', ondelete='cascade')
|
||||
last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now())
|
||||
last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now())
|
||||
status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline')
|
||||
|
||||
def init(self):
|
||||
self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS bus_presence_user_unique ON %s (user_id) WHERE user_id IS NOT NULL" % self._table)
|
||||
|
||||
@api.model
|
||||
def update(self, inactivity_period, identity_field, identity_value):
|
||||
""" Updates the last_poll and last_presence of the current user
|
||||
:param inactivity_period: duration in milliseconds
|
||||
"""
|
||||
# This method is called in method _poll() and cursor is closed right
|
||||
# after; see bus/controllers/main.py.
|
||||
try:
|
||||
# Hide transaction serialization errors, which can be ignored, the presence update is not essential
|
||||
# The errors are supposed from presence.write(...) call only
|
||||
with tools.mute_logger('odoo.sql_db'):
|
||||
self._update(inactivity_period=inactivity_period, identity_field=identity_field, identity_value=identity_value)
|
||||
# commit on success
|
||||
self.env.cr.commit()
|
||||
except OperationalError as e:
|
||||
if e.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY:
|
||||
# ignore concurrency error
|
||||
return self.env.cr.rollback()
|
||||
raise
|
||||
|
||||
@api.model
|
||||
def _update(self, inactivity_period, identity_field, identity_value):
|
||||
presence = self.search([(identity_field, '=', identity_value)], limit=1)
|
||||
# compute last_presence timestamp
|
||||
last_presence = datetime.datetime.now() - datetime.timedelta(milliseconds=inactivity_period)
|
||||
values = {
|
||||
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
}
|
||||
# update the presence or a create a new one
|
||||
if not presence: # create a new presence for the user
|
||||
values[identity_field] = identity_value
|
||||
values['last_presence'] = last_presence
|
||||
self.create(values)
|
||||
else: # update the last_presence if necessary, and write values
|
||||
if presence.last_presence < last_presence:
|
||||
values['last_presence'] = last_presence
|
||||
presence.write(values)
|
||||
11
odoo-bringout-oca-ocb-bus/bus/models/ir_attachment.py
Normal file
11
odoo-bringout-oca-ocb-bus/bus/models/ir_attachment.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_name = 'ir.attachment'
|
||||
_inherit = ["ir.attachment", "bus.listener.mixin"]
|
||||
|
||||
def _bus_channel(self):
|
||||
return self.env.user
|
||||
19
odoo-bringout-oca-ocb-bus/bus/models/ir_http.py
Normal file
19
odoo-bringout-oca-ocb-bus/bus/models/ir_http.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from ..websocket import WebsocketConnectionHandler
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = "ir.http"
|
||||
|
||||
@api.model
|
||||
def get_frontend_session_info(self):
|
||||
session_info = super().get_frontend_session_info()
|
||||
session_info["websocket_worker_version"] = WebsocketConnectionHandler._VERSION
|
||||
return session_info
|
||||
|
||||
def session_info(self):
|
||||
session_info = super().session_info()
|
||||
session_info["websocket_worker_version"] = WebsocketConnectionHandler._VERSION
|
||||
return session_info
|
||||
|
|
@ -7,14 +7,17 @@ class IrModel(models.Model):
|
|||
_inherit = 'ir.model'
|
||||
|
||||
def _get_model_definitions(self, model_names_to_fetch):
|
||||
fields_by_model_names = {}
|
||||
model_definitions = {}
|
||||
for model_name in model_names_to_fetch:
|
||||
model = self.env[model_name]
|
||||
# get fields, relational fields are kept only if the related model is in model_names_to_fetch
|
||||
fields_data_by_fname = {
|
||||
fname: field_data
|
||||
for fname, field_data in model.fields_get(
|
||||
attributes=['name', 'type', 'relation', 'required', 'readonly', 'selection', 'string']
|
||||
attributes={
|
||||
'name', 'type', 'relation', 'required', 'readonly', 'selection',
|
||||
'string', 'definition_record', 'definition_record_field', 'model_field',
|
||||
},
|
||||
).items()
|
||||
if not field_data.get('relation') or field_data['relation'] in model_names_to_fetch
|
||||
}
|
||||
|
|
@ -23,10 +26,11 @@ class IrModel(models.Model):
|
|||
inverse_fields = [
|
||||
field for field in model.pool.field_inverses[model._fields[fname]]
|
||||
if field.model_name in model_names_to_fetch
|
||||
and model.env[field.model_name]._has_field_access(field, 'read')
|
||||
]
|
||||
if inverse_fields:
|
||||
field_data['inverse_fname_by_model_name'] = {field.model_name: field.name for field in inverse_fields}
|
||||
if field_data['type'] == 'many2one_reference':
|
||||
field_data['model_name_ref_fname'] = model._fields[fname].model_field
|
||||
fields_by_model_names[model_name] = fields_data_by_fname
|
||||
return fields_by_model_names
|
||||
model_definitions[model_name] = {"fields": fields_data_by_fname}
|
||||
return model_definitions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.http import SessionExpiredException
|
||||
from odoo.http import request, SessionExpiredException
|
||||
from odoo.tools.misc import OrderedSet
|
||||
from odoo.service import security
|
||||
from ..models.bus import dispatch
|
||||
from ..websocket import wsrequest
|
||||
|
|
@ -9,15 +12,6 @@ class IrWebsocket(models.AbstractModel):
|
|||
_name = 'ir.websocket'
|
||||
_description = 'websocket message handling'
|
||||
|
||||
def _get_im_status(self, im_status_ids_by_model):
|
||||
im_status = {}
|
||||
if 'res.partner' in im_status_ids_by_model:
|
||||
im_status['partners'] = self.env['res.partner'].with_context(active_test=False).search_read(
|
||||
[('id', 'in', im_status_ids_by_model['res.partner'])],
|
||||
['im_status']
|
||||
)
|
||||
return im_status
|
||||
|
||||
def _build_bus_channel_list(self, channels):
|
||||
"""
|
||||
Return the list of channels to subscribe to. Override this
|
||||
|
|
@ -26,33 +20,62 @@ class IrWebsocket(models.AbstractModel):
|
|||
|
||||
:param channels: The channel list sent by the client.
|
||||
"""
|
||||
req = request or wsrequest
|
||||
channels.append('broadcast')
|
||||
channels.extend(self.env.user.all_group_ids)
|
||||
if req.session.uid:
|
||||
channels.append(self.env.user.partner_id)
|
||||
return channels
|
||||
|
||||
def _subscribe(self, data):
|
||||
if not all(isinstance(c, str) for c in data['channels']):
|
||||
raise ValueError("bus.Bus only string channels are allowed.")
|
||||
last_known_notification_id = self.env['bus.bus'].sudo().search([], limit=1, order='id desc').id or 0
|
||||
if data['last'] > last_known_notification_id:
|
||||
data['last'] = 0
|
||||
channels = set(self._build_bus_channel_list(data['channels']))
|
||||
dispatch.subscribe(channels, data['last'], self.env.registry.db_name, wsrequest.ws)
|
||||
def _serve_ir_websocket(self, event_name, data):
|
||||
"""Process websocket events.
|
||||
Modules can override this method to handle their own events. But overriding this method is
|
||||
not recommended and should be carefully considered, because at the time of writing this
|
||||
message, Odoo.sh does not use this method. Each new event should have a corresponding http
|
||||
route and Odoo.sh infrastructure should be updated to reflect it. On top of that, the
|
||||
event processing is very time, ressource and error sensitive."""
|
||||
|
||||
def _update_bus_presence(self, inactivity_period, im_status_ids_by_model):
|
||||
if self.env.user and not self.env.user._is_public():
|
||||
self.env['bus.presence'].update(
|
||||
inactivity_period,
|
||||
identity_field='user_id',
|
||||
identity_value=self.env.uid
|
||||
)
|
||||
im_status_notification = self._get_im_status(im_status_ids_by_model)
|
||||
if im_status_notification:
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'bus/im_status', im_status_notification)
|
||||
def _prepare_subscribe_data(self, channels, last):
|
||||
"""
|
||||
Parse the data sent by the client and return the list of channels
|
||||
and the last known notification id. This will be used both by the
|
||||
websocket controller and the websocket request class when the
|
||||
`subscribe` event is received.
|
||||
|
||||
:param typing.List[str] channels: List of channels to subscribe to sent
|
||||
by the client.
|
||||
:param int last: Last known notification sent by the client.
|
||||
|
||||
:return:
|
||||
A dict containing the following keys:
|
||||
- channels (set of str): The list of channels to subscribe to.
|
||||
- last (int): The last known notification id.
|
||||
|
||||
:raise ValueError: If the list of channels is not a list of strings.
|
||||
"""
|
||||
if not all(isinstance(c, str) for c in channels):
|
||||
raise ValueError("bus.Bus only string channels are allowed.")
|
||||
# sudo - bus.bus: reading non-sensitive last bus id.
|
||||
last = 0 if last > self.env["bus.bus"].sudo()._bus_last_id() else last
|
||||
return {"channels": OrderedSet(self._build_bus_channel_list(list(channels))), "last": last}
|
||||
|
||||
def _after_subscribe_data(self, data):
|
||||
"""Function invoked after subscribe data have been processed.
|
||||
Modules can override this method to add custom behavior."""
|
||||
|
||||
def _subscribe(self, og_data):
|
||||
data = self._prepare_subscribe_data(og_data["channels"], og_data["last"])
|
||||
dispatch.subscribe(data["channels"], data["last"], self.env.registry.db_name, wsrequest.ws)
|
||||
self._after_subscribe_data(data)
|
||||
|
||||
def _on_websocket_closed(self, cookies):
|
||||
"""Function invoked upon WebSocket termination.
|
||||
Modules can override this method to add custom behavior."""
|
||||
|
||||
@classmethod
|
||||
def _authenticate(cls):
|
||||
if wsrequest.session.uid is not None:
|
||||
if not security.check_session(wsrequest.session, wsrequest.env):
|
||||
if not security.check_session(wsrequest.session, wsrequest.env, wsrequest):
|
||||
wsrequest.session.logout(keep_db=True)
|
||||
raise SessionExpiredException()
|
||||
else:
|
||||
|
|
|
|||
8
odoo-bringout-oca-ocb-bus/bus/models/res_groups.py
Normal file
8
odoo-bringout-oca-ocb-bus/bus/models/res_groups.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResGroups(models.Model):
|
||||
_name = 'res.groups'
|
||||
_inherit = ["res.groups", "bus.listener.mixin"]
|
||||
|
|
@ -1,29 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.bus.models.bus_presence import AWAY_TIMER
|
||||
from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
im_status = fields.Char('IM Status', compute='_compute_im_status')
|
||||
|
||||
def _compute_im_status(self):
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
U.partner_id as id,
|
||||
CASE WHEN max(B.last_poll) IS NULL THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
|
||||
ELSE 'online'
|
||||
END as status
|
||||
FROM bus_presence B
|
||||
RIGHT JOIN res_users U ON B.user_id = U.id
|
||||
WHERE U.partner_id IN %s AND U.active = 't'
|
||||
GROUP BY U.partner_id
|
||||
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
|
||||
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
|
||||
for partner in self:
|
||||
partner.im_status = res.get(partner.id, 'im_partner') # if not found, it is a partner, useful to avoid to refresh status in js
|
||||
_name = "res.partner"
|
||||
_inherit = ["res.partner", "bus.listener.mixin"]
|
||||
|
|
|
|||
|
|
@ -1,28 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.bus.models.bus_presence import AWAY_TIMER
|
||||
from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_name = "res.users"
|
||||
_inherit = ["res.users", "bus.listener.mixin"]
|
||||
|
||||
_inherit = "res.users"
|
||||
|
||||
im_status = fields.Char('IM Status', compute='_compute_im_status')
|
||||
|
||||
def _compute_im_status(self):
|
||||
""" Compute the im_status of the users """
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
user_id as id,
|
||||
CASE WHEN age(now() AT TIME ZONE 'UTC', last_poll) > interval %s THEN 'offline'
|
||||
WHEN age(now() AT TIME ZONE 'UTC', last_presence) > interval %s THEN 'away'
|
||||
ELSE 'online'
|
||||
END as status
|
||||
FROM bus_presence
|
||||
WHERE user_id IN %s
|
||||
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
|
||||
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
|
||||
for user in self:
|
||||
user.im_status = res.get(user.id, 'offline')
|
||||
def _bus_channel(self):
|
||||
return self.partner_id
|
||||
|
|
|
|||
11
odoo-bringout-oca-ocb-bus/bus/models/res_users_settings.py
Normal file
11
odoo-bringout-oca-ocb-bus/bus/models/res_users_settings.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_name = 'res.users.settings'
|
||||
_inherit = ["res.users.settings", "bus.listener.mixin"]
|
||||
|
||||
def _bus_channel(self):
|
||||
return self.user_id
|
||||
Loading…
Add table
Add a link
Reference in a new issue