Skip to content
Permalink
Browse files

[IMP] bus: notify user when assets have changed

Use case:
When the server is restarted, the python is updated,
but some users may have an ongoing session in a browser tab
This may lead to code being unsynchronized and ultimately to some
odd bugs

Purpose:
When we are in such a case, notify connected users that assets have changed
And propose them to reload the page

Known caveats:
- This is not a developer's feature
Since assets computing is ORM cached, they have limited
opportunities to rebuild. Namely, the feature won't trigger
each time the JS has changed, rather, it will
when JS has changed AND the cache has been reset somehow

- This not a portal/website feature either
Business clients won't be notified that the JS has changed

- While requests debug=assets do trigger a recomputing
of the *components* of bundles, they do not save a bundle
This means that the requests that sends the notification
cannot be debug=assets

Task 2034462
  • Loading branch information...
kebeclibre committed Nov 5, 2019
1 parent 92ba000 commit 3e4623aaa7e6d125c1a7d03a80352b6be881b1ed
@@ -23,6 +23,8 @@ def _poll(self, dbname, channels, last, options):
# update the user presence
if request.session.uid and 'bus_inactivity' in options:
request.env['bus.presence'].update(options.get('bus_inactivity'))
if request.session.uid:
channels.append((request.db, 'bundle_changed'))
request.cr.close()
request._cr = None
return dispatch.poll(dbname, channels, last, options)
@@ -3,4 +3,5 @@
from . import bus_presence
from . import res_users
from . import res_partner
from . import ir_autovacuum
from . import ir_autovacuum
from . import assetsbundle
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import logging

from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo import models

_logger = logging.getLogger(__name__)


class BusAssetsBundle(AssetsBundle):
TRACKED_BUNDLES = ['web.assets_common', 'web.assets_backend']

def save_attachment(self, type, content):
"""
Each time an attachment is saved
Send a bus notification
An attachment is saved when its hash has changed
@override
"""
saved = super().save_attachment(type, content)

if self.env and self.name in self.TRACKED_BUNDLES:
channel = (self.env.registry.db_name, 'bundle_changed')
message = (self.name, self.version)
self.env['bus.bus'].sendone(channel, message)
_logger.debug('Asset Changed: xml_id: %s -- version: %s' % message)

return saved

def to_node(self, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False):
"""
Mark bundle's dom nodes with identifiable data that contains XML id and version
At most once per asset type (css or js) and per bundle (xmlid)
Even in debug=assets
@override
"""
response = super().to_node(css, js, debug, async_load, defer_load, lazy_load)

if self.name in self.TRACKED_BUNDLES:
for node in response:
if node[0] == 'script' or node[0] == 'link':
node[1]['data-asset-xmlid'] = self.name
node[1]['data-asset-version'] = self.version
break
return response


class BusIrQWeb(models.AbstractModel):

_inherit = 'ir.qweb'

def get_asset_bundle(self, xmlid, files, env=None):
"""
Redirect to be able to select the assetBundle type
@override
"""
return BusAssetsBundle(xmlid, files, env)
@@ -0,0 +1,91 @@
odoo.define('bus.WebClient', function (require) {
"use strict";

var core = require('web.core');
var WebClient = require('web.WebClient');

var _t = core._t;

WebClient.include({
/**
* Detects the presence of assets in DOM's HEAD
*
* @override
*/
start: function () {
var self = this;
this.assetsChangedNotificationId = null;
this._assets = {};

return this._super.apply(this, arguments).then(function () {
var $assets = $('*[data-asset-xmlid]');
$assets.each(function () {
self._assets[$(this).data('asset-xmlid')] = $(this).data('asset-version');
});
});
},
/**
* Assign handler to bus notification
*
* @override
*/
show_application: function () {
this.call('bus_service', 'onNotification', this, this._onNotification);
return this._super.apply(this, arguments);
},
/**
* Displays one notification on user's screen when assets have changed
*
* @private
*/
_displayBundleChangedNotification: function () {
var self = this;
if (!this.assetsChangedNotificationId) {
this.assetsChangedNotificationId = this.call('notification', 'notify', {
title: _t('Update available'),
message: _t('Refresh your browser to take advantage of the latest update of Odoo in your browser'),
sticky: true,
onClose: function () {
self.assetsChangedNotificationId = null;
},
buttons: [{
text: _t('Refresh'),
primary: true,
click: function () {
window.location.reload(true);
}
}],
});
}
},

//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------

/**
* Reacts to bus's notification
*
* @private
* @param {Array} notifications: list of received notifications
*/
_onNotification: function (notifications) {
var seenXmlIds = [];
for (var i in notifications) {
var notif = notifications[i];
if (notif[0][1] === 'bundle_changed') {
var bundleXmlId = notif[1][0];
var bundleVersion = notif[1][1];
if (!seenXmlIds.includes(bundleXmlId)) {
seenXmlIds.push(bundleXmlId);
if (bundleXmlId in this._assets && bundleVersion !== this._assets[bundleXmlId]) {
this._displayBundleChangedNotification();
break;
}
}
}
}
}
});

});
@@ -0,0 +1 @@
from . import test_assetsbundle
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch

import odoo.tests


@odoo.tests.tagged('post_install', '-at_install')
class BusWebTests(odoo.tests.HttpCase):

def test_bundle_sends_bus(self):
db_name = self.env.registry.db_name

send_one_counter = 0
def _patched_sendone(channel, message):
nonlocal send_one_counter
send_one_counter += 1
self.assertEqual(channel, (db_name, 'bundle_changed'))
self.assertEqual(len(message), 2)
self.assertTrue(message[0] in ('web.assets_common', 'web.assets_backend'))
self.assertTrue(isinstance(message[1], str))

with patch('odoo.addons.bus.models.bus.ImBus.sendone', wraps=_patched_sendone, create=True):
self.browser_js('/web', "console.log('test successful')", "", login='admin', timeout=10)

# One sendone for each asset bundle and for each CSS / JS
self.assertEqual(send_one_counter, 4)
@@ -5,6 +5,7 @@
<script type="text/javascript" src="/bus/static/src/js/longpolling_bus.js"></script>
<script type="text/javascript" src="/bus/static/src/js/crosstab_bus.js"></script>
<script type="text/javascript" src="/bus/static/src/js/services/bus_service.js"></script>
<script type="text/javascript" src="/bus/static/src/js/web_client_bus.js"></script>
</xpath>
</template>

@@ -5,15 +5,15 @@

from odoo import models
from odoo.http import request
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.bus.models.assetsbundle import BusAssetsBundle
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.addons.website.models import ir_http
from odoo.tools import html_escape as escape

re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)")


class AssetsBundleMultiWebsite(AssetsBundle):
class AssetsBundleMultiWebsite(BusAssetsBundle):
def _get_asset_url_values(self, id, unique, extra, name, sep, type):
website_id = self.env.context.get('website_id')
website_id_path = website_id and ('%s/' % website_id) or ''

0 comments on commit 3e4623a

Please sign in to comment.
You can’t perform that action at this time.