Merge lp:~openerp-dev/openerp-web/trunk-new-graphview-ged into lp:openerp-web

Proposed by Géry Debongnie
Status: Merged
Merged at revision: 3929
Proposed branch: lp:~openerp-dev/openerp-web/trunk-new-graphview-ged
Merge into: lp:openerp-web
Diff against target: 545 lines (+292/-153)
6 files modified
addons/web_graph/__init__.py (+1/-0)
addons/web_graph/controllers/__init__.py (+1/-0)
addons/web_graph/controllers/main.py (+88/-0)
addons/web_graph/static/src/js/graph_widget.js (+195/-153)
addons/web_graph/static/src/js/pivot_table.js (+4/-0)
addons/web_graph/static/src/xml/web_graph.xml (+3/-0)
To merge this branch: bzr merge lp:~openerp-dev/openerp-web/trunk-new-graphview-ged
Reviewer Review Type Date Requested Status
Xavier (Open ERP) (community) Needs Fixing
Review via email: mp+204888@code.launchpad.net

Description of the change

Export functionality... Only code that changed is in addon web_graph (web)

To post a comment you must log in.
Revision history for this message
Xavier (Open ERP) (xmo-deactivatedaccount) wrote :

* `if (result) { self.$('.graph_options_selection label').toggle(true); }` could be replaced by `self.$('.graph_options_selection label').toggle(result);` I think

* The Response object returned by make_response has a write-only `stream` attribute, so maybe (depending how complex workbook.save is) the StringIO dance could be replaced by:

    response = self.make_response(params)
    workbook.save(response.stream)
    return response

* maybe give a better name to the sheet e.g. the model name?

* `L` could probably benefit from comments to explain what it and its purpose are, maybe even a renaming. It seems to be used as a queue, but I'm not sure a queue of what. Also since it's a FIFO, should probably use collection.deque (.pop(0) is O(n) on a list, it's ~O(1) on deque, also it's called .popleft() which is clearer)

* in many worksheet.write() calls, there's a space missing after the first argument's comma, spacing also somewhat inconsistent in JS e.g. {width:nbr_measures, height:height, title: _t('Total'), id: pivot.cols.headers[0].id } some colons are followed by a space, others not

* maybe bold_style and non_bold_style should be renamed to header (or header_plain) and header_bold? Something like that? They seem to be header styles of sort (I may be wrong)

review: Needs Fixing
4119. By Géry Debongnie

[FIX] many small tweaks, add some comment, rename variables to have better more informative names in excel export functionality in graph view (addon web_graph)

Revision history for this message
Géry Debongnie (gery-debongnie) wrote :

