<commit_before>openerp.web.search = function(openerp) {
var QWeb = openerp.web.qweb,
      _t =  openerp.web._t,
     _lt = openerp.web._lt;
_.mixin({
    sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
});

// Have SearchBox optionally use callback function to produce inputs and facets
// (views) set on callbacks.make_facet and callbacks.make_input keys when
// initializing VisualSearch
var SearchBox_renderFacet = function (facet, position) {
    var view = new (this.app.options.callbacks['make_facet'] || VS.ui.SearchFacet)({
      app   : this.app,
      model : facet,
      order : position
    });

    // Input first, facet second.
    this.renderSearchInput();
    this.facetViews.push(view);
    this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);

    view.calculateSize();
    _.defer(_.bind(view.calculateSize, view));

    return view;
  }; // warning: will not match
// Ensure we're replacing the function we think
if (SearchBox_renderFacet.toString() !== VS.ui.SearchBox.prototype.renderFacet.toString().replace(/(VS\.ui\.SearchFacet)/, "(this.app.options.callbacks['make_facet'] || $1)")) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#renderFacet. "
        + "Please fix replacement.");
}
var SearchBox_renderSearchInput = function () {
    var input = new (this.app.options.callbacks['make_input'] || VS.ui.SearchInput)({position: this.inputViews.length, app: this.app});
    this.$('.VS-search-inner').append(input.render().el);
    this.inputViews.push(input);
  };
// Ensure we're replacing the function we think
if (SearchBox_renderSearchInput.toString() !== VS.ui.SearchBox.prototype.renderSearchInput.toString().replace(/(VS\.ui\.SearchInput)/, "(this.app.options.callbacks['make_input'] || $1)")) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#renderSearchInput. "
        + "Please fix replacement.");
}
var SearchBox_searchEvent = function (e) {
    var query = null;
    this.renderFacets();
    this.focusSearch(e);
    this.app.options.callbacks.search(query, this.app.searchQuery);
  };
if (SearchBox_searchEvent.toString() !== VS.ui.SearchBox.prototype.searchEvent.toString().replace(
        /this\.value\(\);\n[ ]{4}this\.focusSearch\(e\);\n[ ]{4}this\.value\(query\)/,
        'null;\n    this.renderFacets();\n    this.focusSearch(e)')) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#searchEvent. "
        + "Please fix replacement.");
}
_.extend(VS.ui.SearchBox.prototype, {
    renderFacet: SearchBox_renderFacet,
    renderSearchInput: SearchBox_renderSearchInput,
    searchEvent: SearchBox_searchEvent
});
_.extend(VS.model.SearchFacet.prototype, {
    value: function () {
        if (this.has('json')) {
            return this.get('json');
        }
        return this.get('value');
    }
});

openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
    template: "SearchView",
    /**
     * @constructs openerp.web.SearchView
     * @extends openerp.web.OldWidget
     *
     * @param parent
     * @param element_id
     * @param dataset
     * @param view_id
     * @param defaults
     */
    init: function(parent, dataset, view_id, defaults, hidden) {
        this._super(parent);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.defaults = defaults || {};
        this.has_defaults = !_.isEmpty(this.defaults);

        this.inputs = [];
        this.controls = {};

        this.hidden = !!hidden;
        this.headless = this.hidden && !this.has_defaults;

        this.filter_data = {};

        this.ready = $.Deferred();
    },
    start: function() {
        var self = this;
        var p = this._super();

        this.setup_global_completion();
        this.vs = VS.init({
            container: this.$element,
            query: '',
            callbacks: {
                make_facet: this.proxy('make_visualsearch_facet'),
                make_input: this.proxy('make_visualsearch_input'),
                search: function (query, searchCollection) {
                    self.do_search();
                },
                facetMatches: function (callback) {
                },
                valueMatches : function(facet, searchTerm, callback) {
                }
            }
        });

        var search = function () { self.vs.searchBox.searchEvent({}); };
        // searchQuery operations
        this.vs.searchQuery
            .off('add').on('add', search)
            .off('change').on('change', search)
            .off('reset').on('reset', search)
            .off('remove').on('remove', function (record, collection, options) {
                if (options['trigger_search']) {
                    search();
                }
            });

        if (this.hidden) {
            this.$element.hide();
        }
        if (this.headless) {
            this.ready.resolve();
        } else {
            var load_view = this.rpc("/web/searchview/load", {
                model: this.model,
                view_id: this.view_id,
                context: this.dataset.get_context() });
            // FIXME: local eval of domain and context to get rid of special endpoint
            var filters = this.rpc('/web/searchview/get_filters', {
                model: this.model
            }).then(function (filters) { self.custom_filters = filters; });

            $.when(load_view, filters)
                .pipe(function (load) { return load[0]; })
                .then(this.on_loaded);
        }

        this.$element.on('click', '.oe_vs_unfold_drawer', function () {
            self.$element.toggleClass('oe_searchview_open_drawer');
        });

        return $.when(p, this.ready);
    },
    show: function () {
        this.$element.show();
    },
    hide: function () {
        this.$element.hide();
    },

    /**
     * Sets up thingie where all the mess is put?
     */
    setup_stuff_drawer: function () {
        var self = this;
        $('<div class="oe_vs_unfold_drawer">').appendTo(this.$element.find('.VS-search-box'));
        var $drawer = $('<div class="oe_searchview_drawer">').appendTo(this.$element);
        var $filters = $('<div class="oe_searchview_filters">').appendTo($drawer);

        var running_count = 0;
        // get total filters count
        var is_group = function (i) { return i instanceof openerp.web.search.FilterGroup; };
        var filters_count = _(this.controls).chain()
            .flatten()
            .filter(is_group)
            .map(function (i) { return i.filters.length; })
            .sum()
            .value();

        var col1 = [], col2 = _(this.controls).map(function (inputs, group) {
            var filters = _(inputs).filter(is_group);
            return {
                name: group === 'null' ? _t("Filters") : group,
                filters: filters,
                length: _(filters).chain().map(function (i) {
                    return i.filters.length; }).sum().value()
            };
        });

        while (col2.length) {
            // col1 + group should be smaller than col2 + group
            if ((running_count + col2[0].length) <= (filters_count - running_count)) {
                running_count += col2[0].length;
                col1.push(col2.shift());
            } else {
                break;
            }
        }

        // Create a Custom Filter FilterGroup for each custom filter read from
        // the db, add all of this as a group in the smallest column
        [].push.call(col1.length <= col2.length ? col1 : col2, {
            name: _t("Custom Filters"),
            filters: _.map(this.custom_filters, function (filter) {
                // FIXME: handling of ``disabled`` being set
                var f = new openerp.web.search.Filter({attrs: {
                    string: filter.name,
                    context: filter.context,
                    domain: filter.domain
                }}, self);
                return new openerp.web.search.FilterGroup([f], self);
            }),
            length: 3
        });

        return $.when(
            this.render_column(col1, $('<div>').appendTo($filters)),
            this.render_column(col2, $('<div>').appendTo($filters)),
            (new openerp.web.search.Advanced(this).appendTo($drawer)));
    },
    render_column: function (column, $el) {
        return $.when.apply(null, _(column).map(function (group) {
            $('<h3>').text(group.name).appendTo($el);
            return $.when.apply(null,
                _(group.filters).invoke('appendTo', $el));
        }));
    },
    /**
     * Sets up search view's view-wide auto-completion widget
     */
    setup_global_completion: function () {
        // Prevent keydown from within a facet's input from reaching the
        // auto-completion widget and opening the completion list
        this.$element.on('keydown', '.search_facet input', function (e) {
            e.stopImmediatePropagation();
        });

        this.$element.autocomplete({
            source: this.proxy('complete_global_search'),
            select: this.proxy('select_completion'),
            focus: function (e) { e.preventDefault(); },
            html: true,
            minLength: 0,
            delay: 0
        }).data('autocomplete')._renderItem = function (ul, item) {
            // item of completion list
            var $item = $( "<li></li>" )
                .data( "item.autocomplete", item )
                .appendTo( ul );

            if (item.value !== undefined) {
                // regular completion item
                return $item.append(
                    (item.label)
                        ? $('<a>').html(item.label)
                        : $('<a>').text(item.value));
            }
            return $item.text(item.category)
                .css({
                    borderTop: '1px solid #cccccc',
                    margin: 0,
                    padding: 0,
                    zoom: 1,
                    'float': 'left',
                    clear: 'left',
                    width: '100%'
                });
        }
    },
    /**
     * Provide auto-completion result for req.term (an array to `resp`)
     *
     * @param {Object} req request to complete
     * @param {String} req.term searched term to complete
     * @param {Function} resp response callback
     */
    complete_global_search:  function (req, resp) {
        $.when.apply(null, _(this.inputs).chain()
            .invoke('complete', req.term)
            .value()).then(function () {
                resp(_(_(arguments).compact()).flatten(true));
        });
    },

    /**
     * Action to perform in case of selection: create a facet (model)
     * and add it to the search collection
     *
     * @param {Object} e selection event, preventDefault to avoid setting value on object
     * @param {Object} ui selection information
     * @param {Object} ui.item selected completion item
     */
    select_completion: function (e, ui) {
        e.preventDefault();
        this.vs.searchQuery.add(new VS.model.SearchFacet(_.extend(
            {app: this.vs}, ui.item)));
        this.vs.searchBox.searchEvent({});
    },

    /**
     * Builds the right SearchFacet view based on the facet object to render
     * (e.g. readonly facets for filters)
     *
     * @param {Object} options
     * @param {VS.model.SearchFacet} options.model facet object to render
     */
    make_visualsearch_facet: function (options) {
        if (options.model.get('field') instanceof openerp.web.search.FilterGroup) {
            return new openerp.web.search.FilterGroupFacet(options);
        }
        return new VS.ui.SearchFacet(options);
    },
    /**
     * Proxies searches on a SearchInput to the search view's global completion
     *
     * Also disables SearchInput.autocomplete#_move so search view's
     * autocomplete can get the corresponding events, or something.
     *
     * @param options
     */
    make_visualsearch_input: function (options) {
        var self = this, input = new VS.ui.SearchInput(options);
        input.setupAutocomplete = function () {
            _.extend(this.box.autocomplete({
                minLength: 1,
                delay: 0,
                search: function () {
                    self.$element.autocomplete('search', input.box.val());
                    return false;
                }
            }).data('autocomplete'), {
                _move: function () {},
                close: function () { self.$element.autocomplete('close'); }
            });
        };
        return input;
    },

    /**
     * Builds a list of widget rows (each row is an array of widgets)
     *
     * @param {Array} items a list of nodes to convert to widgets
     * @param {Object} fields a mapping of field names to (ORM) field attributes
     * @param {String} [group_name] name of the group to put the new controls in
     */
    make_widgets: function (items, fields, group_name) {
        group_name = group_name || null;
        if (!(group_name in this.controls)) {
            this.controls[group_name] = [];
        }
        var self = this, group = this.controls[group_name];
        var filters = [];
        _.each(items, function (item) {
            if (filters.length && item.tag !== 'filter') {
                group.push(new openerp.web.search.FilterGroup(filters, this));
                filters = [];
            }

            switch (item.tag) {
            case 'separator': case 'newline':
                break;
            case 'filter':
                filters.push(new openerp.web.search.Filter(item, this));
                break;
            case 'group':
                self.make_widgets(item.children, fields, item.attrs.string);
                break;
            case 'field':
                group.push(this.make_field(item, fields[item['attrs'].name]));
                // filters
                self.make_widgets(item.children, fields, group_name);
                break;
            }
        }, this);

        if (filters.length) {
            group.push(new openerp.web.search.FilterGroup(filters, this));
        }
    },
    /**
     * Creates a field for the provided field descriptor item (which comes
     * from fields_view_get)
     *
     * @param {Object} item fields_view_get node for the field
     * @param {Object} field fields_get result for the field
     * @returns openerp.web.search.Field
     */
    make_field: function (item, field) {
        try {
            return new (openerp.web.search.fields.get_any(
                    [item.attrs.widget, field.type]))
                (item, field, this);
        } catch (e) {
            if (! e instanceof openerp.web.KeyNotFound) {
                throw e;
            }
            // KeyNotFound means unknown field type
            console.group('Unknown field type ' + field.type);
            console.error('View node', item);
            console.info('View field', field);
            console.info('In view', this);
            console.groupEnd();
            return null;
        }
    },
    on_loaded: function(data) {
        var self = this;
        this.fields_view = data.fields_view;
        if (data.fields_view.type !== 'search' ||
            data.fields_view.arch.tag !== 'search') {
                throw new Error(_.str.sprintf(
                    "Got non-search view after asking for a search view: type %s, arch root %s",
                    data.fields_view.type, data.fields_view.arch.tag));
        }

        this.make_widgets(
            data.fields_view['arch'].children,
            data.fields_view.fields);

        // load defaults
        return $.when(
                this.setup_stuff_drawer(),
                $.when.apply(null, _(this.inputs).invoke('facet_for_defaults', this.defaults))
                    .then(function () { self.vs.searchQuery.reset(_(arguments).compact()); }))
            .then(function () { self.ready.resolve(); })
    },
    /**
     * Handle event when the user make a selection in the filters management select box.
     */
    on_filters_management: function(e) {
        var self = this;
        var select = this.$element.find(".oe_search-view-filters-management");
        var val = select.val();
        switch(val) {
        case 'advanced_filter':
            this.extended_search.on_activate();
            break;
        case 'add_to_dashboard':
            this.on_add_to_dashboard();
            break;
        case 'manage_filters':
            this.do_action({
                res_model: 'ir.filters',
                views: [[false, 'list'], [false, 'form']],
                type: 'ir.actions.act_window',
                context: {"search_default_user_id": this.session.uid,
                "search_default_model_id": this.dataset.model},
                target: "current",
                limit : 80
            });
            break;
        case 'save_filter':
            var data = this.build_search_data();
            var context = new openerp.web.CompoundContext();
            _.each(data.contexts, function(x) {
                context.add(x);
            });
            var domain = new openerp.web.CompoundDomain();
            _.each(data.domains, function(x) {
                domain.add(x);
            });
            var groupbys = _.pluck(data.groupbys, "group_by").join();
            context.add({"group_by": groupbys});
            var dial_html = QWeb.render("SearchView.managed-filters.add");
            var $dial = $(dial_html);
            openerp.web.dialog($dial, {
                modal: true,
                title: _t("Filter Entry"),
                buttons: [
                    {text: _t("Cancel"), click: function() {
                        $(this).dialog("close");
                    }},
                    {text: _t("OK"), click: function() {
                        $(this).dialog("close");
                        var name = $(this).find("input").val();
                        self.rpc('/web/searchview/save_filter', {
                            model: self.dataset.model,
                            context_to_save: context,
                            domain: domain,
                            name: name
                        }).then(function() {
                            self.reload_managed_filters();
                        });
                    }}
                ]
            });
            break;
        case '':
            this.do_clear();
        }
        if (val.slice(0, 4) == "get:") {
            val = val.slice(4);
            val = parseInt(val, 10);
            var filter = this.managed_filters[val];
            this.do_clear(false).then(_.bind(function() {
                select.val('get:' + val);

                var groupbys = [];
                var group_by = filter.context.group_by;
                if (group_by) {
                    groupbys = _.map(
                        group_by instanceof Array ? group_by : group_by.split(','),
                        function (el) { return { group_by: el }; });
                }
                this.filter_data = {
                    domains: [filter.domain],
                    contexts: [filter.context],
                    groupbys: groupbys
                };
                this.do_search();
            }, this));
        } else {
            select.val('');
        }
    },
    on_add_to_dashboard: function() {
        this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0;
        var self = this,
            menu = openerp.webclient.menu,
            $dialog = $(QWeb.render("SearchView.add_to_dashboard", {
                dashboards : menu.data.data.children,
                selected_menu_id : menu.$element.find('a.active').data('menu')
            }));
        $dialog.find('input').val(this.fields_view.name);
        openerp.web.dialog($dialog, {
            modal: true,
            title: _t("Add to Dashboard"),
            buttons: [
                {text: _t("Cancel"), click: function() {
                    $(this).dialog("close");
                }},
                {text: _t("OK"), click: function() {
                    $(this).dialog("close");
                    var menu_id = $(this).find("select").val(),
                        title = $(this).find("input").val(),
                        data = self.build_search_data(),
                        context = new openerp.web.CompoundContext(),
                        domain = new openerp.web.CompoundDomain();
                    _.each(data.contexts, function(x) {
                        context.add(x);
                    });
                    _.each(data.domains, function(x) {
                           domain.add(x);
                    });
                    self.rpc('/web/searchview/add_to_dashboard', {
                        menu_id: menu_id,
                        action_id: self.getParent().action.id,
                        context_to_save: context,
                        domain: domain,
                        view_mode: self.getParent().active_view,
                        name: title
                    }, function(r) {
                        if (r === false) {
                            self.do_warn("Could not add filter to dashboard");
                        } else {
                            self.do_notify("Filter added to dashboard", '');
                        }
                    });
                }}
            ]
        });
    },
    /**
     * Performs the search view collection of widget data.
     *
     * If the collection went well (all fields are valid), then triggers
     * :js:func:`openerp.web.SearchView.on_search`.
     *
     * If at least one field failed its validation, triggers
     * :js:func:`openerp.web.SearchView.on_invalid` instead.
     *
     * @param e jQuery event object coming from the "Search" button
     */
    do_search: function () {
        var domains = [], contexts = [], groupbys = [], errors = [];

        this.vs.searchQuery.each(function (facet) {
            var field = facet.get('field');
            try {
                var domain = field.get_domain(facet);
                if (domain) {
                    domains.push(domain);
                }
                var context = field.get_context(facet);
                if (context) {
                    contexts.push(context);
                    groupbys.push(context);
                }
            } catch (e) {
                if (e instanceof openerp.web.search.Invalid) {
                    errors.push(e);
                } else {
                    throw e;
                }
            }
        });

        if (!_.isEmpty(errors)) {
            this.on_invalid(errors);
            return;
        }
        return this.on_search(domains, contexts, groupbys);
    },
    /**
     * Triggered after the SearchView has collected all relevant domains and
     * contexts.
     *
     * It is provided with an Array of domains and an Array of contexts, which
     * may or may not be evaluated (each item can be either a valid domain or
     * context, or a string to evaluate in order in the sequence)
     *
     * It is also passed an array of contexts used for group_by (they are in
     * the correct order for group_by evaluation, which contexts may not be)
     *
     * @event
     * @param {Array} domains an array of literal domains or domain references
     * @param {Array} contexts an array of literal contexts or context refs
     * @param {Array} groupbys ordered contexts which may or may not have group_by keys
     */
    on_search: function (domains, contexts, groupbys) {
    },
    /**
     * Triggered after a validation error in the SearchView fields.
     *
     * Error objects have three keys:
     * * ``field`` is the name of the invalid field
     * * ``value`` is the invalid value
     * * ``message`` is the (in)validation message provided by the field
     *
     * @event
     * @param {Array} errors a never-empty array of error objects
     */
    on_invalid: function (errors) {
        this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
    }
});

/** @namespace */
openerp.web.search = {};

openerp.web.search.FilterGroupFacet = VS.ui.SearchFacet.extend({
    events: _.extend({
        'click': 'selectFacet'
    }, VS.ui.SearchFacet.prototype.events),

    render: function () {
        this.setMode('not', 'editing');
        this.setMode('not', 'selected');

        var value = this.model.get('value');
        this.$el.html(QWeb.render('SearchView.filters.facet', {
            facet: this.model
        }));
        // virtual input so SearchFacet code has something to play with
        this.box = $('<input>').val(value);

        return this;
    }
});
/**
 * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
 * find and instantiate its field widgets.
 */
openerp.web.search.fields = new openerp.web.Registry({
    'char': 'openerp.web.search.CharField',
    'text': 'openerp.web.search.CharField',
    'boolean': 'openerp.web.search.BooleanField',
    'integer': 'openerp.web.search.IntegerField',
    'id': 'openerp.web.search.IntegerField',
    'float': 'openerp.web.search.FloatField',
    'selection': 'openerp.web.search.SelectionField',<commit_msg>[FIX] keyboard handling around read-only facets<commit_after>openerp.web.search = function(openerp) {
var QWeb = openerp.web.qweb,
      _t =  openerp.web._t,
     _lt = openerp.web._lt;
_.mixin({
    sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
});

// Have SearchBox optionally use callback function to produce inputs and facets
// (views) set on callbacks.make_facet and callbacks.make_input keys when
// initializing VisualSearch
var SearchBox_renderFacet = function (facet, position) {
    var view = new (this.app.options.callbacks['make_facet'] || VS.ui.SearchFacet)({
      app   : this.app,
      model : facet,
      order : position
    });

    // Input first, facet second.
    this.renderSearchInput();
    this.facetViews.push(view);
    this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);

    view.calculateSize();
    _.defer(_.bind(view.calculateSize, view));

    return view;
  }; // warning: will not match
// Ensure we're replacing the function we think
if (SearchBox_renderFacet.toString() !== VS.ui.SearchBox.prototype.renderFacet.toString().replace(/(VS\.ui\.SearchFacet)/, "(this.app.options.callbacks['make_facet'] || $1)")) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#renderFacet. "
        + "Please fix replacement.");
}
var SearchBox_renderSearchInput = function () {
    var input = new (this.app.options.callbacks['make_input'] || VS.ui.SearchInput)({position: this.inputViews.length, app: this.app});
    this.$('.VS-search-inner').append(input.render().el);
    this.inputViews.push(input);
  };
