[MRG] merge with main branch.

bzr revid: tpa@tinyerp.com-20120419095819-o07ywcs2qnx6s4tm
This commit is contained in:
Turkesh Patel (Open ERP) 2012-04-19 15:28:19 +05:30
commit 6589bfeb81
38 changed files with 2352 additions and 863 deletions

View File

@ -405,14 +405,14 @@ def session_context(request, storage_path, session_cookie='sessionid'):
#----------------------------------------------------------
addons_module = {}
addons_manifest = {}
controllers_class = {}
controllers_class = []
controllers_object = {}
controllers_path = {}
class ControllerType(type):
def __init__(cls, name, bases, attrs):
super(ControllerType, cls).__init__(name, bases, attrs)
controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
class Controller(object):
__metaclass__ = ControllerType
@ -440,12 +440,12 @@ class Root(object):
self.root = '/web/webclient/home'
self.config = options
if self.config.backend == 'local':
conn = LocalConnector()
else:
conn = openerplib.get_connector(hostname=self.config.server_host,
port=self.config.server_port)
self.config.connector = conn
if not hasattr(self.config, 'connector'):
if self.config.backend == 'local':
self.config.connector = LocalConnector()
else:
self.config.connector = openerplib.get_connector(
hostname=self.config.server_host, port=self.config.server_port)
self.session_cookie = 'sessionid'
self.addons = {}
@ -526,7 +526,7 @@ class Root(object):
addons_module[module] = m
addons_manifest[module] = manifest
statics['/%s/static' % module] = path_static
for k, v in controllers_class.items():
for k, v in controllers_class:
if k not in controllers_object:
o = v()
controllers_object[k] = o

View File

@ -909,11 +909,6 @@ class Menu(openerpweb.Controller):
class DataSet(openerpweb.Controller):
_cp_path = "/web/dataset"
@openerpweb.jsonrequest
def fields(self, req, model):
return {'fields': req.session.model(model).fields_get(False,
req.session.eval_context(req.context))}
@openerpweb.jsonrequest
def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
return self.do_search_read(req, model, fields, offset, limit, domain, sort)
@ -949,7 +944,6 @@ class DataSet(openerpweb.Controller):
if fields and fields == ['id']:
# shortcut read if we only want the ids
return {
'ids': ids,
'length': length,
'records': [{'id': id} for id in ids]
}
@ -957,46 +951,10 @@ class DataSet(openerpweb.Controller):
records = Model.read(ids, fields or False, context)
records.sort(key=lambda obj: ids.index(obj['id']))
return {
'ids': ids,
'length': length,
'records': records
}
@openerpweb.jsonrequest
def read(self, req, model, ids, fields=False):
return self.do_search_read(req, model, ids, fields)
@openerpweb.jsonrequest
def get(self, req, model, ids, fields=False):
return self.do_get(req, model, ids, fields)
def do_get(self, req, model, ids, fields=False):
""" Fetches and returns the records of the model ``model`` whose ids
are in ``ids``.
The results are in the same order as the inputs, but elements may be
missing (if there is no record left for the id)
:param req: the JSON-RPC2 request object
:type req: openerpweb.JsonRequest
:param model: the model to read from
:type model: str
:param ids: a list of identifiers
:type ids: list
:param fields: a list of fields to fetch, ``False`` or empty to fetch
all fields in the model
:type fields: list | False
:returns: a list of records, in the same order as the list of ids
:rtype: list
"""
Model = req.session.model(model)
records = Model.read(ids, fields, req.session.eval_context(req.context))
record_map = dict((record['id'], record) for record in records)
return [record_map[id] for id in ids if record_map.get(id)]
@openerpweb.jsonrequest
def load(self, req, model, id, fields):
m = req.session.model(model)
@ -1006,23 +964,6 @@ class DataSet(openerpweb.Controller):
value = r[0]
return {'value': value}
@openerpweb.jsonrequest
def create(self, req, model, data):
m = req.session.model(model)
r = m.create(data, req.session.eval_context(req.context))
return {'result': r}
@openerpweb.jsonrequest
def save(self, req, model, id, data):
m = req.session.model(model)
r = m.write([id], data, req.session.eval_context(req.context))
return {'result': r}
@openerpweb.jsonrequest
def unlink(self, req, model, ids=()):
Model = req.session.model(model)
return Model.unlink(ids, req.session.eval_context(req.context))
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
has_domain = domain_id is not None and domain_id < len(args)
has_context = context_id is not None and context_id < len(args)
@ -1098,19 +1039,7 @@ class DataSet(openerpweb.Controller):
@openerpweb.jsonrequest
def exec_workflow(self, req, model, id, signal):
r = req.session.exec_workflow(model, id, signal)
return {'result': r}
@openerpweb.jsonrequest
def default_get(self, req, model, fields):
Model = req.session.model(model)
return Model.default_get(fields, req.session.eval_context(req.context))
@openerpweb.jsonrequest
def name_search(self, req, model, search_str, domain=[], context={}):
m = req.session.model(model)
r = m.name_search(search_str+'%', domain, '=ilike', context)
return {'result': r}
return req.session.exec_workflow(model, id, signal)
class DataGroup(openerpweb.Controller):
_cp_path = "/web/group"

10
addons/web/static/lib/qunit/qunit.css Executable file → Normal file
View File

@ -1,9 +1,9 @@
/**
* QUnit v1.2.0 - A JavaScript Unit Testing Framework
* QUnit v1.4.0pre - A JavaScript Unit Testing Framework
*
* http://docs.jquery.com/QUnit
*
* Copyright (c) 2011 John Resig, Jörn Zaefferer
* Copyright (c) 2012 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses.
*/
@ -54,6 +54,10 @@
color: #fff;
}
#qunit-header label {
display: inline-block;
}
#qunit-banner {
height: 5px;
}
@ -223,4 +227,6 @@
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

119
addons/web/static/lib/qunit/qunit.js Executable file → Normal file
View File

