[fix] added saving of group_bys in saved filters
[odoo/odoo.git] / addons / web / static / src / js / search.js
1 openerp.web.search = function(openerp) {
2 var QWeb = openerp.web.qweb,
3       _t =  openerp.web._t,
4      _lt = openerp.web._lt;
5
6 openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
7     template: "EmptyComponent",
8     /**
9      * @constructs openerp.web.SearchView
10      * @extends openerp.web.Widget
11      *
12      * @param parent
13      * @param element_id
14      * @param dataset
15      * @param view_id
16      * @param defaults
17      */
18     init: function(parent, dataset, view_id, defaults, hidden) {
19         this._super(parent);
20         this.dataset = dataset;
21         this.model = dataset.model;
22         this.view_id = view_id;
23
24         this.defaults = defaults || {};
25         this.has_defaults = !_.isEmpty(this.defaults);
26
27         this.inputs = [];
28         this.enabled_filters = [];
29
30         this.has_focus = false;
31
32         this.hidden = !!hidden;
33         this.headless = this.hidden && !this.has_defaults;
34
35         this.ready = $.Deferred();
36     },
37     start: function() {
38         this._super();
39         if (this.hidden) {
40             this.$element.hide();
41         }
42         if (this.headless) {
43             this.ready.resolve();
44         } else {
45             this.rpc("/web/searchview/load", {
46                 model: this.model,
47                 view_id: this.view_id,
48                 context: this.dataset.get_context()
49             }, this.on_loaded);
50         }
51         return this.ready.promise();
52     },
53     show: function () {
54         this.$element.show();
55     },
56     hide: function () {
57         this.$element.hide();
58     },
59     /**
60      * Builds a list of widget rows (each row is an array of widgets)
61      *
62      * @param {Array} items a list of nodes to convert to widgets
63      * @param {Object} fields a mapping of field names to (ORM) field attributes
64      * @returns Array
65      */
66     make_widgets: function (items, fields) {
67         var rows = [],
68             row = [];
69         rows.push(row);
70         var filters = [];
71         _.each(items, function (item) {
72             if (filters.length && item.tag !== 'filter') {
73                 row.push(
74                     new openerp.web.search.FilterGroup(
75                         filters, this));
76                 filters = [];
77             }
78
79             if (item.tag === 'newline') {
80                 row = [];
81                 rows.push(row);
82             } else if (item.tag === 'filter') {
83                 if (!this.has_focus) {
84                     item.attrs.default_focus = '1';
85                     this.has_focus = true;
86                 }
87                 filters.push(
88                     new openerp.web.search.Filter(
89                         item, this));
90             } else if (item.tag === 'separator') {
91                 // a separator is a no-op
92             } else {
93                 if (item.tag === 'group') {
94                     // TODO: group and field should be fetched from registries, maybe even filters
95                     row.push(
96                         new openerp.web.search.Group(
97                             item, this, fields));
98                 } else if (item.tag === 'field') {
99                     if (!this.has_focus) {
100                         item.attrs.default_focus = '1';
101                         this.has_focus = true;
102                     }
103                     row.push(
104                         this.make_field(
105                             item, fields[item['attrs'].name]));
106                 }
107             }
108         }, this);
109         if (filters.length) {
110             row.push(new openerp.web.search.FilterGroup(filters, this));
111         }
112
113         return rows;
114     },
115     /**
116      * Creates a field for the provided field descriptor item (which comes
117      * from fields_view_get)
118      *
119      * @param {Object} item fields_view_get node for the field
120      * @param {Object} field fields_get result for the field
121      * @returns openerp.web.search.Field
122      */
123     make_field: function (item, field) {
124         try {
125             return new (openerp.web.search.fields.get_any(
126                     [item.attrs.widget, field.type]))
127                 (item, field, this);
128         } catch (e) {
129             if (! e instanceof openerp.web.KeyNotFound) {
130                 throw e;
131             }
132             // KeyNotFound means unknown field type
133             console.group('Unknown field type ' + field.type);
134             console.error('View node', item);
135             console.info('View field', field);
136             console.info('In view', this);
137             console.groupEnd();
138             return null;
139         }
140     },
141     on_loaded: function(data) {
142         this.fields_view = data.fields_view;
143         if (data.fields_view.type !== 'search' ||
144             data.fields_view.arch.tag !== 'search') {
145                 throw new Error(_.str.sprintf(
146                     "Got non-search view after asking for a search view: type %s, arch root %s",
147                     data.fields_view.type, data.fields_view.arch.tag));
148         }
149         var self = this,
150            lines = this.make_widgets(
151                 data.fields_view['arch'].children,
152                 data.fields_view.fields);
153
154         // for extended search view
155         var ext = new openerp.web.search.ExtendedSearch(this, this.model);
156         lines.push([ext]);
157         this.inputs.push(ext);
158         this.extended_search = ext;
159
160         var render = QWeb.render("SearchView", {
161             'view': data.fields_view['arch'],
162             'lines': lines,
163             'defaults': this.defaults
164         });
165
166         this.$element.html(render);
167
168         var f = this.$element.find('form');
169         this.$element.find('form')
170                 .submit(this.do_search)
171                 .bind('reset', this.do_clear);
172         // start() all the widgets
173         var widget_starts = _(lines).chain().flatten().map(function (widget) {
174             return widget.start();
175         }).value();
176
177         $.when.apply(null, widget_starts).then(function () {
178             self.ready.resolve();
179         });
180
181         this.reload_managed_filters();
182     },
183     reload_managed_filters: function() {
184         var self = this;
185         return this.rpc('/web/searchview/get_filters', {
186             model: this.dataset.model
187         }).then(function(result) {
188             self.managed_filters = result;
189             var filters = self.$element.find(".oe_search-view-filters-management");
190             filters.html(QWeb.render("SearchView.managed-filters", {filters: result}));
191             filters.change(self.on_filters_management);
192         });
193     },
194     /**
195      * Handle event when the user make a selection in the filters management select box.
196      */
197     on_filters_management: function(e) {
198         var self = this;
199         var select = this.$element.find(".oe_search-view-filters-management");
200         var val = select.val();
201         switch(val) {
202         case 'advanced_filter':
203             this.extended_search.on_activate();
204             break;
205         case 'add_to_dashboard':
206             this.on_add_to_dashboard();
207             break;
208         case 'manage_filters':
209             this.do_action({
210                 res_model: 'ir.filters',
211                 views: [[false, 'list'], [false, 'form']],
212                 type: 'ir.actions.act_window',
213                 context: {"search_default_user_id": this.session.uid,
214                 "search_default_model_id": this.dataset.model},
215                 target: "current",
216                 limit : 80
217             });
218             break;
219         case 'save_filter':
220             var data = this.build_search_data();
221             var context = new openerp.web.CompoundContext();
222             _.each(data.contexts, function(x) {
223                 context.add(x);
224             });
225             var domain = new openerp.web.CompoundDomain();
226             _.each(data.domains, function(x) {
227                 domain.add(x);
228             });
229             var groupbys = _.pluck(data.groupbys, "group_by").join();
230             context.add({"group_by": groupbys});
231             var dial_html = QWeb.render("SearchView.managed-filters.add");
232             var $dial = $(dial_html);
233             $dial.dialog({
234                 modal: true,
235                 title: _t("Filter Entry"),
236                 buttons: [
237                     {text: _t("Cancel"), click: function() {
238                         $(this).dialog("close");
239                     }},
240                     {text: _t("OK"), click: function() {
241                         $(this).dialog("close");
242                         var name = $(this).find("input").val();
243                         self.rpc('/web/searchview/save_filter', {
244                             model: self.dataset.model,
245                             context_to_save: context,
246                             domain: domain,
247                             name: name
248                         }).then(function() {
249                             self.reload_managed_filters();
250                         });
251                     }}
252                 ]
253             });
254             break;
255         }
256         if (val.slice(0, 4) == "get:") {
257             val = val.slice(4);
258             val = parseInt(val, 10);
259             var filter = this.managed_filters[val];
260             this.do_clear().then(_.bind(function() {
261                 var groupbys = _.map(filter.context.group_by.split(","), function(el) {
262                     return {"group_by": el};
263                 });
264                 this.on_search([filter.domain], [filter.context], groupbys);
265             }, this));
266         } else {
267             select.val('');
268         }
269     },
270     on_add_to_dashboard: function() {
271         this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0;
272         var self = this,
273             menu = openerp.webclient.menu,
274             $dialog = $(QWeb.render("SearchView.add_to_dashboard", {
275                 dashboards : menu.data.data.children,
276                 selected_menu_id : menu.$element.find('a.active').data('menu')
277             }));
278         $dialog.find('input').val(this.fields_view.name);
279         $dialog.dialog({
280             modal: true,
281             title: _t("Add to Dashboard"),
282             buttons: [
283                 {text: _t("Cancel"), click: function() {
284                     $(this).dialog("close");
285                 }},
286                 {text: _t("OK"), click: function() {
287                     $(this).dialog("close");
288                     var menu_id = $(this).find("select").val(),
289                         title = $(this).find("input").val(),
290                         data = self.build_search_data(),
291                         context = new openerp.web.CompoundContext(),
292                         domain = new openerp.web.CompoundDomain();
293                     _.each(data.contexts, function(x) {
294                         context.add(x);
295                     });
296                     _.each(data.domains, function(x) {
297                            domain.add(x);
298                     });
299                     self.rpc('/web/searchview/add_to_dashboard', {
300                         menu_id: menu_id,
301                         action_id: self.widget_parent.action.id,
302                         context_to_save: context,
303                         domain: domain,
304                         view_mode: self.widget_parent.active_view,
305                         name: title
306                     }, function(r) {
307                         if (r === false) {
308                             self.do_warn("Could not add filter to dashboard");
309                         } else {
310                             self.do_notify("Filter added to dashboard", '');
311                         }
312                     });
313                 }}
314             ]
315         });
316     },
317     /**
318      * Performs the search view collection of widget data.
319      *
320      * If the collection went well (all fields are valid), then triggers
321      * :js:func:`openerp.web.SearchView.on_search`.
322      *
323      * If at least one field failed its validation, triggers
324      * :js:func:`openerp.web.SearchView.on_invalid` instead.
325      *
326      * @param e jQuery event object coming from the "Search" button
327      */
328     do_search: function (e) {
329         if (this.headless && !this.has_defaults) {
330             return this.on_search([], [], []);
331         }
332         // reset filters management
333         var select = this.$element.find(".oe_search-view-filters-management");
334         select.val("_filters");
335
336         if (e && e.preventDefault) { e.preventDefault(); }
337
338         var data = this.build_search_data();
339
340         if (data.errors.length) {
341             this.on_invalid(data.errors);
342             return;
343         }
344
345         this.on_search(data.domains, data.contexts, data.groupbys);
346     },
347     build_search_data: function() {
348         var domains = [],
349            contexts = [],
350              errors = [];
351
352         _.each(this.inputs, function (input) {
353             try {
354                 var domain = input.get_domain();
355                 if (domain) {
356                     domains.push(domain);
357                 }
358
359                 var context = input.get_context();
360                 if (context) {
361                     contexts.push(context);
362                 }
363             } catch (e) {
364                 if (e instanceof openerp.web.search.Invalid) {
365                     errors.push(e);
366                 } else {
367                     throw e;
368                 }
369             }
370         });
371
372         // TODO: do we need to handle *fields* with group_by in their context?
373         var groupbys = _(this.enabled_filters)
374                 .chain()
375                 .map(function (filter) { return filter.get_context();})
376                 .compact()
377                 .value();
378         return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
379     },
380     /**
381      * Triggered after the SearchView has collected all relevant domains and
382      * contexts.
383      *
384      * It is provided with an Array of domains and an Array of contexts, which
385      * may or may not be evaluated (each item can be either a valid domain or
386      * context, or a string to evaluate in order in the sequence)
387      *
388      * It is also passed an array of contexts used for group_by (they are in
389      * the correct order for group_by evaluation, which contexts may not be)
390      *
391      * @event
392      * @param {Array} domains an array of literal domains or domain references
393      * @param {Array} contexts an array of literal contexts or context refs
394      * @param {Array} groupbys ordered contexts which may or may not have group_by keys
395      */
396     on_search: function (domains, contexts, groupbys) {
397     },
398     /**
399      * Triggered after a validation error in the SearchView fields.
400      *
401      * Error objects have three keys:
402      * * ``field`` is the name of the invalid field
403      * * ``value`` is the invalid value
404      * * ``message`` is the (in)validation message provided by the field
405      *
406      * @event
407      * @param {Array} errors a never-empty array of error objects
408      */
409     on_invalid: function (errors) {
410         this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
411     },
412     do_clear: function () {
413         this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
414         this.enabled_filters.splice(0);
415         var string = $('a.searchview_group_string');
416         _.each(string, function(str){
417             $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
418          });
419         this.$element.find('table:last').hide();
420
421         $('.searchview_extended_groups_list').empty();
422         _.each(this.inputs, function (input) {
423             if(input.datewidget && input.datewidget.value) {
424                 input.datewidget.set_value(false);
425             }
426         });
427         return $.async_when().pipe(this.on_clear);
428     },
429     /**
430      * Triggered when the search view gets cleared
431      *
432      * @event
433      */
434     on_clear: function () {
435         this.do_search();
436     },
437     /**
438      * Called by a filter propagating its state changes
439      *
440      * @param {openerp.web.search.Filter} filter a filter which got toggled
441      * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
442      */
443     do_toggle_filter: function (filter, default_enabled) {
444         if (default_enabled || filter.is_enabled()) {
445             this.enabled_filters.push(filter);
446         } else {
447             this.enabled_filters = _.without(
448                 this.enabled_filters, filter);
449         }
450
451         if (!default_enabled) {
452             // selecting a filter after initial loading automatically
453             // triggers refresh
454             this.$element.find('form').submit();
455         }
456     }
457 });
458
459 /** @namespace */
460 openerp.web.search = {};
461 /**
462  * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
463  * find and instantiate its field widgets.
464  */
465 openerp.web.search.fields = new openerp.web.Registry({
466     'char': 'openerp.web.search.CharField',
467     'text': 'openerp.web.search.CharField',
468     'boolean': 'openerp.web.search.BooleanField',
469     'integer': 'openerp.web.search.IntegerField',
470     'float': 'openerp.web.search.FloatField',
471     'selection': 'openerp.web.search.SelectionField',
472     'datetime': 'openerp.web.search.DateTimeField',
473     'date': 'openerp.web.search.DateField',
474     'many2one': 'openerp.web.search.ManyToOneField',
475     'many2many': 'openerp.web.search.CharField',
476     'one2many': 'openerp.web.search.CharField'
477 });
478 openerp.web.search.Invalid = openerp.web.Class.extend( /** @lends openerp.web.search.Invalid# */{
479     /**
480      * Exception thrown by search widgets when they hold invalid values,
481      * which they can not return when asked.
482      *
483      * @constructs openerp.web.search.Invalid
484      * @extends openerp.web.Class
485      *
486      * @param field the name of the field holding an invalid value
487      * @param value the invalid value
488      * @param message validation failure message
489      */
490     init: function (field, value, message) {
491         this.field = field;
492         this.value = value;
493         this.message = message;
494     },
495     toString: function () {
496         return _.str.sprintf(
497             _t("Incorrect value for field %(fieldname)s: [%(value)s] is %(message)s"),
498             {fieldname: this.field, value: this.value, message: this.message}
499         );
500     }
501 });
502 openerp.web.search.Widget = openerp.web.Widget.extend( /** @lends openerp.web.search.Widget# */{
503     template: null,
504     /**
505      * Root class of all search widgets
506      *
507      * @constructs openerp.web.search.Widget
508      * @extends openerp.web.Widget
509      *
510      * @param view the ancestor view of this widget
511      */
512     init: function (view) {
513         this._super(view);
514         this.view = view;
515     },
516     /**
517      * Sets and returns a globally unique identifier for the widget.
518      *
519      * If a prefix is specified, the identifier will be appended to it.
520      *
521      * @params prefix prefix sections, empty/falsy sections will be removed
522      */
523     make_id: function () {
524         this.element_id = _.uniqueId(
525             ['search'].concat(
526                 _.compact(_.toArray(arguments)),
527                 ['']).join('_'));
528         return this.element_id;
529     },
530     /**
531      * "Starts" the widgets. Called at the end of the rendering, this allows
532      * widgets to hook themselves to their view sections.
533      *
534      * On widgets, if they kept a reference to a view and have an element_id,
535      * will fetch and set their root element on $element.
536      */
537     start: function () {
538         this._super();
539         if (this.view && this.element_id) {
540             // id is unique, and no getElementById on elements
541             this.$element = $(document.getElementById(
542                 this.element_id));
543         }
544     },
545     /**
546      * "Stops" the widgets. Called when the view destroys itself, this
547      * lets the widgets clean up after themselves.
548      */
549     stop: function () {
550         delete this.view;
551         this._super();
552     },
553     render: function (defaults) {
554         // FIXME
555         return this._super(_.extend(this, {defaults: defaults}));
556     }
557 });
558 openerp.web.search.add_expand_listener = function($root) {
559     $root.find('a.searchview_group_string').click(function (e) {
560         $root.toggleClass('folded expanded');
561         e.stopPropagation();
562         e.preventDefault();
563     });
564 };
565 openerp.web.search.Group = openerp.web.search.Widget.extend({
566     template: 'SearchView.group',
567     init: function (view_section, view, fields) {
568         this._super(view);
569         this.attrs = view_section.attrs;
570         this.lines = view.make_widgets(
571             view_section.children, fields);
572         this.make_id('group');
573     },
574     start: function () {
575         this._super();
576         openerp.web.search.add_expand_listener(this.$element);
577         var widget_starts = _(this.lines).chain().flatten()
578                 .map(function (widget) { return widget.start(); })
579             .value();
580         return $.when.apply(null, widget_starts);
581     }
582 });
583
584 openerp.web.search.Input = openerp.web.search.Widget.extend( /** @lends openerp.web.search.Input# */{
585     /**
586      * @constructs openerp.web.search.Input
587      * @extends openerp.web.search.Widget
588      *
589      * @param view
590      */
591     init: function (view) {
592         this._super(view);
593         this.view.inputs.push(this);
594         this.style = undefined;
595     },
596     get_context: function () {
597         throw new Error(
598             "get_context not implemented for widget " + this.attrs.type);
599     },
600     get_domain: function () {
601         throw new Error(
602             "get_domain not implemented for widget " + this.attrs.type);
603     },
604     load_attrs: function (attrs) {
605         if (attrs.modifiers) {
606             attrs.modifiers = JSON.parse(attrs.modifiers);
607             attrs.invisible = attrs.modifiers.invisible || false;
608             if (attrs.invisible) {
609                 this.style = 'display: none;'
610             }
611         }
612         this.attrs = attrs;
613     }
614 });
615 openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{
616     template: 'SearchView.filters',
617     /**
618      * Inclusive group of filters, creates a continuous "button" with clickable
619      * sections (the normal display for filters is to be a self-contained button)
620      *
621      * @constructs openerp.web.search.FilterGroup
622      * @extends openerp.web.search.Input
623      *
624      * @param {Array<openerp.web.search.Filter>} filters elements of the group
625      * @param {openerp.web.SearchView} view view in which the filters are contained
626      */
627     init: function (filters, view) {
628         this._super(view);
629         this.filters = filters;
630         this.length = filters.length;
631     },
632     start: function () {
633         this._super();
634         _.each(this.filters, function (filter) {
635             filter.start();
636         });
637     },
638     get_context: function () { },
639     /**
640      * Handles domains-fetching for all the filters within it: groups them.
641      */
642     get_domain: function () {
643         var domains = _(this.filters).chain()
644             .filter(function (filter) { return filter.is_enabled(); })
645             .map(function (filter) { return filter.attrs.domain; })
646             .reject(_.isEmpty)
647             .value();
648
649         if (!domains.length) { return; }
650         if (domains.length === 1) { return domains[0]; }
651         for (var i=domains.length; --i;) {
652             domains.unshift(['|']);
653         }
654         return _.extend(new openerp.web.CompoundDomain(), {
655             __domains: domains
656         });
657     }
658 });
659 openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{
660     template: 'SearchView.filter',
661     /**
662      * Implementation of the OpenERP filters (button with a context and/or
663      * a domain sent as-is to the search view)
664      *
665      * @constructs openerp.web.search.Filter
666      * @extends openerp.web.search.Input
667      *
668      * @param node
669      * @param view
670      */
671     init: function (node, view) {
672         this._super(view);
673         this.load_attrs(node.attrs);
674         this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
675         this.make_id('filter', this.attrs.name);
676     },
677     start: function () {
678         this._super();
679         var self = this;
680         this.$element.click(function (e) {
681             $(this).toggleClass('enabled');
682             self.view.do_toggle_filter(self);
683         });
684     },
685     /**
686      * Returns whether the filter is currently enabled (in use) or not.
687      *
688      * @returns a boolean
689      */
690     is_enabled:function () {
691         return this.$element.hasClass('enabled');
692     },
693     /**
694      * If the filter is present in the defaults (and has a truthy value),
695      * enable the filter.
696      *
697      * @param {Object} defaults the search view's default values
698      */
699     render: function (defaults) {
700         if (this.attrs.name && defaults[this.attrs.name]) {
701             this.classes.push('enabled');
702             this.view.do_toggle_filter(this, true);
703         }
704         return this._super(defaults);
705     },
706     get_context: function () {
707         if (!this.is_enabled()) {
708             return;
709         }
710         return this.attrs.context;
711     },
712     /**
713      * Does not return anything: filter domain is handled at the FilterGroup
714      * level
715      */
716     get_domain: function () { }
717 });
718 openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
719     template: 'SearchView.field',
720     default_operator: '=',
721     /**
722      * @constructs openerp.web.search.Field
723      * @extends openerp.web.search.Input
724      *
725      * @param view_section
726      * @param field
727      * @param view
728      */
729     init: function (view_section, field, view) {
730         this._super(view);
731         this.load_attrs(_.extend({}, field, view_section.attrs));
732         this.filters = new openerp.web.search.FilterGroup(_.compact(_.map(
733             view_section.children, function (filter_node) {
734                 if (filter_node.attrs.string &&
735                         typeof console !== 'undefined' && console.debug) {
736                     console.debug("Filter-in-field with a 'string' attribute "
737                                 + "in view", view);
738                 }
739                 delete filter_node.attrs.string;
740                 return new openerp.web.search.Filter(
741                     filter_node, view);
742         })), view);
743         this.make_id('input', field.type, this.attrs.name);
744     },
745     start: function () {
746         this._super();
747         this.filters.start();
748     },
749     get_context: function () {
750         var val = this.get_value();
751         // A field needs a value to be "active", and a context to send when
752         // active
753         var has_value = (val !== null && val !== '');
754         var context = this.attrs.context;
755         if (!(has_value && context)) {
756             return;
757         }
758         return _.extend(
759             {}, context,
760             {own_values: {self: val}});
761     },
762     /**
763      * Function creating the returned domain for the field, override this
764      * methods in children if you only need to customize the field's domain
765      * without more complex alterations or tests (and without the need to
766      * change override the handling of filter_domain)
767      *
768      * @param {String} name the field's name
769      * @param {String} operator the field's operator (either attribute-specified or default operator for the field
770      * @param {Number|String} value parsed value for the field
771      * @returns {Array<Array>} domain to include in the resulting search
772      */
773     make_domain: function (name, operator, value) {
774         return [[name, operator, value]];
775     },
776     get_domain: function () {
777         var val = this.get_value();
778         if (val === null || val === '') {
779             return;
780         }
781
782         var domain = this.attrs['filter_domain'];
783         if (!domain) {
784             return this.make_domain(
785                 this.attrs.name,
786                 this.attrs.operator || this.default_operator,
787                 val);
788         }
789         return _.extend({}, domain, {own_values: {self: val}});
790     }
791 });
792 /**
793  * Implementation of the ``char`` OpenERP field type:
794  *
795  * * Default operator is ``ilike`` rather than ``=``
796  *
797  * * The Javascript and the HTML values are identical (strings)
798  *
799  * @class
800  * @extends openerp.web.search.Field
801  */
802 openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ {
803     default_operator: 'ilike',
804     get_value: function () {
805         return this.$element.val();
806     }
807 });
808 openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
809     get_value: function () {
810         if (!this.$element.val()) {
811             return null;
812         }
813         var val = this.parse(this.$element.val()),
814           check = Number(this.$element.val());
815         if (isNaN(val) || val !== check) {
816             this.$element.addClass('error');
817             throw new openerp.web.search.Invalid(
818                 this.attrs.name, this.$element.val(), this.error_message);
819         }
820         this.$element.removeClass('error');
821         return val;
822     }
823 });
824 /**
825  * @class
826  * @extends openerp.web.search.NumberField
827  */
828 openerp.web.search.IntegerField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.IntegerField# */{
829     error_message: _t("not a valid integer"),
830     parse: function (value) {
831         try {
832             return openerp.web.parse_value(value, {'widget': 'integer'});
833         } catch (e) {
834             return NaN;
835         }
836     }
837 });
838 /**
839  * @class
840  * @extends openerp.web.search.NumberField
841  */
842 openerp.web.search.FloatField = openerp.web.search.NumberField.extend(/** @lends openerp.web.search.FloatField# */{
843     error_message: _t("not a valid number"),
844     parse: function (value) {
845         try {
846             return openerp.web.parse_value(value, {'widget': 'float'});
847         } catch (e) {
848             return NaN;
849         }
850     }
851 });
852 /**
853  * @class
854  * @extends openerp.web.search.Field
855  */
856 openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
857     // This implementation is a basic <select> field, but it may have to be
858     // altered to be more in line with the GTK client, which uses a combo box
859     // (~ jquery.autocomplete):
860     // * If an option was selected in the list, behave as currently
861     // * If something which is not in the list was entered (via the text input),
862     //   the default domain should become (`ilike` string_value) but **any
863     //   ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
864     //   is specified. So at least get_domain needs to be quite a bit
865     //   overridden (if there's no @value and there is no filter_domain and
866     //   there is no @operator, return [[name, 'ilike', str_val]]
867     template: 'SearchView.field.selection',
868     init: function () {
869         this._super.apply(this, arguments);
870         // prepend empty option if there is no empty option in the selection list
871         this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
872             return !item[1];
873         });
874     },
875     get_value: function () {
876         var index = parseInt(this.$element.val(), 10);
877         if (isNaN(index)) { return null; }
878         var value = this.attrs.selection[index][0];
879         if (value === false) { return null; }
880         return value;
881     },
882     /**
883      * The selection field needs a default ``false`` value in case none is
884      * provided, so that selector options with a ``false`` value (convention
885      * for explicitly empty options) get selected by default rather than the
886      * first (value-holding) option in the selection.
887      *
888      * @param {Object} defaults search default values
889      */
890     render: function (defaults) {
891         if (!defaults[this.attrs.name]) {
892             defaults[this.attrs.name] = false;
893         }
894         return this._super(defaults);
895     }
896 });
897 openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
898     /**
899      * @constructs openerp.web.search.BooleanField
900      * @extends openerp.web.search.BooleanField
901      */
902     init: function () {
903         this._super.apply(this, arguments);
904         this.attrs.selection = [
905             ['true', _t("Yes")],
906             ['false', _t("No")]
907         ];
908     },
909     /**
910      * Search defaults likely to be boolean values (for a boolean field).
911      *
912      * In the HTML, we only want/get strings, and our strings here are ``true``
913      * and ``false``, so ensure we use precisely those by truth-testing the
914      * default value (iif there is one in the view's defaults).
915      *
916      * @param {Object} defaults default values for this search view
917      * @returns {String} rendered boolean field
918      */
919     render: function (defaults) {
920         var name = this.attrs.name;
921         if (name in defaults) {
922             defaults[name] = defaults[name] ? "true" : "false";
923         }
924         return this._super(defaults);
925     },
926     get_value: function () {
927         switch (this.$element.val()) {
928             case 'false': return false;
929             case 'true': return true;
930             default: return null;
931         }
932     }
933 });
934 /**
935  * @class
936  * @extends openerp.web.search.DateField
937  */
938 openerp.web.search.DateField = openerp.web.search.Field.extend(/** @lends openerp.web.search.DateField# */{
939     template: "SearchView.date",
940     start: function () {
941         this._super();
942         this.datewidget = new openerp.web.DateWidget(this);
943         this.datewidget.prependTo(this.$element);
944         this.datewidget.$element.find("input").attr("size", 15);
945         this.datewidget.$element.find("input").attr("autofocus",
946             this.attrs.default_focus === '1' ? 'autofocus' : undefined);
947         this.datewidget.set_value(this.defaults[this.attrs.name] || false);
948     },
949     get_value: function () {
950         return this.datewidget.get_value() || null;
951     }
952 });
953 /**
954  * Implementation of the ``datetime`` openerp field type:
955  *
956  * * Uses the same widget as the ``date`` field type (a simple date)
957  *
958  * * Builds a slighly more complex, it's a datetime range (includes time)
959  *   spanning the whole day selected by the date widget
960  *
961  * @class
962  * @extends openerp.web.DateField
963  */
964 openerp.web.search.DateTimeField = openerp.web.search.DateField.extend(/** @lends openerp.web.search.DateTimeField# */{
965     make_domain: function (name, operator, value) {
966         return ['&', [name, '>=', value + ' 00:00:00'],
967                      [name, '<=', value + ' 23:59:59']];
968     }
969 });
970 openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
971     init: function (view_section, field, view) {
972         this._super(view_section, field, view);
973         var self = this;
974         this.got_name = $.Deferred().then(function () {
975             self.$element.val(self.name);
976         });
977         this.dataset = new openerp.web.DataSet(
978                 this.view, this.attrs['relation']);
979     },
980     start: function () {
981         this._super();
982         this.setup_autocomplete();
983         var started = $.Deferred();
984         this.got_name.then(function () { started.resolve();},
985                            function () { started.resolve(); });
986         return started.promise();
987     },
988     setup_autocomplete: function () {
989         var self = this;
990         this.$element.autocomplete({
991             source: function (req, resp) {
992                 self.dataset.name_search(
993                     req.term, self.attrs.domain, 'ilike', 8, function (data) {
994                         resp(_.map(data, function (result) {
995                             return {id: result[0], label: result[1]}
996                         }));
997                 });
998             },
999             select: function (event, ui) {
1000                 self.id = ui.item.id;
1001                 self.name = ui.item.label;
1002             },
1003             delay: 0
1004         })
1005     },
1006     on_name_get: function (name_get) {
1007         if (!name_get.length) {
1008             delete this.id;
1009             this.got_name.reject();
1010             return;
1011         }
1012         this.name = name_get[0][1];
1013         this.got_name.resolve();
1014     },
1015     render: function (defaults) {
1016         if (defaults[this.attrs.name]) {
1017             this.id = defaults[this.attrs.name];
1018             if (this.id instanceof Array)
1019                 this.id = this.id[0];
1020             // TODO: maybe this should not be completely removed
1021             delete defaults[this.attrs.name];
1022             this.dataset.name_get([this.id], $.proxy(this, 'on_name_get'));
1023         } else {
1024             this.got_name.reject();
1025         }
1026         return this._super(defaults);
1027     },
1028     make_domain: function (name, operator, value) {
1029         if (this.id && this.name) {
1030             if (value === this.name) {
1031                 return [[name, '=', this.id]];
1032             } else {
1033                 delete this.id;
1034                 delete this.name;
1035             }
1036         }
1037         return this._super(name, operator, value);
1038     }
1039 });
1040
1041 openerp.web.search.ExtendedSearch = openerp.web.OldWidget.extend({
1042     template: 'SearchView.extended_search',
1043     identifier_prefix: 'extended-search',
1044     init: function (parent, model) {
1045         this._super(parent);
1046         this.model = model;
1047     },
1048     add_group: function() {
1049         var group = new openerp.web.search.ExtendedSearchGroup(this, this.fields);
1050         group.appendTo(this.$element.find('.searchview_extended_groups_list'));
1051         this.check_last_element();
1052     },
1053     start: function () {
1054         this.$element = $("#" + this.element_id);
1055         this.$element.closest("table.oe-searchview-render-line").css("display", "none");
1056         var self = this;
1057         this.rpc("/web/searchview/fields_get",
1058             {"model": this.model}, function(data) {
1059             self.fields = data.fields;
1060             if (!('id' in self.fields)) {
1061                 self.fields.id = {
1062                     string: 'ID',
1063                     type: 'id'
1064                 }
1065             }
1066             openerp.web.search.add_expand_listener(self.$element);
1067             self.$element.find('.searchview_extended_add_group').click(function (e) {
1068                 self.add_group();
1069             });
1070         });
1071     },
1072     get_context: function() {
1073         return null;
1074     },
1075     get_domain: function() {
1076         if (!this.$element) {
1077             return null; // not a logical state but sometimes it happens
1078         }
1079         if(this.$element.closest("table.oe-searchview-render-line").css("display") == "none") {
1080             return null;
1081         }
1082         return _.reduce(this.widget_children,
1083             function(mem, x) { return mem.concat(x.get_domain());}, []);
1084     },
1085     on_activate: function() {
1086         this.add_group();
1087         var table = this.$element.closest("table.oe-searchview-render-line");
1088         table.css("display", "");
1089         if(this.$element.hasClass("folded")) {
1090             this.$element.toggleClass("folded expanded");
1091         }
1092     },
1093     hide: function() {
1094         var table = this.$element.closest("table.oe-searchview-render-line");
1095         table.css("display", "none");
1096         if(this.$element.hasClass("expanded")) {
1097             this.$element.toggleClass("folded expanded");
1098         }
1099     },
1100     check_last_element: function() {
1101         _.each(this.widget_children, function(x) {x.set_last_group(false);});
1102         if (this.widget_children.length >= 1) {
1103             this.widget_children[this.widget_children.length - 1].set_last_group(true);
1104         }
1105     }
1106 });
1107
1108 openerp.web.search.ExtendedSearchGroup = openerp.web.OldWidget.extend({
1109     template: 'SearchView.extended_search.group',
1110     identifier_prefix: 'extended-search-group',
1111     init: function (parent, fields) {
1112         this._super(parent);
1113         this.fields = fields;
1114     },
1115     add_prop: function() {
1116         var prop = new openerp.web.search.ExtendedSearchProposition(this, this.fields);
1117         var render = prop.render({'index': this.widget_children.length - 1});
1118         this.$element.find('.searchview_extended_propositions_list').append(render);
1119         prop.start();
1120     },
1121     start: function () {
1122         this.$element = $("#" + this.element_id);
1123         var _this = this;
1124         this.add_prop();
1125         this.$element.find('.searchview_extended_add_proposition').click(function () {
1126             _this.add_prop();
1127         });
1128         this.$element.find('.searchview_extended_delete_group').click(function () {
1129             _this.stop();
1130         });
1131     },
1132     get_domain: function() {
1133         var props = _(this.widget_children).chain().map(function(x) {
1134             return x.get_proposition();
1135         }).compact().value();
1136         var choice = this.$element.find(".searchview_extended_group_choice").val();
1137         var op = choice == "all" ? "&" : "|";
1138         return choice == "none" ? ['!'] : [].concat(
1139             _.map(_.range(_.max([0,props.length - 1])), function() { return op; }),
1140             props);
1141     },
1142     stop: function() {
1143         var parent = this.widget_parent;
1144         if (this.widget_parent.widget_children.length == 1)
1145             this.widget_parent.hide();
1146         this._super();
1147         parent.check_last_element();
1148     },
1149     set_last_group: function(is_last) {
1150         this.$element.toggleClass('last_group', is_last);
1151     }
1152 });
1153
1154 openerp.web.search.ExtendedSearchProposition = openerp.web.OldWidget.extend(/** @lends openerp.web.search.ExtendedSearchProposition# */{
1155     template: 'SearchView.extended_search.proposition',
1156     identifier_prefix: 'extended-search-proposition',
1157     /**
1158      * @constructs openerp.web.search.ExtendedSearchProposition
1159      * @extends openerp.web.OldWidget
1160      *
1161      * @param parent
1162      * @param fields
1163      */
1164     init: function (parent, fields) {
1165         this._super(parent);
1166         this.fields = _(fields).chain()
1167             .map(function(val, key) { return _.extend({}, val, {'name': key}); })
1168             .sortBy(function(field) {return field.string;})
1169             .value();
1170         this.attrs = {_: _, fields: this.fields, selected: null};
1171         this.value = null;
1172     },
1173     start: function () {
1174         this.$element = $("#" + this.element_id);
1175         this.select_field(this.fields.length > 0 ? this.fields[0] : null);
1176         var _this = this;
1177         this.$element.find(".searchview_extended_prop_field").change(function() {
1178             _this.changed();
1179         });
1180         this.$element.find('.searchview_extended_delete_prop').click(function () {
1181             _this.stop();
1182         });
1183     },
1184     stop: function() {
1185         var parent;
1186         if (this.widget_parent.widget_children.length == 1)
1187             parent = this.widget_parent;
1188         this._super();
1189         if (parent)
1190             parent.stop();
1191     },
1192     changed: function() {
1193         var nval = this.$element.find(".searchview_extended_prop_field").val();
1194         if(this.attrs.selected == null || nval != this.attrs.selected.name) {
1195             this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
1196         }
1197     },
1198     /**
1199      * Selects the provided field object
1200      *
1201      * @param field a field descriptor object (as returned by fields_get, augmented by the field name)
1202      */
1203     select_field: function(field) {
1204         var self = this;
1205         if(this.attrs.selected != null) {
1206             this.value.stop();
1207             this.value = null;
1208             this.$element.find('.searchview_extended_prop_op').html('');
1209         }
1210         this.attrs.selected = field;
1211         if(field == null) {
1212             return;
1213         }
1214
1215         var type = field.type;
1216         try {
1217             openerp.web.search.custom_filters.get_object(type);
1218         } catch (e) {
1219             if (! e instanceof openerp.web.KeyNotFound) {
1220                 throw e;
1221             }
1222             type = "char";
1223             console.log('Unknow field type ' + e.key);
1224         }
1225         this.value = new (openerp.web.search.custom_filters.get_object(type))
1226                           (this);
1227         if(this.value.set_field) {
1228             this.value.set_field(field);
1229         }
1230         _.each(this.value.operators, function(operator) {
1231             $('<option>', {value: operator.value})
1232                 .text(String(operator.text))
1233                 .appendTo(self.$element.find('.searchview_extended_prop_op'));
1234         });
1235         this.$element.find('.searchview_extended_prop_value').html(
1236             this.value.render({}));
1237         this.value.start();
1238
1239     },
1240     get_proposition: function() {
1241         if ( this.attrs.selected == null)
1242             return null;
1243         var field = this.attrs.selected.name;
1244         var op =  this.$element.find('.searchview_extended_prop_op').val();
1245         var value = this.value.get_value();
1246         return [field, op, value];
1247     }
1248 });
1249
1250 openerp.web.search.ExtendedSearchProposition.Field = openerp.web.OldWidget.extend({
1251     start: function () {
1252         this.$element = $("#" + this.element_id);
1253     }
1254 });
1255 openerp.web.search.ExtendedSearchProposition.Char = openerp.web.search.ExtendedSearchProposition.Field.extend({
1256     template: 'SearchView.extended_search.proposition.char',
1257     identifier_prefix: 'extended-search-proposition-char',
1258     operators: [
1259         {value: "ilike", text: _lt("contains")},
1260         {value: "not ilike", text: _lt("doesn't contain")},
1261         {value: "=", text: _lt("is equal to")},
1262         {value: "!=", text: _lt("is not equal to")},
1263         {value: ">", text: _lt("greater than")},
1264         {value: "<", text: _lt("less than")},
1265         {value: ">=", text: _lt("greater or equal than")},
1266         {value: "<=", text: _lt("less or equal than")}
1267     ],
1268     get_value: function() {
1269         return this.$element.val();
1270     }
1271 });
1272 openerp.web.search.ExtendedSearchProposition.DateTime = openerp.web.search.ExtendedSearchProposition.Field.extend({
1273     template: 'SearchView.extended_search.proposition.empty',
1274     identifier_prefix: 'extended-search-proposition-datetime',
1275     operators: [
1276         {value: "=", text: _lt("is equal to")},
1277         {value: "!=", text: _lt("is not equal to")},
1278         {value: ">", text: _lt("greater than")},
1279         {value: "<", text: _lt("less than")},
1280         {value: ">=", text: _lt("greater or equal than")},
1281         {value: "<=", text: _lt("less or equal than")}
1282     ],
1283     get_value: function() {
1284         return this.datewidget.get_value();
1285     },
1286     start: function() {
1287         this._super();
1288         this.datewidget = new openerp.web.DateTimeWidget(this);
1289         this.datewidget.prependTo(this.$element);
1290     }
1291 });
1292 openerp.web.search.ExtendedSearchProposition.Date = openerp.web.search.ExtendedSearchProposition.Field.extend({
1293     template: 'SearchView.extended_search.proposition.empty',
1294     identifier_prefix: 'extended-search-proposition-date',
1295     operators: [
1296         {value: "=", text: _lt("is equal to")},
1297         {value: "!=", text: _lt("is not equal to")},
1298         {value: ">", text: _lt("greater than")},
1299         {value: "<", text: _lt("less than")},
1300         {value: ">=", text: _lt("greater or equal than")},
1301         {value: "<=", text: _lt("less or equal than")}
1302     ],
1303     get_value: function() {
1304         return this.datewidget.get_value();
1305     },
1306     start: function() {
1307         this._super();
1308         this.datewidget = new openerp.web.DateWidget(this);
1309         this.datewidget.prependTo(this.$element);
1310     }
1311 });
1312 openerp.web.search.ExtendedSearchProposition.Integer = openerp.web.search.ExtendedSearchProposition.Field.extend({
1313     template: 'SearchView.extended_search.proposition.integer',
1314     identifier_prefix: 'extended-search-proposition-integer',
1315     operators: [
1316         {value: "=", text: _lt("is equal to")},
1317         {value: "!=", text: _lt("is not equal to")},
1318         {value: ">", text: _lt("greater than")},
1319         {value: "<", text: _lt("less than")},
1320         {value: ">=", text: _lt("greater or equal than")},
1321         {value: "<=", text: _lt("less or equal than")}
1322     ],
1323     get_value: function() {
1324         try {
1325             return openerp.web.parse_value(this.$element.val(), {'widget': 'integer'});
1326         } catch (e) {
1327             return "";
1328         }
1329     }
1330 });
1331 openerp.web.search.ExtendedSearchProposition.Id = openerp.web.search.ExtendedSearchProposition.Integer.extend({
1332     operators: [{value: "=", text: _lt("is")}]
1333 });
1334 openerp.web.search.ExtendedSearchProposition.Float = openerp.web.search.ExtendedSearchProposition.Field.extend({
1335     template: 'SearchView.extended_search.proposition.float',
1336     identifier_prefix: 'extended-search-proposition-float',
1337     operators: [
1338         {value: "=", text: _lt("is equal to")},
1339         {value: "!=", text: _lt("is not equal to")},
1340         {value: ">", text: _lt("greater than")},
1341         {value: "<", text: _lt("less than")},
1342         {value: ">=", text: _lt("greater or equal than")},
1343         {value: "<=", text: _lt("less or equal than")}
1344     ],
1345     get_value: function() {
1346         try {
1347             return openerp.web.parse_value(this.$element.val(), {'widget': 'float'});
1348         } catch (e) {
1349             return "";
1350         }
1351     }
1352 });
1353 openerp.web.search.ExtendedSearchProposition.Selection = openerp.web.search.ExtendedSearchProposition.Field.extend({
1354     template: 'SearchView.extended_search.proposition.selection',
1355     identifier_prefix: 'extended-search-proposition-selection',
1356     operators: [
1357         {value: "=", text: _lt("is")},
1358         {value: "!=", text: _lt("is not")}
1359     ],
1360     set_field: function(field) {
1361         this.field = field;
1362     },
1363     get_value: function() {
1364         return this.$element.val();
1365     }
1366 });
1367 openerp.web.search.ExtendedSearchProposition.Boolean = openerp.web.search.ExtendedSearchProposition.Field.extend({
1368     template: 'SearchView.extended_search.proposition.boolean',
1369     identifier_prefix: 'extended-search-proposition-boolean',
1370     operators: [
1371         {value: "=", text: _lt("is true")},
1372         {value: "!=", text: _lt("is false")}
1373     ],
1374     get_value: function() {
1375         return true;
1376     }
1377 });
1378
1379 openerp.web.search.custom_filters = new openerp.web.Registry({
1380     'char': 'openerp.web.search.ExtendedSearchProposition.Char',
1381     'text': 'openerp.web.search.ExtendedSearchProposition.Char',
1382     'one2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1383     'many2one': 'openerp.web.search.ExtendedSearchProposition.Char',
1384     'many2many': 'openerp.web.search.ExtendedSearchProposition.Char',
1385
1386     'datetime': 'openerp.web.search.ExtendedSearchProposition.DateTime',
1387     'date': 'openerp.web.search.ExtendedSearchProposition.Date',
1388     'integer': 'openerp.web.search.ExtendedSearchProposition.Integer',
1389     'float': 'openerp.web.search.ExtendedSearchProposition.Float',
1390     'boolean': 'openerp.web.search.ExtendedSearchProposition.Boolean',
1391     'selection': 'openerp.web.search.ExtendedSearchProposition.Selection',
1392
1393     'id': 'openerp.web.search.ExtendedSearchProposition.Id'
1394 });
1395
1396 };
1397
1398 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: