From 7937347d756c0b8bf58ca9c01de24f96d87ffd18 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 6 Feb 2018 08:21:56 +0100 Subject: [PATCH] [IMP] web: invalidate JS cache for some models Models concerned are 'ir.ui.view', 'ir.actions.act_window' and 'ir.filters'. The DataManager's cache is invalidated at each create, update and unlink operation. This cache stores actions (results of calls to /web/action/load route), views (results of load_views RPCs) and filters (fetched alonside views). This rev. thus spares the user from reloading the webclient after performing such operations on these models to see the changes in the interface, as he only has to replay the action to see the changes. --- .../static/src/js/views/basic/basic_model.js | 37 +++++- addons/web/static/tests/views/form_tests.js | 111 +++++++++++++++++- addons/web/static/tests/views/list_tests.js | 57 ++++++++- 3 files changed, 199 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 90854bc577e..b8970a2ec89 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -134,6 +134,14 @@ var x2ManyCommands = { }; var BasicModel = AbstractModel.extend({ + // list of models for which the DataManager's cache should be cleared on + // create, update and delete operations + noCacheModels: [ + 'ir.actions.act_window', + 'ir.filters', + 'ir.ui.view', + ], + /** * @override */ @@ -248,6 +256,8 @@ var BasicModel = AbstractModel.extend({ record.count--; } }); + // optionally clear the DataManager's cache + self._invalidateCache(records[0]); }); }, /** @@ -909,11 +919,14 @@ var BasicModel = AbstractModel.extend({ // Erase changes as they have been applied record._changes = {}; + // Optionally clear the DataManager's cache + self._invalidateCache(record); + self.unfreezeOrder(record.id); // Update the data directly or reload them if (shouldReload) { - self._fetchRecord(record).then(function (record) { + self._fetchRecord(record).then(function () { def.resolve(changedFields); }); } else { @@ -1023,8 +1036,11 @@ var BasicModel = AbstractModel.extend({ method: 'write', args: [resIDs, { active: value }], }) - .then(this.reload.bind(this, parentID)); - + .then(function () { + // optionally clear the DataManager's cache + self._invalidateCache(parent); + return self.reload(parentID); + }); }, /** * Toggle (open/close) a group in a grouped list, then fetches relevant @@ -2937,6 +2953,21 @@ var BasicModel = AbstractModel.extend({ } return context; }, + /** + * Invalidates the DataManager's cache if the main model (i.e. the model of + * its root parent) of the given dataPoint is a model in 'noCacheModels'. + * + * @private + * @param {Object} dataPoint + */ + _invalidateCache: function (dataPoint) { + while (dataPoint.parentID) { + dataPoint = this.localData[dataPoint.parentID]; + } + if (_.contains(this.noCacheModels, dataPoint.model)) { + core.bus.trigger('clear_cache'); + } + }, /** * Returns true if the field is protected against changes, looking for a * readonly modifier unless there is a force_save modifier (checking first diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 023ca593b19..6d8909ff2c3 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1,13 +1,13 @@ odoo.define('web.form_tests', function (require) { "use strict"; +var BasicModel = require('web.BasicModel'); var concurrency = require('web.concurrency'); var config = require('web.config'); var core = require('web.core'); var fieldRegistry = require('web.field_registry'); var FormView = require('web.FormView'); var pyeval = require('web.pyeval'); -var RainbowMan = require('web.RainbowMan'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); @@ -6338,5 +6338,114 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('edition in form view on a "noCache" model', function (assert) { + assert.expect(4); + + testUtils.patch(BasicModel, { + noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']), + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + }); + core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache')); + + form.$('.o_field_widget[name=display_name]').val('new value').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + + assert.verifySteps(['write', 'clear_cache']); + + form.destroy(); + testUtils.unpatch(BasicModel); + }); + + QUnit.test('creation in form view on a "noCache" model', function (assert) { + assert.expect(4); + + testUtils.patch(BasicModel, { + noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']), + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.step('create'); + } + return this._super.apply(this, arguments); + }, + }); + core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache')); + + form.$('.o_field_widget[name=display_name]').val('value').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + + assert.verifySteps(['create', 'clear_cache']); + + form.destroy(); + testUtils.unpatch(BasicModel); + }); + + QUnit.test('deletion in form view on a "noCache" model', function (assert) { + assert.expect(4); + + testUtils.patch(BasicModel, { + noCacheModels: BasicModel.prototype.noCacheModels.concat(['partner']), + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.method === 'unlink') { + assert.step('unlink'); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + hasSidebar: true, + }, + }); + core.bus.on('clear_cache', form, assert.step.bind(assert, 'clear_cache')); + + form.sidebar.$('a:contains(Delete)').click(); + $('.modal .modal-footer .btn-primary').click(); // confirm + + assert.verifySteps(['unlink', 'clear_cache']); + + form.destroy(); + testUtils.unpatch(BasicModel); + }); }); + }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index c84b8351990..cfe00bf9141 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -1,7 +1,9 @@ odoo.define('web.list_tests', function (require) { "use strict"; +var BasicModel = require('web.BasicModel'); var config = require('web.config'); +var core = require('web.core'); var basicFields = require('web.basic_fields'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); @@ -2351,7 +2353,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('navigation with tab on a one2many list with create="0"', function (assert) { assert.expect(4); @@ -3172,7 +3173,6 @@ QUnit.module('Views', { list.destroy(); }); - QUnit.test('basic support for widgets', function (assert) { assert.expect(1); @@ -3223,6 +3223,59 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('list view on a "noCache" model', function (assert) { + assert.expect(8); + + testUtils.patch(BasicModel, { + noCacheModels: BasicModel.prototype.noCacheModels.concat(['foo']), + }); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '', + mockRPC: function (route, args) { + if (_.contains(['create', 'unlink', 'write'], args.method)) { + assert.step(args.method); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + hasSidebar: true, + }, + }); + core.bus.on('clear_cache', list, assert.step.bind(assert, 'clear_cache')); + + // create a new record + list.$buttons.find('.o_list_button_add').click(); + list.$('.o_selected_row .o_field_widget').val('some value').trigger('input'); + list.$buttons.find('.o_list_button_save').click(); + + // edit an existing record + list.$('.o_data_cell:first').click(); + list.$('.o_selected_row .o_field_widget').val('new value').trigger('input'); + list.$buttons.find('.o_list_button_save').click(); + + // delete a record + list.$('.o_data_row:first .o_list_record_selector input').click(); + list.sidebar.$('a:contains(Delete)').click(); + $('.modal .modal-footer .btn-primary').click(); // confirm + + assert.verifySteps([ + 'create', + 'clear_cache', + 'write', + 'clear_cache', + 'unlink', + 'clear_cache', + ]); + + list.destroy(); + testUtils.unpatch(BasicModel); + }); }); });