<commit_before>/*---------------------------------------------------------
 * OpenERP base library
 *---------------------------------------------------------*/

openerp.base$views = function(openerp) {

// process all kind of actions
openerp.base.ActionManager = openerp.base.Controller.extend({
    init: function(session, element_id) {
        this._super(session, element_id);
        this.action = null;
        this.viewmanager = null;
    },
    /**
     * Process an action
     * Supported actions: act_window
     */
    do_action: function(action) {
        // instantiate the right controllers by understanding the action
        this.action = action;
        if(action.type == "ir.actions.act_window") {
            this.viewmanager = new openerp.base.ViewManager(this.session,this.element_id);
            this.viewmanager.do_action_window(action);
            this.viewmanager.start();
        }
    }
});

// This will be ViewManager Abstract/Common
openerp.base.ViewManager =  openerp.base.Controller.extend({
    init: function(session, element_id) {
        this._super(session, element_id);
        this.action = null;
        this.dataset = null;
        this.searchview_id = false;
        this.searchview = null;
        this.search_visible = true;
        // this.views = { "list": { "view_id":1234, "controller": instance} }
        this.views = {};
    },
    start: function() {
    },
    on_mode_switch: function(view_type) {
        for (var i in this.views) { 
           if(i == view_type) {
               this.views[i].controller.$element.show();
           } else {
               this.views[i].controller.$element.hide();
           }
        }
    },
    do_action_window: function(action) {
        var self = this;
        var prefix_id = "#" + this.element_id;
        this.action = action;
        this.dataset = new openerp.base.DataSet(this.session, action.res_model);
        this.dataset.start();

        this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: action.views}));

        this.searchview_id = false;
        if(this.search_visible && action.search_view_id) {
            this.searchview_id = action.search_view_id[0];
            this.searchview = new openerp.base.SearchView(this.session, this.element_id + "_search", this.dataset, this.searchview_id);
            this.searchview.start();
        }
        for(var i = 0; i < action.views.length; i++)  {
            var view_id, controller;
            view_id = action.views[i][0];
            if(action.views[i][1] == "tree") {
                controller = new openerp.base.ListView(this.session, this.element_id + "_view_tree", this.dataset, view_id);
                controller.start();
                this.views.tree = { view_id: view_id, controller: controller };
                this.$element.find(prefix_id + "_button_tree").bind('click',function(){
                    self.on_mode_switch("tree");
                });
            } else if(action.views[i][1] == "form") {
                controller = new openerp.base.FormView(this.session, this.element_id + "_view_form", this.dataset, view_id);
                controller.start();
                this.views.form = { view_id: view_id, controller: controller };
                this.$element.find(prefix_id + "_button_form").bind('click',function(){
                   self.on_mode_switch("form");
                });
            }
        }
        // switch to the first one in sequence
        this.on_mode_switch("tree");
    },
    // create when root, also add to parent when o2m
    on_create: function() {
    },
    on_remove: function() {
    },
    on_edit: function() {
    }
});

// Extends view manager
openerp.base.ViewManagerRoot = openerp.base.Controller.extend({
});

// Extends view manager
openerp.base.ViewManagerUsedAsAMany2One = openerp.base.Controller.extend({
});

/**
 * Management interface between views and the collection of selected OpenERP
 * records (represents the view's state?)
 */
