From 9fe71a5d18a9d9f765ff0315951ec8a96e0de9d4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Sep 2014 15:36:16 +0200 Subject: [PATCH 01/10] [IMP] update qweb-js tests * latest qunit * template loading (handle async) * add format tests --- .../static/lib/qweb/qweb-test-attributes.xml | 6 + addons/web/static/lib/qweb/qweb-test.js.html | 222 ++++++++++-------- addons/web/static/lib/qweb/qweb2.js | 1 - 3 files changed, 130 insertions(+), 99 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb-test-attributes.xml b/addons/web/static/lib/qweb/qweb-test-attributes.xml index 91317d5194b..d0d22b179dc 100644 --- a/addons/web/static/lib/qweb/qweb-test-attributes.xml +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -19,4 +19,10 @@
+ +
+ + +
+ diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html index 469641a1796..510702e82e5 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -1,9 +1,9 @@ - - - + + + @@ -16,9 +16,12 @@ return trim(QWeb.render(template, context)).toLowerCase(); } $(document).ready(function() { - module("Basic output tests", { + QUnit.module("Basic output tests", { setup: function () { - QWeb.add_template('qweb-test-output.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-output.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -27,20 +30,23 @@ } }); - test("Basic escaped output", function () { - equals(render('esc-literal', {}), "ok", "Render a literal string"); - equals(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); - equals(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); + QUnit.test("Basic escaped output", function (assert) { + assert.equal(render('esc-literal', {}), "ok", "Render a literal string"); + assert.equal(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); + assert.equal(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); }); - test("Basic unescaped output", function () { - equals(render('raw-literal', {}), "ok", "Render a literal string"); - equals(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); - equals(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); + QUnit.test("Basic unescaped output", function (assert) { + assert.equal(render('raw-literal', {}), "ok", "Render a literal string"); + assert.equal(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); + assert.equal(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); }); - module("Context-setting tests", { + QUnit.module("Context-setting tests", { setup: function () { - QWeb.add_template('qweb-test-set.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-set.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -48,22 +54,25 @@ QWeb.att = {}; } }); - test("Set literal value", function () { - equals(render('set-from-attribute-literal', {}), "ok", + QUnit.test("Set literal value", function (assert) { + assert.equal(render('set-from-attribute-literal', {}), "ok", "Set a literal value via @t-value"); - equals(render('set-from-body-literal', {}), "ok", + assert.equal(render('set-from-body-literal', {}), "ok", "Set a literal value via @t-set body"); }); - test("Set value looked up from context", function () { - equals(render('set-from-attribute-lookup', {value: 'ok'}), "ok", + QUnit.test("Set value looked up from context", function (assert) { + assert.equal(render('set-from-attribute-lookup', {value: 'ok'}), "ok", "Set a value looked up in context via @t-value"); - equals(render('set-from-body-lookup', {value: 'ok'}), 'ok', + assert.equal(render('set-from-body-lookup', {value: 'ok'}), 'ok', "Set a value looked up in context via @t-set body and @t-esc"); }); - module("Conditionals", { + QUnit.module("Conditionals", { setup: function () { - QWeb.add_template('qweb-test-conditionals.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-conditionals.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -71,52 +80,55 @@ QWeb.att = {}; } }); - test('Basic (single boolean) conditionals', function () { - equals(render('literal-conditional', {}), 'ok', + QUnit.test('Basic (single boolean) conditionals', function (assert) { + assert.equal(render('literal-conditional', {}), 'ok', "Test on a literal value"); - equals(render('boolean-value-conditional', {value: true}), 'ok', + assert.equal(render('boolean-value-conditional', {value: true}), 'ok', "Test on a truthy variable value"); - equals(render('boolean-value-conditional-false', {value: false}), '', + assert.equal(render('boolean-value-conditional-false', {value: false}), '', "Test on a falsy variable value"); }); - test('Boolean expressions in conditionals', function () { - equals(render('negify', {}), 'ok', + QUnit.test('Boolean expressions in conditionals', function (assert) { + assert.equal(render('negify', {}), 'ok', "Negative"); - equals(render('equality', {}), 'ok', + assert.equal(render('equality', {}), 'ok', "Equality"); - equals(render('difference', {}), 'ok', + assert.equal(render('difference', {}), 'ok', "Difference"); - equals(render('and', {}), 'ok', + assert.equal(render('and', {}), 'ok', "Boolean and"); - equals(render('and-js', {}), 'ok', + assert.equal(render('and-js', {}), 'ok', "Boolean and via manually escaped JS operator"); - equals(render('or', {}), 'ok', + assert.equal(render('or', {}), 'ok', "Boolean or"); - equals(render('or-js', {}), 'ok', + assert.equal(render('or-js', {}), 'ok', "Boolean or using JS operator"); }); - test('Comparison boolean tests in conditionals', function () { - equals(render('greater', {}), 'ok', + QUnit.test('Comparison boolean tests in conditionals', function (assert) { + assert.equal(render('greater', {}), 'ok', "Greater"); - equals(render('greater-js', {}), 'ok', + assert.equal(render('greater-js', {}), 'ok', "Greater, JS operator"); - equals(render('lower', {}), 'ok', + assert.equal(render('lower', {}), 'ok', "Lower"); - equals(render('lower-js', {}), 'ok', + assert.equal(render('lower-js', {}), 'ok', "Lower, JS operator"); - equals(render('greater-or-equal', {}), 'ok', + assert.equal(render('greater-or-equal', {}), 'ok', "Greater or Equal"); - equals(render('greater-or-equal-js', {}), 'ok', + assert.equal(render('greater-or-equal-js', {}), 'ok', "Greater or Equal, JS operator"); - equals(render('lower-or-equal', {}), 'ok', + assert.equal(render('lower-or-equal', {}), 'ok', "Lower or Equal"); - equals(render('lower-or-equal-js', {}), 'ok', + assert.equal(render('lower-or-equal-js', {}), 'ok', "Lower or Equal, JS operator"); }); - module("Attributes manipulation", { + QUnit.module("Attributes manipulation", { setup: function () { - QWeb.add_template('qweb-test-attributes.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-attributes.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -124,28 +136,42 @@ QWeb.att = {}; } }); - test('Fixed-name attributes', function () { - equals(render('fixed-literal', {}), '
', + QUnit.test('Fixed-name attributes', function (assert) { + assert.equal(render('fixed-literal', {}), '
', "Fixed name and literal attribute value"); - equals(render('fixed-variable', {value: 'ok'}), '
', + assert.equal(render('fixed-variable', {value: 'ok'}), '
', "Fixed name and variable attribute value"); }); - test('Tuple-based attributes', function () { - equals(render('tuple-literal', {}), '
', + QUnit.test('Tuple-based attributes', function (assert) { + assert.equal(render('tuple-literal', {}), '
', "Tuple-based literal attributes"); - equals(render('tuple-variable', {att: ['foo', 'bar']}), '
', + assert.equal(render('tuple-variable', {att: ['foo', 'bar']}), '
', "Tuple-based variable attributes"); }); - test('Fixed name, formatted value attributes', function () { - equals(render('format-literal', {}), '
', + QUnit.test('Fixed name, formatted value attributes', function (assert) { + assert.equal(render('format-literal', {}), '
', "Literal format"); - equals(render('format-value', {value:'a'}), '
', + assert.equal(render('format-value', {value:'a'}), '
', "Valued format"); + assert.equal( + render('format-expression', {value: 5}), + '
', + "Format strings are evaluated expressions"); + assert.equal(render('format-multiple', { + value1: 0, + value2: 1, + value3: 2, + }), + '
', + "each format string should be evaluated independently"); }); - module("Template calling (including)", { + QUnit.module("Template calling (including)", { setup: function () { - QWeb.add_template('qweb-test-call.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-call.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -153,32 +179,35 @@ QWeb.att = {}; } }); - test('Trivial call invocation', function () { - equals(render('basic-caller', {}), 'ok', + QUnit.test('Trivial call invocation', function (assert) { + assert.equal(render('basic-caller', {}), 'ok', "Direct call of a second template"); }); - test('Call invocation with body', function () { - equals(render('with-unused-body', {}), 'ok', + QUnit.test('Call invocation with body', function (assert) { + assert.equal(render('with-unused-body', {}), 'ok', "Call of a second template with body unused"); - equals(render('with-unused-setbody', {}), 'ok', + assert.equal(render('with-unused-setbody', {}), 'ok', "Call of a second template with body directives unused"); }); - test('Call invocation with body (used by callee)', function () { - equals(render('with-used-body', {}), 'ok', + QUnit.test('Call invocation with body (used by callee)', function (assert) { + assert.equal(render('with-used-body', {}), 'ok', "Call of a second template with body used"); }); - test('Call invocation with parameters set (in body)', function () { - equals(render('with-used-setbody', {}), 'ok', + QUnit.test('Call invocation with parameters set (in body)', function (assert) { + assert.equal(render('with-used-setbody', {}), 'ok', "Call of a second template with parameters"); }); - test('Call invocation in-context (via import)', function () { - equals(render('in-context-import', {}), 'ok', + QUnit.test('Call invocation in-context (via import)', function (assert) { + assert.equal(render('in-context-import', {}), 'ok', "Call with t-import (calls in current context)"); }); - module("Foreach", { + QUnit.module("Foreach", { setup: function () { - QWeb.add_template('qweb-test-foreach.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-foreach.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -187,40 +216,43 @@ } }); var seq = [4,3,2,1,0]; - test('Basic foreach repetition', function () { - equals(QWeb.render('repetition-text-content', {seq:seq}), '*****', + QUnit.test('Basic foreach repetition', function (assert) { + assert.equal(QWeb.render('repetition-text-content', {seq:seq}), '*****', "Repetition of text content via foreach"); - equals(QWeb.render('repetition-dom-content', {seq:seq}).toLowerCase(), '', + assert.equal(QWeb.render('repetition-dom-content', {seq:seq}).toLowerCase(), '', "Repetition of node content via foreach"); - equals(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '', + assert.equal(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '', "A node with a foreach repeats itself"); }); - test("Foreach scope content", function () { - equals(QWeb.render('scope-self', {seq:seq}), '43210', + QUnit.test("Foreach scope content", function (assert) { + assert.equal(QWeb.render('scope-self', {seq:seq}), '43210', "each value of the sequence is available via the sequence name"); - equals(QWeb.render('scope-value', {seq:seq}), '43210', + assert.equal(QWeb.render('scope-value', {seq:seq}), '43210', "each value of the sequence is also via the _value"); - equals(QWeb.render('scope-index', {seq:seq}), '01234', + assert.equal(QWeb.render('scope-index', {seq:seq}), '01234', "the current 0-based index is available via _index"); - equals(QWeb.render('scope-first', {seq:seq}), 'true false false false false ', + assert.equal(QWeb.render('scope-first', {seq:seq}), 'true false false false false ', "_first says whether the current item is the first of the sequence"); - equals(QWeb.render('scope-last', {seq:seq}), 'false false false false true ', + assert.equal(QWeb.render('scope-last', {seq:seq}), 'false false false false true ', "_last says whether the current item is the last of the sequence"); - equals(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ', + assert.equal(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ', "the parity (odd/even) of the current row is available via _parity"); - equals(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ', + assert.equal(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ', "the total length of the sequence is available through _size"); }); - test('Name aliasing via t-as', function () { - equals(QWeb.render('aliasing', {seq:seq}), '43210', + QUnit.test('Name aliasing via t-as', function (assert) { + assert.equal(QWeb.render('aliasing', {seq:seq}), '43210', "the inner value can be re-bound via t-as"); - equals(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ', + assert.equal(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ', "inner loop variables should be rebound as well"); }); - module("Template inheritance tests", { + QUnit.module("Template inheritance tests", { setup: function () { - QWeb.add_template('qweb-test-extend.xml'); + QUnit.stop(); + QWeb.add_template('qweb-test-extend.xml', function () { + QUnit.start(); + }); }, teardown: function () { QWeb.templates = []; @@ -229,10 +261,10 @@ } }); - test("jQuery extend", function () { - equals(render('jquery-extend', {}), '
  • 1
  • 2
  • 3
[[end]]
', + QUnit.test("jQuery extend", function (assert) { + assert.equal(render('jquery-extend', {}), '
  • 1
  • 2
  • 3
[[end]]
', "Extend template with jQuery"); - equals(render('jquery-extend-clone', {}), '
  • one
  • [[cloned template]]
', + assert.equal(render('jquery-extend-clone', {}), '
  • one
  • [[cloned template]]
', "Clone template"); }); }); @@ -240,13 +272,7 @@ -

QWeb test suite

- -

- -
-

-
    -
    test markup, will be hidden
    +
    +
    diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index 78e4fd8b544..c7fa24b8e22 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -248,7 +248,6 @@ QWeb2.Engine = (function() { } self.add_template(xDoc, callback); }); - template = this.load_xml(template, callback); } var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || []; for (var i = 0; i < ec.length; i++) { From 35f5fb46e78aef1feda4d9cfddd93bd356bdca28 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Sep 2014 16:36:19 +0200 Subject: [PATCH 02/10] [IMP] qweb-js: reimplement string interpolation compilation as a single pass --- addons/web/static/lib/qweb/qweb2.js | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index c7fa24b8e22..983a3d4187d 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -601,24 +601,26 @@ QWeb2.Element = (function() { return r; }, string_interpolation : function(s) { + var _this = this; if (!s) { return "''"; } - var regex = /^{(.*)}(.*)/, - src = s.split(/#/), - r = []; - for (var i = 0, ilen = src.length; i < ilen; i++) { - var val = src[i], - m = val.match(regex); - if (m) { - r.push("(" + this.format_expression(m[1]) + ")"); - if (m[2]) { - r.push(this.engine.tools.js_escape(m[2])); - } - } else if (!(i === 0 && val === '')) { - r.push(this.engine.tools.js_escape((i === 0 ? '' : '#') + val)); - } + function append_literal(s) { + s && r.push(_this.engine.tools.js_escape(s)); } + + var re = /#{(.*?)}/g, start = 0, r = [], m; + while (m = re.exec(s)) { + // extract literal string between previous and current match + append_literal(s.slice(start, re.lastIndex - m[0].length)); + // extract matched expression + r.push('(' + this.format_expression(m[1]) + ')'); + // update position of new matching + start = re.lastIndex; + } + // remaining text after last expression + append_literal(s.slice(start)); + return r.join(' + '); }, indent : function() { From 4fb49a67f39c5a2c1d56d8c171ab91f81f5cb307 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Sep 2014 16:42:15 +0200 Subject: [PATCH 03/10] [ADD] qweb-js: jinja-style interpolation pattern --- .../static/lib/qweb/qweb-test-attributes.xml | 13 +++++++++++++ addons/web/static/lib/qweb/qweb-test.js.html | 17 +++++++++++++++++ addons/web/static/lib/qweb/qweb2.js | 4 ++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb-test-attributes.xml b/addons/web/static/lib/qweb/qweb-test-attributes.xml index d0d22b179dc..d03c20e922f 100644 --- a/addons/web/static/lib/qweb/qweb-test-attributes.xml +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -25,4 +25,17 @@
    + + +
    + + +
    + + +
    + + +
    + diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html index 510702e82e5..b768ed9a5a8 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -165,6 +165,23 @@ '
    ', "each format string should be evaluated independently"); }); + QUnit.test('Fixed name, jinja-formatted', function (assert) { + assert.equal(render('format2-literal', {}), '
    ', + "Literal format"); + assert.equal(render('format2-value', {value:'a'}), '
    ', + "Valued format"); + assert.equal( + render('format2-expression', {value: 5}), + '
    ', + "Format strings are evaluated expressions"); + assert.equal(render('format2-multiple', { + value1: 0, + value2: 1, + value3: 2, + }), + '
    ', + "each format string should be evaluated independently"); + }); QUnit.module("Template calling (including)", { setup: function () { diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index 983a3d4187d..e4a792e856f 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -609,12 +609,12 @@ QWeb2.Element = (function() { s && r.push(_this.engine.tools.js_escape(s)); } - var re = /#{(.*?)}/g, start = 0, r = [], m; + var re = /(?:#{(.+?)}|{{(.+?)}})/g, start = 0, r = [], m; while (m = re.exec(s)) { // extract literal string between previous and current match append_literal(s.slice(start, re.lastIndex - m[0].length)); // extract matched expression - r.push('(' + this.format_expression(m[1]) + ')'); + r.push('(' + this.format_expression(m[2] || m[1]) + ')'); // update position of new matching start = re.lastIndex; } From bed6b01c53b520591e6f693dbb5c25cc5b772bbc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Sep 2014 16:55:46 +0200 Subject: [PATCH 04/10] [ADD] qweb-js: escf, rawf for parity with Python version --- .../web/static/lib/qweb/qweb-test-output.xml | 23 +++++++++++++++++++ addons/web/static/lib/qweb/qweb-test.js.html | 11 +++++++++ addons/web/static/lib/qweb/qweb2.js | 14 +++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb-test-output.xml b/addons/web/static/lib/qweb/qweb-test-output.xml index 1135ad9f251..8725bb00a8f 100644 --- a/addons/web/static/lib/qweb/qweb-test-output.xml +++ b/addons/web/static/lib/qweb/qweb-test-output.xml @@ -9,6 +9,19 @@ + + + + + + + + + + + + + @@ -20,4 +33,14 @@ + + + + + + + + + + diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html index b768ed9a5a8..e6095a6259a 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -35,11 +35,22 @@ assert.equal(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); assert.equal(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); }); + QUnit.test("Formatted escaped output", function (assert) { + assert.equal(render('escf-literal', {}), "ok", "Render a literal string"); + assert.equal(render('escf-variable', {ok: 'ok'}), "ok", "Render a string variable"); + assert.equal(render('escf-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); + assert.equal(render('escf-mix', {ok: 'ok'}), "[ok]", "Render a string with additions around the format"); + }); QUnit.test("Basic unescaped output", function (assert) { assert.equal(render('raw-literal', {}), "ok", "Render a literal string"); assert.equal(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); assert.equal(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); }); + QUnit.test("Formatted unescaped output", function (assert) { + assert.equal(render('rawf-literal', {}), "ok", "Render a literal string"); + assert.equal(render('rawf-variable', {ok: 'ok'}), "ok", "Render a string variable"); + assert.equal(render('rawf-notescaped', {ok: ''}), "", "Render a string with data not escaped"); + }); QUnit.module("Context-setting tests", { setup: function () { diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index e4a792e856f..08208fbe3a6 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -28,7 +28,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var QWeb2 = { expressions_cache: {}, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), - ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,raw,js,debug,log'.split(','), + ACTIONS_PRECEDENCE: 'foreach,if,call,set,escf,esc,rawf,raw,js,debug,log'.split(','), WORD_REPLACEMENT: { 'and': '&&', 'or': '||', @@ -739,11 +739,21 @@ QWeb2.Element = (function() { } }, compile_action_esc : function(value) { - this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));"); + this.top("r.push(context.engine.tools.html_escape(" + + this.format_expression(value) + + "));"); + }, + compile_action_escf : function (value) { + this.top("r.push(context.engine.tools.html_escape(" + + this.string_interpolation(value) + + '));'); }, compile_action_raw : function(value) { this.top("r.push(" + (this.format_expression(value)) + ");"); }, + compile_action_rawf: function (value) { + this.top('r.push(' + this.string_interpolation(value) + ');'); + }, compile_action_js : function(value) { this.top("(function(" + value + ") {"); this.bottom("})(dict);"); From 494dcbd0e3ee4370e3c4a5ac6635acf86f0d7481 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 9 Sep 2014 09:17:32 +0200 Subject: [PATCH 05/10] [IMP] qweb doc, tests * document qweb based (mostly) on JS version * convert JS qweb tests to (mostly) language-independent XML so they can be used for JS and Python implementations * add some more tests (e.g. precedence between t-value and body in t-set) * remove ``t-import`` * fix parity in foreach(dict) (and rename some variables to make array and object versions more similar) --- addons/web/doc/index.rst | 1 - addons/web/doc/qweb.rst | 529 ------------------ .../static/lib/qweb/qweb-test-attributes.xml | 49 +- addons/web/static/lib/qweb/qweb-test-call.xml | 43 +- .../lib/qweb/qweb-test-conditionals.xml | 63 +-- .../web/static/lib/qweb/qweb-test-extend.xml | 59 +- .../web/static/lib/qweb/qweb-test-foreach.xml | 55 +- .../web/static/lib/qweb/qweb-test-output.xml | 46 +- addons/web/static/lib/qweb/qweb-test-set.xml | 46 +- addons/web/static/lib/qweb/qweb-test.js.html | 322 ++--------- addons/web/static/lib/qweb/qweb2.js | 50 +- doc/glossary.rst | 9 + doc/index.rst | 2 + doc/reference/http.rst | 2 +- doc/reference/javascript.rst | 3 + doc/reference/qweb.rst | 450 ++++++++++++++- openerp/addons/base/ir/ir_ui_view.py | 2 +- 17 files changed, 730 insertions(+), 1001 deletions(-) delete mode 100644 addons/web/doc/qweb.rst diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 35cd3b23afa..511a271d30e 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -37,7 +37,6 @@ Javascript widget rpc async - qweb client_action testing diff --git a/addons/web/doc/qweb.rst b/addons/web/doc/qweb.rst deleted file mode 100644 index a8225a72fee..00000000000 --- a/addons/web/doc/qweb.rst +++ /dev/null @@ -1,529 +0,0 @@ -QWeb -==== - -QWeb is the template engine used by the OpenERP Web Client. It is an -XML-based templating language, similar to `Genshi -`_, -`Thymeleaf `_ or `Facelets -`_ with a few peculiarities: - -* It's implemented fully in javascript and rendered in the browser. -* Each template file (XML files) contains multiple templates, where - template engine usually have a 1:1 mapping between template files - and templates. -* It has special support in OpenERP Web's - :class:`~instance.web.Widget`, though it can be used outside of - OpenERP Web (and it's possible to use :class:`~instance.web.Widget` - without relying on the QWeb integration). - -The rationale behind using QWeb instead of a more popular template syntax is -that its extension mechanism is very similar to the openerp view inheritance -mechanism. Like openerp views a QWeb template is an xml tree and therefore -xpath or dom manipulations are easy to performs on it. - -Here's an example demonstrating most of the basic QWeb features: - -.. code-block:: xml - - -
    -

    -
      -
    • - - - -
    • -
    -
    - - -
    - -
    -
    -
    -
    -
    -
    - -rendered with the following context: - -.. code-block:: json - - { - "class1": "foo", - "title": "Random Title", - "items": [ - { "name": "foo", "tags": {"bar": "baz", "qux": "quux"} }, - { "name": "Lorem", "tags": { - "ipsum": "dolor", - "sit": "amet", - "consectetur": "adipiscing", - "elit": "Sed", - "hendrerit": "ullamcorper", - "ante": "id", - "vestibulum": "Lorem", - "ipsum": "dolor", - "sit": "amet" - } - } - ] - } - -will yield this section of HTML document (reformated for readability): - -.. code-block:: html - -
    -

    Random Title

    -
      -
    • - foo -
      -
      bar
      -
      baz
      -
      qux
      -
      quux
      -
      -
    • -
    • - Lorem -
      -
      ipsum
      -
      dolor
      -
      sit
      -
      amet
      -
      consectetur
      -
      adipiscing
      -
      elit
      -
      Sed
      -
      hendrerit
      -
      ullamcorper
      -
      -
    • -
    -
    - -API ---- - -While QWeb implements a number of attributes and methods for -customization and configuration, only two things are really important -to the user: - -.. js:class:: QWeb2.Engine - - The QWeb "renderer", handles most of QWeb's logic (loading, - parsing, compiling and rendering templates). - - OpenERP Web instantiates one for the user, and sets it to - ``instance.web.qweb``. It also loads all the template files of the - various modules into that QWeb instance. - - A :js:class:`QWeb2.Engine` also serves as a "template namespace". - - .. js:function:: QWeb2.Engine.render(template[, context]) - - Renders a previously loaded template to a String, using - ``context`` (if provided) to find the variables accessed - during template rendering (e.g. strings to display). - - :param String template: the name of the template to render - :param Object context: the basic namespace to use for template - rendering - :returns: String - - The engine exposes an other method which may be useful in some - cases (e.g. if you need a separate template namespace with, in - OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine` - instance so their templates don't collide with more general - "module" templates): - - .. js:function:: QWeb2.Engine.add_template(templates) - - Loads a template file (a collection of templates) in the QWeb - instance. The templates can be specified as: - - An XML string - QWeb will attempt to parse it to an XML document then load - it. - - A URL - QWeb will attempt to download the URL content, then load - the resulting XML string. - - A ``Document`` or ``Node`` - QWeb will traverse the first level of the document (the - child nodes of the provided root) and load any named - template or template override. - - :type templates: String | Document | Node - - A :js:class:`QWeb2.Engine` also exposes various attributes for - behavior customization: - - .. js:attribute:: QWeb2.Engine.prefix - - Prefix used to recognize :ref:`directives ` - during parsing. A string. By default, ``t``. - - .. js:attribute:: QWeb2.Engine.debug - - Boolean flag putting the engine in "debug mode". Normally, - QWeb intercepts any error raised during template execution. In - debug mode, it leaves all exceptions go through without - intercepting them. - - .. js:attribute:: QWeb2.Engine.jQuery - - The jQuery instance used during :ref:`template inheritance - ` processing. Defaults to - ``window.jQuery``. - - .. js:attribute:: QWeb2.Engine.preprocess_node - - A ``Function``. If present, called before compiling each DOM - node to template code. In OpenERP Web, this is used to - automatically translate text content and some attributes in - templates. Defaults to ``null``. - -.. _qweb-directives: - -Directives ----------- - -A basic QWeb template is nothing more than an XHTML document (as it -must be valid XML), which will be output as-is. But the rendering can -be customized with bits of logic called "directives". Directives are -attributes elements prefixed by :js:attr:`~QWeb2.Engine.prefix` (this -document will use the default prefix ``t``, as does OpenERP Web). - -A directive will usually control or alter the output of the element it -is set on. If no suitable element is available, the prefix itself can -be used as a "no-operation" element solely for supporting directives -(or internal content, which will be rendered). This means: - -.. code-block:: xml - - Something something - -will simply output the string "Something something" (the element -itself will be skipped and "unwrapped"): - -.. code-block:: javascript - - var e = new QWeb2.Engine(); - e.add_template('\ - Test 1\ - Test 2\ - '); - e.render('test1'); // Test 1 - e.render('test2'); // Test 2 - -.. note:: - - The conventions used in directive descriptions are the following: - - * directives are described as compound functions, potentially with - optional sections. Each section of the function name is an - attribute of the element bearing the directive. - - * a special parameter is ``BODY``, which does not have a name and - designates the content of the element. - - * special parameter types (aside from ``BODY`` which remains - untyped) are ``Name``, which designates a valid javascript - variable name, ``Expression`` which designates a valid - javascript expression, and ``Format`` which designates a - Ruby-style format string (a literal string with - ``#{Expression}`` inclusions executed and replaced by their - result) - -.. note:: - - ``Expression`` actually supports a few extensions on the - javascript syntax: because some syntactic elements of javascript - are not compatible with XML and must be escaped, text - substitutions are performed from forms which don't need to be - escaped. Thus the following "keyword operators" are available in - an ``Expression``: ``and`` (maps to ``&&``), ``or`` (maps to - ``||``), ``gt`` (maps to ``>``), ``gte`` (maps to ``>=``), ``lt`` - (maps to ``<``) and ``lte`` (maps to ``<=``). - -.. _qweb-directives-templates: - -Defining Templates -++++++++++++++++++ - -.. _qweb-directive-name: - -.. function:: t-name=name - - :param String name: an arbitrary javascript string. Each template - name is unique in a given - :js:class:`QWeb2.Engine` instance, defining a - new template with an existing name will - overwrite the previous one without warning. - - When multiple templates are related, it is - customary to use dotted names as a kind of - "namespace" e.g. ``foo`` and ``foo.bar`` which - will be used either by ``foo`` or by a - sub-widget of the widget used by ``foo``. - - Templates can only be defined as the children of the document - root. The document root's name is irrelevant (it's not checked) - but is usually ```` for simplicity. - - .. code-block:: xml - - - - - - - - :ref:`t-name ` can be used on an element with - an output as well: - - .. code-block:: xml - - -
    - -
    -
    - - which ensures the template has a single root (if a template has - multiple roots and is then passed directly to jQuery, odd things - occur). - -.. _qweb-directives-output: - -Output -++++++ - -.. _qweb-directive-esc: - -.. function:: t-esc=content - - :param Expression content: - - Evaluates, html-escapes and outputs ``content``. - -.. _qweb-directive-raw: - -.. function:: t-raw=content - - :param Expression content: - - Similar to :ref:`t-esc ` but does *not* - html-escape the result of evaluating ``content``. Should only ever - be used for known-secure content, or will be an XSS attack vector. - -.. _qweb-directive-att: - -.. function:: t-att=map - - :param Expression map: - - Evaluates ``map`` expecting an ``Object`` result, sets each - key:value pair as an attribute (and its value) on the holder - element: - - .. code-block:: xml - - - - will yield - - .. code-block:: html - - - -.. function:: t-att-ATTNAME=value - - :param Name ATTNAME: - :param Expression value: - - Evaluates ``value`` and sets it on the attribute ``ATTNAME`` on - the holder element. - - If ``value``'s result is ``undefined``, suppresses the creation of - the attribute. - -.. _qweb-directive-attf: - -.. function:: t-attf-ATTNAME=value - - :param Name ATTNAME: - :param Format value: - - Similar to :ref:`t-att-* ` but the value of - the attribute is specified via a ``Format`` instead of an - expression. Useful for specifying e.g. classes mixing literal - classes and computed ones. - -.. _qweb-directives-flow: - -Flow Control -++++++++++++ - -.. _qweb-directive-set: - -.. function:: t-set=name (t-value=value | BODY) - - :param Name name: - :param Expression value: - :param BODY: - - Creates a new binding in the template context. If ``value`` is - specified, evaluates it and sets it to the specified - ``name``. Otherwise, processes ``BODY`` and uses that instead. - -.. _qweb-directive-if: - -.. function:: t-if=condition - - :param Expression condition: - - Evaluates ``condition``, suppresses the output of the holder - element and its content of the result is falsy. - -.. _qweb-directive-foreach: - -.. function:: t-foreach=iterable [t-as=name] - - :param Expression iterable: - :param Name name: - - Evaluates ``iterable``, iterates on it and evaluates the holder - element and its body once per iteration round. - - If ``name`` is not specified, computes a ``name`` based on - ``iterable`` (by replacing non-``Name`` characters by ``_``). - - If ``iterable`` yields a ``Number``, treats it as a range from 0 - to that number (excluded). - - While iterating, :ref:`t-foreach ` adds a - number of variables in the context: - - ``#{name}`` - If iterating on an array (or a range), the current value in - the iteration. If iterating on an *object*, the current key. - ``#{name}_all`` - The collection being iterated (the array generated for a - ``Number``) - ``#{name}_value`` - The current iteration value (current item for an array, value - for the current item for an object) - ``#{name}_index`` - The 0-based index of the current iteration round. - ``#{name}_first`` - Whether the current iteration round is the first one. - ``#{name}_parity`` - ``"odd"`` if the current iteration round is odd, ``"even"`` - otherwise. ``0`` is considered even. - -.. _qweb-directive-call: - -.. function:: t-call=template [BODY] - - :param String template: - :param BODY: - - Calls the specified ``template`` and returns its result. If - ``BODY`` is specified, it is evaluated *before* calling - ``template`` and can be used to specify e.g. parameters. This - usage is similar to `call-template with with-param in XSLT - `_. - -.. _qweb-directives-inheritance: - -Template Inheritance and Extension -++++++++++++++++++++++++++++++++++ - -.. _qweb-directive-extend: - -.. function:: t-extend=template BODY - - :param String template: name of the template to extend - - Works similarly to OpenERP models: if used on its own, will alter - the specified template in-place; if used in conjunction with - :ref:`t-name ` will create a new template - using the old one as a base. - - ``BODY`` should be a sequence of :ref:`t-jquery - ` alteration directives. - - .. note:: - - The inheritance in the second form is *static*: the parent - template is copied and transformed when :ref:`t-extend - ` is called. If it is altered later (by - a :ref:`t-extend ` without a - :ref:`t-name `), these changes will *not* - appear in the "child" templates. - -.. _qweb-directive-jquery: - -.. function:: t-jquery=selector [t-operation=operation] BODY - - :param String selector: a CSS selector into the parent template - :param operation: one of ``append``, ``prepend``, ``before``, - ``after``, ``inner`` or ``replace``. - :param BODY: ``operation`` argument, or alterations to perform - - * If ``operation`` is specified, applies the selector to the - parent template to find a *context node*, then applies - ``operation`` (as a jQuery operation) to the *context node*, - passing ``BODY`` as parameter. - - .. note:: - - ``replace`` maps to jQuery's `replaceWith(newContent) - `_, ``inner`` maps to - `html(htmlString) `_. - - * If ``operation`` is not provided, ``BODY`` is evaluated as - javascript code, with the *context node* as ``this``. - - .. warning:: - - While this second form is much more powerful than the first, - it is also much harder to read and maintain and should be - avoided. It is usually possible to either avoid it or - replace it with a sequence of ``t-jquery:t-operation:``. - -Escape Hatches / debugging -++++++++++++++++++++++++++ - -.. _qweb-directive-log: - -.. function:: t-log=expression - - :param Expression expression: - - Evaluates the provided expression (in the current template - context) and logs its result via ``console.log``. - -.. _qweb-directive-debug: - -.. function:: t-debug - - Injects a debugger breakpoint (via the ``debugger;`` statement) in - the compiled template output. - -.. _qweb-directive-js: - -.. function:: t-js=context BODY - - :param Name context: - :param BODY: javascript code - - Injects the provided ``BODY`` javascript code into the compiled - template, passing it the current template context using the name - specified by ``context``. diff --git a/addons/web/static/lib/qweb/qweb-test-attributes.xml b/addons/web/static/lib/qweb/qweb-test-attributes.xml index d03c20e922f..bf6070fed08 100644 --- a/addons/web/static/lib/qweb/qweb-test-attributes.xml +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -2,40 +2,57 @@
    +
    ]]> +
    + {"value": "ok"} +
    ]]>
    +
    ]]> + -
    +
    + {"value": ["foo", "bar"]} +
    ]]> + + +
    + + {"value": {"a": 1, "b": 2, "c": 3}} +
    ]]>
    - -
    - - -
    - - -
    - +
    ]]> - -
    - - +
    - + {"value": "a"} +
    ]]> + +
    - + {"value": 5} +
    ]]> + +
    + { + "value1": 0, + "value2": 1, + "value3": 2 + } +
    + ]]> diff --git a/addons/web/static/lib/qweb/qweb-test-call.xml b/addons/web/static/lib/qweb/qweb-test-call.xml index 3f7bd60ffbc..f71aca0b2ca 100644 --- a/addons/web/static/lib/qweb/qweb-test-call.xml +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -1,30 +1,51 @@ - ok - - - + ok + + - + + ok + - WHEEE + WHEEE + ok + - + + ok + - ok + ok + ok + - + - - + ok + + + + - + + 1 - 1 + + + + + + + + ok + diff --git a/addons/web/static/lib/qweb/qweb-test-conditionals.xml b/addons/web/static/lib/qweb/qweb-test-conditionals.xml index 43d16578bfc..1d038ee5908 100644 --- a/addons/web/static/lib/qweb/qweb-test-conditionals.xml +++ b/addons/web/static/lib/qweb/qweb-test-conditionals.xml @@ -1,59 +1,18 @@ - - ok - - - ok - - - fail + + ok + {"condition": true} + ok - - ok - - - ok - - - ok - - - ok - - - ok - - - ok - - - ok + + fail + {"condition": false} + - - ok - - - ok - - - ok - - - ok - - - - ok - - - ok - - - ok - - - ok + + fail + diff --git a/addons/web/static/lib/qweb/qweb-test-extend.xml b/addons/web/static/lib/qweb/qweb-test-extend.xml index 35181eb8ee9..007caefe383 100644 --- a/addons/web/static/lib/qweb/qweb-test-extend.xml +++ b/addons/web/static/lib/qweb/qweb-test-extend.xml @@ -1,32 +1,37 @@ - -
    • one
    -
    - -
  1. 3
  2. -
    - -
  3. 2
  4. -
    - -
  5. 1
  6. -
    -
    -
    - - - this.attr('class', 'main'); + + +
    • one
    -
    - -
    -
    - - [[end]] - + +
  7. 3
  8. +
    + +
  9. 2
  10. +
    + +
  11. 1
  12. +
    +
    +
    + + this.attr('class', 'main'); + + +
    +
    + + [[end]] + +
    • 1
    • 2
    • 3
    [[end]]
    +]]>
    - -
  13. [[cloned template]]
  14. -
    + +
  15. [[cloned template]]
  16. +
    +
  17. one
  18. [[cloned template]]
  19. +]]>
    diff --git a/addons/web/static/lib/qweb/qweb-test-foreach.xml b/addons/web/static/lib/qweb/qweb-test-foreach.xml index 8e335614c39..9a1e8ecb7ba 100644 --- a/addons/web/static/lib/qweb/qweb-test-foreach.xml +++ b/addons/web/static/lib/qweb/qweb-test-foreach.xml @@ -1,17 +1,46 @@ - * - - + + +[: ] + + +[0: 3 3] +[1: 2 2] +[2: 1 1] + - - - - - - - + + +- first last () + + +- first (even) +- (odd) +- (even) +- (odd) +- last (even) + - - + + + +[: ] + + +[0: 0 0] +[1: 1 1] +[2: 2 2] + + + + + +[: - ] + + {"value": {"a": 1, "b": 2, "c": 3}} + +[0: a 1 - even] +[1: b 2 - odd] +[2: c 3 - even] + diff --git a/addons/web/static/lib/qweb/qweb-test-output.xml b/addons/web/static/lib/qweb/qweb-test-output.xml index 8725bb00a8f..f91196a86cf 100644 --- a/addons/web/static/lib/qweb/qweb-test-output.xml +++ b/addons/web/static/lib/qweb/qweb-test-output.xml @@ -3,44 +3,36 @@ + ok + - + + {"var": "ok"} + ok + - - - - - - - - - - - - - - + + "}]]> + + + ok + - + + {"var": "ok"} + ok + - - - - - - - - - - - + + "}]]> + ]]> diff --git a/addons/web/static/lib/qweb/qweb-test-set.xml b/addons/web/static/lib/qweb/qweb-test-set.xml index 6a7b4f53d23..945fd4a2a0d 100644 --- a/addons/web/static/lib/qweb/qweb-test-set.xml +++ b/addons/web/static/lib/qweb/qweb-test-set.xml @@ -1,15 +1,53 @@ - + + + + ok + + - ok + ok + + + ok + - + + + + {"value": "ok"} + + + ok + + - + + + + + + {"value": "ok"} + + + ok + + + + + + + + + + 2 + + + 1 diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html index e6095a6259a..aac579d04fe 100644 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -15,286 +15,52 @@ function render(template, context) { return trim(QWeb.render(template, context)).toLowerCase(); } + + /** + * Loads the template file, and executes all the test template in a + * qunit module $title + */ + function test(title, template) { + QUnit.module(title, { + setup: function () { + var self = this; + this.qweb = new QWeb2.Engine(); + QUnit.stop(); + this.qweb.add_template(template, function (_, doc) { + self.doc = doc; + QUnit.start(); + }) + } + }); + QUnit.test('autotest', function (assert) { + var templates = this.qweb.templates; + for (var template in templates) { + if (!templates.hasOwnProperty(template)) { continue; } + // ignore templates whose name starts with _, they're + // helpers/internal + if (/^_/.test(template)) { continue; } + + var params = this.doc.querySelector('params#' + template); + var args = params ? JSON.parse(params.textContent) : {}; + + var results = this.doc.querySelector('result#' + template); + assert.equal( + trim(this.qweb.render(template, args)), + trim(results.textContent), + template); + } + }); + } $(document).ready(function() { - QUnit.module("Basic output tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-output.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); + test("Output", 'qweb-test-output.xml'); + test("Context-setting", 'qweb-test-set.xml'); + test("Conditionals", 'qweb-test-conditionals.xml'); + test("Attributes manipulation", 'qweb-test-attributes.xml'); + test("Template calling (to the faraway pages)", + 'qweb-test-call.xml'); + test("Foreach", 'qweb-test-foreach.xml'); - QUnit.test("Basic escaped output", function (assert) { - assert.equal(render('esc-literal', {}), "ok", "Render a literal string"); - assert.equal(render('esc-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('esc-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); - }); - QUnit.test("Formatted escaped output", function (assert) { - assert.equal(render('escf-literal', {}), "ok", "Render a literal string"); - assert.equal(render('escf-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('escf-toescape', {ok: ''}), "<ok>", "Render a string with data to escape"); - assert.equal(render('escf-mix', {ok: 'ok'}), "[ok]", "Render a string with additions around the format"); - }); - QUnit.test("Basic unescaped output", function (assert) { - assert.equal(render('raw-literal', {}), "ok", "Render a literal string"); - assert.equal(render('raw-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('raw-notescaped', {ok: ''}), "", "Render a string with data not escaped"); - }); - QUnit.test("Formatted unescaped output", function (assert) { - assert.equal(render('rawf-literal', {}), "ok", "Render a literal string"); - assert.equal(render('rawf-variable', {ok: 'ok'}), "ok", "Render a string variable"); - assert.equal(render('rawf-notescaped', {ok: ''}), "", "Render a string with data not escaped"); - }); - - QUnit.module("Context-setting tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-set.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test("Set literal value", function (assert) { - assert.equal(render('set-from-attribute-literal', {}), "ok", - "Set a literal value via @t-value"); - assert.equal(render('set-from-body-literal', {}), "ok", - "Set a literal value via @t-set body"); - }); - QUnit.test("Set value looked up from context", function (assert) { - assert.equal(render('set-from-attribute-lookup', {value: 'ok'}), "ok", - "Set a value looked up in context via @t-value"); - assert.equal(render('set-from-body-lookup', {value: 'ok'}), 'ok', - "Set a value looked up in context via @t-set body and @t-esc"); - }); - - QUnit.module("Conditionals", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-conditionals.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Basic (single boolean) conditionals', function (assert) { - assert.equal(render('literal-conditional', {}), 'ok', - "Test on a literal value"); - assert.equal(render('boolean-value-conditional', {value: true}), 'ok', - "Test on a truthy variable value"); - assert.equal(render('boolean-value-conditional-false', {value: false}), '', - "Test on a falsy variable value"); - }); - QUnit.test('Boolean expressions in conditionals', function (assert) { - assert.equal(render('negify', {}), 'ok', - "Negative"); - assert.equal(render('equality', {}), 'ok', - "Equality"); - assert.equal(render('difference', {}), 'ok', - "Difference"); - assert.equal(render('and', {}), 'ok', - "Boolean and"); - assert.equal(render('and-js', {}), 'ok', - "Boolean and via manually escaped JS operator"); - assert.equal(render('or', {}), 'ok', - "Boolean or"); - assert.equal(render('or-js', {}), 'ok', - "Boolean or using JS operator"); - }); - QUnit.test('Comparison boolean tests in conditionals', function (assert) { - assert.equal(render('greater', {}), 'ok', - "Greater"); - assert.equal(render('greater-js', {}), 'ok', - "Greater, JS operator"); - assert.equal(render('lower', {}), 'ok', - "Lower"); - assert.equal(render('lower-js', {}), 'ok', - "Lower, JS operator"); - assert.equal(render('greater-or-equal', {}), 'ok', - "Greater or Equal"); - assert.equal(render('greater-or-equal-js', {}), 'ok', - "Greater or Equal, JS operator"); - assert.equal(render('lower-or-equal', {}), 'ok', - "Lower or Equal"); - assert.equal(render('lower-or-equal-js', {}), 'ok', - "Lower or Equal, JS operator"); - }); - - QUnit.module("Attributes manipulation", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-attributes.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Fixed-name attributes', function (assert) { - assert.equal(render('fixed-literal', {}), '
    ', - "Fixed name and literal attribute value"); - assert.equal(render('fixed-variable', {value: 'ok'}), '
    ', - "Fixed name and variable attribute value"); - }); - QUnit.test('Tuple-based attributes', function (assert) { - assert.equal(render('tuple-literal', {}), '
    ', - "Tuple-based literal attributes"); - assert.equal(render('tuple-variable', {att: ['foo', 'bar']}), '
    ', - "Tuple-based variable attributes"); - }); - QUnit.test('Fixed name, formatted value attributes', function (assert) { - assert.equal(render('format-literal', {}), '
    ', - "Literal format"); - assert.equal(render('format-value', {value:'a'}), '
    ', - "Valued format"); - assert.equal( - render('format-expression', {value: 5}), - '
    ', - "Format strings are evaluated expressions"); - assert.equal(render('format-multiple', { - value1: 0, - value2: 1, - value3: 2, - }), - '
    ', - "each format string should be evaluated independently"); - }); - QUnit.test('Fixed name, jinja-formatted', function (assert) { - assert.equal(render('format2-literal', {}), '
    ', - "Literal format"); - assert.equal(render('format2-value', {value:'a'}), '
    ', - "Valued format"); - assert.equal( - render('format2-expression', {value: 5}), - '
    ', - "Format strings are evaluated expressions"); - assert.equal(render('format2-multiple', { - value1: 0, - value2: 1, - value3: 2, - }), - '
    ', - "each format string should be evaluated independently"); - }); - - QUnit.module("Template calling (including)", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-call.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - QUnit.test('Trivial call invocation', function (assert) { - assert.equal(render('basic-caller', {}), 'ok', - "Direct call of a second template"); - }); - QUnit.test('Call invocation with body', function (assert) { - assert.equal(render('with-unused-body', {}), 'ok', - "Call of a second template with body unused"); - assert.equal(render('with-unused-setbody', {}), 'ok', - "Call of a second template with body directives unused"); - }); - QUnit.test('Call invocation with body (used by callee)', function (assert) { - assert.equal(render('with-used-body', {}), 'ok', - "Call of a second template with body used"); - }); - QUnit.test('Call invocation with parameters set (in body)', function (assert) { - assert.equal(render('with-used-setbody', {}), 'ok', - "Call of a second template with parameters"); - }); - QUnit.test('Call invocation in-context (via import)', function (assert) { - assert.equal(render('in-context-import', {}), 'ok', - "Call with t-import (calls in current context)"); - }); - - QUnit.module("Foreach", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-foreach.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - var seq = [4,3,2,1,0]; - QUnit.test('Basic foreach repetition', function (assert) { - assert.equal(QWeb.render('repetition-text-content', {seq:seq}), '*****', - "Repetition of text content via foreach"); - assert.equal(QWeb.render('repetition-dom-content', {seq:seq}).toLowerCase(), '', - "Repetition of node content via foreach"); - assert.equal(QWeb.render('repetition-self', {seq:seq}).toLowerCase(), '', - "A node with a foreach repeats itself"); - }); - QUnit.test("Foreach scope content", function (assert) { - assert.equal(QWeb.render('scope-self', {seq:seq}), '43210', - "each value of the sequence is available via the sequence name"); - assert.equal(QWeb.render('scope-value', {seq:seq}), '43210', - "each value of the sequence is also via the _value"); - assert.equal(QWeb.render('scope-index', {seq:seq}), '01234', - "the current 0-based index is available via _index"); - assert.equal(QWeb.render('scope-first', {seq:seq}), 'true false false false false ', - "_first says whether the current item is the first of the sequence"); - assert.equal(QWeb.render('scope-last', {seq:seq}), 'false false false false true ', - "_last says whether the current item is the last of the sequence"); - assert.equal(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ', - "the parity (odd/even) of the current row is available via _parity"); - assert.equal(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ', - "the total length of the sequence is available through _size"); - }); - QUnit.test('Name aliasing via t-as', function (assert) { - assert.equal(QWeb.render('aliasing', {seq:seq}), '43210', - "the inner value can be re-bound via t-as"); - assert.equal(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ', - "inner loop variables should be rebound as well"); - }); - - QUnit.module("Template inheritance tests", { - setup: function () { - QUnit.stop(); - QWeb.add_template('qweb-test-extend.xml', function () { - QUnit.start(); - }); - }, - teardown: function () { - QWeb.templates = []; - QWeb.tag = {}; - QWeb.att = {}; - } - }); - - QUnit.test("jQuery extend", function (assert) { - assert.equal(render('jquery-extend', {}), '
    • 1
    • 2
    • 3
    [[end]]
    ', - "Extend template with jQuery"); - assert.equal(render('jquery-extend-clone', {}), '
    • one
    • [[cloned template]]
    ', - "Clone template"); - }); + test('Template inheritance', 'qweb-test-extend.xml'); }); diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index 08208fbe3a6..e080476cec9 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -28,7 +28,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var QWeb2 = { expressions_cache: {}, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), - ACTIONS_PRECEDENCE: 'foreach,if,call,set,escf,esc,rawf,raw,js,debug,log'.split(','), + ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,raw,js,debug,log'.split(','), WORD_REPLACEMENT: { 'and': '&&', 'or': '||', @@ -139,22 +139,11 @@ var QWeb2 = { if (callback) { new_dict['__content__'] = callback(context, new_dict); } - var r = context.engine._render(template, new_dict); - if (_import) { - if (_import === '*') { - this.extend(old_dict, new_dict, ['__caller__', '__template__']); - } else { - _import = _import.split(','); - for (var i = 0, ilen = _import.length; i < ilen; i++) { - var v = _import[i]; - old_dict[v] = new_dict[v]; - } - } - } - return r; + return context.engine._render(template, new_dict); }, foreach: function(context, enu, as, old_dict, callback) { if (enu != null) { + var index, jlen, cur; var size, new_dict = this.extend({}, old_dict); new_dict[as + "_all"] = enu; var as_value = as + "_value", @@ -164,13 +153,13 @@ var QWeb2 = { as_parity = as + "_parity"; if (size = enu.length) { new_dict[as + "_size"] = size; - for (var j = 0, jlen = enu.length; j < jlen; j++) { - var cur = enu[j]; + for (index = 0, jlen = enu.length; index < jlen; index++) { + cur = enu[index]; new_dict[as_value] = cur; - new_dict[as_index] = j; - new_dict[as_first] = j === 0; - new_dict[as_last] = j + 1 === size; - new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); + new_dict[as_index] = index; + new_dict[as_first] = index === 0; + new_dict[as_last] = index + 1 === size; + new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even'); if (cur.constructor === Object) { this.extend(new_dict, cur); } @@ -184,14 +173,14 @@ var QWeb2 = { } this.foreach(context, _enu, as, old_dict, callback); } else { - var index = 0; + index = 0; for (var k in enu) { if (enu.hasOwnProperty(k)) { - var v = enu[k]; - new_dict[as_value] = v; + cur = enu[k]; + new_dict[as_value] = cur; new_dict[as_index] = index; new_dict[as_first] = index === 0; - new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); + new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even'); new_dict[as] = k; callback(context, new_dict); index += 1; @@ -704,11 +693,10 @@ QWeb2.Element = (function() { this.indent(); }, compile_action_call : function(value) { - var _import = this.actions['import'] || ''; if (this.children.length === 0) { - return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));"); + return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict));"); } else { - this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + ", function(context, dict) {"); + this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, null, function(context, dict) {"); this.bottom("}));"); this.indent(); this.top("var r = [];"); @@ -743,17 +731,9 @@ QWeb2.Element = (function() { + this.format_expression(value) + "));"); }, - compile_action_escf : function (value) { - this.top("r.push(context.engine.tools.html_escape(" - + this.string_interpolation(value) - + '));'); - }, compile_action_raw : function(value) { this.top("r.push(" + (this.format_expression(value)) + ");"); }, - compile_action_rawf: function (value) { - this.top('r.push(' + this.string_interpolation(value) + ');'); - }, compile_action_js : function(value) { this.top("(function(" + value + ") {"); this.bottom("})(dict);"); diff --git a/doc/glossary.rst b/doc/glossary.rst index af197a2d28a..d22042b1bf7 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -12,3 +12,12 @@ External identifiers are in the form :samp:`{module}.{id}` (e.g. ``account.invoice_graph``). From within a module, the :samp:`{module}.` prefix can be left out. + + format string + inspired by `jinja variables`_, format strings allow more easily + mixing literal content and computed content (expressions): content + between ``{{`` and ``}}`` is interpreted as an expression and + evaluated, other content is interpreted as literal strings and + displayed as-is + +.. _jinja variables: http://jinja.pocoo.org/docs/dev/templates/#variables diff --git a/doc/index.rst b/doc/index.rst index 5440162574a..f83680e9ebd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,3 +15,5 @@ odoo developer documentation guides reference modules + +.. todolist:: diff --git a/doc/reference/http.rst b/doc/reference/http.rst index c179691c4cb..e36ddf9e713 100644 --- a/doc/reference/http.rst +++ b/doc/reference/http.rst @@ -59,7 +59,7 @@ and defining methods decorated with :func:`~openerp.http.route`:: return stuff() To *override* a controller, :ref:`inherit ` from its -class and override relevant methods:: +class and override relevant methods, re-exposing them if necessary:: class Extension(MyController): @route() diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst index ae7b02c63a4..07f38b77fc1 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -5,6 +5,9 @@ Javascript Widgets ======= +.. qweb integration: ``template`` is an (optional) automatically rendered + template + RPC === diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index c993c0c3c22..8e8a213af44 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -1,26 +1,273 @@ +.. highlight:: xml + .. _reference/qweb: ==== QWeb ==== -Basics -====== +QWeb is the primary templating_ engine used by Odoo\ [#othertemplates]_. It +is an XML templating engine\ [#genshif]_ and used mostly to generate HTML_ +fragments and pages. + +Template directives are specified as XML attributes prefixed with ``t-``, +for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements +and other attributes being rendered directly. + +To avoid element rendering, a placeholder element ```` is also available, +which executes its directive but doesn't generate any output in and of +itself:: + + +

    Test

    +
    + +will result in:: + +

    Test

    + +if ``condition`` is true, but:: + +
    +

    Test

    +
    + +will result in:: + +
    +

    Test

    +
    .. _reference/qweb/output: data output ------------ +=========== + +QWeb has a primary output directive which automatically HTML-escape its +content limiting XSS_ risks when displaying user-provided content: ``esc``. + +``esc`` takes an expression, evaluates it and prints the content:: + +

    + +rendered with the value ``value`` set to ``42`` yields:: + +

    42

    + +There is one other output directive ``raw`` which behaves the same as +respectively ``esc`` but *does not HTML-escape its output*. It can be useful +to display separately constructed markup (e.g. from functions) or already +sanitized user-provided markup. .. _reference/qweb/conditionals: conditionals ------------- +============ + +QWeb has a conditional directive ``if``, which evaluates an expression given +as attribute value:: + +
    + +

    ok

    +
    +
    + +The element is rendered if the condition is true:: + +
    +

    ok

    +
    + +but if the condition is false it is removed from the result:: + +
    +
    + +The conditional rendering applies to the bearer of the directive, which does +not have to be ````:: + +
    +

    ok

    +
    + +will give the same results as the previous example. .. _reference/qweb/loops: loops ------ +===== + +QWeb has an iteration directive ``foreach`` which take an expression returning +the collection to iterate on, and a second parameter ``t-as`` providing the +name to use for the "current item" of the iteration:: + + +

    +
    + +will be rendered as:: + +

    1

    +

    2

    +

    3

    + +Like conditions, ``foreach`` applies to the element bearing the directive's +attribute, and + +:: + +

    + +

    + +is equivalent to the previous example. + +``foreach`` can iterate on an array (the current item will be the current +value), a mapping (the current item will be the current key) or an integer +(equivalent to iterating on an array between 0 inclusive and the provided +integer exclusive). + +In addition to the name passed via ``t-as``, ``foreach`` provides a few other +variables for various data points (``$as`` is the name passed to ``t-as``): + +:samp:`{$as}_all` + the object being iterated over +:samp:`{$as}_value` + the current iteration value, identical to ``$as`` for lists and integers, + but for mappings it provides the value (where ``$as`` provides the key) +:samp:`{$as}_index` + the current iteration index (the first item of the iteration has index 0) +:samp:`{$as}_size` + the size of the collection if it is available +:samp:`{$as}_first` + whether the current item is the first of the iteration (equivalent to + :samp:`{$as}_index == 0`) +:samp:`{$as}_last` + whether the current item is the last of the iteration (equivalent to + :samp:`{$as}_index + 1 == {$as}_size`), requires the iteratee's size be + available +:samp:`{$as}_parity` + either ``"even"`` or ``"odd"``, the parity of the current iteration round +:samp:`{$as}_even` + a boolean flag indicating that the current iteration round is on an even + index +:samp:`{$as}_odd` + a boolean flag indicating that the current iteration round is on an odd + index + +.. _reference/qweb/attributes: + +attributes +========== + +QWeb can compute attributes on-the-fly and set the result of the computation +on the output node. This is done via the ``t-att`` (attribute) directive which +exists in 3 different forms: + +:samp:`t-att-{$name}` + an attribute called ``$name`` is created, the attribute value is evaluated + and the result is set as the attribute's value:: + +
    + + will be rendered as:: + +
    +:samp:`t-attf-{$name}` + same as previous, but the parameter is a :term:`format string` + instead of just an expression, often useful to mix literal and non-literal + string (e.g. classes):: + + +
  20. +
    + + will be rendered as:: + +
  21. 1
  22. +
  23. 2
  24. +
  25. 3
  26. +:samp:`t-att=mapping` + if the parameter is a mapping, each (key, value) pair generates a new + attribute and its value:: + +
    + + will be rendered as:: + +
    +:samp:`t-att=pair` + if the parameter is a pair (tuple or array of 2 element), the first + item of the pair is the name of the attribute and the second item is the + value:: + +
    + + will be rendered as:: + +
    + +setting variables +================= + +QWeb allows creating variables from within the template, to memoize a +computation (to use it multiple times), give a piece of data a clearer name, +... + +This is done via the ``set`` directive, which takes the name of the variable +to create. The value to set can be provided in two ways: + +* a ``t-value`` attribute containing an expression, and the result of its + evaluation will be set:: + + + + + will print ``3`` +* if there is no ``t-value`` attribute, the node's body is rendered and set + as the variable's value:: + + +
  27. ok
  28. +
    + + + will generate ``<li>ok</li>`` (the content is escaped as we + used the ``esc`` directive) + + .. note:: using the result of this operation is a significant use-case for + the ``raw`` directive. + +calling sub-templates +===================== + +QWeb templates can be used for top-level rendering, but they can also be used +from within another template (to avoid duplication or give names to parts of +templates) using the ``t-call`` directive:: + + + +This calls the named template with the execution context of the parent, if +``other_template`` is defined as:: + +

    + +the call above will be rendered as ``

    `` (no content), but:: + + + + +will be rendered as ``

    1

    ``. + +However this has the problem of being visible from outside the ``t-call``. +Alternatively, content set in the body of the ``call`` directive will be +evaluated *before* calling the sub-template, and can alter a local context:: + + + + + Python ====== @@ -33,5 +280,196 @@ Bundles Javascript ========== -loading +Exclusive directives +-------------------- + +The Javascript qweb implementation provides specific directives to handle +defining and overloading/altering templates: + +defining templates +'''''''''''''''''' + +The ``t-name`` directive can only be placed at the top-level of a template +file (direct children to the document root):: + + + + + + + +It takes no other parameter, but can be used with a ```` element or any +other. With a ```` element, the ```` should have a single child. + +The template name is an arbitrary string, although when multiple templates +are related (e.g. called sub-templates) it is customary to use dot-separated +names to indicate hierarchical relationships. + +template inheritance +'''''''''''''''''''' + +Template inheritance is used to alter existing templates in-place, e.g. to +add information to templates created by an other modules. + +Template inheritance is performed via the ``t-extend`` directive which takes +the name of the template to alter as parameter. + +The alteration is then performed with any number of ``t-jquery`` +sub-directives:: + + + +
  29. new element
  30. +
    +
    + +The ``t-jquery`` directives takes a `CSS selector`_. This selector is used +on the extended template to select *context nodes* to which the specified +``t-operation`` is applied: + +``append`` + the node's body is appended at the end of the context node (after the + context node's last child) +``prepend`` + the node's body is prepended to the context node (inserted before the + context node's first child) +``before`` + the node's body is inserted right before the context node +``after`` + the node's body is inserted right after the context node +``inner`` + the node's body replaces the context node's children +``replace`` + the node's body is used to replace the context node itself +No operation + if no ``t-operation`` is specified, the template body is interpreted as + javascript code and executed with the context node as ``this`` + + .. warning:: while much more powerful than other operations, this mode is + also much harder to debug and maintain, it is recommended to + avoid it + +debugging +--------- + +The javascript QWeb implementation provides a few debugging hooks: + +``t-log`` + takes an expression parameter, evaluates the expression during rendering + and logs its result with ``console.log`` +``t-debug`` + triggers a debugger breakpoint during template rendering +``t-js`` + the node's body is javascript code executed during template rendering. + Takes a ``context`` parameter, which is the name under which the rendering + context will be available in the ``t-js``'s body + +Helpers ------- + +.. js:attribute:: openerp.qweb + + An instance of :js:class:`QWeb2.Engine` with all module-defined template + files loaded, and references to standard helper objects ``_`` + (underscore), ``_t`` (translation function) and JSON_. + + :js:func:`openerp.qweb.render ` can be used to + easily render basic module templates + +API +--- + +.. js:class:: QWeb2.Engine + + The QWeb "renderer", handles most of QWeb's logic (loading, + parsing, compiling and rendering templates). + + OpenERP Web instantiates one for the user, and sets it to + ``instance.web.qweb``. It also loads all the template files of the + various modules into that QWeb instance. + + A :js:class:`QWeb2.Engine` also serves as a "template namespace". + + .. js:function:: QWeb2.Engine.render(template[, context]) + + Renders a previously loaded template to a String, using + ``context`` (if provided) to find the variables accessed + during template rendering (e.g. strings to display). + + :param String template: the name of the template to render + :param Object context: the basic namespace to use for template + rendering + :returns: String + + The engine exposes an other method which may be useful in some + cases (e.g. if you need a separate template namespace with, in + OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine` + instance so their templates don't collide with more general + "module" templates): + + .. js:function:: QWeb2.Engine.add_template(templates) + + Loads a template file (a collection of templates) in the QWeb + instance. The templates can be specified as: + + An XML string + QWeb will attempt to parse it to an XML document then load + it. + + A URL + QWeb will attempt to download the URL content, then load + the resulting XML string. + + A ``Document`` or ``Node`` + QWeb will traverse the first level of the document (the + child nodes of the provided root) and load any named + template or template override. + + :type templates: String | Document | Node + + A :js:class:`QWeb2.Engine` also exposes various attributes for + behavior customization: + + .. js:attribute:: QWeb2.Engine.prefix + + Prefix used to recognize directives during parsing. A string. By + default, ``t``. + + .. js:attribute:: QWeb2.Engine.debug + + Boolean flag putting the engine in "debug mode". Normally, + QWeb intercepts any error raised during template execution. In + debug mode, it leaves all exceptions go through without + intercepting them. + + .. js:attribute:: QWeb2.Engine.jQuery + + The jQuery instance used during :ref:`template inheritance + ` processing. Defaults to + ``window.jQuery``. + + .. js:attribute:: QWeb2.Engine.preprocess_node + + A ``Function``. If present, called before compiling each DOM + node to template code. In OpenERP Web, this is used to + automatically translate text content and some attributes in + templates. Defaults to ``null``. + +.. [#genshif] it is similar in that to Genshi_, although it does not use (and + has no support for) `XML namespaces`_ + +.. [#othertemplates] although it uses a few others, either for historical + reasons or because they remain better fits for the + use case. Odoo 8.0 still depends on Jinja_ and Mako_. + +.. _templating: + http://en.wikipedia.org/wiki/Template_processor + +.. _Jinja: http://jinja.pocoo.org +.. _Mako: http://www.makotemplates.org +.. _Genshi: http://genshi.edgewall.org +.. _XML namespaces: http://en.wikipedia.org/wiki/XML_namespace +.. _HTML: http://en.wikipedia.org/wiki/HTML +.. _XSS: http://en.wikipedia.org/wiki/Cross-site_scripting +.. _JSON: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON +.. _CSS selector: http://api.jquery.com/category/selectors/ diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 61e184087a3..540832d1931 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -894,7 +894,7 @@ class view(osv.osv): e.set('data-oe-xpath', node_path) if not e.get('data-oe-model'): return - if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib): + if {'t-esc', 't-raw'}.intersection(e.attrib): # nodes which fully generate their content and have no reason to # be branded because they can not sensibly be edited self._pop_view_branding(e) From 14a677090bacea426152b7287d20a00155415c8e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Sep 2014 08:15:46 +0200 Subject: [PATCH 06/10] [ADD] running of XML cases to python qweb --- openerp/addons/base/ir/ir_qweb.py | 5 ++- openerp/addons/base/tests/test_qweb.py | 61 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index b82fe64099a..d91d4cd5d02 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -163,12 +163,13 @@ class QWeb(orm.AbstractModel): """ Loads an XML document and installs any contained template in the engine """ - if hasattr(document, 'documentElement'): + if not isinstance(document, basestring): + # assume lxml.etree.Element dom = document elif document.startswith(" Date: Thu, 11 Sep 2014 08:38:08 +0200 Subject: [PATCH 07/10] [ADD] qweb: handling of t-att=mapping Changed render_att_att to return an iterable of pairs instead of a pair, and dispatched t-att on whether its result is a Mapping. Also changed qweb test runner so it uses ordereddict for JSON mapping in params, otherwise iteration order (and thus order of attributes in output) is unpredictable and results don't/can't match expectations (as both are strings). Note that this relies on JS implementation details wrt iteration order of mappings. Tests would probably be somewhat less brittle if rendering output was parsed to XML... if that's possible (?) --- addons/website/models/ir_qweb.py | 10 ++++++---- openerp/addons/base/ir/ir_qweb.py | 26 ++++++++++++++++---------- openerp/addons/base/tests/test_qweb.py | 5 ++++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index 40b85fda697..c598773c10b 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -56,11 +56,13 @@ class QWeb(orm.AbstractModel): super(QWeb, self).add_template(qcontext, name, node) def render_att_att(self, element, attribute_name, attribute_value, qwebcontext): - att, val = super(QWeb, self).render_att_att(element, attribute_name, attribute_value, qwebcontext) + URL_ATTRS = self.URL_ATTRS.get(element.tag) + is_website = request.website + for att, val in super(QWeb, self).render_att_att(element, attribute_name, attribute_value, qwebcontext): + if is_website and att == URL_ATTRS and isinstance(val, basestring): + val = qwebcontext.get('url_for')(val) + yield (att, val) - if request.website and att == self.URL_ATTRS.get(element.tag) and isinstance(val, basestring): - val = qwebcontext.get('url_for')(val) - return att, val def get_converter_for(self, field_type): return self.pool.get( diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index d91d4cd5d02..1508baff9d8 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -265,8 +265,12 @@ class QWeb(orm.AbstractModel): if attribute_name.startswith("t-"): for attribute in self._render_att: if attribute_name[2:].startswith(attribute): - att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext) - if val: + attrs = self._render_att[attribute]( + self, element, attribute_name, attribute_value, qwebcontext) + for att, val in attrs: + if not val: continue + if not isinstance(val, str): + val = unicode(val).encode('utf-8') generated_attributes += self.render_attribute(element, att, val, qwebcontext) break else: @@ -336,14 +340,16 @@ class QWeb(orm.AbstractModel): # Attributes def render_att_att(self, element, attribute_name, attribute_value, qwebcontext): if attribute_name.startswith("t-attf-"): - att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext) - elif attribute_name.startswith("t-att-"): - att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext) - else: - att, val = self.eval_object(attribute_value, qwebcontext) - if val and not isinstance(val, str): - val = unicode(val).encode("utf8") - return att, val + return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))] + + if attribute_name.startswith("t-att-"): + return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))] + + result = self.eval_object(attribute_value, qwebcontext) + if isinstance(result, collections.Mapping): + return result.iteritems() + # assume tuple + return [result] # Tags def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext): diff --git a/openerp/addons/base/tests/test_qweb.py b/openerp/addons/base/tests/test_qweb.py index a923a835402..9ac4388cbe2 100644 --- a/openerp/addons/base/tests/test_qweb.py +++ b/openerp/addons/base/tests/test_qweb.py @@ -4,6 +4,7 @@ import json import os.path import glob import re +import collections from lxml import etree import openerp.addons.base.ir.ir_qweb @@ -118,7 +119,9 @@ class TestQWeb(common.TransactionCase): for template in context.templates: if template.startswith('_'): continue param = doc.find('params[@id="{}"]'.format(template)) - params = {} if param is None else json.loads(param.text) + # OrderedDict to ensure JSON mappings are iterated in source order + # so output is predictable & repeatable + params = {} if param is None else json.loads(param.text, object_pairs_hook=collections.OrderedDict) ctx = context.copy() ctx.update(params) From d5e3d121e3733de189605da7f830ff3d7c20b8d8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Sep 2014 10:51:08 +0200 Subject: [PATCH 08/10] [ADD] qweb: call directive's body * __content__ can't be used in Python implementation because safe_eval, so use ``0`` from Python implementation instead * remove postfix from t-call tests because due to implementation details all whitespace crap following a t-name is added to rendered template in Python impl, and don't want to normalize whitespace. --- addons/web/static/lib/qweb/qweb-test-call.xml | 13 ++++++++--- addons/web/static/lib/qweb/qweb2.js | 11 +++++++--- addons/web/static/src/xml/base.xml | 4 ++-- .../website/static/src/xml/website.editor.xml | 2 +- doc/reference/qweb.rst | 22 +++++++++++++++++++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb-test-call.xml b/addons/web/static/lib/qweb/qweb-test-call.xml index f71aca0b2ca..783172c73bd 100644 --- a/addons/web/static/lib/qweb/qweb-test-call.xml +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -1,6 +1,6 @@ ok - + @@ -32,11 +32,18 @@ ok + - - + - 1 - 1 + 1 diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index e080476cec9..c845abd5965 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -26,7 +26,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // TODO: templates orverwritten could be called by t-call="__super__" ? // TODO: t-set + t-value + children node == scoped variable ? var QWeb2 = { - expressions_cache: {}, + expressions_cache: { + // special case for template bodies, __content__ doesn't work in + // Python impl because safe_eval -> assert_no_dunder_name so use + // Python impl's magical 0 variable instead + '0': 'dict[0]', + }, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,raw,js,debug,log'.split(','), WORD_REPLACEMENT: { @@ -137,7 +142,7 @@ var QWeb2 = { var new_dict = this.extend({}, old_dict); new_dict['__caller__'] = old_dict['__template__']; if (callback) { - new_dict['__content__'] = callback(context, new_dict); + new_dict[0] = callback(context, new_dict); } return context.engine._render(template, new_dict); }, @@ -551,7 +556,7 @@ QWeb2.Element = (function() { format_expression : function(e) { /* Naive format expression builder. Replace reserved words and variables to dict[variable] * Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */ - if (QWeb2.expressions_cache[e]) { + if (QWeb2.expressions_cache[e]) { return QWeb2.expressions_cache[e]; } var chars = e.split(''), diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 32ef66d697b..3eb4638ac53 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -577,7 +577,7 @@
    - +
      +The body of the ``call`` directive can be arbitrarily complex (not just +``set`` directives), and its rendered form will be available within the called +template as a magical ``0`` variable:: + +
      + This template was called with content: + +
      + +being called thus:: + + + content + + +will result in:: + +
      + This template was called with content: + content +
      + Python ====== From c5dca416daf8441f57564fdd77beeaa1b27f5bdb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Sep 2014 11:23:13 +0200 Subject: [PATCH 09/10] [IMP] qweb: foreach handling * fix mapping handling to match JS impl: current value set as _value instead of being lost * add handling of integer parameter * only set _size and _last if current iterable is sized --- openerp/addons/base/ir/ir_qweb.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 1508baff9d8..8504c155ac4 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -362,29 +362,40 @@ class QWeb(orm.AbstractModel): inner = widget.format(template_attributes['esc'], options, qwebcontext) return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner) + def _iterate(self, iterable): + if isinstance (iterable, collections.Mapping): + return iterable.iteritems() + + return itertools.izip(*itertools.tee(iterable)) + def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext): expr = template_attributes["foreach"] enum = self.eval_object(expr, qwebcontext) if enum is None: template = qwebcontext.get('__template__') raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template) + if isinstance(enum, int): + enum = range(enum) varname = template_attributes['as'].replace('.', '_') copy_qwebcontext = qwebcontext.copy() - size = -1 + + size = None if isinstance(enum, collections.Sized): size = len(enum) - copy_qwebcontext["%s_size" % varname] = size + copy_qwebcontext["%s_size" % varname] = size + copy_qwebcontext["%s_all" % varname] = enum ru = [] - for index, item in enumerate(enum): + for index, (item, value) in enumerate(self._iterate(enum)): copy_qwebcontext.update({ varname: item, - '%s_value' % varname: item, + '%s_value' % varname: value, '%s_index' % varname: index, '%s_first' % varname: index == 0, - '%s_last' % varname: index + 1 == size, }) + if size is not None: + copy_qwebcontext['%s_last' % varname] = index + 1 == size if index % 2: copy_qwebcontext.update({ '%s_parity' % varname: 'odd', From 5954335222b883873cb996529f5ec5fb8acbea85 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Sep 2014 15:52:38 +0200 Subject: [PATCH 10/10] [ADD] pyqweb-specific stuff, pyqweb APIDoc --- doc/reference/qweb.rst | 90 ++++++++++++++++++++++++++++--- openerp/addons/base/ir/ir_qweb.py | 83 ++++++++++++++++++---------- 2 files changed, 138 insertions(+), 35 deletions(-) diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst index b57d78dcbca..47a4d0a2774 100644 --- a/doc/reference/qweb.rst +++ b/doc/reference/qweb.rst @@ -294,20 +294,97 @@ will result in:: Python ====== -Bundles +Exclusive directives +-------------------- + +asset bundles +''''''''''''' + +.. todo:: have fme write these up because I've no idea how they work + +"smart records" fields formatting +''''''''''''''''''''''''''''''''' + +The ``t-field`` directive can only be used when performing field access +(``a.b``) on a "smart" record (result of the ``browse`` method). It is able +to automatically format based on field type, and is integrated in the +website's rich text edition. + +``t-field-options`` can be used to customize fields, the most common option +is ``widget``, other options are field- or widget-dependent. + +Helpers ------- +Request-based +''''''''''''' + +Most Python-side uses of QWeb are in controllers (and during HTTP requests), +in which case templates stored in the database (as +:ref:`views `) can be trivially rendered by calling +:meth:`openerp.http.HttpRequest.render`: + +.. code-block:: python + + response = http.request.render('my-template', { + 'context_value': 42 + }) + +This automatically creates a :class:`~openerp.http.Response` object which can +be returned from the controller (or further customized to suit). + +View-based +'''''''''' + +At a deeper level than the previous helper is the ``render`` method on +``ir.ui.view``: + +.. py:method:: render(cr, uid, id[, values][, engine='ir.qweb][, context]) + + Renders a QWeb view/template by database id or :term:`external id`. + Templates are automatically loaded from ``ir.ui.view`` records. + + Sets up a number of default values in the rendering context: + + ``request`` + the current :class:`~openerp.http.WebRequest` object, if any + ``debug`` + whether the current request (if any) is in ``debug`` mode + :func:`quote_plus ` + url-encoding utility function + :mod:`json` + the corresponding standard library module + :mod:`time` + the corresponding standard library module + :mod:`datetime` + the corresponding standard library module + `relativedelta `_ + see module + ``keep_query`` + the ``keep_query`` helper function + + :param values: context values to pass to QWeb for rendering + :param str engine: name of the Odoo model to use for rendering, can be + used to expand or customize QWeb locally (by creating + a "new" qweb based on ``ir.qweb`` with alterations) + .. _reference/qweb/javascript: +API +--- + +It is also possible to use the ``ir.qweb`` model directly (and extend it, and +inherit from it): + +.. automodule:: openerp.addons.base.ir.ir_qweb + :members: QWeb, QWebContext, FieldConverter, QwebWidget + Javascript ========== Exclusive directives -------------------- -The Javascript qweb implementation provides specific directives to handle -defining and overloading/altering templates: - defining templates '''''''''''''''''' @@ -466,9 +543,8 @@ API .. js:attribute:: QWeb2.Engine.jQuery - The jQuery instance used during :ref:`template inheritance - ` processing. Defaults to - ``window.jQuery``. + The jQuery instance used during template inheritance processing. + Defaults to ``window.jQuery``. .. js:attribute:: QWeb2.Engine.preprocess_node diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 8504c155ac4..9c90cf43bfa 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -80,6 +80,9 @@ class QWebContext(dict): return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True) def copy(self): + """ Clones the current context, conserving all data and metadata + (loader, template cache, ...) + """ return QWebContext(self.cr, self.uid, dict.copy(self), loader=self.loader, templates=self.templates, @@ -89,28 +92,12 @@ class QWebContext(dict): return self.copy() class QWeb(orm.AbstractModel): - """QWeb Xml templating engine + """ Base QWeb rendering engine - The templating engine use a very simple syntax based "magic" xml - attributes, to produce textual output (even non-xml). - - The core magic attributes are: - - flow attributes: - t-if t-foreach t-call - - output attributes: - t-att t-raw t-esc t-trim - - assignation attribute: - t-set - - QWeb can be extended like any OpenERP model and new attributes can be - added. - - If you need to customize t-fields rendering, subclass the ir.qweb.field - model (and its sub-models) then override :meth:`~.get_converter_for` to - fetch the right field converters for your qweb model. + * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and + create new models called :samp:`ir.qweb.field.{widget}` + * alternatively, override :meth:`~.get_converter_for` and return an + arbitrary model to use as field converter Beware that if you need extensions or alterations which could be incompatible with other subsystems, you should create a local object @@ -162,6 +149,9 @@ class QWeb(orm.AbstractModel): def load_document(self, document, res_id, qwebcontext): """ Loads an XML document and installs any contained template in the engine + + :type document: a parsed lxml.etree element, an unparsed XML document + (as a string) or the path of an XML file to load """ if not isinstance(document, basestring): # assume lxml.etree.Element @@ -180,6 +170,12 @@ class QWeb(orm.AbstractModel): res_id = None def get_template(self, name, qwebcontext): + """ Tries to fetch the template ``name``, either gets it from the + context's template cache or loads one with the context's loader (if + any). + + :raises QWebTemplateNotFound: if the template can not be found or loaded + """ origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0] if qwebcontext.loader and name not in qwebcontext.templates: try: @@ -232,6 +228,15 @@ class QWeb(orm.AbstractModel): return int(bool(self.eval(expr, qwebcontext))) def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None): + """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None) + + Renders the template specified by the provided template name + + :param qwebcontext: context for rendering the template + :type qwebcontext: dict or :class:`QWebContext` instance + :param loader: if ``qwebcontext`` is a dict, loader set into the + context instantiated for rendering + """ if qwebcontext is None: qwebcontext = {} @@ -475,9 +480,22 @@ class QWeb(orm.AbstractModel): element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context) def get_converter_for(self, field_type): + """ returns a :class:`~openerp.models.Model` used to render a + ``t-field``. + + By default, tries to get the model named + :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``. + + :param str field_type: type or widget of field to render + """ return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field']) def get_widget_for(self, widget): + """ returns a :class:`~openerp.models.Model` used to render a + ``t-esc`` + + :param str widget: name of the widget to use, or ``None`` + """ widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget' return self.pool.get(widget_model) or self.pool['ir.qweb.widget'] @@ -509,7 +527,8 @@ class FieldConverter(osv.AbstractModel): def attributes(self, cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None): - """ + """ attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None) + Generates the metadata attributes (prefixed by ``data-oe-`` for the root node of the field conversion. Attribute values are escaped by the parent. @@ -538,21 +557,26 @@ class FieldConverter(osv.AbstractModel): ] def value_to_html(self, cr, uid, value, column, options=None, context=None): - """ Converts a single value to its HTML version/output + """ value_to_html(cr, uid, value, column, options=None, context=None) + + Converts a single value to its HTML version/output """ if not value: return '' return value def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None): - """ Converts the specified field of the browse_record ``record`` to - HTML + """ record_to_html(cr, uid, field_name, record, column, options=None, context=None) + + Converts the specified field of the browse_record ``record`` to HTML """ return self.value_to_html( cr, uid, record[field_name], column, options=options, context=context) def to_html(self, cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None): - """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be + """ to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None) + + Converts a ``t-field`` to its HTML output. A ``t-field`` may be extended by a ``t-field-options``, which is a JSON-serialized mapping of configuration values. @@ -594,13 +618,16 @@ class FieldConverter(osv.AbstractModel): def render_element(self, cr, uid, source_element, t_att, g_att, qweb_context, content): - """ Final rendering hook, by default just calls ir.qweb's ``render_element`` + """ render_element(cr, uid, source_element, t_att, g_att, qweb_context, content) + + Final rendering hook, by default just calls ir.qweb's ``render_element`` """ return self.qweb_object().render_element( source_element, t_att, g_att, qweb_context, content or '') def user_lang(self, cr, uid, context): - """ + """ user_lang(cr, uid, context) + Fetches the res.lang object corresponding to the language code stored in the user's context. Fallbacks to en_US if no lang is present in the context *or the language code is not valid*.