← Back to team overview

openerp-dev-web team mailing list archive

lp:~openerp-dev/openobject-client-web/proto61-viewmanager-improvements-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61

 

Xavier (Open ERP) has proposed merging lp:~openerp-dev/openobject-client-web/proto61-viewmanager-improvements-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61.

Requested reviews:
  OpenERP SA's Web Client R&D (openerp-dev-web)

For more details, see:
https://code.launchpad.net/~openerp-dev/openobject-client-web/proto61-viewmanager-improvements-xmo/+merge/55913

Fixes to the view manager:

* Have the action manager stop() it when performing a new action (and creating a new view manager)
* Full generify content views management via a registry (viewmanager fetches the content views it needs from the registry). This was concurrently done by fme, I merged his mods.
* Removed search_visible and searchview_id, search view visibility now handled via static tag on views (if searchable === false, search view should be hidden)
* Added Python patcher to split `view_mode=tree` in `view_mode=list` (instead of view_type=form) and `view_mode-tree` (instead of view_type=tree), and relevant registration of views (list => ListView; tree => TreeView)
* Added disabling of button for current view type (as switching from tree view to tree view is not — I think — a very useful "feature")
* Fixed race condition between search view loading and default content view loading in case auto_search is set (through deferreds)

Also:
fixed a small bug with contexts and domains handling (in case they have leading spaces mostly)
-- 
https://code.launchpad.net/~openerp-dev/openobject-client-web/proto61-viewmanager-improvements-xmo/+merge/55913
Your team OpenERP R&D Team is subscribed to branch lp:~openerp-dev/openobject-client-web/trunk-proto61.
=== modified file 'addons/base/controllers/main.py'
--- addons/base/controllers/main.py	2011-03-29 12:50:42 +0000
+++ addons/base/controllers/main.py	2011-04-01 12:21:57 +0000
@@ -88,6 +88,7 @@
     @openerpweb.jsonrequest
     def login(self, req, db, login, password):
         req.session.login(db, login, password)
+
         return {
             "session_id": req.session_id,
             "uid": req.session._uid,
@@ -230,8 +231,38 @@
                     req.session.evaluation_context(
                         action['context'])) or []
 
+            self.fix_view_modes(action)
+
         return {"action": actions}
 
+    def fix_view_modes(self, action):
+        """ For historical reasons, OpenERP has weird dealings in relation to
+        view_mode and the view_type attribute (on window actions):
+
+        * one of the view modes is ``tree``, which stands for both list views
+          and tree views
+        * the choice is made by checking ``view_type``, which is either
+          ``form`` for a list view or ``tree`` for an actual tree view
+
+        This methods simply folds the view_type into view_mode by adding a
+        new view mode ``list`` which is the result of the ``tree`` view_mode
+        in conjunction with the ``form`` view_type.
+
+        TODO: this should go into the doc, some kind of "peculiarities" section
+
+        :param dict action: an action descriptor
+        :returns: nothing, the action is modified in place
+        """
+        if action.pop('view_type') != 'form':
+            return
+
+        action['view_mode'] = ','.join(
+            mode if mode != 'tree' else 'list'
+            for mode in action['view_mode'].split(','))
+        action['views'] = [
+            [id, mode if mode != 'tree' else 'list']
+            for id, mode in action['views']
+        ]
 
 class DataSet(openerpweb.Controller):
     _cp_path = "/base/dataset"
@@ -378,7 +409,7 @@
         :param session: Current OpenERP session
         :type session: openerpweb.openerpweb.OpenERPSession
         """
-        domain = elem.get(attr_name)
+        domain = elem.get(attr_name, '').strip()
         if domain:
             try:
                 elem.set(
@@ -403,7 +434,7 @@
         """
         self.parse_domain(elem, 'domain', session)
         self.parse_domain(elem, 'filter_domain', session)
-        context_string = elem.get('context')
+        context_string = elem.get('context', '').strip()
         if context_string:
             try:
                 elem.set('context',

=== modified file 'addons/base/static/src/css/base.css'
--- addons/base/static/src/css/base.css	2011-03-31 17:22:58 +0000
+++ addons/base/static/src/css/base.css	2011-04-01 12:21:57 +0000
@@ -396,7 +396,7 @@
 }
 
 /* View Manager */
