<commit_before>/*jslint browser: true, undef: true *//*global Ext*/
Ext.define('SlateAdmin.model.person.Relationship', {
    extend: 'Ext.data.Model',
    requires: [
//        'Jarvus.ext.override.data.CallbackValidation',
        'SlateAdmin.model.validator.CallbackValidator',
        'SlateAdmin.proxy.Records'
    ],

    idProperty: 'ID',

    fields: [
        {
            name: "ID",
            type: "int",
            allowNull: true
        },
        {
            name: "Class",
            type: "string",
            defaultValue: "Emergence\\People\\Relationship"
        },
        {
            name: "Created",
            type: "date",
            dateFormat: "timestamp",
            allowNull: true
        },
        {
            name: "CreatorID",
            type: "int",
            allowNull: true
        },
        {
            name: "PersonID",
            type: "int"
        },
        {
            name: "RelatedPersonID",
            type: "int",
            allowNull: true
        },
        {
            name: "Label",
            type: "string",
            allowNull: true
        },
        {
            name: "Notes",
            type: "string",
            allowNull: true
        },
        {
            name: 'RelatedPerson',
            allowNull: true
        },
        {
            name: 'InverseRelationship',
            allowNull: true
        }
    ],

    validators: {
        Class: 'presence',
        PersonID: 'presence',
        Label: 'presence',
        RelatedPerson: {
            type: 'callback',
            message: 'Select an existing person or provide a first and last name to add a new person',
            callback: function(val)  {
                return val && (val.ID || (val.FirstName && val.LastName));
            }
        },
        InverseRelationship: {
            type: 'callback',
            message: 'Enter an inverse label for this relationship',
            callback: function(val)  {
                return val && val.Label;
            }
        }
    },

    associations: [{
        type: 'hasOne',
        model: 'SlateAdmin.model.person.Relationship',
        associationKey: 'InverseRelationship',
        getterName: 'getInverseRelationship'
    }],

    proxy: {
        type: 'slaterecords',
        url: '/relationships',
        include: ['RelatedPerson', 'InverseRelationship']
    }
});
<commit_msg>removed commented dependency that has been replaced<commit_after>/*jslint browser: true, undef: true *//*global Ext*/
Ext.define('SlateAdmin.model.person.Relationship', {
    extend: 'Ext.data.Model',
    requires: [
        'SlateAdmin.model.validator.CallbackValidator',
        'SlateAdmin.proxy.Records'
    ],

    idProperty: 'ID',

    fields: [
        {
            name: "ID",
            type: "int",
            allowNull: true
        },
        {
            name: "Class",
            type: "string",
            defaultValue: "Emergence\\People\\Relationship"
        },
        {
            name: "Created",
            type: "date",
            dateFormat: "timestamp",
            allowNull: true
        },
        {
            name: "CreatorID",
            type: "int",
            allowNull: true
        },
        {
            name: "PersonID",
            type: "int"
        },
        {
            name: "RelatedPersonID",
            type: "int",
            allowNull: true
        },
        {
            name: "Label",
            type: "string",
            allowNull: true
        },
        {
            name: "Notes",
            type: "string",
            allowNull: true
        },
        {
            name: 'RelatedPerson',
            allowNull: true
        },
        {
            name: 'InverseRelationship',
            allowNull: true
        }
    ],

    validators: {
        Class: 'presence',
        PersonID: 'presence',
        Label: 'presence',
        RelatedPerson: {
            type: 'callback',
            message: 'Select an existing person or provide a first and last name to add a new person',
            callback: function(val)  {
                return val && (val.ID || (val.FirstName && val.LastName));
            }
        },
        InverseRelationship: {
            type: 'callback',
            message: 'Enter an inverse label for this relationship',
            callback: function(val)  {
                return val && val.Label;
            }
        }
    },

    associations: [{
        type: 'hasOne',
        model: 'SlateAdmin.model.person.Relationship',
        associationKey: 'InverseRelationship',
        getterName: 'getInverseRelationship'
    }],

    proxy: {
        type: 'slaterecords',
        url: '/relationships',
        include: ['RelatedPerson', 'InverseRelationship']
    }
});
<|endoftext|>


<commit_before>Ext.ux.NotificationMgr = {
    positions: [],
    heights: []
};
Ext.ux.Notification = function(config){
	this.config = Ext.apply({},config);
}
Ext.ux.Notification = Ext.extend(Ext.Window, {
	notificationType:'ERROR',
	initComponent : function(){
		Ext.apply(this, {
			//iconCls: this.iconCls || 'icon-notification-info',
			autoHeight: true,
			closeAction:'close',
			plain: false,
			shadow:false,
			draggable: false,
			bodyStyle: 'text-align:left;padding:10px;',
			resizable: false
		});
		if(this.autoDestroy){
			this.task = new Ext.util.DelayedTask(this.close, this);
		}else{
			this.closable = true;
		}
		Ext.ux.Notification.superclass.initComponent.call(this);
    },
	setMessage : function(msg){
		this.body.update(msg);
	},
	/**
	* Comment this section for now. Since for window when setting iconClass frame is checked,
	* which is currently overridden to avoid, that may result double icons in window title.
	* Whenever we decide not to override that, we may want to keep this method.
	* Because I saw that the overriden section is refactored in latest version of extjs.

	setTitle : function(title, iconCls){
        Ext.ux.Notification.superclass.setTitle.call(this, title, iconCls||this.iconCls);
    },*/
    onRender : function(ct, position) {
		Ext.ux.Notification.superclass.onRender.call(this, ct, position);
	},
	onDestroy : function(){
		Ext.ux.NotificationMgr.positions.remove(this.pos);
		Ext.ux.Notification.superclass.onDestroy.call(this);
	},
	afterShow : function(){
		Ext.ux.Notification.superclass.afterShow.call(this);
		this.on('move', function(){
			Ext.ux.NotificationMgr.positions.remove(this.pos);
			if(this.autoDestroy){
				this.task.cancel();
			}
		}, this);
		if(this.autoDestroy){
			this.task.delay(this.hideDelay || 5000);
		}
	},
	animShow : function(){
		this.pos = 0;
		var h = 0;
		while(Ext.ux.NotificationMgr.positions.indexOf(this.pos)>-1){
			h+=Ext.ux.NotificationMgr.heights[this.pos];
			this.pos++;
		}
		Ext.ux.NotificationMgr.positions.push(this.pos);
		Ext.ux.NotificationMgr.heights[this.pos] = this.getSize().height+10;

		this.el.alignTo(this.animateTarget || document, "br-tr", [ -1, -1-(h) ]);
		this.el.slideIn('b', {
			duration: .7
			, callback: this.afterShow
			, scope: this
		});
	},
	animHide : function(){
		Ext.ux.NotificationMgr.positions.remove(this.pos);
		if(this.el)
		this.el.ghost("b", {
			duration: 1
			, remove: false
		});
	},
	start: function(url){
		new Ext.ux.Notification.start(url);
	}
});
Ext.ns("Ext.ux.Notification");
Ext.ux.Notification.Base = function(){};
Ext.ux.Notification.Base = Ext.extend(Ext.util.Observable,{
	showNotification:  function(config) {
	    var win = new Ext.ux.Notification(Ext.apply({
	    	animateTarget: this.notificationEl
			, autoDestroy: false
			,notificationType:'ERROR'
	    }, config));
	    if(win.notificationType == "ERROR"){
	    	win.iconCls = 'icon-notification-error';
	    	win.bodyStyle += 'background-color:#fddcdc;';
	    }
	    if(win.notificationType == "WARNING"){
	    	win.iconCls = 'icon-notification-warning';
	    	win.bodyStyle += 'background-color:#fefcc7;';
	    }
	    if(win.notificationType == "INFO"){
	    	win.iconCls = 'icon-notification-info';
	    	win.bodyStyle += 'background-color:#fff;';
	    }
	    win.bodyStyle += 'padding-bottom:20px;';
	    win.show();
	    return win;
	},
	hideNotification: function(win, delay) {
	    if (win) {
	      (function() {
	        win.animHide();
	      }).defer(delay || 3000);
	    }
	},
	growl: function(TITLE,MESSAGE,TYPE,DURATION){
		var n = this.showNotification({
			width:300,
			notificationType:TYPE,
			title:TITLE,
			html:MESSAGE
		});
		this.hideNotification(n, DURATION*1000);
	}
})
Ext.ux.InstantNotification = Ext.extend(Ext.ux.Notification.Base,{
	constructor: function(config) {
		Ext.ux.InstantNotification.superclass.constructor.call(this, config);
		this.notificationEl = Ext.get("growl-notification-el")?Ext.get("growl-notification-el"):Ext.DomHelper.append(Ext.getBody(),{tag:'div',id:'growl-notification-el',style:'width:300px;position:absolute;bottom:0;right:0'});
		this.growl(config.title,config.message,"INFO",10)
	}
});

