[MERGE] trunk

bzr revid: al@openerp.com-20120725082646-ff6ljt0pquvm0dup
This commit is contained in:
Antony Lesuisse 2012-07-25 10:26:46 +02:00
commit 3235ed8a18
28 changed files with 3994 additions and 784 deletions

1563
addons/web/i18n/fr_CA.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
@charset "UTF-8";
@charset "utf-8";
@font-face {
font-family: "mnmliconsRegular";
src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot");
@ -267,6 +267,45 @@
color: black;
text-decoration: none;
}
.openerp.ui-dialog .oe_about {
background-color: white;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
-moz-border-radius: 0 0 2px 2px;
-webkit-border-radius: 0 0 2px 2px;
border-radius: 0 0 2px 2px;
}
.openerp.ui-dialog .oe_about a {
color: #8a89ba;
}
.openerp.ui-dialog .oe_about a:hover {
text-decoration: underline;
}
.openerp.ui-dialog .oe_about .oe_logo {
margin-left: -6px;
}
.openerp.ui-dialog .oe_about .oe_bottom {
position: absolute;
top: 50%;
left: 0;
right: 0;
bottom: 0;
text-shadow: 0 1px 1px #999999;
background-color: #b41616;
background-image: -webkit-gradient(linear, left top, left bottom, from(#b41616), to(#600606));
background-image: -webkit-linear-gradient(top, #b41616, #600606);
background-image: -moz-linear-gradient(top, #b41616, #600606);
background-image: -ms-linear-gradient(top, #b41616, #600606);
background-image: -o-linear-gradient(top, #b41616, #600606);
background-image: linear-gradient(to bottom, #b41616, #600606);
color: #eeeeee;
padding: 0 16px;
-moz-border-radius: 0 0 2px 2px;
-webkit-border-radius: 0 0 2px 2px;
border-radius: 0 0 2px 2px;
}
.openerp.ui-dialog .oe_about .oe_bottom a {
color: #eeeeee;
}
.openerp.ui-dialog.oe_act_window .ui-dialog-content {
padding: 0px;
}
@ -584,7 +623,7 @@
z-index: 1;
border: 1px solid #afafb6;
background: white;
padding: 6px 0;
padding: 4px 0;
min-width: 140px;
text-align: left;
-moz-border-radius: 3px;
@ -599,6 +638,7 @@
float: none;
display: block;
position: relative;
padding: 2px 8px;
}
.openerp .oe_dropdown_menu > li:hover {
background-color: #f0f0fa;
@ -615,7 +655,6 @@
.openerp .oe_dropdown_menu > li > a {
white-space: nowrap;
display: block;
padding: 4px 15px;
color: #4c4c4c;
text-decoration: none;
}
@ -711,7 +750,7 @@
z-index: 1050;
}
.openerp .oe_login {
background: url("/web/static/src/img/pattern.png") repeat;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
text-align: center;
font-size: 14px;
height: 100%;
@ -945,11 +984,12 @@
}
.openerp .oe_topbar .oe_dropdown_menu li {
float: none;
padding: 3px 12px;
}
.openerp .oe_topbar .oe_dropdown_menu li a {
color: #eeeeee;
}
.openerp .oe_topbar .oe_dropdown_menu li a:hover {
.openerp .oe_topbar .oe_dropdown_menu li:hover {
background-color: #292929;
background-image: -webkit-gradient(linear, left top, left bottom, from(#292929), to(#191919));
background-image: -webkit-linear-gradient(top, #292929, #191919);
@ -1408,20 +1448,31 @@
filter: alpha(opacity=50);
opacity: 0.5;
}
.openerp .oe_searchview .oe_searchview_search {
font-size: 1px;
letter-spacing: -1px;
color: transparent;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
position: absolute;
left: 3px;
top: 1px;
padding: 0;
border: none;
background: transparent;
}
.openerp .oe_searchview .oe_searchview_search:before {
font: 21px "mnmliconsRegular";
content: "r";
color: #a3a3a3;
}
.openerp .oe_searchview .oe_searchview_facets {
min-height: 22px;
}
.openerp .oe_searchview .oe_searchview_facets:before {
color: #cccccc;
font-family: "mnmliconsRegular";
content: "r";
font-size: 130%;
display: inline;
position: relative;
left: 6px;
top: 2px;
color: #a3a3a3;
padding-right: 4px;
margin-left: 15px;
}
.openerp .oe_searchview .oe_searchview_facets * {
vertical-align: top;
@ -1437,7 +1488,7 @@
outline: none;
}
.openerp .oe_searchview .oe_searchview_facets .oe_searchview_input {
padding: 0 3px;
padding: 0 0 0 6px;
}
.openerp .oe_searchview .oe_searchview_facets .oe_searchview_facet {
position: relative;
@ -1600,14 +1651,14 @@
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section li:hover {
background-color: #f0f0fa;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form {
.openerp .oe_searchview .oe_searchview_drawer form {
margin-left: 12px;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form p {
.openerp .oe_searchview .oe_searchview_drawer form p {
margin: 4px 0;
line-height: 18px;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_section form button {
.openerp .oe_searchview .oe_searchview_drawer form button {
margin: 0 0 8px 0;
}
.openerp .oe_searchview .oe_searchview_drawer .oe_searchview_custom {
@ -2180,9 +2231,33 @@
height: auto;
line-height: 16px;
}
.openerp .oe_form_field_one2many .oe_list_buttons.oe_editing .oe_list_save, .openerp .oe_form_field_many2many .oe_list_buttons.oe_editing .oe_list_save {
visibility: hidden;
}
.openerp .oe_form .oe_form_field_many2many > .oe_list .oe_list_pager_single_page {
display: none;
}
.openerp .oe_list_buttons .oe_list_save, .openerp .oe_list_buttons .oe_list_discard {
display: none;
}
.openerp .oe_list_buttons.oe_editing .oe_list_add, .openerp .oe_list_buttons.oe_editing .oe_list_button_import {
display: none;
}
.openerp .oe_list_buttons.oe_editing .oe_list_save {
display: inline-block;
}
.openerp .oe_list_buttons.oe_editing .oe_list_discard {
display: inline;
}
.openerp .oe_list {
position: relative;
}
.openerp .oe_list .oe_form .oe_form_field {
width: auto;
position: absolute;
margin: 0 !important;
padding: 0;
}
.openerp .oe_list_content {
width: 100%;
}
@ -2240,9 +2315,7 @@
}
.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell {
padding: 3px 6px;
}
.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell progress {
width: 100%;
white-space: pre-line;
}
.openerp .oe_list_content > tbody > tr > td > button, .openerp .oe_list_content > tbody > tr > th > button {
border: none;
@ -2290,9 +2363,6 @@
.openerp .oe_list_content .numeric input {
text-align: right;
}
.openerp .oe_list_content .oe_list_edit_row_save:before {
content: "S";
}
.openerp .oe_trad_field.touched {
border: 1px solid green !important;
}

View File

@ -92,9 +92,8 @@ $sheet-max-width: 860px
letter-spacing: -1px
color: transparent
&:before
font-family: "mnmliconsRegular"
font: 21px "mnmliconsRegular"
content: $icon-name
font-size: 20px
color: $color
// }}}
@ -256,6 +255,29 @@ $sheet-max-width: 860px
&:hover
color: black
text-decoration: none
.oe_about
background-color: white
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=)
@include radius(0 0 2px 2px)
a
color: #8A89BA
&:hover
text-decoration: underline
.oe_logo
margin-left: -6px
.oe_bottom
position: absolute
top: 50%
left: 0
right: 0
bottom: 0
text-shadow: 0 1px 1px #999999
@include vertical-gradient(#b41616, #600606)
color: #eee
padding: 0 16px
@include radius(0 0 2px 2px)
a
color: #eee
&.ui-dialog.oe_act_window
.ui-dialog-content
@ -470,7 +492,7 @@ $sheet-max-width: 860px
z-index: 1
border: 1px solid #afafb6
background: white
padding: 6px 0
padding: 4px 0
min-width: 140px
text-align: left
@include radius(3px)
@ -483,10 +505,10 @@ $sheet-max-width: 860px
float: none
display: block
position: relative
padding: 2px 8px
> a
white-space: nowrap
display: block
padding: 4px 15px
color: #4c4c4c
text-decoration: none
&:hover
@ -567,7 +589,7 @@ $sheet-max-width: 860px
// }}}
// Login {{{
.oe_login
background: url("/web/static/src/img/pattern.png") repeat
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=)
text-align: center
font-size: 14px
height: 100%
@ -735,11 +757,12 @@ $sheet-max-width: 860px
@include background-clip()
li
float: none
padding: 3px 12px
a
color: #eee
&:hover
@include vertical-gradient(#292929, #191919)
@include box-shadow(none)
&:hover
@include vertical-gradient(#292929, #191919)
@include box-shadow(none)
// }}}
// Webclient.leftbar {{{
@ -1102,19 +1125,20 @@ $sheet-max-width: 860px
border-right: 5px solid transparent
@include opacity()
.oe_searchview_search
@include text-to-icon("r", #a3a3a3)
@include box-shadow(none)
@include radius(0)
position: absolute
left: 3px
top: 1px
padding: 0
border: none
background: transparent
.oe_searchview_facets
min-height: 22px
&:before
color: #ccc
font-family: "mnmliconsRegular"
content: "r"
font-size: 130%
display: inline
position: relative
left: 6px
top: 2px
color: #a3a3a3
padding-right: 4px
margin-left: 15px
*
vertical-align: top
display: inline-block
@ -1126,7 +1150,7 @@ $sheet-max-width: 860px
&:focus
outline: none
.oe_searchview_input
padding: 0 3px
padding: 0 0 0 6px
.oe_searchview_facet
position: relative
cursor: pointer
@ -1248,13 +1272,13 @@ $sheet-max-width: 860px
// after oe_selected so background color is not overridden
&:hover
background-color: $hover-background
form
margin-left: 12px
p
margin: 4px 0
line-height: 18px
button
margin: 0 0 8px 0
form
margin-left: 12px
p
margin: 4px 0
line-height: 18px
button
margin: 0 0 8px 0
.oe_searchview_custom
padding: 0 8px 8px 8px
form
@ -1721,6 +1745,9 @@ $sheet-max-width: 860px
li
height: auto
line-height: 16px
.oe_list_buttons.oe_editing .oe_list_save
// keep "save row" button hidden in o2m
visibility: hidden
// }}}
// FormView.many2many {{{
.oe_form .oe_form_field_many2many > .oe_list
@ -1728,6 +1755,25 @@ $sheet-max-width: 860px
display: none
// }}}
// ListView {{{
.oe_list_buttons
.oe_list_save, .oe_list_discard
display: none
&.oe_editing
.oe_list_add, .oe_list_button_import
display: none
.oe_list_save
display: inline-block
.oe_list_discard
display: inline
.oe_list
position: relative
.oe_form .oe_form_field
width: auto
position: absolute
margin: 0 !important // dammit
padding: 0
.oe_list_content
width: 100%
td:first-child, th:first-child
@ -1771,8 +1817,7 @@ $sheet-max-width: 860px
border-top: 1px solid #ddd
> td.oe_list_field_cell
padding: 3px 6px
progress
width: 100%
white-space: pre-line
> td, > th
> button
border: none
@ -1800,8 +1845,6 @@ $sheet-max-width: 860px
width: 82px
input
text-align: right
.oe_list_edit_row_save:before
content: "S"
// }}}
// Translation {{{
.oe_trad_field.touched
@ -1910,6 +1953,7 @@ $sheet-max-width: 860px
background-attachment: fixed
>*
opacity: 0.70
// }}}
div.ui-widget-overlay

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -135,8 +135,12 @@ instance.web.Dialog = instance.web.Widget.extend({
this.$element.dialog('close');
},
on_close: function() {
if (this.__tmp_dialog_destroying)
return;
if (this.dialog_options.destroy_on_close) {
this.__tmp_dialog_closing = true;
this.destroy();
this.__tmp_dialog_closing = undefined;
}
},
on_resized: function() {
@ -145,6 +149,11 @@ instance.web.Dialog = instance.web.Widget.extend({
_.each(this.getChildren(), function(el) {
el.destroy();
});
if (! this.__tmp_dialog_closing) {
this.__tmp_dialog_destroying = true;
this.close();
this.__tmp_dialog_destroying = undefined;
}
if (! this.isDestroyed()) {
this.$element.dialog('destroy');
}
@ -248,13 +257,17 @@ instance.web.Loading = instance.web.Widget.extend({
// Block UI after 3s
this.long_running_timer = setTimeout(function () {
self.blocked_ui = true;
$.blockUI();
instance.web.blockUI();
}, 3000);
}
this.count += increment;
if (this.count > 0) {
this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count));
if (instance.connection.debug) {
this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count));
} else {
this.$element.text(_t("Loading"));
}
this.$element.show();
this.getParent().$element.addClass('oe_wait');
} else {
@ -263,7 +276,7 @@ instance.web.Loading = instance.web.Widget.extend({
// Don't unblock if blocked by somebody else
if (self.blocked_ui) {
this.blocked_ui = false;
$.unblockUI();
instance.web.unblockUI();
}
this.$element.fadeOut();
this.getParent().$element.removeClass('oe_wait');
@ -274,7 +287,7 @@ instance.web.Loading = instance.web.Widget.extend({
instance.web.DatabaseManager = instance.web.Widget.extend({
init: function(parent) {
this._super(parent);
this.unblockUIFunction = $.unblockUI;
this.unblockUIFunction = instance.web.unblockUI;
$.validator.addMethod('matches', function (s, _, re) {
return new RegExp(re).test(s);
}, _t("Invalid database name"));
@ -341,16 +354,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
* from unblocking the UI
*/
blockUI: function () {
$.blockUI();
$.unblockUI = function () {};
instance.web.blockUI();
instance.web.unblockUI = function () {};
},
/**
* Reinstates $.unblockUI so third parties can play with blockUI, and
* unblocks the UI
*/
unblockUI: function () {
$.unblockUI = this.unblockUIFunction;
$.unblockUI();
instance.web.unblockUI = this.unblockUIFunction;
instance.web.unblockUI();
},
/**
* Displays an error dialog resulting from the various RPC communications
@ -851,7 +864,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
window.location.href, 'debug');
});
instance.web.dialog($help, {autoOpen: true,
modal: true, width: 960, title: _t("About")});
modal: true, width: 580, height: 290, resizable: false, title: _t("About")});
});
},
});
@ -890,7 +903,7 @@ instance.web.Client = instance.web.Widget.extend({
var doc_width = $(document).width();
var offset = $menu.offset();
var menu_width = $menu.width();
var x = doc_width - offset.left - menu_width - 15;
var x = doc_width - offset.left - menu_width - 2;
if (x < 0) {
$menu.offset({ left: offset.left + x }).width(menu_width);
}
@ -957,10 +970,12 @@ instance.web.WebClient = instance.web.Client.extend({
},
show_login: function() {
var self = this;
self.$('.oe_topbar').hide();
self.login.appendTo(self.$element);
},
show_application: function() {
var self = this;
self.$('.oe_topbar').show();
self.login.$element.hide();
self.menu = new instance.web.Menu(self);
self.menu.replace(this.$element.find('.oe_menu_placeholder'));

View File

@ -550,7 +550,15 @@ $.async_when = function() {
/** Setup blockui */
if ($.blockUI) {
$.blockUI.defaults.baseZ = 1100;
$.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">';
$.blockUI.defaults.message = '<img src="/web/static/src/img/throbber.gif">';
$.blockUI.defaults.css.border = '0';
$.blockUI.defaults.css["background-color"] = '';
}
instance.web.blockUI = function() {
$.blockUI.apply($, arguments);
}
instance.web.unblockUI = function() {
return $.unblockUI.apply($, arguments);
}
/** Setup default session */

View File

@ -376,7 +376,7 @@ instance.web.DataExport = instance.web.Dialog.extend({
exported_fields.unshift({name: 'id', label: 'External ID'});
var export_format = this.$element.find("#export_format").val();
$.blockUI();
instance.web.blockUI();
this.session.get_file({
url: '/web/export/' + export_format,
data: {data: JSON.stringify({
@ -387,7 +387,7 @@ instance.web.DataExport = instance.web.Dialog.extend({
import_compat: Boolean(
this.$element.find("#import_compat").val())
})},
complete: $.unblockUI
complete: instance.web.unblockUI
});
},
close: function() {

View File

@ -330,6 +330,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
});
}
// Launch a search on clicking the oe_searchview_search button
this.$element.on('click', 'button.oe_searchview_search', function (e) {
e.stopImmediatePropagation();
self.do_search();
});
this.$element.on('keydown',
'.oe_searchview_input, .oe_searchview_facet', function (e) {
switch(e.which) {
@ -1475,7 +1481,17 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
facet_for: function (value) {
var self = this;
if (value instanceof Array) {
return $.when(facet_from(this, value));
if (value.length === 2 && _.isString(value[1])) {
return $.when(facet_from(this, value));
}
if (value.length > 1) {
// more than one search_default m2o id? Should we OR them?
throw new Error(
_("M2O search fields do not currently handle multiple default values"));
}
// there are many cases of {search_default_$m2ofield: [id]}, need
// to handle this as if it were a single value.
value = value[0];
}
return this.model.call('name_get', [value], {}).pipe(function (names) {
if (_(names).isEmpty()) { return null; }
@ -1669,6 +1685,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({
return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data"));
},
load_data:function(){
if (!instance.webclient) { return $.Deferred().reject(); }
var self = this,dashboard_menu = instance.webclient.menu.data.data.children;
var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']);
var map_data = function(result){

View File

@ -239,6 +239,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
}
},
/**
*
* @param {Object} [options]
* @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode.
* @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
* @return {$.Deferred}
*/
do_show: function (options) {
var self = this;
options = options || {};
@ -253,23 +260,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
this.$element.show().css('visibility', 'hidden');
this.$element.add(this.$buttons).removeClass('oe_form_dirty');
return this.has_been_loaded.pipe(function() {
var result;
if (self.dataset.index === null) {
// null index means we should start a new record
result = self.on_button_new();
} else {
result = self.dataset.read_index(_.keys(self.fields_view.fields), {
context : { 'bin_size' : true }
}).pipe(self.on_record_loaded);
}
result.pipe(function() {
if (options.editable) {
self.set({mode: "edit"});
var shown = this.has_been_loaded;
if (options.reload !== false) {
shown = shown.pipe(function() {
if (self.dataset.index === null) {
// null index means we should start a new record
return self.on_button_new();
}
self.$element.css('visibility', 'visible');
return self.dataset.read_index(_.keys(self.fields_view.fields), {
context: { 'bin_size': true }
}).pipe(self.on_record_loaded);
});
return result;
}
return shown.pipe(function() {
if (options.editable) {
self.set({mode: "edit"});
}
self.$element.css('visibility', 'visible');
});
},
do_hide: function () {
@ -330,6 +338,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
self.$element.add(self.$buttons).removeClass('oe_form_dirty');
});
},
/**
* Loads and sets up the default values for the model as the current
* record
*
* @return {$.Deferred}
*/
load_defaults: function () {
var keys = _.keys(this.fields_view.fields);
if (keys.length) {
return this.dataset.default_get(keys)
.pipe(this.on_record_loaded);
}
return this.on_record_loaded({});
},
on_form_changed: function() {
this.trigger("view_content_has_changed");
},
@ -600,22 +622,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
on_button_new: function() {
var self = this;
this.set({mode: "edit"});
var def = $.Deferred();
$.when(this.has_been_loaded).then(function() {
return $.when(this.has_been_loaded).pipe(function() {
if (self.can_be_discarded()) {
var keys = _.keys(self.fields_view.fields);
if (keys.length) {
self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() {
def.resolve();
});
} else {
self.on_record_loaded({}).then(function() {
def.resolve();
});
}
return self.load_defaults();
}
});
return def.promise();
},
on_button_edit: function() {
return this.set({mode: "edit"});
@ -674,6 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
values = {},
first_invalid_field = null;
for (var f in self.fields) {
if (!self.fields.hasOwnProperty(f)) { continue; }
f = self.fields[f];
if (!f.is_valid()) {
form_invalid = true;
@ -689,8 +701,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
if (form_invalid) {
self.set({'display_invalid_fields': true});
for (var f in self.fields) {
self.fields[f]._check_css_flags();
for (var g in self.fields) {
if (!self.fields.hasOwnProperty(g)) { continue; }
self.fields[g]._check_css_flags();
}
first_invalid_field.focus();
self.on_invalid();
@ -722,14 +735,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
});});
},
on_invalid: function() {
var msg = "<ul>";
_.each(this.fields, function(f) {
if (!f.is_valid()) {
msg += "<li>" + f.string + "</li>";
}
});
msg += "</ul>";
this.do_warn("The following fields are invalid :", msg);
var warnings = _(this.fields).chain()
.filter(function (f) { return !f.is_valid(); })
.map(function (f) {
return _.str.sprintf('<li>%s</li>',
_.escape(f.string));
}).value();
warnings.unshift('<ul>');
warnings.push('</ul>');
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 = $('<label/>').attr({
var $label = $('<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, {
@ -3004,8 +3027,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
controller.o2m = self;
if (view_type == "list") {
if (self.get("effective_readonly"))
controller.set_editable(false);
if (self.get("effective_readonly")) {
controller.on('edit:before', self, function (e) {
e.cancel = true;
});
}
} else if (view_type === "form") {
if (self.get("effective_readonly")) {
$(".oe_form_buttons", controller.$element).children().remove();
@ -3189,6 +3215,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 +3265,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,21 +3303,8 @@ 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) {
if (this.editable()) {
this._super.apply(this, arguments);
} else {
var self = this;
@ -3328,55 +3357,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 +3577,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;
@ -4079,10 +4111,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend
self.dataset, false,
_.extend({'deletable': false,
'selectable': !self.options.disable_multiple_selection,
'read_only': true,
'import_enabled': false,
'$buttons': self.$buttonpane,
}, self.options.list_view_options || {}));
self.view_list.on('edit:before', self, function (e) {
e.cancel = true;
});
self.view_list.popup = self;
self.view_list.appendTo($(".oe_popup_list", self.$element)).pipe(function() {
self.view_list.do_show();
@ -4314,7 +4348,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
//link.target = '_blank';
link.href = "data:application/octet-stream;base64," + value;
} else {
$.blockUI();
instance.web.blockUI();
this.session.get_file({
url: '/web/binary/saveas_ajax',
data: {data: JSON.stringify({
@ -4324,7 +4358,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
filename_field: (this.node.attrs.filename || ''),
context: this.view.dataset.get_context()
})},
complete: $.unblockUI,
complete: instance.web.unblockUI,
error: instance.webclient.crashmanager.on_rpc_error
});
ev.stopPropagation();

View File

@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
// whether the view rows can be reordered (via vertical drag & drop)
'reorderable': true,
'action_buttons': true,
// if true, the view can't be editable, ignoring the view's and the context's
// instructions
'read_only': false,
// if true, the 'Import', 'Export', etc... buttons will be shown
'import_enabled': true,
},
@ -142,7 +139,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 +285,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 +408,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 +440,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 +673,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 +930,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 = $('<tr>').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 +1009,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 +1173,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 +1182,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 +1866,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 +1882,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<this.length; ++i) {
record = this.records[i];
if (callback(record)) {
return record;
}
}
},
each: function (callback) {
for(var section in this._proxies) {
if (this._proxies.hasOwnProperty(section)) {
@ -1878,6 +1942,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
},
indexOf: function (record) {
return _(this.records).indexOf(record);
},
succ: function (record, options) {
options = options || {wraparound: false};
var result;
for(var section in this._proxies) {
if (!this._proxies.hasOwnProperty(section)) {
continue;
}
if ((result = this._proxies[section].succ(record, options))) {
return result;
}
}
var index = this.indexOf(record);
if (index === -1) { return null; }
var next_index = index + 1;
if (options.wraparound && (next_index === this.length)) {
return this.at(0);
}
return this.at(next_index);
},
pred: function (record, options) {
options = options || {wraparound: false};
var result;
for (var section in this._proxies) {
if (!this._proxies.hasOwnProperty(section)) {
continue;
}
if ((result = this._proxies[section].pred(record, options))) {
return result;
}
}
var index = this.indexOf(record);
if (index === -1) { return null; }
var next_index = index - 1;
if (options.wraparound && (next_index === -1)) {
return this.at(this.length - 1);
}
return this.at(next_index);
}
});
Collection.include(Events);

File diff suppressed because it is too large Load Diff

View File

@ -274,7 +274,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
},
ir_actions_report_xml: function(action, on_closed) {
var self = this;
$.blockUI();
instance.web.blockUI();
self.rpc("/web/session/eval_domain_and_context", {
contexts: [action.context],
domains: []
@ -284,7 +284,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
self.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: $.unblockUI,
complete: instance.web.unblockUI,
success: function(){
if (!self.dialog && on_closed) {
on_closed();
@ -841,7 +841,7 @@ instance.web.Sidebar = instance.web.Widget.extend({
} else {
self.do_attachement_update(self.dataset, self.model_id);
}
$.unblockUI();
instance.web.unblockUI();
});
},
start: function() {
@ -980,7 +980,7 @@ instance.web.Sidebar = instance.web.Widget.extend({
$e.parent().find('input[type=file]').prop('disabled', true);
$e.parent().find('button').prop('disabled', true).find('img, span').toggle();
this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
$.blockUI();
instance.web.blockUI();
}
},
on_attachment_delete: function(e) {
@ -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];

View File

@ -329,21 +329,20 @@
</span>
</t>
<t t-name="UserMenu.about">
<div>
<a class="oe_activate_debug_mode" href="?debug" style="float:right; font-size: 80%;">Activate the developer mode</a>
<h1 style="margin:0;">OpenERP</h1>
<h3 style="margin:15px 0;padding:0;">Version <t t-esc="version_info.version"/></h3>
<p>
Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br />
OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>.
</p>
<p>
Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a>
</p>
<p>
For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a>
</p>
<div class="oe_about">
<a class="oe_activate_debug_mode oe_right" href="?debug">Activate the developer mode</a>
<img class="oe_logo" src="/web/static/src/img/logo2.png"/>
<h3>Version <t t-esc="version_info.version"/></h3>
<div class="oe_bottom">
<p>Copyright © 2004-TODAY OpenERP SA. All Rights Reserved.<br />
OpenERP is a trademark of the <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP SA Company</a>.</p>
<p>Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/agpl.html" style="text-decoration: underline;">GNU Affero General Public License</a></p>
<p>For more information visit <a target="_blank" href="http://openerp.com/" style="text-decoration: underline;">OpenERP.com</a></p>
</div>
</div>
</t>
<t t-name="UserMenu.password">
<form name="change_password_form" method="POST">
@ -630,9 +629,9 @@
<button type="button" class="oe_button oe_list_add oe_highlight">
<t t-esc="widget.options.addable"/>
</button>
<t t-if="widget.options.import_enabled">
<span class="oe_alternative" t-if="widget.options.import_enabled">
<span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a>
</t>
</span>
</t>
</div>
<t t-name="ListView.pager">
@ -652,6 +651,7 @@
<tr t-name="ListView.row" t-att-class="row_parity"
t-att-data-id="record.get('id')"
t-att-style="view.style_for(record)">
<t t-set="asData" t-value="record.toForm().data"/>
<t t-foreach="columns" t-as="column">
<td t-if="column.meta">
@ -663,23 +663,26 @@
<input t-if="!options.radio" type="checkbox" name="radiogroup" t-att-checked="checked"/>
</th>
<t t-foreach="columns" t-as="column">
<t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/>
<t t-set="number" t-value="column.type === 'integer' or column.type == 'float'"/>
<t t-set="modifiers" t-value="column.modifiers_for(asData)"/>
<td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help"
t-att-class="'oe_list_field_cell' + (align ? ' oe_number' : '')
+ (column.tag === 'button' ? ' oe_button' : '')"
t-att-data-field="column.id">
<t t-raw="render_cell(record, column)"/>
</td>
t-attf-class="oe_list_field_cell oe_list_field_#{column.widget or column.type} #{number ? 'oe_number' : ''} #{column.tag === 'button' ? 'oe-button' : ''} #{modifiers.readonly ? 'oe_readonly' : ''}"
t-att-data-field="column.id"
><t t-raw="render_cell(record, column)"/></td>
</t>
<td t-if="options.deletable" class='oe_list_record_delete' width="1">
<button type="button" name="delete" class="oe_i">d</button>
</td>
</tr>
<t t-name="ListView.row.save">
<td>
<button class='oe_i oe_list_edit_row_save' type='button' name='save'/>
</td>
<t t-extend="ListView.buttons">
<t t-jquery="button.oe_list_add" t-operation="after">
<button class="oe_button oe_list_save oe_highlight"
type="button">Save</button>
</t>
<t t-jquery="a.oe_list_button_import" t-operation="after">
<a href="#" class="oe_bold oe_list_discard">discard</a>
</t>
</t>
<t t-name="FormView">
@ -956,7 +959,7 @@
<span class="oe_form_m2o_follow"/>
</t>
<t t-if="!widget.get('effective_readonly')">
<a href="#" class="oe_m2o_cm_button oe_e oe_right">/</a>
<a href="#" tabindex="-1" class="oe_m2o_cm_button oe_e oe_right">/</a>
<div>
<input type="text"
t-att-id="widget.id_for_label"
@ -1216,6 +1219,8 @@
<div class="oe_searchview_clear"/>
<div class="oe_searchview_unfold_drawer" title="Advanced Search..."/>
<div class="oe_searchview_drawer"/>
<button type="button" class="oe_searchview_search"
title="Search Again">Search</button>
</div>
<div t-name="SearchView.InputView"
@ -1403,8 +1408,8 @@
<div t-name="SearchView.addtodashboard" class="oe_searchview_dashboard">
<h4>Add to Dashboard</h4>
<form>
<input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/>
<button class="oe_apply" type="submit">save</button>
<p><input placeholder ="Title of new Dashboard item" title = "Title of new Dashboard item" type="text"/></p>
<button class="oe_apply" type="submit">Save</button>
</form>
</div>
<t t-name="SearchView.addtodashboard.selection">
@ -1498,7 +1503,7 @@
missing columns
-->
<t t-jquery="&gt; :last" t-operation="after">
<td t-if="edited and !options.deletable" class="oe_list_padding"/>
<td t-if="edited and !options.deletable" class="oe-listview-padding"/>
</t>
</t>

View File

@ -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); });
});
});

View File

@ -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");
});
});

View File

@ -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();

View File

@ -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 () {

View File

@ -13,7 +13,7 @@
<script src="/web/static/lib/backbone/backbone.js" type="text/javascript"></script>
<!-- jquery -->
<script src="/web/static/lib/jquery/jquery-1.7.2b1.js"></script>
<script src="/web/static/lib/jquery/jquery-1.7.2.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>
@ -38,6 +38,12 @@
<script src="/web/static/src/js/search.js"></script>
<script src="/web/static/src/js/view_form.js"></script>
<script src="/web/static/src/js/view_list.js"></script>
<script src="/web/static/src/js/view_list_editable.js"></script>
<script src="/web/static/test/testing.js"></script>
<script type="text/javascript">
QUnit.config.testTimeout = 500;
</script>
</head>
<body id="oe" class="openerp">
<h1 id="qunit-header">OpenERP web Test Suite</h1>
@ -56,4 +62,5 @@
<script type="text/javascript" src="/web/static/test/evals.js"></script>
<script type="text/javascript" src="/web/static/test/search.js"></script>
<script type="text/javascript" src="/web/static/test/Widget.js"></script>
<script type="text/javascript" src="/web/static/test/list-editable.js"></script>
</html>

View File

@ -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);
}
}
})();

View File

@ -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 <EMAIL@ADDRESS>, 2012.
#
msgid ""
msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\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 <EMAIL@ADDRESS>\n"
"Language-Team: Ukrainian <uk@li.org>\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 "&nbsp;"
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 ""

View File

@ -359,6 +359,9 @@
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban {
margin-top: 4px;
}
.openerp .oe_kanban_view .oe_kanban_card .oe_dropdown_kanban .oe_kanban_project_times li {
float: left;
}
.openerp .oe_kanban_view .oe_kanban_star {
float: left;
position: inline-block;
@ -401,9 +404,6 @@
position: relative;
top: 2px;
}
.openerp .oe_kanban_view .oe_kanban_project_times li {
float: left;
}
.openerp .oe_kanban_view .oe_kanban_status {
position: relative;
top: 4px;
@ -471,30 +471,25 @@
visibility: hidden;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker {
padding: 3px 6px;
white-space: nowrap;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker li {
float: left;
margin: 0;
padding: 0;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker li a {
display: inline-block;
width: 18px;
height: 18px;
width: 16px;
height: 16px;
border: 1px solid white;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker li a:hover {
border: 1px solid gray !important;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker li:first-child a {
margin-top: 1px;
height: 16px;
border: 1px solid #cccccc;
}
.openerp .oe_kanban_view .oe_kanban_colorpicker li:first-child a:hover {
margin-top: 0px;
height: 18px;
}
.openerp .oe_kanban_view .oe_kanban_color_0 {
background-color: white;
}

View File

@ -307,6 +307,10 @@
text-decoration: none
.oe_dropdown_kanban
margin-top: 4px
.oe_kanban_project_times
li
float: left
.oe_kanban_star
float: left
position: inline-block
@ -337,9 +341,6 @@
position: relative
top: 2px
.oe_kanban_project_times li
float: left
.oe_kanban_status
position: relative
top: 4px
@ -387,24 +388,20 @@
// }}}
// KanbanColorPicker {{{
.oe_kanban_colorpicker
padding: 3px 6px
white-space: nowrap
.oe_kanban_colorpicker li
float: left
margin: 0
padding: 0
a
display: inline-block
width: 18px
height: 18px
width: 16px
height: 16px
border: 1px solid white
a:hover
border: 1px solid gray !important
.oe_kanban_colorpicker li:first-child a
margin-top: 1px
height: 16px
border: 1px solid #ccc
&:hover
margin-top: 0px
height: 18px
// }}}
// KanbanColors {{{
.oe_kanban_color_0

View File

@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openerp-web\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-07-02 09:06+0200\n"
"PO-Revision-Date: 2012-03-29 11:39+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"PO-Revision-Date: 2012-07-24 06:25+0000\n"
"Last-Translator: Tor Syversen <sol-moe@online.no>\n"
"Language-Team: Norwegian Bokmal <nb@li.org>\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-03 05:55+0000\n"
"X-Generator: Launchpad (build 15531)\n"
"X-Launchpad-Export-Date: 2012-07-25 04:52+0000\n"
"X-Generator: Launchpad (build 15679)\n"
#. openerp-web
#: addons/web_process/static/src/js/process.js:261
@ -85,12 +85,12 @@ msgstr "Notater:"
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:59
msgid "Last modified by:"
msgstr ""
msgstr "Sist revidert av:"
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:59
msgid "N/A"
msgstr ""
msgstr "N/A"
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:62
@ -105,7 +105,7 @@ msgstr ""
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:88
msgid "Select Process"
msgstr ""
msgstr "Velg prosess"
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:98
@ -115,4 +115,4 @@ msgstr "Velg"
#. openerp-web
#: addons/web_process/static/src/xml/web_process.xml:109
msgid "Edit Process"
msgstr ""
msgstr "Rediger prosess"

55
doc/form-notes.rst Normal file
View File

@ -0,0 +1,55 @@
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``?
* No facilities for DOM events handling/delegations e.g. handling
keyup/keydown/keypress from a form fields into the form's user.
* Also no way to reverse from a DOM node (e.g. DOMEvent#target) back to a
form view field easily

View File

@ -19,6 +19,9 @@ Contents:
widget
search-view
list-view
form-notes
Older stuff
-----------

466
doc/list-view.rst Normal file
View File

@ -0,0 +1,466 @@
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.
Editability status
++++++++++++++++++
The editability status of a list view can be queried through the
:js:func:`~openerp.web.ListView.editable` method, will return a falsy
value if the listview is not currently editable.
The editability status is based on three flags:
``tree/@editable``
If present, can be either ``"top"`` or ``"bottom"``. Either will
make the list view editable, with new records being respectively
created at the top or at the bottom of the view.
``context.set_editable``
Boolean flag extracted from a search context (during the
:js:func:`~openerp.web.ListView.do_search`` handler), ``true``
will make the view editable (from the top), ``false`` or the
absence of the flag is a noop.
``defaults.editable``
Like ``tree/@editable``, one of absent (``null``)), ``"top"`` or
``"bottom"``, fallback for the list view if none of the previous
two flags are set.
These three flags can only *make* a listview editable, they can *not*
override a previously set flag. To do that, a listview user should
instead cancel :ref:`the edit:before event <listview-edit-before>`.
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.
Generally speaking, an event should only be cancelled (by
setting the ``cancel`` flag to ``true``), uncancelling an
event is undefined as event handlers are executed on a
first-come-first-serve basis and later handlers may
re-cancel an uncancelled event.
.. _listview-edit-before:
``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<String, openerp.web.form.Field>
: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
<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