');
+ this.do_warn("The following fields are invalid :", warnings.join(''));
},
on_saved: function(r, success) {
if (!r.result) {
@@ -807,10 +821,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var ids = this.get_selected_ids();
values["id"] = ids.length > 0 ? ids[0] : false;
_.each(this.fields, function(value_, key) {
- if (_.include(blacklist, key))
+ if (_.include(blacklist, key)) {
return;
- var val = value_.get_value();
- values[key] = val;
+ }
+ values[key] = value_.get_value();
});
return values;
},
@@ -945,6 +959,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
is_create_mode: function() {
return !this.datarecord.id;
},
+ open_translate_dialog: function(field) {
+ return this._super(field);
+ },
});
/**
@@ -988,7 +1005,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
}
});
}
- selector = 'form[version!="7.0"] page,form[version!="7.0"]';
},
render_to: function($target) {
var self = this;
@@ -1122,7 +1138,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
if (found)
return;
- $label = $('').attr({
+ var $label = $('').attr({
'for' : name,
"modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
"string": $field.attr('string'),
@@ -1336,7 +1352,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
return $new_label;
},
handle_common_properties: function($new_element, $node) {
- var str_modifiers = $node.attr("modifiers") || "{}"
+ var str_modifiers = $node.attr("modifiers") || "{}";
var modifiers = JSON.parse(str_modifiers);
var ic = null;
if (modifiers.invisible !== undefined)
@@ -1455,24 +1471,27 @@ instance.web.form.compute_domain = function(expr, fields) {
*/
instance.web.form.InvisibilityChangerMixin = {
init: function(field_manager, invisible_domain) {
- this._ic_field_manager = field_manager
+ var self = this;
+ this._ic_field_manager = field_manager;
this._ic_invisible_modifier = invisible_domain;
this._ic_field_manager.on("view_content_has_changed", this, function() {
- var result = this._ic_invisible_modifier === undefined ? false :
- instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields);
- this.set({"invisible": result});
+ var result = self._ic_invisible_modifier === undefined ? false :
+ instance.web.form.compute_domain(
+ self._ic_invisible_modifier,
+ self._ic_field_manager.fields);
+ self.set({"invisible": result});
});
this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
var check = function() {
- if (this.get("invisible") || this.get('force_invisible')) {
- this.set({"effective_invisible": true});
+ if (self.get("invisible") || self.get('force_invisible')) {
+ self.set({"effective_invisible": true});
} else {
- this.set({"effective_invisible": false});
+ self.set({"effective_invisible": false});
}
};
this.on('change:invisible', this, check);
this.on('change:force_invisible', this, check);
- _.bind(check, this)();
+ check.call(this);
},
start: function() {
this.on("change:effective_invisible", this, this._check_visibility);
@@ -1536,6 +1555,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
var compute_domain = instance.web.form.compute_domain;
var to_set = {};
for (var a in this.modifiers) {
+ if (!this.modifiers.hasOwnProperty(a)) { continue; }
if (!_.include(["invisible"], a)) {
var val = compute_domain(this.modifiers[a], this.view.fields);
to_set[a] = val;
@@ -1682,11 +1702,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
on_confirmed: function() {
var self = this;
- var context = this.node.attrs.context;
- if (context && context.__ref) {
- context = new instance.web.CompoundContext(context);
- context.set_eval_context(this._build_eval_context());
- }
+ var context = this.build_context();
return this.view.do_execute_action(
_.extend({}, this.node.attrs, {context: context}),
@@ -1739,18 +1755,18 @@ instance.web.form.FieldInterface = {
/**
* Get the current value of the widget.
*
- * Must always return a syntaxically correct value to be passed to the "write" method of the osv class in
+ * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
* the OpenERP server, although it is not assumed to respect the constraints applied to the field.
- * For example if the field is marqued as "required", a call to get_value() can return false.
+ * For example if the field is marked as "required", a call to get_value() can return false.
*
* get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
- * return a defaut value according to the type of field.
+ * return a default value according to the type of field.
*
* This method is always assumed to perform synchronously, it can not return a promise.
*
* If there was no user interaction to modify the value of the field, it is always assumed that
* get_value() return the same semantic value than the one passed in the last call to set_value(),
- * altough the syntax can be different. This can be the case for type of fields that have a different
+ * although the syntax can be different. This can be the case for type of fields that have a different
* syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
*/
get_value: function() {},
@@ -1765,7 +1781,7 @@ instance.web.form.FieldInterface = {
*/
is_valid: function() {},
/**
- * Returns true if the field holds a value which is syntaxically correct, ignoring
+ * Returns true if the field holds a value which is syntactically correct, ignoring
* the potential semantic restrictions applied to the field.
*/
is_syntax_valid: function() {},
@@ -1795,6 +1811,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
* @param node
*/
init: function(field_manager, node) {
+ var self = this
this._super(field_manager, node);
this.field_manager = field_manager;
this.name = this.node.attrs.name;
@@ -1809,12 +1826,11 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
this.set({"readonly": this.modifiers['readonly'] === true});
this.set({"force_readonly": false});
var test_effective_readonly = function() {
- this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")});
+ self.set({"effective_readonly": self.get("readonly") || !!self.get("force_readonly")});
};
this.on("change:readonly", this, test_effective_readonly);
this.on("change:force_readonly", this, test_effective_readonly);
- _.bind(test_effective_readonly, this)();
-
+ test_effective_readonly.call(this);
this.on("change:value", this, function() {
if (! this._inhibit_on_change)
this.trigger('changed_value');
@@ -1891,7 +1907,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
*/
delay_focus: function($elem) {
setTimeout(function() {
- $elem.focus();
+ $elem[0].focus();
}, 50);
},
/**
@@ -2234,6 +2250,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
} else {
this.$textarea.attr('disabled', 'disabled');
}
+ this.$element.keyup(function (e) {
+ if (e.which === $.ui.keyCode.ENTER) {
+ e.stopPropagation();
+ }
+ });
this.setupFocus(this.$textarea);
},
set_value: function(value_) {
@@ -2337,6 +2358,7 @@ instance.web.form.FieldTextHtml = instance.web.form.FieldText.extend({
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
template: 'FieldBoolean',
start: function() {
+ var self = this;
this._super.apply(this, arguments);
this.$checkbox = $("input", this.$element);
this.setupFocus(this.$checkbox);
@@ -2344,10 +2366,10 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
this.set({'value': this.$checkbox.is(':checked')});
}, this));
var check_readonly = function() {
- this.$checkbox.prop('disabled', this.get("effective_readonly"));
+ self.$checkbox.prop('disabled', self.get("effective_readonly"));
};
this.on("change:effective_readonly", this, check_readonly);
- _.bind(check_readonly, this)();
+ check_readonly.call(this);
},
set_value: function(value_) {
this._super.apply(this, arguments);
@@ -2960,6 +2982,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
selectable: self.multi_selection,
sortable: false,
import_enabled: false,
+ deletable: true
});
if (self.get("effective_readonly")) {
_.extend(view.options, {
@@ -3189,6 +3212,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
form: 'instance.web.form.One2ManyFormView',
kanban: 'instance.web.form.One2ManyKanbanView',
});
+ this.__ignore_blur = false;
},
switch_view: function(mode, unused) {
if (mode !== 'form') {
@@ -3238,19 +3262,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this._super(parent, dataset, view_id, _.extend(options || {}, {
ListType: instance.web.form.One2ManyList
}));
+ this.on('edit:before', this, this.proxy('_before_edit'));
+ this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
+
+ this.records
+ .bind('add', this.proxy("changed_records"))
+ .bind('edit', this.proxy("changed_records"))
+ .bind('remove', this.proxy("changed_records"));
+ },
+ start: function () {
+ var ret = this._super();
+ this.$element
+ .off('mousedown.handleButtons')
+ .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
+ return ret;
+ },
+ changed_records: function () {
+ this.o2m.trigger_on_change();
},
is_valid: function () {
- var form;
- // A list not being edited is always valid
- if (!(form = this.first_edition_form())) {
- return true;
- }
+ var form = this.editor.form;
+
// If the form has not been modified, the view can only be valid
// NB: is_dirty will also be set on defaults/onchanges/whatever?
// oe_form_dirty seems to only be set on actual user actions
if (!form.$element.is('.oe_form_dirty')) {
return true;
}
+ this.o2m._dirty_flag = true;
// Otherwise validate internal form
return _(form.fields).chain()
@@ -3261,19 +3300,6 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
.all(_.identity)
.value();
},
- first_edition_form: function () {
- var get_form = function (group_or_list) {
- if (group_or_list.edition) {
- return group_or_list.edition_form;
- }
- return _(group_or_list.children).chain()
- .map(get_form)
- .compact()
- .first()
- .value();
- };
- return get_form(this.groups);
- },
do_add_record: function () {
if (this.options.editable) {
this._super.apply(this, arguments);
@@ -3328,55 +3354,58 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
});
},
do_button_action: function (name, id, callback) {
- var _super = _.bind(this._super, this);
-
- this.o2m.view.do_save().then(function () {
- _super(name, id, callback);
- });
- }
-});
-instance.web.form.One2ManyList = instance.web.ListView.List.extend({
- KEY_RETURN: 13,
- // blurring caused by hitting the [Return] key, should skip the
- // autosave-on-blur and let the handler for [Return] do its thing
- __return_blur: false,
- render_row_as_form: function () {
+ if (!_.isNumber(id)) {
+ instance.webclient.notification.warn(
+ _t("Action Button"),
+ _t("The o2m record must be saved before an action can be used"));
+ return;
+ }
+ var parent_form = this.o2m.view;
var self = this;
- return this._super.apply(this, arguments).then(function () {
- // Replace the "Save Row" button with "Cancel Edition"
- self.edition_form.$element
- .undelegate('button.oe_list_edit_row_save', 'click')
- .delegate('button.oe_list_edit_row_save', 'click', function () {
- self.cancel_pending_edition();
- });
-
- // Overload execute_action on the edition form to perform a simple
- // reload_record after the action is done, rather than fully
- // reload the parent view (or something)
- var _execute_action = self.edition_form.do_execute_action;
- self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) {
- return _execute_action.call(this, action, dataset, record_id, function () {
- self.view.reload_record(
- self.view.records.get(record_id));
- });
- };
-
- self.edition_form.on('blurred', null, function () {
- if (self.__return_blur) {
- delete self.__return_blur;
- return;
- }
- if (!self.edition_form.widget_is_stopped) {
- self.view.ensure_saved();
- }
- });
+ this.ensure_saved().pipe(function () {
+ return parent_form.do_save();
+ }).then(function () {
+ self.handle_button(name, id, callback);
});
},
- on_row_keyup: function (e) {
- if (e.which === this.KEY_RETURN) {
- this.__return_blur = true;
+
+ _before_edit: function () {
+ this.__ignore_blur = false;
+ this.editor.form.on('blurred', this, this._on_form_blur);
+ },
+ _before_unedit: function () {
+ this.editor.form.off('blurred', this, this._on_form_blur);
+ },
+ _button_down: function () {
+ // If a button is clicked (usually some sort of action button), it's
+ // the button's responsibility to ensure the editable list is in the
+ // correct state -> ignore form blurring
+ this.__ignore_blur = true;
+ },
+ /**
+ * Handles blurring of the nested form (saves the currently edited row),
+ * unless the flag to ignore the event is set to ``true``
+ *
+ * Makes the internal form go away
+ */
+ _on_form_blur: function () {
+ if (this.__ignore_blur) {
+ this.__ignore_blur = false;
+ return;
}
- this._super(e);
+ // FIXME: why isn't there an API for this?
+ if (this.editor.form.$element.hasClass('oe_form_dirty')) {
+ this.save_edition();
+ return;
+ }
+ this.cancel_edition();
+ },
+ keyup_ENTER: function () {
+ // blurring caused by hitting the [Return] key, should skip the
+ // autosave-on-blur and let the handler for [Return] do its thing (save
+ // the current row *anyway*, then create a new one/edit the next one)
+ this.__ignore_blur = true;
+ this._super.apply(this, arguments);
}
});
@@ -3545,7 +3574,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
},
start: function() {
this._super.apply(this, arguments);
- this.$element.addClass('oe_form_field_many2many');
+ this.$element.addClass('oe_form_field oe_form_field_many2many');
var self = this;
diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js
index edc53f82042..f68f1354099 100644
--- a/addons/web/static/src/js/view_list.js
+++ b/addons/web/static/src/js/view_list.js
@@ -142,7 +142,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
});
},
/**
- * View startup method, the default behavior is to set the ``oe_listw``
+ * View startup method, the default behavior is to set the ``oe_list``
* class on its root element and to perform an RPC load call.
*
* @returns {$.Deferred} loading promise
@@ -288,7 +288,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
}
this.$buttons.find('.oe_list_add')
.click(this.proxy('do_add_record'))
- .prop('disabled', grouped && this.options.editable);
+ .prop('disabled', grouped);
this.$buttons.on('click', '.oe_list_button_import', function() {
self.on_sidebar_import();
return false;
@@ -411,20 +411,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (column.modifiers) {
var modifiers = JSON.parse(column.modifiers);
column.modifiers_for = function (fields) {
- if (!modifiers.invisible) {
- return {};
+ var out = {};
+
+ for (var attr in modifiers) {
+ if (!modifiers.hasOwnProperty(attr)) { continue; }
+ var modifier = modifiers[attr];
+ out[attr] = _.isBoolean(modifier)
+ ? modifier
+ : domain_computer(modifier, fields);
}
- return {
- 'invisible': domain_computer(modifiers.invisible, fields)
- };
+
+ return out;
};
if (modifiers['tree_invisible']) {
column.invisible = '1';
} else {
delete column.invisible;
}
+ column.modifiers = modifiers;
} else {
column.modifiers_for = noop;
+ column.modifiers = {};
}
return column;
};
@@ -436,10 +443,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
if (grouped) {
this.columns.unshift({
id: '_group', tag: '', string: _t("Group"), meta: true,
- modifiers_for: function () { return {}; }
+ modifiers_for: function () { return {}; },
+ modifiers: {}
}, {
id: '_count', tag: '', string: '#', meta: true,
- modifiers_for: function () { return {}; }
+ modifiers_for: function () { return {}; },
+ modifiers: {}
});
}
@@ -667,6 +676,19 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
* @param {Function} callback should be called after the action is executed, if non-null
*/
do_button_action: function (name, id, callback) {
+ this.handle_button(name, id, callback);
+ },
+ /**
+ * Base handling of buttons, can be called when overriding do_button_action
+ * in order to bypass parent overrides.
+ *
+ * This method should not be overridden.
+ *
+ * @param {String} name action name
+ * @param {Object} id id of the record the action should be called on
+ * @param {Function} callback should be called after the action is executed, if non-null
+ */
+ handle_button: function (name, id, callback) {
var action = _.detect(this.columns, function (field) {
return field.name === name;
});
@@ -911,27 +933,42 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
this.record_callbacks = {
'remove': function (event, record) {
- var $row = self.$current.find(
- '[data-id=' + record.get('id') + ']');
+ var $row = self.$current.children(
+ '[data-id=' + record.get('id') + ']');
var index = $row.data('index');
$row.remove();
self.refresh_zebra(index);
},
'reset': function () { return self.on_records_reset(); },
- 'change': function (event, record) {
- var $row = self.$current.find('[data-id=' + record.get('id') + ']');
+ 'change': function (event, record, attribute, value, old_value) {
+ var $row;
+ if (attribute === 'id') {
+ if (old_value) {
+ throw new Error("Setting 'id' attribute on existing record "
+ + JSON.stringify(record.attributes));
+ }
+ if (!_.contains(self.dataset.ids, value)) {
+ // add record to dataset if not already in (added by
+ // the form view?)
+ self.dataset.ids.splice(
+ self.records.indexOf(record), 0, value);
+ }
+ // Set id on new record
+ $row = self.$current.children('[data-id=false]');
+ } else {
+ $row = self.$current.children(
+ '[data-id=' + record.get('id') + ']');
+ }
$row.replaceWith(self.render_record(record));
},
'add': function (ev, records, record, index) {
- var $new_row = $('
').attr({
- 'data-id': record.get('id')
- });
+ var $new_row = $(self.render_record(record));
if (index === 0) {
$new_row.prependTo(self.$current);
} else {
var previous_record = records.at(index-1),
- $previous_sibling = self.$current.find(
+ $previous_sibling = self.$current.children(
'[data-id=' + previous_record.get('id') + ']');
$new_row.insertAfter($previous_sibling);
}
@@ -975,11 +1012,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
e.stopPropagation();
})
.delegate('tr', 'click', function (e) {
- e.stopPropagation();
var row_id = self.row_id(e.currentTarget);
- if (row_id !== undefined) {
+ if (row_id) {
+ e.stopPropagation();
if (!self.dataset.select_id(row_id)) {
- throw "Could not find id in dataset"
+ throw new Error("Could not find id in dataset");
}
self.row_clicked(e);
}
@@ -1139,6 +1176,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
* @returns {String} QWeb rendering of the selected record
*/
render_record: function (record) {
+ var self = this;
var index = this.records.indexOf(record);
return QWeb.render('ListView.row', {
columns: this.columns,
@@ -1147,7 +1185,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
row_parity: (index % 2 === 0) ? 'even' : 'odd',
view: this.view,
render_cell: function () {
- return this.render_cell.apply(this, arguments); }
+ return self.render_cell.apply(self, arguments); }
});
},
/**
@@ -1831,7 +1869,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
* @returns this
*/
remove: function (record) {
- var index = _(this.records).indexOf(record);
+ var index = this.indexOf(record);
if (index === -1) {
_(this._proxies).each(function (proxy) {
proxy.remove(record);
@@ -1847,13 +1885,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
return this;
},
- _onRecordEvent: function (event, record, options) {
+ _onRecordEvent: function (event) {
+ switch(event) {
// don't propagate reset events
- if (event === 'reset') { return; }
+ case 'reset': return;
+ case 'change:id':
+ var record = arguments[1];
+ var new_value = arguments[2];
+ var old_value = arguments[3];
+ // [change:id, record, new_value, old_value]
+ if (this._byId[old_value] === record) {
+ delete this._byId[old_value];
+ this._byId[new_value] = record;
+ }
+ break;
+ }
this.trigger.apply(this, arguments);
},
// underscore-type methods
+ find: function (callback) {
+ var record;
+ for(var section in this._proxies) {
+ if (!this._proxies.hasOwnProperty(section)) {
+ continue
+ }
+ if ((record = this._proxies[section].find(callback))) {
+ return record;
+ }
+ }
+ for(var i=0; i', {
- id: _.uniqueId('oe-editable-row-'),
- 'data-id': record_id,
- 'class': (row ? $(row).attr('class') : ''),
- click: function (e) {e.stopPropagation();}
- })
- .addClass('oe_form oe_form_container')
- .delegate('button.oe_list_edit_row_save', 'click', function () {
- self.save_row();
- })
- .delegate('button', 'keyup', function (e) {
- e.stopImmediatePropagation();
- })
- .keyup(function () {
- return self.on_row_keyup.apply(self, arguments); })
- .keydown(function (e) { e.stopPropagation(); })
- .keypress(function (e) {
- if (e.which === KEY_RETURN) {
- return false;
- }
- });
-
- if (row) {
- $new_row.replaceAll(row);
- } else if (self.options.editable) {
- var $last_child = self.$current.children('tr:last');
- if (self.records.length) {
- if (self.options.editable === 'top') {
- $new_row.insertBefore(
- self.$current.children('[data-id]:first'));
- } else {
- $new_row.insertAfter(
- self.$current.children('[data-id]:last'));
- }
- } else {
- $new_row.prependTo(self.$current);
- }
- if ($last_child.is(':not([data-id])')) {
- $last_child.remove();
- }
- }
- self.edition = true;
- self.edition_id = record_id;
- self.dataset.index = _(self.dataset.ids).indexOf(record_id);
- if (self.dataset.index === -1) {
- self.dataset.index = null;
- }
- self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), {
- $element: $new_row,
- editable_list: self
- });
- // put in $.when just in case FormView.on_loaded becomes asynchronous
- return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
- $new_row.find('> td')
- .end()
- .find('td:last').removeClass('oe_list_field_cell').end();
- // pad in case of groupby
- _(self.columns).each(function (column) {
- if (column.meta) {
- $new_row.prepend('
');
- }
- });
- // Add column for the save, if
- // there is none in the list
- if (!self.options.deletable) {
- self.view.pad_columns(
- 1, {except: $new_row});
- }
-
- self.edition_form.do_show();
- });
- });
- },
- handle_onwrite: function (source_record_id) {
- var self = this;
- var on_write_callback = self.view.fields_view.arch.attrs.on_write;
- if (!on_write_callback) { return; }
- this.dataset.call(on_write_callback, [source_record_id], function (ids) {
- _(ids).each(function (id) {
- var record = self.records.get(id);
- if (!record) {
- // insert after the source record
- var index = self.records.indexOf(
- self.records.get(source_record_id)) + 1;
- record = new instance.web.list.Record({id: id});
- self.records.add(record, {at: index});
- self.dataset.ids.splice(index, 0, id);
- }
- self.reload_record(record);
- });
- });
- },
- /**
- * Saves the current row, and returns a Deferred resolving to an object
- * with the following properties:
- *
- * ``created``
- * Boolean flag indicating whether the record saved was being created
- * (``true`` or edited (``false``)
- * ``edited_record``
- * The result of saving the record (either the newly created record,
- * or the post-edition record), after insertion in the Collection if
- * needs be.
- *
- * @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
- */
- save_row: function () {
- //noinspection JSPotentiallyInvalidConstructorUsage
- var self = this;
- return this.edition_form
- .do_save(null, this.options.editable === 'top')
- .pipe(function (result) {
- if (result.created && !self.edition_id) {
- self.records.add({id: result.result},
- {at: self.options.editable === 'top' ? 0 : null});
- self.edition_id = result.result;
- }
- var edited_record = self.records.get(self.edition_id);
-
- return $.when(
- self.handle_onwrite(self.edition_id),
- self.cancel_pending_edition().then(function () {
- $(self).trigger('saved', [self.dataset]);
- })).pipe(function () {
- return {
- created: result.created || false,
- edited_record: edited_record
- };
- });
- });
- },
- /**
- * If the current list is being edited, ensures it's saved
- */
- ensure_saved: function () {
- if (this.edition) {
- // kinda-hack-ish: if the user has entered data in a field,
- // oe_form_dirty will be set on the form so save, otherwise
- // discard the current (entirely empty) line
- if (this.edition_form.$element.is('.oe_form_dirty')) {
- return this.save_row();
- }
- return this.cancel_pending_edition();
- }
- //noinspection JSPotentiallyInvalidConstructorUsage
- return $.when();
- },
- /**
- * Cancels the edition of the row for the current dataset index
- */
- cancel_edition: function () {
- this.cancel_pending_edition();
- },
- /**
- * Edits record currently selected via dataset
- */
- edit_record: function (record_id) {
- this.render_row_as_form(
- this.$current.find('[data-id=' + record_id + ']'));
- $(this).trigger(
- 'edit',
- [record_id, this.dataset]);
- },
- new_record: function () {
- this.render_row_as_form();
- },
- render_record: function (record) {
- var index = this.records.indexOf(record),
- self = this;
- // FIXME: context dict should probably be extracted cleanly
- return QWeb.render('ListView.row', {
- columns: this.columns,
- options: this.options,
- record: record,
- row_parity: (index % 2 === 0) ? 'even' : 'odd',
- view: this.view,
- render_cell: function () {
- return self.render_cell.apply(self, arguments); },
- edited: !!this.edition_form
- });
+ return null;
}
});
-
- instance.web.ListEditableFormView = instance.web.FormView.extend({
- init: function() {
- this._super.apply(this, arguments);
- this.rendering_engine = new instance.web.ListEditableRenderingEngine(this);
- this.options.initial_mode = "edit";
- },
- renderElement: function() {}
- });
-
- instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
- init: function(view) {
- this.view = view;
- },
- set_fields_view: function(fields_view) {
- this.fvg = fields_view;
- },
- set_tags_registry: function(tags_registry) {
- this.tags_registry = tags_registry;
- },
- set_fields_registry: function(fields_registry) {
- this.fields_registry = fields_registry;
- },
- render_to: function($element) {
- var self = this;
-
- var xml = instance.web.json_node_to_xml(this.fvg.arch);
- var $xml = $(xml);
-
- if (this.view.editable_list.options.selectable)
- $("
").appendTo($element);
-
- $xml.children().each(function(i, el) {
- var modifiers = JSON.parse($(el).attr("modifiers") || "{}");
- var $td = $("
");
- if (modifiers.tree_invisible === true)
- $td.hide();
- var tag_name = el.tagName.toLowerCase();
- var w;
- if (tag_name === "field") {
- var name = $(el).attr("name");
- var key = $(el).attr('widget') || self.fvg.fields[name].type;
- var obj = self.view.fields_registry.get_object(key);
- w = new (obj)(self.view, instance.web.xml_to_json(el));
- self.view.register_field(w, $(el).attr("name"));
- } else {
- var obj = self.tags_registry.get_object(tag_name);
- w = new (obj)(self.view, instance.web.xml_to_json(el));
- }
- w.appendTo($td);
- $td.appendTo($element);
- });
- $(QWeb.render('ListView.row.save')).appendTo($element);
- },
- });
};
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index bd102d867de..ccf34576a1a 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -1074,11 +1074,13 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({
if (self.view.translatable_fields && self.view.translatable_fields.length) {
self.do_load_fields_values(function() {
sup.call(self);
+ // desactivated because it created an exception, plus it does not seem very useful
+ /*
if (field) {
var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child');
self.$element.scrollTo($field_input);
$field_input.focus();
- }
+ }*/
});
} else {
sup.call(self);
@@ -1346,7 +1348,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
if (typeof(node) === 'string') {
return sindent + node;
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
- throw("Node a json node");
+ throw new Error(
+ _.str.sprintf("Node [%s] is not a JSONified XML node",
+ JSON.stringify(node)));
}
for (var attr in node.attrs) {
var vattr = node.attrs[attr];
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 55280f6badb..ed63b5fbfbe 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -630,9 +630,9 @@
-
+ orImport
-
+
@@ -652,6 +652,7 @@
diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js
new file mode 100644
index 00000000000..2313195f626
--- /dev/null
+++ b/addons/web/static/test/list-editable.js
@@ -0,0 +1,352 @@
+$(document).ready(function () {
+ var $fix = $('#qunit-fixture');
+
+ var instance;
+ var baseSetup = function () {
+ instance = openerp.testing.instanceFor('list_editable');
+
+ openerp.testing.loadTemplate(instance);
+
+ openerp.testing.mockifyRPC(instance);
+ };
+
+
+ /**
+ *
+ * @param {String} name
+ * @param {Object} [attrs]
+ * @param {String} [attrs.type="char"]
+ * @param {Boolean} [attrs.required]
+ * @param {Boolean} [attrs.invisible]
+ * @param {Boolean} [attrs.readonly]
+ * @return {Object}
+ */
+ function field(name, attrs) {
+ attrs = attrs || {};
+ attrs.name = name;
+ return _.defaults(attrs, {
+ type: 'char'
+ });
+ }
+
+ /**
+ * @param {Array} fields
+ * @return {Object}
+ */
+ function makeFormView(fields) {
+ var fobj = {};
+ _(fields).each(function (field) {
+ fobj[field.name] = {
+ type: field.type,
+ string: field.string
+ };
+ });
+ var children = _(fields).map(function (field) {
+ return {
+ tag: 'field',
+ attrs: {
+ name: field.name,
+ modifiers: JSON.stringify({
+ required: field.required,
+ invisible: field.invisible,
+ readonly: field.readonly
+ })
+ }
+ }
+ });
+ return {
+ arch: {
+ tag: 'form',
+ attrs: {
+ version: '7.0',
+ 'class': 'oe_form_container'
+ },
+ children: children
+ },
+ fields: fobj
+ };
+ }
+
+ module('editor', {
+ setup: baseSetup
+ });
+ asyncTest('base-state', 2, function () {
+ var e = new instance.web.list.Editor({
+ dataset: {},
+ edition_view: function () {
+ return makeFormView();
+ }
+ });
+ e.appendTo($fix)
+ .always(start)
+ .fail(function (error) { ok(false, error && error.message); })
+ .done(function () {
+ ok(!e.is_editing(), "should not be editing");
+ ok(e.form instanceof instance.web.FormView,
+ "should use default form type");
+ });
+ });
+ asyncTest('toggle-edition-save', 4, function () {
+ instance.connection.responses['/web/dataset/call_kw:create'] = function () {
+ return { result: 42 };
+ };
+ instance.connection.responses['/web/dataset/call_kw:read'] = function () {
+ return { result: [{
+ id: 42,
+ a: false,
+ b: false,
+ c: false
+ }]};
+ };
+ var e = new instance.web.list.Editor({
+ dataset: new instance.web.DataSetSearch(),
+ prepends_on_create: function () { return false; },
+ edition_view: function () {
+ return makeFormView([ field('a'), field('b'), field('c') ]);
+ }
+ });
+ var counter = 0;
+ e.appendTo($fix)
+ .pipe(function () {
+ return e.edit({}, function () {
+ ++counter;
+ });
+ })
+ .pipe(function (form) {
+ ok(e.is_editing(), "should be editing");
+ equal(counter, 3, "should have configured all fields");
+ return e.save();
+ })
+ .always(start)
+ .fail(function (error) { ok(false, error && error.message); })
+ .done(function (record) {
+ ok(!e.is_editing(), "should have stopped editing");
+ equal(record.id, 42, "should have newly created id");
+ })
+ });
+ asyncTest('toggle-edition-cancel', 2, function () {
+ instance.connection.responses['/web/dataset/call_kw:create'] = function () {
+ return { result: 42 };
+ };
+ var e = new instance.web.list.Editor({
+ dataset: new instance.web.DataSetSearch(),
+ prepends_on_create: function () { return false; },
+ edition_view: function () {
+ return makeFormView([ field('a'), field('b'), field('c') ]);
+ }
+ });
+ var counter = 0;
+ e.appendTo($fix)
+ .pipe(function () {
+ return e.edit({}, function () {
+ ++counter;
+ });
+ })
+ .pipe(function (form) {
+ return e.cancel();
+ })
+ .always(start)
+ .fail(function (error) { ok(false, error && error.message); })
+ .done(function (record) {
+ ok(!e.is_editing(), "should have stopped editing");
+ ok(!record.id, "should have no id");
+ })
+ });
+ asyncTest('toggle-save-required', 2, function () {
+ instance.connection.responses['/web/dataset/call_kw:create'] = function () {
+ return { result: 42 };
+ };
+ var e = new instance.web.list.Editor({
+ do_warn: function () {
+ warnings++;
+ },
+ dataset: new instance.web.DataSetSearch(),
+ prepends_on_create: function () { return false; },
+ edition_view: function () {
+ return makeFormView([
+ field('a', {required: true}), field('b'), field('c') ]);
+ }
+ });
+ var counter = 0;
+ var warnings = 0;
+ e.appendTo($fix)
+ .pipe(function () {
+ return e.edit({}, function () {
+ ++counter;
+ });
+ })
+ .pipe(function (form) {
+ return e.save();
+ })
+ .always(start)
+ .done(function () { ok(false, "cancel should not succeed"); })
+ .fail(function () {
+ equal(warnings, 1, "should have been warned");
+ ok(e.is_editing(), "should have kept editing");
+ })
+ });
+
+ module('list-edition', {
+ setup: function () {
+ baseSetup();
+
+ var records = {};
+ _.extend(instance.connection.responses, {
+ '/web/listview/load': function () {
+ return {result: {
+ type: 'tree',
+ fields: {
+ a: {type: 'char', string: "A"},
+ b: {type: 'char', string: "B"},
+ c: {type: 'char', string: "C"}
+ },
+ arch: {
+ tag: 'tree',
+ attrs: {},
+ children: [
+ {tag: 'field', attrs: {name: 'a'}},
+ {tag: 'field', attrs: {name: 'b'}},
+ {tag: 'field', attrs: {name: 'c'}}
+ ]
+ }
+ }};
+ },
+ '/web/dataset/call_kw:create': function (params) {
+ records[42] = _.extend({}, params.params.args[0]);
+ return {result: 42};
+ },
+ '/web/dataset/call_kw:read': function (params) {
+ var id = params.params.args[0][0];
+ if (id in records) {
+ return {result: [records[id]]};
+ }
+ return {result: []};
+ }
+ })
+ }
+ });
+ asyncTest('newrecord', 6, function () {
+ var got_defaults = false;
+ instance.connection.responses['/web/dataset/call_kw:default_get'] = function (params) {
+ var fields = params.params.args[0];
+ deepEqual(
+ fields, ['a', 'b', 'c'],
+ "should ask defaults for all fields");
+ got_defaults = true;
+ return {result: {
+ a: "qux",
+ b: "quux"
+ }};
+ };
+
+ var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
+ var l = new instance.web.ListView({}, ds);
+ l.set_editable(true);
+
+ l.appendTo($fix)
+ .pipe(l.proxy('reload_content'))
+ .pipe(function () {
+ return l.start_edition();
+ })
+ .always(start)
+ .pipe(function () {
+ ok(got_defaults, "should have fetched default values for form");
+ return l.save_edition();
+ })
+ .pipe(function (result) {
+ ok(result.created, "should yield newly created record");
+ equal(result.record.get('a'), "qux",
+ "should have used default values");
+ equal(result.record.get('b'), "quux",
+ "should have used default values");
+ ok(!result.record.get('c'),
+ "should have no value if there was no default");
+ })
+ .fail(function (e) { ok(false, e && e.message || e); });
+ });
+
+ module('list-edition-events', {
+ setup: function () {
+ baseSetup();
+ _.extend(instance.connection.responses, {
+ '/web/listview/load': function () {
+ return {result: {
+ type: 'tree',
+ fields: {
+ a: {type: 'char', string: "A"},
+ b: {type: 'char', string: "B"},
+ c: {type: 'char', string: "C"}
+ },
+ arch: {
+ tag: 'tree',
+ attrs: {},
+ children: [
+ {tag: 'field', attrs: {name: 'a'}},
+ {tag: 'field', attrs: {name: 'b'}},
+ {tag: 'field', attrs: {name: 'c'}}
+ ]
+ }
+ }};
+ },
+ '/web/dataset/call_kw:read': function (params) {
+ return {result: [{
+ id: 1,
+ a: 'foo',
+ b: 'bar',
+ c: 'baz'
+ }]};
+ }
+ });
+ }
+ });
+ asyncTest('edition events', 4, function () {
+ var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
+ var o = {
+ counter: 0,
+ onEvent: function (e) { this.counter++; }
+ };
+ var l = new instance.web.ListView({}, ds);
+ l.set_editable(true);
+ l.on('edit:before edit:after', o, o.onEvent);
+ l.appendTo($fix)
+ .pipe(l.proxy('reload_content'))
+ .always(start)
+ .pipe(function () {
+ ok(l.options.editable, "should be editable");
+ equal(o.counter, 0, "should have seen no event yet");
+ return l.start_edition(l.records.get(1));
+ })
+ .pipe(function () {
+ ok(l.editor.is_editing(), "should be editing");
+ equal(o.counter, 2, "should have seen two edition events");
+ })
+ .fail(function (e) { ok(false, e && e.message); });
+ });
+
+ asyncTest('edition events: cancelling', 3, function () {
+ var edit_after = false;
+ var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
+ var l = new instance.web.ListView({}, ds);
+ l.set_editable(true);
+ l.on('edit:before', {}, function (e) {
+ e.cancel = true;
+ });
+ l.on('edit:after', {}, function () {
+ edit_after = true;
+ });
+ l.appendTo($fix)
+ .pipe(l.proxy('reload_content'))
+ .always(start)
+ .pipe(function () {
+ ok(l.options.editable, "should be editable");
+ return l.start_edition();
+ })
+ // cancelling an event rejects the deferred
+ .pipe($.Deferred().reject(), function () {
+ ok(!l.editor.is_editing(), "should not be editing");
+ ok(!edit_after, "should not have fired the edit:after event");
+ return $.when();
+ })
+ .fail(function (e) { ok(false, e && e.message || e); });
+ });
+});
diff --git a/addons/web/static/test/list-utils.js b/addons/web/static/test/list-utils.js
index 7890e71c6bc..6200ba788ce 100644
--- a/addons/web/static/test/list-utils.js
+++ b/addons/web/static/test/list-utils.js
@@ -133,7 +133,7 @@ $(document).ready(function () {
strictEqual(changed, 1);
});
- module('list-collections-degenerate', {
+ module('list-collections', {
setup: function () {
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
@@ -145,7 +145,7 @@ $(document).ready(function () {
window.openerp.web.list(openerp);
}
});
- test('Fetch from collection', function () {
+ test('degenerate-fetch', function () {
var c = new openerp.web.list.Collection();
strictEqual(c.length, 0);
c.add({id: 1, value: 2});
@@ -163,7 +163,7 @@ $(document).ready(function () {
strictEqual(r2.get('id'), 1);
strictEqual(r2.get('value'), 2);
});
- test('Add at index', function () {
+ test('degenerate-indexed-add', function () {
var c = new openerp.web.list.Collection([
{id: 1, value: 5},
{id: 2, value: 10},
@@ -175,7 +175,7 @@ $(document).ready(function () {
strictEqual(c.at(1).get('value'), 55);
strictEqual(c.at(3).get('value'), 20);
});
- test('Remove record', function () {
+ test('degenerate-remove', function () {
var c = new openerp.web.list.Collection([
{id: 1, value: 5},
{id: 2, value: 10},
@@ -188,7 +188,7 @@ $(document).ready(function () {
equal(c.get(2), undefined);
strictEqual(c.at(1).get('value'), 20);
});
- test('Remove unbind', function () {
+ test('degenerate-remove-bound', function () {
var changed = false,
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]);
c.bind('change', function () { changed = true; });
@@ -198,7 +198,7 @@ $(document).ready(function () {
ok(!changed, 'removed records should not trigger events in their ' +
'parent collection');
});
- test('Reset', function () {
+ test('degenerate-reset', function () {
var event, obj, c = new openerp.web.list.Collection([
{id: 1, value: 5},
{id: 2, value: 10},
@@ -218,7 +218,7 @@ $(document).ready(function () {
strictEqual(c.length, 1);
strictEqual(c.get(42).get('value'), 55);
});
- test('Reset unbind', function () {
+ test('degenerate-reset-bound', function () {
var changed = false,
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]);
c.bind('change', function () { changed = true; });
@@ -229,7 +229,7 @@ $(document).ready(function () {
'parent collection');
});
- test('Events propagation', function () {
+ test('degenerate-propagations', function () {
var values = [];
var c = new openerp.web.list.Collection([
{id: 1, value: 5},
@@ -260,6 +260,82 @@ $(document).ready(function () {
c.at(1).set('wealth', 5);
strictEqual(total, 47);
});
+ test('degenerate-successor', function () {
+ var root = new openerp.web.list.Collection([
+ {id: 1, value: 1},
+ {id: 2, value: 2},
+ {id: 3, value: 3},
+ {id: 4, value: 5},
+ {id: 5, value: 8}
+ ]);
+
+ deepEqual(root.succ(root.at(2)).attributes,
+ root.at(3).attributes,
+ "should return the record at (index + 1) from the pivot");
+ equal(root.succ(root.at(4)), null,
+ "should return null as successor to last record");
+ deepEqual(root.succ(root.at(4), {wraparound: true}).attributes,
+ root.at(0).attributes,
+ "should return index 0 as successor to last record if" +
+ " wraparound is set");
+ deepEqual(root.succ(root.at(2), {wraparound: true}).attributes,
+ root.at(3).attributes,
+ "wraparound should have no effect if not succ(last_record)");
+ });
+ test('successor', function () {
+ var root = new openerp.web.list.Collection();
+ root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
+ root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
+ root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
+
+ deepEqual(root.succ(root.get(3)).attributes,
+ root.get(4).attributes,
+ "should get successor");
+ equal(root.succ(root.get(4)),
+ null,
+ "successors do not cross collections");
+ deepEqual(root.succ(root.get(4), {wraparound: true}).attributes,
+ root.get(3).attributes,
+ "should wraparound within a collection");
+ });
+ test('degenerate-predecessor', function () {
+ var root = new openerp.web.list.Collection([
+ {id: 1, value: 1},
+ {id: 2, value: 2},
+ {id: 3, value: 3},
+ {id: 4, value: 5},
+ {id: 5, value: 8}
+ ]);
+
+ deepEqual(root.pred(root.at(2)).attributes,
+ root.at(1).attributes,
+ "should return the record at (index - 1) from the pivot");
+ equal(root.pred(root.at(0)), null,
+ "should return null as predecessor to first record");
+ deepEqual(root.pred(root.at(0), {wraparound: true}).attributes,
+ root.at(4).attributes,
+ "should return last record as predecessor to first record" +
+ " if wraparound is set");
+ deepEqual(root.pred(root.at(1), {wraparound: true}).attributes,
+ root.at(0).attributes,
+ "wraparound should have no effect if not pred(first_record)");
+ });
+ test('predecessor', function () {
+ var root = new openerp.web.list.Collection();
+ root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
+ root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
+ root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
+
+ deepEqual(root.pred(root.get(4)).attributes,
+ root.get(3).attributes,
+ "should get predecessor");
+ equal(root.pred(root.get(3)),
+ null,
+ "predecessor do not cross collections");
+ deepEqual(root.pred(root.get(3), {wraparound: true}).attributes,
+ root.get(4).attributes,
+ "should wraparound within a collection");
+ });
module('list-hofs', {
setup: function () {
@@ -338,4 +414,33 @@ $(document).ready(function () {
ids, [1, 2, 3, 10, 20, 30],
'tree collections should be deeply iterated');
});
+
+ module("list-weirds", {
+ setup: function () {
+ 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);
+ }
+ });
+ test('set-from-noid', function () {
+ var root = new openerp.web.list.Collection();
+ root.add({v: 3});
+ root.at(0).set('id', 42);
+ var record = root.get(42);
+ equal(root.length, 1);
+ equal(record.get('v'), 3, "should have fetched the original record");
+ });
+ test('set-from-previd', function () {
+ var root = new openerp.web.list.Collection();
+ root.add({id: 1, v: 2});
+ root.get(1).set('id', 42);
+ var record = root.get(42);
+ equal(root.length, 1);
+ equal(record.get('v'), 2, "should have fetched the original record");
+ });
});
diff --git a/addons/web/static/test/registry.js b/addons/web/static/test/registry.js
index da0cb952783..ca689a351e0 100644
--- a/addons/web/static/test/registry.js
+++ b/addons/web/static/test/registry.js
@@ -9,21 +9,6 @@ $(document).ready(function () {
openerp.web.Foo2 = {};
}
});
- test('key fetch', function () {
- var reg = new openerp.web.Registry({
- foo: 'openerp.web.Foo',
- bar: 'openerp.web.Bar',
- quux: 'openerp.web.Quux'
- });
-
- strictEqual(reg.get_object('foo'), openerp.web.Foo);
- raises(function () { reg.get_object('qux'); },
- openerp.web.KeyNotFound,
- "Unknown keys should raise KeyNotFound");
- raises(function () { reg.get_object('quux'); },
- openerp.web.ObjectNotFound,
- "Incorrect file paths should raise ObjectNotFound");
- });
test('key set', function () {
var reg = new openerp.web.Registry();
diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js
index 55092680077..548eef612b5 100644
--- a/addons/web/static/test/search.js
+++ b/addons/web/static/test/search.js
@@ -1,37 +1,8 @@
$(document).ready(function () {
- var xhr = QWeb2.Engine.prototype.get_xhr();
- xhr.open('GET', '/web/static/src/xml/base.xml', false);
- xhr.send(null);
- var doc = xhr.responseXML;
-
- var noop = function () {};
- /**
- * Make connection RPC responses mockable by setting keys on the
- * Connection#responses object (key is the URL, value is the function to
- * call with the RPC request payload)
- *
- * @param {openerp.web.Connection} connection connection instance to mockify
- * @param {Object} [responses] url:function mapping to seed the mock connection
- */
- var mockifyRPC = function (connection, responses) {
- connection.responses = responses || {};
- connection.rpc_function = function (url, payload) {
- if (!(url.url in this.responses)) {
- return $.Deferred().reject({}, 'failed', _.str.sprintf("Url %s not found in mock responses", url.url)).promise();
- }
- return $.when(this.responses[url.url](payload));
- };
- };
-
var instance;
module('query', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
}
});
test('Adding a facet to the query creates a facet and a value', function () {
@@ -167,16 +138,11 @@ $(document).ready(function () {
module('defaults', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
@@ -404,18 +370,11 @@ $(document).ready(function () {
module('completions', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- // date complete
- window.openerp.web.formats(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('calling', 4, function () {
@@ -432,10 +391,7 @@ $(document).ready(function () {
}
});
view.appendTo($('#qunit-fixture'))
- .always(start)
- .fail(function (error) { ok(false, error.message); })
.done(function () {
- stop();
view.complete_global_search({term: "dum"}, function (completions) {
start();
equal(completions.length, 1, "should have a single completion");
@@ -454,7 +410,11 @@ $(document).ready(function () {
var completion = {
label: "Dummy",
facet: {
- field: {get_domain: noop, get_context: noop, get_groupby: noop},
+ field: {
+ get_domain: openerp.testing.noop,
+ get_context: openerp.testing.noop,
+ get_groupby: openerp.testing.noop
+ },
category: 'Dummy',
values: [{label: 'dummy', value: 42}]
}
@@ -476,7 +436,11 @@ $(document).ready(function () {
});
});
asyncTest('facet selection: new value existing facet', 3, function () {
- var field = {get_domain: noop, get_context: noop, get_groupby: noop};
+ var field = {
+ get_domain: openerp.testing.noop,
+ get_context: openerp.testing.noop,
+ get_groupby: openerp.testing.noop
+ };
var completion = {
label: "Dummy",
facet: {
@@ -663,16 +627,11 @@ $(document).ready(function () {
module('search-serialization', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('No facet, no call', 6, function () {
@@ -940,16 +899,11 @@ $(document).ready(function () {
module('removal', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('clear button', function () {
@@ -975,16 +929,11 @@ $(document).ready(function () {
module('drawer', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('is-drawn', 2, function () {
@@ -1003,16 +952,11 @@ $(document).ready(function () {
module('filters', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection, {
+ openerp.testing.mockifyRPC(instance, {
'/web/searchview/load': function () {
// view with a single group of filters
return {result: {fields_view: {
@@ -1117,17 +1061,11 @@ $(document).ready(function () {
module('saved_filters', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.formats(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('checkboxing', 6, function () {
@@ -1183,17 +1121,11 @@ $(document).ready(function () {
module('advanced', {
setup: function () {
- instance = window.openerp.init([]);
- window.openerp.web.corelib(instance);
- window.openerp.web.coresetup(instance);
- window.openerp.web.chrome(instance);
- window.openerp.web.data(instance);
- window.openerp.web.formats(instance);
- window.openerp.web.search(instance);
+ instance = openerp.testing.instanceFor('search');
- instance.web.qweb.add_template(doc);
+ openerp.testing.loadTemplate(instance);
- mockifyRPC(instance.connection);
+ openerp.testing.mockifyRPC(instance);
}
});
asyncTest('single-advanced', 6, function () {
diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html
index e352f63c17c..cab1586f240 100644
--- a/addons/web/static/test/test.html
+++ b/addons/web/static/test/test.html
@@ -13,7 +13,7 @@
-
+
@@ -38,6 +38,12 @@
+
+
+
+
OpenERP web Test Suite
@@ -55,4 +61,5 @@
+
diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js
new file mode 100644
index 00000000000..e9525fc82a0
--- /dev/null
+++ b/addons/web/static/test/testing.js
@@ -0,0 +1,97 @@
+// Test support structures and methods for OpenERP
+openerp.testing = (function () {
+ var xhr = QWeb2.Engine.prototype.get_xhr();
+ xhr.open('GET', '/web/static/src/xml/base.xml', false);
+ xhr.send(null);
+ var doc = xhr.responseXML;
+
+ var dependencies = {
+ corelib: [],
+ coresetup: ['corelib'],
+ data: ['corelib', 'coresetup'],
+ dates: [],
+ formats: ['coresetup', 'dates'],
+ chrome: ['corelib', 'coresetup'],
+ views: ['corelib', 'coresetup', 'data', 'chrome'],
+ search: ['data', 'coresetup', 'formats'],
+ list: ['views', 'data'],
+ form: ['data', 'views', 'list', 'formats'],
+ list_editable: ['list', 'form', 'data'],
+ };
+
+ return {
+ /**
+ * Function which does not do anything
+ */
+ noop: function () { },
+ /**
+ * Loads 'base.xml' template file into qweb for the provided instance
+ *
+ * @param instance openerp instance being initialized, to load the template file in
+ */
+ loadTemplate: function (instance) {
+ instance.web.qweb.add_template(doc);
+ },
+ /**
+ * Alter provided instance's ``connection`` attribute to make response
+ * mockable:
+ *
+ * * The ``responses`` parameter can be used to provide a map of (RPC)
+ * paths (e.g. ``/web/view/load``) to a function returning a response
+ * to the query.
+ * * ``instance,connection`` grows a ``responses`` attribute which is
+ * a map of the same (and is in fact initialized to the ``responses``
+ * parameter if one is provided)
+ *
+ * Note that RPC requests to un-mocked URLs will be rejected with an
+ * error message: only explicitly specified urls will get a response.
+ *
+ * Mocked connections will *never* perform an actual RPC connection.
+ *
+ * @param instance openerp instance being initialized
+ * @param {Object} [responses]
+ */
+ mockifyRPC: function (instance, responses) {
+ var connection = instance.connection;
+ connection.responses = responses || {};
+ connection.rpc_function = function (url, payload) {
+ var fn = this.responses[url.url + ':' + payload.params.method]
+ || this.responses[url.url];
+
+ if (!fn) {
+ return $.Deferred().reject({}, 'failed',
+ _.str.sprintf("Url %s not found in mock responses, with arguments %s",
+ url.url, JSON.stringify(payload.params))
+ ).promise();
+ }
+ return $.when(fn(payload));
+ };
+ },
+ /**
+ * Creates an openerp web instance loading the specified module after
+ * all of its dependencies.
+ *
+ * @param {String} module
+ * @returns OpenERP Web instance
+ */
+ instanceFor: function (module) {
+ var instance = openerp.init([]);
+ this._load(instance, module);
+ return instance;
+ },
+ _load: function (instance, module, loaded) {
+ if (!loaded) { loaded = []; }
+
+ var deps = dependencies[module];
+ if (!deps) { throw new Error("Unknown dependencies for " + module); }
+
+ var to_load = _.difference(deps, loaded);
+ while (!_.isEmpty(to_load)) {
+ this._load(instance, to_load[0], loaded);
+ to_load = _.difference(deps, loaded);
+ }
+ openerp.web[module](instance);
+ loaded.push(module);
+ }
+ }
+})();
diff --git a/addons/web_dashboard/i18n/uk.po b/addons/web_dashboard/i18n/uk.po
new file mode 100644
index 00000000000..fd4df4a2141
--- /dev/null
+++ b/addons/web_dashboard/i18n/uk.po
@@ -0,0 +1,63 @@
+# Ukrainian translation for openerp-web
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openerp-web package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openerp-web\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-07-02 09:06+0200\n"
+"PO-Revision-Date: 2012-07-22 09:32+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Ukrainian \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-23 05:21+0000\n"
+"X-Generator: Launchpad (build 15654)\n"
+
+#. openerp-web
+#: addons/web_dashboard/static/src/js/dashboard.js:60
+msgid "Edit Layout"
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/js/dashboard.js:106
+msgid "Are you sure you want to remove this item ?"
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:4
+msgid "Reset Layout.."
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:6
+msgid "Reset"
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:8
+msgid "Change Layout.."
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:10
+msgid "Change Layout"
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:27
+msgid " "
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:28
+msgid "Create"
+msgstr ""
+
+#. openerp-web
+#: addons/web_dashboard/static/src/xml/web_dashboard.xml:39
+msgid "Choose dashboard layout"
+msgstr ""
diff --git a/doc/form-notes.rst b/doc/form-notes.rst
new file mode 100644
index 00000000000..fc450a336e1
--- /dev/null
+++ b/doc/form-notes.rst
@@ -0,0 +1,49 @@
+Notes on the usage of the Form View as a sub-widget
+===================================================
+
+Undocumented stuff
+------------------
+
+* ``initial_mode`` *option* defines the starting mode of the form
+ view, one of ``view`` and ``edit`` (?). Default value is ``view``
+ (non-editable form).
+
+* ``embedded_view`` *attribute* has to be set separately when
+ providing a view directly, no option available for that usage.
+
+ * View arch **must** contain node with
+ ``@class="oe_form_container"``, otherwise everything will break
+ without any info
+
+ * Root element of view arch not being ``form`` may or may not work
+ correctly, no idea.
+
+ * Freeform views => ``@version="7.0"``
+
+* Form is not entirely loaded (some widgets may not appear) unless
+ ``on_record_loaded`` is called (or ``do_show``, which itself calls
+ ``on_record_loaded``).
+
+* "Empty" form => ``on_button_new`` (...), or manually call
+ ``default_get`` + ``on_record_loaded``
+
+* Form fields default to width: 100%, padding, !important margin, can
+ be reached via ``.oe_form_field``
+
+* Form *will* render buttons and a pager, offers options to locate
+ both outside of form itself (``$buttons`` and ``$pager``), providing
+ empty jquery objects (``$()``) seems to stop displaying both but not
+ sure if there are deleterious side-effects.
+
+ Other options:
+
+ * Pass in ``$(document.createDocumentFragment)`` to ensure it's a
+ DOM-compatible tree completely outside of the actual DOM.
+
+ * ???
+
+* readonly fields probably don't have a background, beware if need of
+ overlay
+
+ * What is the difference between ``readonly`` and
+ ``effective_readonly``?
diff --git a/doc/index.rst b/doc/index.rst
index 9f54b879acc..8608af59d3d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -18,6 +18,9 @@ Contents:
search-view
+ list-view
+ form-notes
+
Older stuff
-----------
diff --git a/doc/list-view.rst b/doc/list-view.rst
new file mode 100644
index 00000000000..0d67c27f963
--- /dev/null
+++ b/doc/list-view.rst
@@ -0,0 +1,455 @@
+List View
+=========
+
+Style Hooks
+-----------
+
+The list view provides a few style hook classes for re-styling of list views in
+various situations:
+
+``.oe_list``
+
+ The root element of the list view, styling rules should be rooted
+ on that class.
+
+``table.oe_list_content``
+
+ The root table for the listview, accessory components may be
+ generated or added outside this section, this is the list view
+ "proper".
+
+``.oe_list_buttons``
+
+ The action buttons array for the list view, with its sub-elements
+
+ ``.oe_list_add``
+
+ The default "Create"/"Add" button of the list view
+
+ ``.oe_alternative``
+
+ The "alternative choice" for the list view, by default text
+ along the lines of "or import" with a link.
+
+``.oe_list_field_cell``
+
+ The cell (``td``) for a given field of the list view, cells which
+ are *not* fields (e.g. name of a group, or number of items in a
+ group) will not have this class. The field cell can be further
+ specified:
+
+ ``.oe_number``
+
+ Numeric cell types (integer and float)
+
+ ``.oe_button``
+
+ Action button (``button`` tag in the view) inside the cell
+
+ ``.oe_readonly``
+
+ Readonly field cell
+
+ ``.oe_list_field_$type``
+
+ Additional class for the precise type of the cell, ``$type``
+ is the field's @widget if there is one, otherwise it's the
+ field's type.
+
+``.oe_list_record_selector``
+
+ Selector cells
+
+Editable list view
+++++++++++++++++++
+
+The editable list view module adds a few supplementary style hook
+classes, for edition situations:
+
+``.oe_editing``
+
+ Added to both ``.oe_list`` and ``.oe_list_button`` (as the
+ buttons may be outside of the list view) when a row of the list is
+ currently being edited.
+
+``tr.oe_edition``
+
+ Class set on the row being edited itself. Note that the edition
+ form is *not* contained within the row, this allows for styling or
+ modifying the row while it's being edited separately. Mostly for
+ fields which can not be edited (e.g. read-only fields).
+
+
+Editable list view
+------------------
+
+List view edition is an extension to the base listview providing the
+capability of inline record edition by delegating to an embedded form
+view.
+
+.. todo::
+
+ cleanup options and settings for editability configuration. Right
+ now there are:
+
+ ``defaults.editable``
+
+ ``null``, ``"top"`` or ``"bottom"``, generally broken and
+ useless
+
+ ``context.set_editable``
+
+ forces ``options.editable`` to ``"bottom"``
+
+ ``view.arch.attrs.editable``
+
+ same as ``defaults.editable``, but applied separately (after
+ reloading the view), if absent delegates to
+ ``options.editable`` which may have been set previously.
+
+ ``options.read_only``
+
+ force options.editable to false, or something?
+
+ .. note:: can probably be replaced by cancelling ``edit:before``
+
+ and :js:func:`~openerp.web.ListView.set_editable` which
+ ultimately behaves weird-as-fuck-ly.
+
+The editable list view module adds a number of methods to the list
+view, on top of implementing the :js:class:`EditorDelegate` protocol:
+
+Interaction Methods
++++++++++++++++++++
+
+.. js:function:: openerp.web.ListView.ensure_saved
+
+ Attempts to resolve the pending edition, if any, by saving the
+ edited row's current state.
+
+ :returns: delegate resolving to all editions having been saved, or
+ rejected if a pending edition could not be saved
+ (e.g. validation failure)
+
+.. js:function:: openerp.web.ListView.start_edition([record][, options])
+
+ Starts editing the provided record inline, through an overlay form
+ view of editable fields in the record.
+
+ If no record is provided, creates a new one according to the
+ editability configuration of the list view.
+
+ This method resolves any pending edition when invoked, before
+ starting a new edition.
+
+ :param record: record to edit, or null to create a new record
+ :type record: :js:class:`~openerp.web.list.Record`
+ :param EditOptions options:
+ :returns: delegate to the form used for the edition
+
+.. js:function:: openerp.web.ListView.save_edition
+
+ Resolves the pending edition.
+
+ :returns: delegate to the save being completed, resolves to an
+ object with two attributes ``created`` (flag indicating
+ whether the saved record was just created or was
+ updated) and ``record`` the reloaded record having been
+ edited.
+
+.. js:function:: openerp.web.ListView.cancel_edition
+
+ Cancels pending edition, cleans up the list view in case of
+ creation (removes the empty record being created).
+
+Utility Methods
++++++++++++++++
+
+.. js:function:: openerp.web.ListView.get_cells_for(row)
+
+ Extracts the cells from a listview row, and puts them in a
+ {fieldname: cell} mapping for analysis and manipulation.
+
+ :param jQuery row:
+ :rtype: Object
+
+.. js:function:: openerp.web.ListView.with_event(event_name, event, action[, args][, trigger_params])
+
+ Executes ``action`` in the context of the view's editor,
+ bracketing it with cancellable event signals.
+
+ :param String event_name: base name for the bracketing event, will
+ be postfixed by ``:before`` and
+ ``:after`` before being called
+ (respectively before and after
+ ``action`` is executed)
+ :param Object event: object passed to the ``:before`` event
+ handlers.
+ :param Function action: function called with the view's editor as
+ its ``this``. May return a deferred.
+ :param Array args: arguments passed to ``action``
+ :param Array trigger_params: arguments passed to the ``:after``
+ event handler alongside the results
+ of ``action``
+
+Behavioral Customizations
++++++++++++++++++++++++++
+
+.. js:function:: openerp.web.ListView.handle_onwrite(record)
+
+ Implements the handling of the ``onwrite`` listview attribute:
+ calls the RPC methods specified by ``@onwrite``, and if that
+ method returns an array of ids loads or reloads the records
+ corresponding to those ids.
+
+ :param record: record being written having triggered the
+ ``onwrite`` callback
+ :type record: openerp.web.list.Record
+ :returns: deferred to all reloadings being done
+
+Events
+++++++
+
+For simpler interactions by/with external users of the listview, the
+view provides a number of dedicated events to its lifecycle.
+
+.. note:: if an event is defined as *cancellable*, it means its first
+ parameter is an object on which the ``cancel`` attribute can
+ be set. If the ``cancel`` attribute is set, the view will
+ abort its current behavior as soon as possible, and rollback
+ any state modification.
+
+``edit:before`` *cancellable*
+
+ Invoked before the list view starts editing a record.
+
+ Provided with an event object with a single property ``record``,
+ holding the attributes of the record being edited (``record`` is
+ empty *but not null* for a new record)
+
+``edit:after``
+
+ Invoked after the list view has gone into an edition state,
+ provided with the attributes of the record being edited (see
+ ``edit:before``) as first parameter and the form used for the
+ edition as second parameter.
+
+``save:before`` *cancellable*
+
+ Invoked right before saving a pending edition, provided with an
+ event object holding the listview's editor (``editor``) and the
+ edition form (``form``)
+
+``save:after``
+
+ Invoked after a save has been completed
+
+``cancel:before`` *cancellable*
+
+ Invoked before cancelling a pending edition, provided with the
+ same information as ``save:before``.
+
+``cancel:after``
+
+ Invoked after a pending edition has been cancelled.
+
+DOM events
+++++++++++
+
+The list view has grown hooks for the ``keyup`` event on its edition
+form (during edition): any such event bubbling out of the edition form
+will be forwarded to a method ``keyup_EVENTNAME``, where ``EVENTNAME``
+is the name of the key in ``$.ui.keyCode``.
+
+The method will also get the event object (originally passed to the
+``keyup`` handler) as its sole parameter.
+
+The base editable list view has handlers for the ``ENTER`` and
+``ESCAPE`` keys.
+
+Editor
+------
+
+The list-edition modules does not generally interact with the embedded
+formview, delegating instead to its
+:js:class:`~openerp.web.list.Editor`.
+
+.. js:class:: openerp.web.list.Editor(parent[, options])
+
+ The editor object provides a more convenient interface to form
+ views, and simplifies the usage of form views for semi-arbitrary
+ edition of stuff.
+
+ However, the editor does *not* task itself with being internally
+ consistent at this point: calling
+ e.g. :js:func:`~openerp.web.list.Editor.edit` multiple times in a
+ row without saving or cancelling each edit is undefined.
+
+ :param parent:
+ :type parent: :js:class:`~openerp.web.Widget`
+ :param EditorOptions options:
+
+ .. js:function:: openerp.web.list.Editor.is_editing([record_state])
+
+ Indicates whether the editor is currently in the process of
+ providing edition for a record.
+
+ Can be filtered by the state of the record being edited
+ (whether it's a record being *created* or a record being
+ *altered*), in which case it asserts both that an edition is
+ underway and that the record being edited respectively does
+ not yet exist in the database or already exists there.
+
+ :param record_state: state of the record being edited.
+ Either ``"new"`` or ``"edit"``.
+ :type record_state: String
+ :rtype: Boolean
+
+ .. js:function:: openerp.web.list.Editor.edit(record, configureField[, options])
+
+ Loads the provided record into the internal form view and
+ displays the form view.
+
+ Will also attempt to focus the first visible field of the form
+ view.
+
+ :param Object record: record to load into the form view
+ (key:value mapping similar to the result
+ of a ``read``)
+ :param configureField: function called with each field of the
+ form view right after the form is
+ displayed, lets whoever called this
+ method do some last-minute
+ configuration of form fields.
+ :type configureField: Function
+ :param EditOptions options:
+ :returns: jQuery delegate to the form object
+
+ .. js:function:: openerp.web.list.Editor.save
+
+ Attempts to save the internal form, then hide it
+
+ :returns: delegate to the record under edition (with ``id``
+ added for a creation). The record is not updated
+ from when it was passed in, aside from the ``id``
+ attribute.
+
+ .. js:function:: openerp.web.list.Editor.cancel
+
+ Attemps to cancel the edition of the internal form, then hide
+ the form
+
+ :returns: delegate to the record under edition
+
+.. js:class:: EditorOptions
+
+ .. js:attribute:: EditorOptions.formView
+
+ Form view (sub)-class to instantiate and delegate edition to.
+
+ By default, :js:class:`~openerp.web.FormView`
+
+ .. js:attribute:: EditorOptions.delegate
+
+ Object used to get various bits of information about how to
+ display stuff.
+
+ By default, uses the editor's parent widget. See
+ :js:class:`EditorDelegate` for the methods and attributes to
+ provide.
+
+.. js:class:: EditorDelegate
+
+ Informal protocol defining the methods and attributes expected of
+ the :js:class:`~openerp.web.list.Editor`'s delegate.
+
+ .. js:attribute:: EditorDelegate.dataset
+
+ The dataset passed to the form view to synchronize the form
+ view and the outer widget.
+
+ .. js:function:: EditorDelegate.edition_view(editor)
+
+ Called by the :js:class:`~openerp.web.list.Editor` object to
+ get a form view (JSON) to pass along to the form view it
+ created.
+
+ The result should be a valid form view, see :doc:`Form Notes
+ ` for various peculiarities of the form view
+ format.
+
+ :param editor: editor object asking for the view
+ :type editor: :js:class:`~openerp.web.list.Editor`
+ :returns: form view
+ :rtype: Object
+
+ .. js:function:: EditorDelegate.prepends_on_create
+
+ By default, the :js:class:`~openerp.web.list.Editor` will
+ append the ids of newly created records to the
+ :js:attr:`EditorDelegate.dataset`. If this method returns
+ ``true``, it will prepend these ids instead.
+
+ :returns: whether new records should be prepended to the
+ dataset (instead of appended)
+ :rtype: Boolean
+
+
+.. js:class:: EditOptions
+
+ Options object optionally passed into a method starting an edition
+ to configure its setup and behavior
+
+ .. js:attribute:: focus_field
+
+ Name of the field to set focus on after setting up the edition
+ of the record.
+
+ If this option is not provided, or the requested field can not
+ be focused (invisible, readonly or not in the view), the first
+ visible non-readonly field is focused.
+
+Changes from 6.1
+----------------
+
+* The editable listview behavior has been rewritten pretty much from
+ scratch, any code touching on editability will have to be modified
+
+ * The overloading of :js:class:`~openerp.web.ListView.Groups` and
+ :js:class:`~openerp.web.ListView.List` for editability has been
+ drastically simplified, and most of the behavior has been moved to
+ the list view itself. Only
+ :js:func:`~openerp.web.ListView.List.row_clicked` is still
+ overridden.
+
+ * A new method ``get_row_for(record) -> jQuery(tr) | null`` has been
+ added to both ListView.List and ListView.Group, it can be called
+ from the list view to get the table row matching a record (if such
+ a row exists).
+
+* :js:func:`~openerp.web.ListView.do_button_action`'s core behavior
+ has been split away to
+ :js:func:`~openerp.web.ListView.handle_button`. This allows bypassing
+ overrides of :js:func:`~openerp.web.ListView.do_button_action` in a
+ parent class.
+
+ Ideally, :js:func:`~openerp.web.ListView.handle_button` should not be
+ overridden.
+
+* Modifiers handling has been improved (all modifiers information
+ should now be available through :js:func:`~Column.modifiers_for`,
+ not just ``invisible``)
+
+* Changed some handling of the list view's record: a record may now
+ have no id, and the listview will handle that correctly (for new
+ records being created) as well as correctly handle the ``id`` being
+ set.
+
+* Extended the internal collections structure of the list view with
+ `#find`_, `#succ`_ and `#pred`_.
+
+.. _#find: http://underscorejs.org/#find
+
+.. _#succ: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:succ
+
+.. _#pred: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:pred