Ext.ux.Notification.start = function(url){
	Ext.onReady(function(){
		var notificationEl = Ext.get("growl-notification-el")?Ext.get("growl-notification-el"):Ext.DomHelper.append(Ext.getBody(),{tag:'div',id:'growl-notification-el',style:'width:300px;position:absolute;bottom:0;right:0'});
		this.showNotification = function(config) {
		    var win = new Ext.ux.Notification(Ext.apply({
		    	animateTarget: notificationEl
				, autoDestroy: false
				,notificationType:'ERROR'
		    }, config));
		    if(win.notificationType == "ERROR"){
		    	win.iconCls = 'icon-notification-error';
		    	win.bodyStyle += 'background-color:#fddcdc;';
		    }
		    if(win.notificationType == "WARNING"){
		    	win.iconCls = 'icon-notification-warning';
		    	win.bodyStyle += 'background-color:#fefcc7;';
		    }
		    if(win.notificationType == "INFO"){
		    	win.iconCls = 'icon-notification-info';
		    	win.bodyStyle += 'background-color:#fff;';
		    }
		    win.bodyStyle += 'padding-bottom:20px;';
		    win.show();

		    return win;
		};
		this.hideNotification = function(win, delay) {
		    if (win) {
		      (function() {
		        win.animHide();
		      }).defer(delay || 3000);
		    }
		};
		this.growl = function(TITLE,MESSAGE,TYPE,DURATION){
			var n = this.showNotification({
				width:300,
				notificationType:TYPE,
				title:TITLE,
				html:MESSAGE
			});
			this.hideNotification(n, DURATION*1000);
		}
		this.sendRequest = {
			url: url,
			success: function(response){
				var obj = Ext.decode(response.responseText);
				if(obj){
					var count = 0;
					var max_duration = 0;
					var defer = 500;
					Ext.each(obj.data,function(d){
						count++;
						if(d.duration > max_duration) max_duration = d.duration;
						var arr = [];
						arr.push(d.title);
						arr.push(d.message);
						arr.push(d.type);
						arr.push(d.duration);
						this.growl.defer(defer*count,this,arr);
					},this)
					new Ext.util.DelayedTask(function(){
						this.ajax.request(Ext.apply(this.sendRequest,{
							params:{
								limit:(3-Ext.ux.NotificationMgr.positions.length)
							}
						}))
					}).delay(count*defer+10000);
				}
			}
		}
		this.ajax = Ext.Ajax;
		this.ajax.timeout = 0;
		this.ajax.request(this.sendRequest);
		this.ajax.on("requestcomplete",function(con,response,options){

		},this)
		function newExcitingAlerts() {
		    var oldTitle = document.title;
		    var msg = "New Notification !! " +oldTitle;
		    var timeoutId = setInterval(function() {
		    	//window.focus();
		        document.title = document.title == msg ? ' ' : msg;

		    }, 1000);
		    window.onmousemove = function() {
		        clearInterval(timeoutId);
		        document.title = oldTitle;
		        window.onmousemove = null;
		    };
		}
		//newExcitingAlerts()
	})
}

<commit_msg>fixed bug related to hideNotification window<commit_after>Ext.ux.NotificationMgr = {
    positions: [],
    heights: []
};
Ext.ux.Notification = function(config){
	this.config = Ext.apply({},config);
}
Ext.ux.Notification = Ext.extend(Ext.Window, {
	notificationType:'ERROR',
	initComponent : function(){
		Ext.apply(this, {
			//iconCls: this.iconCls || 'icon-notification-info',
			autoHeight: true,
			closeAction:'close',
			plain: false,
			shadow:false,
			draggable: false,
			bodyStyle: 'text-align:left;padding:10px;',
			resizable: false
		});
		if(this.autoDestroy){
			this.task = new Ext.util.DelayedTask(this.close, this);
		}else{
			this.closable = true;
		}
		Ext.ux.Notification.superclass.initComponent.call(this);
    },
	setMessage : function(msg){
		this.body.update(msg);
	},
	/**
	* Comment this section for now. Since for window when setting iconClass frame is checked,
	* which is currently overridden to avoid, that may result double icons in window title.
	* Whenever we decide not to override that, we may want to keep this method.
	* Because I saw that the overriden section is refactored in latest version of extjs.

	setTitle : function(title, iconCls){
        Ext.ux.Notification.superclass.setTitle.call(this, title, iconCls||this.iconCls);
    },*/
    onRender : function(ct, position) {
		Ext.ux.Notification.superclass.onRender.call(this, ct, position);
	},
	onDestroy : function(){
		Ext.ux.NotificationMgr.positions.remove(this.pos);
		Ext.ux.Notification.superclass.onDestroy.call(this);
	},
	afterShow : function(){
		Ext.ux.Notification.superclass.afterShow.call(this);
		this.on('move', function(){
			Ext.ux.NotificationMgr.positions.remove(this.pos);
			if(this.autoDestroy){
				this.task.cancel();
			}
		}, this);
		if(this.autoDestroy){
			this.task.delay(this.hideDelay || 5000);
		}
	},
	animShow : function(){
		this.pos = 0;
		var h = 0;
		while(Ext.ux.NotificationMgr.positions.indexOf(this.pos)>-1){
			h+=Ext.ux.NotificationMgr.heights[this.pos];
			this.pos++;
		}
		Ext.ux.NotificationMgr.positions.push(this.pos);
		Ext.ux.NotificationMgr.heights[this.pos] = this.getSize().height+10;

		this.el.alignTo(this.animateTarget || document, "br-tr", [ -1, -1-(h) ]);
		this.el.slideIn('b', {
			duration: .7
			, callback: this.afterShow
			, scope: this
		});
	},
	animHide : function(){
		Ext.ux.NotificationMgr.positions.remove(this.pos);
		if(this.el)
		this.el.ghost("b", {
			duration: 1
			, remove: false
		});
	},
	start: function(url){
		new Ext.ux.Notification.start(url);
	}
});
Ext.ns("Ext.ux.Notification");
Ext.ux.Notification.Base = function(){};
Ext.ux.Notification.Base = Ext.extend(Ext.util.Observable,{
	showNotification:  function(config) {
	    var win = new Ext.ux.Notification(Ext.apply({
	    	animateTarget: this.notificationEl
			, autoDestroy: false
			,notificationType:'ERROR'
	    }, config));
	    if(win.notificationType == "ERROR"){
	    	win.iconCls = 'icon-notification-error';
	    	win.bodyStyle += 'background-color:#fddcdc;';
	    }
	    if(win.notificationType == "WARNING"){
	    	win.iconCls = 'icon-notification-warning';
	    	win.bodyStyle += 'background-color:#fefcc7;';
	    }
	    if(win.notificationType == "INFO"){
	    	win.iconCls = 'icon-notification-info';
	    	win.bodyStyle += 'background-color:#fff;';
	    }
	    win.bodyStyle += 'padding-bottom:20px;';
	    win.show();
	    return win;
	},
	hideNotification: function(win, delay) {
	    if (win) {
	    	win.close.defer(delay || 3000, win);
	    }
	},
	growl: function(TITLE,MESSAGE,TYPE,DURATION){
		var n = this.showNotification({
			width:300,
			notificationType:TYPE,
			title:TITLE,
			html:MESSAGE
		});
		this.hideNotification(n, DURATION*1000);
	}
})
Ext.ux.InstantNotification = Ext.extend(Ext.ux.Notification.Base,{
	constructor: function(config) {
		Ext.ux.InstantNotification.superclass.constructor.call(this, config);
		this.notificationEl = Ext.get("growl-notification-el")?Ext.get("growl-notification-el"):Ext.DomHelper.append(Ext.getBody(),{tag:'div',id:'growl-notification-el',style:'width:300px;position:absolute;bottom:0;right:0'});
		this.growl(config.title,config.message,"INFO",10)
	}
});

Ext.ux.Notification.start = function(url){
	Ext.onReady(function(){
		var notificationEl = Ext.get("growl-notification-el")?Ext.get("growl-notification-el"):Ext.DomHelper.append(Ext.getBody(),{tag:'div',id:'growl-notification-el',style:'width:300px;position:absolute;bottom:0;right:0'});
		this.showNotification = function(config) {
		    var win = new Ext.ux.Notification(Ext.apply({
		    	animateTarget: notificationEl
				, autoDestroy: false
				,notificationType:'ERROR'
		    }, config));
		    if(win.notificationType == "ERROR"){
		    	win.iconCls = 'icon-notification-error';
		    	win.bodyStyle += 'background-color:#fddcdc;';
		    }
		    if(win.notificationType == "WARNING"){
		    	win.iconCls = 'icon-notification-warning';
		    	win.bodyStyle += 'background-color:#fefcc7;';
		    }
		    if(win.notificationType == "INFO"){
		    	win.iconCls = 'icon-notification-info';
		    	win.bodyStyle += 'background-color:#fff;';
		    }
		    win.bodyStyle += 'padding-bottom:20px;';
		    win.show();

		    return win;
		};
		this.hideNotification = function(win, delay) {
		    if (win) {
		      win.close.defer(delay || 3000, win);
		    }
		};
		this.growl = function(TITLE,MESSAGE,TYPE,DURATION){
			var n = this.showNotification({
				width:300,
				notificationType:TYPE,
				title:TITLE,
				html:MESSAGE
			});
			this.hideNotification(n, DURATION*1000);
		}
		this.sendRequest = {
			url: url,
			success: function(response){
				var obj = Ext.decode(response.responseText);
				if(obj){
					var count = 0;
					var max_duration = 0;
					var defer = 500;
					Ext.each(obj.data,function(d){
						count++;
						if(d.duration > max_duration) max_duration = d.duration;
						var arr = [];
						arr.push(d.title);
						arr.push(d.message);
						arr.push(d.type);
						arr.push(d.duration);
						this.growl.defer(defer*count,this,arr);
					},this)
					new Ext.util.DelayedTask(function(){
						this.ajax.request(Ext.apply(this.sendRequest,{
							params:{
								limit:(3-Ext.ux.NotificationMgr.positions.length)
							}
						}))
					}).delay(count*defer+10000);
				}
			}
		}
		this.ajax = Ext.Ajax;
		this.ajax.timeout = 0;
		this.ajax.request(this.sendRequest);
		this.ajax.on("requestcomplete",function(con,response,options){

		},this)
		function newExcitingAlerts() {
		    var oldTitle = document.title;
		    var msg = "New Notification !! " +oldTitle;
		    var timeoutId = setInterval(function() {
		    	//window.focus();
		        document.title = document.title == msg ? ' ' : msg;

		    }, 1000);
		    window.onmousemove = function() {
		        clearInterval(timeoutId);
		        document.title = oldTitle;
		        window.onmousemove = null;
		    };
		}
		//newExcitingAlerts()
	})
}

<|endoftext|>


<commit_before>"use strict";

var assert = require('assert');

var cost = function(c) {
    if (c === '^') return 3;

    if (c === '*') return 2;

    return 1;
};

var walkable = function(c) {
    return !(c === undefined || c === '~');
};

var p = function(x,y) {
    return {
	x: x,
	y: y
    };
};

var manhattanDistance = function(p1,p2) {
    return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
};