openerp.base.DataSet =  openerp.base.Controller.extend({
    init: function(session, model) {
        this._super(session);
        this.model = model;

        this._fields = null;

        this._ids = [];
        this._active_ids = null;
        this._active_id_index = 0;

        this._sort = [];
        this._domain = [];
        this._context = {};
    },
    start: function() {
        // TODO: fields_view_get fields selection?
        this.rpc("/base/dataset/fields", {"model":this.model}, this.on_fields);
    },
    on_fields: function(result) {
        this._fields = result._fields;
        this.on_ready();
    },

    /**
     * Fetch all the records selected by this DataSet, based on its domain
     * and context.
     *
     * Fires the on_ids event.
     *
     * @param {Number} [offset=0] The index from which selected records should be returned
     * @param {Number} [limit=null] The maximum number of records to return
     * @returns itself
     */
    fetch: function (offset, limit) {
        offset = offset || 0;
        limit = limit || null;
        this.rpc('/base/dataset/find', {
            model: this.model,
            fields: this._fields,
            domain: this._domain,
            context: this._context,
            sort: this._sort,
            offset: offset,
            limit: limit
        }, _.bind(function (records) {
            var data_records = _.map(
                records, function (record) {
                    return new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record);
                }, this);

            this.on_fetch(data_records, {
                offset: offset,
                limit: limit,
                domain: this._domain,
                context: this._context,
                sort: this._sort
            });
        }, this));
        return this;
    },
    /**
     * @event
     *
     * Fires after the DataSet fetched the records matching its internal ids selection
     * 
     * @param {Array} records An array of the DataRecord fetched
     * @param event The on_fetch event object
     * @param {Number} event.offset the offset with which the original DataSet#fetch call was performed
     * @param {Number} event.limit the limit set on the original DataSet#fetch call
     * @param {Array} event.domain the domain set on the DataSet before DataSet#fetch was called
     * @param {Object} event.context the context set on the DataSet before DataSet#fetch was called
     * @param {Array} event.sort the sorting criteria used to get the ids
     */
    on_fetch: function (records, event) { },

    /**
     * Fetch all the currently active records for this DataSet (records selected via DataSet#select)
     *
     * @returns itself
     */
    active_ids: function () {
        this.rpc('/base/dataset/get', {
            ids: this.get_active_ids(),
            model: this.model
        }, _.bind(function (records) {
            this.on_active_ids(_.map(
                records, function (record) {
                    return new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record);
                }, this));
        }, this));
        return this;
    },
    /**
     * @event
     *
     * Fires after the DataSet fetched the records matching its internal active ids selection
     *
     * @param {Array} records An array of the DataRecord fetched
     */
    on_active_ids: function (records) { },

    /**
     * Fetches the current active record for this DataSet
     *
     * @returns itself
     */
    active_id: function () {
        this.rpc('/base/dataset/get', {
            ids: [this.get_active_id()],
            model: this.model
        }, _.bind(function (records) {
            var record = records[0];
            this.on_active_id(
                record && new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record));
        }, this));
        return this;
    },
    /**
     * Fires after the DataSet fetched the record matching the current active record
     *
     * @param record the record matching the provided id, or null if there is no record for this id
     */
    on_active_id: function (record) {

    },

    /**
     * Configures the DataSet
     * 
     * @param options DataSet options
     * @param {Array} options.domain the domain to assign to this DataSet for filtering
     * @param {Object} options.context the context this DataSet should use during its calls
     * @param {Array} options.sort the sorting criteria for this DataSet
     * @returns itself
     */
    set: function (options) {
        if (options.domain) {
            this._domain = _.clone(options.domain);
        }
        if (options.context) {
            this._context = _.clone(options.context);
        }
        if (options.sort) {
            this._sort = _.clone(options.sort);
        }
        return this;
    },

    /**
     * Activates the previous id in the active sequence. If there is no previous id, wraps around to the last one
     * @returns itself
     */
    prev: function () {
        this._active_id_index -= 1;
        if (this._active_id_index < 0) {
            this._active_id_index = this._active_ids.length - 1;
        }
        return this;
    },
    /**
     * Activates the next id in the active sequence. If there is no next id, wraps around to the first one
     * @returns itself
     */
    next: function () {
        this._active_id_index += 1;
        if (this._active_id_index >= this._active_ids.length) {
            this._active_id_index = 0;
        }
        return this;
    },

    /**
     * Sets active_ids by value:
     *
     * * Activates all ids part of the current selection
     * * Sets active_id to be the first id of the selection
     *
     * @param {Array} ids the list of ids to activate
     * @returns itself
     */
    select: function (ids) {
        this._active_ids = ids;
        this._active_id_index = 0;
        return this;
    },
    /**
     * Fetches the ids of the currently selected records, if any.
     */
    get_active_ids: function () {
        return this._active_ids;
    },
    /**
     * Sets the current active_id by value
     *
     * If there are no active_ids selected, selects the provided id as the sole active_id
     *
     * If there are ids selected and the provided id is not in them, raise an error
     *
     * @param {Object} id the id to activate
     * @returns itself
     */
    activate: function (id) {
        if(!this._active_ids) {
            this._active_ids = [id];
            this._active_id_index = 0;
        } else {
            var index = _.indexOf(this._active_ids, id);
            if (index == -1) {
                throw new Error(
                    "Could not find id " + id +
                    " in array [" + this._active_ids.join(', ') + "]");
            }
            this._active_id_index = index;
        }
        return this;
    },
    /**
     * Fetches the id of the current active record, if any.
     *
     * @returns record? record id or <code>null</code>
     */
    get_active_id: function () {
        if (!this._active_ids) {
            return null;
        }
        return this._active_ids[this._active_id_index];
    }
});

