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