From 2a94a29f5c378132ff767745bba5f5af9f4e5caa Mon Sep 17 00:00:00 2001 From: Brett Wood Date: Wed, 6 Dec 2017 10:50:58 -0800 Subject: [PATCH] [IMP] bus_presence_override: Add new features. * Add caching so status change in 1 tab affects other open tabs. * Add automatic polling to update status if away or disconnection timers hit --- bus_presence_override/__init__.py | 1 + bus_presence_override/__manifest__.py | 4 +- bus_presence_override/controllers/__init__.py | 5 + bus_presence_override/controllers/main.py | 21 +++ bus_presence_override/models/__init__.py | 2 + bus_presence_override/models/bus_presence.py | 88 +++++++++++++ bus_presence_override/models/res_partner.py | 76 +---------- bus_presence_override/models/res_users.py | 21 +++ .../static/src/js/bus_presence_systray.js | 106 ++++++++++++++++ .../static/src/js/systray.js | 59 --------- .../static/src/less/bus_presence_systray.less | 21 +++ .../static/src/less/systray.less | 47 ------- .../{systray.xml => bus_presence_systray.xml} | 25 ++-- bus_presence_override/tests/__init__.py | 2 + bus_presence_override/tests/bus_setup.py | 28 ++++ .../tests/test_bus_presence.py | 120 ++++++++++++++++++ .../tests/test_res_partner.py | 91 ++----------- bus_presence_override/tests/test_res_users.py | 28 ++++ bus_presence_override/views/assets.xml | 4 +- 19 files changed, 481 insertions(+), 268 deletions(-) create mode 100644 bus_presence_override/controllers/__init__.py create mode 100644 bus_presence_override/controllers/main.py create mode 100644 bus_presence_override/models/bus_presence.py create mode 100644 bus_presence_override/models/res_users.py create mode 100644 bus_presence_override/static/src/js/bus_presence_systray.js delete mode 100644 bus_presence_override/static/src/js/systray.js create mode 100644 bus_presence_override/static/src/less/bus_presence_systray.less delete mode 100644 bus_presence_override/static/src/less/systray.less rename bus_presence_override/static/src/xml/{systray.xml => bus_presence_systray.xml} (54%) create mode 100644 bus_presence_override/tests/bus_setup.py create mode 100644 bus_presence_override/tests/test_bus_presence.py create mode 100644 bus_presence_override/tests/test_res_users.py diff --git a/bus_presence_override/__init__.py b/bus_presence_override/__init__.py index fd02263c..5fb1c17a 100644 --- a/bus_presence_override/__init__.py +++ b/bus_presence_override/__init__.py @@ -2,4 +2,5 @@ # Copyright 2017 LasLabs Inc. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import controllers from . import models diff --git a/bus_presence_override/__manifest__.py b/bus_presence_override/__manifest__.py index 463de09f..aef4d0b1 100644 --- a/bus_presence_override/__manifest__.py +++ b/bus_presence_override/__manifest__.py @@ -9,7 +9,7 @@ "category": "Social", "website": "https://github.com/OCA/social", "author": "LasLabs, Odoo Community Association (OCA)", - "license": "AGPL-3", + "license": "LGPL-3", "application": False, "installable": True, "depends": [ @@ -19,6 +19,6 @@ "views/assets.xml", ], "qweb": [ - "static/src/xml/systray.xml", + "static/src/xml/bus_presence_systray.xml", ], } diff --git a/bus_presence_override/controllers/__init__.py b/bus_presence_override/controllers/__init__.py new file mode 100644 index 00000000..1333eba5 --- /dev/null +++ b/bus_presence_override/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from . import main diff --git a/bus_presence_override/controllers/main.py b/bus_presence_override/controllers/main.py new file mode 100644 index 00000000..194ea5db --- /dev/null +++ b/bus_presence_override/controllers/main.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo.http import request +from odoo.addons.bus.controllers.main import BusController + + +class BusController(BusController): + + def _poll(self, dbname, channels, last, options): + if request.uid: + partner = request.env.user.partner_id + if 'bus_presence_partner_ids' in options: + options['bus_presence_partner_ids'].append(partner.id) + else: + options['bus_presence_partner_ids'] = [partner.id] + + return super(BusController, self)._poll( + dbname, channels, last, options, + ) diff --git a/bus_presence_override/models/__init__.py b/bus_presence_override/models/__init__.py index 77c339cb..ef1c67f9 100644 --- a/bus_presence_override/models/__init__.py +++ b/bus_presence_override/models/__init__.py @@ -2,4 +2,6 @@ # Copyright 2017 LasLabs Inc. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +from . import bus_presence from . import res_partner +from . import res_users diff --git a/bus_presence_override/models/bus_presence.py b/bus_presence_override/models/bus_presence.py new file mode 100644 index 00000000..3806c0f2 --- /dev/null +++ b/bus_presence_override/models/bus_presence.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from datetime import datetime + +from odoo import api, fields, models +from odoo.addons.bus.models.bus_presence import AWAY_TIMER, DISCONNECTION_TIMER + +from ..status_constants import ONLINE, AWAY, OFFLINE + + +class BusPresence(models.Model): + + _inherit = 'bus.presence' + + status_realtime = fields.Selection( + selection=[ + (ONLINE, 'Online'), + (AWAY, 'Away'), + (OFFLINE, 'Offline') + ], + string='Realtime IM Status', + compute='_compute_status_realtime', + help='Status that is affected by disconnection ' + 'and away timers. Used to override the bus.presence ' + 'status field in _get_partners_statuses or ' + '_get_users_statuses if the timers have been reached. ' + 'If wanting to change the user status, write ' + 'directly to the status field.', + ) + partner_id = fields.Many2one( + string='Partner', + related='user_id.partner_id', + comodel_name='res.partner', + ) + + @api.multi + def _get_partners_statuses(self): + self._status_check_disconnection_and_away_timers() + return {rec.partner_id.id: rec.status for rec in self} + + @api.multi + def _get_users_statuses(self): + self._status_check_disconnection_and_away_timers() + return {rec.user_id.id: rec.status for rec in self} + + @api.multi + def _status_check_disconnection_and_away_timers(self): + """ Overrides user-defined status if timers reached """ + for record in self: + + status_realtime = record.status_realtime + status_stored = record.status + + conditions = ( + status_realtime == OFFLINE, + status_realtime == AWAY and status_stored == ONLINE, + ) + + if any(conditions): + record.status = status_realtime + + @api.multi + def _compute_status_realtime(self): + + now_dt = datetime.now() + + for record in self: + + last_poll = fields.Datetime.from_string( + record.last_poll + ) + last_presence = fields.Datetime.from_string( + record.last_presence + ) + + last_poll_s = (now_dt - last_poll).total_seconds() + last_presence_s = (now_dt - last_presence).total_seconds() + + if last_poll_s > DISCONNECTION_TIMER: + record.status_realtime = OFFLINE + + elif last_presence_s > AWAY_TIMER: + record.status_realtime = AWAY + + else: + record.status_realtime = ONLINE diff --git a/bus_presence_override/models/res_partner.py b/bus_presence_override/models/res_partner.py index 732912e3..c5ef1611 100644 --- a/bus_presence_override/models/res_partner.py +++ b/bus_presence_override/models/res_partner.py @@ -2,82 +2,20 @@ # Copyright 2017 LasLabs Inc. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). -from odoo import api, fields, models +from odoo import api, models -from odoo.addons.bus.models.bus_presence import AWAY_TIMER -from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER - -from ..status_constants import ONLINE, AWAY, OFFLINE +from ..status_constants import OFFLINE class ResPartner(models.Model): _inherit = 'res.partner' - im_status_custom = fields.Selection( - selection=[ - (ONLINE, 'Online'), - (AWAY, 'Away'), - (OFFLINE, 'Offline'), - ], - string='Status', - default=ONLINE, - ) - - @api.model - def _get_partners_presence(self): - """ This method gets any relevant partners' im status. - - Example: - - .. code-block:: python - - res = self.env['res.partner']._get_partners_presence() - im_status = res.get(partner.id, OFFLINE) - - Returns: - dict: Structured like so: {partner_id: im_status} - - """ - self.env.cr.execute( - """ - SELECT - U.partner_id as id, - CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) - > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) - > interval %s THEN 'away' - ELSE 'online' - END as status - FROM bus_presence B - JOIN res_users U ON B.user_id = U.id - WHERE U.partner_id IN %s AND U.active = 't' - """, - ("%s seconds" % DISCONNECTION_TIMER, - "%s seconds" % AWAY_TIMER, tuple(self.ids)) - ) - all_status = self.env.cr.dictfetchall() - all_status_dict = dict( - ((status['id'], status['status']) for status in all_status) - ) - return all_status_dict - @api.multi def _compute_im_status(self): - presence = self._get_partners_presence() - + bus_recs = self.env['bus.presence'].search([ + ('partner_id', 'in', self.ids), + ]) + statuses = bus_recs._get_partners_statuses() for record in self: - - record.im_status = presence.get(record.id, OFFLINE) - - computed = record.im_status - custom = record.im_status_custom - - if computed == OFFLINE: - record.im_status_custom = computed - - elif custom in (AWAY, OFFLINE): - record.im_status = custom - - elif computed == AWAY and custom == ONLINE: - record.im_status_custom = computed + record.im_status = statuses.get(record.id, OFFLINE) diff --git a/bus_presence_override/models/res_users.py b/bus_presence_override/models/res_users.py new file mode 100644 index 00000000..1e1209f6 --- /dev/null +++ b/bus_presence_override/models/res_users.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, models + +from ..status_constants import OFFLINE + + +class ResUsers(models.Model): + + _inherit = 'res.users' + + @api.multi + def _compute_im_status(self): + bus_recs = self.env['bus.presence'].search([ + ('user_id', 'in', self.ids), + ]) + statuses = bus_recs._get_users_statuses() + for record in self: + record.im_status = statuses.get(record.id, OFFLINE) diff --git a/bus_presence_override/static/src/js/bus_presence_systray.js b/bus_presence_override/static/src/js/bus_presence_systray.js new file mode 100644 index 00000000..874a531c --- /dev/null +++ b/bus_presence_override/static/src/js/bus_presence_systray.js @@ -0,0 +1,106 @@ +/* Copyright 2017 LasLabs Inc. + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ + +odoo.define('bus_presence_systray', function (require) { + "use strict"; + + var Bus = require('bus.bus').bus; + var DataModel = require('web.DataModel'); + var Session = require('web.session'); + var SystrayMenu = require('web.SystrayMenu'); + var Widget = require('web.Widget'); + var Qweb = require('web.core').qweb; + var LocalStorage = require('web.local_storage'); + + function on(type, listener) { + if (window.addEventListener) { + window.addEventListener(type, listener); + } else { + // IE8 + window.attachEvent('on' + type, listener); + } + } + + var BusPresenceSystray = Widget.extend({ + template: 'bus_presence_systray', + events: { + 'click .o-user-status-select': 'onClickUserStatusSelect', + }, + init: function() { + this._super.apply(this, arguments); + this.resPartnerMod = new DataModel('res.partner'); + this.busPresenceMod = new DataModel('bus.presence'); + Bus.on('notification', this, _.throttle(this.notificationsUpdateCurrentUserStatus.bind(this), 100, {leading: false})); + on('storage', this.onStorage.bind(this)); + }, + start: function () { + this.startDetermineUserStatus(); + Bus.start_polling(); + return this._super(); + }, + startDetermineUserStatus: function () { + if (Bus.is_master === true) { + this.writeBusPresenceStatus('online'); + this.updateUserStatusIcon('online'); + LocalStorage.setItem('user.partner_im_status', 'online'); + } else { + var statusVal = LocalStorage.getItem('user.partner_im_status'); + this.updateUserStatusIcon(statusVal); + } + }, + onStorage: function (event) { + if (event.key === 'user.partner_im_status') { + this.updateUserStatusIcon(event.newValue); + } + }, + notificationsUpdateCurrentUserStatus: function (notifications) { + _.each(notifications, $.proxy( + function (notification) { + var model = notification[0][1]; + var partnerId = notification[1].id; + if (model === 'bus.presence' && partnerId === Session.partner_id) { + var status = notification[1].im_status; + this.updateUserStatusIcon(status); + LocalStorage.setItem('user.partner_im_status', status); + } + }, this) + ); + }, + queryUpdateCurrentUserStatus: function () { + this.resPartnerMod.query(['im_status']) + .filter([['id', '=', Session.partner_id]]) + .first() + .then($.proxy( + function (result) { + this.updateUserStatusIcon(result.im_status); + LocalStorage.setItem('user.partner_im_status', status); + }, this) + ); + }, + updateUserStatusIcon: function (status) { + var options = {'status': status}; + var $icon = this.$('.o-user-systray-status'); + $icon.empty().append($(Qweb.render('mail.chat.UserStatus', options))); + }, + onClickUserStatusSelect: function (event) { + var status = $(event.currentTarget).attr('name'); + this.updateUserStatusIcon(status); + this.writeBusPresenceStatus(status); + LocalStorage.setItem('user.partner_im_status', status); + }, + writeBusPresenceStatus: function (status) { + this.busPresenceMod.query(['id']) + .filter([['partner_id', '=', Session.partner_id]]) + .first() + .then($.proxy( + function (result) { + this.busPresenceMod.call('write', [[result.id], {'status': status}]); + }, this) + ); + }, + }); + + SystrayMenu.Items.push(BusPresenceSystray); + +}); + diff --git a/bus_presence_override/static/src/js/systray.js b/bus_presence_override/static/src/js/systray.js deleted file mode 100644 index 43599cc0..00000000 --- a/bus_presence_override/static/src/js/systray.js +++ /dev/null @@ -1,59 +0,0 @@ -/* Copyright 2017 LasLabs Inc. - License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ - -odoo.define('bus_presence_override.systray', function (require) { - "use strict"; - - var DataModel = require('web.DataModel'); - var session = require('web.session'); - var SystrayMenu = require('web.SystrayMenu'); - var Widget = require('web.Widget'); - - var Systray = Widget.extend({ - template:'systray', - events: { - 'click .o_user_presence_status': 'on_click_user_presence_status', - }, - init: function() { - this._super.apply(this, arguments); - this.Partners = new DataModel('res.partner'); - this.status_icons = { - 'online': 'fa fa-circle o_user_online', - 'away': 'fa fa-circle o_user_idle', - 'offline': 'fa fa-circle-o', - } - }, - start: function () { - this._update_im_status_custom(status='online'); - return this._super() - }, - on_click_user_presence_status: function (event) { - var status = $(event.target).attr('name'); - this._update_im_status_custom(status); - }, - _get_im_status: function () { - var self = this; - this.Partners.query(['im_status']) - .filter([['id', '=', session.partner_id]]) - .first() - .then(function (result) { - self._update_systray_status_icon(result['im_status']); - }); - }, - _update_systray_status_icon: function (status) { - $('#userStatus i').removeClass() - .addClass('o_mail_user_status ' + this.status_icons[status]); - }, - _update_im_status_custom: function (status) { - var self = this; - this.Partners.call('write', [[session.partner_id], {'im_status_custom': status}]) - .then(function () { - self._update_systray_status_icon(status); - }); - }, - - }); - - SystrayMenu.Items.push(Systray); - -}); diff --git a/bus_presence_override/static/src/less/bus_presence_systray.less b/bus_presence_override/static/src/less/bus_presence_systray.less new file mode 100644 index 00000000..32d80e09 --- /dev/null +++ b/bus_presence_override/static/src/less/bus_presence_systray.less @@ -0,0 +1,21 @@ +/* Copyright 2017 LasLabs Inc. + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ + +.o-user-systray-status { + i { + margin-top: 2px; + &:hover { + color: @gray-lighter; + } + } +} +.o-user-presence-dropdown { + min-width: 85px; + .o-user-status-select { + margin-top: 2px; + padding: 4px 8px; + width: 100%; + display: block; + font-size: 13px; + } +} diff --git a/bus_presence_override/static/src/less/systray.less b/bus_presence_override/static/src/less/systray.less deleted file mode 100644 index 67e15406..00000000 --- a/bus_presence_override/static/src/less/systray.less +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright 2017 LasLabs Inc. - License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ - -#userStatus { - &:hover i { - color: #d3d3d3 !important; - } - - i { - padding-top: 2px; - } -} - -.o_user_presence_dropdown { - min-width: 85px; - - .o_user_presence_status { - margin-top: 2px; - padding: 4px 8px; - width: 100%; - display: block; - font-size: 13px; - } - - &:hover { - cursor: pointer; - } - -} - -@media (max-width: @screen-xs-max) { - - .o_user_presence_status { - color: #9d9d9d; - &:hover { - color: #ffffff; - } - } - } - -@media (min-width: @screen-sm-min) { - - .o_user_presence_status:hover { - background-color: #e0e0e0; - } - -} diff --git a/bus_presence_override/static/src/xml/systray.xml b/bus_presence_override/static/src/xml/bus_presence_systray.xml similarity index 54% rename from bus_presence_override/static/src/xml/systray.xml rename to bus_presence_override/static/src/xml/bus_presence_systray.xml index 20383a6e..35eac86e 100644 --- a/bus_presence_override/static/src/xml/systray.xml +++ b/bus_presence_override/static/src/xml/bus_presence_systray.xml @@ -4,29 +4,32 @@ - +
  • - -