// Ensure we're replacing the function we think
if (SearchBox_renderSearchInput.toString() !== VS.ui.SearchBox.prototype.renderSearchInput.toString().replace(/(VS\.ui\.SearchInput)/, "(this.app.options.callbacks['make_input'] || $1)")) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#renderSearchInput. "
        + "Please fix replacement.");
}
var SearchBox_searchEvent = function (e) {
    var query = null;
    this.renderFacets();
    this.focusSearch(e);
    this.app.options.callbacks.search(query, this.app.searchQuery);
  };
if (SearchBox_searchEvent.toString() !== VS.ui.SearchBox.prototype.searchEvent.toString().replace(
        /this\.value\(\);\n[ ]{4}this\.focusSearch\(e\);\n[ ]{4}this\.value\(query\)/,
        'null;\n    this.renderFacets();\n    this.focusSearch(e)')) {
    throw new Error(
        "Trying to replace wrong version of VS.ui.SearchBox#searchEvent. "
        + "Please fix replacement.");
}
_.extend(VS.ui.SearchBox.prototype, {
    renderFacet: SearchBox_renderFacet,
    renderSearchInput: SearchBox_renderSearchInput,
    searchEvent: SearchBox_searchEvent
});
_.extend(VS.model.SearchFacet.prototype, {
    value: function () {
        if (this.has('json')) {
            return this.get('json');
        }
        return this.get('value');
    }
});

openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
    template: "SearchView",
    /**
     * @constructs openerp.web.SearchView
     * @extends openerp.web.OldWidget
     *
     * @param parent
     * @param element_id
     * @param dataset
     * @param view_id
     * @param defaults
     */
    init: function(parent, dataset, view_id, defaults, hidden) {
        this._super(parent);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.defaults = defaults || {};
        this.has_defaults = !_.isEmpty(this.defaults);

        this.inputs = [];
        this.controls = {};

        this.hidden = !!hidden;
        this.headless = this.hidden && !this.has_defaults;

        this.filter_data = {};

        this.ready = $.Deferred();
    },
    start: function() {
        var self = this;
        var p = this._super();

        this.setup_global_completion();
        this.vs = VS.init({
            container: this.$element,
            query: '',
            callbacks: {
                make_facet: this.proxy('make_visualsearch_facet'),
                make_input: this.proxy('make_visualsearch_input'),
                search: function (query, searchCollection) {
                    self.do_search();
                },
                facetMatches: function (callback) {
                },
                valueMatches : function(facet, searchTerm, callback) {
                }
            }
        });

        var search = function () { self.vs.searchBox.searchEvent({}); };
        // searchQuery operations
        this.vs.searchQuery
            .off('add').on('add', search)
            .off('change').on('change', search)
            .off('reset').on('reset', search)
            .off('remove').on('remove', function (record, collection, options) {
                if (options['trigger_search']) {
                    search();
                }
            });

        if (this.hidden) {
            this.$element.hide();
        }
        if (this.headless) {
            this.ready.resolve();
        } else {
            var load_view = this.rpc("/web/searchview/load", {
                model: this.model,
                view_id: this.view_id,
                context: this.dataset.get_context() });
            // FIXME: local eval of domain and context to get rid of special endpoint
            var filters = this.rpc('/web/searchview/get_filters', {
                model: this.model
            }).then(function (filters) { self.custom_filters = filters; });

            $.when(load_view, filters)
                .pipe(function (load) { return load[0]; })
                .then(this.on_loaded);
        }

        this.$element.on('click', '.oe_vs_unfold_drawer', function () {
            self.$element.toggleClass('oe_searchview_open_drawer');
        });

        return $.when(p, this.ready);
    },
    show: function () {
        this.$element.show();
    },
    hide: function () {
        this.$element.hide();
    },

    /**
     * Sets up thingie where all the mess is put?
     */
    setup_stuff_drawer: function () {
        var self = this;
        $('<div class="oe_vs_unfold_drawer">').appendTo(this.$element.find('.VS-search-box'));
        var $drawer = $('<div class="oe_searchview_drawer">').appendTo(this.$element);
        var $filters = $('<div class="oe_searchview_filters">').appendTo($drawer);

        var running_count = 0;
        // get total filters count
        var is_group = function (i) { return i instanceof openerp.web.search.FilterGroup; };
        var filters_count = _(this.controls).chain()
            .flatten()
            .filter(is_group)
            .map(function (i) { return i.filters.length; })
            .sum()
            .value();

        var col1 = [], col2 = _(this.controls).map(function (inputs, group) {
            var filters = _(inputs).filter(is_group);
            return {
                name: group === 'null' ? _t("Filters") : group,
                filters: filters,
                length: _(filters).chain().map(function (i) {
                    return i.filters.length; }).sum().value()
            };
        });

        while (col2.length) {
            // col1 + group should be smaller than col2 + group
            if ((running_count + col2[0].length) <= (filters_count - running_count)) {
                running_count += col2[0].length;
                col1.push(col2.shift());
            } else {
                break;
            }
        }

        // Create a Custom Filter FilterGroup for each custom filter read from
        // the db, add all of this as a group in the smallest column
        [].push.call(col1.length <= col2.length ? col1 : col2, {
            name: _t("Custom Filters"),
            filters: _.map(this.custom_filters, function (filter) {
                // FIXME: handling of ``disabled`` being set
                var f = new openerp.web.search.Filter({attrs: {
                    string: filter.name,
                    context: filter.context,
                    domain: filter.domain
                }}, self);
                return new openerp.web.search.FilterGroup([f], self);
            }),
            length: 3
        });

        return $.when(
            this.render_column(col1, $('<div>').appendTo($filters)),
            this.render_column(col2, $('<div>').appendTo($filters)),
            (new openerp.web.search.Advanced(this).appendTo($drawer)));
    },
    render_column: function (column, $el) {
        return $.when.apply(null, _(column).map(function (group) {
            $('<h3>').text(group.name).appendTo($el);
            return $.when.apply(null,
                _(group.filters).invoke('appendTo', $el));
        }));
    },
    /**
     * Sets up search view's view-wide auto-completion widget
     */
    setup_global_completion: function () {
        // Prevent keydown from within a facet's input from reaching the
        // auto-completion widget and opening the completion list
        this.$element.on('keydown', '.search_facet input', function (e) {
            e.stopImmediatePropagation();
        });

        this.$element.autocomplete({
            source: this.proxy('complete_global_search'),
            select: this.proxy('select_completion'),
            focus: function (e) { e.preventDefault(); },
            html: true,
            minLength: 0,
            delay: 0
        }).data('autocomplete')._renderItem = function (ul, item) {
            // item of completion list
            var $item = $( "<li></li>" )
                .data( "item.autocomplete", item )
                .appendTo( ul );

            if (item.value !== undefined) {
                // regular completion item
                return $item.append(
                    (item.label)
                        ? $('<a>').html(item.label)
                        : $('<a>').text(item.value));
            }
            return $item.text(item.category)
                .css({
                    borderTop: '1px solid #cccccc',
                    margin: 0,
                    padding: 0,
                    zoom: 1,
                    'float': 'left',
                    clear: 'left',
                    width: '100%'
                });
        }
    },
    /**
     * Provide auto-completion result for req.term (an array to `resp`)
     *
     * @param {Object} req request to complete
     * @param {String} req.term searched term to complete
     * @param {Function} resp response callback
     */
    complete_global_search:  function (req, resp) {
        $.when.apply(null, _(this.inputs).chain()
            .invoke('complete', req.term)
            .value()).then(function () {
                resp(_(_(arguments).compact()).flatten(true));
        });
    },

    /**
     * Action to perform in case of selection: create a facet (model)
     * and add it to the search collection
     *
     * @param {Object} e selection event, preventDefault to avoid setting value on object
     * @param {Object} ui selection information
     * @param {Object} ui.item selected completion item
     */
    select_completion: function (e, ui) {
        e.preventDefault();
        this.vs.searchQuery.add(new VS.model.SearchFacet(_.extend(
            {app: this.vs}, ui.item)));
        this.vs.searchBox.searchEvent({});
    },

    /**
     * Builds the right SearchFacet view based on the facet object to render
     * (e.g. readonly facets for filters)
     *
     * @param {Object} options
     * @param {VS.model.SearchFacet} options.model facet object to render
     */
    make_visualsearch_facet: function (options) {
        if (options.model.get('field') instanceof openerp.web.search.FilterGroup) {
            return new openerp.web.search.FilterGroupFacet(options);
        }
        return new VS.ui.SearchFacet(options);
    },
    /**
     * Proxies searches on a SearchInput to the search view's global completion
     *
     * Also disables SearchInput.autocomplete#_move so search view's
     * autocomplete can get the corresponding events, or something.
     *
     * @param options
     */
    make_visualsearch_input: function (options) {
        var self = this, input = new VS.ui.SearchInput(options);
        input.setupAutocomplete = function () {
            _.extend(this.box.autocomplete({
                minLength: 1,
                delay: 0,
                search: function () {
                    self.$element.autocomplete('search', input.box.val());
                    return false;
                }
            }).data('autocomplete'), {
                _move: function () {},
                close: function () { self.$element.autocomplete('close'); }
            });
        };
        return input;
    },

    /**
     * Builds a list of widget rows (each row is an array of widgets)
     *
     * @param {Array} items a list of nodes to convert to widgets
     * @param {Object} fields a mapping of field names to (ORM) field attributes
     * @param {String} [group_name] name of the group to put the new controls in
     */
    make_widgets: function (items, fields, group_name) {
        group_name = group_name || null;
        if (!(group_name in this.controls)) {
            this.controls[group_name] = [];
        }
        var self = this, group = this.controls[group_name];
        var filters = [];
        _.each(items, function (item) {
            if (filters.length && item.tag !== 'filter') {
                group.push(new openerp.web.search.FilterGroup(filters, this));
                filters = [];
            }

            switch (item.tag) {
            case 'separator': case 'newline':
                break;
            case 'filter':
                filters.push(new openerp.web.search.Filter(item, this));
                break;
            case 'group':
                self.make_widgets(item.children, fields, item.attrs.string);
                break;
            case 'field':
                group.push(this.make_field(item, fields[item['attrs'].name]));
                // filters
                self.make_widgets(item.children, fields, group_name);
                break;
            }
        }, this);

        if (filters.length) {
            group.push(new openerp.web.search.FilterGroup(filters, this));
        }
    },
    /**
     * Creates a field for the provided field descriptor item (which comes
     * from fields_view_get)
     *
     * @param {Object} item fields_view_get node for the field
     * @param {Object} field fields_get result for the field
     * @returns openerp.web.search.Field
     */
    make_field: function (item, field) {
        try {
            return new (openerp.web.search.fields.get_any(
                    [item.attrs.widget, field.type]))
                (item, field, this);
        } catch (e) {
            if (! e instanceof openerp.web.KeyNotFound) {
                throw e;
            }
            // KeyNotFound means unknown field type
            console.group('Unknown field type ' + field.type);
            console.error('View node', item);
            console.info('View field', field);
            console.info('In view', this);
            console.groupEnd();
            return null;
        }
    },
    on_loaded: function(data) {
        var self = this;
        this.fields_view = data.fields_view;
        if (data.fields_view.type !== 'search' ||
            data.fields_view.arch.tag !== 'search') {
                throw new Error(_.str.sprintf(
                    "Got non-search view after asking for a search view: type %s, arch root %s",
                    data.fields_view.type, data.fields_view.arch.tag));
        }

        this.make_widgets(
            data.fields_view['arch'].children,
            data.fields_view.fields);

        // load defaults
        return $.when(
                this.setup_stuff_drawer(),
                $.when.apply(null, _(this.inputs).invoke('facet_for_defaults', this.defaults))
                    .then(function () { self.vs.searchQuery.reset(_(arguments).compact()); }))
            .then(function () { self.ready.resolve(); })
    },
    /**
     * Handle event when the user make a selection in the filters management select box.
     */
    on_filters_management: function(e) {
        var self = this;
        var select = this.$element.find(".oe_search-view-filters-management");
        var val = select.val();
        switch(val) {
        case 'advanced_filter':
            this.extended_search.on_activate();
            break;
        case 'add_to_dashboard':
            this.on_add_to_dashboard();
            break;
        case 'manage_filters':
            this.do_action({
                res_model: 'ir.filters',
                views: [[false, 'list'], [false, 'form']],
                type: 'ir.actions.act_window',
                context: {"search_default_user_id": this.session.uid,
                "search_default_model_id": this.dataset.model},
                target: "current",
                limit : 80
            });
            break;
        case 'save_filter':
            var data = this.build_search_data();
            var context = new openerp.web.CompoundContext();
            _.each(data.contexts, function(x) {
                context.add(x);
            });
            var domain = new openerp.web.CompoundDomain();
            _.each(data.domains, function(x) {
                domain.add(x);
            });
            var groupbys = _.pluck(data.groupbys, "group_by").join();
            context.add({"group_by": groupbys});
            var dial_html = QWeb.render("SearchView.managed-filters.add");
            var $dial = $(dial_html);
            openerp.web.dialog($dial, {
                modal: true,
                title: _t("Filter Entry"),
                buttons: [
                    {text: _t("Cancel"), click: function() {
                        $(this).dialog("close");
                    }},
                    {text: _t("OK"), click: function() {
                        $(this).dialog("close");
                        var name = $(this).find("input").val();
                        self.rpc('/web/searchview/save_filter', {
                            model: self.dataset.model,
                            context_to_save: context,
                            domain: domain,
                            name: name
                        }).then(function() {
                            self.reload_managed_filters();
                        });
                    }}
                ]
            });
            break;
        case '':
            this.do_clear();
        }
        if (val.slice(0, 4) == "get:") {
            val = val.slice(4);
            val = parseInt(val, 10);
            var filter = this.managed_filters[val];
            this.do_clear(false).then(_.bind(function() {
                select.val('get:' + val);

                var groupbys = [];
                var group_by = filter.context.group_by;
                if (group_by) {
                    groupbys = _.map(
                        group_by instanceof Array ? group_by : group_by.split(','),
                        function (el) { return { group_by: el }; });
                }
                this.filter_data = {
                    domains: [filter.domain],
                    contexts: [filter.context],
                    groupbys: groupbys
                };
                this.do_search();
            }, this));
        } else {
            select.val('');
        }
    },
    on_add_to_dashboard: function() {
        this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0;
        var self = this,
            menu = openerp.webclient.menu,
            $dialog = $(QWeb.render("SearchView.add_to_dashboard", {
                dashboards : menu.data.data.children,
                selected_menu_id : menu.$element.find('a.active').data('menu')
            }));
        $dialog.find('input').val(this.fields_view.name);
        openerp.web.dialog($dialog, {
            modal: true,
            title: _t("Add to Dashboard"),
            buttons: [
                {text: _t("Cancel"), click: function() {
                    $(this).dialog("close");
                }},
                {text: _t("OK"), click: function() {
                    $(this).dialog("close");
                    var menu_id = $(this).find("select").val(),
                        title = $(this).find("input").val(),
                        data = self.build_search_data(),
                        context = new openerp.web.CompoundContext(),
                        domain = new openerp.web.CompoundDomain();
                    _.each(data.contexts, function(x) {
                        context.add(x);
                    });
                    _.each(data.domains, function(x) {
                           domain.add(x);
                    });
                    self.rpc('/web/searchview/add_to_dashboard', {
                        menu_id: menu_id,
                        action_id: self.getParent().action.id,
                        context_to_save: context,
                        domain: domain,
                        view_mode: self.getParent().active_view,
                        name: title
                    }, function(r) {
                        if (r === false) {
                            self.do_warn("Could not add filter to dashboard");
                        } else {
                            self.do_notify("Filter added to dashboard", '');
                        }
                    });
                }}
            ]
        });
    },
    /**
     * Performs the search view collection of widget data.
     *
     * If the collection went well (all fields are valid), then triggers
     * :js:func:`openerp.web.SearchView.on_search`.
     *
     * If at least one field failed its validation, triggers
     * :js:func:`openerp.web.SearchView.on_invalid` instead.
     *
     * @param e jQuery event object coming from the "Search" button
     */
    do_search: function () {
        var domains = [], contexts = [], groupbys = [], errors = [];

        this.vs.searchQuery.each(function (facet) {
            var field = facet.get('field');
            try {
                var domain = field.get_domain(facet);
                if (domain) {
                    domains.push(domain);
                }
                var context = field.get_context(facet);
                if (context) {
                    contexts.push(context);
                    groupbys.push(context);
                }
            } catch (e) {
                if (e instanceof openerp.web.search.Invalid) {
                    errors.push(e);
                } else {
                    throw e;
                }
            }
        });

        if (!_.isEmpty(errors)) {
            this.on_invalid(errors);
            return;
        }
        return this.on_search(domains, contexts, groupbys);
    },
    /**
     * Triggered after the SearchView has collected all relevant domains and
     * contexts.
     *
     * It is provided with an Array of domains and an Array of contexts, which
     * may or may not be evaluated (each item can be either a valid domain or
     * context, or a string to evaluate in order in the sequence)
     *
     * It is also passed an array of contexts used for group_by (they are in
     * the correct order for group_by evaluation, which contexts may not be)
     *
     * @event
     * @param {Array} domains an array of literal domains or domain references
     * @param {Array} contexts an array of literal contexts or context refs
     * @param {Array} groupbys ordered contexts which may or may not have group_by keys
     */
    on_search: function (domains, contexts, groupbys) {
    },
    /**
     * Triggered after a validation error in the SearchView fields.
     *
     * Error objects have three keys:
     * * ``field`` is the name of the invalid field
     * * ``value`` is the invalid value
     * * ``message`` is the (in)validation message provided by the field
     *
     * @event
     * @param {Array} errors a never-empty array of error objects
     */
    on_invalid: function (errors) {
        this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
    }
});