openerp.base.DataRecord =  openerp.base.Controller.extend({
    init: function(session, model, fields, values) {
        this._super(session, null);
        this.model = model;
        this.id = values.id || null;
        this.fields = fields;
        this.values = values;
    },
    on_change: function() {
    },
    on_reload: function() {
    }
});

openerp.base.SearchView = openerp.base.Controller.extend({
    init: function(session, element_id, dataset, view_id) {
        this._super(session, element_id);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.inputs = [];
    },
    start: function() {
        //this.log('Starting SearchView '+this.model+this.view_id)
        this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
    },
    /**
     * 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
     * @returns Array
     */
    make_widgets: function (items, fields) {
        var rows = [],
            row = [];
        rows.push(row);
        var filters = [];
        _.each(items, function (item) {
            if (filters.length && item.tag !== 'filter') {
                row.push(
                    new openerp.base.search.FilterGroup(
                        filters, this));
                filters = [];
            }

            if (item.tag === 'newline') {
                row = [];
                rows.push(row);
            } else if (item.tag === 'filter') {
                filters.push(
                    new openerp.base.search.Filter(
                        item, this));
            } else if (item.tag === 'separator') {
                // a separator is a no-op
            } else {
                if (item.tag === 'group') {
                    // TODO: group and field should be fetched from registries, maybe even filters
                    row.push(
                        new openerp.base.search.Group(
                            item, this, fields));
                } else if (item.tag === 'field') {
                    row.push(
                        this.make_field(
                            item, fields[item['attrs'].name]));
                }
            }
        }, this);
        if (filters.length) {
            row.push(new openerp.base.search.FilterGroup(filters, this));
        }

        return rows;
    },
    /**
     * 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.base.search.Field
     */
    make_field: function (item, field) {
        // TODO: should fetch from an actual registry
        // TODO: register fields in self?
        switch (field.type) {
            case 'char':
                return new openerp.base.search.CharField(
                    item, field, this);
            case 'float':
                return new openerp.base.search.FloatField(
                    item, field, this);
            case 'datetime':
                return new openerp.base.search.DateTimeField(
                    item, field, this);
            case 'one2many':
                return new openerp.base.search.OneToManyField(
                    item, field, this);
            case 'many2one':
                return new openerp.base.search.ManyToOneField(
                    item, field, this);
            default:
                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 lines = this.make_widgets(
            data.fields_view['arch'].children,
            data.fields_view.fields);

        // TODO: get default values
        var default_values = {};

        var render = QWeb.render("SearchView", {
            'view': data.fields_view['arch'],
            'lines': lines,
            'defaults': default_values
        });
        this.$element.html(render);

        this.$element.find('form')
                .submit(this.on_search)
                .bind('reset', this.on_reset);
        setTimeout(function () {
            // start() all the widgets
            _(lines).chain().flatten().each(function (widget) {
                widget.start();
            });
        }, 0);
    },
    on_search: function (e) {
        e.preventDefault();
        var inputs_ = _(this.inputs);

        var domains = inputs_.chain().
            map(function (input) { return input.get_domain(); }).
            compact().
            value();
        var contexts = inputs_.chain()
            .map(function (input) { return input.get_context(); })
            .compact()
            .value();
        console.log('domains', domains);
        console.log('contexts', contexts);
    },
    on_reset: function (e) {
        e.preventDefault();
        console.log('on_reset');
    }
});

openerp.base.search = {};
openerp.base.search.Invalid = Class.extend({
    init: function (field, value, message) {
        this.field = field;
        this.value = value;
        this.message = message;
    },
    toString: function () {
        return ('Incorrect value for field ' + this.field +
                ': [' + this.value + '] is ' + this.message);
    }
});
openerp.base.search.Widget = openerp.base.Controller.extend({
    template: null,
    init: function (view) {
        this.view = view;
    },
    /**
     * Sets and returns a globally unique identifier for the widget.
     *
     * If a prefix is appended, the identifier will be appended to it.
     *
     * @params sections prefix sections, empty/falsy sections will be removed
     */
    make_id: function () {
        this.element_id = _.uniqueId(
            ['search'].concat(
                _.compact(_.toArray(arguments)),
                ['']).join('_'));
        return this.element_id;
    },
    /**
     * "Starts" the widgets. Called at the end of the rendering, this allows
     * widgets to hook themselves to their view sections.
     *
     * On widgets, if they kept a reference to a view and have an element_id,
     * will fetch and set their root element on $element.
     */
    start: function () {
        this._super();
        if (this.view && this.element_id) {
            // id is unique, and no getElementById on elements
            this.$element = $(document.getElementById(
                this.element_id));
        }
    },
    /**
     * "Stops" the widgets. Called when the view destroys itself, this
     * lets the widgets clean up after themselves.
     */
    stop: function () {
        delete this.view;
        this._super();
    },
    render: function (defaults) {
        return QWeb.render(
            this.template, _.extend(this, {
                defaults: defaults
        }));
    }
});
openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({
    template: 'SearchView.filters',
    init: function (filters, view) {
        this._super(view);
        this.filters = filters;
    },
    start: function () {
        this._super();
        _.each(this.filters, function (filter) {
            filter.start();
        });
    }
});
openerp.base.search.Group = openerp.base.search.Widget.extend({
    template: 'SearchView.group',
    // TODO: contain stuff
    // TODO: @expand
    init: function (view_section, view, fields) {
        this._super(view);
        this.attrs = view_section.attrs;
        this.lines = view.make_widgets(
            view_section.children, fields);
        this.make_id('group');
    },
    start: function () {
        this._super();
        _(this.lines)
            .chain()
            .flatten()
            .each(function (widget) { widget.start(); });
    }
});
openerp.base.search.Input = openerp.base.search.Widget.extend({
    init: function (view) {
        this._super(view);
        this.view.inputs.push(this);
    },
    get_context: function () {
        throw new Error(
            "get_context not implemented for widget " + this.attrs.type);
    },
    get_domain: function () {
        throw new Error(
            "get_domain not implemented for widget " + this.attrs.type);
    }
});
openerp.base.search.Filter = openerp.base.search.Input.extend({
    template: 'SearchView.filter',
    // TODO: force rendering
    init: function (node, view) {
        this._super(view);
        this.attrs = node.attrs;
        this.make_id('filter', this.attrs.name);
    },
    start: function () {
        this._super();
        var $view_form = this.view.$element.find('form');
        this.$element.click(function () {
            $(this).toggleClass('enabled');
            $view_form.submit();
        });
    },
    get_context: function () {
        if (!this.$element.hasClass('enabled')) {
            return;
        }
        return this.attrs.context;
    },
    get_domain: function () {
        if (!this.$element.hasClass('enabled')) {
            return;
        }
        return this.attrs.domain;
    }
});
openerp.base.search.Field = openerp.base.search.Input.extend({
    template: 'SearchView.field',
    default_operator: '=',
    // TODO: set default values
    // TODO: get context, domain
    // TODO: holds Filters
    init: function (view_section, field, view) {
        this._super(view);
        this.attrs = _.extend({}, field, view_section.attrs);
        this.filters = new openerp.base.search.FilterGroup(_.map(
            view_section.children, function (filter_node) {
                return new openerp.base.search.Filter(
                    filter_node, view);
        }), view);
        this.make_id('input', field.type, this.attrs.name);
    },
    start: function () {
        this._super();
        this.filters.start();
    },
    get_context: function () {
        if (!this.attrs.context) {
            return;
        }
        return this.attrs.context;
    },
    get_domain: function () {
        if (!this.attrs['filter_domain']) {
            return [
                this.attrs.name,
                this.attrs.operator || this.operator,
                this.get_value()
            ];
        }
        return this.attrs['filter_domain'];
    }
});
openerp.base.search.CharField = openerp.base.search.Field.extend({
    get_value: function () {
        return this.$element.val();
    }
});
openerp.base.search.FloatField = openerp.base.search.Field.extend({
    get_value: function () {
        var val = Number(this.$element.val());
        if (isNaN(val)) {<commit_msg>[FIX] it turns out the default operator is weirdly named 'default_operator', not 'die_walküre'. Weird<commit_after>/*---------------------------------------------------------
 * OpenERP base library
 *---------------------------------------------------------*/

openerp.base$views = function(openerp) {

// process all kind of actions
openerp.base.ActionManager = openerp.base.Controller.extend({
    init: function(session, element_id) {
        this._super(session, element_id);
        this.action = null;
        this.viewmanager = null;
    },
    /**
     * Process an action
     * Supported actions: act_window
     */
    do_action: function(action) {
        // instantiate the right controllers by understanding the action
        this.action = action;
        if(action.type == "ir.actions.act_window") {
            this.viewmanager = new openerp.base.ViewManager(this.session,this.element_id);
            this.viewmanager.do_action_window(action);
            this.viewmanager.start();
        }
    }
});

// This will be ViewManager Abstract/Common
openerp.base.ViewManager =  openerp.base.Controller.extend({
    init: function(session, element_id) {
        this._super(session, element_id);
        this.action = null;
        this.dataset = null;
        this.searchview_id = false;
        this.searchview = null;
        this.search_visible = true;
        // this.views = { "list": { "view_id":1234, "controller": instance} }
        this.views = {};
    },
    start: function() {
    },
    on_mode_switch: function(view_type) {
        for (var i in this.views) { 
           if(i == view_type) {
               this.views[i].controller.$element.show();
           } else {
               this.views[i].controller.$element.hide();
           }
        }
    },
    do_action_window: function(action) {
        var self = this;
        var prefix_id = "#" + this.element_id;
        this.action = action;
        this.dataset = new openerp.base.DataSet(this.session, action.res_model);
        this.dataset.start();

        this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: action.views}));

        this.searchview_id = false;
        if(this.search_visible && action.search_view_id) {
            this.searchview_id = action.search_view_id[0];
            this.searchview = new openerp.base.SearchView(this.session, this.element_id + "_search", this.dataset, this.searchview_id);
            this.searchview.start();
        }
        for(var i = 0; i < action.views.length; i++)  {
            var view_id, controller;
            view_id = action.views[i][0];
            if(action.views[i][1] == "tree") {
                controller = new openerp.base.ListView(this.session, this.element_id + "_view_tree", this.dataset, view_id);
                controller.start();
                this.views.tree = { view_id: view_id, controller: controller };
                this.$element.find(prefix_id + "_button_tree").bind('click',function(){
                    self.on_mode_switch("tree");
                });
            } else if(action.views[i][1] == "form") {
                controller = new openerp.base.FormView(this.session, this.element_id + "_view_form", this.dataset, view_id);
                controller.start();
                this.views.form = { view_id: view_id, controller: controller };
                this.$element.find(prefix_id + "_button_form").bind('click',function(){
                   self.on_mode_switch("form");
                });
            }
        }
        // switch to the first one in sequence
        this.on_mode_switch("tree");
    },
    // create when root, also add to parent when o2m
    on_create: function() {
    },
    on_remove: function() {
    },
    on_edit: function() {
    }
});

// Extends view manager
openerp.base.ViewManagerRoot = openerp.base.Controller.extend({
});

// Extends view manager
openerp.base.ViewManagerUsedAsAMany2One = openerp.base.Controller.extend({
});

/**
 * Management interface between views and the collection of selected OpenERP
 * records (represents the view's state?)
 */
openerp.base.DataSet =  openerp.base.Controller.extend({
    init: function(session, model) {
        this._super(session);
        this.model = model;

        this._fields = null;

        this._ids = [];
        this._active_ids = null;
        this._active_id_index = 0;

        this._sort = [];
        this._domain = [];
        this._context = {};
    },
    start: function() {
        // TODO: fields_view_get fields selection?
        this.rpc("/base/dataset/fields", {"model":this.model}, this.on_fields);
    },
    on_fields: function(result) {
        this._fields = result._fields;
        this.on_ready();
    },

    /**
     * Fetch all the records selected by this DataSet, based on its domain
     * and context.
     *
     * Fires the on_ids event.
     *
     * @param {Number} [offset=0] The index from which selected records should be returned
     * @param {Number} [limit=null] The maximum number of records to return
     * @returns itself
     */
    fetch: function (offset, limit) {
        offset = offset || 0;
        limit = limit || null;
        this.rpc('/base/dataset/find', {
            model: this.model,
            fields: this._fields,
            domain: this._domain,
            context: this._context,
            sort: this._sort,
            offset: offset,
            limit: limit
        }, _.bind(function (records) {
            var data_records = _.map(
                records, function (record) {
                    return new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record);
                }, this);

            this.on_fetch(data_records, {
                offset: offset,
                limit: limit,
                domain: this._domain,
                context: this._context,
                sort: this._sort
            });
        }, this));
        return this;
    },
    /**
     * @event
     *
     * Fires after the DataSet fetched the records matching its internal ids selection
     * 
     * @param {Array} records An array of the DataRecord fetched
     * @param event The on_fetch event object
     * @param {Number} event.offset the offset with which the original DataSet#fetch call was performed
     * @param {Number} event.limit the limit set on the original DataSet#fetch call
     * @param {Array} event.domain the domain set on the DataSet before DataSet#fetch was called
     * @param {Object} event.context the context set on the DataSet before DataSet#fetch was called
     * @param {Array} event.sort the sorting criteria used to get the ids
     */
    on_fetch: function (records, event) { },

    /**
     * Fetch all the currently active records for this DataSet (records selected via DataSet#select)
     *
     * @returns itself
     */
    active_ids: function () {
        this.rpc('/base/dataset/get', {
            ids: this.get_active_ids(),
            model: this.model
        }, _.bind(function (records) {
            this.on_active_ids(_.map(
                records, function (record) {
                    return new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record);
                }, this));
        }, this));
        return this;
    },
    /**
     * @event
     *
     * Fires after the DataSet fetched the records matching its internal active ids selection
     *
     * @param {Array} records An array of the DataRecord fetched
     */
    on_active_ids: function (records) { },

    /**
     * Fetches the current active record for this DataSet
     *
     * @returns itself
     */
    active_id: function () {
        this.rpc('/base/dataset/get', {
            ids: [this.get_active_id()],
            model: this.model
        }, _.bind(function (records) {
            var record = records[0];
            this.on_active_id(
                record && new openerp.base.DataRecord(
                        this.session, this.model,
                        this._fields, record));
        }, this));
        return this;
    },
    /**
     * Fires after the DataSet fetched the record matching the current active record
     *
     * @param record the record matching the provided id, or null if there is no record for this id
     */
    on_active_id: function (record) {

    },

    /**
     * Configures the DataSet
     * 
     * @param options DataSet options
     * @param {Array} options.domain the domain to assign to this DataSet for filtering
     * @param {Object} options.context the context this DataSet should use during its calls
     * @param {Array} options.sort the sorting criteria for this DataSet
     * @returns itself
     */
    set: function (options) {
        if (options.domain) {
            this._domain = _.clone(options.domain);
        }
        if (options.context) {
            this._context = _.clone(options.context);
        }
        if (options.sort) {
            this._sort = _.clone(options.sort);
        }
        return this;
    },

    /**
     * Activates the previous id in the active sequence. If there is no previous id, wraps around to the last one
     * @returns itself
     */
    prev: function () {
        this._active_id_index -= 1;
        if (this._active_id_index < 0) {
            this._active_id_index = this._active_ids.length - 1;
        }
        return this;
    },
    /**
     * Activates the next id in the active sequence. If there is no next id, wraps around to the first one
     * @returns itself
     */
    next: function () {
        this._active_id_index += 1;
        if (this._active_id_index >= this._active_ids.length) {
            this._active_id_index = 0;
        }
        return this;
    },

    /**
     * Sets active_ids by value:
     *
     * * Activates all ids part of the current selection
     * * Sets active_id to be the first id of the selection
     *
     * @param {Array} ids the list of ids to activate
     * @returns itself
     */
    select: function (ids) {
        this._active_ids = ids;
        this._active_id_index = 0;
        return this;
    },
    /**
     * Fetches the ids of the currently selected records, if any.
     */
    get_active_ids: function () {
        return this._active_ids;
    },
    /**
     * Sets the current active_id by value
     *
     * If there are no active_ids selected, selects the provided id as the sole active_id
     *
     * If there are ids selected and the provided id is not in them, raise an error
     *
     * @param {Object} id the id to activate
     * @returns itself
     */
    activate: function (id) {
        if(!this._active_ids) {
            this._active_ids = [id];
            this._active_id_index = 0;
        } else {
            var index = _.indexOf(this._active_ids, id);
            if (index == -1) {
                throw new Error(
                    "Could not find id " + id +
                    " in array [" + this._active_ids.join(', ') + "]");
            }
            this._active_id_index = index;
        }
        return this;
    },
    /**
     * Fetches the id of the current active record, if any.
     *
     * @returns record? record id or <code>null</code>
     */
    get_active_id: function () {
        if (!this._active_ids) {
            return null;
        }
        return this._active_ids[this._active_id_index];
    }
});

openerp.base.DataRecord =  openerp.base.Controller.extend({
    init: function(session, model, fields, values) {
        this._super(session, null);
        this.model = model;
        this.id = values.id || null;
        this.fields = fields;
        this.values = values;
    },
    on_change: function() {
    },
    on_reload: function() {
    }
});

openerp.base.SearchView = openerp.base.Controller.extend({
    init: function(session, element_id, dataset, view_id) {
        this._super(session, element_id);
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;

        this.inputs = [];
    },
    start: function() {
        //this.log('Starting SearchView '+this.model+this.view_id)
        this.rpc("/base/searchview/load", {"model": this.model, "view_id":this.view_id}, this.on_loaded);
    },
    /**
     * 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
     * @returns Array
     */
    make_widgets: function (items, fields) {
        var rows = [],
            row = [];
        rows.push(row);
        var filters = [];
        _.each(items, function (item) {
            if (filters.length && item.tag !== 'filter') {
                row.push(
                    new openerp.base.search.FilterGroup(
                        filters, this));
                filters = [];
            }

            if (item.tag === 'newline') {
                row = [];
                rows.push(row);
            } else if (item.tag === 'filter') {
                filters.push(
                    new openerp.base.search.Filter(
                        item, this));
            } else if (item.tag === 'separator') {
                // a separator is a no-op
            } else {
                if (item.tag === 'group') {
                    // TODO: group and field should be fetched from registries, maybe even filters
                    row.push(
                        new openerp.base.search.Group(
                            item, this, fields));
                } else if (item.tag === 'field') {
                    row.push(
                        this.make_field(
                            item, fields[item['attrs'].name]));
                }
            }
        }, this);
        if (filters.length) {
            row.push(new openerp.base.search.FilterGroup(filters, this));
        }

        return rows;
    },
    /**
     * 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.base.search.Field
     */
    make_field: function (item, field) {
        // TODO: should fetch from an actual registry
        // TODO: register fields in self?
        switch (field.type) {
            case 'char':
                return new openerp.base.search.CharField(
                    item, field, this);
            case 'float':
                return new openerp.base.search.FloatField(
                    item, field, this);
            case 'datetime':
                return new openerp.base.search.DateTimeField(
                    item, field, this);
            case 'one2many':
                return new openerp.base.search.OneToManyField(
                    item, field, this);
            case 'many2one':
                return new openerp.base.search.ManyToOneField(
                    item, field, this);
            default:
                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 lines = this.make_widgets(
            data.fields_view['arch'].children,
            data.fields_view.fields);

        // TODO: get default values
        var default_values = {};

        var render = QWeb.render("SearchView", {
            'view': data.fields_view['arch'],
            'lines': lines,
            'defaults': default_values
        });
        this.$element.html(render);

        this.$element.find('form')
                .submit(this.on_search)
                .bind('reset', this.on_reset);
        setTimeout(function () {
            // start() all the widgets
            _(lines).chain().flatten().each(function (widget) {
                widget.start();
            });
        }, 0);
    },
    on_search: function (e) {
        e.preventDefault();
        var inputs_ = _(this.inputs);

        var domains = inputs_.chain().
            map(function (input) { return input.get_domain(); }).
            compact().
            value();
        var contexts = inputs_.chain()
            .map(function (input) { return input.get_context(); })
            .compact()
            .value();
        console.log('domains', domains);
        console.log('contexts', contexts);
    },
    on_reset: function (e) {
        e.preventDefault();
        console.log('on_reset');
    }
});

openerp.base.search = {};
openerp.base.search.Invalid = Class.extend({
    init: function (field, value, message) {
        this.field = field;
        this.value = value;
        this.message = message;
    },
    toString: function () {
        return ('Incorrect value for field ' + this.field +
                ': [' + this.value + '] is ' + this.message);
    }
});
openerp.base.search.Widget = openerp.base.Controller.extend({
    template: null,
    init: function (view) {
        this.view = view;
    },
    /**
     * Sets and returns a globally unique identifier for the widget.
     *
     * If a prefix is appended, the identifier will be appended to it.
     *
     * @params sections prefix sections, empty/falsy sections will be removed
     */
    make_id: function () {
        this.element_id = _.uniqueId(
            ['search'].concat(
                _.compact(_.toArray(arguments)),
                ['']).join('_'));
        return this.element_id;
    },
    /**
     * "Starts" the widgets. Called at the end of the rendering, this allows
     * widgets to hook themselves to their view sections.
     *
     * On widgets, if they kept a reference to a view and have an element_id,
     * will fetch and set their root element on $element.
     */
    start: function () {
        this._super();
        if (this.view && this.element_id) {
            // id is unique, and no getElementById on elements
            this.$element = $(document.getElementById(
                this.element_id));
        }
    },
    /**
     * "Stops" the widgets. Called when the view destroys itself, this
     * lets the widgets clean up after themselves.
     */
    stop: function () {
        delete this.view;
        this._super();
    },
    render: function (defaults) {
        return QWeb.render(
            this.template, _.extend(this, {
                defaults: defaults
        }));
    }
});
openerp.base.search.FilterGroup = openerp.base.search.Widget.extend({
    template: 'SearchView.filters',
    init: function (filters, view) {
        this._super(view);
        this.filters = filters;
    },
    start: function () {
        this._super();
        _.each(this.filters, function (filter) {
            filter.start();
        });
    }
});
openerp.base.search.Group = openerp.base.search.Widget.extend({
    template: 'SearchView.group',
    // TODO: contain stuff
    // TODO: @expand
    init: function (view_section, view, fields) {
        this._super(view);
        this.attrs = view_section.attrs;
        this.lines = view.make_widgets(
            view_section.children, fields);
        this.make_id('group');
    },
    start: function () {
        this._super();
        _(this.lines)
            .chain()
            .flatten()
            .each(function (widget) { widget.start(); });
    }
});
openerp.base.search.Input = openerp.base.search.Widget.extend({
    init: function (view) {
        this._super(view);
        this.view.inputs.push(this);
    },
    get_context: function () {
        throw new Error(
            "get_context not implemented for widget " + this.attrs.type);
    },
    get_domain: function () {
        throw new Error(
            "get_domain not implemented for widget " + this.attrs.type);
    }
});
openerp.base.search.Filter = openerp.base.search.Input.extend({
    template: 'SearchView.filter',
    // TODO: force rendering
    init: function (node, view) {
        this._super(view);
        this.attrs = node.attrs;
        this.make_id('filter', this.attrs.name);
    },
    start: function () {
        this._super();
        var $view_form = this.view.$element.find('form');
        this.$element.click(function () {
            $(this).toggleClass('enabled');
            $view_form.submit();
        });
    },
    get_context: function () {
        if (!this.$element.hasClass('enabled')) {
            return;
        }
        return this.attrs.context;
    },
    get_domain: function () {
        if (!this.$element.hasClass('enabled')) {
            return;
        }
        return this.attrs.domain;
    }
});
openerp.base.search.Field = openerp.base.search.Input.extend({
    template: 'SearchView.field',
    default_operator: '=',
    // TODO: set default values
    // TODO: get context, domain
    // TODO: holds Filters
    init: function (view_section, field, view) {
        this._super(view);
        this.attrs = _.extend({}, field, view_section.attrs);
        this.filters = new openerp.base.search.FilterGroup(_.map(
            view_section.children, function (filter_node) {
                return new openerp.base.search.Filter(
                    filter_node, view);
        }), view);
        this.make_id('input', field.type, this.attrs.name);
    },
    start: function () {
        this._super();
        this.filters.start();
    },
    get_context: function () {
        if (!this.attrs.context) {
            return;
        }
        return this.attrs.context;
    },
    get_domain: function () {
        if (!this.attrs['filter_domain']) {
            return [
                this.attrs.name,
                this.attrs.operator || this.default_operator,
                this.get_value()
            ];
        }
        return this.attrs['filter_domain'];
    }
});
openerp.base.search.CharField = openerp.base.search.Field.extend({
    get_value: function () {
        return this.$element.val();
    }
});
openerp.base.search.FloatField = openerp.base.search.Field.extend({
    get_value: function () {
        var val = Number(this.$element.val());
        if (isNaN(val)) {
{'fraction_non_alphanumeric': 0.1251170464172649, 'fraction_numerical': 0.0011593168948142863, 'mean_word_length': 1.7584404403173237, 'pattern_counts': {'":': 14, '<': 11, '<?xml version=': 0, '>': 9, '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': '5314374', 'n_tokens_mistral': 12614, 'n_tokens_neox': 12270, 'n_words': 3133}