<commit_before>var mouseChange = require('mouse-change')
var mouseWheel = require('mouse-wheel')
var identity = require('gl-mat4/identity')
var perspective = require('gl-mat4/perspective')
var lookAt = require('gl-mat4/lookAt')

module.exports = createCamera

var isBrowser = typeof window !== 'undefined'

function createCamera (regl, props_) {
  var props = props_ || {}
  var cameraState = {
    view: identity(new Float32Array(16)),
    projection: identity(new Float32Array(16)),
    center: new Float32Array(props.center || 3),
    theta: props.theta || 0,
    phi: props.phi || 0,
    distance: Math.log(props.distance || 10.0),
    eye: new Float32Array(3),
    up: new Float32Array(props.up || [0, 1, 0]),
    fovy: props.fovy || Math.PI / 4.0,
    near: typeof props.near !== 'undefined' ? props.near : 0.01,
    far: typeof props.far !== 'undefined' ? props.far : 1000.0,
    flipY: !!props.flipY,
    dtheta: 0,
    dphi: 0
  }

  var element = props.element
  var damping = typeof props.damping !== 'undefined' ? props.damping : 0.9

  var right = new Float32Array([1, 0, 0])
  var front = new Float32Array([0, 0, 1])

  var minDistance = Math.log('minDistance' in props ? props.minDistance : 0.1)
  var maxDistance = Math.log('maxDistance' in props ? props.maxDistance : 1000)

  var ddistance = 0

  var prevX = 0
  var prevY = 0

  if (isBrowser && props.mouse !== false) {
    var target = element || window
    var width = element ? element.offsetWidth : window.innerWidth
    var height = element ? element.offsetHeight : window.innerHeight
    mouseChange(target, function (buttons, x, y) {
      if (buttons & 1) {
        var dx = (x - prevX) / width
        var dy = (y - prevY) / height
        var w = Math.max(cameraState.distance, 0.5)

        cameraState.dtheta += w * dx
        cameraState.dphi += w * dy
      }
      prevX = x
      prevY = y
    })
    mouseWheel(target, function (dx, dy) {
      ddistance += dy / height
    })
  }

  function damp (x) {
    var xd = x * damping
    if (Math.abs(xd) < 0.1) {
      return 0
    }
    return xd
  }

  function clamp (x, lo, hi) {
    return Math.min(Math.max(x, lo), hi)
  }

  function updateCamera (props) {
    Object.keys(props).forEach(function (prop) {
      cameraState[prop] = props[prop]
    })

    var center = cameraState.center
    var eye = cameraState.eye
    var up = cameraState.up
    var dtheta = cameraState.dtheta
    var dphi = cameraState.dphi

    cameraState.theta += dtheta
    cameraState.phi = clamp(
      cameraState.phi + dphi,
      -Math.PI / 2.0,
      Math.PI / 2.0)
    cameraState.distance = clamp(
      cameraState.distance + ddistance,
      minDistance,
      maxDistance)

    cameraState.dtheta = damp(dtheta)
    cameraState.dphi = damp(dphi)
    ddistance = damp(ddistance)

    var theta = cameraState.theta
    var phi = cameraState.phi
    var r = Math.exp(cameraState.distance)

    var vf = r * Math.sin(theta) * Math.cos(phi)
    var vr = r * Math.cos(theta) * Math.cos(phi)
    var vu = r * Math.sin(phi)

    for (var i = 0; i < 3; ++i) {
      eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
    }

    lookAt(cameraState.view, eye, center, up)
  }

  var injectContext = regl({
    context: Object.assign({}, cameraState, {
      projection: function (context) {
        perspective(cameraState.projection,
          cameraState.fovy,
          context.viewportWidth / context.viewportHeight,
          cameraState.near,
          cameraState.far)
        if (cameraState.flipY) { cameraState.projection[5] *= -1 }
        return cameraState.projection
      }
    }),
    uniforms: Object.keys(cameraState).reduce(function (uniforms, name) {
      uniforms[name] = regl.context(name)
      return uniforms
    }, {})
  })

  function setupCamera (props, block) {
    if (!block) {
      block = props
      props = {}
    }
    updateCamera(props)
    injectContext(block)
  }

  Object.keys(cameraState).forEach(function (name) {
    setupCamera[name] = cameraState[name]
  })

  return setupCamera
}
<commit_msg>Change to source<commit_after>var mouseChange = require('mouse-change')
var mouseWheel = require('mouse-wheel')
var identity = require('gl-mat4/identity')
var perspective = require('gl-mat4/perspective')
var lookAt = require('gl-mat4/lookAt')

module.exports = createCamera

var isBrowser = typeof window !== 'undefined'

function createCamera (regl, props_) {
  var props = props_ || {}
  var cameraState = {
    view: identity(new Float32Array(16)),
    projection: identity(new Float32Array(16)),
    center: new Float32Array(props.center || 3),
    theta: props.theta || 0,
    phi: props.phi || 0,
    distance: Math.log(props.distance || 10.0),
    eye: new Float32Array(3),
    up: new Float32Array(props.up || [0, 1, 0]),
    fovy: props.fovy || Math.PI / 4.0,
    near: typeof props.near !== 'undefined' ? props.near : 0.01,
    far: typeof props.far !== 'undefined' ? props.far : 1000.0,
    flipY: !!props.flipY,
    dtheta: 0,
    dphi: 0
  }

  var element = props.element
  var damping = typeof props.damping !== 'undefined' ? props.damping : 0.9

  var right = new Float32Array([1, 0, 0])
  var front = new Float32Array([0, 0, 1])

  var minDistance = Math.log('minDistance' in props ? props.minDistance : 0.1)
  var maxDistance = Math.log('maxDistance' in props ? props.maxDistance : 1000)

  var ddistance = 0

  var prevX = 0
  var prevY = 0

  if (isBrowser && props.mouse !== false) {
    var source = element || window
    var width = element ? element.offsetWidth : window.innerWidth
    var height = element ? element.offsetHeight : window.innerHeight
    mouseChange(source, function (buttons, x, y) {
      if (buttons & 1) {
        var dx = (x - prevX) / width
        var dy = (y - prevY) / height
        var w = Math.max(cameraState.distance, 0.5)

        cameraState.dtheta += w * dx
        cameraState.dphi += w * dy
      }
      prevX = x
      prevY = y
    })
    mouseWheel(source, function (dx, dy) {
      ddistance += dy / height
    })
  }

  function damp (x) {
    var xd = x * damping
    if (Math.abs(xd) < 0.1) {
      return 0
    }
    return xd
  }

  function clamp (x, lo, hi) {
    return Math.min(Math.max(x, lo), hi)
  }

  function updateCamera (props) {
    Object.keys(props).forEach(function (prop) {
      cameraState[prop] = props[prop]
    })

    var center = cameraState.center
    var eye = cameraState.eye
    var up = cameraState.up
    var dtheta = cameraState.dtheta
    var dphi = cameraState.dphi

    cameraState.theta += dtheta
    cameraState.phi = clamp(
      cameraState.phi + dphi,
      -Math.PI / 2.0,
      Math.PI / 2.0)
    cameraState.distance = clamp(
      cameraState.distance + ddistance,
      minDistance,
      maxDistance)

    cameraState.dtheta = damp(dtheta)
    cameraState.dphi = damp(dphi)
    ddistance = damp(ddistance)

    var theta = cameraState.theta
    var phi = cameraState.phi
    var r = Math.exp(cameraState.distance)

    var vf = r * Math.sin(theta) * Math.cos(phi)
    var vr = r * Math.cos(theta) * Math.cos(phi)
    var vu = r * Math.sin(phi)

    for (var i = 0; i < 3; ++i) {
      eye[i] = center[i] + vf * front[i] + vr * right[i] + vu * up[i]
    }

    lookAt(cameraState.view, eye, center, up)
  }

  var injectContext = regl({
    context: Object.assign({}, cameraState, {
      projection: function (context) {
        perspective(cameraState.projection,
          cameraState.fovy,
          context.viewportWidth / context.viewportHeight,
          cameraState.near,
          cameraState.far)
        if (cameraState.flipY) { cameraState.projection[5] *= -1 }
        return cameraState.projection
      }
    }),
    uniforms: Object.keys(cameraState).reduce(function (uniforms, name) {
      uniforms[name] = regl.context(name)
      return uniforms
    }, {})
  })

  function setupCamera (props, block) {
    if (!block) {
      block = props
      props = {}
    }
    updateCamera(props)
    injectContext(block)
  }

  Object.keys(cameraState).forEach(function (name) {
    setupCamera[name] = cameraState[name]
  })

  return setupCamera
}
<|endoftext|>


