odoo.define('web.AbstractController', function (require) { "use strict"; /** * The Controller class is the class coordinating the model and the renderer. * It is the C in MVC, and is what was formerly known in Odoo as a View. * * Its role is to listen to events bubbling up from the model/renderer, and call * the appropriate methods if necessary. It also render control panel buttons, * and react to changes in the search view. Basically, all interactions from * the renderer/model with the outside world (meaning server/reading in session/ * reading localstorage, ...) has to go through the controller. */ var AbstractAction = require('web.AbstractAction'); var ajax = require('web.ajax'); var concurrency = require('web.concurrency'); var config = require('web.config'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var QWeb = core.qweb; var AbstractController = AbstractAction.extend(ControlPanelMixin, { custom_events: { open_record: '_onOpenRecord', switch_view: '_onSwitchView', }, events: { 'click a[type="action"]': '_onActionClicked', }, /** * @constructor * @param {Widget} parent * @param {AbstractModel} model * @param {AbstractRenderer} renderer * @param {object} params * @param {string} params.modelName * @param {string} [params.controllerID] an id to ease the communication * with upstream components * @param {any} [params.handle] a handle that will be given to the model (some id) * @param {any} params.initialState the initialState * @param {boolean} params.isMultiRecord * @param {Object[]} params.actionViews * @param {string} params.viewType * @param {boolean} params.withControlPanel set to false to hide the * ControlPanel */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.model = model; this.renderer = renderer; this.modelName = params.modelName; this.handle = params.handle; 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 // ControlPanel will be handled by the View this.displayName = params.displayName; this.isMultiRecord = params.isMultiRecord; this.searchable = params.searchable; this.searchView = params.searchView; this.searchViewHidden = params.searchViewHidden; this.groupable = params.groupable; this.enableTimeRangeMenu = params.enableTimeRangeMenu; this.actionViews = params.actionViews; this.viewType = params.viewType; this.withControlPanel = params.withControlPanel !== false; // override this.need_control_panel so that the ActionManager doesn't // update the control panel when it isn't visible (this is a temporary // hack that can be removed as soon as the CP'll be handled by the view) this.need_control_panel = this.withControlPanel; }, /** * Simply renders and updates the url. * * @returns {Deferred} */ start: function () { var self = this; this.$el.addClass('o_view_controller'); // render the ControlPanel elements (buttons, pager, sidebar...) this.controlPanelElements = this._renderControlPanelElements(); return $.when( this._super.apply(this, arguments), this.renderer.appendTo(this.$el) ).then(function () { return self._update(self.initialState); }); }, /** * @override */ destroy: function () { if (this.$buttons) { this.$buttons.off(); } if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) { this.controlPanelElements.$switch_buttons.off(); } return this._super.apply(this, arguments); }, /** * Called each time the controller is attached into the DOM. */ on_attach_callback: function () { if (this.searchView) { this.searchView.on_attach_callback(); } this.renderer.on_attach_callback(); }, /** * Called each time the controller is detached from the DOM. */ on_detach_callback: function () { this.renderer.on_detach_callback(); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ canBeRemoved: function () { // AAB: get rid of this option when on_hashchange mechanism is improved return this.discardChanges(undefined, {readonlyIfRealDiscard: true}); }, /** * Discards the changes made on the record associated to the given ID, or * all changes made by the current controller if no recordID is given. For * example, when the user opens the 'home' screen, the action manager calls * this method on the active view to make sure it is ok to open the home * screen (and lose all current state). * * Note that it returns a deferred, because the view could choose to ask the * user if he agrees to discard. * * @param {string} [recordID] * if not given, we consider all the changes made by the controller * @returns {Deferred} resolved if properly discarded, rejected otherwise */ discardChanges: function (recordID) { return $.when(); }, /** * Returns any special keys that may be useful when reloading the view to * get the same effect. This is necessary for saving the current view in * the favorites. For example, a graph view might want to add a key to * save the current graph type. * * @returns {Object} */ getContext: function () { return {}; }, /** * Returns a title that may be displayed in the breadcrumb area. For * example, the name of the record. * * note: this will be moved to AbstractAction * * @returns {string} */ getTitle: function () { return this.displayName; }, /** * Gives the focus to the renderer */ giveFocus: function() { this.renderer.giveFocus(); }, /** * The use of this method is discouraged. It is still snakecased, because * it currently is used in many templates, but we will move to a simpler * mechanism as soon as we can. * * @deprecated * @param {string} action type of action, such as 'create', 'read', ... * @returns {boolean} */ is_action_enabled: function (action) { return this.activeActions[action]; }, /** * Short helper method to reload the view * * @param {Object} [params] This object will simply be given to the update * @returns {Deferred} */ reload: function (params) { return this.update(params || {}); }, /** * For views that require a pager, this method will be called to allow the * controller to instantiate and render a pager. Note that in theory, the * controller can actually render whatever he wants in the pager zone. If * your view does not want a pager, just let this method empty. * * @param {jQuery Node} $node */ renderPager: function ($node) { }, /** * Same as renderPager, but for the 'sidebar' zone (the zone with the menu * dropdown in the control panel next to the buttons) * * @param {jQuery Node} $node */ renderSidebar: function ($node) { }, /** * This is the main entry point for the controller. Changes from the search * view arrive in this method, and internal changes can sometimes also call * this method. It is basically the way everything notifies the controller * that something has changed. * * The update method is responsible for fetching necessary data, then * updating the renderer and wait for the rendering to complete. * * @param {Object} params will be given to the model and to the renderer * @param {Object} [options] * @param {boolean} [options.reload=true] if true, the model will reload data * * @returns {Deferred} */ update: function (params, options) { var self = this; var shouldReload = (options && 'reload' in options) ? options.reload : true; var def = shouldReload ? this.model.reload(this.handle, params) : $.when(); // we check here that the updateIndex of the control panel hasn't changed // between the start of the update request and the moment the controller // asks the control panel to update itself ; indeed, it could happen that // another action/controller is executed during this one reloads itself, // and if that one finishes first, it replaces this controller in the DOM, // and this controller should no longer update the control panel. // note that this won't be necessary as soon as each controller will have // its own control panel var cpUpdateIndex = this.cp_bus && this.cp_bus.updateIndex; return this.dp.add(def).then(function (handle) { if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) { return; } self.handle = handle || self.handle; // update handle if we reloaded var state = self.model.get(self.handle); var localState = self.renderer.getLocalState(); return self.dp.add(self.renderer.updateState(state, params)).then(function () { if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) { return; } self.renderer.setLocalState(localState); return self._update(state); }); }); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * This method is the way a view can notifies the outside world that * something has changed. The main use for this is to update the url, for * example with a new id. * * @private * @param {Object} [state] information that will be pushed to the outside * world */ _pushState: function (state) { this.trigger_up('push_state', { controllerID: this.controllerID, 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 or js files, 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': '