summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/abstract_model.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/abstract_model.js')
-rw-r--r--addons/web/static/src/js/views/abstract_model.js226
1 files changed, 224 insertions, 2 deletions
diff --git a/addons/web/static/src/js/views/abstract_model.js b/addons/web/static/src/js/views/abstract_model.js
index d18367b1756..db2cce99935 100644
--- a/addons/web/static/src/js/views/abstract_model.js
+++ b/addons/web/static/src/js/views/abstract_model.js
@@ -12,25 +12,199 @@ odoo.define('web.AbstractModel', function (require) {
* Note that the model is not a widget, it does not need to be rendered or
* appended to the dom. However, it inherits from the EventDispatcherMixin,
* in order to be able to notify its parent by bubbling events up.
+ *
+ * The model is able to generate sample (fake) data when there is no actual data
+ * in database. This feature can be activated by instantiating the model with
+ * param "useSampleModel" set to true. In this case, the model instantiates a
+ * duplicated version of itself, parametrized to call a SampleServer (JS)
+ * instead of doing RPCs. Here is how it works: the main model first load the
+ * data normally (from database), and then checks whether the result is empty or
+ * not. If it is, it asks the sample model to load with the exact same params,
+ * and it thus enters in "sample" mode. The model keeps doing this at reload,
+ * but only if the (re)load params haven't changed: as soon as a param changes,
+ * the "sample" mode is left, and it never enters it again in the future (in the
+ * lifetime of the model instance). To access those sample data from the outside,
+ * 'get' must be called with the the option "withSampleData" set to true. In
+ * this case, if the main model is in "sample" mode, it redirects the call to the
+ * sample model.
*/
var fieldUtils = require('web.field_utils');
var mvc = require('web.mvc');
+const SampleServer = require('web.SampleServer');
+
var AbstractModel = mvc.Model.extend({
+ /**
+ * @param {Widget} parent
+ * @param {Object} [params={}]
+ * @param {Object} [params.fields]
+ * @param {string} [params.modelName]
+ * @param {boolean} [params.isSampleModel=false] if true, will fetch data
+ * from a SampleServer instead of doing RPCs
+ * @param {boolean} [params.useSampleModel=false] if true, will use a sample
+ * model to generate sample data when there is no "real" data in database
+ * @param {AbstractModel} [params.SampleModel] the AbstractModel class
+ * to instantiate as sample model. This model won't do any rpc, but will
+ * rather call a SampleServer that will generate sample data. This param
+ * must be set when params.useSampleModel is true.
+ */
+ init(parent, params = {}) {
+ this._super(...arguments);
+ this.useSampleModel = params.useSampleModel || false;
+ if (params.isSampleModel) {
+ this.isSampleModel = true;
+ this.sampleServer = new SampleServer(params.modelName, params.fields);
+ } else if (this.useSampleModel) {
+ const sampleModelParams = Object.assign({}, params, {
+ isSampleModel: true,
+ SampleModel: null,
+ useSampleModel: false,
+ });
+ this.sampleModel = new params.SampleModel(this, sampleModelParams);
+ this._isInSampleMode = false;
+ }
+ },
+
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
+ * Override to call get on the sampleModel when we are in sample mode, and
+ * option 'withSampleData' is set to true.
+ *
+ * @override
+ * @param {any} _
+ * @param {Object} [options]
+ * @param {boolean} [options.withSampleData=false]
+ */
+ get(_, options) {
+ let state;
+ if (options && options.withSampleData && this._isInSampleMode) {
+ state = this.sampleModel.__get(...arguments);
+ } else {
+ state = this.__get(...arguments);
+ }
+ return state;
+ },
+ /**
+ * Under some conditions, the model is designed to generate sample data if
+ * there is no real data in database. This function returns a boolean which
+ * indicates the mode of the model: if true, we are in "sample" mode.
+ *
+ * @returns {boolean}
+ */
+ isInSampleMode() {
+ return !!this._isInSampleMode;
+ },
+ /**
+ * Disables the sample data (forever) on this model instance.
+ */
+ leaveSampleMode() {
+ if (this.useSampleModel) {
+ this.useSampleModel = false;
+ this._isInSampleMode = false;
+ this.sampleModel.destroy();
+ }
+ },
+ /**
+ * Override to check if we need to call the sample model (and if so, to do
+ * it) after loading the data, in the case where there is no real data to
+ * display.
+ *
+ * @override
+ */
+ async load(params) {
+ this.loadParams = params;
+ const handle = await this.__load(...arguments);
+ await this._callSampleModel('__load', handle, ...arguments);
+ return handle;
+ },
+ /**
* When something changes, the data may need to be refetched. This is the
* job for this method: reloading (only if necessary) all the data and
* making sure that they are ready to be redisplayed.
+ * Sometimes, we reload the data with the "same" params as the initial load
+ * params (see '_haveParamsChanged'). When we do, if we were in "sample" mode,
+ * we call again the sample server after the reload if there is still no data
+ * to display. When the parameters change, we automatically leave "sample"
+ * mode.
+ *
+ * @param {any} _
+ * @param {Object} [params]
+ * @returns {Promise}
+ */
+ async reload(_, params) {
+ const handle = await this.__reload(...arguments);
+ if (this._isInSampleMode) {
+ if (!this._haveParamsChanged(params)) {
+ await this._callSampleModel('__reload', handle, ...arguments);
+ } else {
+ this.leaveSampleMode();
+ }
+ }
+ return handle;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} method
+ * @param {any} handle
+ * @param {...any} args
+ * @returns {Promise}
+ */
+ async _callSampleModel(method, handle, ...args) {
+ if (this.useSampleModel && this._isEmpty(handle)) {
+ try {
+ if (method === '__load') {
+ await this.sampleModel.__load(...args);
+ } else if (method === '__reload') {
+ await this.sampleModel.__reload(...args);
+ }
+ this._isInSampleMode = true;
+ } catch (error) {
+ if (error instanceof SampleServer.UnimplementedRouteError) {
+ this.leaveSampleMode();
+ } else {
+ throw error;
+ }
+ }
+ } else {
+ this.leaveSampleMode();
+ }
+ },
+ /**
+ * @private
+ * @returns {Object}
+ */
+ __get() {
+ return {};
+ },
+ /**
+ * This function can be overriden to determine if the result of a load or
+ * a reload is empty. In the affirmative, we will try to generate sample
+ * data to prevent from having an empty state to display.
+ *
+ * @private
+ * @params {any} handle, the value returned by a load or a reload
+ * @returns {boolean}
+ */
+ _isEmpty(/* handle */) {
+ return false;
+ },
+ /**
+ * To override to do the initial load of the data (this function is supposed
+ * to be called only once).
*
- * @param {Object} params
+ * @private
* @returns {Promise}
*/
- reload: function (params) {
+ async __load() {
return Promise.resolve();
},
/**
@@ -57,6 +231,54 @@ var AbstractModel = mvc.Model.extend({
}
return value;
},
+ /**
+ * To override to reload data (this function may be called several times,
+ * after the initial load has been done).
+ *
+ * @private
+ * @returns {Promise}
+ */
+ async __reload() {
+ return Promise.resolve();
+ },
+ /**
+ * Determines whether or not the given params (reload params) differ from
+ * the initial ones (this.loadParams). This is used to leave "sample" mode
+ * as soon as a parameter (e.g. domain) changes.
+ *
+ * @private
+ * @param {Object} [params={}]
+ * @param {Object} [params.context]
+ * @param {Array[]} [params.domain]
+ * @param {Object} [params.timeRanges]
+ * @param {string[]} [params.groupBy]
+ * @returns {boolean}
+ */
+ _haveParamsChanged(params = {}) {
+ for (const key of ['context', 'domain', 'timeRanges']) {
+ if (key in params) {
+ const diff = JSON.stringify(params[key]) !== JSON.stringify(this.loadParams[key]);
+ if (diff) {
+ return true;
+ }
+ }
+ }
+ if (this.useSampleModel && 'groupBy' in params) {
+ return JSON.stringify(params.groupBy) !== JSON.stringify(this.loadParams.groupedBy);
+ }
+ },
+ /**
+ * Override to redirect all rpcs to the SampleServer if this.isSampleModel
+ * is true.
+ *
+ * @override
+ */
+ async _rpc() {
+ if (this.isSampleModel) {
+ return this.sampleServer.mockRpc(...arguments);
+ }
+ return this._super(...arguments);
+ },
});
return AbstractModel;