<commit_before>openerp.web.data_import = function(openerp) {
var QWeb = openerp.web.qweb,
    _t = openerp.web._t;
/**
 * Safari does not deal well at all with raw JSON data being returned. As a
 * result, we're going to cheat by using a pseudo-jsonp: instead of getting
 * JSON data in the iframe, we're getting a ``script`` tag which consists of a
 * function call and the returned data (the json dump).
 *
 * The function is an auto-generated name bound to ``window``, which calls
 * back into the callback provided here.
 *
 * @param {Object} form the form element (DOM or jQuery) to use in the call
 * @param {Object} attributes jquery.form attributes object
 * @param {Function} callback function to call with the returned data
 */
function jsonp(form, attributes, callback) {
    attributes = attributes || {};
    var options = {jsonp: _.uniqueId('import_callback_')};
    window[options.jsonp] = function () {
        delete window[options.jsonp];
        callback.apply(null, arguments);
    };
    if ('data' in attributes) {
        _.extend(attributes.data, options);
    } else {
        _.extend(attributes, {data: options});
    }
    $(form).ajaxSubmit(attributes);
}

openerp.web.DataImport = openerp.web.Dialog.extend({
    template: 'ImportDataView',
    dialog_title: {toString: function () { return _t("Import Data"); }},
    init: function(parent, dataset){
        var self = this;
        this._super(parent, {});
        this.model = parent.model;
        this.fields = [];
        this.all_fields = [];
        this.fields_with_defaults = [];
        this.required_fields = null;

        var convert_fields = function (root, prefix) {
            prefix = prefix || '';
            _(root.fields).each(function (f) {
                self.all_fields.push(prefix + f.name);
                if (f.fields) {
                    convert_fields(f, prefix + f.id + '/');
                }
            });
        };
        this.ready  = $.Deferred.queue().then(function () {
            self.required_fields = _(self.fields).chain()
                .filter(function (field) {
                    return field.required &&
                           !_.include(self.fields_with_defaults, field.id); })
                .pluck('name')
                .value();
            convert_fields(self);
            self.all_fields.sort();
        });
    },
    start: function() {
        var self = this;
        this._super();
        this.open({
            buttons: [
                {text: _t("Close"), click: function() { self.stop(); }},
                {text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
            ],
            close: function(event, ui) {
                self.stop();
            }
        });
        this.toggle_import_button(false);
        this.$element.find('#csvfile').change(this.on_autodetect_data);
        this.$element.find('fieldset').change(this.on_autodetect_data);
        this.$element.delegate('fieldset legend', 'click', function() {
            $(this).parent().toggleClass('oe-closed');
        });
        this.ready.push(new openerp.web.DataSet(this, this.model).call(
            'fields_get', [], function (fields) {
                self.graft_fields(fields);
                self.ready.push(new openerp.web.DataSet(self, self.model)
                        .default_get(_.pluck(self.fields, 'id')).then(function (fields) {
                    _.each(fields, function(val, key) {
                        if (val) {
                            self.fields_with_defaults.push(key);
                        }
                    });
                })
            )
        }));
    },
    graft_fields: function (fields, parent, level) {
        parent = parent || this;
        level = level || 0;

        var self = this;
        if (level === 0) {
            parent.fields.push({
                id: 'id',
                name: 'id',
                string: _t('External ID'),
                required: false
            });
        }
        _(fields).each(function (field, field_name) {
            // Ignore spec for id field
            // Don't import function fields (function and related)
            if (field_name === 'id') {
                return;
            }
            // Skip if there's no state which could disable @readonly,
            // if a field is ever always readonly we can't import it
            if (field.readonly) {
                // no states at all
                if (_.isEmpty(field.states)) { return; }
                // no state altering @readonly
                if (!_.any(field.states, function (modifiers) {
                    return _(modifiers).chain().pluck(0).contains('readonly').value();
                })) { return; }
            }

            var f = {
                id: field_name,
                name: field_name,
                string: field.string,
                required: field.required
            };

            switch (field.type) {
            case 'many2many':
            case 'many2one':
                // push a copy for the bare many2one field, to allow importing
                // using name_search too - even if we default to exporting the XML ID
                var many2one_field = _.extend({}, f)
                parent.fields.push(many2one_field);
                f.name += '/id';
                break;
            case 'one2many':
                f.name += '/id';
                f.fields = [];
                // only fetch sub-fields to a depth of 2 levels
                if (level < 2) {
                    self.ready.push(new openerp.web.DataSet(self, field.relation).call(
                        'fields_get', [], function (fields) {
                            self.graft_fields(fields, f, level+1);
                    }));
                }
                break;
            }
            parent.fields.push(f);
        });
    },
    toggle_import_button: function (newstate) {
        this.$element.dialog('widget')
                .find('.oe-dialog-import-button')
                .button('option', 'disabled', !newstate);
    },
    do_import: function() {
        if(!this.$element.find('#csvfile').val()) { return; }
        var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
        var with_headers = this.$element.find('#file_has_headers').prop('checked');
        if (!lines_to_skip && with_headers) {
            lines_to_skip = 1;
        }
        var indices = [], fields = [];
        this.$element.find(".sel_fields").each(function(index, element) {
            var val = element.value;
            if (!val) {
                return;
            }
            indices.push(index);
            fields.push(val);
        });

        jsonp(this.$element.find('#import_data'), {
            url: '/web/import/import_data',
            data: {
                model: this.model,
                meta: JSON.stringify({
                    skip: lines_to_skip,
                    indices: indices,
                    fields: fields
                })
            }
        }, this.on_import_results);
    },
    on_autodetect_data: function() {
        if(!this.$element.find('#csvfile').val()) { return; }
        jsonp(this.$element.find('#import_data'), {
            url: '/web/import/detect_data'
        }, this.on_import_results);
    },
    on_import_results: function(results) {
        this.$element.find('#result').empty();
        var headers, result_node = this.$element.find("#result");

        if (results['error']) {
            result_node.append(QWeb.render('ImportView.error', {
                'error': results['error']}));
            this.$element.find('fieldset').removeClass('oe-closed');
            return;
        }
        if (results['success']) {
            if (this.widget_parent.widget_parent.active_view == "list") {
                this.widget_parent.reload_content();
            }
            this.stop();
            return;
        }

        if (results['records']) {
            var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
                with_headers = this.$element.find('#file_has_headers').prop('checked');
            headers = with_headers ? results.records[0] : null;

            result_node.append(QWeb.render('ImportView.result', {
                'headers': headers,
                'records': lines_to_skip ? results.records.slice(lines_to_skip)
                          : with_headers ? results.records.slice(1)
                          : results.records
            }));
            this.$element.find('fieldset').addClass('oe-closed');
        }
        this.$element.find('form').removeClass('oe-import-no-result');

        this.$element.delegate('.oe-m2o-drop-down-button', 'click', function () {
            $(this).prev('input').focus();
        });

        var self = this;
        this.ready.then(function () {
            var $fields = self.$element.find('.sel_fields').bind('blur', function () {
                if (this.value && !_(self.all_fields).contains(this.value)) {
                    this.value = '';
                }
            }).autocomplete({
                minLength: 0,
                source: self.all_fields,
                change: self.on_check_field_values
            }).focus(function () {
                $(this).autocomplete('search');
            });
            // Column auto-detection
            _(headers).each(function (header, index) {
                var field_name = self.match_column_to_field(header);
                if (field_name) {
                    $fields.eq(index).val(field_name);
                }
            });
            self.on_check_field_values();
        });
    },
    /**
     * Returns the name of the field (nested) matching the provided column name
     *
     * @param {String} name column name to look for
     * @param {Array} [fields] fields to look into for the provided name
     * @returns {String|undefined}
     */
    match_column_to_field: function (name, fields) {
        fields = fields || this.fields;
        var f;
        f = _(fields).detect(function (field) {
            // TODO: levenshtein between header and field.string
            return field.name === name
        });
        if (!f) {
            f = _(fields).detect(function (field) {
                // TODO: levenshtein between header and field.string
               return field.string.toLowerCase() === name.toLowerCase();
            });
        }
        if (f) { return f.name; }

        // if ``name`` is a path (o2m), we need to recurse through its .fields
        var index = name.indexOf('/');
        if (index === -1) { return undefined; }
        // Get the first path section, try to find the matching field
        var column_name = name.substring(0, index);
        f = _(fields).detect(function (field) {
            // field.name for o2m is $foo/id, so we want to match on id
            return field.id === column_name
        });
        if (!f) {
            f = _(fields).detect(function (field) {
            // field.name for o2m is $foo/id, so we want to match on id
            return field.string.toLowerCase() === column_name.toLowerCase()
            });
        }
        if (!f) { return undefined; }

        // if we found a matching field for the first path section, recurse in
        // its own .fields to try and get the rest of the path matched
        var rest = this.match_column_to_field(
                name.substring(index+1), f.fields);
        if (!rest) { return undefined; }
        return f.id + '/' + rest;
    },
    /**
     * Looks through all the field selections, and tries to find if two
     * (or more) columns were matched to the same model field.
     *
     * Returns a map of the multiply-mapped fields to an array of offending
     * columns (not actually columns, but the inputs containing the same field
     * names).
     *
     * Also has the side-effect of marking the discovered inputs with the class
     * ``duplicate_fld``.
     *
     * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
     */
    find_duplicate_fields: function() {
        // Maps values to DOM nodes, in order to discover duplicates
        var values = {}, duplicates = {};
        this.$element.find(".sel_fields").each(function(index, element) {
            var value = element.value;
            var $element = $(element).removeClass('duplicate_fld');
            if (!value) { return; }

            if (!(value in values)) {
                values[value] = element;
            } else {
                var same_valued_field = values[value];
                if (value in duplicates) {
                    duplicates[value].push(element);
                } else {
                    duplicates[value] = [same_valued_field, element];
                }
                $element.add(same_valued_field).addClass('duplicate_fld');
            }
        });
        return duplicates;
    },
    on_check_field_values: function () {
        this.$element.find("#message, #msg").remove();

        var required_valid = this.check_required();

        var duplicates = this.find_duplicate_fields();
        if (_.isEmpty(duplicates)) {
            this.toggle_import_button(required_valid);
        } else {
            var $err = $('
'+_t("Destination fields should only be selected once, some fields are selected more than once:")+'
').insertBefore(this.$element.find('#result'));
            var $dupes = $('
').appendTo($err);
            _(duplicates).each(function(elements, value) {
                $('
').text(value).appendTo($dupes);
                _(elements).each(function(element) {
                    var cell = $(element).closest('td');
                    $('
').text(cell.parent().children().index(cell)).appendTo($dupes);
                });
            });
            this.toggle_import_button(false);
        }

    },
    check_required: function() {
        if (!this.required_fields.length) { return true; }

        var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
            .pluck('value')
            .compact()
            .value();

        var missing_fields = _.difference(this.required_fields, selected_fields);
        if (missing_fields.length) {
            this.$element.find("#result").before('
' + _t("*Required Fields are not selected :") + missing_fields + '.
');
            return false;
        }
        return true;
    },
    stop: function() {
        this.$element.remove();
        this._super();
    }
});
};
<commit_msg>[FIX] web: data import: correctly handle 'required' many2one - user can provided any of base fieldname or with the '/id'<commit_after>openerp.web.data_import = function(openerp) {
var QWeb = openerp.web.qweb,
    _t = openerp.web._t;
/**
 * Safari does not deal well at all with raw JSON data being returned. As a
 * result, we're going to cheat by using a pseudo-jsonp: instead of getting
 * JSON data in the iframe, we're getting a ``script`` tag which consists of a
 * function call and the returned data (the json dump).
 *
 * The function is an auto-generated name bound to ``window``, which calls
 * back into the callback provided here.
 *
 * @param {Object} form the form element (DOM or jQuery) to use in the call
 * @param {Object} attributes jquery.form attributes object
 * @param {Function} callback function to call with the returned data
 */
function jsonp(form, attributes, callback) {
    attributes = attributes || {};
    var options = {jsonp: _.uniqueId('import_callback_')};
    window[options.jsonp] = function () {
        delete window[options.jsonp];
        callback.apply(null, arguments);
    };
    if ('data' in attributes) {
        _.extend(attributes.data, options);
    } else {
        _.extend(attributes, {data: options});
    }
    $(form).ajaxSubmit(attributes);
}

openerp.web.DataImport = openerp.web.Dialog.extend({
    template: 'ImportDataView',
    dialog_title: {toString: function () { return _t("Import Data"); }},
    init: function(parent, dataset){
        var self = this;
        this._super(parent, {});
        this.model = parent.model;
        this.fields = [];
        this.all_fields = [];
        this.fields_with_defaults = [];
        this.required_fields = null;

        var convert_fields = function (root, prefix) {
            prefix = prefix || '';
            _(root.fields).each(function (f) {
                self.all_fields.push(prefix + f.name);
                if (f.fields) {
                    convert_fields(f, prefix + f.id + '/');
                }
            });
        };
        this.ready  = $.Deferred.queue().then(function () {
            self.required_fields = _(self.fields).chain()
                .filter(function (field) {
                    return field.required &&
                           !_.include(self.fields_with_defaults, field.id); })
                .pluck('id')
                .uniq()
                .value();
            convert_fields(self);
            self.all_fields.sort();
        });
    },
    start: function() {
        var self = this;
        this._super();
        this.open({
            buttons: [
                {text: _t("Close"), click: function() { self.stop(); }},
                {text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
            ],
            close: function(event, ui) {
                self.stop();
            }
        });
        this.toggle_import_button(false);
        this.$element.find('#csvfile').change(this.on_autodetect_data);
        this.$element.find('fieldset').change(this.on_autodetect_data);
        this.$element.delegate('fieldset legend', 'click', function() {
            $(this).parent().toggleClass('oe-closed');
        });
        this.ready.push(new openerp.web.DataSet(this, this.model).call(
            'fields_get', [], function (fields) {
                self.graft_fields(fields);
                self.ready.push(new openerp.web.DataSet(self, self.model)
                        .default_get(_.pluck(self.fields, 'id')).then(function (fields) {
                    _.each(fields, function(val, key) {
                        if (val) {
                            self.fields_with_defaults.push(key);
                        }
                    });
                })
            )
        }));
    },
    graft_fields: function (fields, parent, level) {
        parent = parent || this;
        level = level || 0;

        var self = this;
        if (level === 0) {
            parent.fields.push({
                id: 'id',
                name: 'id',
                string: _t('External ID'),
                required: false
            });
        }
        _(fields).each(function (field, field_name) {
            // Ignore spec for id field
            // Don't import function fields (function and related)
            if (field_name === 'id') {
                return;
            }
            // Skip if there's no state which could disable @readonly,
            // if a field is ever always readonly we can't import it
            if (field.readonly) {
                // no states at all
                if (_.isEmpty(field.states)) { return; }
                // no state altering @readonly
                if (!_.any(field.states, function (modifiers) {
                    return _(modifiers).chain().pluck(0).contains('readonly').value();
                })) { return; }
            }

            var f = {
                id: field_name,
                name: field_name,
                string: field.string,
                required: field.required
            };

            switch (field.type) {
            case 'many2many':
            case 'many2one':
                // push a copy for the bare many2one field, to allow importing
                // using name_search too - even if we default to exporting the XML ID
                var many2one_field = _.extend({}, f)
                parent.fields.push(many2one_field);
                f.name += '/id';
                break;
            case 'one2many':
                f.name += '/id';
                f.fields = [];
                // only fetch sub-fields to a depth of 2 levels
                if (level < 2) {
                    self.ready.push(new openerp.web.DataSet(self, field.relation).call(
                        'fields_get', [], function (fields) {
                            self.graft_fields(fields, f, level+1);
                    }));
                }
                break;
            }
            parent.fields.push(f);
        });
    },
    toggle_import_button: function (newstate) {
        this.$element.dialog('widget')
                .find('.oe-dialog-import-button')
                .button('option', 'disabled', !newstate);
    },
    do_import: function() {
        if(!this.$element.find('#csvfile').val()) { return; }
        var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
        var with_headers = this.$element.find('#file_has_headers').prop('checked');
        if (!lines_to_skip && with_headers) {
            lines_to_skip = 1;
        }
        var indices = [], fields = [];
        this.$element.find(".sel_fields").each(function(index, element) {
            var val = element.value;
            if (!val) {
                return;
            }
            indices.push(index);
            fields.push(val);
        });

        jsonp(this.$element.find('#import_data'), {
            url: '/web/import/import_data',
            data: {
                model: this.model,
                meta: JSON.stringify({
                    skip: lines_to_skip,
                    indices: indices,
                    fields: fields
                })
            }
        }, this.on_import_results);
    },
    on_autodetect_data: function() {
        if(!this.$element.find('#csvfile').val()) { return; }
        jsonp(this.$element.find('#import_data'), {
            url: '/web/import/detect_data'
        }, this.on_import_results);
    },
    on_import_results: function(results) {
        this.$element.find('#result').empty();
        var headers, result_node = this.$element.find("#result");

        if (results['error']) {
            result_node.append(QWeb.render('ImportView.error', {
                'error': results['error']}));
            this.$element.find('fieldset').removeClass('oe-closed');
            return;
        }
        if (results['success']) {
            if (this.widget_parent.widget_parent.active_view == "list") {
                this.widget_parent.reload_content();
            }
            this.stop();
            return;
        }

        if (results['records']) {
            var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
                with_headers = this.$element.find('#file_has_headers').prop('checked');
            headers = with_headers ? results.records[0] : null;

            result_node.append(QWeb.render('ImportView.result', {
                'headers': headers,
                'records': lines_to_skip ? results.records.slice(lines_to_skip)
                          : with_headers ? results.records.slice(1)
                          : results.records
            }));
            this.$element.find('fieldset').addClass('oe-closed');
        }
        this.$element.find('form').removeClass('oe-import-no-result');

        this.$element.delegate('.oe-m2o-drop-down-button', 'click', function () {
            $(this).prev('input').focus();
        });

        var self = this;
        this.ready.then(function () {
            var $fields = self.$element.find('.sel_fields').bind('blur', function () {
                if (this.value && !_(self.all_fields).contains(this.value)) {
                    this.value = '';
                }
            }).autocomplete({
                minLength: 0,
                source: self.all_fields,
                change: self.on_check_field_values
            }).focus(function () {
                $(this).autocomplete('search');
            });
            // Column auto-detection
            _(headers).each(function (header, index) {
                var field_name = self.match_column_to_field(header);
                if (field_name) {
                    $fields.eq(index).val(field_name);
                }
            });
            self.on_check_field_values();
        });
    },
    /**
     * Returns the name of the field (nested) matching the provided column name
     *
     * @param {String} name column name to look for
     * @param {Array} [fields] fields to look into for the provided name
     * @returns {String|undefined}
     */
    match_column_to_field: function (name, fields) {
        fields = fields || this.fields;
        var f;
        f = _(fields).detect(function (field) {
            // TODO: levenshtein between header and field.string
            return field.name === name
        });
        if (!f) {
            f = _(fields).detect(function (field) {
                // TODO: levenshtein between header and field.string
               return field.string.toLowerCase() === name.toLowerCase();
            });
        }
        if (f) { return f.name; }

        // if ``name`` is a path (o2m), we need to recurse through its .fields
        var index = name.indexOf('/');
        if (index === -1) { return undefined; }
        // Get the first path section, try to find the matching field
        var column_name = name.substring(0, index);
        f = _(fields).detect(function (field) {
            // field.name for o2m is $foo/id, so we want to match on id
            return field.id === column_name
        });
        if (!f) {
            f = _(fields).detect(function (field) {
            // field.name for o2m is $foo/id, so we want to match on id
            return field.string.toLowerCase() === column_name.toLowerCase()
            });
        }
        if (!f) { return undefined; }

        // if we found a matching field for the first path section, recurse in
        // its own .fields to try and get the rest of the path matched
        var rest = this.match_column_to_field(
                name.substring(index+1), f.fields);
        if (!rest) { return undefined; }
        return f.id + '/' + rest;
    },
    /**
     * Looks through all the field selections, and tries to find if two
     * (or more) columns were matched to the same model field.
     *
     * Returns a map of the multiply-mapped fields to an array of offending
     * columns (not actually columns, but the inputs containing the same field
     * names).
     *
     * Also has the side-effect of marking the discovered inputs with the class
     * ``duplicate_fld``.
     *
     * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
     */
    find_duplicate_fields: function() {
        // Maps values to DOM nodes, in order to discover duplicates
        var values = {}, duplicates = {};
        this.$element.find(".sel_fields").each(function(index, element) {
            var value = element.value;
            var $element = $(element).removeClass('duplicate_fld');
            if (!value) { return; }

            if (!(value in values)) {
                values[value] = element;
            } else {
                var same_valued_field = values[value];
                if (value in duplicates) {
                    duplicates[value].push(element);
                } else {
                    duplicates[value] = [same_valued_field, element];
                }
                $element.add(same_valued_field).addClass('duplicate_fld');
            }
        });
        return duplicates;
    },
    on_check_field_values: function () {
        this.$element.find("#message, #msg").remove();

        var required_valid = this.check_required();

        var duplicates = this.find_duplicate_fields();
        if (_.isEmpty(duplicates)) {
            this.toggle_import_button(required_valid);
        } else {
            var $err = $('
'+_t("Destination fields should only be selected once, some fields are selected more than once:")+'
').insertBefore(this.$element.find('#result'));
            var $dupes = $('
').appendTo($err);
            _(duplicates).each(function(elements, value) {
                $('
').text(value).appendTo($dupes);
                _(elements).each(function(element) {
                    var cell = $(element).closest('td');
                    $('
').text(cell.parent().children().index(cell)).appendTo($dupes);
                });
            });
            this.toggle_import_button(false);
        }

    },
    check_required: function() {
        if (!this.required_fields.length) { return true; }

        // get real field name (stripping the trailing "/id" for many2one field).
        // For many2one fields, user can use any of the two fields:
        // - base field name for name_search() import
        // - with the "/id" for direct referencing
        // In both case we must check if field is required only based on his
        // real name.
        var get_field_real_id = _.bind(function(field_name) {
            var f = _.detect(this.fields, function(field) {
                return field.name === field_name
            });
            if (!f) { return field_name };
            return f.id;
        }, this);

        var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
            .pluck('value')
            .compact()
            .map(get_field_real_id)
            .value();

        var missing_fields = _.difference(this.required_fields, selected_fields);
        if (missing_fields.length) {
            this.$element.find("#result").before('
' + _t("*Required Fields are not selected :") + missing_fields + '.
');
            return false;
        }
        return true;
    },
    stop: function() {
        this.$element.remove();
        this._super();
    }
});
};
<|endoftext|>


<commit_before>"use strict";

const uuid = require('node-uuid');

const ExpressSplit = (user_options) => {
  const default_options = {
    experiments: {},
    storage:                     'in-memory', // in-memory, mysql
    db_pool:                     false,
    db_table_experiments:        'split_experiments',
    db_table_users:              'split_users',
    use_cookies:                 false,
    cookie_name:                 '_splituid',
    cookie_max_age:              15552000000 // 180 days in milliseconds
  };
  const options = Object.assign(default_options, user_options);

  const storage = new SplitStorage(options);
  const gui     = new SplitGui();

  const check_to_set_cookie = (req, res) => {
    if (options.use_cookies === true) {
      if (req.split.id === false) {
        if (req.cookies[options.cookie_name]) {
          req.split.id = parseInt(req.cookies[options.cookie_name].substring(0, 9), 10);
        } else {
          const uid = parseInt(uuid.v4().replace(/[^0-9]/g, '').substring(0, 9), 10);
          req.split.id = uid;
        }
      }
      res.cookie(options.cookie_name, req.split.id, { maxAge: options.cookie_max_age });
    }
  };

  return function(req, res, next) {
    req.split = {
      id: false,
      set_id: (user_id) => {
        if (!isNaN(user_id) &&
            parseInt(Number(user_id)) === user_id &&
            !isNaN(parseInt(user_id, 10))) {
          req.split.id = user_id;
        } else {
          throw new Error(`Split set_id() must receive an integer`);
        }
      },
      start: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        storage.addUserOption(req.split.id, experiment_id, callback);
      },
      get: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        return storage.getUserOption(req.split.id, experiment_id, callback);
      },
      finish: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        storage.addConversion(experiment_id, req.split.id, callback);
      },
      results: (callback) => {
        return storage.getResults(callback);
      },
      gui: (req, res) => {
        let experiments = {};
        return storage.getResults((results) => {
          for (let r in results.results) {
            const result = results.results[r];
            const experiment_name = result.experiment;
            if (!experiments.hasOwnProperty(experiment_name)) {
              experiments[experiment_name] = {results: []};
            }
            delete result.experiment;
            experiments[experiment_name].results.push(result);
          }
          gui.render(req, res, experiments);
        });
      },
      __test: () => {
        // Exposes private objects for unit testing
        return {
          user_options:    user_options,
          default_options: default_options,
          options:         options,
          storage:         storage
        }
      }
    };
    next();
}};

class SplitStorage {
  constructor(options) {
    if (options.storage == 'in-memory') {
      this.storage = new SplitStorageInMemory(options);
    } else if (options.storage == 'mysql') {
      this.storage = new SplitStorageMysql(options);
    } else {
      throw new Error(`Split does not support the storage type ${options.storage}`);
    }
  }

  addUserOption(user_id, experiment_id, callback) {
    this.storage.addUserOption(user_id, experiment_id, callback);
  }

  getUserOption(user_id, experiment_id, callback) {
    return this.storage.getUserOption(user_id, experiment_id, callback);
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.storage.addImpression(experiment_id, chosen_option, callback);
  }

  addConversion(experiment_id, user_id, callback) {
    this.storage.addConversion(experiment_id, user_id, callback);
  }

  getResults(callback) {
    return this.storage.getResults(callback);
  }
}

class SplitStorageAbstract {
  constructor() {
    const methods = ['addUserOption', 'getUserOption', 'addImpression', 'addConversion', 'getResults'];
    for (let m in methods) {
      if (typeof this[methods[m]] === 'String') {
        throw new TypeError(`The method ${methods[m]} must be overriden when deriving from SplitStorageAbstract`);
      }
    }
  }

  generateRandomOption(user_id, experiment_options) {
    const chosen_index = Math.floor(seeded_rand(user_id) * experiment_options.length);
    return experiment_options[chosen_index];
  }
}

class SplitStorageInMemory extends SplitStorageAbstract {
  constructor(options) {
    super();
    this.users        = {};
    this.results      = {};
    this.experiments  = options.experiments;
    for (let e in options.experiments) {
      this.results[e] = {};
      const ops = options.experiments[e].options;
      for (let o in ops) {
        this.results[e][ops[o]] = {
          impressions: 0,
          conversions: 0
        };
      }
    }
  }

  addUserOption(user_id, experiment_id, callback) {
    if (!this.users[user_id]) {
      this.users[user_id] = {};
    }
    if (!this.users[user_id][experiment_id]) {
      const chosen_option = this.generateRandomOption(user_id, this.experiments[experiment_id].options);
      this.users[user_id][experiment_id] = {option: chosen_option, converted: false};
      this.addImpression(experiment_id, chosen_option, callback);
    } else {
      callback();
    }
  }

  getUserOption(user_id, experiment_id, callback) {
    try {
      callback(this.users[user_id][experiment_id].option);
    } catch (e) {
      callback(this.experiments[experiment_id].options[0]);
    }
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.results[experiment_id][chosen_option].impressions++;
    callback();
  }

  addConversion(experiment_id, user_id, callback) {
    if (this.users[user_id][experiment_id].converted === false) {
      this.getUserOption(user_id, experiment_id, (option) => {
        this.results[experiment_id][option].conversions++;
        this.users[user_id][experiment_id].converted = true;
        callback();
      });
    } else {
      callback();
    }
  }

  getResults(callback) {
    callback({results: this.results});
  }
}

class SplitStorageMysql extends SplitStorageAbstract {
  // The construction of the tables is async, assuming that it won't take long and no users will be missed
  constructor(options) {
    super();
    this.pool   = options.db_pool;
    this.tables = {
      experiments: options.db_table_experiments,
      users:       options.db_table_users
    };
    this.experiments = options.experiments;
    // Create tables
    this.pool.query(`CREATE TABLE IF NOT EXISTS ?? (user_id INT NOT NULL, experiment VARCHAR(45) NOT NULL, experiment_option VARCHAR(45) NOT NULL, converted TINYINT(1) NOT NULL DEFAULT 0, UNIQUE INDEX user_option (user_id ASC, experiment ASC, experiment_option ASC))`, [this.tables.users], (err) => {
      if (err){
        console.error(err, 'Creating the users table failed');
      }
    });
    this.pool.query(`CREATE TABLE IF NOT EXISTS ?? (experiment VARCHAR(45) NOT NULL, experiment_option VARCHAR(45) NOT NULL, weight FLOAT NULL DEFAULT NULL, impressions INT NOT NULL DEFAULT 0,
  conversions INT NOT NULL DEFAULT 0, UNIQUE INDEX experiment_options (experiment ASC, experiment_option ASC))`, [this.tables.experiments], (err) => {
      if (err){
        console.error(err, 'Creating the experiments table failed');
      } else {
        // Add new experiments
        for (let e in this.experiments) {
          const ops = this.experiments[e].options;
          for (let o in ops) {
            this.pool.query(`INSERT INTO ?? (experiment, experiment_option) VALUES (?, ?) ON DUPLICATE KEY UPDATE experiment_option=experiment_option`, [this.tables.experiments, e, ops[o]], (err) => {
              if (err){
                console.error(err, `Inserting the experiment option ${ops[o]} under ${e} failed`);
              }
            });
          }
        }
      }
    });
  }

  addUserOption(user_id, experiment_id, callback) {
    const chosen_option = this.generateRandomOption(user_id, this.experiments[experiment_id].options);
    this.pool.query(`INSERT INTO ?? (user_id, experiment, experiment_option) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE user_id=user_id`, [this.tables.users, user_id, experiment_id, chosen_option], (err) => {
      if (err){
        console.error(err, `Inserting a user option failed: ${user_id}, ${experiment_id} - ${chosen_option}`);
      }
      this.addImpression(experiment_id, chosen_option, callback);
    });
  }

  getUserOption(user_id, experiment_id, callback) {
    this.pool.query(`SELECT experiment_option FROM ?? WHERE user_id = ? AND experiment = ? LIMIT 1`, [this.tables.users, user_id, experiment_id], (err, result) => {
      if (err){
        console.error(err, `Select user option failed: ${user_id}, ${experiment_id}`);
        callback(this.experiments[experiment_id][0]);
      } else {
        callback(result[0]['experiment_option']);
      }
    });
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.pool.query(`UPDATE ?? SET impressions = impressions + 1 WHERE experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.experiments, experiment_id, chosen_option], (err) => {
      if (err){
        console.error(err, `Incrementing impressions failed: ${experiment_id}, ${chosen_option}`);
      }
      callback();
    });
  }

  addConversion(experiment_id, user_id, callback) {
    this.getUserOption(user_id, experiment_id, (chosen_option) => {
      this.pool.query(`UPDATE ?? SET converted = 1 WHERE user_id = ? AND  experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.users, user_id, experiment_id, chosen_option], (err, result) => {
        if (err){
          console.error(err, `Updating conversion failed: ${user_id}, ${experiment_id} - ${chosen_option}`);
          callback();
        } else if (result.changedRows === 1) {
          this.pool.query(`UPDATE ?? SET conversions = conversions + 1 WHERE experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.experiments, experiment_id, chosen_option], (err) => {
            if (err){
              console.error(err, `Incrementing conversions failed: ${experiment_id}, ${chosen_option}`);
            }
            callback();
          });
        } else {
          callback();
        }
      });
    });
  }

  getResults(callback) {
    this.pool.query(`SELECT * FROM ??`, [this.tables.experiments], (err, results) => {
      if (err){
        console.error(err, 'Fetching experiments failed');
        callback({results: []});
      } else {
        callback({results: results});
      }
    });
  }
}

class SplitGui {
  constructor() {
    this.hbs = require('handlebars');
    this.fs  = require('fs');
  }

  render(req, res, results) {
    console.log(JSON.stringify(results, null, 2));
    const view_path = __dirname + '/views/main.handlebars';
    this.fs.readFile(view_path, 'utf-8', (err, data) => {
      if (err) {
        res.send(`

Express-Split error

Could not read the template file

${JSON.stringify(err, null, 2)}

Results data

${JSON.stringify(results, null, 2)}

        `);
        res.end();
        return;
      }
      const template = this.hbs.compile(data);
      res.send(template({experiments: results}));
    });
  }
}

const seeded_rand = (seed) => {
  seed = (seed * 9301 + 49297) % 233280;
  return seed / 233280;
};

module.exports = ExpressSplit;
<commit_msg>fix use Object.keys().foreach instead of for...in<commit_after>"use strict";

const uuid = require('node-uuid');

const ExpressSplit = (user_options) => {
  const default_options = {
    experiments: {},
    storage:                     'in-memory', // in-memory, mysql
    db_pool:                     false,
    db_table_experiments:        'split_experiments',
    db_table_users:              'split_users',
    use_cookies:                 false,
    cookie_name:                 '_splituid',
    cookie_max_age:              15552000000 // 180 days in milliseconds
  };
  const options = Object.assign(default_options, user_options);

  const storage = new SplitStorage(options);
  const gui     = new SplitGui();

  const check_to_set_cookie = (req, res) => {
    if (options.use_cookies === true) {
      if (req.split.id === false) {
        if (req.cookies[options.cookie_name]) {
          req.split.id = parseInt(req.cookies[options.cookie_name].substring(0, 9), 10);
        } else {
          const uid = parseInt(uuid.v4().replace(/[^0-9]/g, '').substring(0, 9), 10);
          req.split.id = uid;
        }
      }
      res.cookie(options.cookie_name, req.split.id, { maxAge: options.cookie_max_age });
    }
  };

  return function(req, res, next) {
    req.split = {
      id: false,
      set_id: (user_id) => {
        if (!isNaN(user_id) &&
            parseInt(Number(user_id)) === user_id &&
            !isNaN(parseInt(user_id, 10))) {
          req.split.id = user_id;
        } else {
          throw new Error(`Split set_id() must receive an integer`);
        }
      },
      start: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        storage.addUserOption(req.split.id, experiment_id, callback);
      },
      get: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        return storage.getUserOption(req.split.id, experiment_id, callback);
      },
      finish: (experiment_id, callback) => {
        check_to_set_cookie(req, res);
        storage.addConversion(experiment_id, req.split.id, callback);
      },
      results: (callback) => {
        return storage.getResults(callback);
      },
      gui: (req, res) => {
        let experiments = {};
        return storage.getResults((experiments_results) => {
          Object.keys(experiments_results.results).forEach((r) => {
            const result = experiments_results.results[r];
            const experiment_name = r;
            if (!experiments.hasOwnProperty(experiment_name)) {
              experiments[experiment_name] = {results: {}};
            }
            delete result.experiment;
            experiments[experiment_name].results = result;
          })
          gui.render(req, res, experiments);
        });
      },
      __test: () => {
        // Exposes private objects for unit testing
        return {
          user_options:    user_options,
          default_options: default_options,
          options:         options,
          storage:         storage
        }
      }
    };
    next();
}};

class SplitStorage {
  constructor(options) {
    if (options.storage == 'in-memory') {
      this.storage = new SplitStorageInMemory(options);
    } else if (options.storage == 'mysql') {
      this.storage = new SplitStorageMysql(options);
    } else {
      throw new Error(`Split does not support the storage type ${options.storage}`);
    }
  }

  addUserOption(user_id, experiment_id, callback) {
    this.storage.addUserOption(user_id, experiment_id, callback);
  }

  getUserOption(user_id, experiment_id, callback) {
    return this.storage.getUserOption(user_id, experiment_id, callback);
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.storage.addImpression(experiment_id, chosen_option, callback);
  }

  addConversion(experiment_id, user_id, callback) {
    this.storage.addConversion(experiment_id, user_id, callback);
  }

  getResults(callback) {
    return this.storage.getResults(callback);
  }
}

class SplitStorageAbstract {
  constructor() {
    const methods = ['addUserOption', 'getUserOption', 'addImpression', 'addConversion', 'getResults'];
    Object.keys(methods).forEach((m) => {
      if (this[methods[m]] === undefined) {
        throw new TypeError(`The method ${methods[m]} must be overriden when deriving from SplitStorageAbstract`);
      }
    })
  }

  generateRandomOption(user_id, experiment_options) {
    const chosen_index = Math.floor(seeded_rand(user_id) * experiment_options.length);
    return experiment_options[chosen_index];
  }
}

class SplitStorageInMemory extends SplitStorageAbstract {
  constructor(options) {
    super();
    this.users        = {};
    this.results      = {};
    this.experiments  = options.experiments;
    Object.keys(options.experiments).forEach((e) => {
      this.results[e] = {};
      const ops = options.experiments[e].options;
      Object.keys(ops).forEach((o) => {
        this.results[e][ops[o]] = {
          impressions: 0,
          conversions: 0
        };
      })
    })
  }

  addUserOption(user_id, experiment_id, callback) {
    if (!this.users[user_id]) {
      this.users[user_id] = {};
    }
    if (!this.users[user_id][experiment_id]) {
      const chosen_option = this.generateRandomOption(user_id, this.experiments[experiment_id].options);
      this.users[user_id][experiment_id] = {option: chosen_option, converted: false};
      this.addImpression(experiment_id, chosen_option, callback);
    } else {
      callback();
    }
  }

  getUserOption(user_id, experiment_id, callback) {
    try {
      callback(this.users[user_id][experiment_id].option);
    } catch (e) {
      callback(this.experiments[experiment_id].options[0]);
    }
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.results[experiment_id][chosen_option].impressions++;
    callback();
  }

  addConversion(experiment_id, user_id, callback) {
    if (this.users[user_id][experiment_id].converted === false) {
      this.getUserOption(user_id, experiment_id, (option) => {
        this.results[experiment_id][option].conversions++;
        this.users[user_id][experiment_id].converted = true;
        callback();
      });
    } else {
      callback();
    }
  }

  getResults(callback) {
    callback({results: this.results});
  }
}

class SplitStorageMysql extends SplitStorageAbstract {
  // The construction of the tables is async, assuming that it won't take long and no users will be missed
  constructor(options) {
    super();
    this.pool   = options.db_pool;
    this.tables = {
      experiments: options.db_table_experiments,
      users:       options.db_table_users
    };
    this.experiments = options.experiments;
    // Create tables
    this.pool.query(`CREATE TABLE IF NOT EXISTS ?? (user_id INT NOT NULL, experiment VARCHAR(45) NOT NULL, experiment_option VARCHAR(45) NOT NULL, converted TINYINT(1) NOT NULL DEFAULT 0, UNIQUE INDEX user_option (user_id ASC, experiment ASC, experiment_option ASC))`, [this.tables.users], (err) => {
      if (err){
        console.error(err, 'Creating the users table failed');
      }
    });
    this.pool.query(`CREATE TABLE IF NOT EXISTS ?? (experiment VARCHAR(45) NOT NULL, experiment_option VARCHAR(45) NOT NULL, weight FLOAT NULL DEFAULT NULL, impressions INT NOT NULL DEFAULT 0,
  conversions INT NOT NULL DEFAULT 0, UNIQUE INDEX experiment_options (experiment ASC, experiment_option ASC))`, [this.tables.experiments], (err) => {
      if (err){
        console.error(err, 'Creating the experiments table failed');
      } else {
        // Add new experiments
        Object.keys(this.experiments).forEach((e) => {
          const ops = this.experiments[e].options;
          Object.keys(ops).forEach((o) => {
            this.pool.query(`INSERT INTO ?? (experiment, experiment_option) VALUES (?, ?) ON DUPLICATE KEY UPDATE experiment_option=experiment_option`, [this.tables.experiments, e, ops[o]], (err) => {
              if (err){
                console.error(err, `Inserting the experiment option ${ops[o]} under ${e} failed`);
              }
            });
          })
        })
      }
    });
  }

  addUserOption(user_id, experiment_id, callback) {
    const chosen_option = this.generateRandomOption(user_id, this.experiments[experiment_id].options);
    this.pool.query(`INSERT INTO ?? (user_id, experiment, experiment_option) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE user_id=user_id`, [this.tables.users, user_id, experiment_id, chosen_option], (err) => {
      if (err){
        console.error(err, `Inserting a user option failed: ${user_id}, ${experiment_id} - ${chosen_option}`);
      }
      this.addImpression(experiment_id, chosen_option, callback);
    });
  }

  getUserOption(user_id, experiment_id, callback) {
    this.pool.query(`SELECT experiment_option FROM ?? WHERE user_id = ? AND experiment = ? LIMIT 1`, [this.tables.users, user_id, experiment_id], (err, result) => {
      if (err){
        console.error(err, `Select user option failed: ${user_id}, ${experiment_id}`);
        callback(this.experiments[experiment_id][0]);
      } else {
        callback(result[0]['experiment_option']);
      }
    });
  }

  addImpression(experiment_id, chosen_option, callback) {
    this.pool.query(`UPDATE ?? SET impressions = impressions + 1 WHERE experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.experiments, experiment_id, chosen_option], (err) => {
      if (err){
        console.error(err, `Incrementing impressions failed: ${experiment_id}, ${chosen_option}`);
      }
      callback();
    });
  }

  addConversion(experiment_id, user_id, callback) {
    this.getUserOption(user_id, experiment_id, (chosen_option) => {
      this.pool.query(`UPDATE ?? SET converted = 1 WHERE user_id = ? AND  experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.users, user_id, experiment_id, chosen_option], (err, result) => {
        if (err){
          console.error(err, `Updating conversion failed: ${user_id}, ${experiment_id} - ${chosen_option}`);
          callback();
        } else if (result.changedRows === 1) {
          this.pool.query(`UPDATE ?? SET conversions = conversions + 1 WHERE experiment = ? AND experiment_option = ? LIMIT 1`, [this.tables.experiments, experiment_id, chosen_option], (err) => {
            if (err){
              console.error(err, `Incrementing conversions failed: ${experiment_id}, ${chosen_option}`);
            }
            callback();
          });
        } else {
          callback();
        }
      });
    });
  }

  getResults(callback) {
    this.pool.query(`SELECT * FROM ??`, [this.tables.experiments], (err, results) => {
      if (err){
        console.error(err, 'Fetching experiments failed');
        callback({results: []});
      } else {
        callback({results: results});
      }
    });
  }
}

class SplitGui {
  constructor() {
    this.hbs = require('handlebars');
    this.fs  = require('fs');
  }

  render(req, res, results) {
    console.log(JSON.stringify(results, null, 2));
    const view_path = __dirname + '/views/main.handlebars';
    this.fs.readFile(view_path, 'utf-8', (err, data) => {
      if (err) {
        res.send(`

Express-Split error

Could not read the template file

${JSON.stringify(err, null, 2)}

Results data

${JSON.stringify(results, null, 2)}

        `);
        res.end();
        return;
      }
      const template = this.hbs.compile(data);
      res.send(template({experiments: results}));
    });
  }
}

const seeded_rand = (seed) => {
  seed = (seed * 9301 + 49297) % 233280;
  return seed / 233280;
};

module.exports = ExpressSplit;
<|endoftext|>


<commit_before>// namespace
var App = App || {};

App.LoadingState = (function () {
    "use strict";

    var fn = function (game) {
        Phaser.State.call(this, game);
    };

    fn.prototype = Object.create(Phaser.State.prototype);
    fn.prototype.constructor = fn;

    fn.prototype.init = function () {
        this.game.global.mob = {};
    };

    fn.prototype.preload = function () {
        // load json configuration files
        this.game.load.json('assetsConfig', 'assets/json/assets.json');
        this.game.load.spritesheet('alienBlue', 'assets/images/alienBlue_sheet.png', 66, 96);
    };

    fn.prototype.create = function () {
        this.game.asset_manager = new App.AssetManager(this.game);
        this.game.state.start('WebSocket');
    };

    return fn;
})();
<commit_msg>Add some more console debug spewage to help me follow the loading.js State flow and order of execution. (No functional changes.)<commit_after>// namespace
var App = App || {};

App.LoadingState = (function () {
    "use strict";

    console.log("LoadingState Compiling ...");
    var fn = function (game) {
        console.log("LoadingState.constructor Running...");
        Phaser.State.call(this, game);
    };

    fn.prototype = Object.create(Phaser.State.prototype);
    fn.prototype.constructor = fn;

    fn.prototype.init = function () {
        this.game.global.mob = {};
    };

    fn.prototype.preload = function () {
        console.log("LoadingState.preload Running...");
        // load json configuration files
        this.game.load.json('assetsConfig', 'assets/json/assets.json');
        this.game.load.spritesheet('alienBlue', 'assets/images/alienBlue_sheet.png', 66, 96);
    };

    fn.prototype.create = function () {
        console.log("LoadingState.create Running...");
        this.game.asset_manager = new App.AssetManager(this.game);
        this.game.state.start('WebSocket');
    };

    return fn;
})();
<|endoftext|>


<commit_before>// Import React
import React from "react";

// Import Spectacle Core tags
import {
  BlockQuote,
  Cite,
  Deck,
  Heading,
  ListItem,
  List,
  Quote,
  Slide,
  Text,
  Appear,
  Typeface,
} from "spectacle";

// Import image preloader util
import preloader from "spectacle/lib/utils/preloader";

// Import theme
import createTheme from "spectacle/lib/themes/default";

// Require CSS
require("normalize.css");
require("spectacle/lib/themes/default/index.css");

const images = {
};

preloader(images);

const theme = createTheme({
  primary: "linear-gradient(to left, #A43931, #1D4350 )",
  secondary: "#1D4350", //blue
  tertiary: "#fff",
  quartenary: "#CECECE",
  quinary: "#DC2424",
  senary: "#4A569D"
}, {
  primary: { name: "Lato", googleFont: true, styles: [ "400", "700i" ] },
  secondary: { name: "Raleway", googleFont: true, styles: [ "400", "700i" ] }
});

export default class Presentation extends React.Component {
  render() {
    return (
      <Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}>
        <Slide transition={["zoom"]} bgColor="primary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} fit caps lineHeight={1} textColor="tertiary" textFont="secondary">
            Civis
            </Heading>
          </Typeface>
            <Appear>
              <Text transition={["zoom"]}
                margin="10px 0 0" textColor="tertiary" size={1} fit bold>
                Be a Better Citizen
              </Text>
            </Appear>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} >THE PROBLEM</Heading>
          </Typeface>
          <List>
            <Appear>
              <ListItem>Slow and Complicated Government Websites</ListItem>
            </Appear>
            <Appear>
              <ListItem>Information Overload</ListItem>
            </Appear>
            <Appear>
              <ListItem>Busy Lives</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} >THE PROBLEM</Heading>
          </Typeface>

            <Quote>
              A modern web application that makes it easy to stay informed and
              in touch with your representatives.
            </Quote>

        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Address Form gif</Heading>
          <List>
            <Appear>
              <ListItem>Find your Represntatives in Database</ListItem>
            </Appear>
            <Appear>
              <ListItem>Search by Address or Zip Code</ListItem>
            </Appear>
            <Appear>
              <ListItem>Geolocation</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Rep Index to Rep Page Gif</Heading>
          <List>
            <Appear>
              <ListItem>News Alerts</ListItem>
            </Appear>
            <Appear>
              <ListItem>General Information</ListItem>
            </Appear>
            <Appear>
              <ListItem>Articles</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>APIs</Heading>
          <List>
            <Appear>
              <ListItem>NYT</ListItem>
            </Appear>
            <Appear>
              <ListItem>ProPublica</ListItem>
            </Appear>
            <Appear>
              <ListItem>Google Maps / Civic Information</ListItem>
            </Appear>
            <Appear>
              <ListItem>Wikipedia</ListItem>
            </Appear>
            <Appear>
              <ListItem>Twitter</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Native App gif in phone image</Heading>
          <List>
            <Appear>
              <ListItem>Native Capability for iOS and Android</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Roadblocks</Heading>
          <List>
            <Appear>
              <ListItem>Article Search</ListItem>
            </Appear>
            <Appear>
              <ListItem>Representatives</ListItem>
            </Appear>
            <Appear>
              <ListItem>APIs</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Roadmap</Heading>
          <List>
            <Appear>
              <ListItem>Local Representatives</ListItem>
            </Appear>
            <Appear>
              <ListItem>Better ways to contact: twitter, email form</ListItem>
            </Appear>
            <Appear>
              <ListItem>Automated Alerts: mailers</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Techstack</Heading>
        </Slide>
        <Slide transition={["zoom"]} bgColor="primary">
          <Heading size={1} fit caps lineHeight={1} textColor="tertiary">
            Civis
          </Heading>
        </Slide>
      </Deck>
    );
  }
}
<commit_msg>updates slide 1 and 2<commit_after>// Import React
import React from "react";

// Import Spectacle Core tags
import {
  BlockQuote,
  Cite,
  Deck,
  Heading,
  ListItem,
  List,
  Quote,
  Slide,
  Text,
  Appear,
  Typeface,
} from "spectacle";

// Import image preloader util
import preloader from "spectacle/lib/utils/preloader";

// Import theme
import createTheme from "spectacle/lib/themes/default";

// Require CSS
require("normalize.css");
require("spectacle/lib/themes/default/index.css");

const images = {
};

preloader(images);

const theme = createTheme({
  primary: "linear-gradient(to left, #A43931, #1D4350 )",
  secondary: "#1D4350", //blue
  tertiary: "#fff",
  quartenary: "#757575",// grey
  quinary: "#A43931", //red
  senary: "000000"

}, {
  primary: { name: "Lato", googleFont: true, styles: [ "400", "700i" ] },
  secondary: { name: "Raleway", googleFont: true, styles: [ "400", "700i" ] }
});

export default class Presentation extends React.Component {
  render() {
    return (
      <Deck transition={["zoom", "slide"]} transitionDuration={500} theme={theme}>
        <Slide transition={["zoom"]} bgColor="primary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} fit caps lineHeight={1} textColor="tertiary" textFont="secondary">
            Civis
            </Heading>
          </Typeface>
            <Appear>
              <Text transition={["zoom"]}
                margin="10px 0 0" textColor="tertiary" size={1} fit bold>
                Democracy Depends on You!
              </Text>
            </Appear>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading textColor="quinary" size={1} >Why Civis?</Heading>

          </Typeface>
            <Appear>
              <Text textColor="senary">Civis makes it easy to stay informed and in touch with your legislators.
              </Text>
            </Appear>
            <Appear>
              <Text textColor="senary">For people who want to be politically active!</Text>
            </Appear>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} fit caps lineHeight={1}>THE PROBLEM</Heading>
          </Typeface>
          <List>
            <Appear>
              <ListItem>Slow and Complicated Government Websites</ListItem>
            </Appear>
            <Appear>
              <ListItem>Information Overload</ListItem>
            </Appear>
            <Appear>
              <ListItem>Busy Lives</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Typeface googleFont="Raleway" weight={600}>
            <Heading size={1} fit caps lineHeight={1} >SOLUTION</Heading>
          </Typeface>

            <Quote>
              A modern web application that makes it easy to stay informed and
              in touch with your representatives.
            </Quote>

        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Address Form gif</Heading>
          <List>
            <Appear>
              <ListItem>Find your Represntatives in Database</ListItem>
            </Appear>
            <Appear>
              <ListItem>Search by Address or Zip Code</ListItem>
            </Appear>
            <Appear>
              <ListItem>Geolocation</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Rep Index to Rep Page Gif</Heading>
          <List>
            <Appear>
              <ListItem>News Alerts</ListItem>
            </Appear>
            <Appear>
              <ListItem>General Information</ListItem>
            </Appear>
            <Appear>
              <ListItem>Articles</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>APIs</Heading>
          <List>
            <Appear>
              <ListItem>NYT</ListItem>
            </Appear>
            <Appear>
              <ListItem>ProPublica</ListItem>
            </Appear>
            <Appear>
              <ListItem>Google Maps / Civic Information</ListItem>
            </Appear>
            <Appear>
              <ListItem>Wikipedia</ListItem>
            </Appear>
            <Appear>
              <ListItem>Twitter</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="tertiary" textColor="secondary">
          <Heading size={1} textColor="secondary">FEATURES</Heading>
          <Heading size={5}>Native App gif in phone image</Heading>
          <List>
            <Appear>
              <ListItem>Native Capability for iOS and Android</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Roadblocks</Heading>
          <List>
            <Appear>
              <ListItem>Article Search</ListItem>
            </Appear>
            <Appear>
              <ListItem>Representatives</ListItem>
            </Appear>
            <Appear>
              <ListItem>APIs</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Roadmap</Heading>
          <List>
            <Appear>
              <ListItem>Local Representatives</ListItem>
            </Appear>
            <Appear>
              <ListItem>Better ways to contact: twitter, email form</ListItem>
            </Appear>
            <Appear>
              <ListItem>Automated Alerts: mailers</ListItem>
            </Appear>
          </List>
        </Slide>
        <Slide transition={["fade"]} bgColor="secondary" textColor="tertiary">
          <Heading size={1}>Techstack</Heading>
        </Slide>
        <Slide transition={["zoom"]} bgColor="primary">
          <Heading size={1} fit caps lineHeight={1} textColor="tertiary">
            Civis
          </Heading>
        </Slide>
      </Deck>
    );
  }
}
<|endoftext|>


<commit_before>HTMLCollection.prototype.forEach = function (callback) {
	Array.prototype.forEach.call(this, callback);
};

NodeList.prototype.forEach = function (callback) {
	Array.prototype.forEach.call(this, callback);
};

RegExp.quote = function (str) {
	return (str + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
};

if (typeof String.prototype.startsWith !== 'function') {
	String.prototype.startsWith = function (str) {
		return this.slice(0, str.length) === str;
	};
}

if (!Math.sign) {
	Math.sign = function (x) {
		x = +x;

		if (x === 0 || isNaN(x)) {
			return x;
		}

		return x > 0 ? 1 : -1;
	}
}

function prev(node, selector) {
	var children = node.parentNode.querySelectorAll(selector);
	var index = Array.prototype.indexOf.call(children, node);

	return index > 0 ? children.item(index - 1) : null;
}

function next(node, selector) {
	var children = node.parentNode.querySelectorAll(selector);
	var index = Array.prototype.indexOf.call(children, node);

	return index < children.length - 1 ? children.item(index + 1) : null;
}

function isTouchDevice() {
	return "ontouchstart" in window;
}

function getSelectionText() {
	var text = "";
	if (window.getSelection) {
		text = window.getSelection().toString();
	} else if (document.selection && document.selection.type != "Control") {
		text = document.selection.createRange().text;
	}
	return text;
}

// thanks to https://remysharp.com/2010/07/21/throttling-function-calls
function throttle(fn, threshhold, scope) {
	threshhold || (threshhold = 250);
	var last, deferTimer;
	return function () {
		var context = scope || this;

		var now = +new Date,
			args = arguments;
		if (last && now < last + threshhold) {
			// hold on to it
			clearTimeout(deferTimer);
			deferTimer = setTimeout(function () {
				last = now;
				fn.apply(context, args);
			}, threshhold);
		} else {
			last = now;
			fn.apply(context, args);
		}
	};
}
<commit_msg>Simplify forEach prototype<commit_after>HTMLCollection.prototype.forEach = Array.prototype.forEach;
NodeList.prototype.forEach = Array.prototype.forEach;

RegExp.quote = function (str) {
	return (str + '').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
};

if (typeof String.prototype.startsWith !== 'function') {
	String.prototype.startsWith = function (str) {
		return this.slice(0, str.length) === str;
	};
}

if (!Math.sign) {
	Math.sign = function (x) {
		x = +x;

		if (x === 0 || isNaN(x)) {
			return x;
		}

		return x > 0 ? 1 : -1;
	}
}

function prev(node, selector) {
	var children = node.parentNode.querySelectorAll(selector);
	var index = Array.prototype.indexOf.call(children, node);

	return index > 0 ? children.item(index - 1) : null;
}

function next(node, selector) {
	var children = node.parentNode.querySelectorAll(selector);
	var index = Array.prototype.indexOf.call(children, node);

	return index < children.length - 1 ? children.item(index + 1) : null;
}

function isTouchDevice() {
	return "ontouchstart" in window;
}

function getSelectionText() {
	var text = "";
	if (window.getSelection) {
		text = window.getSelection().toString();
	} else if (document.selection && document.selection.type != "Control") {
		text = document.selection.createRange().text;
	}
	return text;
}

// thanks to https://remysharp.com/2010/07/21/throttling-function-calls
function throttle(fn, threshhold, scope) {
	threshhold || (threshhold = 250);
	var last, deferTimer;
	return function () {
		var context = scope || this;

		var now = +new Date,
			args = arguments;
		if (last && now < last + threshhold) {
			// hold on to it
			clearTimeout(deferTimer);
			deferTimer = setTimeout(function () {
				last = now;
				fn.apply(context, args);
			}, threshhold);
		} else {
			last = now;
			fn.apply(context, args);
		}
	};
}
<|endoftext|>


<commit_before>// Script to find regressions
//
// It should use as few liberator methods as possible, but fall back to standard mozilla/DOM methods
// The reason it, we don't want to find regressions in the regressions script, and it should survive
// massive changes in the internal liberator API, but just test for functionality of
// user-visible commands/mappings
//
// NOTE: It is preferable to run this script in a clean profile or at least do NOT use
// :mkvimperatorrc afterwards, as it can remove commands/mappings, etc.
//
// Usage: :regr[essions]

/////////////////////////////////////////////////////////////////////////////////////////
// Put definitions here which might change due to internal liberator refactoring
/////////////////////////////////////////////////////////////////////////////////////////

var multilineOutput  = document.getElementById("liberator-multiline-output")
var singlelineOutput = document.getElementById("liberator-commandline-command")

/////////////////////////////////////////////////////////////////////////////////////////
// TESTS
//
// They are run in order, so you can specify commands which expect side effects of a
// previous command
/////////////////////////////////////////////////////////////////////////////////////////

// A series of ex commands or mappings, each with a
// function checking whether the command succeeded
// If the string starts with a ":" it is executed as an ex command, otherwise as a mapping
// You can also mix commands mappings
let tests = [
    { cmds: [":!dir"],
      verify: function () Tests.getMultilineOutput().length > 10 },
    { cmds: [":abbr VIMP vimperator labs", ":abbr"],
      verify: function () Tests.getMultilineOutput().indexOf("vimperator labs") >= 0 },
    { cmds: [":unabbr VIMP", ":abbr"],
      verify: function () Tests.getMultilineOutput().indexOf("vimperator labs") == -1 },
    { cmds: [":bmarks"],
      verify: function () Tests.getMultilineOutput().length > 100 },
    { cmds: [":echo \"test\""],
      verify: function () Tests.getOutput() == "test" },
    // { cmds: [":echomsg \"testmsg\""],
    //   verify: function () Tests.getOutput() == "testmsg" },
    // { cmds: [":echoerr \"testerr\""],
    //   verify: function () Tests.getOutput() == "testerr" },
    { cmds: ["gg", "<C-f>"], // NOTE: does not work when there is no page to scroll, we should load a large page before doing these tests
      verify: function () this._initialPos.y != Tests.getBufferPosition().y,
      init: function () this._initialPos = Tests.getBufferPosition() }
];

// these functions highly depend on the liberator API, so use ex command tests whenever possible
let functions = [
    function () { return bookmarks.get("").length > 0 }, // will fail for people without bookmarks :( Might want to add one before
    function () { return history.get("").length > 0 }
];

/////////////////////////////////////////////////////////////////////////////////////////
// functions below should be as generic as possible, and not require being rewritten
// even after doing major vimperator refactoring
/////////////////////////////////////////////////////////////////////////////////////////

// I want to move the functions to top-level for brevity, but not until we have
// solved the namespace issue of loading simple function () { ... } declarations
// into "modules".
var Tests = {
    resetEnvironment: function ()
    {
        multilineOutput.contentDocument.body.innerHTML = "";
        singlelineOutput.value = "";
    },

    getOutput:           function () multilineOutput.contentDocument.body.textContent || singlelineOutput.value,
    getMultilineOutput:  function () multilineOutput.contentDocument.body.textContent,
    getSinglelineOutput: function () singlelineOutput.value,

    getBufferPosition: function ()
    {
        let win = window.content;
        return { x: win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0,
                 y: win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0 }
    },

    // TODO: need to find a way to wait for page load
    getLocation: function () window.content.document.location.href
}

commands.addUserCommand(["regr[essions]"],
    "Run regression tests",
    function (args, special)
    {
        // TODO: might need to increase the 'messages' option temprarily
        // TODO: count (better even range) support to just run test 34 of 102
        // TODO: bang support to either: a) run commands like deleting bookmarks which
        //       should only be done in a clean profile or b) run functions and not
        //       just ex command tests; Yet to be decided

        function run ()
        {
			let now = Date.now();
            let totalTests = tests.length + functions.length;
			let successfulTests = 0;
            let currentTest = 1;

            // 1.) run commands and mappings tests
            tests.forEach(function (test) {
                let testDescription = util.clip(test.cmds.join(" -> "), 80);
                liberator.echomsg("Running test " + currentTest++ + " of " + totalTests + ": " + testDescription);
                Tests.resetEnvironment();
                if ("init" in test)
                    test.init();

                test.cmds.forEach(function (cmd) {
                    if (cmd.indexOf(":") == 0)
                        liberator.execute(cmd);
                    else
                        events.feedkeys(cmd);
                });

                if (!test.verify())
                    liberator.echoerr("Test failed: " + testDescription);
				else
					successfulTests++;
            });

            // 2.) Run function tests
            functions.forEach(function (func) {
                liberator.echomsg("Running test " + currentTest++ + " of " + totalTests + ": " + util.clip(func.toString().replace(/[\s\n]+/gm, " "), 80));
                Tests.resetEnvironment();

                if (!func())
                    liberator.echoerr("Test failed!");
				else
					successfulTests++;
            });

			liberator.echomsg(successfulTests + " of " + totalTests + " tests successfully completed in " + ((Date.now() - now) / 1000.0) + " msec");
            liberator.execute(":messages");
        }

        if (special)
            run();
        else
            commandline.input("Running tests should always be done in a new profile. Use ! or confirm with 'yes' to continue:", function (res) { if (res == "yes") run(); } );
    },
    {
        bang: true,
        argCount: 0
    });

// vimperator: set et ts=4 sw=4 :
<commit_msg>added ability to just run single tests of the regression suite<commit_after>// Script to find regressions
//
// It should use as few liberator methods as possible, but fall back to standard mozilla/DOM methods
// The reason it, we don't want to find regressions in the regressions script, and it should survive
// massive changes in the internal liberator API, but just test for functionality of
// user-visible commands/mappings
//
// NOTE: It is preferable to run this script in a clean profile or at least do NOT use
// :mkvimperatorrc afterwards, as it can remove commands/mappings, etc.
//
// Usage: :[count]regr[essions]
// When [count] is given, just run this test. TODO: move to :regressions [spec]?

/////////////////////////////////////////////////////////////////////////////////////////
// Put definitions here which might change due to internal liberator refactoring
/////////////////////////////////////////////////////////////////////////////////////////

var multilineOutput  = document.getElementById("liberator-multiline-output")
var singlelineOutput = document.getElementById("liberator-commandline-command")

/////////////////////////////////////////////////////////////////////////////////////////
// TESTS
//
// They are run in order, so you can specify commands which expect side effects of a
// previous command
/////////////////////////////////////////////////////////////////////////////////////////

// A series of ex commands or mappings, each with a
// function checking whether the command succeeded
// If the string starts with a ":" it is executed as an ex command, otherwise as a mapping
// You can also mix commands mappings
let tests = [
    { cmds: [":!dir"],
      verify: function () Tests.getMultilineOutput().length > 10 },
    { cmds: [":abbr VIMP vimperator labs", ":abbr"],
      verify: function () Tests.getMultilineOutput().indexOf("vimperator labs") >= 0 },
    { cmds: [":unabbr VIMP", ":abbr"],
      verify: function () Tests.getMultilineOutput().indexOf("vimperator labs") == -1 },
    { cmds: [":bmarks"],
      verify: function () Tests.getMultilineOutput().length > 100 },
    { cmds: [":echo \"test\""],
      verify: function () Tests.getOutput() == "test" },
    // { cmds: [":echomsg \"testmsg\""],
    //   verify: function () Tests.getOutput() == "testmsg" },
    // { cmds: [":echoerr \"testerr\""],
    //   verify: function () Tests.getOutput() == "testerr" },
    { cmds: ["gg", "<C-f>"], // NOTE: does not work when there is no page to scroll, we should load a large page before doing these tests
      verify: function () this._initialPos.y != Tests.getBufferPosition().y,
      init: function () this._initialPos = Tests.getBufferPosition() }
];

// these functions highly depend on the liberator API, so use ex command tests whenever possible
let functions = [
    function () { return bookmarks.get("").length > 0 }, // will fail for people without bookmarks :( Might want to add one before
    function () { return history.get("").length > 0 }
];

/////////////////////////////////////////////////////////////////////////////////////////
// functions below should be as generic as possible, and not require being rewritten
// even after doing major vimperator refactoring
/////////////////////////////////////////////////////////////////////////////////////////

// I want to move the functions to top-level for brevity, but not until we have
// solved the namespace issue of loading simple function () { ... } declarations
// into "modules".
var Tests = {
    resetEnvironment: function ()
    {
        multilineOutput.contentDocument.body.innerHTML = "";
        singlelineOutput.value = "";
    },

    getOutput:           function () multilineOutput.contentDocument.body.textContent || singlelineOutput.value,
    getMultilineOutput:  function () multilineOutput.contentDocument.body.textContent,
    getSinglelineOutput: function () singlelineOutput.value,

    getBufferPosition: function ()
    {
        let win = window.content;
        return { x: win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0,
                 y: win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0 }
    },

    // TODO: need to find a way to wait for page load
    getLocation: function () window.content.document.location.href
}

commands.addUserCommand(["regr[essions]"],
    "Run regression tests",
    function (args, special, count)
    {
        // TODO: might need to increase the 'messages' option temprarily
        // TODO: count (better even range) support to just run test 34 of 102
        // TODO: bang support to either: a) run commands like deleting bookmarks which
        //       should only be done in a clean profile or b) run functions and not
        //       just ex command tests; Yet to be decided

        function run ()
        {
			let now = Date.now();
            let totalTests = tests.length + functions.length;
			let successfulTests = 0;
            let currentTest = 0;

			// TODO: might want to unify 'tests' and 'functions' handling
            // 1.) run commands and mappings tests
			for (let [, test] in Iterator(tests))
			{
				if (count >= 1 && ++currentTest != count)
					continue;

                let testDescription = util.clip(test.cmds.join(" -> "), 80);
                liberator.echomsg("Running test " + currentTest + " of " + totalTests + ": " + testDescription);
                Tests.resetEnvironment();
                if ("init" in test)
                    test.init();

                test.cmds.forEach(function (cmd) {
                    if (cmd[0] == ":")
                        liberator.execute(cmd);
                    else
                        events.feedkeys(cmd);
                });

                if (!test.verify())
                    liberator.echoerr("Test " + currentTest + " failed: " + testDescription);
				else
					successfulTests++;
            }

            // 2.) Run function tests
			for (let [, func] in Iterator(functions))
			{
				if (count >= 1 && ++currentTest != count)
					continue;

                liberator.echomsg("Running test " + currentTest + " of " + totalTests + ": " + util.clip(func.toString().replace(/[\s\n]+/gm, " "), 80));
                Tests.resetEnvironment();

                if (!func())
                    liberator.echoerr("Test " + currentTest + " failed!");
				else
					successfulTests++;
            }

			liberator.echomsg(successfulTests + " of " + (count >= 1 ? 1 : totalTests) + " tests successfully completed in " + ((Date.now() - now) / 1000.0) + " msec");
            liberator.execute(":messages");
        }

        if (special)
            run();
        else
            commandline.input("Running tests should always be done in a new profile. Use ! or confirm with 'yes' to continue:", function (res) { if (res == "yes") run(); } );
    },
    {
        bang: true,
		argCount: 0,
		count: true
    });

// vimperator: set et ts=4 sw=4 :
<|endoftext|>