var Map = function(rows) {
    this.rows = rows;

    var findIt = function(rows, it) {
	for (var y=0;y<rows.length;++y)
	    for (var x=0;x<rows[y].length;++x)
		if (rows[y][x] === it)
		    return p(x,y);
    };

    this.goal = function() {
	return findIt(this.rows, 'X');
    };

    this.start = function() {
	return findIt(this.rows, '@');
    };

    this.moves = function(p) {
	var choices = [];

	return choices;
    };

    return this;
};

var load = function(s) {
    return new Map(s.split('\n'));
};

describe('a* search algorithm', function() {

    describe('map', function() {

	var defaultMap = '@.\n.X';

	it('finds goal', function() {
	    var map = load(defaultMap);
	    var p = map.goal();

	    assert.equal(1, p.x);
	    assert.equal(1, p.y);
	});

	it('finds start', function() {
	    var map = load(defaultMap);
	    var p = map.start();

	    assert.equal(0, p.x);
	    assert.equal(0, p.y);
	});

	it.skip('finds choices', function() {
	    var map = load(defaultMap);

	    var choices = map.moves(p);

	    assert.equal(2, choices.length);
	});
    });

    describe('costs', function() {
	it('flatlands cost 1', function() {
	    assert.equal(1, cost('.'));
	    assert.equal(1, cost('@'));
	    assert.equal(1, cost('X'));
	});

	it('forest costs 2', function() {
	    assert.equal(2, cost('*'));
	});

	it('mountain costs 3', function() {
	    assert.equal(3, cost('^'));
	});

	it('walkable', function() {
	    assert(walkable('.'));
	    assert(walkable('*'));
	    assert(walkable('^'));
	    assert(walkable('@'));
	    assert(walkable('X'));
	});

	it('non-walkable', function() {
	    assert(!walkable(undefined));
	    assert(!walkable('~'));
	});
    });

    describe('Manhattan distance', function() {
	it('works in a straight line', function() {
	    assert.equal(5, manhattanDistance(p(0,0), p(5,0)));
	    assert.equal(3, manhattanDistance(p(0,3), p(0,0)));
	});
    });
});
<commit_msg>now back to a failing test<commit_after>"use strict";

var assert = require('assert');

var cost = function(c) {
    if (c === '^') return 3;

    if (c === '*') return 2;

    return 1;
};

var walkable = function(c) {
    return !(c === undefined || c === '~');
};

var p = function(x,y) {
    return {
	x: x,
	y: y
    };
};

var manhattanDistance = function(p1,p2) {
    return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
};

var Map = function(rows) {
    this.rows = rows;

    var findIt = function(rows, it) {
	for (var y=0;y<rows.length;++y)
	    for (var x=0;x<rows[y].length;++x)
		if (rows[y][x] === it)
		    return p(x,y);
    };

    this.goal = function() {
	return findIt(this.rows, 'X');
    };

    this.start = function() {
	return findIt(this.rows, '@');
    };

    this.moves = function(p) {
	var choices = [];

	return choices;
    };

    return this;
};

var load = function(s) {
    return new Map(s.split('\n'));
};

describe('a* search algorithm', function() {

    describe('map', function() {

	var defaultMap = '@.\n.X';

	it('finds goal', function() {
	    var map = load(defaultMap);
	    var p = map.goal();

	    assert.equal(1, p.x);
	    assert.equal(1, p.y);
	});

	it('finds start', function() {
	    var map = load(defaultMap);
	    var p = map.start();

	    assert.equal(0, p.x);
	    assert.equal(0, p.y);
	});

	it('finds choices', function() {
	    var map = load(defaultMap);

	    var choices = map.moves(p);

	    assert.equal(2, choices.length);
	});
    });

    describe('costs', function() {
	it('flatlands cost 1', function() {
	    assert.equal(1, cost('.'));
	    assert.equal(1, cost('@'));
	    assert.equal(1, cost('X'));
	});

	it('forest costs 2', function() {
	    assert.equal(2, cost('*'));
	});

	it('mountain costs 3', function() {
	    assert.equal(3, cost('^'));
	});

	it('walkable', function() {
	    assert(walkable('.'));
	    assert(walkable('*'));
	    assert(walkable('^'));
	    assert(walkable('@'));
	    assert(walkable('X'));
	});

	it('non-walkable', function() {
	    assert(!walkable(undefined));
	    assert(!walkable('~'));
	});
    });

    describe('Manhattan distance', function() {
	it('works in a straight line', function() {
	    assert.equal(5, manhattanDistance(p(0,0), p(5,0)));
	    assert.equal(3, manhattanDistance(p(0,3), p(0,0)));
	});
    });
});
<|endoftext|>


<commit_before>// @flow
import React, { Component, createElement } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import createFieldArrayProps from './createFieldArrayProps'
import { mapValues } from 'lodash'
import plain from './structure/plain'
import type { Structure } from './types'
import type { Props, DefaultProps } from './ConnectedFieldArray.types'

const propsToNotUpdateFor = ['_reduxForm', 'value']

const createConnectedFieldArray = (structure: Structure<*, *>) => {
  const { deepEqual, getIn, size } = structure
  const getSyncError = (syncErrors: Object, name: string) => {
    // For an array, the error can _ONLY_ be under _error.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return plain.getIn(syncErrors, `${name}._error`)
  }

  const getSyncWarning = (syncWarnings: Object, name: string) => {
    // For an array, the warning can _ONLY_ be under _warning.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return getIn(syncWarnings, `${name}._warning`)
  }

  class ConnectedFieldArray extends Component<Props> {
    static defaultProps: DefaultProps
    ref: ?React.Component<*, *>

    shouldComponentUpdate(nextProps: Props) {
      // Update if the elements of the value array was updated.
      const thisValue = this.props.value
      const nextValue = nextProps.value

      if (thisValue && nextValue) {
        let nextValueItemsSame = nextValue.every(val => thisValue.includes(val))
        let nextValueItemsOrderChanged = nextValue.some(
          (val, index) => val !== thisValue[index]
        )
        if (
          thisValue.length !== nextValue.length ||
          (nextValueItemsSame && nextValueItemsOrderChanged) ||
          (nextProps.rerenderOnEveryChange &&
            thisValue.some((val, index) => !deepEqual(val, nextValue[index])))
        ) {
          return true
        }
      }

      const nextPropsKeys = Object.keys(nextProps)
      const thisPropsKeys = Object.keys(this.props)
      // if we have children, we MUST update in React 16
      // https://twitter.com/erikras/status/915866544558788608
      return !!(
        this.props.children ||
        nextProps.children ||
        nextPropsKeys.length !== thisPropsKeys.length ||
        nextPropsKeys.some(prop => {
          // useful to debug rerenders
          // if (!plain.deepEqual(this.props[ prop ], nextProps[ prop ])) {
          //   console.info(prop, 'changed', this.props[ prop ], '==>', nextProps[ prop ])
          // }
          return (
            !~propsToNotUpdateFor.indexOf(prop) &&
            !deepEqual(this.props[prop], nextProps[prop])
          )
        })
      )
    }

    get dirty(): boolean {
      return this.props.dirty
    }

    get pristine(): boolean {
      return this.props.pristine
    }

    get value(): any {
      return this.props.value
    }

    getRenderedComponent() {
      return this.ref
    }

    saveRef = (ref: ?React.Component<*, *>) => {
      this.ref = ref
    }

    getValue = (index: number): any =>
      this.props.value && getIn(this.props.value, String(index))

    render() {
      const {
        component,
        withRef,
        name,
        _reduxForm, // eslint-disable-line no-unused-vars
        validate, // eslint-disable-line no-unused-vars
        warn, // eslint-disable-line no-unused-vars
        rerenderOnEveryChange, // eslint-disable-line no-unused-vars
        ...rest
      } = this.props
      const props = createFieldArrayProps(
        structure,
        name,
        _reduxForm.form,
        _reduxForm.sectionPrefix,
        this.getValue,
        rest
      )
      if (withRef) {
        props.ref = this.saveRef
      }
      return createElement(component, props)
    }
  }

  ConnectedFieldArray.propTypes = {
    component: PropTypes.oneOfType([PropTypes.func, PropTypes.string])
      .isRequired,
    props: PropTypes.object,
    rerenderOnEveryChange: PropTypes.bool
  }

  ConnectedFieldArray.defaultProps = {
    rerenderOnEveryChange: false
  }

  ConnectedFieldArray.contextTypes = {
    _reduxForm: PropTypes.object
  }

  const connector = connect(
    (state, ownProps) => {
      const { name, _reduxForm: { initialValues, getFormState } } = ownProps
      const formState = getFormState(state)
      const initial =
        getIn(formState, `initial.${name}`) ||
        (initialValues && getIn(initialValues, name))
      const value = getIn(formState, `values.${name}`)
      const submitting = getIn(formState, 'submitting')
      const syncError = getSyncError(getIn(formState, 'syncErrors'), name)
      const syncWarning = getSyncWarning(getIn(formState, 'syncWarnings'), name)
      const pristine = deepEqual(value, initial)
      return {
        asyncError: getIn(formState, `asyncErrors.${name}._error`),
        dirty: !pristine,
        pristine,
        state: getIn(formState, `fields.${name}`),
        submitError: getIn(formState, `submitErrors.${name}._error`),
        submitFailed: getIn(formState, 'submitFailed'),
        submitting,
        syncError,
        syncWarning,
        value,
        length: size(value)
      }
    },
    (dispatch, ownProps) => {
      const { name, _reduxForm } = ownProps
      const {
        arrayInsert,
        arrayMove,
        arrayPop,
        arrayPush,
        arrayRemove,
        arrayRemoveAll,
        arrayShift,
        arraySplice,
        arraySwap,
        arrayUnshift
      } = _reduxForm
      return mapValues(
        {
          arrayInsert,
          arrayMove,
          arrayPop,
          arrayPush,
          arrayRemove,
          arrayRemoveAll,
          arrayShift,
          arraySplice,
          arraySwap,
          arrayUnshift
        },
        actionCreator =>
          bindActionCreators(actionCreator.bind(null, name), dispatch)
      )
    },
    undefined,
    { withRef: true }
  )
  return connector(ConnectedFieldArray)
}

