Browse Source
[IMP] bus_presence_override: Add new features.
[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 hitpull/382/head
Brett Wood
7 years ago
committed by
Holger Brunn
19 changed files with 481 additions and 268 deletions
-
1bus_presence_override/__init__.py
-
4bus_presence_override/__manifest__.py
-
5bus_presence_override/controllers/__init__.py
-
21bus_presence_override/controllers/main.py
-
2bus_presence_override/models/__init__.py
-
88bus_presence_override/models/bus_presence.py
-
76bus_presence_override/models/res_partner.py
-
21bus_presence_override/models/res_users.py
-
106bus_presence_override/static/src/js/bus_presence_systray.js
-
59bus_presence_override/static/src/js/systray.js
-
21bus_presence_override/static/src/less/bus_presence_systray.less
-
47bus_presence_override/static/src/less/systray.less
-
25bus_presence_override/static/src/xml/bus_presence_systray.xml
-
2bus_presence_override/tests/__init__.py
-
28bus_presence_override/tests/bus_setup.py
-
120bus_presence_override/tests/test_bus_presence.py
-
91bus_presence_override/tests/test_res_partner.py
-
28bus_presence_override/tests/test_res_users.py
-
4bus_presence_override/views/assets.xml
@ -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 |
@ -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, |
|||
) |
@ -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 |
@ -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) |
@ -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); |
|||
|
|||
}); |
|||
|
@ -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); |
|||
|
|||
}); |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,28 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from odoo.tests.common import TransactionCase |
|||
|
|||
|
|||
class BusSetup(TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super(BusSetup, self).setUp() |
|||
self.u_admin = self.env.ref('base.user_root') |
|||
self.p_admin = self.u_admin.partner_id |
|||
|
|||
self.u_demo = self.env.ref('base.user_demo') |
|||
self.p_demo = self.u_demo.partner_id |
|||
|
|||
self.pres_admin = self._get_bus_presence(self.u_admin) |
|||
self.pres_demo = self._get_bus_presence(self.u_demo) |
|||
|
|||
# AWAY_TIMER = 55 seconds |
|||
# DISCONNECTION_TIMER = 1800 seconds (30 minutes) |
|||
|
|||
def _get_bus_presence(self, user): |
|||
pres = self.env['bus.presence'].search([('user_id', '=', user.id)]) |
|||
if not pres: |
|||
pres = self.env['bus.presence'].create({'user_id': user.id}) |
|||
return pres |
@ -0,0 +1,120 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from mock import patch |
|||
|
|||
from .bus_setup import BusSetup |
|||
from ..status_constants import ONLINE, AWAY, OFFLINE |
|||
|
|||
AWAY_TIMER = 'odoo.addons.bus_presence_override.models.' \ |
|||
'bus_presence.AWAY_TIMER' |
|||
|
|||
DISCONNECTION_TIMER = 'odoo.addons.bus_presence_override.models.' \ |
|||
'bus_presence.DISCONNECTION_TIMER' |
|||
|
|||
|
|||
class TestBusPresence(BusSetup): |
|||
|
|||
@patch(AWAY_TIMER, 10000000) |
|||
@patch(DISCONNECTION_TIMER, 10000000) |
|||
def test_compute_status_realtime_online(self): |
|||
""" It should be computed to online """ |
|||
self.assertEquals( |
|||
self.pres_admin.status_realtime, |
|||
ONLINE, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 10000000) |
|||
@patch(DISCONNECTION_TIMER, 0) |
|||
def test_compute_status_realtime_offline(self): |
|||
""" It should be computed to offline """ |
|||
self.assertEquals( |
|||
self.pres_admin.status_realtime, |
|||
OFFLINE, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 0) |
|||
@patch(DISCONNECTION_TIMER, 10000000) |
|||
def test_compute_status_realtime_away(self): |
|||
""" It should be computed to away """ |
|||
self.assertEquals( |
|||
self.pres_admin.status_realtime, |
|||
AWAY, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 0) |
|||
@patch(DISCONNECTION_TIMER, 0) |
|||
def test_compute_status_realtime_both(self): |
|||
""" It should be computed to offline even though away as well """ |
|||
self.assertEquals( |
|||
self.pres_admin.status_realtime, |
|||
OFFLINE, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 0) |
|||
@patch(DISCONNECTION_TIMER, 0) |
|||
def test_status_check_timers_offline(self): |
|||
""" It should be changed to offline from online """ |
|||
self.pres_admin.status = ONLINE |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
ONLINE, |
|||
) |
|||
self.pres_admin._status_check_disconnection_and_away_timers() |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
OFFLINE, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 0) |
|||
@patch(DISCONNECTION_TIMER, 10000000) |
|||
def test_status_check_timers_away(self): |
|||
""" It should be changed to away from online """ |
|||
self.pres_admin.status = ONLINE |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
ONLINE, |
|||
) |
|||
self.pres_admin._status_check_disconnection_and_away_timers() |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
AWAY, |
|||
) |
|||
|
|||
@patch(AWAY_TIMER, 0) |
|||
@patch(DISCONNECTION_TIMER, 10000000) |
|||
def test_status_check_timers_unchanged(self): |
|||
""" It should remain at offline even if status_realtime away """ |
|||
self.pres_admin.status = OFFLINE |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
OFFLINE, |
|||
) |
|||
self.pres_admin._status_check_disconnection_and_away_timers() |
|||
self.assertEquals( |
|||
self.pres_admin.status, |
|||
OFFLINE, |
|||
) |
|||
|
|||
def test_get_partners_im_statuses(self): |
|||
""" It should include demo and admin partner statuses """ |
|||
recs = self.env['bus.presence'].search([( |
|||
'partner_id', 'in', [self.p_admin.id, self.p_demo.id])] |
|||
) |
|||
statuses = recs._get_partners_statuses() |
|||
self.assertIn( |
|||
self.p_admin.id, |
|||
statuses, |
|||
) |
|||
|
|||
def test_get_users_im_statuses(self): |
|||
""" It should include demo and admin user statuses """ |
|||
recs = self.env['bus.presence'].search([( |
|||
'user_id', 'in', [self.u_admin.id, self.u_demo.id])] |
|||
) |
|||
statuses = recs._get_users_statuses() |
|||
self.assertIn( |
|||
self.u_admin.id, |
|||
statuses, |
|||
) |
@ -0,0 +1,28 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2017 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from mock import patch |
|||
|
|||
from .bus_setup import BusSetup |
|||
from ..status_constants import ONLINE |
|||
|
|||
|
|||
AWAY_TIMER = 'odoo.addons.bus_presence_override.models.' \ |
|||
'bus_presence.AWAY_TIMER' |
|||
|
|||
DISCONNECTION_TIMER = 'odoo.addons.bus_presence_override.models.' \ |
|||
'bus_presence.DISCONNECTION_TIMER' |
|||
|
|||
|
|||
class TestResUsers(BusSetup): |
|||
|
|||
@patch(AWAY_TIMER, 10000000) |
|||
@patch(DISCONNECTION_TIMER, 10000000) |
|||
def test_compute_im_status_online(self): |
|||
""" It should be computed to online """ |
|||
self.pres_admin.status = ONLINE |
|||
self.assertEquals( |
|||
self.u_admin.im_status, |
|||
ONLINE, |
|||
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue