[ADD] evaluator proof of concept: colors in list rows

bzr revid: xmo@openerp.com-20110929144036-7u2w6pf6lwkvzck0
This commit is contained in:
Xavier Morel 2011-09-29 16:40:36 +02:00
commit 3cde15fe01
8 changed files with 695 additions and 1 deletions

View File

@ -27,6 +27,7 @@
"static/lib/underscore/underscore.js",
"static/lib/underscore/underscore.string.js",
"static/lib/labjs/LAB.src.js",
"static/lib/py.parse/lib/py.js",
"static/src/js/boot.js",
"static/src/js/core.js",
"static/src/js/dates.js",

View File

@ -0,0 +1,5 @@
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
node: 87fb1b67d6a13f10a1a328104ee4d4b2c36801ec
branch: default
latesttag: 0.2
latesttagdistance: 1

View File

@ -0,0 +1 @@
Parser and evaluator of Python expressions

View File

@ -0,0 +1,14 @@
* Parser
since parsing expressions, try with a pratt parser
http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
http://effbot.org/zone/simple-top-down-parsing.htm
Evaluator
---------
* Stop busyworking trivial binary operator
* Make it *trivial* to build Python type-wrappers
* Implement Python's `data model
protocols<http://docs.python.org/reference/datamodel.html#basic-customization>`_
for *all* supported operations, optimizations can come later
* Automatically type-wrap everything (for now anyway)

View File