The fixing has been done...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'addons/web_graph/__init__.py'
2--- addons/web_graph/__init__.py 2012-10-23 15:26:46 +0000
3+++ addons/web_graph/__init__.py 2014-02-05 13:14:23 +0000
4@@ -0,0 +1,1 @@
5+import controllers
6
7=== added directory 'addons/web_graph/controllers'
8=== added file 'addons/web_graph/controllers/__init__.py'
9--- addons/web_graph/controllers/__init__.py 1970-01-01 00:00:00 +0000
10+++ addons/web_graph/controllers/__init__.py 2014-02-05 13:14:23 +0000
11@@ -0,0 +1,1 @@
12+import main
13\ No newline at end of file
14
15=== added file 'addons/web_graph/controllers/main.py'
16--- addons/web_graph/controllers/main.py 1970-01-01 00:00:00 +0000
17+++ addons/web_graph/controllers/main.py 2014-02-05 13:14:23 +0000
18@@ -0,0 +1,88 @@
19+from openerp import http
20+import simplejson
21+from openerp.http import request, serialize_exception as _serialize_exception
22+from cStringIO import StringIO
23+from collections import deque
24+
25+try:
26+ import xlwt
27+except ImportError:
28+ xlwt = None
29+
30+class TableExporter(http.Controller):
31+
32+ @http.route('/web_graph/check_xlwt', type='json', auth='none')
33+ def check_xlwt(self):
34+ return xlwt is not None
35+
36+
37+ @http.route('/web_graph/export_xls', type='http', auth="user")
38+ def export_xls(self, data, token):
39+ jdata = simplejson.loads(data)
40+ nbr_measures = jdata['nbr_measures']
41+ workbook = xlwt.Workbook()
42+ worksheet = workbook.add_sheet(jdata['title'])
43+ header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
44+ header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;")
45+ bold = xlwt.easyxf("font: bold on;")
46+
47+ # Step 1: writing headers
48+ headers = jdata['headers']
49+
50+ # x,y: current coordinates
51+ # carry: queue containing cell information when a cell has a >= 2 height
52+ # and the drawing code needs to add empty cells below
53+ x, y, carry = 1, 0, deque()
54+ for i, header_row in enumerate(headers):
55+ worksheet.write(i,0, '', header_plain)
56+ for header in header_row:
57+ while (carry and carry[0]['x'] == x):
58+ cell = carry.popleft()
59+ for i in range(nbr_measures):
60+ worksheet.write(y, x+i, '', header_plain)
61+ if cell['height'] > 1:
62+ carry.append({'x': x, 'height':cell['height'] - 1})
63+ x = x + nbr_measures
64+ style = header_plain if 'expanded' in header else header_bold
65+ for i in range(header['width']):
66+ worksheet.write(y, x + i, header['title'] if i == 0 else '', style)
67+ if header['height'] > 1:
68+ carry.append({'x': x, 'height':header['height'] - 1})
69+ x = x + header['width'];
70+ while (carry and carry[0]['x'] == x):
71+ cell = carry.popleft()
72+ for i in range(nbr_measures):
73+ worksheet.write(y, x+i, '', header_plain)
74+ if cell['height'] > 1:
75+ carry.append({'x': x, 'height':cell['height'] - 1})
76+ x = x + nbr_measures
77+ x, y = 1, y + 1
78+
79+ # Step 2: measure row
80+ if nbr_measures > 1:
81+ worksheet.write(y,0, '', header_plain)
82+ for measure in jdata['measure_row']:
83+ style = header_bold if measure['is_bold'] else header_plain
84+ worksheet.write(y, x, measure['text'], style);
85+ x = x + 1
86+ y = y + 1
87+
88+ # Step 3: writing data
89+ x = 0
90+ for row in jdata['rows']:
91+ worksheet.write(y, x, row['indent'] * ' ' + row['title'], header_plain)
92+ for cell in row['cells']:
93+ x = x + 1
94+ if cell.get('is_bold', False):
95+ worksheet.write(y, x, cell['value'], bold)
96+ else:
97+ worksheet.write(y, x, cell['value'])
98+ x, y = 0, y + 1
99+
100+ response = request.make_response(None,
101+ headers=[('Content-Type', 'application/vnd.ms-excel'),
102+ ('Content-Disposition', 'attachment; filename=table.xls;')],
103+ cookies={'fileToken': token})
104+ workbook.save(response.stream)
105+
106+ return response
107
108=== modified file 'addons/web_graph/static/src/js/graph_widget.js'
109--- addons/web_graph/static/src/js/graph_widget.js 2014-01-28 15:11:13 +0000
110+++ addons/web_graph/static/src/js/graph_widget.js 2014-02-05 13:14:23 +0000
111@@ -25,6 +25,7 @@
112 this.bar_ui = options.bar_ui || 'group';
113 this.graph_view = options.graph_view || null;
114 this.pivot_options = options;
115+ this.title = options.title || 'Data';
116 },
117
118 start: function() {
119@@ -39,6 +40,10 @@
120 this.$('.graph_heatmap label').addClass('disabled');
121 }
122
123+ openerp.session.rpc('/web_graph/check_xlwt').then(function (result) {
124+ self.$('.graph_options_selection label').toggle(result);
125+ });
126+
127 return this.model.call('fields_get', []).then(function (f) {
128 self.fields = f;
129 self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')};
130@@ -85,8 +90,8 @@
131 groupbys = _.flatten(_.map(filters, function (filter) {
132 var groupby = py.eval(filter.attrs.context).group_by;
133 if (!(groupby instanceof Array)) { groupby = [groupby]; }
134- return _.map(groupby, function(g) {
135- return {field: g, filter: filter};
136+ return _.map(groupby, function(g) {
137+ return {field: g, filter: filter};
138 });
139 }));
140
141@@ -264,6 +269,9 @@
142 case 'update_values':
143 this.pivot.update_data().then(this.proxy('display_data'));
144 break;
145+ case 'export_data':
146+ this.export_xls();
147+ break;
148 }
149 },
150
151@@ -356,13 +364,117 @@
152 },
153
154 // ----------------------------------------------------------------------
155+ // Convert Pivot data structure into table structure :
156+ // compute rows, cols, colors, cell width, cell height, ...
157+ // ----------------------------------------------------------------------
158+ build_table: function() {
159+ return {
160+ headers: this.build_headers(),
161+ measure_row: this.build_measure_row(),
162+ rows: this.build_rows(),
163+ nbr_measures: this.pivot.measures.length,
164+ title: this.title,
165+ };
166+ },
167+
168+ build_headers: function () {
169+ var pivot = this.pivot,
170+ nbr_measures = pivot.measures.length,
171+ height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
172+ rows = [];
173+
174+ _.each(pivot.cols.headers, function (col) {
175+ if (col.path.length === 0) { return;}
176+ var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1),
177+ cell_height = col.expanded ? 1 : height - col.path.length + 1,
178+ cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded};
179+ if (rows[col.path.length - 1]) {
180+ rows[col.path.length - 1].push(cell);
181+ } else {
182+ rows[col.path.length - 1] = [cell];
183+ }
184+ });
185+
186+ if (pivot.get_cols_leaves().length > 1) {
187+ rows[0].push({width: nbr_measures, height: height, title: _t('Total'), id: pivot.main_col().id });
188+ }
189+ if (pivot.cols.headers.length === 1) {
190+ rows = [[{width: nbr_measures, height: 1, title: _t('Total'), id: pivot.main_col().id, expanded: false}]];
191+ }
192+ return rows;
193+ },
194+
195+ build_measure_row: function () {
196+ var nbr_leaves = this.pivot.get_cols_leaves().length,
197+ nbr_cols = nbr_leaves + ((nbr_leaves > 1) ? 1 : 0),
198+ result = [],
199+ add_total = this.pivot.get_cols_leaves().length > 1,
200+ i, m;
201+ for (i = 0; i < nbr_cols; i++) {
202+ for (m = 0; m < this.pivot.measures.length; m++) {
203+ result.push({
204+ text:this.pivot.measures[m].string,
205+ is_bold: add_total && (i === nbr_cols - 1)
206+ });
207+ }
208+ }
209+ return result;
210+ },
211+
212+ make_cell: function (row, col, value, index) {
213+ var formatted_value = openerp.web.format_value(value, {type:this.pivot.measures[index].type}),
214+ cell = {value:formatted_value};
215+
216+ if (this.heatmap_mode === 'none') { return cell; }
217+ var total = (this.heatmap_mode === 'both') ? this.pivot.get_total()[index]
218+ : (this.heatmap_mode === 'row') ? this.pivot.get_total(row)[index]
219+ : this.pivot.get_total(col)[index];
220+ var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
221+ if (color < 255) {
222+ cell.color = color;
223+ }
224+ return cell;
225+ },
226+
227+ build_rows: function () {
228+ var self = this,
229+ pivot = this.pivot,
230+ m, cell;
231+
232+ return _.map(pivot.rows.headers, function (row) {
233+ var cells = [];
234+ _.each(pivot.get_cols_leaves(), function (col) {
235+ var values = pivot.get_values(row.id,col.id);
236+ for (m = 0; m < pivot.measures.length; m++) {
237+ cells.push(self.make_cell(row,col,values[m], m));
238+ }
239+ });
240+ if (pivot.get_cols_leaves().length > 1) {
241+ var totals = pivot.get_total(row);
242+ for (m = 0; m < pivot.measures.length; m++) {
243+ cell = self.make_cell(row, pivot.main_col(), totals[m], m);
244+ cell.is_bold = 'true';
245+ cells.push(cell);
246+ }
247+ }
248+ return {
249+ id: row.id,
250+ indent: row.path.length,
251+ title: row.title,
252+ expanded: row.expanded,
253+ cells: cells,
254+ };
255+ });
256+ },
257+
258+ // ----------------------------------------------------------------------
259 // Main display method
260 // ----------------------------------------------------------------------
261 display_data: function () {
262 this.$('.graph_main_content svg').remove();
263 this.$('.graph_main_content div').remove();
264 this.table.empty();
265- this.table.toggleClass('heatmap', this.heatmap_mode !== 'none')
266+ this.table.toggleClass('heatmap', this.heatmap_mode !== 'none');
267 this.width = this.$el.width();
268 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
269
270@@ -384,159 +496,75 @@
271 // Drawing the table
272 // ----------------------------------------------------------------------
273 draw_table: function () {
274- this.draw_top_headers();
275- _.each(this.pivot.rows.headers, this.proxy('draw_row'));
276- },
277-
278- make_border_cell: function (colspan, rowspan, headercell) {
279- var tag = (headercell) ? $('<th>') : $('<td>');
280- return tag.addClass('graph_border')
281- .attr('colspan', colspan || 1)
282- .attr('rowspan', rowspan || 1);
283- },
284-
285- make_header_title: function (header) {
286- return $('<span> ')
287- .addClass('web_graph_click')
288- .attr('href', '#')
289- .addClass((header.expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
290- .text(' ' + (header.title || 'Undefined'));
291- },
292-
293- draw_top_headers: function () {
294- var self = this,
295- thead = $('<thead>'),
296- pivot = this.pivot,
297- height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
298- header_cells = [[this.make_border_cell(1, height, true)]];
299-
300- function set_dim (cols) {
301- _.each(cols.children, set_dim);
302- if (cols.children.length === 0) {
303- cols.height = height - cols.path.length + 1;
304- cols.width = 1;
305- } else {
306- cols.height = 1;
307- cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
308- }
309- }
310-
311- function make_col_header (col) {
312- var cell = self.make_border_cell(col.width*pivot.measures.length, col.height, true);
313- return cell.append(self.make_header_title(col).attr('data-id', col.id));
314- }
315-
316- function make_cells (queue, level) {
317- var col = queue[0];
318- queue = _.rest(queue).concat(col.children);
319- if (col.path.length == level) {
320- _.last(header_cells).push(make_col_header(col));
321- } else {
322- level +=1;
323- header_cells.push([make_col_header(col)]);
324- }
325- if (queue.length !== 0) {
326- make_cells(queue, level);
327- }
328- }
329-
330- set_dim(pivot.main_col()); // add width and height info to columns headers
331- if (pivot.main_col().children.length === 0) {
332- make_cells(pivot.cols.headers, 0);
333+ var table = this.build_table();
334+ this.draw_headers(table.headers);
335+ this.draw_measure_row(table.measure_row);
336+ this.draw_rows(table.rows);
337+ },
338+
339+ make_header_cell: function (header) {
340+ var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>'))
341+ .addClass('graph_border')
342+ .attr('rowspan', header.height)
343+ .attr('colspan', header.width);
344+ var content = $('<span>').addClass('web_graph_click')
345+ .attr('href','#')
346+ .text(' ' + (header.title || _t('Undefined')))
347+ .attr('data-id', header.id);
348+ if (_.has(header, 'expanded')) {
349+ content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
350 } else {
351- make_cells(pivot.main_col().children, 1);
352- if (pivot.get_cols_leaves().length > 1) {
353- header_cells[0].push(self.make_border_cell(pivot.measures.length, height, true).text(_t('Total')).css('font-weight', 'bold'));
354- }
355- }
356-
357- _.each(header_cells, function (cells) {
358- thead.append($('<tr>').append(cells));
359- });
360-
361- if (pivot.measures.length >= 2) {
362- thead.append(self.make_measure_row());
363- }
364-
365- self.table.append(thead);
366- },
367-
368- make_measure_cells: function () {
369- return _.map(this.pivot.measures, function (measure) {
370- return $('<th>').addClass('measure_row').text(measure.string);
371- });
372- },
373-
374- make_measure_row: function() {
375- var self = this,
376- cols = this.pivot.cols.headers,
377- measure_row = $('<tr>');
378-
379- measure_row.append($('<th>'));
380-
381- _.each(cols, function (col) {
382- if (!col.children.length) {
383- measure_row.append(self.make_measure_cells());
384- }
385- });
386-
387- if (this.pivot.get_cols_leaves().length > 1) {
388- measure_row.append(self.make_measure_cells());
389- }
390- return measure_row;
391- },
392-
393- draw_row: function (row) {
394- var self = this,
395- pivot = this.pivot,
396- measure_types = _.pluck(this.pivot.measures, 'type'),
397- html_row = $('<tr>'),
398- row_header = this.make_border_cell(1,1)
399- .append(this.make_header_title(row).attr('data-id', row.id))
400- .addClass('graph_border');
401-
402- for (var i = 0; i < row.path.length; i++) {
403- row_header.prepend($('<span>', {class:'web_graph_indent'}));
404- }
405-
406- html_row.append(row_header);
407-
408- _.each(pivot.cols.headers, function (col) {
409- if (!col.children.length) {
410- var values = pivot.get_values(row.id, col.id);
411- for (var i = 0; i < values.length; i++) {
412- html_row.append(make_cell(values[i], measure_types[i], i, col));
413+ content.css('font-weight', 'bold');
414+ }
415+ if (_.has(header, 'indent')) {
416+ for (var i = 0; i < header.indent; i++) { cell.prepend($('<span>', {class:'web_graph_indent'})); }
417+ }
418+ return cell.append(content);
419+ },
420+
421+ draw_headers: function (headers) {
422+ var make_cell = this.make_header_cell,
423+ empty_cell = $('<th>').attr('rowspan', headers.length),
424+ thead = $('<thead>');
425+
426+ _.each(headers, function (row) {
427+ var html_row = $('<tr>');
428+ _.each(row, function (header) {
429+ html_row.append(make_cell(header));
430+ });
431+ thead.append(html_row);
432+ });
433+ thead.children(':first').prepend(empty_cell);
434+ this.table.append(thead);
435+ },
436+
437+ draw_measure_row: function (measure_row) {
438+ if (this.pivot.measures.length === 1) { return; }
439+ var html_row = $('<tr>').append('<th>');
440+ _.each(measure_row, function (cell) {
441+ var measure_cell = $('<th>').addClass('measure_row').text(cell.text);
442+ if (cell.is_bold) {measure_cell.css('font-weight', 'bold');}
443+ html_row.append(measure_cell);
444+ });
445+ this.$('thead').append(html_row);
446+ },
447+
448+ draw_rows: function (rows) {
449+ var table = this.table,
450+ make_cell = this.make_header_cell;
451+
452+ _.each(rows, function (row) {
453+ var html_row = $('<tr>').append(make_cell(row));
454+ _.each(row.cells, function (cell) {
455+ var html_cell = $('<td>').text(cell.value);
456+ if (_.has(cell, 'color')) {
457+ html_cell.css('background-color', $.Color(255, cell.color, cell.color));
458 }
459- }
460+ if (cell.is_bold) { html_cell.css('font-weight', 'bold'); }
461+ html_row.append(html_cell);
462+ });
463+ table.append(html_row);
464 });
465-
466- if (pivot.get_cols_leaves().length > 1) {
467- var total_vals = pivot.get_total(row);
468- for (var j = 0; j < total_vals.length; j++) {
469- var cell = make_cell(total_vals[j], measure_types[j], j, pivot.cols[0]).css('font-weight', 'bold');
470- html_row.append(cell);
471- }
472- }
473-
474- this.table.append(html_row);
475-
476- function make_cell (value, measure_type, index, col) {
477- var cell = $('<td>');
478- if (value === undefined) {
479- return cell;
480- }
481- cell.text(openerp.web.format_value(value, {type: measure_type}));
482- var total = (self.heatmap_mode === 'both') ? pivot.get_total()[index]
483- : (self.heatmap_mode === 'row') ? pivot.get_total(row)[index]
484- : (self.heatmap_mode === 'col') ? pivot.get_total(col)[index]
485- : undefined;
486-
487- if (self.heatmap_mode !== 'none') {
488- var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
489- cell.css('background-color', $.Color(255, color, color));
490- }
491- return cell;
492- }
493 },
494
495 // ----------------------------------------------------------------------
496@@ -693,6 +721,20 @@
497 });
498 },
499
500+ // ----------------------------------------------------------------------
501+ // Controller stuff...
502+ // ----------------------------------------------------------------------
503+ export_xls: function() {
504+ var c = openerp.webclient.crashmanager;
505+ openerp.web.blockUI();
506+ this.session.get_file({
507+ url: '/web_graph/export_xls',
508+ data: {data: JSON.stringify(this.build_table())},
509+ complete: openerp.web.unblockUI,
510+ error: c.rpc_error.bind(c)
511+ });
512+ },
513+
514 });
515
516 // Utility function: returns true if the beginning of array2 is array1 and
517
518=== modified file 'addons/web_graph/static/src/js/pivot_table.js'
519--- addons/web_graph/static/src/js/pivot_table.js 2014-01-28 15:11:57 +0000
520+++ addons/web_graph/static/src/js/pivot_table.js 2014-02-05 13:14:23 +0000
521@@ -127,6 +127,10 @@
522 return this._get_headers_with_depth(this.rows.headers, depth);
523 },
524
525+ get_ancestor_leaves: function (header) {
526+ return _.where(this.get_ancestors_and_self(header), {expanded:false});
527+ },
528+
529 // return all non expanded rows
530 get_rows_leaves: function () {
531 return _.where(this.rows.headers, {expanded:false});
532
533=== modified file 'addons/web_graph/static/src/xml/web_graph.xml'
534--- addons/web_graph/static/src/xml/web_graph.xml 2014-01-17 14:43:03 +0000
535+++ addons/web_graph/static/src/xml/web_graph.xml 2014-02-05 13:14:23 +0000
536@@ -41,6 +41,9 @@
537 <label class="btn btn-default" data-choice="update_values" title="Reload Data">
538 <span class="fa fa-refresh"></span>
539 </label>
540+ <label class="btn btn-default" data-choice="export_data" title="Export Data" style="display:none">
541+ <span class="fa fa-download"></span>
542+ </label>
543 </div>
544 <div class="btn-group">
545 <label class="btn btn-default dropdown-toggle" data-toggle="dropdown">