export default createConnectedFieldArray
<commit_msg>Replaced includes with indexOf (#3535)<commit_after>// @flow
import React, { Component, createElement } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import createFieldArrayProps from './createFieldArrayProps'
import { mapValues } from 'lodash'
import plain from './structure/plain'
import type { Structure } from './types'
import type { Props, DefaultProps } from './ConnectedFieldArray.types'

const propsToNotUpdateFor = ['_reduxForm', 'value']

const createConnectedFieldArray = (structure: Structure<*, *>) => {
  const { deepEqual, getIn, size } = structure
  const getSyncError = (syncErrors: Object, name: string) => {
    // For an array, the error can _ONLY_ be under _error.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return plain.getIn(syncErrors, `${name}._error`)
  }

  const getSyncWarning = (syncWarnings: Object, name: string) => {
    // For an array, the warning can _ONLY_ be under _warning.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return getIn(syncWarnings, `${name}._warning`)
  }

  class ConnectedFieldArray extends Component<Props> {
    static defaultProps: DefaultProps
    ref: ?React.Component<*, *>

    shouldComponentUpdate(nextProps: Props) {
      // Update if the elements of the value array was updated.
      const thisValue = this.props.value
      const nextValue = nextProps.value

      if (thisValue && nextValue) {
        let nextValueItemsSame = nextValue.every(
          val => ~thisValue.indexOf(val)
        )
        let nextValueItemsOrderChanged = nextValue.some(
          (val, index) => val !== thisValue[index]
        )
        if (
          thisValue.length !== nextValue.length ||
          (nextValueItemsSame && nextValueItemsOrderChanged) ||
          (nextProps.rerenderOnEveryChange &&
            thisValue.some((val, index) => !deepEqual(val, nextValue[index])))
        ) {
          return true
        }
      }

      const nextPropsKeys = Object.keys(nextProps)
      const thisPropsKeys = Object.keys(this.props)
      // if we have children, we MUST update in React 16
      // https://twitter.com/erikras/status/915866544558788608
      return !!(
        this.props.children ||
        nextProps.children ||
        nextPropsKeys.length !== thisPropsKeys.length ||
        nextPropsKeys.some(prop => {
          // useful to debug rerenders
          // if (!plain.deepEqual(this.props[ prop ], nextProps[ prop ])) {
          //   console.info(prop, 'changed', this.props[ prop ], '==>', nextProps[ prop ])
          // }
          return (
            !~propsToNotUpdateFor.indexOf(prop) &&
            !deepEqual(this.props[prop], nextProps[prop])
          )
        })
      )
    }

    get dirty(): boolean {
      return this.props.dirty
    }

    get pristine(): boolean {
      return this.props.pristine
    }

    get value(): any {
      return this.props.value
    }

    getRenderedComponent() {
      return this.ref
    }

    saveRef = (ref: ?React.Component<*, *>) => {
      this.ref = ref
    }

    getValue = (index: number): any =>
      this.props.value && getIn(this.props.value, String(index))

    render() {
      const {
        component,
        withRef,
        name,
        _reduxForm, // eslint-disable-line no-unused-vars
        validate, // eslint-disable-line no-unused-vars
        warn, // eslint-disable-line no-unused-vars
        rerenderOnEveryChange, // eslint-disable-line no-unused-vars
        ...rest
      } = this.props
      const props = createFieldArrayProps(
        structure,
        name,
        _reduxForm.form,
        _reduxForm.sectionPrefix,
        this.getValue,
        rest
      )
      if (withRef) {
        props.ref = this.saveRef
      }
      return createElement(component, props)
    }
  }

  ConnectedFieldArray.propTypes = {
    component: PropTypes.oneOfType([PropTypes.func, PropTypes.string])
      .isRequired,
    props: PropTypes.object,
    rerenderOnEveryChange: PropTypes.bool
  }

  ConnectedFieldArray.defaultProps = {
    rerenderOnEveryChange: false
  }

  ConnectedFieldArray.contextTypes = {
    _reduxForm: PropTypes.object
  }

  const connector = connect(
    (state, ownProps) => {
      const { name, _reduxForm: { initialValues, getFormState } } = ownProps
      const formState = getFormState(state)
      const initial =
        getIn(formState, `initial.${name}`) ||
        (initialValues && getIn(initialValues, name))
      const value = getIn(formState, `values.${name}`)
      const submitting = getIn(formState, 'submitting')
      const syncError = getSyncError(getIn(formState, 'syncErrors'), name)
      const syncWarning = getSyncWarning(getIn(formState, 'syncWarnings'), name)
      const pristine = deepEqual(value, initial)
      return {
        asyncError: getIn(formState, `asyncErrors.${name}._error`),
        dirty: !pristine,
        pristine,
        state: getIn(formState, `fields.${name}`),
        submitError: getIn(formState, `submitErrors.${name}._error`),
        submitFailed: getIn(formState, 'submitFailed'),
        submitting,
        syncError,
        syncWarning,
        value,
        length: size(value)
      }
    },
    (dispatch, ownProps) => {
      const { name, _reduxForm } = ownProps
      const {
        arrayInsert,
        arrayMove,
        arrayPop,
        arrayPush,
        arrayRemove,
        arrayRemoveAll,
        arrayShift,
        arraySplice,
        arraySwap,
        arrayUnshift
      } = _reduxForm
      return mapValues(
        {
          arrayInsert,
          arrayMove,
          arrayPop,
          arrayPush,
          arrayRemove,
          arrayRemoveAll,
          arrayShift,
          arraySplice,
          arraySwap,
          arrayUnshift
        },
        actionCreator =>
          bindActionCreators(actionCreator.bind(null, name), dispatch)
      )
    },
    undefined,
    { withRef: true }
  )
  return connector(ConnectedFieldArray)
}

export default createConnectedFieldArray
<|endoftext|>


<commit_before>const ldap = require('ldapjs-no-python');
const logger = require('./logger');
const utils = require('./utils');

DN = 'dn';

function unbind(client) {
	client.unbind(err => {
		if (err) {
			logger.log.error('Failed to unbind LDAP binding. Reason : [%s]', utils.formatErr(err));
		}
	});
}

function bind(client, bindDN, bindPassword) {
	return new Promise(
		(resolve, reject) => {
			client.bind(bindDN, bindPassword, err => {
				if (err) {
					reject(err);
					return;
				}

				resolve();
			});
		}
	);
}

// binds directly by opts.username and opts.password ( not by ldap's username and password )
function bindDirectly(client, opts, resolve, reject) {
	return bind(client, opts.username, opts.password)
		.then(_ => {
			unbind(client);
			resolve();
		})
		.catch(_ => {
			unbind(client);
			reject();
		});
}

function search(client, opts) {
	return new Promise(
		(resolve, reject) => {
			let filter = utils.isEmptyStr(opts.ldap.findBy) ? `(|(sAMAccountName=${opts.username})(userPrincipalName=${opts.username}))` : `${opts.ldap.findBy}=${opts.username}`;

			const searchOptions = {
				scope: "sub",
				filter: filter,
				attributes: ['dn']
			};

			const result = [];

			client.search(opts.ldap.baseDN, searchOptions, (err, res) => {
				if (err) {
					reject(err);
					return;
				}

				res.on('searchEntry', entry => {
					result.push(entry.object);
				});

				res.on('searchReference', referral => {
				});

				res.on('error', err => {
					reject(err);
				});

				res.on('end', res => {
					if (result.length < 1) {
						reject('No users found');
						return;
					}

					if (result.length > 1) {
						reject('Found multiple users');
						return;
					}

					const dn = result[0][DN];
					if (dn === undefined) {
						reject(`The [${opts.username}] user was found but it doesn't have a [${DN}] attribute`);
					} else {
						resolve(dn);
					}
				});
			});
		}
	);
}

function authUser(opts) {
	return new Promise(
		(resolve, reject) => {
			// creating ldap client
			const client = ldap.createClient({
				url: opts.ldap.url
			});

			// error handler
			client.on('error', function (err) {
				unbind(client);
				reject(err);
			});

			// bindDN not provided ? trying to bind ( authenticate ) directly by opts.username and opts.password
			if (utils.isEmptyStr(opts.ldap.bindDN)) {
				// binding to username & password
				bindDirectly(client, opts, resolve, reject);
				return;
			}

			// binding to bindDN
			bind(client, opts.ldap.bindDN, opts.ldap.bindPassword)
				.then(_ => search(client, opts))
				.then(userDN => bind(client, userDN, opts.password))
				.then(_ => {
					unbind(client);
					resolve();
				})
				// previous stuff didn't work, trying to bind directly
				.catch(_ => bindDirectly(client, opts, resolve, reject));
		}
	);
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
module.exports = authUser;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////<commit_msg>- simplified LDAP utils<commit_after>const ldap = require('ldapjs-no-python');
const logger = require('./logger');
const utils = require('./utils');

DN = 'dn';

function unbind(client) {
	client.unbind(err => {
		if (err) {
			logger.log.error('Failed to unbind LDAP binding. Reason : [%s]', utils.formatErr(err));
		}
	});
}

function bind(client, bindDN, bindPassword) {
	return new Promise(
		(resolve, reject) => {
			client.bind(bindDN, bindPassword, err => {
				if (err) {
					reject(err);
					return;
				}

				resolve();
			});
		}
	);
}

function search(client, opts) {
	return new Promise(
		(resolve, reject) => {
			let filter = utils.isEmptyStr(opts.ldap.findBy) ? `(|(sAMAccountName=${opts.username})(userPrincipalName=${opts.username}))` : `${opts.ldap.findBy}=${opts.username}`;

			const searchOptions = {
				scope: "sub",
				filter: filter,
				attributes: ['dn']
			};

			const result = [];

			client.search(opts.ldap.baseDN, searchOptions, (err, res) => {
				if (err) {
					reject(err);
					return;
				}

				res.on('searchEntry', entry => {
					result.push(entry.object);
				});

				res.on('searchReference', referral => {
				});

				res.on('error', err => {
					reject(err);
				});

				res.on('end', res => {
					if (result.length < 1) {
						reject('No users found');
						return;
					}

					if (result.length > 1) {
						reject('Found multiple users');
						return;
					}

					const dn = result[0][DN];
					if (dn === undefined) {
						reject(`The [${opts.username}] user was found but it doesn't have a [${DN}] attribute`);
					} else {
						resolve(dn);
					}
				});
			});
		}
	);
}

function authUser(opts) {
	return new Promise(
		(resolve, reject) => {
			// creating ldap client
			const client = ldap.createClient({
				url: opts.ldap.url
			});

			// error handler
			client.on('error', function (err) {
				unbind(client);
				reject(err);
			});

			let promise;
			if (!utils.isEmptyStr(opts.ldap.bindDN) && !utils.isEmptyStr(opts.ldap.bindPassword)) {
				// binding to bindDN if AD doesn't allow anonymous connection
				promise = bind(client, opts.ldap.bindDN, opts.ldap.bindPassword);
			} else {
				promise = Promise.resolve();
			}

			promise.then(_ => search(client, opts))
				.then(userDN => bind(client, userDN, opts.password))
				.then(_ => {
					logger.log.debug(`The [${opts.username}] user is successfully authenticated in LDAP`);
					unbind(client);
					resolve();
				})
				.catch(err => {
					logger.log.error(`Failed to authenticated [${opts.username}] user in LDAP. Reason : [%s]`, utils.formatErr(err));
					unbind(client);
					reject();
				});
		}
	);
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
module.exports = authUser;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////<|endoftext|>


<commit_before>/**
 * This widget renders the visualization defined by a VisualizationModel onto
 * a canvas element that will fill the parent element.
 */
cinema.views.VisualizationCanvasWidget = Backbone.View.extend({
    // Expose primitive events from the canvas for building interactors
    events: {
        'click .c-vis-render-canvas': function (e) {
            this.trigger('c:click', e);
        },
        'dblclick .c-vis-render-canvas': function (e) {
            this.trigger('c:dblclick', e);
        },
        'mousedown .c-vis-render-canvas': function (e) {
            this.trigger('c:mousedown', e);
        },
        'mousemove .c-vis-render-canvas': function (e) {
            this.trigger('c:mousemove', e);
        },
        'mouseup .c-vis-render-canvas': function (e) {
            this.trigger('c:mouseup', e);
        },
        'mousewheel .c-vis-render-canvas': function (e) {
            this.trigger('c:mousewheel', e);
        },
        'keypress .c-vis-render-canvas': function (e) {
            this.trigger('c:keypress', e);
        },
        'contextmenu .c-vis-render-canvas': function (e) {
            e.preventDefault();
        }
    },

    initialize: function (settings) {
        var args = settings.visModel.get('arguments');

        this.visModel = settings.visModel;
        this.query = settings.query || "AABBCBDBEBFBGCHCICJCKC"; // TODO figure out what this means
        this.drawingCenter = settings.drawingCenter || [0, 0];
        this.zoomLevel = settings.zoomLevel || 1.0;
        this.backgroundColor = settings.backgroundColor || '#ffffff';
        this.orderMapping = {};
        this.viewpoint = settings.viewpoint || {
            time: args.time['default'],
            phi: args.phi['default'],
            theta: args.theta['default']
        };

        this.compositeManager = new cinema.utilities.CompositeImageManager({
            visModel: this.visModel
        });

        this._computeLayerOffset();
        this._first = true;

        this.compositeManager.on('c:error', function (e) {
            this.trigger('c:error', e);
        }, this).on('c:data.ready', function (data) {
            this._writeCompositeBuffer(data);

            if (this._first) {
                this._first = false;
                this.resetCamera();
            }

            this.drawImage();
        }, this);
    },

    render: function () {
        this.$el.html(cinema.templates.visCanvas());

        // Fetch and render the default phi/time/theta image.
        return this.showViewpoint();
    },

    _computeOffset: function (order) {
        for (var i = 0; i < order.length; i += 1) {
            var offset = this.layerOffset[order[i]];
            if (offset > -1) {
                return offset;
            }
        }
        return -1;
    },

    _computeLayerOffset: function () {
        this.layerOffset = {};

        for (var i = 0; i < this.query.length; i += 2) {
            var layer = this.query[i];

            if (this.query[i + 1] === '_') {
                this.layerOffset[layer] = -1;
            } else {
                this.layerOffset[layer] = this.visModel.numberOfLayers() - 1 -
                    this.visModel.get('metadata').offset[this.query.substr(i, 2)];
            }
        }
    },

    _computeCompositeInfo: function (data) {
        var composite = data.json['pixel-order'].split('+'),
            count = composite.length;
        /*jshint -W016 */
        while (count--) {
            var str = composite[count];
            if (str[0] === '@') {
                composite[count] = Number(str.substr(1));
            } else if (!_.has(this.orderMapping, str)) {
                this.orderMapping[str] = this._computeOffset(str);
            }
        }

        data.composite = composite;
    },

    /**
     * Computes the composite image and writes it into the composite buffer.
     * @param data The payload from the composite image manager c:data.ready
     * callback. This will write computed composite data back into that
     * cache entry so it won't have to recompute it.
     */
    _writeCompositeBuffer: function (data) {
        if (!_.has(data, 'composite')) {
            this._computeCompositeInfo(data);
        }

        var renderCanvas = this.$('.c-vis-render-canvas')[0],
            compositeCanvas = this.$('.c-vis-composite-buffer')[0],
            spriteCanvas = this.$('.c-vis-spritesheet-buffer')[0],
            dim = this.visModel.imageDimensions(),
            spritesheetDim = this.visModel.spritesheetDimensions(),
            spriteCtx = spriteCanvas.getContext('2d'),
            compositeCtx = compositeCanvas.getContext('2d');

        $(spriteCanvas).attr({
            width: spritesheetDim[0],
            height: spritesheetDim[1]
        });
        $(compositeCanvas).attr({
            width: dim[0],
            height: dim[1]
        });

        // Fill full spritesheet buffer with raw image data
        spriteCtx.drawImage(data.image, 0, 0);

        var pixelBuffer = spriteCtx.getImageData(0, 0,
                  spritesheetDim[0], spritesheetDim[1]).data,
            frontBuffer,
            pixelIdx = 0;

        // Fill the background if backgroundColor is specified
        if (this.backgroundColor) {
            compositeCtx.fillStyle = this.backgroundColor;
            compositeCtx.fillRect(0, 0, dim[0], dim[1]);
            frontBuffer = compositeCtx.getImageData(0, 0, dim[0], dim[1]);
        } else { // Otherwise use the bottom spritesheet image as a background
            frontBuffer = spriteCtx.getImageData(
                0, (this.visModel.numberOfLayers() - 1) * dim[1], dim[0], dim[1]);
        }

        var frontPixels = frontBuffer.data;

        for (var i = 0; i < data.composite.length; i += 1) {
            var order = data.composite[i];
            if (order > 0) {
                pixelIdx += order;
            } else {
                var offset = this.orderMapping[order];

                if (offset > -1) {
                    var localIdx = 4 * pixelIdx;
                    offset *= dim[0] * dim[1] * 4;
                    offset += localIdx;
                    frontPixels[localIdx] = pixelBuffer[offset];
                    frontPixels[localIdx + 1] = pixelBuffer[offset + 1];
                    frontPixels[localIdx + 2] = pixelBuffer[offset + 2];
                    frontPixels[localIdx + 3] = 255;
                }
                pixelIdx += 1;
            }
        }

        // Draw buffer to composite canvas
        compositeCtx.putImageData(frontBuffer, 0, 0);
    },

    /**
     * Call this after data has been successfully rendered onto the composite
     * canvas, and it will draw it with the correct scale, zoom, and center
     * onto the render canvas.
     */
    drawImage: function () {
        var renderCanvas = this.$('.c-vis-render-canvas')[0],
            compositeCanvas = this.$('.c-vis-composite-buffer')[0],
            w = this.$el.parent().width(),
            h = this.$el.parent().height(),
            iw = compositeCanvas.width,
            ih = compositeCanvas.height;

        $(renderCanvas).attr({
            width: w,
            height: h
        });
        renderCanvas.getContext('2d').clearRect(0, 0, w, h);

        var tw = Math.floor(iw * this.zoomLevel),
        th = Math.floor(ih * this.zoomLevel),
        dx = (tw > w) ? (tw - w) : (w - tw),
        dy = (th > h) ? (th - h) : (h - th),
        centerBounds = [(w - dx) / 2, (h - dy) / 2, (w + dx) / 2, (h + dy) / 2];

        if (this.drawingCenter[0] < centerBounds[0] ||
            this.drawingCenter[0] > centerBounds[2] ||
            this.drawingCenter[1] < centerBounds[1] ||
            this.drawingCenter[1] > centerBounds[3]) {
            this.drawingCenter = [
                Math.min(Math.max(this.drawingCenter[0], centerBounds[0]), centerBounds[2]),
                Math.min(Math.max(this.drawingCenter[1], centerBounds[1]), centerBounds[3])
            ];

        }
        var tx = this.drawingCenter[0] - (tw / 2),
            ty = this.drawingCenter[1] - (th / 2);

        renderCanvas.getContext('2d').drawImage(
            compositeCanvas,
            0,   0, iw, ih,  // Source image   [Location,Size]
            tx, ty, tw, th); // Target drawing [Location,Size]
    },

    /**
     * Reset the zoom level and drawing center such that the image is
     * centered and zoomed to fit within the parent container.
     */
    resetCamera: function () {
        var w = this.$el.parent().width(),
            h = this.$el.parent().height(),
            iw = this.$('.c-vis-composite-buffer').width(),
            ih = this.$('.c-vis-composite-buffer').height();

        this.zoomLevel = Math.min(w / iw, h / ih);
        this.drawingCenter = [w / 2, h / 2];
    },

    /**
     * Change the viewpoint to show a different image.
     * @param viewpoint An object containing "time", "phi", and "theta" keys. If you
     * do not pass this, simply renders the current this.viewpoint value.
     * @return this, for chainability
     */
    showViewpoint: function (viewpoint) {
        if (viewpoint) {
            this.viewpoint = viewpoint;
        }
        this.compositeManager.updateViewpoint(this.viewpoint);

        return this;
    },

    /**
     * Maps an [x, y] value relative to the canvas element to an [x, y] value
     * relative to the image being rendered on the canvas.
     * @param coords 2-length list representing [x, y] offset into the canvas
     * element.
     * @returns the corresponding [x, y] value of the image being rendered on
     * the canvas, respecting zoom level and drawing center, or null if the
     * input coordinates are on a part of the canvas outside of the image render
     * bounds. If not null, this will be a value bounded in each dimension by
     * the length of the composited image in that dimension.
     */
    mapToImageCoordinates: function (coords) {
        // TODO
    }
});
<commit_msg>Don't limit panning to viewport bounds<commit_after>/**
 * This widget renders the visualization defined by a VisualizationModel onto
 * a canvas element that will fill the parent element.
 */
cinema.views.VisualizationCanvasWidget = Backbone.View.extend({
    // Expose primitive events from the canvas for building interactors
    events: {
        'click .c-vis-render-canvas': function (e) {
            this.trigger('c:click', e);
        },
        'dblclick .c-vis-render-canvas': function (e) {
            this.trigger('c:dblclick', e);
        },
        'mousedown .c-vis-render-canvas': function (e) {
            this.trigger('c:mousedown', e);
        },
        'mousemove .c-vis-render-canvas': function (e) {
            this.trigger('c:mousemove', e);
        },
        'mouseup .c-vis-render-canvas': function (e) {
            this.trigger('c:mouseup', e);
        },
        'mousewheel .c-vis-render-canvas': function (e) {
            this.trigger('c:mousewheel', e);
        },
        'keypress .c-vis-render-canvas': function (e) {
            this.trigger('c:keypress', e);
        },
        'contextmenu .c-vis-render-canvas': function (e) {
            e.preventDefault();
        }
    },

    initialize: function (settings) {
        var args = settings.visModel.get('arguments');

        this.visModel = settings.visModel;
        this.query = settings.query || "AABBCBDBEBFBGCHCICJCKC"; // TODO figure out what this means
        this.drawingCenter = settings.drawingCenter || [0, 0];
        this.zoomLevel = settings.zoomLevel || 1.0;
        this.backgroundColor = settings.backgroundColor || '#ffffff';
        this.orderMapping = {};
        this.viewpoint = settings.viewpoint || {
            time: args.time['default'],
            phi: args.phi['default'],
            theta: args.theta['default']
        };

        this.compositeManager = new cinema.utilities.CompositeImageManager({
            visModel: this.visModel
        });

        this._computeLayerOffset();
        this._first = true;

        this.compositeManager.on('c:error', function (e) {
            this.trigger('c:error', e);
        }, this).on('c:data.ready', function (data) {
            this._writeCompositeBuffer(data);

            if (this._first) {
                this._first = false;
                this.resetCamera();
            }

            this.drawImage();
        }, this);
    },

    render: function () {
        this.$el.html(cinema.templates.visCanvas());

        // Fetch and render the default phi/time/theta image.
        return this.showViewpoint();
    },

    _computeOffset: function (order) {
        for (var i = 0; i < order.length; i += 1) {
            var offset = this.layerOffset[order[i]];
            if (offset > -1) {
                return offset;
            }
        }
        return -1;
    },

    _computeLayerOffset: function () {
        this.layerOffset = {};

        for (var i = 0; i < this.query.length; i += 2) {
            var layer = this.query[i];

            if (this.query[i + 1] === '_') {
                this.layerOffset[layer] = -1;
            } else {
                this.layerOffset[layer] = this.visModel.numberOfLayers() - 1 -
                    this.visModel.get('metadata').offset[this.query.substr(i, 2)];
            }
        }
    },

    _computeCompositeInfo: function (data) {
        var composite = data.json['pixel-order'].split('+'),
            count = composite.length;
        /*jshint -W016 */
        while (count--) {
            var str = composite[count];
            if (str[0] === '@') {
                composite[count] = Number(str.substr(1));
            } else if (!_.has(this.orderMapping, str)) {
                this.orderMapping[str] = this._computeOffset(str);
            }
        }

        data.composite = composite;
    },

    /**
     * Computes the composite image and writes it into the composite buffer.
     * @param data The payload from the composite image manager c:data.ready
     * callback. This will write computed composite data back into that
     * cache entry so it won't have to recompute it.
     */
    _writeCompositeBuffer: function (data) {
        if (!_.has(data, 'composite')) {
            this._computeCompositeInfo(data);
        }

        var renderCanvas = this.$('.c-vis-render-canvas')[0],
            compositeCanvas = this.$('.c-vis-composite-buffer')[0],
            spriteCanvas = this.$('.c-vis-spritesheet-buffer')[0],
            dim = this.visModel.imageDimensions(),
            spritesheetDim = this.visModel.spritesheetDimensions(),
            spriteCtx = spriteCanvas.getContext('2d'),
            compositeCtx = compositeCanvas.getContext('2d');

        $(spriteCanvas).attr({
            width: spritesheetDim[0],
            height: spritesheetDim[1]
        });
        $(compositeCanvas).attr({
            width: dim[0],
            height: dim[1]
        });

        // Fill full spritesheet buffer with raw image data
        spriteCtx.drawImage(data.image, 0, 0);

        var pixelBuffer = spriteCtx.getImageData(0, 0,
                  spritesheetDim[0], spritesheetDim[1]).data,
            frontBuffer,
            pixelIdx = 0;

        // Fill the background if backgroundColor is specified
        if (this.backgroundColor) {
            compositeCtx.fillStyle = this.backgroundColor;
            compositeCtx.fillRect(0, 0, dim[0], dim[1]);
            frontBuffer = compositeCtx.getImageData(0, 0, dim[0], dim[1]);
        } else { // Otherwise use the bottom spritesheet image as a background
            frontBuffer = spriteCtx.getImageData(
                0, (this.visModel.numberOfLayers() - 1) * dim[1], dim[0], dim[1]);
        }

        var frontPixels = frontBuffer.data;

        for (var i = 0; i < data.composite.length; i += 1) {
            var order = data.composite[i];
            if (order > 0) {
                pixelIdx += order;
            } else {
                var offset = this.orderMapping[order];

                if (offset > -1) {
                    var localIdx = 4 * pixelIdx;
                    offset *= dim[0] * dim[1] * 4;
                    offset += localIdx;
                    frontPixels[localIdx] = pixelBuffer[offset];
                    frontPixels[localIdx + 1] = pixelBuffer[offset + 1];
                    frontPixels[localIdx + 2] = pixelBuffer[offset + 2];
                    frontPixels[localIdx + 3] = 255;
                }
                pixelIdx += 1;
            }
        }

        // Draw buffer to composite canvas
        compositeCtx.putImageData(frontBuffer, 0, 0);
    },

    /**
     * Call this after data has been successfully rendered onto the composite
     * canvas, and it will draw it with the correct scale, zoom, and center
     * onto the render canvas.
     */
    drawImage: function () {
        var renderCanvas = this.$('.c-vis-render-canvas')[0],
            compositeCanvas = this.$('.c-vis-composite-buffer')[0],
            w = this.$el.parent().width(),
            h = this.$el.parent().height(),
            iw = compositeCanvas.width,
            ih = compositeCanvas.height;

        $(renderCanvas).attr({
            width: w,
            height: h
        });
        renderCanvas.getContext('2d').clearRect(0, 0, w, h);

        var tw = Math.floor(iw * this.zoomLevel),
        th = Math.floor(ih * this.zoomLevel),
        dx = (tw > w) ? (tw - w) : (w - tw),
        dy = (th > h) ? (th - h) : (h - th),
        centerBounds = [(w - dx) / 2, (h - dy) / 2, (w + dx) / 2, (h + dy) / 2];

        var tx = this.drawingCenter[0] - (tw / 2),
            ty = this.drawingCenter[1] - (th / 2);

        renderCanvas.getContext('2d').drawImage(
            compositeCanvas,
            0,   0, iw, ih,  // Source image   [Location,Size]
            tx, ty, tw, th); // Target drawing [Location,Size]
    },

    /**
     * Reset the zoom level and drawing center such that the image is
     * centered and zoomed to fit within the parent container.
     */
    resetCamera: function () {
        var w = this.$el.parent().width(),
            h = this.$el.parent().height(),
            iw = this.$('.c-vis-composite-buffer').width(),
            ih = this.$('.c-vis-composite-buffer').height();

        this.zoomLevel = Math.min(w / iw, h / ih);
        this.drawingCenter = [w / 2, h / 2];
    },

    /**
     * Change the viewpoint to show a different image.
     * @param viewpoint An object containing "time", "phi", and "theta" keys. If you
     * do not pass this, simply renders the current this.viewpoint value.
     * @return this, for chainability
     */
    showViewpoint: function (viewpoint) {
        if (viewpoint) {
            this.viewpoint = viewpoint;
        }
        this.compositeManager.updateViewpoint(this.viewpoint);

        return this;
    },

    /**
     * Maps an [x, y] value relative to the canvas element to an [x, y] value
     * relative to the image being rendered on the canvas.
     * @param coords 2-length list representing [x, y] offset into the canvas
     * element.
     * @returns the corresponding [x, y] value of the image being rendered on
     * the canvas, respecting zoom level and drawing center, or null if the
     * input coordinates are on a part of the canvas outside of the image render
     * bounds. If not null, this will be a value bounded in each dimension by
     * the length of the composited image in that dimension.
     */
    mapToImageCoordinates: function (coords) {
        // TODO
    }
});
<|endoftext|>


<commit_before>/* eslint-env mocha */
/* global expect, testContext */
/* eslint-disable prefer-arrow-callback, no-unused-expressions */
/* eslint key-spacing: [2, { "mode": "minimum" }] */
import factory from 'src/test/factories'
import {withDBCleanup, useFixture, mockIdmUsersById} from 'src/test/helpers'
import {getPlayerById} from 'src/server/db/player'
import {findQuestionsByStat} from 'src/server/db/question'
import {STAT_DESCRIPTORS} from 'src/common/models/stat'
import reloadSurveyAndQuestionData from 'src/server/actions/reloadSurveyAndQuestionData'

import updatePlayerStatsForProject from 'src/server/actions/updatePlayerStatsForProject'

describe(testContext(__filename), function () {
  describe('updatePlayerStatsForProject', function () {
    withDBCleanup()
    this.timeout(8000)
    useFixture.buildSurvey()

    beforeEach('Setup Survey Data', async function () {
      await reloadSurveyAndQuestionData()

      this.setupSurveyData = async customResponses => {
        const {playerResponses, projectResponses} = await _getQuestionsAndReponses(customResponses)

        await this.buildSurvey([
          ...playerResponses.map(q => ({questionId: q.questionId, subjectIds: () => this.project.playerIds})),
          ...projectResponses.map(q => ({questionId: q.questionId, subjectIds: () => this.project.id})),
        ])

        const responseData = []
        this.project.playerIds.forEach((respondentId, respondentNum) => {
          this.project.playerIds.forEach((subjectId, subjectNum) => {
            playerResponses.forEach(qr => {
              responseData.push({
                questionId: qr.questionId,
                surveyId: this.survey.id,
                respondentId,
                subjectId,
                value: qr.value ? qr.value : qr.values[subjectNum],
              })
            })
          })

          projectResponses.forEach(qr => {
            responseData.push({
              questionId: qr.questionId,
              surveyId: this.survey.id,
              respondentId,
              subjectId: this.project.id,
              value: qr.value ? qr.value : qr.values[respondentNum],
            })
          })
        })

        await factory.createMany('response', responseData)
      }
    })

    it('updates the players\' stats based on the survey responses', async function () {
      await this.setupSurveyData()
      const playerId = this.project.playerIds[0]

      await mockIdmUsersById(this.project.playerIds)
      await getPlayerById(playerId).update({stats: {elo: {rating: 1300}}}).run()
      await updatePlayerStatsForProject(this.project)
      const updatedPlayer = await getPlayerById(playerId)

      expect(updatedPlayer.stats.ecc).to.eq(100)
      expect(updatedPlayer.stats.xp).to.eq(35)
      expect(updatedPlayer.stats.elo).to.deep.eq({
        rating: 1204,
        matches: 3,
      })
      expect(updatedPlayer.stats.projects).to.deep.eq({
        [this.project.id]: {
          challenge: 7,
          th: 83,
          cc: 67,
          cultureContributionStructure: 50,
          cultureContributionSafety: 67,
          cultureContributionTruth: 83,
          cultureContributionChallenge: 83,
          cultureContributionSupport: 67,
          cultureContributionEngagement: 50,
          cultureContributionEnjoyment: 67,
          tp: 83,
          receptiveness: 67,
          flexibleLeadership: 50,
          resultsFocus: 33,
          frictionReduction: 17,
          ec: 25,
          ecd: 0,
          abc: 4,
          rc: 25,
          rcSelf: 25,
          rcOther: 25,
          rcPerHour: 0.71,
          hours: 35,
          teamHours: 140,
          ecc: 100,
          xp: 35,
          elo: {
            rating: 1204,
            matches: 3,
            score: 0.71,
            kFactor: 100,
          }
        },
      })
    })

    it('does not compute Elo for coaches', async function () {
      await this.setupSurveyData()

      const playerInfoOverrides = this.project.playerIds.map((id, i) => ({
        id,
        roles: i === 0 ? ['coach', 'player'] : ['player'],
      }))
      await mockIdmUsersById(this.project.playerIds, playerInfoOverrides)

      const [proPlayerId, regularPlayerId] = this.project.playerIds
      await getPlayerById(proPlayerId).replace(p => p.without('stats').merge({stats: p('stats').without('elo')})).run()

      await updatePlayerStatsForProject(this.project)

      const updatedProPlayer = await getPlayerById(proPlayerId)
      const updatedRegularPlayer = await getPlayerById(regularPlayerId)

      expect(updatedProPlayer.stats).to.not.contain.all.keys('elo')
      expect(updatedProPlayer.stats.projects[this.project.id]).to.not.contain.all.keys('elo')

      expect(updatedRegularPlayer.stats).to.contain.all.keys('elo')
      expect(updatedRegularPlayer.stats.projects[this.project.id]).to.contain.all.keys('elo')
    })

    it('ignores players who have reported 0 hours', async function () {
      await this.setupSurveyData({
        [STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION]: [35, 25, 25, 15],
        [STAT_DESCRIPTORS.PROJECT_HOURS]: ['35', '35', '35', '0'],
      })

      const activePlayerId = this.project.playerIds[0]
      const inactivePlayerId = this.project.playerIds[3]
      const initialInactivePlayer = await getPlayerById(inactivePlayerId)

      await mockIdmUsersById(this.project.playerIds)
      await getPlayerById(activePlayerId).update({stats: {elo: {rating: 1300}}}).run()
      await updatePlayerStatsForProject(this.project)
      const updatedActivePlayer = await getPlayerById(activePlayerId)
      const updatedInactivePlayer = await getPlayerById(inactivePlayerId)

      // the stats for the inactive player shouldn't change
      expect(initialInactivePlayer.stats).to.deep.eq(updatedInactivePlayer.stats)

      // the stats for the active player should be such that the inactive player
      // was ignored
      expect(updatedActivePlayer.stats.ecc).to.eq(123)
      expect(updatedActivePlayer.stats.xp).to.eq(43.05)
      expect(updatedActivePlayer.stats.elo).to.deep.eq({
        rating: 1250,
        matches: 2,
      })
      expect(updatedActivePlayer.stats.projects).to.deep.eq({
        [this.project.id]: {
          challenge: 7,
          th: 83,
          cc: 67,
          cultureContributionStructure: 50,
          cultureContributionSafety: 67,
          cultureContributionTruth: 83,
          cultureContributionChallenge: 83,
          cultureContributionSupport: 67,
          cultureContributionEngagement: 50,
          cultureContributionEnjoyment: 67,
          tp: 83,
          receptiveness: 67,
          flexibleLeadership: 50,
          resultsFocus: 33,
          frictionReduction: 17,
          ec: 33,
          ecd: 8,
          abc: 3,
          rc: 41,
          rcSelf: 41,
          rcOther: 41,
          rcPerHour: 1.17,
          hours: 35,
          teamHours: 105,
          ecc: 123,
          xp: 43.05,
          elo: {
            rating: 1250,
            matches: 2,
            score: 1.17,
            kFactor: 100,
          }
        },
      })
    })
  })
})

async function _getQuestionsAndReponses(customResponses = {}) {
  const playerDescriptors = [
    STAT_DESCRIPTORS.TECHNICAL_HEALTH,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT,
    STAT_DESCRIPTORS.TEAM_PLAY,
    STAT_DESCRIPTORS.RECEPTIVENESS,
    STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP,
    STAT_DESCRIPTORS.RESULTS_FOCUS,
    STAT_DESCRIPTORS.FRICTION_REDUCTION,
    STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION,
    STAT_DESCRIPTORS.CHALLENGE,
  ]
  const projectDescriptors = [
    STAT_DESCRIPTORS.PROJECT_HOURS,
    STAT_DESCRIPTORS.CHALLENGE,
  ]

  const defaultResponses = {
    [STAT_DESCRIPTORS.TECHNICAL_HEALTH]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE]: 4,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT]: 4,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT]: 5,
    [STAT_DESCRIPTORS.TEAM_PLAY]: 6,
    [STAT_DESCRIPTORS.RECEPTIVENESS]: 5,
    [STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP]: 4,
    [STAT_DESCRIPTORS.RESULTS_FOCUS]: 3,
    [STAT_DESCRIPTORS.FRICTION_REDUCTION]: 2,
    [STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION]: 20,
    [STAT_DESCRIPTORS.CHALLENGE]: 7,
    [STAT_DESCRIPTORS.PROJECT_HOURS]: '35',
    [STAT_DESCRIPTORS.CHALLENGE]: 7,
  }
  const responses = {...defaultResponses, ...customResponses}

  const getQId = descriptor => findQuestionsByStat(descriptor).filter({active: true})(0)('id')
  const qAndR = async descriptor => {
    const val = responses[descriptor]
    const qr = {questionId: await getQId(descriptor)}
    if (Array.isArray(val)) {
      qr.values = val
    } else {
      qr.value = val
    }
    return qr
  }

  const playerResponses = await Promise.all(playerDescriptors.map(async desc => await qAndR(desc)))
  const projectResponses = await Promise.all(projectDescriptors.map(async desc => await qAndR(desc)))

  return {playerResponses, projectResponses}
}