/** @namespace */
openerp.web.search = {};

openerp.web.search.FilterGroupFacet = VS.ui.SearchFacet.extend({
    events: _.extend({
        'click': 'selectFacet'
    }, VS.ui.SearchFacet.prototype.events),

    render: function () {
        this.setMode('not', 'editing');
        this.setMode('not', 'selected');

        var value = this.model.get('value');
        this.$el.html(QWeb.render('SearchView.filters.facet', {
            facet: this.model
        }));
        // virtual input so SearchFacet code has something to play with
        this.box = $('<input>').val(value);

        return this;
    },
    enableEdit: function () {
        this.selectFacet()
    },
    keydown: function (e) {
        var key = VS.app.hotkeys.key(e);
        if (key !== 'right') {
            return VS.ui.SearchFacet.prototype.keydown.call(this, e);
        }
        e.preventDefault();
        this.deselectFacet();
        this.options.app.searchBox.focusNextFacet(this, 1);
    }
});
/**
 * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
 * find and instantiate its field widgets.
 */
openerp.web.search.fields = new openerp.web.Registry({
    'char': 'openerp.web.search.CharField',
    'text': 'openerp.web.search.CharField',
    'boolean': 'openerp.web.search.BooleanField',
    'integer': 'openerp.web.search.IntegerField',
    'id': 'openerp.web.search.IntegerField',
    'float': 'openerp.web.search.FloatField',
    'selection': 'openerp.web.search.SelectionField',
{'fraction_non_alphanumeric': 0.13046183762761304, 'fraction_numerical': 0.0018473505104521148, 'mean_word_length': 1.7009453781512605, 'pattern_counts': {'":': 6, '<': 29, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3810988', 'n_tokens_mistral': 14442, 'n_tokens_neox': 13767, 'n_words': 3219}