@ -0,0 +1,546 @@
var py = {};
(function (exports) {
var NUMBER = /^\d$/,
NAME_FIRST = /^[a-zA-Z_]$/,
NAME = /^[a-zA-Z0-9_]$/;
var create = function (o, props) {
function F() {}
F.prototype = o;
var inst = new F;
for(var name in props) {
if(!props.hasOwnProperty(name)) { continue; }
inst[name] = props[name];
}
return inst;
};
var symbols = {};
var comparators = {};
var Base = {
nud: function () { throw new Error(this.id + " undefined as prefix"); },
led: function (led) { throw new Error(this.id + " undefined as infix"); },
toString: function () {
if (this.id === '(constant)' || this.id === '(number)' || this.id === '(name)' || this.id === '(string)') {
return [this.id.slice(0, this.id.length-1), ' ', this.value, ')'].join('');
} else if (this.id === '(end)') {
return '(end)';
} else if (this.id === '(comparator)' ) {
var repr = ['(comparator', this.expressions[0]];
for (var i=0;i<this.operators.length; ++i) {
repr.push(this.operators[i], this.expressions[i+1]);
}
return repr.join(' ') + ')';
}
var out = [this.id, this.first, this.second, this.third]
.filter(function (r){return r}).join(' ');
return '(' + out + ')';
}
};
function symbol(id, bp) {
bp = bp || 0;
var s = symbols[id];
if (s) {
if (bp > s.lbp) {
s.lbp = bp;
}
return s;
}
return symbols[id] = create(Base, {
id: id,
lbp: bp
});
}
function constant(id) {
symbol(id).nud = function () {
this.id = "(constant)";
this.value = id;
return this;
};
}
function prefix(id, bp, nud) {
symbol(id).nud = nud || function () {
this.first = expression(bp);
return this
}
}
function infix(id, bp, led) {
symbol(id, bp).led = led || function (left) {
this.first = left;
this.second = expression(bp);
return this;
}
}
function infixr(id, bp) {
symbol(id, bp).led = function (left) {
this.first = left;
this.second = expression(bp - 1);
return this;
}
}
function comparator(id) {
comparators[id] = true;
var bp = 60;
infix(id, bp, function (left) {
this.id = '(comparator)';
this.operators = [id];
this.expressions = [left, expression(bp)];
while (token.id in comparators) {
this.operators.push(token.id);
advance();
this.expressions.push(
expression(bp));
}
return this;
});
}
constant('None'); constant('False'); constant('True');
symbol('(number)').nud = function () { return this; };
symbol('(name)').nud = function () { return this; };
symbol('(string)').nud = function () { return this; };
symbol('(end)');
symbol(':'); symbol(')'); symbol(']'); symbol('}'); symbol(',');
symbol('else');
symbol('lambda', 20).nud = function () {
this.first = [];
if (token.id !== ':') {
for(;;) {
if (token.id !== '(name)') {
throw new Error('Excepted an argument name');
}
this.first.push(token);
advance();
if (token.id !== ',') {
break;
}
advance(',');
}
}
advance(':');
this.second = expression();
return this;
};
infix('if', 20, function (left) {
this.first = left;
this.second = expression();
advance('else');
this.third = expression();
return this;
});
infixr('or', 30); infixr('and', 40); prefix('not', 50);
comparator('in'); comparator('not in');
comparator('is'); comparator('is not');
comparator('<'); comparator('<=');
comparator('>'); comparator('>=');
comparator('<>'); comparator('!='); comparator('==');
infix('|', 70); infix('^', 80), infix('&', 90);
infix('<<', 100); infix('>>', 100);
infix('+', 110); infix('-', 110);
infix('*', 120); infix('/', 120);
infix('//', 120), infix('%', 120);
prefix('-', 130); prefix('+', 130); prefix('~', 130);
infixr('**', 140);
infix('.', 150, function (left) {
if (token.id !== '(name)') {
throw new Error('Expected attribute name, got ', token.id);
}
this.first = left;
this.second = token;
advance();
return this;
});
symbol('(', 150).nud = function () {
this.first = [];
var comma = false;
if (token.id !== ')') {
while (true) {
if (token.id === ')') {
break;
}
this.first.push(expression());
if (token.id !== ',') {
break;
}
comma = true;
advance(',');
}
}
advance(')');
if (!this.first.length || comma) {
return this;
} else {
return this.first[0];
}
};
symbol('(').led = function (left) {
this.first = left;
this.second = [];
if (token.id !== ")") {
for(;;) {
this.second.push(expression());
if (token.id !== ',') {
break;
}
advance(',');
}
}
advance(")");
return this;
};
infix('[', 150, function (left) {
this.first = left;
this.second = expression();
advance("]");
return this;
});
symbol('[').nud = function () {
this.first = [];
if (token.id !== ']') {
for (;;) {
if (token.id === ']') {
break;
}
this.first.push(expression());
if (token.id !== ',') {
break;
}
advance(',');
}
}
advance(']');
return this;
};
symbol('{').nud = function () {
this.first = [];
if (token.id !== '}') {
for(;;) {
if (token.id === '}') {
break;
}
var key = expression();
advance(':');
var value = expression();
this.first.push([key, value]);
if (token.id !== ',') {
break;
}
advance(',');
}
}
advance('}');
return this;
};
var longops = {
'*': ['*'],
'<': ['<', '=', '>'],
'>': ['=', '>'],
'!': ['='],
'=': ['='],
'/': ['/']
};
function Tokenizer() {
this.states = ['initial'];
this.tokens = [];
}
Tokenizer.prototype = {
builder: function (empty) {
var key = this.states[0] + '_builder';
if (empty) {
var value = this[key];
delete this[key];
return value;
} else {
return this[key] = this[key] || [];
}
},
simple: function (type) {
this.tokens.push({type: type});
},
push: function (new_state) {
this.states.push(new_state);
},
pop: function () {
this.states.pop();
},
feed: function (str, index) {
var s = this.states;
return this[s[s.length - 1]](str, index);
},
initial: function (str, index) {
var character = str[index];
if (character in longops) {
var follow = longops[character];
for(var i=0, len=follow.length; i<len; ++i) {
if (str[index+1] === follow[i]) {
character += follow[i];
index++;
break;
}
}
}
if (character === ' ') {
return index+1;
} else if (character === '\0') {
this.tokens.push(symbols['(end)']);
return index + 1
} else if (character === '"' || character === "'") {
this.push('string');
return index + 1;
} else if (NUMBER.test(character)) {
this.push('number');
return index;
} else if (NAME_FIRST.test(character)) {
this.push('name');
return index;
} else if (character in symbols) {
this.tokens.push(create(symbols[character]));
return index + 1;
}
throw new Error("Tokenizing failure of <<" + str + ">> at index " + index
+ ", character [[" + character + "]]"
+ "; parsed so far: " + this.tokens);
},
string: function (str, index) {
var character = str[index];
if (character === '"' || character === "'") {
this.tokens.push(create(symbols['(string)'], {
value: this.builder(true).join('')
}));
this.pop();
return index + 1;
}
this.builder().push(character);
return index + 1;
},
number: function (str, index) {
var character = str[index];
if (!NUMBER.test(character)) {
this.tokens.push(create(symbols['(number)'], {
value: parseFloat(this.builder(true).join(''))
}));
this.pop();
return index;
}
this.builder().push(character);
return index + 1;
},
name: function (str, index) {
var character = str[index];
if (!NAME.test(character)) {
var name = this.builder(true).join('');
var symbol = symbols[name];
if (symbol) {
if (name === 'in' && this.tokens[this.tokens.length-1].id === 'not') {
symbol = symbols['not in'];
this.tokens.pop();
} else if (name === 'not' && this.tokens[this.tokens.length-1].id === 'is') {
symbol = symbols['is not'];
this.tokens.pop();
}
this.tokens.push(create(symbol));
} else {
this.tokens.push(create(symbols['(name)'], {
value: name
}));
}
this.pop();
return index;
}
this.builder().push(character);
return index + 1;
}
};
exports.tokenize = function tokenize(str) {
var index = 0,
tokenizer = new Tokenizer(str);
str += '\0';
do {
index = tokenizer.feed(str, index);
} while (index !== str.length);
return tokenizer.tokens;
};
var token, next;
function expression(rbp) {
rbp = rbp || 0;
var t = token;
token = next();
var left = t.nud();
while (rbp < token.lbp) {
t = token;
token = next();
left = t.led(left);
}
return left;
}
function advance(id) {
if (id && token.id !== id) {
throw new Error(
'Expected "' + id + '", got "' + token.id + '"');
}
token = next();
}
exports.object = create({}, {});
exports.bool = function (arg) { return !!arg; };
exports.tuple = create(exports.object, {
__contains__: function (value) {
for(var i=0, len=this.values.length; i<len; ++i) {
if (this.values[i] === value) {
return true;
}
}
return false;
},
toJSON: function () {
return this.values;
}
});
exports.list = exports.tuple;
exports.dict = create(exports.object, {
toJSON: function () {
return this.values;
}
});
exports.parse = function (toks) {
var index = 0;
token = toks[0];
next = function () { return toks[++index]; };
return expression();
};
var evaluate_operator = function (operator, a, b) {
switch (operator) {
case '==': case 'is': return a === b;
case '!=': case 'is not': return a !== b;
case '<': return a < b;
case '<=': return a <= b;
case '>': return a > b;
case '>=': return a >= b;
case 'in':
if (typeof b === 'string') {
return b.indexOf(a) !== -1;
}
return b.__contains__(a);
case 'not in':
if (typeof b === 'string') {
return b.indexOf(a) === -1;
}
return !b.__contains__(a);
}
throw new Error('SyntaxError: unknown comparator [[' + operator + ']]');
};
exports.evaluate = function (expr, context) {
switch (expr.id) {
case '(name)':
var val = context[expr.value];
if (val === undefined) {
throw new Error("NameError: name '" + expr.value + "' is not defined");
}
return val;
case '(string)':
case '(number)':
return expr.value;
case '(constant)':
if (expr.value === 'None')
return null;
else if (expr.value === 'False')
return false;
else if (expr.value === 'True')
return true;
throw new Error("SyntaxError: unknown constant '" + expr.value + "'");
case '(comparator)':
var result, left = exports.evaluate(expr.expressions[0], context);
for(var i=0; i<expr.operators.length; ++i) {
result = evaluate_operator(
expr.operators[i],
left,
left = exports.evaluate(expr.expressions[i+1], context));
if (!result) { return false; }
}
return true;
case '-':
if (expr.second) {
throw new Error('SyntaxError: binary [-] not implemented yet');
}
return -(exports.evaluate(expr.first, context));
case 'not':
return !(exports.evaluate(expr.first, context));
case 'and':
return (exports.evaluate(expr.first, context)
&& exports.evaluate(expr.second, context));
case 'or':
return (exports.evaluate(expr.first, context)
|| exports.evaluate(expr.second, context));
case '(':
if (expr.second) {
var fn = exports.evaluate(expr.first, context), args=[];
for (var jj=0; jj<expr.second.length; ++jj) {
args.push(exports.evaluate(
expr.second[jj], context));
}
return fn.apply(null, args);
}
var tuple_exprs = expr.first,
tuple_values = [];
for (var j=0, len=tuple_exprs.length; j<len; ++j) {
tuple_values.push(exports.evaluate(
tuple_exprs[j], context));
}
return create(exports.tuple, {values: tuple_values});
case '[':
if (expr.second) {
throw new Error('SyntaxError: indexing not implemented yet');
}
var list_exprs = expr.first, list_values = [];
for (var k=0; k<list_exprs.length; ++k) {
list_values.push(exports.evaluate(
list_exprs[k], context));
}
return create(exports.list, {values: list_values});
case '{':
var dict_exprs = expr.first, dict_values = {};
for(var l=0; l<dict_exprs.length; ++l) {
dict_values[exports.evaluate(dict_exprs[l][0], context)] =
exports.evaluate(dict_exprs[l][1], context);
}
return create(exports.dict, {values: dict_values});
case '.':
if (expr.second.id !== '(name)') {
throw new Error('SyntaxError: ' + expr);
}
return exports.evaluate(expr.first, context)[expr.second.value];
default:
throw new Error('SyntaxError: Unknown node [[' + expr.id + ']]');
}
};
exports.eval = function (str, context) {
return exports.evaluate(
exports.parse(
exports.tokenize(
str)),
context);
}
})(typeof exports === 'undefined' ? py : exports);

