Commit 993bff90 authored by Christophe Simonis's avatar Christophe Simonis
Browse files

[MERGE] forward port of branch 7.0 up to revid 4067 chs@openerp.com-20131114142639-ng7wzfjwvvel2nhv

bzr revid: dle@openerp.com-20131112134311-h1vsux0ge17bsqkc
bzr revid: chs@openerp.com-20131114134731-n324awyon0spq624
bzr revid: chs@openerp.com-20130823145204-xwpnlwg0gg2259f6
bzr revid: chs@openerp.com-20130906170157-e7m4pjskyi47q82o
bzr revid: dle@openerp.com-20130909170408-wxgoduzggap6o4ng
bzr revid: dle@openerp.com-20130919141212-ridtrvvfwvu6calr
bzr revid: dle@openerp.com-20131018120136-fvoq337kgx74njsy
bzr revid: dle@openerp.com-20131023103308-18pj2gqq3imrcir7
bzr revid: chs@openerp.com-20131030180528-hqsztaujjjqev8ky
bzr revid: dle@openerp.com-20131106100128-mx8mnguvp321wick
bzr revid: chs@openerp.com-20131115104909-3u3mu40g9xnler88
parents 079bf317 1e4b2695
......@@ -741,9 +741,6 @@
display: block;
color: #4c4c4c;
text-decoration: none;
width: 200px;
text-overflow: ellipsis;
overflow: hidden;
}
.openerp .oe_dropdown_menu > li > a:hover {
text-decoration: none;
......@@ -1455,7 +1452,7 @@
display: table-row;
height: inherit;
}
.openerp .oe_view_manager .oe_view_manager_view_kanban {
.openerp .oe_view_manager .oe_view_manager_view_kanban:not(:empty) {
height: inherit;
}
.openerp .oe_view_manager table.oe_view_manager_header {
......
......@@ -632,9 +632,6 @@ $sheet-padding: 16px
display: block
color: #4c4c4c
text-decoration: none
width: 200px
text-overflow: ellipsis
overflow: hidden
&:hover
text-decoration: none
.oe_dropdown_arrow:after
......@@ -1171,7 +1168,7 @@ $sheet-padding: 16px
.oe_view_manager_body
display: table-row
height: inherit
.oe_view_manager_view_kanban
.oe_view_manager_view_kanban:not(:empty)
height: inherit
table.oe_view_manager_header
......
......@@ -432,7 +432,7 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
// TODO: reorder results to match ids list
return this._model.call('read',
[ids, fields || false],
{context: this._model.context(options.context)});
{context: this.get_context(options.context)});
},
/**
* Read a slice of the records represented by this DataSet, based on its
......@@ -724,7 +724,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({
});
},
get_domain: function (other_domain) {
this._model.domain(other_domain);
return this._model.domain(other_domain);
},
alter_ids: function (ids) {
this._super(ids);
......@@ -761,6 +761,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
this._super.apply(this, arguments);
this.reset_ids([]);
this.last_default_get = {};
this.running_reads = [];
},
default_get: function(fields, options) {
var self = this;
......@@ -827,6 +828,9 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
this.to_write = [];
this.cache = [];
this.delete_all = false;
_.each(_.clone(this.running_reads), function(el) {
el.reject();
});
},
read_ids: function (ids, fields, options) {
var self = this;
......@@ -842,7 +846,6 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
to_get.push(id);
}
});
var completion = $.Deferred();
var return_records = function() {
var records = _.map(ids, function(id) {
return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id});
......@@ -877,10 +880,20 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
}, 0);
});
}
completion.resolve(records);
return $.when(records);
};
if(to_get.length > 0) {
var rpc_promise = this._super(to_get, fields, options).done(function(records) {
var def = $.Deferred();
self.running_reads.push(def);
def.always(function() {
self.running_reads = _.without(self.running_reads, def);
});
this._super(to_get, fields, options).then(function() {
def.resolve.apply(def, arguments);
}, function() {
def.reject.apply(def, arguments);
});
return def.then(function(records) {
_.each(records, function(record, index) {
var id = to_get[index];
var cached = _.detect(self.cache, function(x) {return x.id === id;});
......@@ -891,13 +904,11 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
cached.values = _.defaults(_.clone(cached.values), record);
}
});
return_records();
return return_records();
});
$.when(rpc_promise).fail(function() {completion.reject();});
} else {
return_records();
return return_records();
}
return completion.promise();
},
/**
* Invalidates caching of a record in the dataset to ensure the next read
......
......@@ -1540,7 +1540,7 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
context: context
}).then(function (results) {
if (_.isEmpty(results)) { return null; }
return [{label: _.escape(self.attrs.string)}].concat(
return [{label: self.attrs.string}].concat(
_(results).map(function (result) {
return {
label: _.escape(result[1]),
......@@ -1726,7 +1726,10 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
var $name = this.$('input:first');
var private_filter = !this.$('#oe_searchview_custom_public').prop('checked');
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
if (_.isEmpty($name.val())){
this.do_warn(_t("Error"), _t("Filter name is required."));
return false;
}
var search = this.view.build_search_data();
instance.web.pyeval.eval_domains_and_contexts({
domains: search.domains,
......
......@@ -3027,7 +3027,7 @@ instance.web.form.CompletionFieldMixin = {
values.push({
label: _t("Search More..."),
action: function() {
dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) {
dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
self._search_create_popup("search", data);
});
},
......@@ -3649,7 +3649,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
_.extend(view.options, {
addable: null,
selectable: self.multi_selection,
sortable: false,
sortable: true,
import_enabled: false,
deletable: true
});
......@@ -4044,7 +4044,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
else
return $.when();
}).done(function () {
self.handle_button(name, id, callback);
if (!self.o2m.options.reload_on_button) {
self.handle_button(name, id, callback);
}else {
self.handle_button(name, id, function(){
self.o2m.view.reload();
});
}
});
},
......
......@@ -496,17 +496,21 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
*
* @returns {$.Deferred} promise to content reloading
*/
reload_content: function () {
reload_content: synchronized(function () {
var self = this;
self.$el.find('.oe_list_record_selector').prop('checked', false);
this.records.reset();
var reloaded = $.Deferred();
this.$el.find('.oe_list_content').append(
this.groups.render(function () {
if (self.dataset.index == null && self.records.length ||
self.dataset.index >= self.records.length) {
if (self.dataset.index == null) {
if (self.records.length) {
self.dataset.index = 0;
}
} else if (self.dataset.index >= self.records.length) {
self.dataset.index = 0;
}
self.compute_aggregates();
reloaded.resolve();
}));
......@@ -515,7 +519,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
limit: this._limit
});
return reloaded.promise();
},
}),
reload: function () {
return this.reload_content();
},
......@@ -1328,6 +1332,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
.removeClass('ui-icon-triangle-1-s')
.addClass('ui-icon-triangle-1-e');
child.close();
// force recompute the selection as closing group reset properties
var selection = self.get_selection();
$(self).trigger('selected', [selection.ids, this.records]);
}
});
}
......@@ -1338,22 +1345,29 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
if (group.grouped_on) {
var row_data = {};
row_data[group.grouped_on] = group;
var group_label = _t("Undefined");
var group_column = _(self.columns).detect(function (column) {
return column.id === group.grouped_on; });
if (! group_column) {
throw new Error(_.str.sprintf(
_t("Grouping on field '%s' is not possible because that field does not appear in the list view."),
group.grouped_on));
}
var group_label;
try {
group_label = group_column.format(row_data, {
value_if_empty: _t("Undefined"),
process_modifiers: false
});
} catch (e) {
group_label = _.str.escapeHTML(row_data[group_column.id].value);
if (group_column) {
try {
group_label = group_column.format(row_data, {
value_if_empty: _t("Undefined"),
process_modifiers: false
});
} catch (e) {
group_label = _.str.escapeHTML(row_data[group_column.id].value);
}
} else {
group_label = group.value;
if (group_label instanceof Array) {
group_label = group_label[1];
}
if (group_label === false) {
group_label = _t('Undefined');
}
group_label = _.str.escapeHTML(group_label);
}
// group_label is html-clean (through format or explicit
// escaping if format failed), can inject straight into HTML
$group_column.html(_.str.sprintf(_t("%s (%d)"),
......@@ -1425,14 +1439,13 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
var view = this.view,
limit = view.limit(),
d = new $.Deferred(),
page = this.datagroup.openable ? this.page : view.page;
var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
var options = { offset: page * limit, limit: limit, context: {bin_size: true} };
//TODO xmo: investigate why we need to put the setTimeout
$.async_when().done(function() {
dataset.read_slice(fields, options).done(function (records) {
return $.async_when().then(function() {
return dataset.read_slice(fields, options).then(function (records) {
// FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
if (self.records.length) {
self.records.reset(null, {silent: true});
......@@ -1464,13 +1477,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
self.records.add(records, {silent: true});
list.render();
d.resolve(list);
if (_.isEmpty(records)) {
view.no_result();
}
return list;
});
});
return d.promise();
},
setup_resequence_rows: function (list, dataset) {
// drag and drop enabled if list is not sorted and there is a
......@@ -1550,11 +1562,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
self.render_groups(groups));
if (post_render) { post_render(); }
}, function (dataset) {
self.render_dataset(dataset).done(function (list) {
self.render_dataset(dataset).then(function (list) {
self.children[null] = list;
self.elements =
[list.$current.replaceAll($el)[0]];
self.setup_resequence_rows(list, dataset);
}).always(function() {
if (post_render) { post_render(); }
});
});
......@@ -1597,6 +1610,22 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
}
});
/**
* Serializes concurrent calls to this asynchronous method. The method must
* return a deferred or promise.
*
* Current-implementation is class-serialized (the mutex is common to all
* instances of the list view). Can be switched to instance-serialized if
* having concurrent list views becomes possible and common.
*/
function synchronized(fn) {
var fn_mutex = new $.Mutex();
return function () {
var args = _.toArray(arguments);
args.unshift(this);
return fn_mutex.exec(fn.bind.apply(fn, args));
};
}
var DataGroup = instance.web.Class.extend({
init: function(parent, model, domain, context, group_by, level) {
this.model = new instance.web.Model(model, context, domain);
......@@ -1608,6 +1637,10 @@ var DataGroup = instance.web.Class.extend({
},
list: function (fields, ifGroups, ifRecords) {
var self = this;
if (!_.isEmpty(this.group_by)) {
// ensure group_by fields are read.
fields = _.unique((fields || []).concat(this.group_by));
}
var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by);
$.when(query).done(function (querygroups) {
// leaf node
......
......@@ -163,12 +163,13 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
var is_loaded = 0,
$this = $(this),
record_id = $this.data('id'),
row_parent_id = $this.data('row-parent-id'),
record = self.records[record_id],
children_ids = record[self.children_field];
_(children_ids).each(function(childid) {
if (self.$el.find('#treerow_' + childid).length) {
if (self.$el.find('#treerow_' + childid).is(':hidden')) {
if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').length ) {
if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').is(':hidden')) {
is_loaded = -1;
} else {
is_loaded++;
......@@ -180,7 +181,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
self.getdata(record_id, children_ids);
}
} else {
self.showcontent(record_id, is_loaded < 0);
self.showcontent($this, record_id, is_loaded < 0);
}
});
},
......@@ -192,7 +193,6 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
_(records).each(function (record) {
self.records[record.id] = record;
});
var $curr_node = self.$el.find('#treerow_' + id);
var children_rows = QWeb.render('TreeView.rows', {
'records': records,
......@@ -201,9 +201,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
'fields': self.fields,
'level': $curr_node.data('level') || 0,
'render': instance.web.format_value,
'color_for': self.color_for
'color_for': self.color_for,
'row_parent_id': id
});
if ($curr_node.length) {
$curr_node.addClass('oe_open');
$curr_node.after(children_rows);
......@@ -243,14 +243,13 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
},
// show & hide the contents
showcontent: function (record_id, show) {
this.$el.find('#treerow_' + record_id)
.toggleClass('oe_open', show);
showcontent: function (curnode,record_id, show) {
curnode.parent('tr').toggleClass('oe_open', show);
_(this.records[record_id][this.children_field]).each(function (child_id) {
var $child_row = this.$el.find('#treerow_' + child_id);
var $child_row = this.$el.find('[id=treerow_' + child_id + '][data-row-parent-id='+ curnode.data('id') +']');
if ($child_row.hasClass('oe_open')) {
this.showcontent(child_id, false);
$child_row.toggleClass('oe_open',show);
this.showcontent($child_row, child_id, false);
}
$child_row.toggle(show);
}, this);
......
......@@ -684,7 +684,8 @@
<tr t-name="TreeView.rows"
t-foreach="records" t-as="record"
t-att-id="'treerow_' + record.id"
t-att-data-id="record.id" t-att-data-level="level + 1">
t-att-data-id="record.id" t-att-data-level="level + 1"
t-att-data-row-parent-id="row_parent_id">
<t t-set="children" t-value="record[children_field]"/>
<t t-set="class" t-value="children and children.length ? 'treeview-tr' : 'treeview-td'"/>
<t t-set="rank" t-value="'oe-treeview-first'"/>
......@@ -1651,8 +1652,8 @@
<ul class="oe_searchview_custom_list"/>
<div class="oe_searchview_custom">
<h4>Save current filter</h4>
<form>
<p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
<form class="oe_form">
<p class="oe_form_required"><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
<p>
<input id="oe_searchview_custom_public" type="checkbox"/>
<label for="oe_searchview_custom_public">Share with all users</label>
......
......@@ -1293,7 +1293,7 @@ openerp.testing.section('search.filters.saved', {
return view.appendTo($fix)
.then(function () {
$fix.find('.oe_searchview_custom input#oe_searchview_custom_input')
.text("filter name")
.val("filter name")
.end()
.find('.oe_searchview_custom button').click();
return done.promise();
......
......@@ -566,8 +566,8 @@
display: inline-block !important; }
.openerp_ie .oe_kanban_view .oe_kanban_group_title_vertical {
-ms-writing-mode: lr-tb !important;
background: #f0eeee;
top: -5px !important; }
background: #f0eeee;}
.openerp_ie .oe_kanban_view.oe_kanban_grouped .oe_kanban_group_header {
height: 1%; }
......
......@@ -584,7 +584,6 @@
.oe_kanban_group_title_vertical
-ms-writing-mode: lr-tb !important
background: rgb(240, 238, 238)
top: -5px !important
&.oe_kanban_grouped
.oe_kanban_group_header
height: 1%
......
......@@ -235,7 +235,11 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(grouping_fields);
if (!_.isEmpty(grouping_fields)) {
// ensure group_by fields are read.
self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields));
}
var grouping = new instance.web.Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields);
return self.alive($.when(grouping)).done(function(groups) {
self.remove_no_result();
if (groups) {
......
from openerp.osv import orm, fields
class TestObject(orm.Model):
class TestObject(orm.TransientModel):
_name = 'web_tests_demo.model'
_columns = {
......
......@@ -253,7 +253,7 @@ class ir_fields_converter(orm.Model):
if not isinstance(selection, (tuple, list)):
# FIXME: Don't pass context to avoid translations?
# Or just copy context & remove lang?
selection = selection(model, cr, uid)
selection = selection(model, cr, uid, context=None)
for item, label in selection:
labels = self._get_translations(
cr, uid, ('selection', 'model', 'code'), label, context=context)
......
......@@ -304,5 +304,47 @@ class test_partner_recursion(common.TransactionCase):
cr, uid, p1, p2, p3 = self.cr, self.uid, self.p1, self.p2, self.p3
self.assertTrue(self.res_partner.write(cr, uid, [p1,p2,p3], {'phone': '123456'}))
class test_translation(common.TransactionCase):
def setUp(self):
super(test_translation, self).setUp()
self.res_category = self.registry('res.partner.category')
self.ir_translation = self.registry('ir.translation')
cr, uid = self.cr, self.uid
self.registry('ir.translation').load(cr, ['base'], ['fr_BE'])
self.cat_id = self.res_category.create(cr, uid, {'name': 'Customers'})
self.ir_translation.create(cr, uid, {'name': 'res.partner.category,name', 'module':'base',
'value': 'Clients', 'res_id': self.cat_id, 'lang':'fr_BE', 'state':'translated', 'type': 'model'})
def test_101_create_translated_record(self):
cr, uid = self.cr, self.uid
no_context_cat = self.res_category.browse(cr, uid, self.cat_id)
self.assertEqual(no_context_cat.name, 'Customers', "Error in basic name_get")
fr_context_cat = self.res_category.browse(cr, uid, self.cat_id, context={'lang':'fr_BE'})
self.assertEqual(fr_context_cat.name, 'Clients', "Translation not found")
def test_102_duplicate_record(self):
cr, uid = self.cr, self.uid
self.new_cat_id = self.res_category.copy(cr, uid, self.cat_id, context={'lang':'fr_BE'})
no_context_cat = self.res_category.browse(cr, uid, self.new_cat_id)
self.assertEqual(no_context_cat.name, 'Customers', "Duplication did not set untranslated value")
fr_context_cat = self.res_category.browse(cr, uid, self.new_cat_id, context={'lang':'fr_BE'})
self.assertEqual(fr_context_cat.name, 'Clients', "Did not found translation for initial value")
def test_103_duplicate_record_fr(self):
cr, uid = self.cr, self.uid
self.new_fr_cat_id = self.res_category.copy(cr, uid, self.cat_id, default={'name': 'Clients (copie)'}, context={'lang':'fr_BE'})
no_context_cat = self.res_category.browse(cr, uid, self.new_fr_cat_id)
self.assertEqual(no_context_cat.name, 'Clients (copie)', "Duplication with default value not applied")
fr_context_cat = self.res_category.browse(cr, uid, self.new_fr_cat_id, context={'lang':'fr_BE'})
self.assertEqual(fr_context_cat.name, 'Clients', "Did not found translation for initial value")
if __name__ == '__main__':
unittest2.main()
......@@ -5016,7 +5016,6 @@ class BaseModel(object):
# TODO it seems fields_get can be replaced by _all_columns (no need for translation)
fields = self.fields_get(cr, uid, context=context)
translation_records = []
for field_name, field_def in fields.items():
# we must recursively copy the translations for o2o and o2m
if field_def['type'] == 'one2many':
......@@ -5030,22 +5029,31 @@ class BaseModel(object):
target_obj.copy_translations(cr, uid, old_child, new_child, context=context)
# and for translatable fields we keep them for copy
elif field_def.get('translate'):
trans_name = ''
if field_name in self._columns:
trans_name = self._name + "," + field_name
res_id = new_id
elif field_name in self._inherit_fields:
trans_name = self._inherit_fields[field_name][0] + "," + field_name
if trans_name:
trans_ids = trans_obj.search(cr, uid, [
('name', '=', trans_name),
('res_id', '=', old_id)
])
translation_records.extend(trans_obj.read(cr, uid, trans_ids, context=context))
# get the id of the parent record to set the translation
inherit_field_name = self._inherit_fields[field_name][1]
res_id = self.read(cr, uid, [new_id], [inherit_field_name], context=context)[0][inherit_field_name][0]
else:
continue
for record in translation_records:
del record['id']
record['res_id'] = new_id
trans_obj.create(cr, uid, record, context=context)
trans_ids = trans_obj.search(cr, uid, [
('name', '=', trans_name),
('res_id', '=', old_id)
])
records = trans_obj.read(cr, uid, trans_ids, context=context)
for record in records:
del record['id']
# remove source to avoid triggering _set_src
del record['source']
record.update({'res_id': res_id})
trans_obj.create(cr, uid, record, context=context)