@ -1,9 +1,9 @@
/**
* QUnit v1.2.0 - A JavaScript Unit Testing Framework
* QUnit v1.4.0pre - A JavaScript Unit Testing Framework
*
* http://docs.jquery.com/QUnit
*
* Copyright (c) 2011 John Resig, Jörn Zaefferer
* Copyright (c) 2012 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses.
*/
@ -13,8 +13,11 @@
var defined = {
setTimeout: typeof window.setTimeout !== "undefined",
sessionStorage: (function() {
var x = "qunit-test-string";
try {
return !!sessionStorage.getItem;
sessionStorage.setItem(x, x);
sessionStorage.removeItem(x);
return true;
} catch(e) {
return false;
}
@ -25,11 +28,10 @@ var testId = 0,
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty;
var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
var Test = function(name, testName, expected, async, callback) {
this.name = name;
this.testName = testName;
this.expected = expected;
this.testEnvironmentArg = testEnvironmentArg;
this.async = async;
this.callback = callback;
this.assertions = [];
@ -62,6 +64,10 @@ Test.prototype = {
runLoggingCallbacks( 'moduleStart', QUnit, {
name: this.module
} );
} else if (config.autorun) {
runLoggingCallbacks( 'moduleStart', QUnit, {
name: this.module
} );
}
config.current = this;
@ -69,9 +75,6 @@ Test.prototype = {
setup: function() {},
teardown: function() {}
}, this.moduleTestEnvironment);
if (this.testEnvironmentArg) {
extend(this.testEnvironment, this.testEnvironmentArg);
}
runLoggingCallbacks( 'testStart', QUnit, {
name: this.testName,
@ -274,17 +277,12 @@ var QUnit = {
},
test: function(testName, expected, callback, async) {
var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg;
var name = '<span class="test-name">' + escapeInnerText(testName) + '</span>';
if ( arguments.length === 2 ) {
callback = expected;
expected = null;
}
// is 2nd argument a testEnvironment?
if ( expected && typeof expected === 'object') {
testEnvironmentArg = expected;
expected = null;
}
if ( config.currentModule ) {
name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
@ -294,7 +292,7 @@ var QUnit = {
return;
}
var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
var test = new Test(name, testName, expected, async, callback);
test.module = config.currentModule;
test.moduleTestEnvironment = config.currentModuleTestEnviroment;
test.queue();
@ -312,6 +310,9 @@ var QUnit = {
* @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
*/
ok: function(a, msg) {
if (!config.current) {
throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2));
}
a = !!a;
var details = {
result: a,
@ -447,9 +448,14 @@ var QUnit = {
QUnit.constructor = F;
})();
// Backwards compatibility, deprecated
QUnit.equals = QUnit.equal;
QUnit.same = QUnit.deepEqual;
// deprecated; still export them to window to provide clear error messages
// next step: remove entirely
QUnit.equals = function() {
throw new Error("QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead");
};
QUnit.same = function() {
throw new Error("QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead");
};
// Maintain internal state
var config = {
@ -513,8 +519,7 @@ if ( typeof exports === "undefined" || typeof require === "undefined" ) {
extend(window, QUnit);
window.QUnit = QUnit;
} else {
extend(exports, QUnit);
exports.QUnit = QUnit;
module.exports = QUnit;
}
// define these after exposing globals to keep them in these QUnit namespace only
@ -536,6 +541,16 @@ extend(QUnit, {
semaphore: 0
});
var qunit = id( "qunit" );
if ( qunit ) {
qunit.innerHTML =
'<h1 id="qunit-header">' + escapeInnerText( document.title ) + '</h1>' +
'<h2 id="qunit-banner"></h2>' +
'<div id="qunit-testrunner-toolbar"></div>' +
'<h2 id="qunit-userAgent"></h2>' +
'<ol id="qunit-tests"></ol>';
}
var tests = id( "qunit-tests" ),
banner = id( "qunit-banner" ),
result = id( "qunit-testresult" );
@ -564,15 +579,15 @@ extend(QUnit, {
/**
* Resets the test setup. Useful for tests that modify the DOM.
*
* If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
* If jQuery is available, uses jQuery's replaceWith(), otherwise use replaceChild
*/
reset: function() {
if ( window.jQuery ) {
jQuery( "#qunit-fixture" ).html( config.fixture );
} else {
var main = id( 'qunit-fixture' );
if ( main ) {
main.innerHTML = config.fixture;
var main = id( 'qunit-fixture' );
if ( main ) {
if ( window.jQuery ) {
jQuery( main ).replaceWith( config.fixture.cloneNode(true) );
} else {
main.parentNode.replaceChild(config.fixture.cloneNode(true), main);
}
}
},
@ -636,6 +651,9 @@ extend(QUnit, {
},
push: function(result, actual, expected, message) {
if (!config.current) {
throw new Error("assertion outside test context, was " + sourceFromStacktrace());
}
var details = {
result: result,
message: message,
@ -645,21 +663,22 @@ extend(QUnit, {
message = escapeInnerText(message) || (result ? "okay" : "failed");
message = '<span class="test-message">' + message + "</span>";
expected = escapeInnerText(QUnit.jsDump.parse(expected));
actual = escapeInnerText(QUnit.jsDump.parse(actual));
var output = message + '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>';
if (actual != expected) {
output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>';
output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>';
}
var output = message;
if (!result) {
expected = escapeInnerText(QUnit.jsDump.parse(expected));
actual = escapeInnerText(QUnit.jsDump.parse(actual));
output += '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>';
if (actual != expected) {
output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>';
output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>';
}
var source = sourceFromStacktrace();
if (source) {
details.source = source;
output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeInnerText(source) + '</pre></td></tr>';
}
output += "</table>";
}
output += "</table>";
runLoggingCallbacks( 'log', QUnit, details );
@ -779,7 +798,7 @@ QUnit.load = function() {
var main = id('qunit-fixture');
if ( main ) {
config.fixture = main.innerHTML;
config.fixture = main.cloneNode(true);
}
if (config.autostart) {
@ -847,6 +866,15 @@ function done() {
].join(" ");
}
// clear own sessionStorage items if all tests passed
if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
for (var key in sessionStorage) {
if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-") === 0 ) {
sessionStorage.removeItem(key);
}
}
}
runLoggingCallbacks( 'done', QUnit, {
failed: config.stats.bad,
passed: passed,
@ -881,16 +909,21 @@ function validTest( name ) {
// so far supports only Firefox, Chrome and Opera (buggy)
// could be extended in the future to use something like https://github.com/csnover/TraceKit
function sourceFromStacktrace() {
function sourceFromStacktrace(offset) {
offset = offset || 3;
try {
throw new Error();
} catch ( e ) {
if (e.stacktrace) {
// Opera
return e.stacktrace.split("\n")[6];
return e.stacktrace.split("\n")[offset + 3];
} else if (e.stack) {
// Firefox, Chrome
return e.stack.split("\n")[4];
var stack = e.stack.split("\n");
if (/^error$/i.test(stack[0])) {
stack.shift();
}
return stack[offset];
} else if (e.sourceURL) {
// Safari, PhantomJS
// TODO sourceURL points at the 'throw new Error' line above, useless
@ -989,6 +1022,7 @@ function fail(message, exception, callback) {
if ( typeof console !== "undefined" && console.error && console.warn ) {
console.error(message);
console.error(exception);
console.error(exception.stack);
console.warn(callback.toString());
} else if ( window.opera && opera.postError ) {
@ -1368,9 +1402,9 @@ QUnit.jsDump = (function() {
var ret = [ ];
QUnit.jsDump.up();
for ( var key in map ) {
var val = map[key];
var val = map[key];
ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack));
}
}
QUnit.jsDump.down();
return join( '{', ret, '}' );
},
@ -1594,4 +1628,5 @@ QUnit.diff = (function() {
};
})();
})(this);
// get at whatever the global object is, like window in browsers
})( (function() {return this}).call() );

View File

@ -21,16 +21,13 @@
color: #4c4c4c;
font-size: 13px;
background: white;
}
.openerp a {
text-decoration: none;
}
.openerp {
/* http://www.quirksmode.org/dom/inputfile.html
* http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image
*/
}
.openerp a {
text-decoration: none;
}
.openerp table {
padding: 0;
font-size: 13px;
@ -1620,6 +1617,15 @@
height: auto;
line-height: 16px;
}
.openerp .oe_listview_nocontent > img {
float: left;
margin-right: 1.5em;
}
.openerp .oe_listview_nocontent > div {
overflow: hidden;
padding: 6px;
font-size: 125%;
}
.openerp .oe-listview-content {
width: 100%;
}
@ -1777,23 +1783,10 @@
font-weight: bold;
}
.openerp .oe_layout_debugging .oe_form_group {
border: 2px dashed red;
outline: 2px dashed red;
}
.openerp .oe_layout_debugging .oe_form_group_cell {
border: 1px solid blue;
padding-bottom: 1em;
}
.openerp .oe_layout_debugging .oe_layout_debug_cell {
color: white;
background: #669966;
font-size: 80%;
text-align: center;
}
.openerp .oe_layout_debugging .oe_layout_debug_cell {
display: block;
}
.openerp .oe_layout_debug_cell {
display: none;
outline: 1px solid blue;
}
.openerp .oe_debug_view {
float: left;

View File

@ -78,12 +78,10 @@ $colour4: #8a89ba
color: #4c4c4c
font-size: 13px
background: white
// }}}
// Tag reset {{{
a
text-decoration: none
// }}}
.openerp
// Tag reset {{{
table
padding: 0
font-size: 13px
@ -1366,6 +1364,15 @@ $colour4: #8a89ba
// }}}
// ListView {{{
.oe_listview_nocontent
> img
float: left
margin-right: 1.5em
> div
// don't encroach on my arrow
overflow: hidden
padding: 6px
font-size: 125%
.oe-listview-content
width: 100%
thead, tfoot
@ -1480,23 +1487,12 @@ $colour4: #8a89ba
.oe_tooltip_technical_title
font-weight: bold
// }}}
// Debugging stuff {{{
.oe_layout_debugging
.oe_form_group
border: 2px dashed red
outline: 2px dashed red
.oe_form_group_cell
border: 1px solid blue
padding-bottom: 1em
.oe_layout_debug_cell
color: white
background: #696
font-size: 80%
text-align: center
.oe_layout_debug_cell
display: block
.oe_layout_debug_cell
display: none
outline: 1px solid blue
.oe_debug_view
float: left
@ -1529,4 +1525,3 @@ $colour4: #8a89ba
// au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers <afile> > "%:p:r.css"
// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 846 B

View File

@ -1390,6 +1390,7 @@ instance.web.Connection = instance.web.CallbackEnabled.extend( /** @lends instan
// an invalid session or no session at all), refresh session data
// (should not change, but just in case...)
_.extend(self, {
session_id: result.session_id,
db: result.db,
username: result.login,
uid: result.uid,

File diff suppressed because it is too large Load Diff

View File

@ -111,7 +111,7 @@ instance.web.format_value = function (value, descriptor, value_if_empty) {
return value_if_empty === undefined ? '' : value_if_empty;
}
var l10n = _t.database.parameters;
switch (descriptor.widget || descriptor.type) {
switch (descriptor.type || (descriptor.field && descriptor.field.type)) {
case 'id':
return value.toString();
case 'integer':
@ -171,7 +171,7 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
case "":
return value_if_empty === undefined ? false : value_if_empty;
}
switch (descriptor.widget || descriptor.type) {
switch (descriptor.type || (descriptor.field && descriptor.field.type)) {
case 'integer':
var tmp;
do {

View File

@ -1403,7 +1403,6 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/*
var type = field.type;
var obj = instance.web.search.custom_filters.get_object(type);
if(obj === null) {
console.log('Unknow field type ' + e.key);
obj = instance.web.search.custom_filters.get_object("char");
}
this.value = new (obj) (this);

View File

@ -0,0 +1,79 @@
openerp.test_support = {
setup_connection: function (connection) {
var origin = location.protocol+"//"+location.host;
_.extend(connection, {
origin: origin,
prefix: origin,
server: origin, // keep chs happy
//openerp.web.qweb.default_dict['_s'] = this.origin;
rpc_function: connection.rpc_json,
session_id: false,
uid: false,
username: false,
user_context: {},
db: false,
openerp_entreprise: false,
// this.module_list = openerp._modules.slice();
// this.module_loaded = {};
// _(this.module_list).each(function (mod) {
// self.module_loaded[mod] = true;
// });
context: {},
shortcuts: [],
active_id: null
});
return connection.session_reload();
},
module: function (title, tested_core, nonliterals) {
var conf = QUnit.config.openerp = {};
QUnit.module(title, {
setup: function () {
QUnit.stop();
var oe = conf.openerp = window.openerp.init();
window.openerp.web[tested_core](oe);
var done = openerp.test_support.setup_connection(oe.connection);
if (nonliterals) {
done = done.pipe(function () {
return oe.connection.rpc('/tests/add_nonliterals', {
domains: nonliterals.domains || [],
contexts: nonliterals.contexts || []
}).then(function (r) {
oe.domains = r.domains;
oe.contexts = r.contexts;
});
});
}
done.always(QUnit.start)
.then(function () {
conf.openerp = oe;
}, function (e) {
QUnit.test(title, function () {
console.error(e);
QUnit.ok(false, 'Could not obtain a session:' + e.debug);
});
});
}
});
},
test: function (title, fn) {
var conf = QUnit.config.openerp;
QUnit.test(title, function () {
QUnit.stop();
fn(conf.openerp);
});
},
expect: function (promise, fn) {
promise.always(QUnit.start)
.done(function () { QUnit.ok(false, 'RPC requests should not succeed'); })
.fail(function (e) {
if (e.code !== 200) {
QUnit.equal(e.code, 200, 'Testing connector should raise RPC faults');
if (typeof console !== 'undefined' && console.error) {
console.error(e.data.debug);
}
return;
}
fn(e.data.fault_code);
})
}
};

View File

@ -24,7 +24,6 @@ instance.web.FormView = instance.web.View.extend({
* @param {instance.web.DataSet} dataset the dataset this view will work with
* @param {String} view_id the identifier of the OpenERP view object
* @param {Object} options
* - sidebar : [true|false]
* - resize_textareas : [true|false|max_height]
*
* @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
@ -109,7 +108,7 @@ instance.web.FormView = instance.web.View.extend({
self.on_pager_action(action);
});
if (!this.sidebar && this.options.sidebar) {
if (!this.sidebar && this.options.$sidebar) {
this.sidebar = new instance.web.Sidebar(this);
this.sidebar.appendTo(this.$sidebar);
if(this.fields_view.toolbar) {
@ -247,7 +246,7 @@ instance.web.FormView = instance.web.View.extend({
}
_(this.fields).each(function (field, f) {
field.reset();
field._dirty_flag = false;
var result = field.set_value(self.datarecord[f] || false);
set_values.push(result);
});
@ -258,7 +257,7 @@ instance.web.FormView = instance.web.View.extend({
_.each(self.fields_order, function(field_name) {
if (record[field_name] !== undefined) {
var field = self.fields[field_name];
field.dirty = true;
field._dirty_flag = true;
self.do_onchange(field);
}
});
@ -281,9 +280,6 @@ instance.web.FormView = instance.web.View.extend({
},
on_form_changed: function() {
this.trigger("view_content_has_changed");
_.each(this.get_widgets(), function(w) {
w.update_dom();
});
},
do_notify_change: function() {
this.$element.addClass('oe_form_dirty');
@ -469,7 +465,7 @@ instance.web.FormView = instance.web.View.extend({
var value_ = result.value[f];
if (field.get_value() != value_) {
field.set_value(value_);
field.dirty = true;
field._dirty_flag = true;
if (!_.contains(processed, field.name)) {
this.do_onchange(field, processed);
}
@ -611,11 +607,10 @@ instance.web.FormView = instance.web.View.extend({
f = self.fields[f];
if (!f.is_valid()) {
form_invalid = true;
f.update_dom(true);
if (!first_invalid_field) {
first_invalid_field = f;
}
} else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f.is_dirty())) {
} else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
// Special case 'id' field, do not save this field
// on 'create' : save all non readonly fields
// on 'edit' : save non readonly modified fields
@ -623,20 +618,23 @@ instance.web.FormView = instance.web.View.extend({
}
}
if (form_invalid) {
self.set({'display_invalid_fields': true});
first_invalid_field.focus();
self.on_invalid();
return $.Deferred().reject();
} else {
self.set({'display_invalid_fields': false});
var save_deferral;
if (!self.datarecord.id) {
//console.log("FormView(", self, ") : About to create", values);
save_deferral = self.dataset.create(values).pipe(function(r) {
return self.on_created(r, undefined, prepend_on_create);
}, null);
} else if (_.isEmpty(values)) {
} else if (_.isEmpty(values) && ! self.force_dirty) {
//console.log("FormView(", self, ") : Nothing to save");
save_deferral = $.Deferred().resolve({}).promise();
} else {
self.force_dirty = false;
//console.log("FormView(", self, ") : About to save", values);
save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) {
return self.on_saved(r);
@ -754,7 +752,7 @@ instance.web.FormView = instance.web.View.extend({
},
is_dirty: function() {
return _.any(this.fields, function (value_) {
return value_.is_dirty();
return value_._dirty_flag;
});
},
is_interactible_record: function() {
@ -853,6 +851,14 @@ instance.web.FormView = instance.web.View.extend({
if (this.get_field(name).translate) {
this.translatable_fields.push(field);
}
field.on('changed_value', this, function() {
field._dirty_flag = true;
if (field.is_syntax_valid()) {
this.do_onchange(field);
this.on_form_changed(true);
this.do_notify_change();
}
});
},
get_field: function(field_name) {
return this.fields_view.fields[field_name];
@ -947,10 +953,10 @@ instance.web.form.FormRenderingEngine = instance.web.Class.extend({
},
toggle_layout_debugging: function() {
if (!this.$target.has('.oe_layout_debug_cell:first').length) {
this.$target.find('[title]').removeAttr('title');
this.$target.find('.oe_form_group_cell').each(function() {
var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'),
$span = $('<span class="oe_layout_debug_cell"/>').text(text);
$span.prependTo($(this));
var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
$(this).attr('title', text);
});
}
this.$target.toggleClass('oe_layout_debugging');
@ -1209,7 +1215,6 @@ instance.web.form.FormDialog = instance.web.Dialog.extend({
start: function() {
this._super();
this.form = new instance.web.FormView(this, this.dataset, this.view_id, {
sidebar: false,
pager: false
});
this.form.appendTo(this.$element);
@ -1380,8 +1385,6 @@ instance.web.form.Widget = instance.web.Widget.extend(_.extend({}, instance.web.
}
this.set(to_set);
},
update_dom: function() {
},
do_attach_tooltip: function(widget, trigger, options) {
widget = widget || this;
trigger = trigger || this.$element;
@ -1464,6 +1467,7 @@ instance.web.form.WidgetButton = instance.web.form.Widget.extend({
// TODO fme: provide enter key binding to widgets
this.view.default_focus_button = this;
}
this.view.on('view_content_has_changed', this, this.check_disable);
},
start: function() {
this._super.apply(this, arguments);
@ -1510,6 +1514,7 @@ instance.web.form.WidgetButton = instance.web.form.Widget.extend({
}
};
if (!this.node.attrs.special) {
this.view.force_dirty = true;
return this.view.recursive_save().pipe(exec_action);
} else {
return exec_action();
@ -1530,10 +1535,6 @@ instance.web.form.WidgetButton = instance.web.form.Widget.extend({
self.view.reload();
});
},
update_dom: function() {
this._super.apply(this, arguments);
this.check_disable();
},
check_disable: function() {
var disabled = (this.force_disabled || !this.view.is_interactible_record());
this.$element.prop('disabled', disabled);
@ -1546,7 +1547,8 @@ instance.web.form.WidgetButton = instance.web.form.Widget.extend({
* able to provide the features necessary for the fields to work.
*
* Properties:
* - ...
* - display_invalid_fields : if true, all fields where is_valid() return true should
* be displayed as invalid.
* Events:
* - view_content_has_changed : when the values of the fields have changed. When
* this event is triggered all fields should reprocess their modifiers.
@ -1574,7 +1576,7 @@ instance.web.form.FieldManagerInterface = {
* - force_readonly: boolean, When it is true, the field should always appear
* in read only mode, no matter what the value of the "readonly" property can be.
* Events:
* - ...
* - changed_value: triggered to inform the view to check on_changes
*
*/
instance.web.form.FieldInterface = {
@ -1623,7 +1625,21 @@ instance.web.form.FieldInterface = {
* Inform the current object of the id it should use to match a html <label> that exists somewhere in the
* view.
*/
set_input_id: function(id) {}
set_input_id: function(id) {},
/**
* Returns true if is_syntax_valid() returns true and the value is semantically
* valid too according to the semantic restrictions applied to the field.
*/
is_valid: function() {},
/**
* Returns true if the field holds a value which is syntaxically correct, ignoring
* the potential semantic restrictions applied to the field.
*/
is_syntax_valid: function() {},
/**
* Must set the focus on the field.
*/
focus: function() {},
};
/**
@ -1632,6 +1648,9 @@ instance.web.form.FieldInterface = {
* Properties:
* - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
* the values of the "readonly" property and the "force_readonly" property on the field manager.
* - value: useful property to hold the value of the field. By default, set_value() and get_value()
* set and retrieve the value property. Changing the value property also triggers automatically
* a 'changed_value' event that inform the view to trigger on_changes.
*
*/
instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends instance.web.form.AbstractField# */{
@ -1649,7 +1668,6 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins
this.set({'value': false});
this.field = this.field_manager.get_field(this.name);
this.set({required: this.modifiers['required'] === true});
this.dirty = false;
// some events to make the property "effective_readonly" sync automatically with "readonly" and
// "force_readonly"
@ -1662,17 +1680,18 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins
_.bind(test_effective_readonly, this)();
this.on("change:value", this, function() {
if (this._field_is_started && ! this._inhibit_on_change)
this._on_ui_change();
if (! this._inhibit_on_change)
this.trigger('changed_value');
this._check_css_flags();
});
},
start: function() {
this._super.apply(this, arguments);
// quick hack, will change later
this._field_is_started = true;
if (this.field.translate) {
this.$element.addClass('oe_form_field_translatable');
this.$element.find('.oe_field_translate').click(this.on_translate);
this.$element.find('.oe_field_translate').click(_.bind(function() {
this.field_manager.open_translate_dialog(this);
}, this));
}
if (instance.connection.debug) {
this.do_attach_tooltip(this, this.$element);
@ -1689,51 +1708,46 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins
this._inhibit_on_change = true;
this.set({'value': value_});
this._inhibit_on_change = false;
this.update_dom();
},
on_translate: function() {
this.field_manager.open_translate_dialog(this);
},
get_value: function() {
return this.get('value');
},
is_valid: function() {
return this.is_syntax_valid() && (! this.get('required') || ! this.is_false());
},
is_syntax_valid: function() {
return true;
},
is_dirty: function() {
return this.dirty && !this.get("effective_readonly");
/**
* Method useful to implement to ease validity testing. Must return true if the current
* value is similar to false in OpenERP.
*/
is_false: function() {
return this.get('value') === false;
},
update_dom: function(show_invalid) {
this._super.apply(this, arguments);
_check_css_flags: function(show_invalid) {
if (this.field.translate) {
this.$element.find('.oe_field_translate').toggle(!this.field_manager.is_create_mode());
}
if (!this.disable_utility_classes) {
if (show_invalid) {
if (this.field_manager.get('display_invalid_fields')) {
this.$element.toggleClass('oe_form_invalid', !this.is_valid());
}
}
},
_on_ui_change: function() {
this.dirty = true;
if (this.is_valid()) {
this.view.do_onchange(this);
this.view.on_form_changed(true);
this.view.do_notify_change();
} else {
this.update_dom(true);
}
focus: function() {
},
focus: function($element) {
if ($element) {
setTimeout(function() {
$element.focus();
}, 50);
}
},
reset: function() {
this.dirty = false;
/**
* Utility method to focus an element, but only after a small amount of time.
*/
delay_focus: function($elem) {
setTimeout(function() {
$elem.focus();
}, 50);
},
/**
* Utility method to get the widget options defined in the field xml description.
*/
get_definition_options: function() {
if (!this.definition_options) {
var str = this.node.attrs.options || '{}';
@ -1785,8 +1799,8 @@ instance.web.form.ReinitializeFieldMixin = {
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
template: 'FieldChar',
init: function (view, node) {
this._super(view, node);
init: function (field_manager, node) {
this._super(field_manager, node);
this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
},
initialize_content: function() {
@ -1810,19 +1824,22 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}
this.$element.text(show_value);
}
},
is_valid: function() {
is_syntax_valid: function() {
if (!this.get("effective_readonly")) {
try {
var value_ = instance.web.parse_value(this.$element.find('input').val(), this, '');
return (! this.get("required")) || value_ !== '';
return true;
} catch(e) {
return false;
}
}
return true;
},
focus: function($element) {
this._super($element || this.$element.find('input:first'));
is_false: function() {
return this.get('value') === '';
},
focus: function() {
this.delay_focus(this.$element.find('input:first'));
}
}));
@ -1846,7 +1863,7 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
}
},
on_button_clicked: function() {
if (!this.get('value') || !this.is_valid()) {
if (!this.get('value') || !this.is_syntax_valid()) {
this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
} else {
location.href = 'mailto:' + this.get('value');
@ -1886,8 +1903,8 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
is_field_number: true,
init: function (view, node) {
this._super(view, node);
init: function (field_manager, node) {
this._super(field_manager, node);
this.set({'value': 0});
if (this.node.attrs.digits) {
this.digits = py.eval(node.attrs.digits);
@ -1958,7 +1975,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({
this.$input.prop('readonly', this.readonly);
this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
},
is_valid: function() {
is_valid_: function() {
var value_ = this.$input.val();
if (value_ === "") {
return true;
@ -1978,7 +1995,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({
return instance.web.format_value(v, {"widget": this.type_of_date});
},
on_change: function() {
if (this.is_valid()) {
if (this.is_valid_()) {
this.set_value_from_ui_();
}
}
@ -2020,14 +2037,18 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten
this.$element.text(instance.web.format_value(this.get('value'), this, ''));
}
},
is_valid: function() {
is_syntax_valid: function() {
if (!this.get("effective_readonly")) {
return this.datewidget.is_valid() && (!this.get("required") || this.datewidget.get_value());
return this.datewidget.is_valid_();
}
return true;
},
focus: function($element) {
this._super($element || (this.datewidget && this.datewidget.$input));
is_false: function() {
return this.get('value') === '';
},
focus: function() {
if (this.datewidget && this.datewidget.$input)
this.delay_focus(this.datewidget.$input);
}
}));
@ -2040,13 +2061,14 @@ instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
template: 'FieldText',
initialize_content: function() {
this.$textarea = undefined;
this.$textarea = this.$element.find('textarea');
this.resized = false;
if (!this.get("effective_readonly")) {
this.$textarea = this.$element.find('textarea');
this.$textarea.change(_.bind(function() {
this.set({'value': instance.web.parse_value(this.$textarea.val(), this)});
}, this));
this.resized = false;
} else {
this.$textarea.attr('disabled', 'disabled');
}
},
set_value: function(value_) {
@ -2055,29 +2077,28 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}
},
render_value: function() {
var show_value = instance.web.format_value(this.get('value'), this, '');
if (!this.get("effective_readonly")) {
this.$textarea.val(show_value);
if (!this.resized && this.view.options.resize_textareas) {
this.do_resize(this.view.options.resize_textareas);
this.resized = true;
}
} else {
this.$element.text(show_value);
this.$textarea.val(show_value);
if (!this.resized && this.view.options.resize_textareas) {
this.do_resize(this.view.options.resize_textareas);
this.resized = true;
}
},
is_valid: function() {
is_syntax_valid: function() {
if (!this.get("effective_readonly")) {
try {
var value_ = instance.web.parse_value(this.$textarea.val(), this, '');
return !this.get("required") || value_ !== '';
return true;
} catch(e) {
return false;
}
}
return true;
},
is_false: function() {
return this.get('value') === '';
},
focus: function($element) {
this._super($element || this.$textarea);
this.delay_focus(this.$textarea);
},
do_resize: function(max_height) {
max_height = parseInt(max_height, 10);
@ -2099,9 +2120,6 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}
$div.remove();
$input.height(new_height);
},
reset: function() {
this.resized = false;
}
}));
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
@ -2122,8 +2140,8 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
this._super.apply(this, arguments);
this.$checkbox[0].checked = value_;
},
focus: function($element) {
this._super($element || this.$checkbox);
focus: function() {
this.delay_focus(this.$checkbox);
}
});
@ -2153,9 +2171,9 @@ instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({
instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
template: 'FieldSelection',
init: function(view, node) {
init: function(field_manager, node) {
var self = this;
this._super(view, node);
this._super(field_manager, node);
this.values = _.clone(this.field.selection);
_.each(this.values, function(v, i) {
if (v[0] === false && v[1] === '') {
@ -2209,16 +2227,15 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte
this.$element.text(option ? option[1] : this.values[0][1]);
}
},
is_valid: function() {
is_syntax_valid: function() {
if (this.get("effective_readonly")) {
return true;
}
var value_ = this.values[this.$element.find('select')[0].selectedIndex];
var invalid_ = !(value_ && !(this.get("required") && value_[0] === false));
return ! invalid_;
return !! value_;
},
focus: function($element) {
this._super($element || this.$element.find('select:first'));
focus: function() {
this.delay_focus(this.$element.find('select:first'));
}
}));
@ -2266,14 +2283,15 @@ instance.web.form.dialog = function(content, options) {
instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
template: "FieldMany2One",
init: function(view, node) {
this._super(view, node);
init: function(field_manager, node) {
this._super(field_manager, node);
this.limit = 7;
this.set({'value': false});
this.display_value = {};
this.last_search = [];
this.floating = false;
this.inhibit_on_change = false;
this.orderer = new instance.web.DropMisordered();
},
start: function() {
this._super();
@ -2422,14 +2440,10 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
var search_val = request.term;
var self = this;
if (this.abort_last) {
this.abort_last();
delete this.abort_last;
}
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
dataset.name_search(search_val, self.build_domain(), 'ilike',
this.limit + 1, function(data) {
this.orderer.add(dataset.name_search(
search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) {
self.last_search = data;
// possible selections for the m2o
var values = _.map(data, function(x) {
@ -2468,7 +2482,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
response(values);
});
this.abort_last = dataset.abort_last;
},
_quick_create: function(name) {
var self = this;
@ -2476,15 +2489,15 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
self._search_create_popup("form", undefined, {"default_name": name});
};
if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
dataset.name_create(name, function(data) {
self.display_value = {};
self.display_value["" + data[0]] = data[1];
self.set({value: data[0]});
}).fail(function(error, event) {
event.preventDefault();
slow_create();
});
new instance.web.DataSet(this, this.field.relation, self.build_context())
.name_create(name, function(data) {
self.display_value = {};
self.display_value["" + data[0]] = data[1];
self.set({value: data[0]});
}).fail(function(error, event) {
event.preventDefault();
slow_create();
});
} else
slow_create();
},
@ -2559,11 +2572,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
this._super(value_);
this.inhibit_on_change = false;
},
is_valid: function() {
return !this.get("required") || !! this.get("value");
is_false: function() {
return ! this.get("value");
},
focus: function ($element) {
this._super($element || this.$input);
focus: function () {
this.delay_focus(this.$input);
}
}));
@ -2615,8 +2628,8 @@ var commands = {
instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
multi_selection: false,
disable_utility_classes: true,
init: function(view, node) {
this._super(view, node);
init: function(field_manager, node) {
this._super(field_manager, node);
lazy_build_o2m_kanban_view();
this.is_loaded = $.Deferred();
this.initial_is_loaded = this.is_loaded;
@ -2655,7 +2668,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
trigger_on_change: function() {
var tmp = this.doing_on_change;
this.doing_on_change = true;
this._on_ui_change();
this.trigger('changed_value');
this.doing_on_change = tmp;
},
load_views: function() {
@ -2668,7 +2681,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
var view = {
view_id: false,
view_type: mode == "tree" ? "list" : mode,
options: { sidebar : false }
options: {}
};
if (self.field.views && self.field.views[mode]) {
view.embedded_view = self.field.views[mode];
@ -2690,7 +2703,9 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
});
this.views = views;
this.viewmanager = new instance.web.ViewManager(this, this.dataset, views, {});
this.viewmanager = new instance.web.ViewManager(this, this.dataset, views, {
$sidebar: false,
});
this.viewmanager.template = 'One2Many.viewmanager';
this.viewmanager.registry = instance.web.views.extend({
list: 'instance.web.form.One2ManyListView',
@ -2864,7 +2879,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
return false;
}, this));
},
is_valid: function() {
is_syntax_valid: function() {
if (!this.viewmanager.views[this.viewmanager.active_view])
return true;
var view = this.viewmanager.views[this.viewmanager.active_view].controller;
@ -2878,10 +2893,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
}
return true;
},
is_dirty: function() {
this.save_any_view();
return this._super();
}
});
instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
@ -2960,7 +2971,7 @@ instance.web.form.One2ManyFormView = instance.web.FormView.extend({
on_loaded: function(data) {
this._super(data);
var self = this;
this.$form_header.find('button.oe_form_button_create').click(function() {
this.$buttons.find('button.oe_form_button_create').click(function() {
self.do_save().then(self.on_button_new);
});
},
@ -3008,8 +3019,8 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
multi_selection: false,
disable_utility_classes: true,
init: function(view, node) {
this._super(view, node);
init: function(field_manager, node) {
this._super(field_manager, node);
this.is_loaded = $.Deferred();
this.initial_is_loaded = this.is_loaded;
this.is_setted = $.Deferred();
@ -3406,7 +3417,7 @@ instance.web.form.FormOpenPopup = instance.web.OldWidget.extend(/** @lends insta
setup_form_view: function() {
var self = this;
var FormClass = instance.web.views.get_object('form');
var options = _.clone(self.options.form_view_options);
var options = _.clone(self.options.form_view_options) || {};
options.initial_mode = this.options.readonly ? "view" : "edit";
this.view_form = new FormClass(this, this.dataset, false, options);
if (this.options.alternative_form_view) {
@ -3448,20 +3459,20 @@ instance.web.form.FormOpenDataset = instance.web.ProxyDataSet.extend({
instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
template: 'FieldReference',
init: function(view, node) {
this._super(view, node);
init: function(field_manager, node) {
this._super(field_manager, node);
this.fields_view = {
fields: {
selection: {
selection: view.fields_view.fields[this.name].selection
selection: this.view.fields_view.fields[this.name].selection
},
m2o: {
relation: null
}
}
};
this.get_fields_values = view.get_fields_values;
this.get_selected_ids = view.get_selected_ids;
this.get_fields_values = this.view.get_fields_values;
this.get_selected_ids = this.view.get_selected_ids;
this.do_onchange = this.on_form_changed = this.do_notify_change = this.on_nop;
this.dataset = this.view.dataset;
this.view_id = 'reference_' + _.uniqueId();
@ -3510,11 +3521,8 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte
this.m2o.renderElement();
this.m2o.start();
},
is_valid: function() {
return this.get("required") === false || typeof(this.get_value()) === 'string';
},
is_dirty: function() {
return this.selection.is_dirty() || this.m2o.is_dirty();
is_false: function() {
return typeof(this.get_value()) !== 'string';
},
set_value: function(value_) {
this._super(value_);
@ -3548,8 +3556,8 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte
}));
instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, {
init: function(view, node) {
this._super(view, node);
init: function(field_manager, node) {
this._super(field_manager, node);
this.iframe = this.element_id + '_iframe';
this.binary_value = false;
},

View File

@ -336,7 +336,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
}
// Sidebar
if (!this.sidebar && this.options.sidebar && this.options.$sidebar) {
if (!this.sidebar && this.options.$sidebar) {
this.sidebar = new instance.web.Sidebar(this);
this.sidebar.appendTo(this.options.$sidebar);
this.sidebar.add_toolbar(this.fields_view.toolbar);
@ -499,7 +499,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
view_id: this.view_id,
view_type: "tree",
context: this.dataset.get_context(context),
toolbar: this.options.sidebar
toolbar: !!this.options.$sidebar
}, callback);
}
},
@ -805,6 +805,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
.attr('colspan', this.previous_colspan);
this.previous_colspan = null;
}
},
no_result: function () {
if (this.groups.group_by
|| !this.options.action
|| !this.options.action.help) {
return;
}
this.$element.children('table').replaceWith(
$('<div class="oe_listview_nocontent">')
.append($('<img>', { src: '/web/static/src/img/list_empty_arrow.png' }))
.append($('<div>').html(this.options.action.help)));
}
});
instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{
@ -1402,6 +1413,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
self.records.add(records, {silent: true});
list.render();
d.resolve(list);
if (_.isEmpty(records)) {
view.no_result();
}
});});
return d.promise();
},

View File

@ -283,7 +283,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
// Lazy loading of views
var controllerclass = this.registry.get_object(view_type);
var options = _.clone(view.options);
if (view_type === "form") {
if (view_type === "form" && this.action) {
switch (this.action.target) {
case 'new':
case 'inline':
@ -353,6 +353,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
* @returns {$.Deferred} switching end signal
*/
on_prev_view: function (options) {
options = options || {};
var current_view = this.views_history.pop();
var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
if (options.created && current_view === 'form' && previous_view === 'list') {
@ -1055,7 +1056,7 @@ instance.web.View = instance.web.Widget.extend({
"model": this.dataset.model,
"view_id": this.view_id,
"view_type": this.view_type,
toolbar: this.options.sidebar,
toolbar: !!this.options.$sidebar,
context: context
}).pipe(this.on_loaded);
}
@ -1072,7 +1073,6 @@ instance.web.View = instance.web.Widget.extend({
// All possible views options should be defaulted here
$sidebar: null,
sidebar_id: null,
sidebar: true,
action: null,
action_views_ids: {}
});
@ -1163,7 +1163,6 @@ instance.web.View = instance.web.Widget.extend({
*/
set_embedded_view: function(embedded_view) {
this.embedded_view = embedded_view;
this.options.sidebar = false;
},
do_show: function () {
this.$element.show();

View File

@ -324,7 +324,7 @@
</ul>
</t>
<t t-name="Menu.secondary.link">
<a href="#"
<a t-attf-href="#menu_id=#{menu.id}&amp;action_id=#{menu.action ? menu.action.split(',')[1] : ''}"
t-att-class="menu.children.length ? 'oe_menu_toggler' : 'oe_menu_leaf'"
t-att-data-menu="menu.id"
t-att-data-action-model="menu.action ? menu.action.split(',')[0] : ''"
@ -972,16 +972,16 @@
</t>
</t>
<t t-name="FieldText">
<t t-if="!widget.get('effective_readonly')">
<div class="oe_form_field_text">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
></textarea><img class="oe_field_translate oe_input_icon" t-if="widget.field.translate" t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"/>
</div>
</t>
<div class="oe_form_field_text">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
></textarea>
<img class="oe_field_translate oe_input_icon" t-if="widget.field.translate"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"/>
</div>
</t>
<t t-name="web.datetimepicker">
<div class="oe_datepicker_root oe_form_field_datetime">

View File

@ -2,8 +2,8 @@ $(document).ready(function () {
var openerp;
module('web-class', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
}
});
test('Basic class creation', function () {

View File

@ -1,143 +0,0 @@
module("Class");
test("base", function() {
ok(!!nova.Class, "Class does exist");
ok(!!nova.Class.extend, "extend does exist");
var Claz = nova.Class.extend({
test: function() {
return "ok";
}
});
equal(new Claz().test(), "ok");
var Claz2 = Claz.extend({
test: function() {
return this._super() + "2";
}
});
equal(new Claz2().test(), "ok2");
});
module("DestroyableMixin");
test("base", function() {
var Claz = nova.Class.extend(_.extend({}, nova.DestroyableMixin, {}));
var x = new Claz();
equal(!!x.isDestroyed(), false);
x.destroy();
equal(x.isDestroyed(), true);
});
module("ParentedMixin");
test("base", function() {
var Claz = nova.Class.extend(_.extend({}, nova.ParentedMixin, {}));
var x = new Claz();
var y = new Claz();
y.setParent(x);
equal(y.getParent(), x);
equal(x.getChildren()[0], y);
x.destroy();
equal(y.isDestroyed(), true);
});
module("Events");
test("base", function() {
var x = new nova.internal.Events();
var tmp = 0;
var fct = function() {tmp = 1;};
x.on("test", fct);
equal(tmp, 0);
x.trigger("test");
equal(tmp, 1);
tmp = 0;
x.off("test", fct);
x.trigger("test");
equal(tmp, 0);
});
module("EventDispatcherMixin");
test("base", function() {
var Claz = nova.Class.extend(_.extend({}, nova.EventDispatcherMixin, {}));
var x = new Claz();
var y = new Claz();
var tmp = 0;
var fct = function() {tmp = 1;};
x.on("test", y, fct);
equal(tmp, 0);
x.trigger("test");
equal(tmp, 1);
tmp = 0;
x.off("test", y, fct);
x.trigger("test");
equal(tmp, 0);
tmp = 0;
x.on("test", y, fct);
y.destroy();
x.trigger("test");
equal(tmp, 0);
});
module("GetterSetterMixin");
test("base", function() {
var Claz = nova.Class.extend(_.extend({}, nova.GetterSetterMixin, {}));
var x = new Claz();
var y = new Claz();
x.set({test: 1});
equal(x.get("test"), 1);
var tmp = 0;
x.on("change:test", y, function(model, options) {
tmp = 1;
equal(options.oldValue, 1);
equal(options.newValue, 2);
equal(x.get("test"), 2);
equal(model, x);
});
x.set({test: 2});
equal(tmp, 1);
});
test("change event only when changed", function() {
var Claz = nova.Class.extend(_.extend({}, nova.GetterSetterMixin, {}));
var x = new Claz();
var exec1 = false;
var exec2 = false;
x.on("change:test", null, function() {exec1 = true;});
x.on("change", null, function() {exec2 = true;});
x.set({"test": 3});
equal(exec1, true);
equal(exec2, true);
exec1 = false;
exec2 = false;
x.set({"test": 3});
equal(exec1, false);
equal(exec2, false);
});
module("Widget");
test("base", function() {
var Claz = nova.Widget.extend({
renderElement: function() {
this.$element.attr("id", "testdiv");
this.$element.html("test");
}
});
var x = new Claz();
x.appendTo($("body"));
var $el = $("#testdiv");
equal($el.length, 1);
equal($el.parents()[0], $("body")[0]);
equal($el.html(), "test");
var y = new Claz(x);
equal(y.getParent(), x);
x.destroy();
$el = $("#testdiv");
equal($el.length, 0);
});

View File

@ -3,8 +3,9 @@ $(document).ready(function () {
module("eval.contexts", {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
}
});
test('context_sequences', function () {

View File

@ -2,8 +2,9 @@ $(document).ready(function () {
var openerp;
module("form.widget", {
setup: function () {
openerp = window.openerp.init(true);
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);

View File

@ -3,8 +3,9 @@ $(document).ready(function () {
module('server-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
}
});
@ -40,8 +41,9 @@ $(document).ready(function () {
module('web-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
window.openerp.web.formats(openerp);
}
@ -206,8 +208,9 @@ $(document).ready(function () {
});
module('custom-date-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
window.openerp.web.formats(openerp);
}

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP</title>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
<script src="/web/static/lib/qunit/qunit.js" type="text/javascript"></script>
<script src="/web/static/lib/underscore/underscore.js" type="text/javascript"></script>
<script src="/web/static/lib/underscore/underscore.string.js" type="text/javascript"></script>
<!-- jquery -->
<script src="/web/static/lib/jquery/jquery-1.6.4.js"></script>
<script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
<script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
<script src="/web/static/lib/datejs/globalization/en-US.js"></script>
<script src="/web/static/lib/datejs/core.js"></script>
<script src="/web/static/lib/datejs/parser.js"></script>
<script src="/web/static/lib/datejs/sugarpak.js"></script>
<script src="/web/static/lib/datejs/extras.js"></script>
<script src="/web/static/lib/qweb/qweb2.js"></script>
<script src="/web/static/lib/py.js/lib/py.js"></script>
<script src="/web/static/src/js/boot.js"></script>
<script src="/web/static/src/js/corelib.js"></script>
<script src="/web/static/src/js/coresetup.js"></script>
<script src="/web/static/src/js/dates.js"></script>
<script src="/web/static/src/js/formats.js"></script>
<script src="/web/static/src/js/chrome.js"></script>
<script src="/web/static/src/js/data.js"></script>
<script src="/web/static/src/js/test_support.js"></script>
</head>
<body id="oe" class="openerp">
<h1 id="qunit-header">
OpenERP Web Test Suite: javascript to XML-RPC (excluded)
</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture"></div>
<script type="text/javascript" src="/web/static/test/fulltest/dataset.js"></script>
</body>
</html>

View File

@ -0,0 +1,241 @@
$(document).ready(function () {
var t = window.openerp.test_support;
function context_(c) {
return _.extend({ lang: 'en_US', tz: 'UTC', uid: 87539319 }, c);
}
t.module('Dataset shortcuts', 'data');
t.test('read_index', function (openerp) {
var ds = new openerp.web.DataSet(
{session: openerp.connection}, 'some.model');
ds.ids = [10, 20, 30, 40, 50];
ds.index = 2;
t.expect(ds.read_index(['a', 'b', 'c']), function (result) {
strictEqual(result.method, 'read');
strictEqual(result.model, 'some.model');
strictEqual(result.args.length, 2);
deepEqual(result.args[0], [30]);
deepEqual(result.kwargs, {
context: context_()
});
});
});
t.test('default_get', function (openerp) {
var ds = new openerp.web.DataSet(
{session: openerp.connection}, 'some.model', {foo: 'bar'});
t.expect(ds.default_get(['a', 'b', 'c']), function (result) {
strictEqual(result.method, 'default_get');
strictEqual(result.model, 'some.model');
strictEqual(result.args.length, 1);
deepEqual(result.args[0], ['a', 'b', 'c']);
deepEqual(result.kwargs, {
context: context_({foo: 'bar'})
});
});
});
t.test('create', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'some.model');
t.expect(ds.create({foo: 1, bar: 2}), function (r) {
strictEqual(r.method, 'create');
strictEqual(r.args.length, 1);
deepEqual(r.args[0], {foo: 1, bar: 2});
deepEqual(r.kwargs, {
context: context_()
});
});
});
t.test('write', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.write(42, {foo: 1}), function (r) {
strictEqual(r.method, 'write');
strictEqual(r.args.length, 2);
deepEqual(r.args[0], [42]);
deepEqual(r.args[1], {foo: 1});
deepEqual(r.kwargs, {
context: context_()
});
});
// FIXME: can't run multiple sessions in the same test(), fucks everything up
// t.expect(ds.write(42, {foo: 1}, { context: {lang: 'bob'} }), function (r) {
// strictEqual(r.args.length, 3);
// strictEqual(r.args[2].lang, 'bob');
// });
});
t.test('unlink', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.unlink([42]), function (r) {
strictEqual(r.method, 'unlink');
strictEqual(r.args.length, 1);
deepEqual(r.args[0], [42]);
deepEqual(r.kwargs, {
context: context_()
});
});
});
t.test('call', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.call('frob', ['a', 'b', 42]), function (r) {
strictEqual(r.method, 'frob');
strictEqual(r.args.length, 3);
deepEqual(r.args, ['a', 'b', 42]);
ok(_.isEmpty(r.kwargs));
});
});
t.test('name_get', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.name_get([1, 2], null), function (r) {
strictEqual(r.method, 'name_get');
strictEqual(r.args.length, 1);
deepEqual(r.args[0], [1, 2]);
deepEqual(r.kwargs, {
context: context_()
});
});
});
t.test('name_search, name', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.name_search('bob'), function (r) {
strictEqual(r.method, 'name_search');
strictEqual(r.args.length, 0);
deepEqual(r.kwargs, {
name: 'bob',
args: false,
operator: 'ilike',
context: context_(),
limit: 0
});
});
});
t.test('name_search, domain & operator', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.name_search(0, [['foo', '=', 3]], 'someop'), function (r) {
strictEqual(r.method, 'name_search');
strictEqual(r.args.length, 0);
deepEqual(r.kwargs, {
name: '',
args: [['foo', '=', 3]],
operator: 'someop',
context: context_(),
limit: 0
});
});
});
t.test('exec_workflow', function (openerp) {
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
t.expect(ds.exec_workflow(42, 'foo'), function (r) {
strictEqual(r['service'], 'object');
strictEqual(r.method, 'exec_workflow');
// db, id, password, model, method, id
strictEqual(r.args.length, 6);
strictEqual(r.args[4], 'foo');
strictEqual(r.args[5], 42);
});
});
t.test('DataSetSearch#read_slice', function (openerp) {
var ds = new openerp.web.DataSetSearch({session: openerp.connection}, 'mod');
t.expect(ds.read_slice(['foo', 'bar'], {
domain: [['foo', '>', 42], ['qux', '=', 'grault']],
context: {peewee: 'herman'},
offset: 160,
limit: 80
}), function (r) {
strictEqual(r.method, 'search');
strictEqual(r.args.length, 5);
deepEqual(r.args[0], [['foo', '>', 42], ['qux', '=', 'grault']]);
strictEqual(r.args[1], 160);
strictEqual(r.args[2], 80);
strictEqual(r.args[3], false);
strictEqual(r.args[4].peewee, 'herman');
ok(_.isEmpty(r.kwargs));
});
});
t.test('DataSetSearch#read_slice sorted', function (openerp) {
var ds = new openerp.web.DataSetSearch({session: openerp.connection}, 'mod');
ds.sort('foo');
ds.sort('foo');
ds.sort('bar');
t.expect(ds.read_slice(['foo', 'bar'], { }), function (r) {
strictEqual(r.method, 'search');
strictEqual(r.args.length, 5);
deepEqual(r.args[0], []);
strictEqual(r.args[1], 0);
strictEqual(r.args[2], false);
strictEqual(r.args[3], 'bar ASC, foo DESC');
deepEqual(r.args[4], context_());
ok(_.isEmpty(r.kwargs));
});
});
t.module('Nonliterals', 'data', {
domains: [
"[('model_id', '=', parent.model)]",
"[('product_id','=',product_id)]"
],
contexts: ['{a: b > c}']
});
t.test('Dataset', function (openerp) {
var ds = new openerp.web.DataSetSearch(
{session: openerp.connection}, 'mod');
var c = new openerp.web.CompoundContext(
{a: 'foo', b: 3, c: 5}, openerp.contexts[0]);
t.expect(ds.read_slice(['foo', 'bar'], {
context: c
}), function (r) {
strictEqual(r.method, 'search');
deepEqual(r.args[4], context_({
foo: false,
a: 'foo',
b: 3,
c: 5
}));
ok(_.isEmpty(r.kwargs));
});
});
t.test('name_search', function (openerp) {
var eval_context = {
active_id: 42,
active_ids: [42],
active_model: 'mod',
parent: {model: 'qux'}
};
var ds = new openerp.web.DataSet(
{session: openerp.connection}, 'mod',
new openerp.web.CompoundContext({})
.set_eval_context(eval_context));
var domain = new openerp.web.CompoundDomain(openerp.domains[0])
.set_eval_context(eval_context);
t.expect(ds.name_search('foo', domain, 'ilike', 0), function (r) {
strictEqual(r.method, 'name_search');
strictEqual(r.args.length, 0);
deepEqual(r.kwargs, {
name: 'foo',
args: [['model_id', '=', 'qux']],
operator: 'ilike',
context: context_(),
limit: 0
});
});
});
});

View File

@ -10,7 +10,13 @@ $(document).ready(function () {
};
module('list-events', {
setup: function () {
openerp = window.openerp.init();
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
@ -90,7 +96,13 @@ $(document).ready(function () {
module('list-records', {
setup: function () {
openerp = window.openerp.init();
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
@ -123,7 +135,13 @@ $(document).ready(function () {
module('list-collections-degenerate', {
setup: function () {
openerp = window.openerp.init();
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});
@ -245,7 +263,13 @@ $(document).ready(function () {
module('list-hofs', {
setup: function () {
openerp = window.openerp.init();
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
}
});

View File

@ -1,74 +0,0 @@
$(document).ready(function () {
var openerp,
make_form = function (default_values) {
var fields = {};
_(default_values).each(function (value, name) {
fields[name] = value instanceof Function ? value : {
get_on_change_value: function () { return value; }
};
});
return _.extend(new openerp.web.FormView(null, {}),
{fields: fields});
};
module("form.onchange", {
setup: function () {
openerp = window.openerp.init(true);
window.openerp.web.core(openerp);
window.openerp.web.chrome(openerp);
// views loader stuff
window.openerp.web.data(openerp);
window.openerp.web.views(openerp);
window.openerp.web.list(openerp);
window.openerp.web.form(openerp);
}
});
test('Parse args-less onchange', function () {
var f = new openerp.web.FormView(null, {});
var result = f.parse_on_change('on_change_foo()', {});
equal(result.method, 'on_change_foo');
deepEqual(result.args, []);
});
test('Parse 1-arg onchange', function () {
var f = make_form({foo: 3});
var result = f.parse_on_change('on_change_foo(foo)', {});
equal(result.method, 'on_change_foo');
deepEqual(result.args, [3]);
});
test('Parse 2-args onchange', function () {
var f = make_form({foo: 3, bar: 'qux'});
var result = f.parse_on_change('on_change_foo(bar, foo)', {});
equal(result.method, 'on_change_foo');
deepEqual(result.args, ['qux', 3]);
});
test('Literal null', function () {
var f = make_form();
var result = f.parse_on_change('on_null(None)', {});
deepEqual(result.args, [null]);
});
test('Literal true', function () {
var f = make_form();
var result = f.parse_on_change('on_null(True)', {});
deepEqual(result.args, [true]);
});
test('Literal false', function () {
var f = make_form();
var result = f.parse_on_change('on_null(False)', {});
deepEqual(result.args, [false]);
});
test('Literal string', function () {
var f = make_form();
var result = f.parse_on_change('on_str("foo")', {});
deepEqual(result.args, ['foo']);
var result2 = f.parse_on_change("on_str('foo')", {});
deepEqual(result2.args, ['foo']);
});
test('Literal number', function () {
var f = make_form();
var result = f.parse_on_change('on_str(42)', {});
deepEqual(result.args, [42]);
var result2 = f.parse_on_change("on_str(-25)", {});
deepEqual(result2.args, [-25]);
var result3 = f.parse_on_change("on_str(25.02)", {});
deepEqual(result3.args, [25.02]);
});
});

View File

@ -2,8 +2,8 @@ $(document).ready(function () {
var openerp;
module('Registry', {
setup: function () {
openerp = window.openerp.init(true);
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
openerp.web.Foo = {};
openerp.web.Bar = {};
openerp.web.Foo2 = {};

View File

@ -0,0 +1,131 @@
$(document).ready(function () {
var openerp;
module('Misordered resolution management', {
setup: function () {
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.data(openerp);
}
});
test('Resolve all correctly ordered, sync', function () {
var dm = new openerp.web.DropMisordered(), flag = false;
var d1 = $.Deferred(), d2 = $.Deferred(),
r1 = dm.add(d1), r2 = dm.add(d2);
$.when(r1, r2).done(function () {
flag = true;
});
d1.resolve();
d2.resolve();
ok(flag);
});
test("Don't resolve mis-ordered, sync", function () {
var dm = new openerp.web.DropMisordered(),
done1 = false, done2 = false,
fail1 = false, fail2 = false;
var d1 = $.Deferred(), d2 = $.Deferred();
dm.add(d1).then(function () { done1 = true; },
function () { fail1 = true; });
dm.add(d2).then(function () { done2 = true; },
function () { fail2 = true; });
d2.resolve();
d1.resolve();
// d1 is in limbo
ok(!done1);
ok(!fail1);
// d2 is resolved
ok(done2);
ok(!fail2);
});
test('Fail mis-ordered flag, sync', function () {
var dm = new openerp.web.DropMisordered(true),
done1 = false, done2 = false,
fail1 = false, fail2 = false;
var d1 = $.Deferred(), d2 = $.Deferred();
dm.add(d1).then(function () { done1 = true; },
function () { fail1 = true; });
dm.add(d2).then(function () { done2 = true; },
function () { fail2 = true; });
d2.resolve();
d1.resolve();
// d1 is failed
ok(!done1);
ok(fail1);
// d2 is resolved
ok(done2);
ok(!fail2);
});
asyncTest('Resolve all correctly ordered, sync', 1, function () {
var dm = new openerp.web.DropMisordered();
var d1 = $.Deferred(), d2 = $.Deferred(),
r1 = dm.add(d1), r2 = dm.add(d2);
setTimeout(function () { d1.resolve(); }, 100);
setTimeout(function () { d2.resolve(); }, 200);
$.when(r1, r2).done(function () {
start();
ok(true);
});
});
asyncTest("Don't resolve mis-ordered, sync", 4, function () {
var dm = new openerp.web.DropMisordered(),
done1 = false, done2 = false,
fail1 = false, fail2 = false;
var d1 = $.Deferred(), d2 = $.Deferred();
dm.add(d1).then(function () { done1 = true; },
function () { fail1 = true; });
dm.add(d2).then(function () { done2 = true; },
function () { fail2 = true; });
setTimeout(function () { d1.resolve(); }, 200);
setTimeout(function () { d2.resolve(); }, 100);
setTimeout(function () {
start();
// d1 is in limbo
ok(!done1);
ok(!fail1);
// d2 is resolved
ok(done2);
ok(!fail2);
}, 400);
});
asyncTest('Fail mis-ordered flag, sync', 4, function () {
var dm = new openerp.web.DropMisordered(true),
done1 = false, done2 = false,
fail1 = false, fail2 = false;
var d1 = $.Deferred(), d2 = $.Deferred();
dm.add(d1).then(function () { done1 = true; },
function () { fail1 = true; });
dm.add(d2).then(function () { done2 = true; },
function () { fail2 = true; });
setTimeout(function () { d1.resolve(); }, 200);
setTimeout(function () { d2.resolve(); }, 100);
setTimeout(function () {
start();
// d1 is failed
ok(!done1);
ok(fail1);
// d2 is resolved
ok(done2);
ok(!fail2);
}, 400);
});
});

View File

@ -26,10 +26,9 @@
<script src="/web/static/lib/py.js/lib/py.js"></script>
<script src="/web/static/lib/novajs/src/nova.js"></script>
<script src="/web/static/src/js/boot.js"></script>
<script src="/web/static/src/js/core.js"></script>
<script src="/web/static/src/js/corelib.js"></script>
<script src="/web/static/src/js/coresetup.js"></script>
<script src="/web/static/src/js/dates.js"></script>
<script src="/web/static/src/js/formats.js"></script>
<script src="/web/static/src/js/chrome.js"></script>
@ -52,6 +51,6 @@
<script type="text/javascript" src="/web/static/test/form.js"></script>
<script type="text/javascript" src="/web/static/test/list-utils.js"></script>
<script type="text/javascript" src="/web/static/test/formats.js"></script>
<script type="text/javascript" src="/web/static/test/onchange.js"></script>
<script type="text/javascript" src="/web/static/test/rpc.js"></script>
<script type="text/javascript" src="/web/static/test/evals.js"></script>
</html>

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import xmlrpclib
from ..common.openerplib.main import Connector
execute_map = {}
class TestConnector(Connector):
def db_list_lang(self):
return [('en_US', u'Test Language')]
def common_authenticate(self, db, login, password, environment):
return 87539319
def common_login(self, db, login, password):
return self.common_authenticate(db, login, password, {})
def object_execute_kw(self, db, uid, password, model, method, args, kwargs):
if model in execute_map and hasattr(execute_map[model], method):
return getattr(execute_map[model], method)(*args, **kwargs)
raise xmlrpclib.Fault({
'model': model,
'method': method,
'args': args,
'kwargs': kwargs
}, '')
def send(self, service_name, method, *args):
method_name = '%s_%s' % (service_name, method)
if hasattr(self, method_name):
return getattr(self, method_name)(*args)
raise xmlrpclib.Fault({
'service': service_name,
'method': method,
'args': args
}, '')

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from ..common import http, nonliterals
from ..controllers.main import Session
UID = 87539319
DB = 'test_db'
LOGIN = 'test_login'
PASSWORD = 'test_password'
CONTEXT = {'lang': 'en_US', 'tz': 'UTC', 'uid': UID}
def bind(session):
session.bind(DB, UID, LOGIN, PASSWORD)
session.context = CONTEXT
session.build_connection().set_login_info(DB, LOGIN, PASSWORD, UID)
class TestController(http.Controller):
_cp_path = '/tests'
@http.jsonrequest
def add_nonliterals(self, req, domains, contexts):
return {
'domains': [nonliterals.Domain(req.session, domain)
for domain in domains],
'contexts': [nonliterals.Context(req.session, context)
for context in contexts]
}
class TestSession(Session):
_cp_path = '/web/session'
def session_info(self, req):
if not req.session._uid:
bind(req.session)
return {
"session_id": req.session_id,
"uid": req.session._uid,
"context": CONTEXT,
"db": req.session._db,
"login": req.session._login,
"openerp_entreprise": False,
}

View File

@ -102,9 +102,9 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
this.init_scheduler();
if (this.options.sidebar) {
if (! this.sidebar && this.options.$sidebar) {
this.sidebar = new instance.web_calendar.Sidebar(this);
this.has_been_loaded.pipe(this.sidebar.appendTo(this.$element));
this.has_been_loaded.pipe(this.sidebar.appendTo(this.options.$sidebar));
}
return this.has_been_loaded.resolve();
@ -158,7 +158,7 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
scheduler.setCurrentView(scheduler._date);
},
refresh_minical: function() {
if (this.options.sidebar) {
if (this.sidebar) {
scheduler.updateCalendar(this.sidebar.mini_calendar);
}
},
@ -217,7 +217,7 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
scheduler.parse(res_events, 'json');
this.refresh_scheduler();
this.refresh_minical();
if (!no_filter_reload && this.options.sidebar) {
if (!no_filter_reload && this.sidebar) {
this.sidebar.filter.on_events_loaded(sidebar_items);
}
},
@ -412,7 +412,6 @@ instance.web_calendar.CalendarFormDialog = instance.web.Dialog.extend({
var self = this;
this._super();
this.form = new instance.web.FormView(this, this.dataset, this.view_id, {
sidebar: false,
pager: false
});
this.form.appendTo(this.$element);

View File

@ -107,7 +107,7 @@ openerp.web_process = function (instance) {
if(this.process_id)
return def.resolve().promise();
this.process_dataset = new instance.web.DataSetStatic(this, "process.process", this.session.context);
this.process_dataset = new instance.web.DataSet(this, "process.process", this.session.context);
this.process_dataset
.call("search_by_model", [self.process_model,self.session.context])
.done(function(res) {
@ -237,7 +237,7 @@ openerp.web_process = function (instance) {
},
jump_to_view: function(model, id) {
var self = this;
var dataset = new instance.web.DataSetStatic(this, 'ir.values', this.session.context);
var dataset = new instance.web.DataSet(this, 'ir.values', this.session.context);
dataset.call('get',
['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context],
function(res) {

353
doc/async.rst Normal file
View File

@ -0,0 +1,353 @@
Don't stop the world now: asynchronous development and Javascript
=================================================================
As a language (and runtime), javascript is fundamentally
single-threaded. This means any blocking request or computation will
blocks the whole page (and, in older browsers, the software itself
even preventing users from switching to an other tab): a javascript
environment can be seen as an event-based runloop where application
developers have no control over the runloop itself.
As a result, performing long-running synchronous network requests or
other types of complex and expensive accesses is frowned upon and
asynchronous APIs are used instead.
Asynchronous code rarely comes naturally, especially for developers
used to synchronous server-side code (in Python, Java or C#) where the
code will just block until the deed is gone. This is increased further
when asynchronous programming is not a first-class concept and is
instead implemented on top of callbacks-based programming, which is
the case in javascript.
The goal of this guide is to provide some tools to deal with
asynchronous systems, and warn against systematic issues or dangers.
Deferreds
---------
Deferreds are a form of `promises`_. OpenERP Web currently uses
`jQuery's deferred`_, but any `CommonJS Promises/A`_ implementation
should work.
The core idea of deferreds is that potentially asynchronous methods
will return a :js:class:`Deferred` object instead of an arbitrary
value or (most commonly) nothing.
This object can then be used to track the end of the asynchronous
operation by adding callbacks onto it, either success callbacks or
error callbacks.
A great advantage of deferreds over simply passing callback functions
directly to asynchronous methods is the ability to :ref:`compose them
<deferred-composition>`.
Using deferreds
~~~~~~~~~~~~~~~
`CommonJS Promises/A`_ deferreds have only one method of importance:
:js:func:`Deferred.then`. This method is used to attach new callbacks
to the deferred object.
* the first parameter attaches a success callback, called when the
deferred object is successfully resolved and provided with the
resolved value(s) for the asynchronous operation.
* the second parameter attaches a failure callback, called when the
deferred object is rejected and provided with rejection values
(often some sort of error message).
Callbacks attached to deferreds are never "lost": if a callback is
attached to an already resolved or rejected deferred, the callback
will be called (or ignored) immediately. A deferred is also only ever
resolved or rejected once, and is either resolved or rejected: a given
deferred can not call a single success callback twice, or call both a
success and a failure callbacks.
:js:func:`~Deferred.then` should be the method you'll use most often
when interacting with deferred objects (and thus asynchronous APIs).
Building deferreds
~~~~~~~~~~~~~~~~~~
After using asynchronous APIs may come the time to build them: for
`mocks`_, to compose deferreds from multiple source in a complex
manner, in order to let the current operations repaint the screen or
give other events the time to unfold, ...
This is easy using jQuery's deferred objects.
.. note:: this section is an implementation detail of jQuery Deferred
objects, the creation of promises is not part of any
standard (even tentative) that I know of. If you are using
deferred objects which are not jQuery's, their API may (and
often will) be completely different.
Deferreds are created by invoking their constructor [#]_ without any
argument. This creates a :js:class:`Deferred` instance object with the
following methods:
:js:func:`Deferred.resolve`
As its name indicates, this method moves the deferred to the
"Resolved" state. It can be provided as many arguments as
necessary, these arguments will be provided to any pending success
callback.
:js:func:`Deferred.reject`
Similar to :js:func:`~Deferred.resolve`, but moves the deferred to
the "Rejected" state and calls pending failure handlers.
:js:func:`Deferred.promise`
Creates a readonly view of the deferred object. It is generally a
good idea to return a promise view of the deferred to prevent
callers from resolving or rejecting the deferred in your stead.
:js:func:`~Deferred.reject` and :js:func:`~Deferred.resolve` are used
to inform callers that the asynchronous operation has failed (or
succeeded). These methods should simply be called when the
asynchronous operation has ended, to notify anybody interested in its
result(s).
.. _deferred-composition:
Composing deferreds
~~~~~~~~~~~~~~~~~~~
What we've seen so far is pretty nice, but mostly doable by passing
functions to other functions (well adding functions post-facto would
probably be a chore... still, doable).
Deferreds truly shine when code needs to compose asynchronous
operations in some way or other, as they can be used as a basis for
such composition.
There are two main forms of compositions over deferred: multiplexing
and piping/cascading.
Deferred multiplexing
`````````````````````
The most common reason for multiplexing deferred is simply performing
2+ asynchronous operations and wanting to wait until all of them are
done before moving on (and executing more stuff).
The jQuery multiplexing function for promises is :js:func:`when`.
.. note:: the multiplexing behavior of jQuery's :js:func:`when` is an
(incompatible, mostly) extension of the behavior defined in
`CommonJS Promises/B`_.
This function can take any number of promises [#]_ and will return a
promise.
This returned promise will be resolved when *all* multiplexed promises
are resolved, and will be rejected as soon as one of the multiplexed
promises is rejected (it behaves like Python's ``all()``, but with
promise objects instead of boolean-ish).
The resolved values of the various promises multiplexed via
:js:func:`when` are mapped to the arguments of :js:func:`when`'s
success callback, if they are needed. The resolved values of a promise
are at the same index in the callback's arguments as the promise in
the :js:func:`when` call so you will have:
.. code-block:: javascript
$.when(p0, p1, p2, p3).then(
function (results0, results1, results2, results3) {
// code
});
.. warning::
in a normal mapping, each parameter to the callback would be an
array: each promise is conceptually resolved with an array of 0..n
values and these values are passed to :js:func:`when`'s
callback. But jQuery treats deferreds resolving a single value
specially, and "unwraps" that value.
For instance, in the code block above if the index of each promise
is the number of values it resolves (0 to 3), ``results0`` is an
empty array, ``results2`` is an array of 2 elements (a pair) but
``results1`` is the actual value resolved by ``p1``, not an array.
Deferred chaining
`````````````````
A second useful composition is starting an asynchronous operation as
the result of an other asynchronous operation, and wanting the result
of both: :js:func:`Deferred.then` returns the deferred on which it was
called, so handle e.g. OpenERP's search/read sequence with this would
require something along the lines of:
.. code-block:: javascript
var result = $.Deferred();
Model.search(condition).then(function (ids) {
Model.read(ids, fields).then(function (records) {
result.resolve(records);
});
});
return result.promise();
While it doesn't look too bad for trivial code, this quickly gets
unwieldy.
Instead, jQuery provides a tool to handle this kind of chains:
:js:func:`Deferred.pipe`.
:js:func:`~Deferred.pipe` has the same signature as
:js:func:`~Deferred.then` and could be used in the same manner
provided its return value was not used.
It differs from :js:func:`~Deferred.then` in two ways: it returns a
new promise object, not the one it was called with, and the return
values of the callbacks is actually important to it: whichever
callback is called,
* If the callback is not set (not provided or left to null), the
resolution or rejection value(s) is simply forwarded to
:js:func:`~Deferred.pipe`'s promise (it's essentially a noop)
* If the callback is set and does not return an observable object (a
deferred or a promise), the value it returns (``undefined`` if it
does not return anything) will replace the value it was given, e.g.
.. code-block:: javascript
promise.pipe(function () {
console.log('called');
});
will resolve with the sole value ``undefined``.
* If the callback is set and returns an observable object, that object
will be the actual resolution (and result) of the pipe. This means a
resolved promise from the failure callback will resolve the pipe,
and a failure promise from the success callback will reject the
pipe.
This provides an easy way to chain operation successes, and the
previous piece of code can now be rewritten:
.. code-block:: javascript
return Model.search(condition).pipe(function (ids) {
return Model.read(ids, fields);
});
the result of the whole expression will encode failure if either
``search`` or ``read`` fails (with the right rejection values), and
will be resolved with ``read``'s resolution values if the chain
executes correctly.
:js:func:`~Deferred.pipe` is also useful to adapt third-party
promise-based APIs, in order to filter their resolution value counts
for instance (to take advantage of :js:func:`when` 's special treatment
of single-value promises).
jQuery.Deferred API
~~~~~~~~~~~~~~~~~~~
.. js:function:: when(deferreds…)
:param deferreds: deferred objects to multiplex
:returns: a multiplexed deferred
:rtype: :js:class:`Deferred`
.. js:class:: Deferred
.. js:function:: Deferred.then(doneCallback[, failCallback])
Attaches new callbacks to the resolution or rejection of the
deferred object. Callbacks are executed in the order they are
attached to the deferred.
To provide only a failure callback, pass ``null`` as the
``doneCallback``, to provide only a success callback the
second argument can just be ignored (and not passed at all).
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.done(doneCallback)
Attaches a new success callback to the deferred, shortcut for
``deferred.then(doneCallback)``.
This is a jQuery extension to `CommonJS Promises/A`_ providing
little value over calling :js:func:`~Deferred.then` directly,
it should be avoided.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.fail(failCallback)
Attaches a new failure callback to the deferred, shortcut for
``deferred.then(null, failCallback)``.
A second jQuery extension to `Promises/A <CommonJS
Promises/A>`_. Although it provides more value than
:js:func:`~Deferred.done`, it still is not much and should be
avoided as well.
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.promise()
Returns a read-only view of the deferred object, with all
mutators (resolve and reject) methods removed.
.. js:function:: Deferred.resolve(value…)
Called to resolve a deferred, any value provided will be
passed onto the success handlers of the deferred object.
Resolving a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.reject(value…)
Called to reject (fail) a deferred, any value provided will be
passed onto the failure handler of the deferred object.
Rejecting a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.pipe(doneFilter[, failFilter])
Filters the result of a deferred, able to transform a success
into failure and a failure into success, or to delay
resolution further.
.. [#] or simply calling :js:class:`Deferred` as a function, the
result is the same
.. [#] or not-promises, the `CommonJS Promises/B`_ role of
:js:func:`when` is to be able to treat values and promises
uniformly: :js:func:`when` will pass promises through directly,
but non-promise values and objects will be transformed into a
resolved promise (resolving themselves with the value itself).
jQuery's :js:func:`when` keeps this behavior making deferreds
easy to build from "static" values, or allowing defensive code
where expected promises are wrapped in :js:func:`when` just in
case.
.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A
.. _CommonJS Promises/B: http://wiki.commonjs.org/wiki/Promises/B
.. _mocks: http://en.wikipedia.org/wiki/Mock_object

108
doc/changelog-6.2.rst Normal file
View File

@ -0,0 +1,108 @@
API changes from OpenERP Web 6.1 to 6.2
=======================================
DataSet -> Model
----------------
The 6.1 ``DataSet`` API has been deprecated in favor of the smaller
and more orthogonal :doc:`Model </rpc>` API, which more closely
matches the API in OpenERP Web's Python side and in OpenObject addons
and removes most stateful behavior of DataSet.
Migration guide
~~~~~~~~~~~~~~~
* Actual arbitrary RPC calls can just be remapped on a
:js:class:`~openerp.web.Model` instance:
.. code-block:: javascript
dataset.call(method, args)
or
.. code-block:: javascript
dataset.call_and_eval(method, args)
can be replaced by calls to :js:func:`openerp.web.Model.call`:
.. code-block:: javascript
model.call(method, args)
If callbacks are passed directly to the older methods, they need to
be added to the new one via ``.then()``.
.. note::
The ``context_index`` and ``domain_index`` features were not
ported, context and domain now need to be passed in "in full",
they won't be automatically filled with the user's current
context.
* Shorcut methods (``name_get``, ``name_search``, ``unlink``,
``write``, ...) should be ported to
:js:func:`openerp.web.Model.call`, using the server's original
signature. On the other hand, the non-shortcut equivalents can now
use keyword arguments (see :js:func:`~openerp.web.Model.call`'s
signature for details)
* ``read_slice``, which allowed a single round-trip to perform a
search and a read, should be reimplemented via
:js:class:`~openerp.web.Query` objects (see:
:js:func:`~openerp.web.Model.query`) for clearer and simpler
code. ``read_index`` should be replaced by a
:js:class:`~openerp.web.Query` as well, combining
:js:func:`~openerp.web.Query.offset` and
:js:func:`~openerp.web.Query.first`.
Rationale
~~~~~~~~~
Renaming
The name *DataSet* exists in the CS community consciousness, and
(as its name implies) it's a set of data (often fetched from a
database, maybe lazily). OpenERP Web's dataset behaves very
differently as it does not store (much) data (only a bunch of ids
and just enough state to break things). The name "Model" matches
the one used on the Python side for the task of building an RPC
proxy to OpenERP objects.
API simplification
``DataSet`` has a number of methods which serve as little more
than shortcuts, or are there due to domain and context evaluation
issues in 6.1.
The shortcuts really add little value, and OpenERP Web 6.2 embeds
a restricted Python evaluator (in javascript) meaning most of the
context and domain parsing & evaluation can be moved to the
javascript code and does not require cooperative RPC bridging.
DataGroup -> also Model
-----------------------
Alongside the deprecation of ``DataSet`` for
:js:class:`~openerp.web.Model`, OpenERP Web 6.2 also deprecates
``DataGroup`` and its subtypes in favor of a single method on
:js:class:`~openerp.web.Query`:
:js:func:`~openerp.web.Query.group_by`.
Migration guide
~~~~~~~~~~~~~~~
Rationale
~~~~~~~~~
While the ``DataGroup`` API worked (mostly), it is quite odd and
alien-looking, a bit too Smalltalk-inspired (behaves like a
self-contained flow-control structure for reasons which may or may not
have been good).
Because it is heavily related to ``DataSet`` (as it *yields*
``DataSet`` objects), deprecating ``DataSet`` automatically deprecates
``DataGroup`` (if we want to stay consistent), which is a good time to
make the API more imperative and look more like what most developers
are used to.

View File

@ -8,6 +8,17 @@ Welcome to OpenERP Web's documentation!
Contents:
.. toctree::
:maxdepth: 1
changelog-6.2
async
rpc
Older stuff
-----------
.. toctree::
:maxdepth: 2

345
doc/rpc.rst Normal file
View File

@ -0,0 +1,345 @@
Outside the box: network interactions
=====================================
Building static displays is all nice and good and allows for neat
effects (and sometimes you're given data to display from third parties
so you don't have to make any effort), but a point generally comes
where you'll want to talk to the world and make some network requests.
OpenERP Web provides two primary APIs to handle this, a low-level
JSON-RPC based API communicating with the Python section of OpenERP
Web (and of your addon, if you have a Python part) and a high-level
API above that allowing your code to talk directly to the OpenERP
server, using familiar-looking calls.
All networking APIs are :doc:`asynchronous </async>`. As a result, all
of them will return :js:class:`Deferred` objects (whether they resolve
those with values or not). Understanding how those work before before
moving on is probably necessary.
High-level API: calling into OpenERP models
-------------------------------------------
Access to OpenERP object methods (made available through XML-RPC from
the server) is done via the :js:class:`openerp.web.Model` class. This
class maps onto the OpenERP server objects via two primary methods,
:js:func:`~openerp.web.Model.call` and
:js:func:`~openerp.web.Model.query`.
:js:func:`~openerp.web.Model.call` is a direct mapping to the
corresponding method of the OpenERP server object. Its usage is
similar to that of the OpenERP Model API, with three differences:
* The interface is :doc:`asynchronous </async>`, so instead of
returning results directly RPC method calls will return
:js:class:`Deferred` instances, which will themselves resolve to the
result of the matching RPC call.
* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
``__getattr__`` or ``method_missing``, there needs to be an explicit
method to dispatch RPC methods.
* No notion of pooler, the model proxy is instantiated where needed,
not fetched from an other (somewhat global) object
.. code-block:: javascript
var Users = new Model('res.users');
Users.call('change_password', ['oldpassword', 'newpassword'],
{context: some_context}).then(function (result) {
// do something with change_password result
});
:js:func:`~openerp.web.Model.query` is a shortcut for a builder-style
interface to searches (``search`` + ``read`` in OpenERP RPC terms). It
returns a :js:class:`~openerp.web.Query` object which is immutable but
allows building new :js:class:`~openerp.web.Query` instances from the
first one, adding new properties or modifiying the parent object's:
.. code-block:: javascript
Users.query(['name', 'login', 'user_email', 'signature'])
.filter([['active', '=', true], ['company_id', '=', main_company]])
.limit(15)
.all().then(function (users) {
// do work with users records
});
The query is only actually performed when calling one of the query
serialization methods, :js:func:`~openerp.web.Query.all` and
:js:func:`~openerp.web.Query.first`. These methods will perform a new
RPC call every time they are called.
For that reason, it's actually possible to keep "intermediate" queries
around and use them differently/add new specifications on them.
.. js:class:: openerp.web.Model(name)
.. js:attribute:: openerp.web.Model.name
name of the OpenERP model this object is bound to
.. js:function:: openerp.web.Model.call(method[, args][, kwargs])
Calls the ``method`` method of the current model, with the
provided positional and keyword arguments.
:param String method: method to call over rpc on the
:js:attr:`~openerp.web.Model.name`
:param Array<> args: positional arguments to pass to the
method, optional
:param Object<> kwargs: keyword arguments to pass to the
method, optional
:rtype: Deferred<>
.. js:function:: openerp.web.Model.query(fields)
:param Array<String> fields: list of fields to fetch during
the search
:returns: a :js:class:`~openerp.web.Query` object
representing the search to perform
.. js:class:: openerp.web.Query(fields)
The first set of methods is the "fetching" methods. They perform
RPC queries using the internal data of the object they're called
on.
.. js:function:: openerp.web.Query.all()
Fetches the result of the current
:js:class:`~openerp.web.Query` object's search.
:rtype: Deferred<Array<>>
.. js:function:: openerp.web.Query.first()
Fetches the **first** result of the current
:js:class:`~openerp.web.Query`, or ``null`` if the current
:js:class:`~openerp.web.Query` does have any result.
:rtype: Deferred<Object | null>
.. js:function:: openerp.web.Query.count()
Fetches the number of records the current
:js:class:`~openerp.web.Query` would retrieve.
:rtype: Deferred<Number>
.. js:function:: openerp.web.Query.group_by(grouping...)
Fetches the groups for the query, using the first specified
grouping parameter
:param Array<String> grouping: Lists the levels of grouping
asked of the server. Grouping
can actually be an array or
varargs.
:rtype: Deferred<Array<openerp.web.Group>> | null
The second set of methods is the "mutator" methods, they create a
**new** :js:class:`~openerp.web.Query` object with the relevant
(internal) attribute either augmented or replaced.
.. js:function:: openerp.web.Query.context(ctx)
Adds the provided ``ctx`` to the query, on top of any existing
context
.. js:function:: openerp.web.Query.filter(domain)
Adds the provided domain to the query, this domain is
``AND``-ed to the existing query domain.
.. js:function:: opeenrp.web.Query.offset(offset)
Sets the provided offset on the query. The new offset
*replaces* the old one.
.. js:function:: openerp.web.Query.limit(limit)
Sets the provided limit on the query. The new limit *replaces*
the old one.
.. js:function:: openerp.web.Query.order_by(fields…)
Overrides the model's natural order with the provided field
specifications. Behaves much like Django's `QuerySet.order_by
<https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by>`_:
* Takes 1..n field names, in order of most to least importance
(the first field is the first sorting key). Fields are
provided as strings.
* A field specifies an ascending order, unless it is prefixed
with the minus sign "``-``" in which case the field is used
in the descending order
Divergences from Django's sorting include a lack of random sort
(``?`` field) and the inability to "drill down" into relations
for sorting.
Aggregation (grouping)
~~~~~~~~~~~~~~~~~~~~~~
OpenERP has powerful grouping capacities, but they are kind-of strange
in that they're recursive, and level n+1 relies on data provided
directly by the grouping at level n. As a result, while ``read_group``
works it's not a very intuitive API.
OpenERP Web 6.2 eschews direct calls to ``read_group`` in favor of
calling a method of :js:class:`~openerp.web.Query`, `much in the way
it is one in SQLAlchemy
<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
.. code-block:: javascript
some_query.group_by(['field1', 'field2']).then(function (groups) {
// do things with the fetched groups
});
This method is asynchronous when provided with 1..n fields (to group
on) as argument, but it can also be called without any field (empty
fields collection or nothing at all). In this case, instead of
returning a Deferred object it will return ``null``.
When grouping criterion come from a third-party and may or may not
list fields (e.g. could be an empty list), this provides two ways to
test the presence of actual subgroups (versus the need to perform a
regular query for records):
* A check on ``group_by``'s result and two completely separate code
paths
.. code-block:: javascript
var groups;
if (groups = some_query.group_by(gby)) {
groups.then(function (gs) {
// groups
});
}
// no groups
* Or a more coherent code path using :js:func:`when`'s ability to
coerce values into deferreds:
.. code-block:: javascript
$.when(some_query.group_by(gby)).then(function (groups) {
if (!groups) {
// No grouping
} else {
// grouping, even if there are no groups (groups
// itself could be an empty array)
}
});
The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
an array of :js:class:`~openerp.web.data.Group`.
Synchronizing views (provisional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note:: this API may not be final, and may not even remain
While the high-level RPC API is mostly stateless, some objects in
OpenERP Web need to share state information. One of those is OpenERP
views, especially between "collection-based" views (lists, graphs) and
"record-based" views (forms, diagrams), which gets its very own API
for traversing collections of records, the aptly-named
:js:class:`~openerp.web.Traverser`.
A :js:class:`~openerp.web.Traverser` is linked to a
:js:class:`~openerp.web.Model` and is used to iterate over it
asynchronously (and using indexes).
.. js:class:: openerp.web.Traverser(model)
.. js:function:: openerp.web.Traverser.model()
:returns: the :js:class:`~openerp.web.Model` this traverser
instance is bound to
.. js:function:: openerp.web.Traverser.index([idx])
If provided with an index parameter, sets that as the new
index for the traverser.
:param Number idx: the new index for the traverser
:returns: the current index for the traverser
.. js:function:: openerp.web.Traverser.current([fields])
Fetches the traverser's "current" record (that is, the record
at the current index of the traverser)
:param Array<String> fields: fields to return in the record
:rtype: Deferred<>
.. js:function:: openerp.web.Traverser.next([fields])
Increases the traverser's internal index by one, the fetches
the corresponding record. Roughly equivalent to:
.. code-block:: javascript
var idx = traverser.index();
traverser.index(idx+1);
traverser.current();
:param Array<String> fields: fields to return in the record
:rtype: Deferred<>
.. js:function:: openerp.web.Traverser.previous([fields])
Similar to :js:func:`~openerp.web.Traverser.next` but iterates
the traverser backwards rather than forward.
:param Array<String> fields: fields to return in the record
:rtype: Deferred<>
.. js:function:: openerp.web.Traverser.size()
Shortcut to checking the size of the backing model, calling
``traverser.size()`` is equivalent to calling
``traverser.model().query([]).count()``
:rtype: Deferred<Number>
Low-level API: RPC calls to Python side
---------------------------------------
While the previous section is great for calling core OpenERP code
(models code), it does not work if you want to call the Python side of
OpenERP Web.
For this, a lower-level API exists on on
:js:class:`~openerp.web.Connection` objects (usually available through
``openerp.connection``): the ``rpc`` method.
This method simply takes an absolute path (which is the combination of
the Python controller's ``_cp_path`` attribute and the name of the
method you want to call) and a mapping of attributes to values (applied
as keyword arguments on the Python method [#]_). This function fetches
the return value of the Python methods, converted to JSON.
For instance, to call the ``eval_domain_and_context`` of the
:class:`~web.controllers.main.Session` controller:
.. code-block:: javascript
openerp.connection.rpc('/web/session/eval_domain_and_context', {
domains: ds,
contexts: cs
}).then(function (result) {
// handle result
});
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
is not terminal, it returns a query which can still be altered.
.. [#] except for ``context``, which is extracted and stored in the
request object itself.

View File

@ -50,6 +50,19 @@ logging_opts.add_option("--log-config", dest="log_config", default=os.path.join(
help="Logging configuration file", metavar="FILE")
optparser.add_option_group(logging_opts)
testing_opts = optparse.OptionGroup(optparser, "Testing")
testing_opts.add_option('--test-mode', dest='test_mode',
action='store_true', default=False,
help="Starts test mode, which provides a few"
" (utterly unsafe) APIs for testing purposes and"
" sets up a special connector which always raises"
" errors on tentative server access. These errors"
" serialize RPC query information (service,"
" method, arguments list) in the fault_code"
" attribute of the error object returned to the"
" client. This lets javascript code assert the" \
" XMLRPC consequences of its queries.")
optparser.add_option_group(testing_opts)
if __name__ == "__main__":
(options, args) = optparser.parse_args(sys.argv[1:])
@ -78,6 +91,12 @@ if __name__ == "__main__":
options.backend = 'xmlrpc'
os.environ["TZ"] = "UTC"
if options.test_mode:
import web.test_support
import web.test_support.controllers
options.connector = web.test_support.TestConnector()
logging.getLogger('werkzeug').setLevel(logging.WARNING)
if sys.version_info >= (2, 7) and os.path.exists(options.log_config):
with open(options.log_config) as file:
dct = json.load(file)