-.openerp .views_switchers {
+.openerp .views-switchers {
     text-align: right;
 }
 

=== modified file 'addons/base/static/src/js/chrome.js'
--- addons/base/static/src/js/chrome.js	2011-03-31 12:20:18 +0000
+++ addons/base/static/src/js/chrome.js	2011-04-01 12:21:57 +0000
@@ -184,8 +184,19 @@
      * Controller start
      * event binding, rpc and callback calling required to initialize the
      * object can happen here
+     *
+     * Returns a promise object letting callers (subclasses and direct callers)
+     * know when this component is done starting
+     *
+     * @returns {jQuery.Deferred}
      */
     start: function() {
+        // returns an already fulfilled promise. Maybe we could return nothing?
+        // $.when can take non-deferred and in that case it simply considers
+        // them all as fulfilled promises.
+        // But in thise case we *have* to ensure callers use $.when and don't
+        // try to call deferred methods on this return value.
+        return $.Deferred().done().promise();
     },
     stop: function() {
     },
@@ -250,6 +261,19 @@
             }
         });
     },
+    /**
+     * Executes an RPC call, registering the provided callbacks.
+     *
+     * Registers a default error callback if none is provided, and handles
+     * setting the correct session id and session context in the parameter
+     * objects
+     *
+     * @param {String} url RPC endpoint
+     * @param {Object} params call parameters
+     * @param {Function} success_callback function to execute on RPC call success
+     * @param {Function} error_callback function to execute on RPC call failure
+     * @returns {jQuery.Deferred} jquery-provided ajax deferred
+     */
     rpc: function(url, params, success_callback, error_callback) {
         // Construct a JSON-RPC2 request, method is currently unused
         params.session_id = this.session_id;
@@ -259,17 +283,22 @@
         error_callback = typeof(error_callback) != "undefined" ? error_callback : this.on_rpc_error;
 
         // Call using the rpc_mode
-        this.rpc_ajax(url, {
+        return this.rpc_ajax(url, {
             jsonrpc: "2.0",
             method: "call",
             params: params,
             id:null
         }, success_callback, error_callback);
     },
+    /**
+     * Raw JSON-RPC call
+     *
+     * @returns {jQuery.Deferred} ajax-based deferred object
+     */
     rpc_ajax: function(url, payload, success_callback, error_callback) {
         var self = this;
         this.on_rpc_request();
-        $.ajax({
+        return $.ajax({
             type: "POST",
             url: url,
             dataType: 'json',
@@ -446,9 +475,18 @@
         if(this.session)
             this.session.log.apply(this.session,arguments);
     },
+    /**
+     * Performs a JSON-RPC call
+     *
+     * @param {String} url endpoint url
+     * @param {Object} data RPC parameters
+     * @param {Function} success RPC call success callback
+     * @param {Function} error RPC call error callback
+     * @returns {jQuery.Deferred} deferred object for the RPC call
+     */
     rpc: function(url, data, success, error) {
         // TODO: support additional arguments ?
-        this.session.rpc(url, data, success, error);
+        return this.session.rpc(url, data, success, error);
     }
 });
 

=== modified file 'addons/base/static/src/js/form.js'
--- addons/base/static/src/js/form.js	2011-03-31 17:22:58 +0000
+++ addons/base/static/src/js/form.js	2011-04-01 12:21:57 +0000
@@ -1,7 +1,21 @@
 
 openerp.base.form = function (openerp) {
 
-openerp.base.FormView =  openerp.base.Controller.extend({
+openerp.base.views.add('form', 'openerp.base.FormView');
+openerp.base.FormView =  openerp.base.Controller.extend(
+    /** @lends openerp.base.FormView# */{
+    /**
+     * Indicates that this view is not searchable, and thus that no search
+     * view should be displayed (if there is one active).
+     */
+    searchable: false,
+    /**
+     * @constructs
+     * @param {openerp.base.Session} session the current openerp session
+     * @param {String} element_id this view's root element id
+     * @param {openerp.base.DataSet} dataset the dataset this view will work with
+     * @param {String} view_id the identifier of the OpenERP view object
+     */
     init: function(session, element_id, dataset, view_id) {
         this._super(session, element_id);
         this.dataset = dataset;
@@ -16,7 +30,7 @@
     },
     start: function() {
         //this.log('Starting FormView '+this.model+this.view_id)
-        this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
+        return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
     },
     on_loaded: function(data) {
         var self = this;

=== modified file 'addons/base/static/src/js/list.js'
--- addons/base/static/src/js/list.js	2011-03-31 15:14:41 +0000
+++ addons/base/static/src/js/list.js	2011-04-01 12:21:57 +0000
@@ -1,6 +1,7 @@
 
 openerp.base.list = function (openerp) {
 
+openerp.base.views.add('list', 'openerp.base.ListView');
 openerp.base.ListView = openerp.base.Controller.extend({
     init: function(session, element_id, dataset, view_id) {
         this._super(session, element_id);
@@ -22,7 +23,7 @@
     },
     start: function() {
         //this.log('Starting ListView '+this.model+this.view_id)
-        this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+        return this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
     },
     on_loaded: function(data) {
         this.fields_view = data.fields_view;

=== modified file 'addons/base/static/src/js/search.js'
--- addons/base/static/src/js/search.js	2011-03-31 13:25:52 +0000
+++ addons/base/static/src/js/search.js	2011-04-01 12:21:57 +0000
@@ -14,7 +14,13 @@
     },
     start: function() {
         //this.log('Starting SearchView '+this.model+this.view_id)
-        this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+        return this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
+    },
+    show: function () {
+        this.$element.show();
+    },
+    hide: function () {
+        this.$element.hide();
     },
     /**
      * Builds a list of widget rows (each row is an array of widgets)

=== modified file 'addons/base/static/src/js/views.js'
--- addons/base/static/src/js/views.js	2011-03-31 14:44:22 +0000
+++ addons/base/static/src/js/views.js	2011-04-01 12:21:57 +0000
@@ -17,6 +17,9 @@
     do_action: function(action) {
         // instantiate the right controllers by understanding the action
         if(action.type == "ir.actions.act_window") {
+            if (this.viewmanager) {
+                this.viewmanager.stop();
+            }
             this.viewmanager = new openerp.base.ViewManager(this.session,this.element_id);
             this.viewmanager.do_action_window(action);
             this.viewmanager.start();
@@ -24,54 +27,59 @@
     }
 });
 
+/**
+ * Registry for all the main views
+ */
+openerp.base.views = new openerp.base.Registry();
 openerp.base.ViewManager =  openerp.base.Controller.extend({
-// This will be ViewManager Abstract/Common
+    // This will be ViewManager Abstract/Common
     init: function(session, element_id) {
         this._super(session, element_id);
         this.action = null;
         this.dataset = null;
-        this.searchview_id = false;
         this.searchview = null;
-        this.search_visible = true;
         this.active_view = null;
-        this.auto_search = false;
         // this.views = { "list": { "view_id":1234, "controller": instance} }
         this.views = {};
     },
     start: function() {
     },
+    /**
+     * Asks the view manager to switch visualization mode.
+     *
+     * @param {String} view_type type of view to display
+     * @returns {jQuery.Deferred} new view loading promise
+     */
     on_mode_switch: function(view_type) {
+        var view_promise;
         this.active_view = view_type;
         var view = this.views[view_type];
         if (!view.controller) {
             // Lazy loading of views
-            var controller;
-            switch (view_type) {
-                case 'tree':
-                    controller = new openerp.base.ListView(this.session, this.element_id + "_view_tree", this.dataset, view.view_id);
-                    break;
-                case 'form':
-                    controller = new openerp.base.FormView(this.session, this.element_id + "_view_form", this.dataset, view.view_id);
-                    break;
-                case 'calendar':
-                    controller = new openerp.base.CalendarView(this.session, this.element_id + "_view_calendar", this.dataset, view.view_id);
-                    break;
-                case 'gantt':
-                    controller = new openerp.base.GanttView(this.session, this.element_id + "_view_gantt", this.dataset, view.view_id);
-                    break;
-            }
-            controller.start();
+            var controller = new (openerp.base.views.get_object(view_type))(
+                this.session, this.element_id + "_view_" + view_type,
+                this.dataset, view.view_id);
+            view_promise = controller.start();
             this.views[view_type].controller = controller;
-            if (this.auto_search) {
-                this.searchview.on_loaded.add_last(this.searchview.do_search);
-                this.auto_search = false;
-            }
-        }
+        }
+
+        if (view.controller.searchable === false) {
+            this.searchview.hide();
+        } else {
+            this.searchview.show();
+        }
+
+        this.$element
+            .find('.views-switchers button').removeAttr('disabled')
+            .filter('[data-view-type="' + view_type + '"]')
+                .attr('disabled', true);
+
         for (var i in this.views) {
             if (this.views[i].controller) {
                this.views[i].controller.$element.toggle(i === view_type);
             }
         }
+        return view_promise;
     },
     /**
      * Extract search view defaults from the current action's context.
@@ -90,37 +98,51 @@
         });
         return defaults;
     },
+    /**
+     * Sets up the current viewmanager's search view.
+     *
+     * @param action the action being executed
+     * @returns {jQuery.Deferred} search view startup deferred
+     */
+    setup_search_view:function (action) {
+        var self = this;
+        if (this.searchview) {
+            this.searchview.stop();
+        }
+
+        var searchview = this.searchview = new openerp.base.SearchView(
+                this.session, this.element_id + "_search",
+                this.dataset, action.search_view_id[0] || false,
+                this.search_defaults());
+        searchview.on_search.add(function() {
+            self.views[self.active_view].controller.do_search.apply(self, arguments);
+        });
+        return searchview.start();
+    },
     do_action_window: function(action) {
         var self = this;
-        var prefix_id = "#" + this.element_id;
         this.action = action;
         this.dataset = new openerp.base.DataSet(this.session, action.res_model);
         this.dataset.start();
 
         this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: action.views}));
 
-        this.searchview_id = false;
-        if (this.search_visible && action.search_view_id) {
-            this.searchview_id = action.search_view_id[0];
-            var searchview = this.searchview = new openerp.base.SearchView(
-                    this.session, this.element_id + "_search",
-                    this.dataset, this.searchview_id,
-                    this.search_defaults());
-            searchview.on_search.add(function() {
-                self.views[self.active_view].controller.do_search.apply(self, arguments);
-            });
-            searchview.start();
+        var searchview_loaded = this.setup_search_view(action);
 
-            this.auto_search = action.auto_search;
-        }
-        this.$element.find('.views_switchers button').click(function() {
+        this.$element.find('.views-switchers button').click(function() {
             self.on_mode_switch($(this).data('view-type'));
         });
         _.each(action.views, function(view) {
             self.views[view[1]] = { view_id: view[0], controller: null };
         });
+
         // switch to the first one in sequence
-        this.on_mode_switch(action.views[0][1]);
+        var inital_view_loaded = this.on_mode_switch(action.views[0][1]);
+
+        if (action['auto_search']) {
+            $.when(searchview_loaded, inital_view_loaded)
+                .then(this.searchview.do_search);
+        }
     },
     // create when root, also add to parent when o2m
     on_create: function() {
@@ -229,19 +251,42 @@
     }
 });
 
+openerp.base.views.add('calendar', 'openerp.base.CalendarView');
 openerp.base.CalendarView = openerp.base.Controller.extend({
-// Dhtmlx scheduler ?
+    start: function () {
+        this._super();
+        this.$element.append('Calendar view');
+    }
 });
 
+openerp.base.views.add('gantt', 'openerp.base.GanttView');
 openerp.base.GanttView = openerp.base.Controller.extend({
-// Dhtmlx gantt ?
+    start: function () {
+        this._super();
+        this.$element.append('Gantt view');
+    }
+});
+
+openerp.base.views.add('tree', 'openerp.base.TreeView');
+/**
+ * Genuine tree view (the one displayed as a tree, not the list)
+ */
+openerp.base.TreeView = openerp.base.Controller.extend({
+    start: function () {
+        this._super();
+        this.$element.append('Tree view');
+    }
 });
 
 openerp.base.DiagramView = openerp.base.Controller.extend({
-// 
 });
 
+openerp.base.views.add('graph', 'openerp.base.GraphView');
 openerp.base.GraphView = openerp.base.Controller.extend({
+    start: function () {
+        this._super();
+        this.$element.append('Graph view');
+    }
 });
 
 openerp.base.ProcessView = openerp.base.Controller.extend({

=== modified file 'addons/base/static/src/xml/base.xml'
--- addons/base/static/src/xml/base.xml	2011-03-31 17:22:58 +0000
+++ addons/base/static/src/xml/base.xml	2011-04-01 12:21:57 +0000
@@ -126,16 +126,16 @@
 </t>
 <t t-name="ViewManager">
     <!-- TODO prefix id with the element_id of the controller t-attf-id="#{prefix}_localid" -->
-    <div class="views_switchers">
+    <div class="views-switchers">
         <t t-foreach="views" t-as="view">
             <button type="button" t-att-data-view-type="view[1]">
                 <t t-esc="view[1]"/>
             </button>
         </t>
     </div>
-    <div t-attf-id="#{prefix}_search"></div>
+    <div t-attf-id="#{prefix}_search"/>
     <t t-foreach="views" t-as="view">
-        <div t-attf-id="#{prefix}_view_#{view[1]}"></div>
+        <div t-attf-id="#{prefix}_view_#{view[1]}"/>
     </t>
 </t>
 <t t-name="ListView">

=== modified file 'addons/base/test/test_menu.py'
--- addons/base/test/test_menu.py	2011-03-21 15:40:34 +0000
+++ addons/base/test/test_menu.py	2011-04-01 12:21:57 +0000
@@ -97,3 +97,55 @@
                  }]
             }]
         )
+
+class ActionMungerTest(unittest2.TestCase):
+    def setUp(self):
+        self.menu = base.controllers.main.Menu()
+    def test_actual_treeview(self):
+        action = {
+            "views": [[False, "tree"], [False, "form"],
+                      [False, "calendar"]],
+            "view_type": "tree",
+            "view_id": False,
+            "view_mode": "tree,form,calendar"
+        }
+        changed = action.copy()
+        del action['view_type']
+        self.menu.fix_view_modes(changed)
+
+        self.assertEqual(changed, action)
+
+    def test_list_view(self):
+        action = {
+            "views": [[False, "tree"], [False, "form"],
+                      [False, "calendar"]],
+            "view_type": "form",
+            "view_id": False,
+            "view_mode": "tree,form,calendar"
+        }
+        self.menu.fix_view_modes(action)
+
+        self.assertEqual(action, {
+            "views": [[False, "list"], [False, "form"],
+                      [False, "calendar"]],
+            "view_id": False,
+            "view_mode": "list,form,calendar"
+        })
+
+    def test_redundant_views(self):
+
+        action = {
+            "views": [[False, "tree"], [False, "form"],
+                      [False, "calendar"], [42, "tree"]],
+            "view_type": "form",
+            "view_id": False,
+            "view_mode": "tree,form,calendar"
+        }
+        self.menu.fix_view_modes(action)
+
+        self.assertEqual(action, {
+            "views": [[False, "list"], [False, "form"],
+                      [False, "calendar"], [42, "list"]],
+            "view_id": False,
+            "view_mode": "list,form,calendar"
+        })

=== modified file 'addons/base/test/test_view.py'
--- addons/base/test/test_view.py	2011-03-29 11:01:30 +0000
+++ addons/base/test/test_view.py	2011-04-01 12:21:57 +0000
@@ -32,7 +32,7 @@
 
     def test_convert_literal_domain(self):
         e = xml.etree.ElementTree.Element(
-            'field', domain="[('somefield', '=', 3)]")
+            'field', domain="  [('somefield', '=', 3)]  ")
         self.view.parse_domains_and_contexts(e, None)
 
         self.assertEqual(
@@ -75,7 +75,7 @@
 
     def test_convert_literal_context(self):
         e = xml.etree.ElementTree.Element(
-            'field', context="{'some_prop':  3}")
+            'field', context="  {'some_prop':  3}  ")
         self.view.parse_domains_and_contexts(e, None)
 
         self.assertEqual(


Follow ups