View File

@ -0,0 +1,90 @@
var py = require('../lib/py.js'),
assert = require('assert');
// Literals
assert.strictEqual(py.eval('1'), 1);
assert.strictEqual(py.eval('None'), null);
assert.strictEqual(py.eval('False'), false);
assert.strictEqual(py.eval('True'), true);
assert.strictEqual(py.eval('"somestring"'), 'somestring');
assert.strictEqual(py.eval("'somestring'"), 'somestring');
assert.deepEqual(py.eval("()").toJSON(), []);
assert.deepEqual(py.eval("[]").toJSON(), []);
assert.deepEqual(py.eval("{}").toJSON(), {});
assert.deepEqual(py.eval("(None, True, False, 0, 1, 'foo')").toJSON(),
[null, true, false, 0, 1, 'foo']);
assert.deepEqual(py.eval("[None, True, False, 0, 1, 'foo']").toJSON(),
[null, true, false, 0, 1, 'foo']);
assert.deepEqual(py.eval("{'foo': 1, foo: 2}", {foo: 'bar'}).toJSON(),
{foo: 1, bar: 2});
// Equality tests
assert.ok(py.eval(
"foo == 'foo'", {foo: 'foo'}));
// Inequality
assert.ok(py.eval(
"foo != bar", {foo: 'foo', bar: 'bar'}));
// Comparisons
assert.ok(py.eval('3 < 5'));
assert.ok(py.eval('5 >= 3'));
assert.ok(py.eval('3 >= 3'));
assert.ok(!py.eval('5 < 3'));
assert.ok(py.eval('1 < 3 < 5'));
assert.ok(py.eval('5 > 3 > 1'));
assert.ok(py.eval('1 < 3 > 2 == 2 > -2 not in (0, 1, 2)'));
// string rich comparisons
assert.ok(py.eval(
'date >= current', {date: '2010-06-08', current: '2010-06-05'}));
// Boolean operators
assert.ok(py.eval(
"foo == 'foo' or foo == 'bar'", {foo: 'bar'}));
assert.ok(py.eval(
"foo == 'foo' and bar == 'bar'", {foo: 'foo', bar: 'bar'}));
// - lazyness, second clauses NameError if not short-circuited
assert.ok(py.eval(
"foo == 'foo' or bar == 'bar'", {foo: 'foo'}));
assert.ok(!py.eval(
"foo == 'foo' and bar == 'bar'", {foo: 'bar'}));
// contains (in)
assert.ok(py.eval(
"foo in ('foo', 'bar')", {foo: 'bar'}));
assert.ok(py.eval('1 in (1, 2, 3, 4)'));
assert.ok(!py.eval('1 in (2, 3, 4)'));
assert.ok(py.eval('type in ("url",)', {type: 'url'}));
assert.ok(!py.eval('type in ("url",)', {type: 'ur'}));
assert.ok(py.eval('1 not in (2, 3, 4)'));
assert.ok(py.eval('type not in ("url",)', {type: 'ur'}));
assert.ok(py.eval(
"foo in ['foo', 'bar']", {foo: 'bar'}));
// string contains
assert.ok(py.eval('type in "view"', {type: 'view'}));
assert.ok(!py.eval('type in "view"', {type: 'bob'}));
assert.ok(py.eval('type in "url"', {type: 'ur'}));
// Literals
assert.strictEqual(py.eval('False'), false);
assert.strictEqual(py.eval('True'), true);
assert.strictEqual(py.eval('None'), null);
assert.ok(py.eval('foo == False', {foo: false}));
assert.ok(!py.eval('foo == False', {foo: true}));
// conversions
assert.strictEqual(
py.eval('bool(date_deadline)', {bool: py.bool, date_deadline: '2008'}),
true);
// getattr
assert.ok(py.eval('foo.bar', {foo: {bar: true}}));
assert.ok(!py.eval('foo.bar', {foo: {bar: false}}));
// complex expressions
assert.ok(py.eval(
"state=='pending' and not(date_deadline and (date_deadline < current_date))",
{state: 'pending', date_deadline: false}));
assert.ok(py.eval(
"state=='pending' and not(date_deadline and (date_deadline < current_date))",
{state: 'pending', date_deadline: '2010-05-08', current_date: '2010-05-08'}));;

