← Back to team overview

openerp-dev-web team mailing list archive

lp:~openerp-dev/openobject-client-web/trunk-proto61-new-listviews-section2-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61

 

Xavier (Open ERP) has proposed merging lp:~openerp-dev/openobject-client-web/trunk-proto61-new-listviews-section2-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/trunk-proto61-new-listviews-section2-xmo/+merge/57339

* Fixed a few bugs
* Styling of list view
* Basic attrs and invisible handling (invisible not dynamic)
* Basic actions from list view
-- 
https://code.launchpad.net/~openerp-dev/openobject-client-web/trunk-proto61-new-listviews-section2-xmo/+merge/57339
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-04-11 11:35:16 +0000
+++ addons/base/controllers/main.py	2011-04-12 15:05:50 +0000
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 import glob, os
+import pprint
 from xml.etree import ElementTree
 from cStringIO import StringIO
 
@@ -334,18 +335,17 @@
         :param int offset: from which index should the results start being returned
         :param int limit: the maximum number of records to return
         :param list domain: the search domain for the query
-        :param dict context: the context in which the search should be executed
         :param list sort: sorting directives
         :returns: a list of result records
         :rtype: list
         """
         Model = request.session.model(model)
         ids = Model.search(domain or [], offset or 0, limit or False,
-                           sort or False, context or False)
+                           sort or False, request.context)
         if fields and fields == ['id']:
             # shortcut read if we only want the ids
             return map(lambda id: {'id': id}, ids)
-        return Model.read(ids, fields or False)
+        return Model.read(ids, fields or False, request.context)
 
     @openerpweb.jsonrequest
     def get(self, request, model, ids, fields=False):
@@ -363,6 +363,9 @@
         :type model: str
         :param ids: a list of identifiers
         :type ids: list
+        :param fields: a list of fields to fetch, ``False`` or empty to fetch
+                       all fields in the model
+        :type fields: list | False
         :returns: a list of records, in the same order as the list of ids
         :rtype: list
         """
@@ -407,16 +410,20 @@
         return {'result': r}
 
 class View(openerpweb.Controller):
-    def fields_view_get(self, session, model, view_id, view_type, transform=True, toolbar=False, submenu=False):
-        Model = session.model(model)
-        r = Model.fields_view_get(view_id, view_type, {}, toolbar, submenu)
+    def fields_view_get(self, request, model, view_id, view_type,
+                        transform=True, toolbar=False, submenu=False):
+        Model = request.session.model(model)
+        fvg = Model.fields_view_get(view_id, view_type, request.context,
+                                    toolbar, submenu)
         if transform:
-            context = {} # TODO: dict(ctx_sesssion, **ctx_action)
-            xml = self.transform_view(r['arch'], session, context)
+            evaluation_context = request.session.evaluation_context(
+                request.context or {})
+            xml = self.transform_view(
+                fvg['arch'], request.session, evaluation_context)
         else:
-            xml = ElementTree.fromstring(r['arch'])
-        r['arch'] = Xml2Json.convert_element(xml)
-        return r
+            xml = ElementTree.fromstring(fvg['arch'])
+        fvg['arch'] = Xml2Json.convert_element(xml)
+        return fvg
 
     def normalize_attrs(self, elem, context):
         """ Normalize @attrs, @invisible, @required, @readonly and @states, so
@@ -430,19 +437,16 @@
         :param dict context: evaluation context
         """
         # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
-        attrs = eval(elem.attrib.get('attrs', '{}'))
+        attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
         if 'states' in elem.attrib:
-            if 'invisible' not in attrs:
-                attrs['invisible'] = []
-                # This should be done by the server
-            attrs['invisible'].append(('state', 'not in', elem.attrib['states'].split(',')))
-            del(elem.attrib['states'])
+            attrs.setdefault('invisible', [])\
+                .append(('state', 'not in', elem.attrib.pop('states').split(',')))
         if attrs:
-            elem.attrib['attrs'] = simplejson.dumps(attrs)
+            elem.set('attrs', simplejson.dumps(attrs))
         for a in ['invisible', 'readonly', 'required']:
             if a in elem.attrib:
                 # In the XML we trust
