File: bus_presence.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (119 lines) | stat: -rw-r--r-- 4,973 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# -*- coding: utf-8 -*-
from datetime import timedelta

from odoo import api, fields, models
from odoo import tools
from odoo.service.model import PG_CONCURRENCY_EXCEPTIONS_TO_RETRY

UPDATE_PRESENCE_DELAY = 60
DISCONNECTION_TIMER = UPDATE_PRESENCE_DELAY + 5
AWAY_TIMER = 1800  # 30 minutes
PRESENCE_OUTDATED_TIMER = 12 * 60 * 60  # 12 hours


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_presence' 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)

    def create(self, values):
        presences = super().create(values)
        presences._invalidate_im_status()
        presences._send_presence()
        return presences

    def write(self, values):
        status_by_user = {presence._get_identity_field_name(): presence.status for presence in self}
        result = super().write(values)
        updated = self.filtered(lambda p: status_by_user[p._get_identity_field_name()] != p.status)
        updated._invalidate_im_status()
        updated._send_presence()
        return result

    def unlink(self):
        self._send_presence("offline")
        return super().unlink()

    @api.model
    def update_presence(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_presence(inactivity_period=inactivity_period, identity_field=identity_field, identity_value=identity_value)
                # commit on success
                self.env.cr.commit()
        except PG_CONCURRENCY_EXCEPTIONS_TO_RETRY:
            # ignore concurrency error
            return self.env.cr.rollback()

    def _get_bus_target(self):
        self.ensure_one()
        return self.user_id.partner_id if self.user_id else None

    def _get_identity_field_name(self):
        self.ensure_one()
        return "user_id" if self.user_id else None

    def _get_identity_data(self):
        self.ensure_one()
        return {"partner_id": self.user_id.partner_id.id} if self.user_id else None

    @api.model
    def _update_presence(self, inactivity_period, identity_field, identity_value):
        presence = self.search([(identity_field, "=", identity_value)])
        values = {
            "last_poll": fields.Datetime.now(),
            "last_presence": fields.Datetime.now() - timedelta(milliseconds=inactivity_period),
            "status": "away" if inactivity_period > AWAY_TIMER * 1000 else "online",
        }
        if not presence:
            values[identity_field] = identity_value
            presence = self.create(values)
        else:
            presence.write(values)

    def _invalidate_im_status(self):
        self.user_id.invalidate_recordset(["im_status"])
        self.user_id.partner_id.invalidate_recordset(["im_status"])

    def _send_presence(self, im_status=None, bus_target=None):
        """Send notification related to bus presence update.

        :param im_status: 'online', 'away' or 'offline'
        """
        for presence in self:
            identity_data = presence._get_identity_data()
            target = presence._get_bus_target()
            target = bus_target or (target and (target, "presence"))
            if identity_data and target:
                self.env["bus.bus"]._sendone(
                    target,
                    "bus.bus/im_status_updated",
                    {"im_status": im_status or presence.status, **identity_data},
                )

    @api.autovacuum
    def _gc_bus_presence(self):
        self.search(
            [("last_poll", "<", fields.Datetime.now() - timedelta(seconds=PRESENCE_OUTDATED_TIMER))]
        ).unlink()