/**
 * Test match results:
 *
 *   1300  1000  1000  1000
 *   a     b     c     d
 *   --------------------------------------
 *   1300, 1000 =  1265, 1035
 *   a     b       a     b
 *   --------------------------------------
 *   1300, 1000 -> 1265, 1000 =  1233, 1032
 *   a     c       a     c       a     c
 *   --------------------------------------
 *   1300, 1000 -> 1233, 1000 =  1204, 1029
 *   a     d       a     d       a     d
 *   --------------------------------------
 *   1000, 1000 -> 1035, 1032 =  1035, 1032
 *   b     c       b     c       b     c
 *   --------------------------------------
 *   1000, 1000 -> 1035, 1029 =  1034, 1030
 *   b     d       b     d       b     d
 *   --------------------------------------
 *   1000, 1000 -> 1032, 1030 =  1032, 1030
 *   c     d       c     d       c     d
 *   --------------------------------------
 *   1204  1034  1032  1030
 *   a     b     c     d
*/
<commit_msg>clean up nock scopes before each test<commit_after>/* eslint-env mocha */
/* global expect, testContext */
/* eslint-disable prefer-arrow-callback, no-unused-expressions */
/* eslint key-spacing: [2, { "mode": "minimum" }] */
import factory from 'src/test/factories'
import {withDBCleanup, useFixture, mockIdmUsersById} from 'src/test/helpers'
import {getPlayerById} from 'src/server/db/player'
import {findQuestionsByStat} from 'src/server/db/question'
import {STAT_DESCRIPTORS} from 'src/common/models/stat'
import reloadSurveyAndQuestionData from 'src/server/actions/reloadSurveyAndQuestionData'

