openerp-dev-web team mailing list archive
-
openerp-dev-web team
-
Mailing list archive
-
Message #05384
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"><<</button>
-
- <span class="oe_pager_index">0</span> / <span class="oe_pager_count">0</span>
-
- <button type="button" data-pager-action="next">>></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"
+ ><<</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">>></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<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