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