diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index bf4f3a616f3..f4f323b01ba 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) {
* reading localstorage, ...) has to go through the controller.
*/
+var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
@@ -54,7 +55,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this.activeActions = params.activeActions;
this.controllerID = params.controllerID;
this.initialState = params.initialState;
-
+ this.bannerRoute = params.bannerRoute;
// use a DropPrevious to correctly handle concurrent updates
this.dp = new concurrency.DropPrevious();
// those arguments are temporary, they won't be necessary as soon as the
@@ -280,6 +281,54 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
state: state || {},
});
},
+ /**
+ * Renders the html provided by the route specified by the
+ * bannerRoute attribute on the controller (banner_route in the template).
+ * Renders it before the view output and add a css class 'o_has_banner' to it.
+ * There can be only one banner displayed at a time.
+ *
+ * If the banner contains stylesheet links, they are moved to
+ * (and will only be fetched once).
+ *
+ * Route example:
+ * @http.route('/module/hello', auth='user', type='json')
+ * def hello(self):
+ * return {'html': 'hello, world '}
+ *
+ * @private
+ * @returns {Deferred}
+ */
+ _renderBanner: function () {
+ if (this.bannerRoute !== undefined) {
+ var self = this;
+ return this.dp
+ .add(this._rpc({route: this.bannerRoute}))
+ .then(function (response) {
+ if (!response.html) {
+ self.$el.removeClass('o_has_banner');
+ return $.when();
+ }
+ self.$el.addClass('o_has_banner');
+ var $banner = $(response.html);
+ // we should only display one banner at a time
+ if (self._$banner && self._$banner.remove) {
+ self._$banner.remove();
+ }
+ // Stylesheets are moved to and we wait for them to be loaded
+ // to prevent displaying unstyled content.
+ var defs = [];
+ $('link[rel="stylesheet"]', $banner).each(function (i, link) {
+ defs.push(ajax.loadCSS(link.href));
+ link.remove();
+ });
+ return $.when.apply($, defs).then(function () {
+ $banner.prependTo(self.$el);
+ self._$banner = $banner;
+ });
+ });
+ }
+ return $.when();
+ },
/**
* Renders the control elements (buttons, pager and sidebar) of the current
* view.
@@ -370,7 +419,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
});
this._pushState();
- return $.when();
+ return this._renderBanner();
},
//--------------------------------------------------------------------------
@@ -405,12 +454,39 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
* When a user clicks on an link with type="action", we need to actually
* do the action. This kind of links is used a lot in no-content helpers.
*
+ * The may have
+ * - a data-method and data-model attribute, in that case the corresponding
+ * rpc will be called. If that rpc returns an action it will be executed.
+ * - a data-reload-on-close attribute, in that case the view will be
+ * reloaded after the dialog has been closed.
+ *
* @private
* @param {OdooEvent} event
*/
_onActionClicked: function (event) {
- event.preventDefault();
- this.do_action(event.currentTarget.name);
+ var $target = $(event.currentTarget);
+ var self = this;
+ var model = $target.data('model');
+ var method = $target.data('method');
+
+ if (method !== undefined && model !== undefined) {
+ var options = {};
+ if ($target.data('reload-on-close')) {
+ options.on_close = function () {
+ self.trigger_up('reload');
+ };
+ }
+ this.dp.add(this._rpc({
+ model: model,
+ method: method,
+ })).then(function (action) {
+ if (action !== undefined) {
+ self.do_action(action, options);
+ }
+ });
+ } else {
+ this.do_action($target.attr('name'));
+ }
},
/**
* Intercepts the 'switch_view' event to add the controllerID into the data,
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index fe95ada0a7f..7b1d21e6ea5 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -121,6 +121,7 @@ var AbstractView = Class.extend({
groupable: this.groupable,
isEmbedded: this.isEmbedded,
controllerID: params.controllerID,
+ bannerRoute: this.arch.attrs.banner_route,
};
// AAB: these params won't be necessary as soon as the ControlPanel will
// be instantiated by the View
diff --git a/addons/web/static/src/scss/banner.scss b/addons/web/static/src/scss/banner.scss
new file mode 100644
index 00000000000..b119d9ab273
--- /dev/null
+++ b/addons/web/static/src/scss/banner.scss
@@ -0,0 +1,5 @@
+.o_has_banner {
+ .o_view_nocontent {
+ top: 30%;
+ }
+}
diff --git a/addons/web/static/tests/views/abstract_view_banner_tests.js b/addons/web/static/tests/views/abstract_view_banner_tests.js
new file mode 100644
index 00000000000..21532ed60d9
--- /dev/null
+++ b/addons/web/static/tests/views/abstract_view_banner_tests.js
@@ -0,0 +1,82 @@
+odoo.define('web.abstract_view_banner_tests', function (require) {
+"use strict";
+
+var AbstractRenderer = require('web.AbstractRenderer');
+var AbstractView = require('web.AbstractView');
+
+var testUtils = require('web.test_utils');
+var createAsyncView = testUtils.createAsyncView;
+
+var TestRenderer = AbstractRenderer.extend({
+ _renderView: function () {
+ this.$el.addClass('test_content');
+ return this._super();
+ },
+});
+
+var TestView = AbstractView.extend({
+ type: 'test',
+ config: _.extend({}, AbstractView.prototype.config, {
+ Renderer: TestRenderer
+ }),
+});
+
+var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
+
+QUnit.module('Views', {
+ beforeEach: function () {
+ this.data = {
+ test_model: {
+ fields: {},
+ records: [],
+ },
+ };
+ },
+ afterEach: function () {
+ $('head link[href$="' + test_css_url + '"]').remove();
+ }
+ }, function () {
+ QUnit.module('BasicRenderer');
+
+ QUnit.test("The banner should be fetched from the route", function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ var banner_html =
+ '';
+
+ createAsyncView({
+ View: TestView,
+ model: 'test_model',
+ data: this.data,
+ arch: ' ',
+ mockRPC: function (route, args) {
+ if (route === '/module/hello_banner') {
+ assert.step(route);
+ return $.when({html: banner_html});
+ }
+ return this._super(route, args);
+ },
+ }).then(function (view) {
+ var $banner = view.$('.hello_banner');
+ assert.strictEqual($banner.length, 1,
+ "The view should contain the response from the controller.");
+ assert.verifySteps(['/module/hello_banner'], "The banner should be fetched.");
+
+ var $head_link = $('head link[href$="' + test_css_url + '"]');
+ assert.strictEqual($head_link.length, 1,
+ "The stylesheet should have been added to head.");
+
+ var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
+ assert.strictEqual($banner_link.length, 0,
+ "The stylesheet should have been removed from the banner.");
+ view.destroy();
+ done();
+ });
+ });
+ }
+);
+});
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index d57bad4e2e9..1ab7d27e26a 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -52,6 +52,7 @@
+
@@ -528,6 +529,7 @@
+
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 08848604926..8a733dedee4 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -44,6 +44,45 @@ otherwise.
``application``
website feature defining togglable views. By default, views are always
applied
+``banner_route``
+ a route address to be fetched and prepended to the view.
+
+ If this attribute is set, the
+ :ref:`controller route url` will be fetched and
+ displayed above the view. The json response from the controller should
+ contain an "html" key.
+
+ If the html contains a stylesheet tag, it will be
+ removed and appended to .
+
+ To interact with the backend you can use tags. Please take
+ a look at the documentation of the _onActionClicked method of
+ AbstractController (*addons/web/static/src/js/views/abstract_controller.js*)
+ for more details.
+
+ Only views extending AbstractView and AbstractController can use this
+ attribute, like :ref:`reference/views/form`, :ref:`reference/views/kanban`,
+ :ref:`reference/views/list`, ...
+
+ Example:
+
+ .. code-block:: xml
+
+
+
+ .. code-block:: python
+
+ class MyController(odoo.http.Controller):
+ @http.route('/module_name/hello', auth='user', type='json')
+ def hello(self):
+ return {
+ 'html': """
+
+
+
hello, world
+ """
+ }
.. _reference/views/inheritance:
diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng
index 068f88d909a..ff758cb7ed3 100644
--- a/odoo/addons/base/rng/tree_view.rng
+++ b/odoo/addons/base/rng/tree_view.rng
@@ -30,6 +30,7 @@
+