View File

@ -49,6 +49,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
this.model = dataset.model;
this.view_id = view_id;
this.previous_colspan = null;
this.colors = null;
this.columns = [];
@ -75,6 +76,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
}
self.compute_aggregates();
});
},
/**
* Retrieves the view's number of records per page (|| section)
@ -131,6 +133,30 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
this.$element.addClass('oe-listview');
return this.reload_view(null, null, true);
},
/**
* Returns the color for the provided record in the current view (from the
* ``@colors`` attribute)
*
* @param {Record} record record for the current row
* @returns {String} CSS color declaration
*/
color_for: function (record) {
if (!this.colors) { return ''; }
var context = _.extend({}, record.attributes, {
uid: this.session.uid,
current_date: new Date().toString('yyyy-MM-dd')
// TODO: time, datetime, relativedelta
});
for(var i=0, len=this.colors.length; i<len; ++i) {
var pair = this.colors[i],
color = pair[0],
expression = pair[1];
if (py.evaluate(expression, context)) {
return 'color: ' + color + ';';
}
}
return '';
},
/**
* Called after loading the list view's description, sets up such things
* as the view table's columns, renders the table itself and hooks up the
@ -159,6 +185,16 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView#
this.fields_view = data;
this.name = "" + this.fields_view.arch.attrs.string;
if (this.fields_view.arch.attrs.colors) {
this.colors = _(this.fields_view.arch.attrs.colors.split(';')).map(
function (color_pair) {
var pair = color_pair.split(':'),
color = pair[0],
expr = pair[1];
return [color, py.parse(py.tokenize(expr))];
});
}
this.setup_columns(this.fields_view.fields, grouped);
this.$element.html(QWeb.render("ListView", this));

View File

@ -616,7 +616,8 @@
</t>
</t>
<tr t-name="ListView.row" t-att-class="row_parity"
t-att-data-id="record.get('id')">
t-att-data-id="record.get('id')"
t-att-style="view.color_for(record)">
<t t-foreach="columns" t-as="column">
<td t-if="column.meta">