import updatePlayerStatsForProject from 'src/server/actions/updatePlayerStatsForProject'

describe(testContext(__filename), function () {
  describe('updatePlayerStatsForProject', function () {
    withDBCleanup()
    this.timeout(8000)
    useFixture.buildSurvey()

    beforeEach('Setup Survey Data', async function () {
      useFixture.nockClean()
      await reloadSurveyAndQuestionData()

      this.setupSurveyData = async customResponses => {
        const {playerResponses, projectResponses} = await _getQuestionsAndReponses(customResponses)

        await this.buildSurvey([
          ...playerResponses.map(q => ({questionId: q.questionId, subjectIds: () => this.project.playerIds})),
          ...projectResponses.map(q => ({questionId: q.questionId, subjectIds: () => this.project.id})),
        ])

        const responseData = []
        this.project.playerIds.forEach((respondentId, respondentNum) => {
          this.project.playerIds.forEach((subjectId, subjectNum) => {
            playerResponses.forEach(qr => {
              responseData.push({
                questionId: qr.questionId,
                surveyId: this.survey.id,
                respondentId,
                subjectId,
                value: qr.value ? qr.value : qr.values[subjectNum],
              })
            })
          })

          projectResponses.forEach(qr => {
            responseData.push({
              questionId: qr.questionId,
              surveyId: this.survey.id,
              respondentId,
              subjectId: this.project.id,
              value: qr.value ? qr.value : qr.values[respondentNum],
            })
          })
        })

        await factory.createMany('response', responseData)
      }
    })

    it('updates the players\' stats based on the survey responses', async function () {
      await this.setupSurveyData()
      const playerId = this.project.playerIds[0]

      await mockIdmUsersById(this.project.playerIds)
      await getPlayerById(playerId).update({stats: {elo: {rating: 1300}}}).run()
      await updatePlayerStatsForProject(this.project)
      const updatedPlayer = await getPlayerById(playerId)

      expect(updatedPlayer.stats.ecc).to.eq(100)
      expect(updatedPlayer.stats.xp).to.eq(35)
      expect(updatedPlayer.stats.elo).to.deep.eq({
        rating: 1204,
        matches: 3,
      })
      expect(updatedPlayer.stats.projects).to.deep.eq({
        [this.project.id]: {
          challenge: 7,
          th: 83,
          cc: 67,
          cultureContributionStructure: 50,
          cultureContributionSafety: 67,
          cultureContributionTruth: 83,
          cultureContributionChallenge: 83,
          cultureContributionSupport: 67,
          cultureContributionEngagement: 50,
          cultureContributionEnjoyment: 67,
          tp: 83,
          receptiveness: 67,
          flexibleLeadership: 50,
          resultsFocus: 33,
          frictionReduction: 17,
          ec: 25,
          ecd: 0,
          abc: 4,
          rc: 25,
          rcSelf: 25,
          rcOther: 25,
          rcPerHour: 0.71,
          hours: 35,
          teamHours: 140,
          ecc: 100,
          xp: 35,
          elo: {
            rating: 1204,
            matches: 3,
            score: 0.71,
            kFactor: 100,
          }
        },
      })
    })

    it('does not compute Elo for coaches', async function () {
      await this.setupSurveyData()

      const playerInfoOverrides = this.project.playerIds.map((id, i) => ({
        id,
        roles: i === 0 ? ['coach', 'player'] : ['player'],
      }))
      await mockIdmUsersById(this.project.playerIds, playerInfoOverrides)

      const [proPlayerId, regularPlayerId] = this.project.playerIds
      await getPlayerById(proPlayerId).replace(p => p.without('stats').merge({stats: p('stats').without('elo')})).run()

      await updatePlayerStatsForProject(this.project)

      const updatedProPlayer = await getPlayerById(proPlayerId)
      const updatedRegularPlayer = await getPlayerById(regularPlayerId)

      expect(updatedProPlayer.stats).to.not.contain.all.keys('elo')
      expect(updatedProPlayer.stats.projects[this.project.id]).to.not.contain.all.keys('elo')

      expect(updatedRegularPlayer.stats).to.contain.all.keys('elo')
      expect(updatedRegularPlayer.stats.projects[this.project.id]).to.contain.all.keys('elo')
    })

    it('ignores players who have reported 0 hours', async function () {
      await this.setupSurveyData({
        [STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION]: [35, 25, 25, 15],
        [STAT_DESCRIPTORS.PROJECT_HOURS]: ['35', '35', '35', '0'],
      })

      const activePlayerId = this.project.playerIds[0]
      const inactivePlayerId = this.project.playerIds[3]
      const initialInactivePlayer = await getPlayerById(inactivePlayerId)

      await mockIdmUsersById(this.project.playerIds)
      await getPlayerById(activePlayerId).update({stats: {elo: {rating: 1300}}}).run()
      await updatePlayerStatsForProject(this.project)
      const updatedActivePlayer = await getPlayerById(activePlayerId)
      const updatedInactivePlayer = await getPlayerById(inactivePlayerId)

      // the stats for the inactive player shouldn't change
      expect(initialInactivePlayer.stats).to.deep.eq(updatedInactivePlayer.stats)

      // the stats for the active player should be such that the inactive player
      // was ignored
      expect(updatedActivePlayer.stats.ecc).to.eq(123)
      expect(updatedActivePlayer.stats.xp).to.eq(43.05)
      expect(updatedActivePlayer.stats.elo).to.deep.eq({
        rating: 1250,
        matches: 2,
      })
      expect(updatedActivePlayer.stats.projects).to.deep.eq({
        [this.project.id]: {
          challenge: 7,
          th: 83,
          cc: 67,
          cultureContributionStructure: 50,
          cultureContributionSafety: 67,
          cultureContributionTruth: 83,
          cultureContributionChallenge: 83,
          cultureContributionSupport: 67,
          cultureContributionEngagement: 50,
          cultureContributionEnjoyment: 67,
          tp: 83,
          receptiveness: 67,
          flexibleLeadership: 50,
          resultsFocus: 33,
          frictionReduction: 17,
          ec: 33,
          ecd: 8,
          abc: 3,
          rc: 41,
          rcSelf: 41,
          rcOther: 41,
          rcPerHour: 1.17,
          hours: 35,
          teamHours: 105,
          ecc: 123,
          xp: 43.05,
          elo: {
            rating: 1250,
            matches: 2,
            score: 1.17,
            kFactor: 100,
          }
        },
      })
    })
  })
})

