diff --git a/src/common.js b/src/common.js index 43b8959be7..92979e3841 100644 --- a/src/common.js +++ b/src/common.js @@ -238,4 +238,722 @@ } } }); + + Sao.common.udlex = Sao.class_(Object, { + init: function(instream) { + + var Stream = Sao.class_(Object, { + init: function(stream) { + this.stream = stream.split(''); + this.i = 0; + }, + read: function(length) { + if (length === undefined) { + length = 1; + } + if (this.i >= this.stream.length) { + return null; + } + var value = this.stream + .slice(this.i, this.i + length).join(); + this.i += length; + return value; + } + }); + this.instream = new Stream(instream); + this.eof = null; + this.commenters = ''; + this.nowordchars = [':', '>', '<', '=', '!', '"', ';', '(', ')']; + this.whitespace = ' \t\r\n'; + this.whitespace_split = false; + this.quotes = '"'; + this.escape = '\\'; + this.escapedquotes = '"'; + this.state = ' '; + this.pushback = []; + this.token = ''; + }, + get_token: function() { + if (this.pushback.length > 0) { + return this.pushback.shift(); + } + var raw = this.read_token(); + return raw; + }, + read_token: function() { + var quoted = false; + var escapedstate = ' '; + while (true) { + var nextchar = this.instream.read(1); + if (this.state === null) { + this.token = ''; // past en of file + break; + } else if (this.state == ' ') { + if (!nextchar) { + this.state = null; // end of file + break; + } else if (this.whitespace.contains(nextchar)) { + if (this.token || quoted) { + break; // emit current token + } else { + continue; + } + } else if (this.commenters.contains(nextchar)) { + // TODO readline + } else if (this.escape.contains(nextchar)) { + escapedstate = 'a'; + this.state = nextchar; + } else if (this.nowordchars.indexOf(nextchar) == -1) { + this.token = nextchar; + this.state = 'a'; + } else if (this.quotes.contains(nextchar)) { + this.state = nextchar; + } else if (this.whitespace_split) { + this.token = nextchar; + this.state = 'a'; + } else { + this.token = nextchar; + if (this.token || quoted) { + break; // emit current token + } else { + continue; + } + } + } else if (this.quotes.contains(this.state)) { + quoted = true; + if (!nextchar) { // end of file + throw 'no closing quotation'; + } + if (nextchar == this.state) { + this.state = 'a'; + } else if (this.escape.contains(nextchar) && + this.escapedquotes.contains(this.state)) { + escapedstate = this.state; + this.state = nextchar; + } else { + this.token = this.token + nextchar; + } + } else if (this.escape.contains(this.state)) { + if (!nextchar) { // end of file + throw 'no escaped character'; + } + if (this.quotes.contains(escapedstate) && + (nextchar != this.state) && + (nextchar != escapedstate)) { + this.token = this.token + this.state; + } + this.token = this.token + nextchar; + this.state = escapedstate; + } else if (this.state == 'a') { + if (!nextchar) { + this.state = null; // end of file + break; + } else if (this.whitespace.contains(nextchar)) { + this.state = ' '; + if (this.token || quoted) { + break; // emit current token + } else { + continue; + } + } else if (this.commenters.contains(nextchar)) { + // TODO + } else if (this.quotes.contains(nextchar)) { + this.state = nextchar; + } else if (this.escape.contains(nextchar)) { + escapedstate = 'a'; + this.state = nextchar; + } else if ((this.nowordchars.indexOf(nextchar) == -1) || + this.quotes.contains(nextchar) || + this.whitespace_split) { + this.token = this.token + nextchar; + } else { + this.pushback.unshift(nextchar); + this.state = ' '; + if (this.token) { + break; // emit current token + } else { + continue; + } + } + } + } + var result = this.token; + this.token = ''; + if (!quoted && result === '') { + result = null; + } + return result; + }, + next: function() { + var token = this.get_token(); + if (token == this.eof) { + return null; + } + return token; + } + }); + + Sao.common.DomainParser = Sao.class_(Object, { + OPERATORS: ['!=', '<=', '>=', '=', '!', '<', '>'], + init: function(fields) { + this.fields = {}; + this.strings = {}; + for (var name in fields) { + var field = fields[name]; + if (field.searchable || (field.searchable === undefined)) { + this.fields[name] = field; + this.strings[field.string.toLowerCase()] = field; + } + } + }, + parse: function(input) { + try { + var lex = new Sao.common.udlex(input); + var tokens = []; + while (true) { + var token = lex.next(); + if (token === null) { + break; + } + tokens.push(token); + } + tokens = this.group_operator(tokens); + tokens = this.parenthesize(tokens); + tokens = this.group(tokens); + tokens = this.operatorize(tokens, 'or'); + tokens = this.operatorize(tokens, 'and'); + tokens = this.parse_clause(tokens); + return this.simplify(tokens); + } catch (e) { + if (e == 'no closing quotation') { + return this.parse(input + '"'); + } + throw e; + } + }, + string: function(domain) { + + var string = function(clause) { + if (jQuery.isEmptyObject(clause)) { + return ''; + } + var escaped; + if ((typeof clause[0] == 'string') && + ((clause[0] in this.fields) || + (clause[0] == 'rec_name'))) { + var name = clause[0]; + var operator = clause[1]; + var value = clause[2]; + if (!(name in this.fields)) { + escaped = value.replace('%%', '__'); + if (escaped.startsWith('%') && escaped.endsWith('%')) { + value = value.slice(1, -1); + } + return this.quote(value); + } + var field = this.fields[name]; + if (operator.contains('ilike')) { + escaped = value.replace('%%', '__'); + if (escaped.startsWith('%') && escaped.endsWith('%')) { + value = value.slice(1, -1); + } else if (!escaped.contains('%')) { + if (operator == 'ilike') { + operator = '='; + } else { + operator = '!'; + } + value = value.replace('%%', '%'); + } + } + var def_operator = this.default_operator(field); + if ((def_operator == operator.trim()) || + (operator.contains(def_operator) && + operator.contains('not'))) { + operator = operator.replace(def_operator, '') + .replace('not', '!').trim(); + } + if (operator.endsWith('in')) { + if (operator == 'not in') { + operator = '!'; + } else { + operator = ''; + } + } + var formatted_value = this.format_value(field, value); + if ((this.OPERATORS.indexOf(operator) >= 0) && + (['char', 'text', 'sha', 'selection'] + .indexOf(field.type) >= 0) && + (value === '')) { + formatted_value = '""'; + } + return (this.quote(field.string) + ': ' + + operator + formatted_value); + } else { + return '(' + this.string(clause) + ')'; + } + }; + string = string.bind(this); + + if (jQuery.isEmptyObject(domain)) { + return ''; + } + var nary = ' '; + if ((domain[0] == 'AND') || (domain[0] == 'OR')) { + if (domain[0] == 'OR') { + nary = ' or '; + } + domain = domain.slice(1); + } + return domain.map(string).join(nary); + }, + group_operator: function(tokens) { + var cur = tokens[0]; + var nex = null; + var result = []; + tokens.slice(1).forEach(function(nex) { + if ((nex == '=') && cur && + (this.OPERATORS.indexOf(cur + nex) >= 0)) { + result.push(cur + nex); + cur = null; + } else { + if (cur !== null) { + result.push(cur); + } + cur = nex; + } + }.bind(this)); + if (cur !== null) { + result.push(cur); + } + return result; + }, + parenthesize: function(tokens) { + var result = []; + var current = result; + var parent = []; + tokens.forEach(function(token, i) { + if (current === undefined) { + return; + } + if (token == '(') { + parent.push(current); + current = current[current.push([]) - 1]; + } else if (token == ')') { + current = parent.pop(); + } else { + current.push(token); + } + }); + return result; + }, + group: function(tokens) { + var result = []; + + var _group = function(parts) { + var result = []; + var push_result = function(part) { + result.push([part]); + }; + var i = parts.indexOf(':'); + if (i == -1) { + parts.forEach(push_result); + return result; + } + var sub_group = function(name, lvalue) { + return function(part) { + if (!jQuery.isEmptyObject(name)) { + if (!jQuery.isEmptyObject(lvalue)) { + if (part[0] !== null) { + lvalue.push(part[0]); + } + result.push(name.concat([lvalue])); + } else { + result.push(name.concat(part)); + } + name.splice(0, name.length); + } else { + result.push(part); + } + }; + }; + for (var j = 0; j < i; j++) { + var name = parts.slice(j, i).join(' '); + if (name.toLowerCase() in this.strings) { + if (!jQuery.isEmptyObject(parts.slice(0, j))) { + parts.slice(0, j).forEach(push_result); + } else { + push_result(null); + } + name = [name]; + if (((i + 1) < parts.length) && + (this.OPERATORS.indexOf(parts[i + 1]) >= 0)) { + name = name.concat([parts[i + 1]]); + i += 1; + } else { + name = name.concat([null]); + } + var lvalue = []; + while ((i + 2) < parts.length) { + if (parts[i + 2] == ';') { + lvalue.push(parts[i + 1]); + i += 2; + } else { + break; + } + } + _group(parts.slice(i + 1)).forEach( + sub_group(name, lvalue)); + if (!jQuery.isEmptyObject(name)) { + if (!jQuery.isEmptyObject(lvalue)) { + result.push(name.concat([lvalue])); + } else { + result.push(name.concat([null])); + } + } + break; + } + } + return result; + }; + _group = _group.bind(this); + + var parts = []; + tokens.forEach(function(token) { + if (token instanceof Array) { + _group(parts).forEach(function(group) { + if (!Sao.common.compare(group, [null])) { + result.push(group); + } + }); + parts = []; + result.push(this.group(token)); + } else { + parts.push(token); + } + }.bind(this)); + _group(parts).forEach(function(group) { + if (!Sao.common.compare(group, [null])) { + result.push(group); + } + }); + return result; + }, + operatorize: function(tokens, operator) { + var result = []; + operator = operator || 'or'; + tokens = jQuery.extend([], tokens); + var test = function(value) { + if (value instanceof Array) { + return Sao.common.compare(value, [operator]); + } else { + return value == operator; + } + }; + var cur = tokens.shift(); + while (test(cur)) { + cur = tokens.shift(); + } + if (cur === undefined) { + return result; + } + if (cur instanceof Array) { + cur = this.operatorize(cur, operator); + } + var nex = null; + while (!jQuery.isEmptyObject(tokens)) { + nex = tokens.shift(); + if ((nex instanceof Array) && !test(nex)) { + nex = this.operatorize(nex, operator); + } + if (test(nex)) { + nex = tokens.shift(); + while (test(nex)) { + nex = tokens.shift(); + } + if (nex instanceof Array) { + nex = this.operatorize(nex, operator); + } + if (nex !== undefined) { + cur = [operator.toUpperCase(), cur, nex]; + } else { + if (!test(cur)) { + result.push([operator.toUpperCase(), cur]); + cur = null; + } + } + nex = null; + } else { + if (!test(cur)) { + result.push(cur); + } + cur = nex; + } + } + if (jQuery.isEmptyObject(tokens)) { + if ((nex !== null) && !test(nex)) { + result.push(nex); + } else if ((cur !== null) && !test(nex)) { + result.push(cur); + } + } + return result; + }, + parse_clause: function(tokens) { + var result = []; + tokens.forEach(function(clause) { + if ((clause == 'OR') || (clause == 'AND')) { + result.push(clause); + } else if ((clause.length == 1) && + !(clause[0] instanceof Array)) { + result.push(['rec_name', 'ilike', this.likify(clause[0])]); + } else if ((clause.length == 3) && + (clause[0].toLowerCase() in this.strings)) { + var name = clause[0]; + var operator = clause[1]; + var value = clause[2]; + var field = this.strings[clause[0].toLowerCase()]; + if (operator === null) { + operator = this.default_operator(field); + } + if (value instanceof Array) { + if (operator == '!') { + operator = 'not in'; + } else { + operator = 'in'; + } + } + if (operator == '!') { + operator = this.negate_operator( + this.default_operator(field)); + } + if (operator.contains('like')) { + value = this.likify(value); + } + if (['integer', 'float', 'numeric', 'datetime', 'date', + 'time'].indexOf(field.type) >= 0) { + if (value && value.contains('..')) { + var values = value.split('..', 2); + var lvalue = this.convert_value(field, values[0]); + var rvalue = this.convert_value(field, values[1]); + result.push([ + [field.name, '>=', lvalue], + [field.name, '<', rvalue] + ]); + return; + } + } + if (value instanceof Array) { + value = value.map(function(v) { + return this.convert_value(field, v); + }.bind(this)); + } else { + value = this.convert_value(field, value); + } + result.push([field.name, operator, value]); + } else { + result.push(this.parse_clause(clause)); + } + }.bind(this)); + return result; + }, + likify: function(value) { + if (!value) { + return '%'; + } + var escaped = value.replace('%%', '__'); + if (escaped.contains('%')) { + return value; + } else { + return '%' + value + '%'; + } + }, + quote: function(value) { + if (typeof value != 'string') { + return value; + } + var tests = [':', ' ', '(', ')'].concat(this.OPERATORS); + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + if (value.contains(test)) { + return '"' + value + '"'; + } + } + return value; + }, + default_operator: function(field) { + if (['char', 'text', 'many2one', 'many2many', 'one2many'] + .indexOf(field.type) >= 0) { + return 'ilike'; + } else { + return '='; + } + }, + negate_operator: function(operator) { + switch (operator) { + case 'ilike': + return 'not ilike'; + case '=': + return '!='; + case 'in': + return 'not in'; + } + }, + convert_value: function(field, value) { + var convert_selection = function() { + if (typeof value == 'string') { + for (var i = 0; i < field.selection.length; i++) { + var selection = field.selection[i]; + var key = selection[0]; + var text = selection[1]; + if (value.toLowerCase() == text.toLowerCase()) { + return key; + } + } + } + return value; + }; + + var converts = { + 'boolean': function() { + if (typeof value == 'string') { + return ['y', 'yes', 'true', 't', '1'].some( + function(test) { + return test.toLowerCase().startsWith( + value.toLowerCase()); + }); + } else { + return Boolean(value); + } + }, + 'float': function() { + var result = Number(value); + if (isNaN(result) || value === '' || value === null) { + return null; + } else { + return result; + } + }, + 'integer': function() { + var result = parseInt(value, 10); + if (isNaN(result)) { + return null; + } else { + return result; + } + }, + 'numeric': function() { + var result = new Sao.Decimal(value); + if (isNaN(result.valueOf()) || + value === '' || value === null) { + return null; + } else { + return result; + } + }, + 'selection': convert_selection, + 'reference': convert_selection, + // TODO datetime + // TODO date + // TODO time + 'many2one': function() { + if (value === '') { + return null; + } else { + return value; + } + } + }; + var func = converts[field.type]; + if (func) { + return func(); + } else { + return value; + } + }, + format_value: function(field, value) { + var format_float = function() { + if (!value && value !== 0 && value !== new Sao.Decimal(0)) { + return ''; + } + var digit = parseInt((field.digits || [16, 2])[1], 10); + if (isNaN(digit)) { + digit = 2; + } + return value.toFixed(digit); + }; + var format_selection = function() { + for (var i = 0; i < field.selection.length; i++) { + if (field.selection[i][0] == value) { + return field.selection[i][1]; + } + } + return value || ''; + }; + + var converts = { + 'boolean': function() { + if (value) { + return 'True'; // TODO translate + } else { + return 'False'; + } + }, + 'integer': function() { + if (value || value === 0) { + return '' + parseInt(value, 10); + } else { + return ''; + } + }, + 'float': format_float, + 'numeric': format_float, + 'selection': format_selection, + 'reference': format_selection, + // TODO datetime + // TODO date + // TODO time + 'many2one': function() { + if (value === null) { + return ''; + } else { + return value; + } + } + }; + if (value instanceof Array) { + return value.map(function(v) { + return this.format_value(field, v); + }.bind(this)).join(';'); + } else { + var func = converts[field.type]; + if (func) { + return this.quote(func(value)); + } else if (value === null) { + return ''; + } else { + return this.quote(value); + } + } + }, + simplify: function(value) { + if (value instanceof Array) { + if ((value.length == 1) && (value[0] instanceof Array) && + ((value[0][0] == 'AND') || (value[0][0] == 'OR') || + (value[0][0] instanceof Array))) { + return this.simplify(value[0]); + } else if ((value.length == 2) && + ((value[0] == 'AND') || (value[0] == 'OR')) && + (value[1] instanceof Array)) { + return this.simplify(value[1]); + } else if ((value.length == 3) && + ((value[0] == 'AND') || (value[0] == 'OR')) && + (value[1] instanceof Array) && + (value[0] == value[1][0])) { + value = this.simplify(value[1]).concat([value[2]]); + } + return value.map(this.simplify.bind(this)); + } + return value; + } + }); }()); diff --git a/src/screen.js b/src/screen.js index 2cab4969bc..052a46b79e 100644 --- a/src/screen.js +++ b/src/screen.js @@ -66,6 +66,9 @@ this.el.append(this.content_box); } }, + set_text: function(value) { + this.search_entry.val(value); + }, search_prev: function() { this.screen.search_prev(this.search_entry.val()); }, @@ -136,6 +139,8 @@ } else { this.row_activate = attributes.row_activate; } + this.fields_view_tree = null; + this.domain_parser = null; }, load_next_view: function() { if (this.view_to_load) { @@ -158,7 +163,11 @@ var arch = view.arch; var fields = view.fields; var xml_view = jQuery(jQuery.parseXML(arch)); - // TODO loading lazy/eager + + if (xml_view.children().prop('tagName') == 'tree') { + this.fields_view_tree = view; + } + var loading = 'eager'; if (xml_view.children().prop('tagName') == 'form') { loading = 'lazy'; @@ -209,11 +218,23 @@ }, search_filter: function(search_string) { var domain = []; - // TODO domain parser - if (domain.length && this.attributes.domain) { - domain.unshift('AND'); - domain.push(this.attributes.domain); + if (this.domain_parser && !this.parent) { + if (search_string || search_string === '') { + domain = this.domain_parser.parse(search_string); + } else { + domain = this.attributes.search_value; + } + this.screen_container.set_text( + this.domain_parser.string(domain)); + } else { + domain = [['id', 'in', this.group.map(function(r) { + return r.id; + })]]; + } + + if (!jQuery.isEmptyObject(domain) && this.attributes.domain) { + domain = ['AND', domain, this.attributes.domain]; } else domain = this.attributes.domain || []; @@ -423,12 +444,87 @@ }, search_active: function(active) { if (active && !this.parent) { + if (!this.fields_view_tree) { + this.model.execute('fields_view_get', + [false, 'tree'], this.context) + .then(function(view) { + this.fields_view_tree = view; + this.search_active(active); + }.bind(this)); + return; + } + if (!this.domain_parser) { + var fields = jQuery.extend({}, + this.fields_view_tree.fields); + + var set_selection = function(props) { + return function(selection) { + props.selection = selection; + }; + }; + for (var name in fields) { + if (!fields.hasOwnProperty(name)) { + continue; + } + var props = fields[name]; + if ((props.type != 'selection') && + (props.type != 'reference')) { + continue; + } + if (props.selection instanceof Array) { + continue; + } + this.get_selection(props).then(set_selection); + } + + // Filter only fields in XML view + var xml_view = jQuery(jQuery.parseXML( + this.fields_view_tree.arch)); + var xml_fields = xml_view.find('tree').children() + .filter(function(node) { + return node.tagName == 'field'; + }).map(function(node) { + return node.getAttribute('name'); + }); + var dom_fields = {}; + xml_fields.each(function(name) { + dom_fields[name] = fields[name]; + }); + if (!('id' in fields)) { + fields.id = { + 'string': 'ID', // TODO translate + 'name': 'id', + 'type': 'integer' + }; + } + this.domain_parser = new Sao.common.DomainParser(fields); + } this.screen_container.set_screen(this); this.screen_container.show_filter(); } else { this.screen_container.hide_filter(); } }, + get_selection: function(props) { + var prm; + var change_with = props.selection_change_with; + if (change_with) { + var values = {}; + change_with.forEach(function(p) { + values[p] = null; + }); + prm = this.model.execute(props.selection, + [values]); + } else { + prm = this.model.execute(props.selection, + []); + } + return prm.then(function(selection) { + return selection.sort(function(a, b) { + return a[1].localeCompare(b[1]); + }); + }); + }, search_prev: function(search_string) { this.offset -= this.limit; this.search_filter(search_string); diff --git a/tests/sao.js b/tests/sao.js index ba03f8dfca..5525247e48 100644 --- a/tests/sao.js +++ b/tests/sao.js @@ -778,6 +778,432 @@ new Date(2010, 1, 22, 10, 30, 20, 2).valueOf()); }); + QUnit.test('DomainParser.group_operator', function() { + var parser = new Sao.common.DomainParser(); + QUnit.ok(Sao.common.compare(parser.group_operator(['a', '>', '=']), + ['a', '>=']), "group_operator(['a', '>', '='])"); + QUnit.ok(Sao.common.compare(parser.group_operator(['>', '=', 'b']), + ['>=', 'b']), "group_operator(['>', '=', 'b'])"); + QUnit.ok(Sao.common.compare(parser.group_operator(['a', '=', 'b']), + ['a', '=', 'b']), "group_operator(['a', '=', 'b'])"); + QUnit.ok(Sao.common.compare(parser.group_operator(['a', '>', '=', 'b']), + ['a', '>=', 'b']), "group_operator(['a', '>', '=', 'b'])"); + QUnit.ok(Sao.common.compare(parser.group_operator(['a', '>', '=', '=']), + ['a', '>=', '=']), "group_operator(['a', '>', '=', '='])"); + }); + + QUnit.test('DomainParser.parenthesize', function() { + var parser = new Sao.common.DomainParser(); + [ + [['a'], ['a']], + [['a', 'b'], ['a', 'b']], + [['(', 'a', ')'], [['a']]], + [['a', 'b', '(', 'c', '(', 'd', 'e', ')', 'f', ')', 'g'], + ['a', 'b', ['c', ['d', 'e'], 'f'], 'g']], + [['a', 'b', '(', 'c'], ['a', 'b', ['c']]], + [['a', 'b', '(', 'c', '(', 'd', 'e', ')', 'f'], + ['a', 'b', ['c', ['d', 'e'], 'f']]], + [['a', 'b', ')'], ['a', 'b']], + [['a', 'b', ')', 'c', ')', 'd)'], ['a', 'b']] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + QUnit.ok(Sao.common.compare(parser.parenthesize(value), result), + 'parenthesize(' + JSON.stringify(value) + ')'); + }); + }); + + QUnit.test('DomainParser.group', function() { + var parser = new Sao.common.DomainParser({ + 'name': { + 'string': 'Name' + }, + 'firstname': { + 'string': 'First Name' + }, + 'surname': { + 'string': '(Sur)Name' + } + }); + var udlex = function(input) { + var lex = new Sao.common.udlex(input); + var tokens = []; + while (true) { + var token = lex.next(); + if (token === null) { + break; + } + tokens.push(token); + } + return tokens; + }; + [ + ['Name: Doe', [['Name', null, 'Doe']]], + ['"(Sur)Name": Doe', [['(Sur)Name', null, 'Doe']]], + ['Name: Doe Name: John', [ + ['Name', null, 'Doe'], + ['Name', null, 'John'] + ]], + ['Name: Name: John', [ + ['Name', null, null], + ['Name', null, 'John'] + ]], + ['First Name: John', [['First Name', null, 'John']]], + ['Name: Doe First Name: John', [ + ['Name', null, 'Doe'], + ['First Name', null, 'John'] + ]], + ['First Name: John Name: Doe', [ + ['First Name', null, 'John'], + ['Name', null, 'Doe'] + ]], + ['First Name: John First Name: Jane', [ + ['First Name', null, 'John'], + ['First Name', null, 'Jane'] + ]], + ['Name: John Doe', [ + ['Name', null, 'John'], + ['Doe'] + ]], + ['Name: "John Doe"', [['Name', null, 'John Doe']]], + ['Name: =Doe', [['Name', '=', 'Doe']]], + ['Name: =Doe Name: >John', [ + ['Name', '=', 'Doe'], + ['Name', '>', 'John'] + ]], + ['First Name: =John First Name: =Jane', [ + ['First Name', '=', 'John'], + ['First Name', '=', 'Jane'] + ]], + ['Name: John;Jane', [['Name', null, ['John', 'Jane']]]], + ['Name: John;', [['Name', null, ['John']]]], + ['Name: John;Jane Name: Doe', [ + ['Name', null, ['John', 'Jane']], + ['Name', null, 'Doe'] + ]], + ['Name: John; Name: Doe', [ + ['Name', null, ['John']], + ['Name', null, 'Doe'] + ]], + ['Name:', [['Name', null, null]]], + ['Name: =', [['Name', '=', null]]], + ['Name: =""', [['Name', '=', '']]], + ['Name: = ""', [['Name', '=', '']]], + ['Name: = Name: Doe', [ + ['Name', '=', null], + ['Name', null, 'Doe'] + ]] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + QUnit.ok(Sao.common.compare(parser.group(udlex(value)), result), + 'group(udlex(' + JSON.stringify(value) + ')'); + }); + }); + + QUnit.test('DomainParser.operatorize', function() { + var parser = new Sao.common.DomainParser(); + var a = ['a', 'a', 'a']; + var b = ['b', 'b', 'b']; + var c = ['c', 'c', 'c']; + [ + [['a'], ['a']], + [['a', 'or', 'b'], [['OR', 'a', 'b']]], + [['a', 'or', 'b', 'or', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]], + [['a', 'b', 'or', 'c'], ['a', ['OR', 'b', 'c']]], + [['a', 'or', 'b', 'c'], [['OR', 'a', 'b'], 'c']], + [['a', ['b', 'c']], ['a', ['b', 'c']]], + [['a', ['b', 'c'], 'd'], ['a', ['b', 'c'], 'd']], + [['a', 'or', ['b', 'c']], [['OR', 'a', ['b', 'c']]]], + [['a', 'or', ['b', 'c'], 'd'], + [['OR', 'a', ['b', 'c']], 'd']], + [['a', ['b', 'c'], 'or', 'd'], + ['a', ['OR', ['b', 'c'], 'd']]], + [['a', 'or', ['b', 'or', 'c']], + [['OR', 'a', [['OR', 'b', 'c']]]]], + [['or'], []], + [['or', 'a'], ['a']], + [['a', ['or', 'b']], ['a', ['b']]], + [['a', 'or', 'or', 'b'], [['OR', 'a', 'b']]], + [['or', 'or', 'a'], ['a']], + [['or', 'or', 'a', 'b'], ['a', 'b']], + [['or', 'or', 'a', 'or', 'b'], [['OR', 'a', 'b']]], + [['a', ['b', 'or', 'c']], ['a', [['OR', 'b', 'c']]]], + [[a, [b, ['or'], c]], [a, [['OR', b, c]]]], + [['a', ['b', 'or']], ['a', [['OR', 'b']]]] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + QUnit.ok(Sao.common.compare(parser.operatorize(value), result), + 'operatorize(' + JSON.stringify(value) + ')'); + }); + }); + + QUnit.test('DomainParser.convert_value', function() { + var parser = new Sao.common.DomainParser(); + + var test_func = function(test) { + var value = test[0]; + var result = test[1]; + QUnit.strictEqual(parser.convert_value(this, value), result, + 'convert_value(' + JSON.stringify(this) + ', ' + + JSON.stringify(value) + ')'); + }; + + var field = { + 'type': 'boolean' + }; + [ + ['Y', true], + ['yes', true], + ['t', true], + ['1', true], + ['N', false], + ['False', false], + ['no', false], + ['0', false], + [null, false] + ].forEach(test_func, field); + + field = { + 'type': 'float' + }; + [ + ['1', 1.0], + ['1.5', 1.5], + ['', null], + ['test', null], + [null, null] + ].forEach(test_func, field); + + field = { + 'type': 'integer' + }; + [ + ['1', 1], + ['1.5', 1], + ['', null], + ['test', null], + [null, null] + ].forEach(test_func, field); + + field = { + 'type': 'numeric' + }; + [ + ['1', new Sao.Decimal(1)], + ['1.5', new Sao.Decimal('1.5')], + ['', null], + ['test', null], + [null, null] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + value = parser.convert_value(field, value); + if (value !== null) { + value = value.toString(); + } + if (result !== null) { + result = result.toString(); + } + QUnit.strictEqual(value, result, + 'convert_value(' + JSON.stringify(field) + ', ' + + JSON.stringify(test[0]) + ')'); + }); + + field = { + 'type': 'selection', + 'selection': [ + ['male', 'Male'], + ['female', 'Female'] + ] + }; + [ + ['Male', 'male'], + ['male', 'male'], + ['test', 'test'] + ].forEach(test_func, field); + }); + + QUnit.test('DomainParser.parse_clause', function() { + var parser = new Sao.common.DomainParser({ + 'name': { + 'string': 'Name', + 'name': 'name', + 'type': 'char' + }, + 'integer': { + 'string': 'Integer', + 'name': 'integer', + 'type': 'integer' + }, + 'selection': { + 'string': 'Selection', + 'name': 'selection', + 'type': 'selection', + 'selection': [ + ['male', 'Male'], + ['female', 'Female'] + ] + } + }); + [ + [[['John']], [['rec_name', 'ilike', '%John%']]], + [[['Name', null, null]], [['name', 'ilike', '%']]], + [[['Name', '=', null]], [['name', '=', null]]], + [[['Name', '=', '']], [['name', '=', '']]], + [[['Name', null, 'Doe']], [['name', 'ilike', '%Doe%']]], + [[['Name', '!', 'Doe']], [['name', 'not ilike', '%Doe%']]], + [[['Name', null, ['John', 'Jane']]], + [['name', 'in', ['John', 'Jane']]]], + [[['Name', '!', ['John', 'Jane']]], + [['name', 'not in', ['John', 'Jane']]]], + [[['Selection', null, ['Male', 'Female']]], + [['selection', 'in', ['male', 'female']]]], + [[['Integer', null, null]], [['integer', '=', null]]], + [[['Integer', null, '3..5']], [[ + ['integer', '>=', 3], + ['integer', '<', 5] + ]]] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + QUnit.ok(Sao.common.compare(parser.parse_clause(value), result), + 'parse_clause(' + JSON.stringify(value) + ')'); + }); + }); + + QUnit.test('DomainParser.format_value', function() { + var parser = new Sao.common.DomainParser(); + + var test_func = function(test) { + var value = test[0]; + var result = test[1]; + QUnit.strictEqual(parser.format_value(this, value), result, + 'format_value(' + JSON.stringify(this) + ', ' + + JSON.stringify(value) + ')'); + }; + + var field = { + 'type': 'boolean' + }; + [ + [true, 'True'], + [false, 'False'], + [null, 'False'] + ].forEach(test_func, field); + + field = { + 'type': 'integer' + }; + [ + [1, '1'], + [1.5, '1'], + [0, '0'], + [0.0, '0'], + [false, ''], + [null, ''] + ].forEach(test_func, field); + + field = { + 'type': 'float', + 'digits': [16, 2] + }; + [ + [1, '1.00'], + [1.5, '1.50'], + [0, '0.00'], + [0.0, '0.00'], + [false, ''], + [null, ''] + ].forEach(test_func, field); + + field = { + 'type': 'numeric', + 'digits': [16, 2] + }; + [ + [new Sao.Decimal(1), '1.00'], + [new Sao.Decimal('1.5'), '1.50'], + [new Sao.Decimal(0), '0.00'], + [new Sao.Decimal('0.0'), '0.00'], + [false, ''], + [null, ''] + ].forEach(test_func, field); + + field = { + 'type': 'selection', + 'selection': [ + ['male', 'Male'], + ['female', 'Female'] + ] + }; + [ + ['male', 'Male'], + ['test', 'test'], + [false, ''], + [null, ''] + ].forEach(test_func, field); + }); + + QUnit.test('DomainParser.string', function() { + var parser = new Sao.common.DomainParser({ + 'name': { + 'string': 'Name', + 'type': 'char' + }, + 'surname': { + 'string': '(Sur)Name', + 'type': 'char' + }, + 'date': { + 'string': 'Date', + 'type': 'date' + } + }); + + [ + [[['name', '=', 'Doe']], 'Name: =Doe'], + [[['name', '=', null]], 'Name: ='], + [[['name', '=', '']], 'Name: =""'], + [[['name', 'ilike', '%']], 'Name: '], + [[['name', 'ilike', '%Doe%']], 'Name: Doe'], + [[['name', 'ilike', 'Doe']], 'Name: =Doe'], + [[['name', 'ilike', 'Doe%']], 'Name: Doe%'], + [[['name', 'ilike', 'Doe%%']], 'Name: =Doe%'], + [[['name', 'not ilike', '%Doe%']], 'Name: !Doe'], + [[['name', 'in', ['John', 'Jane']]], 'Name: John;Jane'], + [[['name', 'not in', ['John', 'Jane']]], 'Name: !John;Jane'], + [[ + ['name', 'ilike', '%Doe%'], + ['name', 'ilike', '%Jane%'] + ], 'Name: Doe Name: Jane'], + [['AND', + ['name', 'ilike', '%Doe%'], + ['name', 'ilike', '%Jane%'] + ], 'Name: Doe Name: Jane'], + [['OR', + ['name', 'ilike', '%Doe%'], + ['name', 'ilike', '%Jane%'] + ], 'Name: Doe or Name: Jane'], + [[ + ['name', 'ilike', '%Doe%'], + ['OR', + ['name', 'ilike', '%John%'], + ['name', 'ilike', '%Jane%'] + ] + ], 'Name: Doe (Name: John or Name: Jane)'], + [[], ''], + [[['surname', 'ilike', '%Doe%']], '"(Sur)Name": Doe'] + //[[['date', '>=', new Date(2012, 10, 24)]], 'Date: >=10/24/2012'] + ].forEach(function(test) { + var value = test[0]; + var result = test[1]; + QUnit.strictEqual(parser.string(value), result, + 'string(' + JSON.stringify(value) + ')'); + }); + }); + QUnit.test('CRUD', function() { var run_tests = function() { var User = new Sao.Model('res.user');