[MERGE] xmo listview, bug in the form pager

bzr revid: al@openerp.com-20110413013711-vp63eigz5aadm6wc
This commit is contained in:
Antony Lesuisse 2011-04-13 03:37:11 +02:00
commit 3ac70cbecf
14 changed files with 708 additions and 199 deletions

View File

@ -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 @@ class DataSet(openerpweb.Controller):
: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 @@ class DataSet(openerpweb.Controller):
: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 @@ class DataSet(openerpweb.Controller):
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 @@ class View(openerpweb.Controller):
: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 @@ class FormView(View):
@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 @@ class ListView(View):
@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):

View File

@ -443,6 +443,7 @@ body.openerp {
padding-left: 10px;
}
.openerp .oe-searchview-render-line {
width:100%;
}
@ -462,6 +463,75 @@ body.openerp {
background: url(../img/icons/gtk-remove.png) repeat-y;
padding-left: 18px;
}
/* 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 {
@ -489,7 +559,6 @@ body.openerp {
background: #f9f9f9;
}
/* Form */
.openerp table.oe_frame td {
color: #4c4c4c;

View File

@ -108,6 +108,8 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
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', {

View File

@ -311,6 +311,64 @@ openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base
/** @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;
@ -335,69 +393,11 @@ openerp.base.form.Widget = openerp.base.Controller.extend({
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]);
this[a] = compute_domain(this.attrs[a], this.view.fields);
}
},
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;
},
update_dom: function() {
this.$element.toggle(!this.invisible);
},

View File

@ -8,7 +8,9 @@ openerp.base.ListView = openerp.base.Controller.extend(
// 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 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @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 @@ openerp.base.ListView = openerp.base.Controller.extend(
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 @@ openerp.base.ListView = openerp.base.Controller.extend(
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 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @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 @@ openerp.base.ListView = openerp.base.Controller.extend(
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 @@ openerp.base.ListView = openerp.base.Controller.extend(
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 @@ openerp.base.ListView = openerp.base.Controller.extend(
// 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 @@ openerp.base.ListView = openerp.base.Controller.extend(
// 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 @@ openerp.base.ListView = openerp.base.Controller.extend(
// 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 @@ openerp.base.ListView = openerp.base.Controller.extend(
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();
}
});

View File

@ -144,8 +144,10 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
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();
},
@ -159,6 +161,23 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
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 [];
}
});
@ -180,8 +199,6 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
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);
@ -189,10 +206,12 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
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']) {
@ -206,6 +225,28 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
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];
}
});

View File

@ -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
<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'>

View File

@ -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));
});
});

View File

@ -24,7 +24,9 @@ $(document).ready(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);
window.openerp.base.list(openerp);
}
});
@ -36,7 +38,11 @@ $(document).ready(function () {
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 @@ $(document).ready(function () {
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 @@ $(document).ready(function () {
{}, 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 @@ $(document).ready(function () {
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 @@ $(document).ready(function () {
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 @@ $(document).ready(function () {
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)')

View File

@ -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>

View File

@ -14,14 +14,15 @@ class TestDataSetController(unittest2.TestCase):
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 @@ class TestDataSetController(unittest2.TestCase):
]
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 @@ class TestDataSetController(unittest2.TestCase):
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(

View File

@ -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 @@ class ViewTest(unittest2.TestCase):
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')

View File

@ -57,7 +57,7 @@ class CalendarView(View):
@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 @@ class CalendarView(View):
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]}

View File

@ -196,7 +196,7 @@ class OpenERPSession(object):
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 @@ class OpenERPSession(object):
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