-                avalue = bool(eval(elem.attrib.get(a, 'False'),
+                avalue = bool(eval(elem.get(a, 'False'),
                                    {'context': context or {}}))
                 if not avalue:
                     del elem.attrib[a]
@@ -514,7 +518,7 @@
 
     @openerpweb.jsonrequest
     def load(self, req, model, view_id, toolbar=False):
-        fields_view = self.fields_view_get(req.session, model, view_id, 'form', toolbar=toolbar)
+        fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
         return {'fields_view': fields_view}
 
 class ListView(View):
@@ -522,15 +526,86 @@
 
     @openerpweb.jsonrequest
     def load(self, req, model, view_id, toolbar=False):
-        fields_view = self.fields_view_get(req.session, model, view_id, 'tree', toolbar=toolbar)
+        fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
         return {'fields_view': fields_view}
 
+    def fields_view_get(self, request, model, view_id, view_type="tree",
+                        transform=True, toolbar=False, submenu=False):
+        """ Sets @editable on the view's arch if it isn't already set and
+        ``set_editable`` is present in the request context
+        """
+        view = super(ListView, self).fields_view_get(
+            request, model, view_id, view_type, transform, toolbar, submenu)
+
+        view_attributes = view['arch']['attrs']
+        if request.context.get('set_editable')\
+                and 'editable' not in view_attributes:
+            view_attributes['editable'] = 'bottom'
+        return view
+
+    @openerpweb.jsonrequest
+    def fill(self, request, model, id, domain,
+             offset=0, limit=False):
+        return self.do_fill(request, model, id, domain, offset, limit)
+
+    def do_fill(self, request, model, id, domain,
+                offset=0, limit=False):
+        """ Returns all information needed to fill a table:
+
+        * view with processed ``editable`` flag
+        * fields (columns) with processed ``invisible`` flag
+        * rows with processed ``attrs`` and ``colors``
+
+        .. note:: context is passed through ``request`` parameter
+
+        :param request: OpenERP request
+        :type request: openerpweb.openerpweb.JsonRequest
+        :type str model: OpenERP model for this list view
+        :type int id: view_id, or False if none provided
+        :param list domain: the search domain to search for
+        :param int offset: search offset, for pagination
+        :param int limit: search limit, for pagination
+        :returns: hell if I have any idea yet
+        """
+        view = self.fields_view_get(request, model, id)
+
+        rows = DataSet().do_search_read(request, model,
+                                        offset=offset, limit=limit,
+                                        domain=domain)
+        eval_context = request.session.evaluation_context(
+            request.context)
+        return [
+            {'data': dict((key, {'value': value})
+                          for key, value in row.iteritems()),
+             'color': self.process_colors(view, row, eval_context)}
+            for row in rows
+        ]
+
+    def process_colors(self, view, row, context):
+        colors = view['arch']['attrs'].get('colors')
+
+        if not colors:
+            return None
+
+        color = [
+            pair.split(':')[0]
+            for pair in colors.split(';')
+            if eval(pair.split(':')[1], dict(context, **row))
+        ]
+
+        if not color:
+            return None
+        elif len(color) == 1:
+            return color[0]
+        return 'maroon'
+
+
 class SearchView(View):
     _cp_path = "/base/searchview"
 
     @openerpweb.jsonrequest
     def load(self, req, model, view_id):
-        fields_view = self.fields_view_get(req.session, model, view_id, 'search')
+        fields_view = self.fields_view_get(req, model, view_id, 'search')
         return {'fields_view': fields_view}
 
 class Action(openerpweb.Controller):

=== modified file 'addons/base/static/src/css/base.css'
--- addons/base/static/src/css/base.css	2011-04-12 14:28:01 +0000
+++ addons/base/static/src/css/base.css	2011-04-12 15:05:50 +0000
@@ -443,6 +443,76 @@
     padding-left: 10px;
 }
 
+/* List */
+.openerp .oe-listview table {
+    clear: right;
+    width: 100%;
+    border-spacing: 0;
+    border: 1px solid silver;
+}
+
+.openerp .oe-listview tr.odd {
+    background-color: #f3f3f3;
+}
+.openerp .oe-listview tbody tr:hover {
+    background-color: #ecebf2;
+}
+.openerp .oe-listview tbody tr:hover {
+    background-color: #eae9f0;
+}
+
+.openerp .oe-listview td,
+.openerp .oe-listview th {
+    vertical-align: middle;
+}
+
+.openerp .oe-listview .oe-field-cell button {
+    padding: 0;
+    border: none;
+    background: none;
+}
+.openerp .oe-listview .oe-field-cell button:active {
+    opacity: 0.5;
+}
+
+.openerp .oe-listview th.oe-actions {
+    text-align: left;
+}
+.openerp .oe-listview th.oe-list-pager {
+    text-align: right;
+}
+
+/** list rounded corners
+
+    rounded corners are a pain on tables: need to round not only table, but
+    also on the first and last children of the first and last row
+ */
+.openerp .oe-listview table {
+    -webkit-border-radius: 7px;
+    -moz-border-radius: 7px;
+    border-radius: 7px;
+}
+.openerp .oe-listview table thead tr:first-child th:first-child {
+    -webkit-border-top-left-radius: 7px;
+    -moz-border-radius-topleft: 7px;
+    border-top-left-radius: 7px;
+}
+.openerp .oe-listview table thead tr:first-child th:last-child {
+    -webkit-border-top-right-radius: 7px;
+    -moz-border-radius-topright: 7px;
+    border-top-right-radius: 7px;
+}
+.openerp .oe-listview table tbody tr:last-child th:first-child {
+    -webkit-border-bottom-left-radius: 7px;
+    -moz-border-radius-bottomleft: 7px;
+    border-bottom-left-radius: 7px;
+}
+.openerp .oe-listview table tbody tr:last-child td:last-child {
+    -webkit-border-bottom-right-radius: 7px;
+    -moz-border-radius-bottomright: 7px;
+    border-bottom-right-radius: 7px;
+}
+
 /* Notebook */
 .openerp .oe_form_notebook {
     padding: 0px;
@@ -469,7 +539,6 @@
     background: #f9f9f9;
 }
 
-
 /* Form */
 .openerp table.oe_frame td {
     color: #4c4c4c;

=== modified file 'addons/base/static/src/js/data.js'
--- addons/base/static/src/js/data.js	2011-04-11 11:35:16 +0000
+++ addons/base/static/src/js/data.js	2011-04-12 15:05:50 +0000
@@ -108,6 +108,8 @@
         this.notification.notify("Unlink", ids);
     },
     call: function (method, ids, args, callback) {
+        this.notification.notify(
+            "Calling", this.model + '#' + method + '(' + ids + ')');
         ids = ids || [];
         args = args || [];
         return this.rpc('/base/dataset/call', {

=== modified file 'addons/base/static/src/js/form.js'
--- addons/base/static/src/js/form.js	2011-04-12 14:14:59 +0000
+++ addons/base/static/src/js/form.js	2011-04-12 15:05:50 +0000
@@ -306,6 +306,64 @@
 /** @namespace */
 openerp.base.form = {};
 
+openerp.base.form.compute_domain = function(expr, fields) {
+    var stack = [];
+    for (var i = expr.length - 1; i >= 0; i--) {
+        var ex = expr[i];
+        if (ex.length == 1) {
+            var top = stack.pop();
+            switch (ex[0]) {
+                case '|':
+                    stack.push(stack.pop() || top);
+                    continue;
+                case '&':
+                    stack.push(stack.pop() && top);
+                    continue;
+                case '!':
+                    stack.push(!top);
+                    continue;
+                default:
+                    throw new Error('Unknown domain operator ' + ex[0]);
+            }
+        }
+
+        var field = fields[ex[0]].value;
+        var op = ex[1];
+        var val = ex[2];
+
+        switch (op.toLowerCase()) {
+            case '=':
+            case '==':
+                stack.push(field == val);
+                break;
+            case '!=':
+            case '<>':
+                stack.push(field != val);
+                break;
+            case '<':
+                stack.push(field < val);
+                break;
+            case '>':
+                stack.push(field > val);
+                break;
+            case '<=':
+                stack.push(field <= val);
+                break;
+            case '>=':
+                stack.push(field >= val);
+                break;
+            case 'in':
+                stack.push(_.indexOf(val, field) > -1);
+                break;
+            case 'not in':
+                stack.push(_.indexOf(val, field) == -1);
+                break;
+            default:
+                this.log("Unsupported operator in attrs :", op);
+        }
+    }
+    return _.indexOf(stack, false) == -1;
+},
 openerp.base.form.Widget = openerp.base.Controller.extend({
     init: function(view, node) {
         this.view = view;
@@ -330,68 +388,10 @@
         this.$element = $('#' + this.element_id);
     },
     process_attrs: function() {
+        var compute_domain = openerp.base.form.compute_domain;
         for (var a in this.attrs) {
-            this[a] = this.eval_attrs(this.attrs[a]);
-        }
-    },
-    eval_attrs: function(expr) {
-        var stack = [];
-        for (var i = 0; i < expr.length; i++) {
-            var ex = expr[i];
-            if (ex.length == 1) {
-                stack.push(ex[0]);
-                continue;
-            }
-
-            var field = this.view.fields[ex[0]].value;
-            var op = ex[1];
-            var val = ex[2];
-
-            switch (op.toLowerCase()) {
-                case '=':
-                case '==':
-                    stack.push(field == val);
-                    break;
-                case '!=':
-                case '<>':
-                    stack.push(field != val);
-                    break;
-                case '<':
-                    stack.push(field < val);
-                    break;
-                case '>':
-                    stack.push(field > val);
-                    break;
-                case '<=':
-                    stack.push(field <= val);
-                    break;
-                case '>=':
-                    stack.push(field >= val);
-                    break;
-                case 'in':
-                    stack.push(_.indexOf(val, field) > -1);
-                    break;
-                case 'not in':
-                    stack.push(_.indexOf(val, field) == -1);
-                    break;
-                default:
-                    this.log("Unsupported operator in attrs :", op);
-            }
-        }
-
-        for (var j = stack.length-1; j >- 1; j--) {
-            switch (stack[j]) {
-                case '|':
-                    var result = stack[j + 1] || stack[j + 2];
-                    stack.splice(j, 3, result);
-                    break;
-                case '&':
-                    var result = stack[j + 1] && stack[j + 2];
-                    stack.splice(j, 3, result);
-                    break;
-            }
-        }
-        return _.indexOf(stack, false) == -1;
+            this[a] = compute_domain(this.attrs[a], this.view.fields);
+        }
     },
     update_dom: function() {
         this.$element.toggle(!this.invisible);

=== modified file 'addons/base/static/src/js/list.js'
--- addons/base/static/src/js/list.js	2011-04-08 16:10:23 +0000
+++ addons/base/static/src/js/list.js	2011-04-12 15:05:50 +0000
@@ -8,7 +8,9 @@
         // list rows can be deleted
         'deletable': true,
         // whether the column headers should be displayed
-        'header': true
+        'header': true,
+        // display addition button, with that label
+        'addable': "New"
     },
     /**
      * @constructs
@@ -21,6 +23,7 @@
      * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
      * @param {Boolean} [options.header=true] should the list's header be displayed
      * @param {Boolean} [options.deletable=true] are the list rows deletable
+     * @param {null|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
      */
     init: function(view_manager, session, element_id, dataset, view_id, options) {
         this._super(session, element_id);
@@ -35,20 +38,37 @@
         this.options = _.extend({}, this.defaults, options || {});
     },
     start: function() {
-        //this.log('Starting ListView '+this.model+this.view_id)
+        this.$element.addClass('oe-listview');
         return this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id,
             toolbar:!!this.view_manager.sidebar}, this.on_loaded);
     },
     on_loaded: function(data) {
+        var self = this;
         this.fields_view = data.fields_view;
         //this.log(this.fields_view);
         this.name = "" + this.fields_view.arch.attrs.string;
 
         var fields = this.fields_view.fields;
+        var domain_computer = openerp.base.form.compute_domain;
         this.columns = _(this.fields_view.arch.children).chain()
             .map(function (field) {
                 var name = field.attrs.name;
-                return _.extend({id: name, tag: field.tag}, field.attrs, fields[name]);
+                var column = _.extend({id: name, tag: field.tag},
+                                      field.attrs, fields[name]);
+                // attrs computer
+                if (column.attrs) {
+                    var attrs = eval('(' + column.attrs + ')');
+                    column.attrs_for = function (fields) {
+                        var result = {};
+                        for (var attr in attrs) {
+                            result[attr] = domain_computer(attrs[attr], fields);
+                        }
+                        return result;
+                    };
+                } else {
+                    column.attrs_for = function () { return {}; };
+                }
+                return column;
             }).value();
 
         this.visible_columns = _.filter(this.columns, function (column) {
@@ -57,20 +77,46 @@
         this.$element.html(QWeb.render("ListView", this));
 
         // Head hook
-        this.$element.find('#oe-list-delete').click(this.do_delete_selected);
+        this.$element.find('#oe-list-add').click(this.do_add_record);
+        this.$element.find('#oe-list-delete')
+                .hide()
+                .click(this.do_delete_selected);
 
+        var $table = this.$element.find('table');
         // Cell events
-        this.$element.find('table').delegate(
-                'th.oe-record-selector', 'click', function (e) {
-                    // A click in the selection cell should not activate the
-                    // linking feature
-                    e.stopImmediatePropagation();
+        $table.delegate(
+            'th.oe-record-selector', 'click', function (e) {
+                // TODO: ~linear performances, would a simple counter work?
+                if ($table.find('th.oe-record-selector input:checked').length) {
+                    $table.find('#oe-list-delete').show();
+                } else {
+                    $table.find('#oe-list-delete').hide();
+                }
+                // A click in the selection cell should not activate the
+                // linking feature
+                e.stopImmediatePropagation();
         });
-        this.$element.find('table').delegate(
+        $table.delegate(
+            'td.oe-field-cell button', 'click', function (e) {
+                var $cell = $(e.currentTarget).closest('td');
+                var col_index = $cell.prevAll('td').length;
+                var field = self.visible_columns[col_index];
+                var action = field.name;
+
+                var $row = $cell.parent('tr');
+                var row = self.rows[$row.prevAll().length];
+
+                var context = _.extend(
+                        {}, self.dataset.context, field.context || {});
+                self.dataset.call(action, [row.data.id.value], [context],
+                                  self.do_reload);
+                e.stopImmediatePropagation();
+            });
+        $table.delegate(
                 'td.oe-record-delete button', 'click', this.do_delete);
 
         // Global rows handlers
-        this.$element.find('table').delegate(
+        $table.delegate(
                 'tr', 'click', this.on_select_row);
 
         // sidebar stuff
@@ -85,11 +131,27 @@
      * @returns {Promise} promise to the end of view rendering (list views are asynchronously filled for improved responsiveness)
      */
     do_fill_table: function(records) {
+        var $table = this.$element.find('table');
         this.rows = records;
 
-        var $table = this.$element.find('table');
+        // Keep current selected record, if it's still in our new search
+        var current_record_id = this.dataset.ids[this.dataset.index];
+        this.dataset.ids = _(records).chain().map(function (record) {
+            return record.data.id.value;
+        }).value();
+        this.dataset.index = _.indexOf(this.dataset.ids, current_record_id);
+        if (this.dataset.index === -1) {
+            this.dataset.index = 0;
+        }
+
+        // TODO: offset, length, count
+        var results = this.rows.length;
+        $table.find('.oe-pager-last').text(results);
+        $table.find('.oe-pager-total').text(results);
+
+
         // remove all data lines
-        $table.find('tbody').remove();
+        var $old_body = $table.find('tbody');
 
         // add new content
         var columns = this.columns,
@@ -100,10 +162,10 @@
         var PAGE_SIZE = 50,
             bodies_count = Math.ceil(this.rows.length / PAGE_SIZE),
             body = 0,
-            $body = $('<tbody>').appendTo($table);
+            $body = $('<tbody class="ui-widget-content">').appendTo($table);
 
+        var rendered = $.Deferred();
         var render_body = function () {
-            var rendered = $.Deferred();
             setTimeout(function () {
                 $body.append(
                     QWeb.render("ListView.rows", {
@@ -118,9 +180,29 @@
                     rendered.resolve();
                 }
             }, 0);
-            return rendered.promise();
         };
-        return render_body();
+        render_body();
+
+        return rendered.promise().then(function () {
+            $old_body.remove();
+        });
+    },
+    /**
+     * Asks the view manager to switch to a different view, using the provided
+     * record index (within the current dataset).
+     *
+     * If the index is null, ``switch_to_record`` asks for the creation of a
+     * new record.
+     *
+     * @param {Number|null} index the record index (in the current dataset) to switch to
+     * @param {String} [view="form"] the view to switch to
+     */
+    switch_to_record:function (index, view) {
+        view = view || 'form';
+        this.dataset.index = index;
+        _.delay(_.bind(function () {
+            this.view_manager.on_mode_switch(view);
+        }, this));
     },
     on_select_row: function (event) {
         var $target = $(event.currentTarget);
@@ -130,26 +212,41 @@
         // count number of preceding siblings to line clicked
         var row = this.rows[$target.prevAll().length];
 
-        var index = _.indexOf(this.dataset.ids, row.id);
+        var index = _.indexOf(this.dataset.ids, row.data.id.value);
         if (index == undefined || index === -1) {
             return;
         }
-        this.dataset.index = index;
-        _.delay(_.bind(function () {
-            this.view_manager.on_mode_switch('form');
-        }, this));
-
-    },
+            this.switch_to_record(index);
+        },
     do_show: function () {
-        // TODO: re-trigger search
         this.$element.show();
+        if (this.hidden) {
+            this.do_reload();
+            this.hidden = false;
+        }
     },
     do_hide: function () {
         this.$element.hide();
+        this.hidden = true;
+    },
+    /**
+     * Reloads the search view based on the current settings (dataset & al)
+     */
+    do_reload: function () {
+        // TODO: need to do 5 billion tons of pre-processing, bypass
+        // DataSet for now
+        //self.dataset.read_slice(self.dataset.fields, 0, self.limit,
+        // self.do_fill_table);
+        return this.rpc('/base/listview/fill', {
+            'model': this.dataset.model,
+            'id': this.view_id,
+            'context': this.dataset.context,
+            'domain': this.dataset.domain
+        }, this.do_fill_table);
     },
     do_search: function (domains, contexts, groupbys) {
         var self = this;
-        this.rpc('/base/session/eval_domain_and_context', {
+        return this.rpc('/base/session/eval_domain_and_context', {
             domains: domains,
             contexts: contexts,
             group_by_seq: groupbys
@@ -157,12 +254,12 @@
             // TODO: handle non-empty results.group_by with read_group
             self.dataset.context = results.context;
             self.dataset.domain = results.domain;
-            self.dataset.read_slice(self.dataset.fields, 0, self.limit, self.do_fill_table);
+            return self.do_reload();
         });
     },
     do_update: function () {
         var self = this;
-        self.dataset.read_ids(self.dataset.ids, self.dataset.fields, self.do_fill_table);
+        //self.dataset.read_ids(self.dataset.ids, self.dataset.fields, self.do_fill_table);
     },
     /**
      * Handles the signal to delete a line from the DOM
@@ -173,7 +270,17 @@
         // don't link to forms
         e.stopImmediatePropagation();
         this.dataset.unlink(
-            [this.rows[$(e.currentTarget).closest('tr').prevAll().length].id]);
+            [this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value]);
+    },
+    /**
+     * Handles signal for the addition of a new record (can be a creation,
+     * can be the addition from a remote source, ...)
+     *
+     * The default implementation is to switch to a new record on the form view
+     */
+    do_add_record: function () {
+        this.notification.notify('Add', "New record");
+        this.switch_to_record(null);
     },
     /**
      * Handles deletion of all selected lines
@@ -195,7 +302,7 @@
         var rows = this.rows;
         return this.$element.find('th.oe-record-selector input:checked')
                 .closest('tr').map(function () {
-            return rows[$(this).prevAll().length].id;
+            return rows[$(this).prevAll().length].data.id.value;
         }).get();
     }
 });

=== modified file 'addons/base/static/src/js/views.js'
--- addons/base/static/src/js/views.js	2011-04-12 14:11:18 +0000
+++ addons/base/static/src/js/views.js	2011-04-12 15:05:50 +0000
@@ -116,8 +116,10 @@
             this.searchview.stop();
         }
         this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults);
-        this.searchview.on_search.add(function() {
-            self.views[self.active_view].controller.do_search.apply(self, arguments);
+        this.searchview.on_search.add(function(domains, contexts, groupbys) {
+            self.views[self.active_view].controller.do_search.call(
+                self, domains.concat(self.domains()),
+                      contexts.concat(self.contexts()), groupbys);
         });
         return this.searchview.start();
     },
@@ -131,6 +133,23 @@
     on_remove: function() {
     },
     on_edit: function() {
+    },
+    /**
+     * Domains added on searches by the view manager, to override in subsequent
+     * view manager in order to add new pieces of domains to searches
+     *
+     * @returns an empty list
+     */
+    domains: function () {
+        return [];
+    },
+    /**
+     * Contexts added on searches by the view manager.
+     *
+     * @returns an empty list
+     */
+    contexts: function () {
+        return [];
     }
 });
 
@@ -152,8 +171,6 @@
             this.sidebar.start();
         }
 
-        // init search view
-        var view_id = this.action.search_view_id ? this.action.search_view_id[0] || false : false;
         var search_defaults = {};
         _.each(this.action.context, function (value, key) {
             var match = /^search_default_(.*)$/.exec(key);
@@ -161,10 +178,12 @@
                 search_defaults[match[1]] = value;
             }
         });
-        var searchview_loaded = null;
-        if (view_id) {
-            searchview_loaded = this.setup_search_view(view_id,search_defaults);
-        }
+
+        // init search view
+        var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
+
+        var searchview_loaded = this.setup_search_view(
+                searchview_id || false, search_defaults);
 
         // schedule auto_search
         if (searchview_loaded != null && this.action['auto_search']) {
@@ -178,6 +197,28 @@
             this.sidebar.stop();
         }
         this._super();
+    },
+    /**
+     * adds action domain to the search domains
+     *
+     * @returns the action's domain
+     */
+    domains: function () {
+        if (!this.action.domain) {
+            return [];
+        }
+        return [this.action.domain];
+    },
+    /**
+     * adds action context to the search contexts
+     *
+     * @returns the action's context
+     */
+    contexts: function () {
+        if (!this.action.context) {
+            return [];
+        }
+        return [this.action.context];
     }
 });
 

=== modified file 'addons/base/static/src/xml/base.xml'
--- addons/base/static/src/xml/base.xml	2011-04-12 13:19:48 +0000
+++ addons/base/static/src/xml/base.xml	2011-04-12 15:05:50 +0000
@@ -154,38 +154,37 @@
 	</tr>
     </table>
 </t>
-<t t-name="ListView">
-    <!--
-    <h3><t t-esc="fields_view.arch['@string']"/></h3>
-    -->
-    <div class="oe_list_header">
-        <div class="oe_list_buttons">
-            <!--<button type="button" class="oe_list_button_new">New</button>-->
-        </div>
-        <div class="oe_list_pager">
-            <button type="button" data-pager-action="first">First</button>
-            <button type="button" data-pager-action="previous">&lt;&lt;</button>
-
-            <span class="oe_pager_index">0</span> / <span class="oe_pager_count">0</span>
-
-            <button type="button" data-pager-action="next">&gt;&gt;</button>
-            <button type="button" data-pager-action="last">Last</button>
-        </div>
-    </div>
-    <table>
-        <t t-call="ListView.header"/>
-    </table>
-</t>
-<t t-name="ListView.header">
-    <thead t-if="options.header">
-        <tr t-if="options.selectable and options.deletable">
-            <th t-att-colspan="visible_columns.length + 2">
-                <button type="button" id="oe-list-delete">
-                    Delete Selected
-                </button>
+<table t-name="ListView">
+    <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/>
+    <t t-set="actions_span" t-value="Math.floor((options.deletable or options.addable) ? columns_count/2 : 0)"/>
+    <thead class="ui-widget-header">
+        <tr t-if="options.selectable">
+            <th t-if="actions_span" t-att-colspan="actions_span"
+                class="oe-actions">
+                <button type="button" id="oe-list-add"
+                        t-if="options.addable">
+                    <t t-esc="options.addable"/>
+                </button>
+                <button type="button" id="oe-list-delete"
+                        t-if="options.deletable">
+                    Delete
+                </button>
+            </th>
+            <th t-att-colspan="columns_count - actions_span"
+                class="oe-list-pager">
+                <button type="button" data-pager-action="first">First</button>
+                <button type="button" data-pager-action="previous"
+                        >&lt;&lt;</button>
+
+                <span class="oe-pager-first">1</span>
+                to <span class="oe-pager-last">1</span>
+                of <span class="oe-pager-total">1</span>
+
+                <button type="button" data-pager-action="next">&gt;&gt;</button>
+                <button type="button" data-pager-action="last">Last</button>
             </th>
         </tr>
-        <tr>
+        <tr t-if="options.header">
             <th t-if="options.selectable"/>
             <t t-foreach="columns" t-as="column">
                 <th t-if="column.invisible !== '1'">
@@ -197,27 +196,36 @@
             <th t-if="options.deletable"/>
         </tr>
     </thead>
-</t>
+</table>
 <t t-name="ListView.rows" t-foreach="rows" t-as="row">
-    <t t-call="ListView.row"/>
+    <t t-call="ListView.row">
+        <t t-set="style" t-value="null"/>
+        <t-if test="row.color">
+            <t t-set="style" t-value="'color: ' + row.color"/>
+        </t-if>
+    </t>
 </t>
-<tr t-name="ListView.row">
+<tr t-name="ListView.row" t-att-style="style"
+    t-att-class="row_parity">
     <th t-if="options.selectable" class="oe-record-selector">
         <input type="checkbox"/>
     </th>
     <t t-foreach="columns" t-as="column">
-        <!-- TODO: handle attrs -->
-        <td t-if="column.invisible !== '1'" t-att-title="column.help">
-            <t t-set="is_button" t-value="column.tag === 'button'"/>
-            <!-- TODO: get correct widget from form -->
-            <t t-if="!is_button and row[column.id]">
-                <t t-esc="row[column.id]"/>
+        <t t-set="attrs" t-value="column.attrs_for(row.data)"/>
+        <td t-if="column.invisible !== '1'" t-att-title="column.help"
+            class="oe-field-cell">
+            <t t-if="!attrs.invisible">
+                <t t-set="is_button" t-value="column.tag === 'button'"/>
+                <!-- TODO: get correct widget from form -->
+                <t t-if="!is_button and row['data'][column.id].value">
+                    <t t-esc="row['data'][column.id].value"/>
+                </t>
+                <button type="button" t-att-title="column.help"
+                        t-if="is_button">
+                    <img t-att-src="'/base/static/src/img/icons/' + column.icon + '.png'"
+                         t-att-alt="column.string"/>
+                </button>
             </t>
-            <button type="button" t-att-title="column.help"
-                    t-if="is_button">
-                <img t-att-src="'/base/static/src/img/icons/' + column.icon + '.png'"
-                     t-att-alt="column.string"/>
-            </button>
         </td>
     </t>
     <td t-if="options.deletable" class='oe-record-delete'>

=== added file 'addons/base/static/test/form.js'
--- addons/base/static/test/form.js	1970-01-01 00:00:00 +0000
+++ addons/base/static/test/form.js	2011-04-12 15:05:50 +0000
@@ -0,0 +1,56 @@
+$(document).ready(function () {
+    var openerp;
+    module("form.widget", {
+        setup: function () {
+            openerp = window.openerp.init(true);
+            window.openerp.base.chrome(openerp);
+            // views loader stuff
+            window.openerp.base.data(openerp);
+            window.openerp.base.views(openerp);
+            window.openerp.base.form(openerp);
+        }
+    });
+    test("compute_domain", function () {
+        var fields = {
+            'a': {value: 3},
+            'group_method': {value: 'line'},
+            'select1': {value: 'day'},
+            'rrule_type': {value: 'monthly'}
+        };
+        ok(openerp.base.form.compute_domain(
+            [['a', '=', 3]], fields));
+        ok(openerp.base.form.compute_domain(
+            [['group_method','!=','count']], fields));
+        ok(openerp.base.form.compute_domain(
+            [['select1','=','day'], ['rrule_type','=','monthly']], fields));
+    });
+    test("compute_domain or", function () {
+        var base = {
+            'section_id': {value: null},
+            'user_id': {value: null},
+            'member_ids': {value: null}
+        };
+
+        var domain = ['|', ['section_id', '=', 42],
+                      '|', ['user_id','=',3],
+                           ['member_ids', 'in', [3]]];
+
+        ok(openerp.base.form.compute_domain(domain, _.extend(
+            {}, base, {'section_id': {value: 42}})));
+        ok(openerp.base.form.compute_domain(domain, _.extend(
+            {}, base, {'user_id': {value: 3}})));
+
+        ok(openerp.base.form.compute_domain(domain, _.extend(
+            {}, base, {'member_ids': {value: 3}})));
+    });
+    test("compute_domain not", function () {
+        var fields = {
+            'a': {value: 5},
+            'group_method': {value: 'line'}
+        };
+        ok(openerp.base.form.compute_domain(
+            ['!', ['a', '=', 3]], fields));
+        ok(openerp.base.form.compute_domain(
+            ['!', ['group_method','=','count']], fields));
+    });
+});

=== modified file 'addons/base/static/test/list.js'
--- addons/base/static/test/list.js	2011-04-07 15:35:27 +0000
+++ addons/base/static/test/list.js	2011-04-12 15:05:50 +0000
@@ -24,7 +24,9 @@
             openerp = window.openerp.init(true);
             window.openerp.base.chrome(openerp);
             // views loader stuff
+            window.openerp.base.data(openerp);
             window.openerp.base.views(openerp);
+            window.openerp.base.form(openerp);
             window.openerp.base.list(openerp);
         }
     });
@@ -36,7 +38,11 @@
 
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{}, {}, {}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}}
+        ]).then(function () {
             ok(are(listview.$element.find('tbody th'),
                    '.oe-record-selector'));
             ok(are(listview.$element.find('tbody th input'),
@@ -52,7 +58,11 @@
 
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{}, {}, {}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}}
+        ]).then(function () {
             equal(listview.$element.find('tbody th').length, 0);
             start();
         });
