[IMP] More dataset refactoring
[odoo/odoo.git] / addons / web_kanban / static / src / js / kanban.js
1 openerp.web_kanban = function (openerp) {
2
3 var _t = openerp.web._t,
4    _lt = openerp.web._lt;
5 var QWeb = openerp.web.qweb;
6 openerp.web.views.add('kanban', 'openerp.web_kanban.KanbanView');
7
8 openerp.web_kanban.KanbanView = openerp.web.View.extend({
9     template: "KanbanView",
10     display_name: _lt('Kanban'),
11     default_nr_columns: 3,
12     init: function (parent, dataset, view_id, options) {
13         this._super(parent);
14         this.set_default_options(options);
15         this.dataset = dataset;
16         this.view_id = view_id;
17         this.fields_view = {};
18         this.fields_keys = [];
19         this.group_by = null;
20         this.state = {
21             groups : {},
22             records : {}
23         };
24         this.groups = [];
25         this.form_dialog = new openerp.web.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
26         this.form_dialog.on_form_dialog_saved.add_last(this.do_reload);
27         this.aggregates = {};
28         this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
29         this.qweb = new QWeb2.Engine();
30         this.qweb.debug = openerp.connection.debug;
31         this.qweb.default_dict = _.clone(QWeb.default_dict);
32         this.has_been_loaded = $.Deferred();
33         this.search_domain = this.search_context = this.search_group_by = null;
34         this.currently_dragging = {};
35     },
36     start: function() {
37         this._super();
38         this.$element.find('button.oe_kanban_button_new').click(this.do_add_record);
39         this.$groups = this.$element.find('.oe_kanban_groups tr');
40         var context = new openerp.web.CompoundContext(this.dataset.get_context());
41         return this.rpc('/web/view/load', {
42                 'model': this.dataset.model,
43                 'view_id': this.view_id,
44                 'view_type': 'kanban',
45                 context: context
46             }, this.on_loaded);
47     },
48     on_loaded: function(data) {
49         this.fields_view = data;
50         this.fields_keys = _.keys(this.fields_view.fields);
51         this.add_qweb_template();
52         this.has_been_loaded.resolve();
53     },
54     add_qweb_template: function() {
55         for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
56             var child = this.fields_view.arch.children[i];
57             if (child.tag === "templates") {
58                 this.transform_qweb_template(child);
59                 this.qweb.add_template(openerp.web.json_node_to_xml(child));
60                 break;
61             } else if (child.tag === 'field') {
62                 this.extract_aggregates(child);
63             }
64         }
65     },
66     extract_aggregates: function(node) {
67         for (var j = 0, jj = this.group_operators.length; j < jj;  j++) {
68             if (node.attrs[this.group_operators[j]]) {
69                 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
70                 break;
71             }
72         }
73     },
74     transform_qweb_template: function(node) {
75         var qweb_prefix = QWeb.prefix;
76         switch (node.tag) {
77             case 'field':
78                 node.tag = qweb_prefix;
79                 node.attrs[qweb_prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
80                 this.extract_aggregates(node);
81                 break
82             case 'button':
83             case 'a':
84                 var type = node.attrs.type || '';
85                 if (_.indexOf('action,object,edit,delete,color'.split(','), type) !== -1) {
86                     _.each(node.attrs, function(v, k) {
87                         if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
88                             node.attrs['data-' + k] = v;
89                             delete(node.attrs[k]);
90                         }
91                     });
92                     if (node.attrs['data-states']) {
93                         var states = _.map(node.attrs['data-states'].split(','), function(state) {
94                             return "record.state.raw_value == '" + _.str.trim(state) + "'";
95                         });
96                         node.attrs[qweb_prefix + '-if'] = states.join(' or ');
97                     }
98                     if (node.attrs['data-kanban_states']) {
99                         var states = _.map(node.attrs['data-kanban_states'].split(','), function(state) {
100                             return "record.kanban_state.raw_value == '" + _.str.trim(state) + "'";
101                         });
102                         node.attrs[qweb_prefix + '-if'] = states.join(' or ');
103                     }
104                     if (node.attrs['data-string']) {
105                         node.attrs.title = node.attrs['data-string'];
106                     }
107                     if (node.attrs['data-icon']) {
108                         node.children = [{
109                             tag: 'img',
110                             attrs: {
111                                 src: openerp.connection.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
112                                 width: '16',
113                                 height: '16'
114                             }
115                         }];
116                     }
117                     if (node.tag == 'a') {
118                         node.attrs.href = '#';
119                     } else {
120                         node.attrs.type = 'button';
121                     }
122                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
123                 }
124                 break;
125         }
126         if (node.children) {
127             for (var i = 0, ii = node.children.length; i < ii; i++) {
128                 this.transform_qweb_template(node.children[i]);
129             }
130         }
131     },
132     do_add_record: function() {
133         this.dataset.index = null;
134         this.do_switch_view('form');
135     },
136     do_search: function(domain, context, group_by) {
137         var self = this;
138         this.search_domain = domain;
139         this.search_context = context;
140         this.search_group_by = group_by;
141         $.when(this.has_been_loaded).then(function() {
142             self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
143             self.datagroup = new openerp.web.DataGroup(self, self.dataset.model, domain, context, self.group_by ? [self.group_by] : []);
144             self.datagroup.list(self.fields_keys, self.do_process_groups, self.do_process_dataset);
145         });
146     },
147     do_process_groups: function(groups) {
148         this.do_clear_groups();
149         this.dataset.ids = [];
150         var self = this,
151             remaining = groups.length - 1,
152             groups_array = [];
153         _.each(groups, function (group, index) {
154             var group_name = group.value,
155                 group_value = group.value,
156                 group_aggregates = {};
157             if (group.value instanceof Array) {
158                 group_name = group.value[1];
159                 group_value = group.value[0];
160             }
161             _.each(self.aggregates, function(value, key) {
162                 group_aggregates[value] = group.aggregates[key];
163             });
164             var dataset = new openerp.web.DataSetSearch(self, self.dataset.model, group.context, group.domain);
165             dataset.read_slice(self.fields_keys, {'domain': group.domain, 'context': group.context}).then(function(records) {
166                 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
167                 groups_array[index] = new openerp.web_kanban.KanbanGroup(self, records, group_value, group_name, group_aggregates);
168                 if (!remaining--) {
169                     self.dataset.index = self.dataset.ids.length ? 0 : null;
170                     self.do_add_groups(groups_array);
171                 }
172             });
173         });
174     },
175     do_process_dataset: function(dataset) {
176         var self = this;
177         this.do_clear_groups();
178         this.dataset.read_slice(this.fields_keys).then(function(records) {
179             var groups = [];
180             while (records.length) {
181                 for (var i = 0; i < self.default_nr_columns; i++) {
182                     if (!groups[i]) {
183                         groups[i] = [];
184                     }
185                     groups[i].push(records.shift());
186                 }
187             }
188             for (var i = 0; i < groups.length; i++) {
189                 groups[i] = new openerp.web_kanban.KanbanGroup(self, _.compact(groups[i]));
190             }
191             self.do_add_groups(groups);
192         });
193     },
194     do_reload: function() {
195         this.do_search(this.search_domain, this.search_context, this.search_group_by);
196     },
197     do_clear_groups: function() {
198         _.each(this.groups, function(group) {
199             group.stop();
200         });
201         this.groups = [];
202         this.$element.find('.oe_kanban_groups_headers, .oe_kanban_groups_records').empty();
203     },
204     do_add_groups: function(groups) {
205         var self = this;
206         _.each(groups, function(group) {
207             self.groups[group.undefined_title ? 'unshift' : 'push'](group);
208         });
209         _.each(this.groups, function(group) {
210             group.appendTo(self.$element.find('.oe_kanban_groups_headers'));
211         });
212         this.on_groups_started();
213     },
214     on_groups_started: function() {
215         var self = this;
216         this.compute_groups_width();
217         if (this.group_by) {
218             this.$element.find('.oe_kanban_column').sortable({
219                 connectWith: '.oe_kanban_column',
220                 handle : '.oe_kanban_draghandle',
221                 start: function(event, ui) {
222                     self.currently_dragging.index = ui.item.index();
223                     self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
224                 },
225                 stop: function(event, ui) {
226                     var record = ui.item.data('widget'),
227                         old_index = self.currently_dragging.index,
228                         new_index = ui.item.index(),
229                         old_group = self.currently_dragging.group,
230                         new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
231                     if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
232                         self.on_record_moved(record, old_group, old_index, new_group, new_index);
233                     }
234                 },
235                 scroll: false
236             });
237         } else {
238             this.$element.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
239         }
240     },
241     on_record_moved : function(record, old_group, old_index, new_group, new_index) {
242         var self = this;
243         if (old_group === new_group) {
244             new_group.records.splice(old_index, 1);
245             new_group.records.splice(new_index, 0, record);
246             new_group.do_save_sequences();
247         } else {
248             old_group.records.splice(old_index, 1);
249             new_group.records.splice(new_index, 0, record);
250             record.group = new_group;
251             var data = {};
252             data[this.group_by] = new_group.value;
253             this.dataset.write(record.id, data, {}, function() {
254                 record.do_reload();
255                 new_group.do_save_sequences();
256             }).fail(function(error, evt) {
257                 evt.preventDefault();
258                 alert("An error has occured while moving the record to this group.");
259                 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
260             });
261         }
262     },
263     compute_groups_width: function() {
264         var unfolded = 0;
265         _.each(this.groups, function(group) {
266             unfolded += group.state.folded ? 0 : 1;
267             group.$element.css('width', '');
268         });
269         _.each(this.groups, function(group) {
270             if (!group.state.folded) {
271                 group.$element.css('width', Math.round(100/unfolded) + '%');
272             }
273         });
274     },
275
276     do_show: function() {
277         this.do_push_state({});
278         return this._super();
279     }
280 });
281
282 openerp.web_kanban.KanbanGroup = openerp.web.Widget.extend({
283     template: 'KanbanView.group_header',
284     init: function (parent, records, value, title, aggregates) {
285         var self = this;
286         this._super(parent);
287         this.view = parent;
288         this.value = value;
289         this.title = title;
290         if (title === false) {
291             this.title = _t('Undefined');
292             this.undefined_title = true;
293         }
294         this.aggregates = aggregates || {};
295         var key = this.view.group_by + '-' + value;
296         if (!this.view.state.groups[key]) {
297             this.view.state.groups[key] = {
298                 folded: false
299             }
300         }
301         this.state = this.view.state.groups[key];
302         this.$records = null;
303         this.records = _.map(records, function(record) {
304             return new openerp.web_kanban.KanbanRecord(self, record);
305         });
306     },
307     start: function() {
308         var self = this,
309             def = this._super();
310         this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
311         this.$records.appendTo(this.view.$element.find('.oe_kanban_groups_records'));
312         _.each(this.records, function(record) {
313             record.appendTo(self.$records);
314         });
315         this.$element.find(".oe_kanban_fold_icon").click(function() {
316             self.do_toggle_fold();
317             self.view.compute_groups_width();
318             return false;
319         });
320         if (this.state.folded) {
321             this.do_toggle_fold();
322         }
323         this.$element.data('widget', this);
324         this.$records.data('widget', this);
325         return def;
326     },
327     stop: function() {
328         this._super();
329         if (this.$records) {
330             this.$records.remove();
331         }
332     },
333     remove_record: function(id, remove_from_dataset) {
334         for (var i = 0, ii = this.records.length; i < ii; i++) {
335             if (this.records[i]['id'] === id) {
336                 this.records.splice(i, 1);
337             }
338         }
339     },
340     do_toggle_fold: function(compute_width) {
341         this.$element.toggleClass('oe_kanban_group_folded');
342         this.$records.find('.oe_kanban_record').toggle();
343         this.state.folded = this.$element.is('.oe_kanban_group_folded');
344     },
345     do_save_sequences: function() {
346         var self = this;
347         if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
348             _.each(this.records, function(record, index) {
349                 self.view.dataset.write(record.id, { sequence : index });
350             });
351         }
352     }
353 });
354
355 openerp.web_kanban.KanbanRecord = openerp.web.Widget.extend({
356     template: 'KanbanView.record',
357     init: function (parent, record) {
358         this._super(parent);
359         this.group = parent;
360         this.view = parent.view;
361         this.id = null;
362         this.set_record(record);
363         if (!this.view.state.records[this.id]) {
364             this.view.state.records[this.id] = {
365                 folded: false
366             };
367         }
368         this.state = this.view.state.records[this.id];
369     },
370     set_record: function(record) {
371         this.id = record.id;
372         this.record = this.transform_record(record);
373     },
374     start: function() {
375         this._super();
376         this.$element.data('widget', this);
377         this.bind_events();
378     },
379     transform_record: function(record) {
380         var self = this,
381             new_record = {};
382         _.each(record, function(value, name) {
383             var r = _.clone(self.view.fields_view.fields[name] || {});
384             if ((r.type === 'date' || r.type === 'datetime') && value) {
385                 r.raw_value = openerp.web.auto_str_to_date(value);
386             } else {
387                 r.raw_value = value;
388             }
389             r.value = openerp.web.format_value(value, r);
390             new_record[name] = r;
391         });
392         return new_record;
393     },
394     render: function() {
395         this.qweb_context = {
396             record: this.record,
397             widget: this
398         }
399         for (var p in this) {
400             if (_.str.startsWith(p, 'kanban_')) {
401                 this.qweb_context[p] = _.bind(this[p], this);
402             }
403         }
404         return this._super({
405             'content': this.view.qweb.render('kanban-box', this.qweb_context)
406         });
407     },
408     bind_events: function() {
409         var self = this,
410             $show_on_click = self.$element.find('.oe_kanban_box_show_onclick');
411         $show_on_click.toggle(this.state.folded);
412         this.$element.find('.oe_kanban_box_show_onclick_trigger').click(function() {
413             $show_on_click.toggle();
414             self.state.folded = !self.state.folded;
415         });
416
417         this.$element.find('[tooltip]').tipTip({
418             maxWidth: 500,
419             defaultPosition: 'top',
420             content: function() {
421                 var template = $(this).attr('tooltip');
422                 if (!self.view.qweb.has_template(template)) {
423                     return false;
424                 }
425                 return self.view.qweb.render(template, self.qweb_context);
426             }
427         });
428
429         this.$element.find('.oe_kanban_action').click(function() {
430             var $action = $(this),
431                 type = $action.data('type') || 'button',
432                 method = 'do_action_' + (type === 'action' ? 'object' : type);
433             if (_.str.startsWith(type, 'switch_')) {
434                 self.view.do_switch_view(type.substr(7));
435             } else if (typeof self[method] === 'function') {
436                 self[method]($action);
437             } else {
438                 self.do_warn("Kanban: no action for type : " + type);
439             }
440             return false;
441         });
442     },
443     do_action_delete: function($action) {
444         var self = this;
445         if (confirm(_t("Are you sure you want to delete this record ?"))) {
446             return $.when(this.view.dataset.unlink([this.id])).then(function() {
447                 self.group.remove_record(self.id)
448                 self.stop();
449             });
450         }
451     },
452     do_action_edit: function($action) {
453         var self = this;
454         if ($action.attr('target') === 'dialog') {
455             this.view.form_dialog.select_id(this.id).then(function() {
456                 self.view.form_dialog.open();
457             });
458         } else {
459             if (self.view.dataset.select_id(this.id)) {
460                 this.view.do_switch_view('form');
461             } else {
462                 this.do_warn("Kanban: could not find id#" + id);
463             }
464         }
465     },
466     do_action_color: function($action) {
467         var self = this,
468             colors = '#FFFFFF,#CCCCCC,#FFC7C7,#FFF1C7,#E3FFC7,#C7FFD5,#C7FFFF,#C7D5FF,#E3C7FF,#FFC7F1'.split(','),
469             $cpicker = $(QWeb.render('KanbanColorPicker', { colors : colors, columns: 2 }));
470         $action.after($cpicker);
471         $cpicker.mouseenter(function() {
472             clearTimeout($cpicker.data('timeoutId'));
473         }).mouseleave(function(evt) {
474             var timeoutId = setTimeout(function() { $cpicker.remove() }, 500);
475             $cpicker.data('timeoutId', timeoutId);
476         });
477         $cpicker.find('a').click(function() {
478             var data = {};
479             data[$action.data('name')] = $(this).data('color');
480             self.view.dataset.write(self.id, data, {}, function() {
481                 self.record[$action.data('name')] = $(this).data('color');
482                 self.do_reload();
483             });
484             $cpicker.remove();
485             return false;
486         });
487     },
488     do_action_object: function ($action) {
489         var button_attrs = $action.data();
490         this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
491     },
492     do_reload: function() {
493         var self = this;
494         this.view.dataset.read_ids([this.id], this.view.fields_keys).then(function(records) {
495             if (records.length) {
496                 self.set_record(records[0]);
497                 self.do_render();
498             } else {
499                 self.stop();
500             }
501         });
502     },
503     do_render: function() {
504         this.$element.html(this.render());
505         this.bind_events();
506     },
507     kanban_color: function(variable) {
508         var number_of_color_schemes = 10,
509             index = 0;
510         switch (typeof(variable)) {
511             case 'string':
512                 for (var i=0, ii=variable.length; i<ii; i++) {
513                     index += variable.charCodeAt(i);
514                 }
515                 break;
516             case 'number':
517                 index = Math.round(variable);
518                 break;
519             default:
520                 return '';
521         }
522         var color = (index % number_of_color_schemes);
523         return 'oe_kanban_color_' + color;
524     },
525     kanban_gravatar: function(email, size) {
526         size = size || 22;
527         email = _.str.trim(email || '').toLowerCase();
528         var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
529         var email_md5 = $.md5(email);
530         return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
531     },
532     kanban_image: function(model, field, id) {
533         id = id || '';
534         return openerp.connection.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + id;
535     },
536     kanban_text_ellipsis: function(s, size) {
537         size = size || 160;
538         if (!s) {
539             return '';
540         } else if (s.length <= size) {
541             return s;
542         } else {
543             return s.substr(0, size) + '...';
544         }
545     }
546 });
547 };
548
549 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: