[ADD] treeview

bzr revid: xmo@openerp.com-20110809155516-f35pjfviu0zunkis
This commit is contained in:
Xavier Morel
2011-08-09 17:55:16 +02:00
11 changed files with 367 additions and 86 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
"static/lib/underscore/underscore.string.js",
"static/src/js/boot.js",
"static/src/js/core.js",
"static/src/js/dates.js",
"static/src/js/formats.js",
"static/src/js/chrome.js",
"static/src/js/views.js",
"static/src/js/data.js",
+28 -4
View File
@@ -390,10 +390,10 @@ def load_actions_from_ir_values(req, key, key2, models, meta, context):
Values = req.session.model('ir.values')
actions = Values.get(key, key2, models, meta, context)
return [(id, name, clean_action(action, req.session))
return [(id, name, clean_action(action, req.session, context=context))
for id, name, action in actions]
def clean_action(action, session):
def clean_action(action, session, context=None):
action.setdefault('flags', {})
if action['type'] != 'ir.actions.act_window':
return action
@@ -401,7 +401,7 @@ def clean_action(action, session):
if isinstance(action.get('context'), basestring):
action['context'] = eval(
action['context'],
session.evaluation_context()) or {}
session.evaluation_context(context=context)) or {}
if isinstance(action.get('domain'), basestring):
action['domain'] = eval(
@@ -559,6 +559,7 @@ class DataSet(openerpweb.Controller):
:rtype: list
"""
Model = request.session.model(model)
context, domain = eval_context_and_domain(
request.session, request.context, domain)
@@ -581,9 +582,14 @@ class DataSet(openerpweb.Controller):
}
@openerpweb.jsonrequest
def read(self, request, model, ids, fields=False):
return self.do_search_read(request, model, ids, fields)
@openerpweb.jsonrequest
def get(self, request, model, ids, fields=False):
return self.do_get(request, model, ids, fields)
def do_get(self, request, model, ids, fields=False):
""" Fetches and returns the records of the model ``model`` whose ids
are in ``ids``.
@@ -668,6 +674,12 @@ class DataSet(openerpweb.Controller):
Model = req.session.model(model)
return Model.default_get(fields, req.session.eval_context(req.context))
@openerpweb.jsonrequest
def name_search(self, req, model, search_str, domain=[], context={}):
m = req.session.model(model)
r = m.name_search(search_str+'%', domain, '=ilike', context)
return {'result': r}
class DataGroup(openerpweb.Controller):
_cp_path = "/base/group"
@openerpweb.jsonrequest
@@ -1001,6 +1013,19 @@ class Action(openerpweb.Controller):
return clean_action(req.session.model('ir.actions.server').run(
[action_id], req.session.eval_context(req.context)), req.session)
class TreeView(View):
_cp_path = "/base/treeview"
@openerpweb.jsonrequest
def load(self, req, model, view_id, toolbar=False):
return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
@openerpweb.jsonrequest
def action(self, req, model, id):
return load_actions_from_ir_values(
req,'action', 'tree_but_open',[(model, id)],
False, req.session.eval_context(req.context))
def export_csv(fields, result):
fp = StringIO()
writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
@@ -1213,4 +1238,3 @@ class Export(View):
return export_xls(field, result)
else:
return export_csv(field, result)
+33 -4
View File
@@ -269,7 +269,7 @@ label.error {
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
color: #eee;
text-shadow: #222 0 1px 0;
text-decoration: none;
@@ -1053,7 +1053,7 @@ label.error {
padding-left: 10px;
padding-right: 21px;
height: 21px;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #ebe9e9 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(100%,#ebe9e9)); /* Chrome,Safari4+ */
@@ -1062,7 +1062,7 @@ background: -o-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* Opera11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* IE10+ */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#EBE9E9',GradientType=0 ); /* IE6-9 */
background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */
border: 1px solid #D2CFCF;
border-right-width: 0;
border-left-width: 0;
@@ -1167,7 +1167,9 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */
.openerp .oe-m2o-disabled-cm {
color: grey;
}
.parent_top {
vertical-align: text-top;
}
.openerp .oe-dialog-warning p {
padding-left: 1em;
font-size: 1.2em;
@@ -1180,3 +1182,30 @@ background: linear-gradient(top, #ffffff 0%,#ebe9e9 100%); /* W3C */
-webkit-box-shadow: none;
box-shadow: none;
}
.openerp .oe-treeview-table {
width: 100%;
}
.treeview-tr, .treeview-td {
cursor: pointer;
vertical-align: top;
text-align: left;
}
.treeview-tr span, .treeview-td span {
display: block;
}
.treeview-tr:first-of-type {
background: transparent url(/base/static/src/img/expand.gif) 0 50% no-repeat;
}
.oe-open .treeview-tr:first-of-type {
background-image: url(/base/static/src/img/collapse.gif);
}
.treeview-tr:first-of-type span,
.treeview-td:first-of-type span {
margin-left: 16px;
}
.treeview-header {
text-align: left;
vertical-align: top;
}
+1 -1
View File
@@ -60,7 +60,7 @@
openerp.base = function(instance) {
openerp.base.core(instance);
openerp.base.dates(instance);
openerp.base.formats(instance);
openerp.base.chrome(instance);
openerp.base.data(instance);
if (openerp.base.views) {
@@ -1,5 +1,5 @@
openerp.base.dates = function(openerp) {
openerp.base.formats = function(openerp) {
/**
* Converts a string to a Date javascript object using OpenERP's
@@ -133,5 +133,75 @@ openerp.base.format_time = function(obj) {
return fts(obj.getHours(),2) + ":" + fts(obj.getMinutes(),2) + ":"
+ fts(obj.getSeconds(),2);
};
/**
* Formats a single atomic value based on a field descriptor
*
* @param {Object} value read from OpenERP
* @param {Object} descriptor union of orm field and view field
* @param {Object} [descriptor.widget] widget to use to display the value
* @param {Object} descriptor.type fallback if no widget is provided, or if the provided widget is unknown
* @param {Object} [descriptor.digits] used for the formatting of floats
* @param {String} [value_if_empty=''] returned if the ``value`` argument is considered empty
*/
openerp.base.format_value = function (value, descriptor, value_if_empty) {
// If NaN value, display as with a `false` (empty cell)
if (typeof value === 'number' && isNaN(value)) {
value = false;
}
switch (value) {
case false:
case Infinity:
case -Infinity:
return value_if_empty === undefined ? '' : value_if_empty;
}
switch (descriptor.widget || descriptor.type) {
case 'integer':
return _.sprintf('%d', value);
case 'float':
var precision = descriptor.digits ? descriptor.digits[1] : 2;
return _.sprintf('%.' + precision + 'f', value);
case 'float_time':
return _.sprintf("%02d:%02d",
Math.floor(value),
Math.round((value % 1) * 60));
case 'progressbar':
return _.sprintf(
'<progress value="%.2f" max="100.0">%.2f%%</progress>',
value, value);
case 'many2one':
// name_get value format
return value[1];
default:
return value;
}
};
/**
* Formats a provided cell based on its field type
*
* @param {Object} row_data record whose values should be displayed in the cell
* @param {Object} column column descriptor
* @param {"button"|"field"} column.tag base control type
* @param {String} column.type widget type for a field control
* @param {String} [column.string] button label
* @param {String} [column.icon] button icon
* @param {String} [value_if_empty=''] what to display if the field's value is ``false``
*/
openerp.base.format_cell = function (row_data, column, value_if_empty) {
var attrs = column.modifiers_for(row_data);
if (attrs.invisible) { return ''; }
if (column.tag === 'button') {
return [
'<button type="button" title="', column.string || '', '">',
'<img src="/base/static/src/img/icons/', column.icon, '.png"',
' alt="', column.string || '', '"/>',
'</button>'
].join('')
}
return openerp.base.format_value(
row_data[column.id].value, column, value_if_empty);
}
};
+4 -62
View File
@@ -1,63 +1,5 @@
openerp.base.list = function (openerp) {
openerp.base.views.add('list', 'openerp.base.ListView');
openerp.base.list = {
/**
* Formats the rendring of a given value based on its field type
*
* @param {Object} row_data record whose values should be displayed in the cell
* @param {Object} column column descriptor
* @param {"button"|"field"} column.tag base control type
* @param {String} column.type widget type for a field control
* @param {String} [column.string] button label
* @param {String} [column.icon] button icon
* @param {String} [value_if_empty=''] what to display if the field's value is ``false``
*/
render_cell: function (row_data, column, value_if_empty) {
var attrs = column.modifiers_for(row_data);
if (attrs.invisible) { return ''; }
if (column.tag === 'button') {
return [
'<button type="button" title="', column.string || '', '">',
'<img src="/base/static/src/img/icons/', column.icon, '.png"',
' alt="', column.string || '', '"/>',
'</button>'
].join('')
}
var value = row_data[column.id].value;
// If NaN value, display as with a `false` (empty cell)
if (typeof value === 'number' && isNaN(value)) {
value = false;
}
switch (value) {
case false:
case Infinity:
case -Infinity:
return value_if_empty === undefined ? '' : value_if_empty;
}
switch (column.widget || column.type) {
case 'integer':
return _.sprintf('%d', value);
case 'float':
var precision = column.digits ? column.digits[1] : 2;
return _.sprintf('%.' + precision + 'f', value);
case 'float_time':
return _.sprintf("%02d:%02d",
Math.floor(value),
Math.round((value % 1) * 60));
case 'progressbar':
return _.sprintf(
'<progress value="%.2f" max="100.0">%.2f%%</progress>',
value, value);
case 'many2one':
// name_get value format
return value[1];
default:
return value;
}
}
};
openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ {
defaults: {
// records can be selected one by one
@@ -637,7 +579,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
}
$footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
.html(openerp.base.list.render_cell(aggregation, column));
.html(openerp.base.format_cell(aggregation, column));
});
}
// TODO: implement reorder (drag and drop rows)
@@ -725,7 +667,7 @@ openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base.
this.$current = this.$_element.clone(true);
this.$current.empty().append(
QWeb.render('ListView.rows', _.extend({
render_cell: openerp.base.list.render_cell}, this)));
render_cell: openerp.base.format_cell}, this)));
},
/**
* Gets the ids of all currently selected records, if any
@@ -856,7 +798,7 @@ openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base.
row: this.rows[record_index],
row_parity: (record_index % 2 === 0) ? 'even' : 'odd',
row_index: record_index,
render_cell: openerp.base.list.render_cell
render_cell: openerp.base.format_cell
});
},
/**
@@ -1016,7 +958,7 @@ openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.bas
row_data[group.grouped_on] = group;
var group_column = _(self.columns).detect(function (column) {
return column.id === group.grouped_on; });
$group_column.html(openerp.base.list.render_cell(
$group_column.html(openerp.base.format_cell(
row_data, group_column, "Undefined"
));
if (group.openable) {
+186 -5
View File
@@ -5,22 +5,203 @@
openerp.base.view_tree = function(openerp) {
openerp.base.views.add('tree', 'openerp.base.TreeView');
openerp.base.TreeView = openerp.base.Widget.extend({
/**
* Genuine tree view (the one displayed as a tree, not the list)
*/
openerp.base.TreeView = openerp.base.View.extend({
/**
* Indicates that this view is not searchable, and thus that no search
* view should be displayed (if there is one active).
*/
searchable : false,
init: function(parent, element_id, dataset, view_id, options) {
this._super(parent, element_id);
this.dataset = dataset;
this.model = dataset.model;
this.view_id = view_id;
this.records = {};
this.options = _.extend({}, this.defaults, options || {});
},
start: function () {
this._super();
this.$element.append('Tree view');
return this.rpc("/base/treeview/load", {
model: this.model,
view_id: this.view_id,
toolbar: this.view_manager ? !!this.view_manager.sidebar : false
}, this.on_loaded);
},
/**
* Returns the list of fields needed to correctly read objects.
*
* Gathers the names of all fields in fields_view_get, and adds the
* field_parent (children_field in the tree view) if it's not already one
* of the fields to fetch
*
* @returns {Array} an array of fields which can be provided to DataSet.read_slice and others
*/
fields_list: function () {
var fields = _.keys(this.fields);
if (!_(fields).contains(this.children_field)) {
fields.push(this.children_field);
}
return fields;
},
on_loaded: function (fields_view) {
var self = this;
var has_toolbar = !!fields_view.arch.attrs.toolbar;
// field name in OpenERP is kinda stupid: this is the name of the field
// holding the ids to the children of the current node, why call it
// field_parent?
this.children_field = fields_view['field_parent'];
this.fields_view = fields_view;
_(this.fields_view.arch.children).each(function (field) {
if (field.attrs.modifiers) {
field.attrs.modifiers = JSON.parse(field.attrs.modifiers);
}
});
this.fields = fields_view.fields;
this.hook_row_click();
this.$element.html(QWeb.render('TreeView', {
'title': this.fields_view.arch.attrs.string,
'fields_view': this.fields_view.arch.children,
'fields': this.fields,
'toolbar': has_toolbar
}));
this.dataset.read_slice(this.fields_list(), 0, false, function (records) {
if (!has_toolbar) {
// WARNING: will do a second read on the same ids, but only on
// first load so not very important
self.getdata(null, _(records).pluck('id'));
return;
}
var $select = self.$element.find('select')
.change(function () {
var $option = $(this).find(':selected');
self.getdata($option.val(), $option.data('children'));
});
_(records).each(function (record) {
self.records[record.id] = record;
$('<option>')
.val(record.id)
.text(record.name)
.data('children', record[self.children_field])
.appendTo($select);
});
if (!_.isEmpty(records)) {
$select.change();
}
});
},
/**
* Sets up opening a row
*/
hook_row_click: function () {
var self = this;
this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) {
e.stopImmediatePropagation();
self.activate($(this).closest('tr').data('id'));
});
this.$element.delegate('.treeview-tr', 'click', function () {
var is_loaded = 0,
$this = $(this),
record_id = $this.data('id'),
record = self.records[record_id],
children_ids = record[self.children_field];
_(children_ids).each(function(childid) {
if (self.$element.find('#treerow_' + childid).length) {
if (self.$element.find('#treerow_' + childid).is(':hidden')) {
is_loaded = -1;
} else {
is_loaded++;
}
}
});
if (is_loaded === 0) {
if (!$this.parent().hasClass('oe-open')) {
self.getdata(record_id, children_ids);
}
} else {
self.showcontent(record_id, is_loaded < 0);
}
});
},
// get child data of selected value
getdata: function (id, children_ids) {
var self = this;
self.dataset.read_ids(children_ids, this.fields_list(), function (records) {
_(records).each(function (record) {
self.records[record.id] = record;
});
var $curr_node = self.$element.find('#treerow_' + id);
var children_rows = QWeb.render('TreeView.rows', {
'records': records,
'children_field': self.children_field,
'fields_view': self.fields_view.arch.children,
'fields': self.fields,
'level': $curr_node.data('level') || 0,
'render': openerp.base.format_value
});
if ($curr_node.length) {
$curr_node.addClass('oe-open');
$curr_node.after(children_rows);
} else {
self.$element.find('tbody').html(children_rows);
}
});
},
// Get details in listview
activate: function(id) {
var self = this;
this.rpc('/base/treeview/action', {
id: id,
model: this.dataset.model,
context: new openerp.base.CompoundContext(
this.dataset.get_context(), {
active_model: this.dataset.model,
active_id: id,
active_ids: [id]})
}, function (actions) {
if (!actions.length) { return; }
var action = actions[0][2];
action.flags.new_window = true;
self.session.action_manager.do_action(action);
});
},
// show & hide the contents
showcontent: function (record_id, show) {
this.$element.find('#treerow_' + record_id)
.toggleClass('oe-open', show);
_(this.records[record_id][this.children_field]).each(function (child_id) {
var $child_row = this.$element.find('#treerow_' + child_id);
if ($child_row.hasClass('oe-open')) {
this.showcontent(child_id, false);
}
$child_row.toggle(show);
}, this);
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
this.hidden = true;
}
});
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
+4 -4
View File
@@ -219,7 +219,7 @@ openerp.base.ViewManager = openerp.base.Widget.extend({
},
/**
* Event launched when a controller has been inited.
*
*
* @param {String} view_type type of view
* @param {String} view the inited controller
*/
@@ -291,11 +291,11 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
this.session = parent.session;
var dataset;
if (!action.res_id) {
dataset = new openerp.base.DataSetSearch(this, action.res_model, action.context || null);
dataset = new openerp.base.DataSetSearch(this, action.res_model, action.context || null, action.domain || null);
} else {
dataset = new openerp.base.DataSetStatic(this, action.res_model, {}, [action.res_id]);
if (action.context) {
// TODO fme: should normalize all DataSets constructors to (session, model, context, ...)
// TODO fme: should normalize all DataSets constructors to (session, model, context, domain, ...)
dataset.context = action.context;
}
}
@@ -512,7 +512,7 @@ openerp.base.View = openerp.base.Widget.extend({
* Directly set a view to use instead of calling fields_view_get. This method must
* be called before start(). When an embedded view is set, underlying implementations
* of openerp.base.View must use the provided view instead of any other one.
*
*
* @param embedded_view A view.
*/
set_embedded_view: function(embedded_view) {
+35
View File
@@ -449,6 +449,41 @@
</ul>
</div>
</t>
<t t-name="TreeView">
<h2 class="oe_view_title"><t t-esc="title"/></h2>
<select t-if="toolbar" style="width: 30%">
</select>
<table class="oe-treeview-table">
<thead>
<tr>
<th t-foreach="fields_view" t-as="field"
t-if="!field.attrs.modifiers.tree_invisible"
class="treeview-header">
<t t-esc="fields[field.attrs.name].string" />
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</t>
<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 t-set="children" t-value="record[children_field]"/>
<t t-set="has_children" t-value="children and children.length"/>
<td t-foreach="fields_view" t-as="field"
t-if="!field.attrs.modifiers.tree_invisible"
t-att-data-id="record.id"
t-att-style="!field_index ? 'background-position: ' + 19*level + 'px; padding-left: ' + 19*level + 'px' : undefined"
t-att-class="!field_index and has_children ? 'treeview-tr' : 'treeview-td'">
<span t-if="!field.attrs.modifiers.invisible">
<t t-esc="render(record[field.attrs.name], fields[field.attrs.name])" />
</span>
</td>
</tr>
<table t-name="ListView" class="oe-listview-content">
<t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/>
<thead class="ui-widget-header">
+1 -1
View File
@@ -18,7 +18,7 @@
<script src="/base/static/src/js/boot.js"></script>
<script src="/base/static/src/js/core.js"></script>
<script src="/base/static/src/js/dates.js"></script>
<script src="/base/static/src/js/formats.js"></script>
<script src="/base/static/src/js/chrome.js"></script>
<script src="/base/static/src/js/data.js"></script>
<script src="/base/static/src/js/views.js"></script>
+3 -3
View File
@@ -19,10 +19,10 @@
<script type="text/javascript" src="/base/static/src/js/base.js"></script>
<script type="text/javascript" src="/base/static/src/js/chrome.js"></script>
<script type="text/javascript" src="/base/static/src/js/data.js"></script>
<script type="text/javascript" src="/base/static/src/js/dates.js"></script>
<script type="text/javascript" src="/base/static/src/js/formats.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/web_mobile.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/chrome_mobile.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/web_mobile.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/chrome_mobile.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/list_mobile.js"></script>
<script type="text/javascript" src="/web_mobile/static/src/js/form_mobile.js"></script>