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
=== modified file 'addons/web_graph/__init__.py'
--- addons/web_graph/__init__.py 2012-10-23 15:26:46 +0000
+++ addons/web_graph/__init__.py 2014-02-05 13:14:23 +0000
@@ -0,0 +1,1 @@
1import controllers
02
=== added directory 'addons/web_graph/controllers'
=== added file 'addons/web_graph/controllers/__init__.py'
--- addons/web_graph/controllers/__init__.py 1970-01-01 00:00:00 +0000
+++ addons/web_graph/controllers/__init__.py 2014-02-05 13:14:23 +0000
@@ -0,0 +1,1 @@
1import main
0\ No newline at end of file2\ No newline at end of file
13
=== added file 'addons/web_graph/controllers/main.py'
--- addons/web_graph/controllers/main.py 1970-01-01 00:00:00 +0000
+++ addons/web_graph/controllers/main.py 2014-02-05 13:14:23 +0000
@@ -0,0 +1,88 @@
1from openerp import http
2import simplejson
3from openerp.http import request, serialize_exception as _serialize_exception
4from cStringIO import StringIO
5from collections import deque
6
7try:
8 import xlwt
9except ImportError:
10 xlwt = None
11
12class TableExporter(http.Controller):
13
14 @http.route('/web_graph/check_xlwt', type='json', auth='none')
15 def check_xlwt(self):
16 return xlwt is not None
17
18
19 @http.route('/web_graph/export_xls', type='http', auth="user")
20 def export_xls(self, data, token):
21 jdata = simplejson.loads(data)
22 nbr_measures = jdata['nbr_measures']
23 workbook = xlwt.Workbook()
24 worksheet = workbook.add_sheet(jdata['title'])
25 header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
26 header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;")
27 bold = xlwt.easyxf("font: bold on;")
28
29 # Step 1: writing headers
30 headers = jdata['headers']
31
32 # x,y: current coordinates
33 # carry: queue containing cell information when a cell has a >= 2 height
34 # and the drawing code needs to add empty cells below
35 x, y, carry = 1, 0, deque()
36 for i, header_row in enumerate(headers):
37 worksheet.write(i,0, '', header_plain)
38 for header in header_row:
39 while (carry and carry[0]['x'] == x):
40 cell = carry.popleft()
41 for i in range(nbr_measures):
42 worksheet.write(y, x+i, '', header_plain)
43 if cell['height'] > 1:
44 carry.append({'x': x, 'height':cell['height'] - 1})
45 x = x + nbr_measures
46 style = header_plain if 'expanded' in header else header_bold
47 for i in range(header['width']):
48 worksheet.write(y, x + i, header['title'] if i == 0 else '', style)
49 if header['height'] > 1:
50 carry.append({'x': x, 'height':header['height'] - 1})
51 x = x + header['width'];
52 while (carry and carry[0]['x'] == x):
53 cell = carry.popleft()
54 for i in range(nbr_measures):
55 worksheet.write(y, x+i, '', header_plain)
56 if cell['height'] > 1:
57 carry.append({'x': x, 'height':cell['height'] - 1})
58 x = x + nbr_measures
59 x, y = 1, y + 1
60
61 # Step 2: measure row
62 if nbr_measures > 1:
63 worksheet.write(y,0, '', header_plain)
64 for measure in jdata['measure_row']:
65 style = header_bold if measure['is_bold'] else header_plain
66 worksheet.write(y, x, measure['text'], style);
67 x = x + 1
68 y = y + 1
69
70 # Step 3: writing data
71 x = 0
72 for row in jdata['rows']:
73 worksheet.write(y, x, row['indent'] * ' ' + row['title'], header_plain)
74 for cell in row['cells']:
75 x = x + 1
76 if cell.get('is_bold', False):
77 worksheet.write(y, x, cell['value'], bold)
78 else:
79 worksheet.write(y, x, cell['value'])
80 x, y = 0, y + 1
81
82 response = request.make_response(None,
83 headers=[('Content-Type', 'application/vnd.ms-excel'),
84 ('Content-Disposition', 'attachment; filename=table.xls;')],
85 cookies={'fileToken': token})
86 workbook.save(response.stream)
87
88 return response
089
=== modified file 'addons/web_graph/static/src/js/graph_widget.js'
--- addons/web_graph/static/src/js/graph_widget.js 2014-01-28 15:11:13 +0000
+++ addons/web_graph/static/src/js/graph_widget.js 2014-02-05 13:14:23 +0000
@@ -25,6 +25,7 @@
25 this.bar_ui = options.bar_ui || 'group';25 this.bar_ui = options.bar_ui || 'group';
26 this.graph_view = options.graph_view || null;26 this.graph_view = options.graph_view || null;
27 this.pivot_options = options;27 this.pivot_options = options;
28 this.title = options.title || 'Data';
28 },29 },
2930
30 start: function() {31 start: function() {
@@ -39,6 +40,10 @@
39 this.$('.graph_heatmap label').addClass('disabled');40 this.$('.graph_heatmap label').addClass('disabled');
40 }41 }
4142
43 openerp.session.rpc('/web_graph/check_xlwt').then(function (result) {
44 self.$('.graph_options_selection label').toggle(result);
45 });
46
42 return this.model.call('fields_get', []).then(function (f) {47 return this.model.call('fields_get', []).then(function (f) {
43 self.fields = f;48 self.fields = f;
44 self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')};49 self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')};
@@ -85,8 +90,8 @@
85 groupbys = _.flatten(_.map(filters, function (filter) {90 groupbys = _.flatten(_.map(filters, function (filter) {
86 var groupby = py.eval(filter.attrs.context).group_by;91 var groupby = py.eval(filter.attrs.context).group_by;
87 if (!(groupby instanceof Array)) { groupby = [groupby]; }92 if (!(groupby instanceof Array)) { groupby = [groupby]; }
88 return _.map(groupby, function(g) { 93 return _.map(groupby, function(g) {
89 return {field: g, filter: filter}; 94 return {field: g, filter: filter};
90 });95 });
91 }));96 }));
9297
@@ -264,6 +269,9 @@
264 case 'update_values':269 case 'update_values':
265 this.pivot.update_data().then(this.proxy('display_data'));270 this.pivot.update_data().then(this.proxy('display_data'));
266 break;271 break;
272 case 'export_data':
273 this.export_xls();
274 break;
267 }275 }
268 },276 },
269277
@@ -356,13 +364,117 @@
356 },364 },
357365
358 // ----------------------------------------------------------------------366 // ----------------------------------------------------------------------
367 // Convert Pivot data structure into table structure :
368 // compute rows, cols, colors, cell width, cell height, ...
369 // ----------------------------------------------------------------------
370 build_table: function() {
371 return {
372 headers: this.build_headers(),
373 measure_row: this.build_measure_row(),
374 rows: this.build_rows(),
375 nbr_measures: this.pivot.measures.length,
376 title: this.title,
377 };
378 },
379
380 build_headers: function () {
381 var pivot = this.pivot,
382 nbr_measures = pivot.measures.length,
383 height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
384 rows = [];
385
386 _.each(pivot.cols.headers, function (col) {
387 if (col.path.length === 0) { return;}
388 var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1),
389 cell_height = col.expanded ? 1 : height - col.path.length + 1,
390 cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded};
391 if (rows[col.path.length - 1]) {
392 rows[col.path.length - 1].push(cell);
393 } else {
394 rows[col.path.length - 1] = [cell];
395 }
396 });
397
398 if (pivot.get_cols_leaves().length > 1) {
399 rows[0].push({width: nbr_measures, height: height, title: _t('Total'), id: pivot.main_col().id });
400 }
401 if (pivot.cols.headers.length === 1) {
402 rows = [[{width: nbr_measures, height: 1, title: _t('Total'), id: pivot.main_col().id, expanded: false}]];
403 }
404 return rows;
405 },
406
407 build_measure_row: function () {
408 var nbr_leaves = this.pivot.get_cols_leaves().length,
409 nbr_cols = nbr_leaves + ((nbr_leaves > 1) ? 1 : 0),
410 result = [],
411 add_total = this.pivot.get_cols_leaves().length > 1,
412 i, m;
413 for (i = 0; i < nbr_cols; i++) {
414 for (m = 0; m < this.pivot.measures.length; m++) {
415 result.push({
416 text:this.pivot.measures[m].string,
417 is_bold: add_total && (i === nbr_cols - 1)
418 });
419 }
420 }
421 return result;
422 },
423
424 make_cell: function (row, col, value, index) {
425 var formatted_value = openerp.web.format_value(value, {type:this.pivot.measures[index].type}),
426 cell = {value:formatted_value};
427
428 if (this.heatmap_mode === 'none') { return cell; }
429 var total = (this.heatmap_mode === 'both') ? this.pivot.get_total()[index]
430 : (this.heatmap_mode === 'row') ? this.pivot.get_total(row)[index]
431 : this.pivot.get_total(col)[index];
432 var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
433 if (color < 255) {
434 cell.color = color;
435 }
436 return cell;
437 },
438
439 build_rows: function () {
440 var self = this,
441 pivot = this.pivot,
442 m, cell;
443
444 return _.map(pivot.rows.headers, function (row) {
445 var cells = [];
446 _.each(pivot.get_cols_leaves(), function (col) {
447 var values = pivot.get_values(row.id,col.id);
448 for (m = 0; m < pivot.measures.length; m++) {
449 cells.push(self.make_cell(row,col,values[m], m));
450 }
451 });
452 if (pivot.get_cols_leaves().length > 1) {
453 var totals = pivot.get_total(row);
454 for (m = 0; m < pivot.measures.length; m++) {
455 cell = self.make_cell(row, pivot.main_col(), totals[m], m);
456 cell.is_bold = 'true';
457 cells.push(cell);
458 }
459 }
460 return {
461 id: row.id,
462 indent: row.path.length,
463 title: row.title,
464 expanded: row.expanded,
465 cells: cells,
466 };
467 });
468 },
469
470 // ----------------------------------------------------------------------
359 // Main display method471 // Main display method
360 // ----------------------------------------------------------------------472 // ----------------------------------------------------------------------
361 display_data: function () {473 display_data: function () {
362 this.$('.graph_main_content svg').remove();474 this.$('.graph_main_content svg').remove();
363 this.$('.graph_main_content div').remove();475 this.$('.graph_main_content div').remove();
364 this.table.empty();476 this.table.empty();
365 this.table.toggleClass('heatmap', this.heatmap_mode !== 'none')477 this.table.toggleClass('heatmap', this.heatmap_mode !== 'none');
366 this.width = this.$el.width();478 this.width = this.$el.width();
367 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));479 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
368480
@@ -384,159 +496,75 @@
384 // Drawing the table496 // Drawing the table
385 // ----------------------------------------------------------------------497 // ----------------------------------------------------------------------
386 draw_table: function () {498 draw_table: function () {
387 this.draw_top_headers();499 var table = this.build_table();
388 _.each(this.pivot.rows.headers, this.proxy('draw_row'));500 this.draw_headers(table.headers);
389 },501 this.draw_measure_row(table.measure_row);
390502 this.draw_rows(table.rows);
391 make_border_cell: function (colspan, rowspan, headercell) {503 },
392 var tag = (headercell) ? $('<th>') : $('<td>');504
393 return tag.addClass('graph_border')505 make_header_cell: function (header) {
394 .attr('colspan', colspan || 1)506 var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>'))
395 .attr('rowspan', rowspan || 1);507 .addClass('graph_border')
396 },508 .attr('rowspan', header.height)
397509 .attr('colspan', header.width);
398 make_header_title: function (header) {510 var content = $('<span>').addClass('web_graph_click')
399 return $('<span> ')511 .attr('href','#')
400 .addClass('web_graph_click')512 .text(' ' + (header.title || _t('Undefined')))
401 .attr('href', '#')513 .attr('data-id', header.id);
402 .addClass((header.expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')514 if (_.has(header, 'expanded')) {
403 .text(' ' + (header.title || 'Undefined'));515 content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
404 },
405
406 draw_top_headers: function () {
407 var self = this,
408 thead = $('<thead>'),
409 pivot = this.pivot,
410 height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
411 header_cells = [[this.make_border_cell(1, height, true)]];
412
413 function set_dim (cols) {
414 _.each(cols.children, set_dim);
415 if (cols.children.length === 0) {
416 cols.height = height - cols.path.length + 1;
417 cols.width = 1;
418 } else {
419 cols.height = 1;
420 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
421 }
422 }
423
424 function make_col_header (col) {
425 var cell = self.make_border_cell(col.width*pivot.measures.length, col.height, true);
426 return cell.append(self.make_header_title(col).attr('data-id', col.id));
427 }
428
429 function make_cells (queue, level) {
430 var col = queue[0];
431 queue = _.rest(queue).concat(col.children);
432 if (col.path.length == level) {
433 _.last(header_cells).push(make_col_header(col));
434 } else {
435 level +=1;
436 header_cells.push([make_col_header(col)]);
437 }
438 if (queue.length !== 0) {
439 make_cells(queue, level);
440 }
441 }
442
443 set_dim(pivot.main_col()); // add width and height info to columns headers
444 if (pivot.main_col().children.length === 0) {
445 make_cells(pivot.cols.headers, 0);
446 } else {516 } else {
447 make_cells(pivot.main_col().children, 1);517 content.css('font-weight', 'bold');
448 if (pivot.get_cols_leaves().length > 1) {518 }
449 header_cells[0].push(self.make_border_cell(pivot.measures.length, height, true).text(_t('Total')).css('font-weight', 'bold'));519 if (_.has(header, 'indent')) {
450 }520 for (var i = 0; i < header.indent; i++) { cell.prepend($('<span>', {class:'web_graph_indent'})); }
451 }521 }
452522 return cell.append(content);
453 _.each(header_cells, function (cells) {523 },
454 thead.append($('<tr>').append(cells));524
455 });525 draw_headers: function (headers) {
456 526 var make_cell = this.make_header_cell,
457 if (pivot.measures.length >= 2) {527 empty_cell = $('<th>').attr('rowspan', headers.length),
458 thead.append(self.make_measure_row());528 thead = $('<thead>');
459 }529
460530 _.each(headers, function (row) {
461 self.table.append(thead);531 var html_row = $('<tr>');
462 },532 _.each(row, function (header) {
463533 html_row.append(make_cell(header));
464 make_measure_cells: function () {534 });
465 return _.map(this.pivot.measures, function (measure) {535 thead.append(html_row);
466 return $('<th>').addClass('measure_row').text(measure.string);536 });
467 });537 thead.children(':first').prepend(empty_cell);
468 },538 this.table.append(thead);
469539 },
470 make_measure_row: function() {540
471 var self = this,541 draw_measure_row: function (measure_row) {
472 cols = this.pivot.cols.headers,542 if (this.pivot.measures.length === 1) { return; }
473 measure_row = $('<tr>');543 var html_row = $('<tr>').append('<th>');
474544 _.each(measure_row, function (cell) {
475 measure_row.append($('<th>'));545 var measure_cell = $('<th>').addClass('measure_row').text(cell.text);
476546 if (cell.is_bold) {measure_cell.css('font-weight', 'bold');}
477 _.each(cols, function (col) {547 html_row.append(measure_cell);
478 if (!col.children.length) {548 });
479 measure_row.append(self.make_measure_cells());549 this.$('thead').append(html_row);
480 }550 },
481 });551
482552 draw_rows: function (rows) {
483 if (this.pivot.get_cols_leaves().length > 1) {553 var table = this.table,
484 measure_row.append(self.make_measure_cells());554 make_cell = this.make_header_cell;
485 }555
486 return measure_row;556 _.each(rows, function (row) {
487 },557 var html_row = $('<tr>').append(make_cell(row));
488558 _.each(row.cells, function (cell) {
489 draw_row: function (row) {559 var html_cell = $('<td>').text(cell.value);
490 var self = this,560 if (_.has(cell, 'color')) {
491 pivot = this.pivot,561 html_cell.css('background-color', $.Color(255, cell.color, cell.color));
492 measure_types = _.pluck(this.pivot.measures, 'type'),
493 html_row = $('<tr>'),
494 row_header = this.make_border_cell(1,1)
495 .append(this.make_header_title(row).attr('data-id', row.id))
496 .addClass('graph_border');
497
498 for (var i = 0; i < row.path.length; i++) {
499 row_header.prepend($('<span>', {class:'web_graph_indent'}));
500 }
501
502 html_row.append(row_header);
503
504 _.each(pivot.cols.headers, function (col) {
505 if (!col.children.length) {
506 var values = pivot.get_values(row.id, col.id);
507 for (var i = 0; i < values.length; i++) {
508 html_row.append(make_cell(values[i], measure_types[i], i, col));
509 }562 }
510 }563 if (cell.is_bold) { html_cell.css('font-weight', 'bold'); }
564 html_row.append(html_cell);
565 });
566 table.append(html_row);
511 });567 });
512
513 if (pivot.get_cols_leaves().length > 1) {
514 var total_vals = pivot.get_total(row);
515 for (var j = 0; j < total_vals.length; j++) {
516 var cell = make_cell(total_vals[j], measure_types[j], j, pivot.cols[0]).css('font-weight', 'bold');
517 html_row.append(cell);
518 }
519 }
520
521 this.table.append(html_row);
522
523 function make_cell (value, measure_type, index, col) {
524 var cell = $('<td>');
525 if (value === undefined) {
526 return cell;
527 }
528 cell.text(openerp.web.format_value(value, {type: measure_type}));
529 var total = (self.heatmap_mode === 'both') ? pivot.get_total()[index]
530 : (self.heatmap_mode === 'row') ? pivot.get_total(row)[index]
531 : (self.heatmap_mode === 'col') ? pivot.get_total(col)[index]
532 : undefined;
533
534 if (self.heatmap_mode !== 'none') {
535 var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
536 cell.css('background-color', $.Color(255, color, color));
537 }
538 return cell;
539 }
540 },568 },
541569
542 // ----------------------------------------------------------------------570 // ----------------------------------------------------------------------
@@ -693,6 +721,20 @@
693 });721 });
694 },722 },
695723
724 // ----------------------------------------------------------------------
725 // Controller stuff...
726 // ----------------------------------------------------------------------
727 export_xls: function() {
728 var c = openerp.webclient.crashmanager;
729 openerp.web.blockUI();
730 this.session.get_file({
731 url: '/web_graph/export_xls',
732 data: {data: JSON.stringify(this.build_table())},
733 complete: openerp.web.unblockUI,
734 error: c.rpc_error.bind(c)
735 });
736 },
737
696});738});
697739
698// Utility function: returns true if the beginning of array2 is array1 and740// Utility function: returns true if the beginning of array2 is array1 and
699741
=== modified file 'addons/web_graph/static/src/js/pivot_table.js'
--- addons/web_graph/static/src/js/pivot_table.js 2014-01-28 15:11:57 +0000
+++ addons/web_graph/static/src/js/pivot_table.js 2014-02-05 13:14:23 +0000
@@ -127,6 +127,10 @@
127 return this._get_headers_with_depth(this.rows.headers, depth);127 return this._get_headers_with_depth(this.rows.headers, depth);
128 },128 },
129129
130 get_ancestor_leaves: function (header) {
131 return _.where(this.get_ancestors_and_self(header), {expanded:false});
132 },
133
130 // return all non expanded rows134 // return all non expanded rows
131 get_rows_leaves: function () {135 get_rows_leaves: function () {
132 return _.where(this.rows.headers, {expanded:false});136 return _.where(this.rows.headers, {expanded:false});
133137
=== modified file 'addons/web_graph/static/src/xml/web_graph.xml'
--- addons/web_graph/static/src/xml/web_graph.xml 2014-01-17 14:43:03 +0000
+++ addons/web_graph/static/src/xml/web_graph.xml 2014-02-05 13:14:23 +0000
@@ -41,6 +41,9 @@
41 <label class="btn btn-default" data-choice="update_values" title="Reload Data">41 <label class="btn btn-default" data-choice="update_values" title="Reload Data">
42 <span class="fa fa-refresh"></span>42 <span class="fa fa-refresh"></span>
43 </label>43 </label>
44 <label class="btn btn-default" data-choice="export_data" title="Export Data" style="display:none">
45 <span class="fa fa-download"></span>
46 </label>
44 </div>47 </div>
45 <div class="btn-group">48 <div class="btn-group">
46 <label class="btn btn-default dropdown-toggle" data-toggle="dropdown">49 <label class="btn btn-default dropdown-toggle" data-toggle="dropdown">