async function _getQuestionsAndReponses(customResponses = {}) {
  const playerDescriptors = [
    STAT_DESCRIPTORS.TECHNICAL_HEALTH,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT,
    STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT,
    STAT_DESCRIPTORS.TEAM_PLAY,
    STAT_DESCRIPTORS.RECEPTIVENESS,
    STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP,
    STAT_DESCRIPTORS.RESULTS_FOCUS,
    STAT_DESCRIPTORS.FRICTION_REDUCTION,
    STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION,
    STAT_DESCRIPTORS.CHALLENGE,
  ]
  const projectDescriptors = [
    STAT_DESCRIPTORS.PROJECT_HOURS,
    STAT_DESCRIPTORS.CHALLENGE,
  ]

  const defaultResponses = {
    [STAT_DESCRIPTORS.TECHNICAL_HEALTH]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_STRUCTURE]: 4,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SAFETY]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_TRUTH]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_CHALLENGE]: 6,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_SUPPORT]: 5,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENGAGEMENT]: 4,
    [STAT_DESCRIPTORS.CULTURE_CONTRIBUTION_ENJOYMENT]: 5,
    [STAT_DESCRIPTORS.TEAM_PLAY]: 6,
    [STAT_DESCRIPTORS.RECEPTIVENESS]: 5,
    [STAT_DESCRIPTORS.FLEXIBLE_LEADERSHIP]: 4,
    [STAT_DESCRIPTORS.RESULTS_FOCUS]: 3,
    [STAT_DESCRIPTORS.FRICTION_REDUCTION]: 2,
    [STAT_DESCRIPTORS.RELATIVE_CONTRIBUTION]: 20,
    [STAT_DESCRIPTORS.CHALLENGE]: 7,
    [STAT_DESCRIPTORS.PROJECT_HOURS]: '35',
    [STAT_DESCRIPTORS.CHALLENGE]: 7,
  }
  const responses = {...defaultResponses, ...customResponses}

  const getQId = descriptor => findQuestionsByStat(descriptor).filter({active: true})(0)('id')
  const qAndR = async descriptor => {
    const val = responses[descriptor]
    const qr = {questionId: await getQId(descriptor)}
    if (Array.isArray(val)) {
      qr.values = val
    } else {
      qr.value = val
    }
    return qr
  }

  const playerResponses = await Promise.all(playerDescriptors.map(async desc => await qAndR(desc)))
  const projectResponses = await Promise.all(projectDescriptors.map(async desc => await qAndR(desc)))

  return {playerResponses, projectResponses}
}

/**
 * Test match results:
 *
 *   1300  1000  1000  1000
 *   a     b     c     d
 *   --------------------------------------
 *   1300, 1000 =  1265, 1035
 *   a     b       a     b
 *   --------------------------------------
 *   1300, 1000 -> 1265, 1000 =  1233, 1032
 *   a     c       a     c       a     c
 *   --------------------------------------
 *   1300, 1000 -> 1233, 1000 =  1204, 1029
 *   a     d       a     d       a     d
 *   --------------------------------------
 *   1000, 1000 -> 1035, 1032 =  1035, 1032
 *   b     c       b     c       b     c
 *   --------------------------------------
 *   1000, 1000 -> 1035, 1029 =  1034, 1030
 *   b     d       b     d       b     d
 *   --------------------------------------
 *   1000, 1000 -> 1032, 1030 =  1032, 1030
 *   c     d       c     d       c     d
 *   --------------------------------------
 *   1204  1034  1032  1030
 *   a     b     c     d
*/
<|endoftext|>