@@ -62,7 +72,11 @@
                 {}, null, 'qunit-fixture', {model: null});
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: 1}}},
+                {data: {id: {value: 2}}},
+                {data: {id: {value: 3}}}
+        ]).then(function () {
             listview.$element.find('tbody th input:eq(2)')
                              .attr('checked', true);
             deepEqual(listview.get_selection(), [3]);
@@ -78,7 +92,11 @@
 
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}},
+                {data: {id: {value: null}}}
+        ]).then(function () {
             equal(
                 listview.$element.find('tbody tr td.oe-record-delete button').length,
                 3);
@@ -95,7 +113,11 @@
 
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: 1}}},
+                {data: {id: {value: 2}}},
+                {data: {id: {value: 3}}}
+        ]).then(function () {
             listview.$element.find('tbody td.oe-record-delete:eq(2) button').click();
             deepEqual(deleted, [3]);
             listview.$element.find('tbody td.oe-record-delete:eq(0) button').click();
@@ -112,7 +134,11 @@
 
         listview.on_loaded(fvg);
 
-        listview.do_fill_table([{id: 1}, {id: 2}, {id: 3}]).then(function () {
+        listview.do_fill_table([
+                {data: {id: {value: 1}}},
+                {data: {id: {value: 2}}},
+                {data: {id: {value: 3}}}
+        ]).then(function () {
             listview.$element.find('tbody th input:eq(2)')
                              .attr('checked', true);
             listview.$element.find('tbody th input:eq(1)')

=== modified file 'addons/base/static/test/test.html'
--- addons/base/static/test/test.html	2011-04-07 12:12:22 +0000
+++ addons/base/static/test/test.html	2011-04-12 15:05:50 +0000
@@ -21,6 +21,7 @@
     <script src="/base/static/src/js/data.js"></script>
     <script src="/base/static/src/js/views.js"></script>
     <script src="/base/static/src/js/search.js"></script>
+    <script src="/base/static/src/js/form.js"></script>
     <script src="/base/static/src/js/list.js"></script>
     <script type="text/javascript">
         QWeb.add_template('/base/static/src/xml/base.xml');
@@ -36,5 +37,6 @@
     </body>
     <script type="text/javascript" src="/base/static/test/registry.js"></script>
     <script type="text/javascript" src="/base/static/test/search-date.js"></script>
+    <script type="text/javascript" src="/base/static/test/form.js"></script>
     <script type="text/javascript" src="/base/static/test/list.js"></script>
 </html>

=== modified file 'addons/base/test/test_dataset.py'
--- addons/base/test/test_dataset.py	2011-03-23 12:08:06 +0000
+++ addons/base/test/test_dataset.py	2011-04-12 15:05:50 +0000
@@ -14,14 +14,15 @@
         self.search.return_value = []
         self.read.return_value = []
 
-        self.assertFalse(self.dataset.do_find(self.request, 'fake.model'))
-        self.read.assert_called_once_with([], False)
+        self.assertFalse(self.dataset.do_search_read(self.request, 'fake.model'))
+        self.read.assert_called_once_with([], False, self.request.context)
 
     def test_regular_find(self):
         self.search.return_value = [1, 2, 3]
 
-        self.dataset.do_find(self.request, 'fake.model')
-        self.read.assert_called_once_with([1, 2, 3], False)
+        self.dataset.do_search_read(self.request, 'fake.model')
+        self.read.assert_called_once_with([1, 2, 3], False,
+                                          self.request.context)
 
     def test_ids_shortcut(self):
         self.search.return_value = [1, 2, 3]
@@ -32,7 +33,7 @@
         ]
 
         self.assertEqual(
-            self.dataset.do_find(self.request, 'fake.model', ['id']),
+            self.dataset.do_search_read(self.request, 'fake.model', ['id']),
             [{'id': 1}, {'id': 2}, {'id': 3}])
         self.assertFalse(self.read.called)
 
@@ -46,7 +47,7 @@
         result = self.dataset.do_get(
             self.request, 'fake.model', [3, 2, 1])
         self.read.assert_called_once_with(
-            [3, 2, 1])
+            [3, 2, 1], False)
         self.assertFalse(self.search.called)
 
         self.assertEqual(

=== modified file 'addons/base/test/test_view.py'
--- addons/base/test/test_view.py	2011-04-01 09:48:35 +0000
+++ addons/base/test/test_view.py	2011-04-12 15:05:50 +0000
@@ -1,34 +1,23 @@
+import copy
 import xml.etree.ElementTree
 import mock
 
 import unittest2
+import simplejson
 
 import base.controllers.main
 import openerpweb.nonliterals
 import openerpweb.openerpweb
 
+def field_attrs(fields_view_get, fieldname):
+    (field,) =  filter(lambda f: f['attrs'].get('name') == fieldname,
+                       fields_view_get['arch']['children'])
+    return field['attrs']
+
 #noinspection PyCompatibility
-class ViewTest(unittest2.TestCase):
+class DomainsAndContextsTest(unittest2.TestCase):
     def setUp(self):
         self.view = base.controllers.main.View()
-    def test_identity(self):
-        base_view = """
-            <form string="Title">
-                <group>
-                    <field name="some_field"/>
-                    <field name="some_other_field"/>
-                </group>
-                <field name="stuff"/>
-            </form>
-        """
-
-        pristine = xml.etree.ElementTree.fromstring(base_view)
-        transformed = self.view.transform_view(base_view, None)
-
-        self.assertEqual(
-             xml.etree.ElementTree.tostring(transformed),
-             xml.etree.ElementTree.tostring(pristine)
-        )
 
     def test_convert_literal_domain(self):
         e = xml.etree.ElementTree.Element(
@@ -115,3 +104,137 @@
             openerpweb.nonliterals.Context(
                 session, key=e.get('context').key).get_context_string(),
             context_string)
+
+class AttrsNormalizationTest(unittest2.TestCase):
+    def setUp(self):
+        self.view = base.controllers.main.View()
+
+    def test_identity(self):
+        base_view = """
+            <form string="Title">
+                <group>
+                    <field name="some_field"/>
+                    <field name="some_other_field"/>
+                </group>
+                <field name="stuff"/>
+            </form>
+        """
+
+        pristine = xml.etree.ElementTree.fromstring(base_view)
+        transformed = self.view.transform_view(base_view, None)
+
+        self.assertEqual(
+             xml.etree.ElementTree.tostring(transformed),
+             xml.etree.ElementTree.tostring(pristine)
+        )
+    def test_transform_states(self):
+        element = xml.etree.ElementTree.Element(
+            'field', states="open,closed")
+        self.view.normalize_attrs(element, {})
+
+        self.assertIsNone(element.get('states'))
+        self.assertEqual(
+            simplejson.loads(element.get('attrs')),
+            {'invisible': [['state', 'not in', ['open', 'closed']]]})
+
+    def test_transform_invisible(self):
+        element = xml.etree.ElementTree.Element(
+            'field', invisible="context.get('invisible_country', False)")
+
+        empty_context = copy.deepcopy(element)
+        self.view.normalize_attrs(empty_context, {})
+        self.assertEqual(empty_context.get('invisible'), None)
+
+        full_context = copy.deepcopy(element)
+        self.view.normalize_attrs(full_context, {'invisible_country': True})
+        self.assertEqual(full_context.get('invisible'), '1')
+
+    def test_transform_invisible_list_column(self):
+        req = mock.Mock()
+        req.context =  {'set_editable':True, 'set_visible':True,
+                        'gtd_visible':True, 'user_invisible':True}
+        req.session.evaluation_context = \
+            openerpweb.openerpweb.OpenERPSession().evaluation_context
+        req.session.model('project.task').fields_view_get.return_value = {
+            'arch': '''
+            <tree colors="grey:state in ('cancelled','done');blue:state == 'pending';red:date_deadline and (date_deadline&lt;current_date) and (state in ('draft','pending','open'))" string="Tasks">
+                <field name="sequence" invisible="not context.get('seq_visible', False)"/>
+                <field name="user_id" invisible="context.get('user_invisible', False)"/>
+                <field name="delegated_user_id" invisible="context.get('show_delegated', True)"/>
+                <field name="total_hours" invisible="1"/>
+                <field name="date_deadline" invisible="context.get('deadline_visible',True)"/>
+                <field name="type_id" invisible="context.get('set_visible',False)"/>
+            </tree>
+        '''}
+        parsed_view = base.controllers.main.View().fields_view_get(
+            req, 'project.task', 42, 'tree')
+
+        self.assertTrue(field_attrs(parsed_view, 'sequence')['invisible'])
+        self.assertTrue(field_attrs(parsed_view, 'user_id')['invisible'])
+        self.assertTrue(
+            field_attrs(parsed_view, 'delegated_user_id')['invisible'])
+        self.assertTrue(field_attrs(parsed_view, 'total_hours')['invisible'])
+        self.assertTrue(
+            field_attrs(parsed_view, 'date_deadline')['invisible'])
+        self.assertTrue(field_attrs(parsed_view, 'type_id')['invisible'])
+
+class ListViewTest(unittest2.TestCase):
+    def setUp(self):
+        self.view = base.controllers.main.ListView()
+        self.request = mock.Mock()
+        self.request.context = {'set_editable': True}
+    def test_no_editable_editable_context(self):
+        self.request.session.model('fake').fields_view_get.return_value = \
+            {'arch': '<tree><field name="foo"/></tree>'}
+        view = self.view.fields_view_get(self.request, 'fake', False)
+
+        self.assertEqual(view['arch']['attrs']['editable'],
+                         'bottom')
+    def test_editable_top_editable_context(self):
+        self.request.session.model('fake').fields_view_get.return_value = \
+            {'arch': '<tree editable="top"><field name="foo"/></tree>'}
+        view = self.view.fields_view_get(self.request, 'fake', False)
+
+        self.assertEqual(view['arch']['attrs']['editable'],
+                         'top')
+
+    def test_editable_bottom_editable_context(self):
+        self.request.session.model('fake').fields_view_get.return_value = \
+            {'arch': '<tree editable="bottom"><field name="foo"/></tree>'}
+        view = self.view.fields_view_get(self.request, 'fake', False)
+
+        self.assertEqual(view['arch']['attrs']['editable'],
+                         'bottom')
+
+    def test_color_nocolor(self):
+        self.assertEqual(
+            self.view.process_colors(
+                {'arch': {'attrs': {}, 'children': []}}, {}, {}),
+            None)
+    def test_color_literal(self):
+        self.assertEqual(
+            self.view.process_colors(
+                {'arch': {'attrs': {'colors': 'black:1'}}, 'children': []},
+                {}, {}),
+            'black')
+    def test_color_miss(self):
+        self.assertEqual(
+            self.view.process_colors(
+                {'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('pending')"}},
+                 'children': []
+                }, {'state': 'open'}, {}),
+            None)
+    def test_color_compute(self):
+        self.assertEqual(
+            self.view.process_colors(
+                {'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('pending')"}},
+                 'children': []
+                }, {'state': 'done'}, {}),
+            'grey')
+    def test_color_multiple(self):
+        self.assertEqual(
+            self.view.process_colors(
+                {'arch': {'attrs': {'colors': "grey:state in ('cancelled','done');blue:state in ('done')"}},
+                 'children': []
+                }, {'state': 'done'}, {}),
+            'maroon')

=== modified file 'addons/base_calendar/controllers/main.py'
--- addons/base_calendar/controllers/main.py	2011-04-07 16:41:06 +0000
+++ addons/base_calendar/controllers/main.py	2011-04-12 15:05:50 +0000
@@ -57,7 +57,7 @@
     
     @openerpweb.jsonrequest
     def load(self, req, model, view_id):
-        fields_view = self.fields_view_get(req.session, model, view_id, 'calendar')
+        fields_view = self.fields_view_get(req, model, view_id, 'calendar')
         return {'fields_view':fields_view}
     
     def convert(self, event):
@@ -354,4 +354,3 @@
         title = title.strip()
         description = ', '.join(description).strip()
         return {'id': event['id'], 'start_date': str(DT.datetime(*starts[:6])), 'end_date': str(DT.datetime(*ends[:6])), 'text': title, 'title': description, 'color': self.colors[event[self.color_field]][-1]}
-        
\ No newline at end of file

=== modified file 'openerpweb/openerpweb.py'
--- openerpweb/openerpweb.py	2011-04-08 16:10:23 +0000
+++ openerpweb/openerpweb.py	2011-04-12 15:05:50 +0000
@@ -196,7 +196,7 @@
         if isinstance(context_to_eval, dict):
             return context_to_eval
 
-        ctx = context or {}
+        ctx = dict(context or {})
         ctx['context'] = ctx
 
         # if the domain was unpacked from JSON, it needs the current
@@ -252,7 +252,7 @@
         if isinstance(domain, list):
             return domain
 
-        ctx = context or {}
+        ctx = dict(context or {})
         ctx['context'] = ctx
 
         # if the domain was unpacked from JSON, it needs the current


Follow ups