<commit_before>#!/usr/bin/env node

var url = require("url");
var net = require("net");
var _ = require("lodash");
var async = require("async");
var Tracker = require("peer-search/tracker");
var events = require('events');

var cfg = require("../lib/cfg");
var log = require("../lib/log");
var db = require("../lib/db");
var indexer = require("../lib/indexer");
var importer = require("../lib/importer");

var mp = new events.EventEmitter();
var buffer = { };
mp.db = db; // expose db

/* Config - dependant stuff
 */
cfg.on("ready", function() {
	db.listenReplications(cfg.dbId); // start our replication server
	db.findReplications(cfg.dbId); // replicate to other instances

	log.important("DB Path "+cfg.dbPath);
	log.important("we have "+cfg.sources.length+" sources");

	if (cfg.sources) cfg.sources.forEach(mp.importQueue.push);
});
cfg.on("updated", function() {
	// currently concurrency for importQueue is 1 so checking buffer[] works
	// TODO: fix this, since some sources may be completed by re-added with setTimeout because of source.interval
	if (cfg.sources) cfg.sources.forEach(function(source) { if (! buffer[source.url]) mp.importQueue.push(source) });
});

/* Collect infoHashes from source
 */
mp.importQueue = async.queue(function(source, next) {
	source = typeof(source) == "string" ? { url: source } : source;
	log.important("importing from "+source.url);
	importer.collect(source, function(err, status) {
		if (err) log.error(err);
		else {
			log.important("importing finished from "+source.url+", "+status.found+" infoHashes, "+status.imported+" of them new, through "+status.type+" importer ("+(status.end-status.start)+"ms)");
			buffering(source, status.found);
		}

		if (source.interval) setTimeout(function() { mp.importQueue.push(source) }, source.interval); // repeat at interval - re-push

		next();
	}, function(hash, extra) {
		log.hash(hash, "collect");
		if (!argv["disable-process"]) mp.processQueue.push({ infoHash: hash, extra: extra, hints: extra && extra.hints, source: source });
		// extra - collected from the source, can be info like uploaders/downloaders, category, etc.
		// hints - hints to particular meta information already found from the source, like imdb_id, season/episode
	});
}, 1);

/* Process & index infoHashes
 */
mp.processQueue = async.queue(function(task, _next) {
	var next = _.once(function() { called = true; buffering(task.source); _next() }), called = false;
	setTimeout(function() { next(); if (!called) log.error("process timeout for "+task.infoHash) }, 10*1000);

	log.hash(task.infoHash, "processing");

	// consider using db.indexes.seeders to figure out a skip case here; don't overcomplicate though
	db.get(task.infoHash, function(err, res) {
		if (err) {
			log.error(err);
			return next();
		}

		// Pass a merge of existing torrent objects as a base for indexing
		var noChanges;
		task.torrent = res && res.length && indexer.merge(res.sort(function(a, b) { return a.seq - b.seq }).map(function(x) { return x.value }));
		async.auto({
			index: function(cb) { indexer.index(task, { }, function(err, tor, nochanges) { noChanges = nochanges; cb(err, tor) }) },
			seedleech: function(cb) { (task.torrent && task.torrent.popularityUpdated > (Date.now() - cfg.popularityTTL)) ? cb() : indexer.seedleech(task.infoHash, cb) }
		}, function(err, indexing) {
			if (err) {
				if (task.callback) task.callback(err); log.error(task.infoHash, err);
				return next();
			}

			// Note that this is a _.merge, popularity is not overriden
			var torrent = _.merge(indexing.index, indexing.seedleech ? { popularity: indexing.seedleech, popularityUpdated: Date.now() } : { });
			if ( ! (res.length == 1 && noChanges)) db.merge(torrent.infoHash, res, torrent);

			mp.emit("found", task.source.url, torrent);

			next();
			if (task.callback) task.callback(null, torrent);

			if (torrent.uninteresting && !res.length) log.warning(torrent.infoHash+" / "+torrent.name+" is non-interesting, no files indexed");
			log.hash(task.infoHash, "processed");
		});
	});
}, 6);

/* Emit buffering event
 */
function buffering(source, total) {
	if (! (source && source.url)) return;
	if (! buffer[source.url]) buffer[source.url] = { progress: 0, total: 0 };
	if (!isNaN(total)) return buffer[source.url].total = total;
	buffer[source.url].progress++;
	var perc;
	perc = buffer[source.url].progress/buffer[source.url].total;
	perc = (Math.floor(perc * 100) / 100).toFixed(2);
	mp.emit("buffering", source, perc);
	if (perc == 1) {
		mp.emit("finished", source);
		delete buffer[source.url];
	}
}

/* Programatic usage of this
 */
if (module.parent) return module.exports = mp;

/* Log number of torrents we have
 */
async.forever(function(next) {
	log.important("We have "+db.indexes.seeders.size+" torrents, "+mp.processQueue.length()+" queued");
	setTimeout(next, 5000);
});

/* Simple dump
 */
var argv = module.parent ? { } : require("minimist")(process.argv.slice(2));
if (argv["db-dump"]) db.createReadStream()
.on("data", function(d) {
	d.value.files.forEach(function(f) {
		console.log([d.value.infoHash, f.path, f.imdb_id, f.season, f.episode].filter(function(x) { return x }).join(" / "))
	});
});

/* Stremio Addon interface
 */
if (cfg.stremioAddon) require("../stremio-addon/addon")(cfg.stremioAddon);

<commit_msg>add arg --disable-collect<commit_after>#!/usr/bin/env node

var url = require("url");
var net = require("net");
var _ = require("lodash");
var async = require("async");
var Tracker = require("peer-search/tracker");
var events = require('events');

var cfg = require("../lib/cfg");
var log = require("../lib/log");
var db = require("../lib/db");
var indexer = require("../lib/indexer");
var importer = require("../lib/importer");

var mp = new events.EventEmitter();
var buffer = { };
mp.db = db; // expose db

/* Config - dependant stuff
 */
cfg.on("ready", function() {
	db.listenReplications(cfg.dbId); // start our replication server
	db.findReplications(cfg.dbId); // replicate to other instances

	log.important("DB Path "+cfg.dbPath);
	log.important("we have "+cfg.sources.length+" sources");

	if (cfg.sources) cfg.sources.forEach(mp.importQueue.push);
});
cfg.on("updated", function() {
	// currently concurrency for importQueue is 1 so checking buffer[] works
	// TODO: fix this, since some sources may be completed by re-added with setTimeout because of source.interval
	if (!argv["disable-collect"] && cfg.sources) cfg.sources.forEach(function(source) { if (! buffer[source.url]) mp.importQueue.push(source) });
});

/* Collect infoHashes from source
 */
mp.importQueue = async.queue(function(source, next) {
	source = typeof(source) == "string" ? { url: source } : source;
	log.important("importing from "+source.url);
	importer.collect(source, function(err, status) {
		if (err) log.error(err);
		else {
			log.important("importing finished from "+source.url+", "+status.found+" infoHashes, "+status.imported+" of them new, through "+status.type+" importer ("+(status.end-status.start)+"ms)");
			buffering(source, status.found);
		}

		if (source.interval) setTimeout(function() { mp.importQueue.push(source) }, source.interval); // repeat at interval - re-push

		next();
	}, function(hash, extra) {
		log.hash(hash, "collect");
		if (!argv["disable-process"]) mp.processQueue.push({ infoHash: hash, extra: extra, hints: extra && extra.hints, source: source });
		// extra - collected from the source, can be info like uploaders/downloaders, category, etc.
		// hints - hints to particular meta information already found from the source, like imdb_id, season/episode
	});
}, 1);

/* Process & index infoHashes
 */
mp.processQueue = async.queue(function(task, _next) {
	var next = _.once(function() { called = true; buffering(task.source); _next() }), called = false;
	setTimeout(function() { next(); if (!called) log.error("process timeout for "+task.infoHash) }, 10*1000);

	log.hash(task.infoHash, "processing");

	// consider using db.indexes.seeders to figure out a skip case here; don't overcomplicate though
	db.get(task.infoHash, function(err, res) {
		if (err) {
			log.error(err);
			return next();
		}

		// Pass a merge of existing torrent objects as a base for indexing
		var noChanges;
		task.torrent = res && res.length && indexer.merge(res.sort(function(a, b) { return a.seq - b.seq }).map(function(x) { return x.value }));
		async.auto({
			index: function(cb) { indexer.index(task, { }, function(err, tor, nochanges) { noChanges = nochanges; cb(err, tor) }) },
			seedleech: function(cb) { (task.torrent && task.torrent.popularityUpdated > (Date.now() - cfg.popularityTTL)) ? cb() : indexer.seedleech(task.infoHash, cb) }
		}, function(err, indexing) {
			if (err) {
				if (task.callback) task.callback(err); log.error(task.infoHash, err);
				return next();
			}

			// Note that this is a _.merge, popularity is not overriden
			var torrent = _.merge(indexing.index, indexing.seedleech ? { popularity: indexing.seedleech, popularityUpdated: Date.now() } : { });
			if ( ! (res.length == 1 && noChanges)) db.merge(torrent.infoHash, res, torrent);

			mp.emit("found", task.source.url, torrent);

			next();
			if (task.callback) task.callback(null, torrent);

			if (torrent.uninteresting && !res.length) log.warning(torrent.infoHash+" / "+torrent.name+" is non-interesting, no files indexed");
			log.hash(task.infoHash, "processed");
		});
	});
}, 6);

/* Emit buffering event
 */
function buffering(source, total) {
	if (! (source && source.url)) return;
	if (! buffer[source.url]) buffer[source.url] = { progress: 0, total: 0 };
	if (!isNaN(total)) return buffer[source.url].total = total;
	buffer[source.url].progress++;
	var perc;
	perc = buffer[source.url].progress/buffer[source.url].total;
	perc = (Math.floor(perc * 100) / 100).toFixed(2);
	mp.emit("buffering", source, perc);
	if (perc == 1) {
		mp.emit("finished", source);
		delete buffer[source.url];
	}
}

/* Programatic usage of this
 */
if (module.parent) return module.exports = mp;

/* Log number of torrents we have
 */
async.forever(function(next) {
	log.important("We have "+db.indexes.seeders.size+" torrents, "+mp.processQueue.length()+" queued");
	setTimeout(next, 5000);
});

/* Simple dump
 */
var argv = module.parent ? { } : require("minimist")(process.argv.slice(2));
if (argv["db-dump"]) db.createReadStream()
.on("data", function(d) {
	d.value.files.forEach(function(f) {
		console.log([d.value.infoHash, f.path, f.imdb_id, f.season, f.episode].filter(function(x) { return x }).join(" / "))
	});
});

/* Stremio Addon interface
 */
if (cfg.stremioAddon) require("../stremio-addon/addon")(cfg.stremioAddon);

<|endoftext|>


<commit_before>#!/usr/bin/env node

var argv = require("minimist")(process.argv.slice(2));
var url = require("url");
var net = require("net");
var _ = require("lodash");
var hat = require("hat");

module.dbPath = argv["db-path"] || "./db";
module.dbId = argv["db-identifier"] || argv["db-id"] || argv["id"]; // use minimist alias
module.dbId = (module.dbId && module.dbId.length==40 && parseInt(module.dbId, 16)) ? module.dbId : hat(160,16);

var db = require("../lib/db");
var indexer = require("../lib/indexer");

db.listenReplications(module.dbId); // start our replication server
db.findReplications(module.dbId); // replicate to other instances

/*
if (argv.replicate) {
	var c = net.connect(url.parse(argv.replicate).port);
	c.on("connect", function() { c.pipe(db.getSyncStream()).pipe(c) });
};
*/

/*
setTimeout(function() {
	db.torrents.put("xxxxx "+Math.random(), "test");
	db.torrents.put("testy test"+Math.random(), "foo");
	db.torrents.put("another key", "is testing");
}, 500);
*/

// WARNING we'll need to iterate through the whole dataset on intro in any case to generate query index
setInterval(function() {
	var count = 0;
	db.torrents.createReadStream().on("data",function(d){ count++ }).on("end", function() { console.log("We have "+count+" torrents") })
}, 3000);
<commit_msg>Process & import queues<commit_after>#!/usr/bin/env node

var argv = require("minimist")(process.argv.slice(2));
var url = require("url");
var net = require("net");
var _ = require("lodash");
var hat = require("hat");
var async = require("async");

module.dbPath = argv["db-path"] || "./db";
module.dbId = argv["db-identifier"] || argv["db-id"] || argv["id"]; // use minimist alias
module.dbId = (module.dbId && module.dbId.length==40 && parseInt(module.dbId, 16)) ? module.dbId : hat(160,16);

var db = require("../lib/db");
var log = require("../lib/log");
var indexer = require("../lib/indexer");
//var importer = require("../lib/importer");

db.listenReplications(module.dbId); // start our replication server
db.findReplications(module.dbId); // replicate to other instances

/* Collect infoHashes from source
 */
var importQueue = async.queue(function(source, next) {

}, 1);

/* Process & index infoHashes
 */
var processQueue = async.queue(function(hash, next) {

}, 5);

/* Log number of torrents we have
 */
setInterval(function() {
	var count = 0;
	db.torrents.createKeyStream()
		.on("data",function(d) { count++ })
		.on("end", function() { log.important("We have "+count+" torrents") });
}, 5000);
<|endoftext|>


<commit_before>var map = null;
var legend = document.getElementById('legend');
var mapStyle = require('./map-style.json');
var pointsStyle = require('./points-style.json');

function initMap() {
	// Create a map object and specify the DOM element for display.
	map = new google.maps.Map(document.getElementById('map'), {
		center: {
			lat: 38.922239,
			lng: -95.794675
		},
		disableDefaultUI: true,
		styles: mapStyle,
		zoom: 5
	});
	map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);

}

var pageviews = 0;
var commitments = 0;

var from = null;
var to = null;

var lastEvaluatedKey;

// Displays the points data provided.
function displayPoints(data) {
	return new Promise(function(resolve) {

		if (data.hasOwnProperty('LastEvaluatedKey')) lastEvaluatedKey = data.LastEvaluatedKey.Id;
		else lastEvaluatedKey = 'finished';

		for (var i = 0; i < data.Items.length; i++) {
			var LatLng = new google.maps.LatLng({
				lat: data.Items[i].Coordinates[0],
				lng: data.Items[i].Coordinates[1]
			});

			var circle = new google.maps.Circle({
				fillColor: pointsStyle[data.Items[i].Action].fillColor,
				fillOpacity: pointsStyle[data.Items[i].Action].fillOpacity,
				map: map,
				center: LatLng,
				radius: pointsStyle[data.Items[i].Action].radius,
				strokeWeight: 0
			});

			if (data.Items[i].Action === 'view') pageviews++;

			if (data.Items[i].Action === 'commitment') commitments++;

		}

		document.getElementById('pageviews').innerText = pageviews;
		document.getElementById('commitments').innerText = commitments;

		resolve('All points processed');

	});
}

function get(url) {

	// Return a new promise.
	return new Promise(function(resolve, reject) {
		// Do the usual XHR stuff
		var req = new XMLHttpRequest();
		req.open('GET', url);
		req.setRequestHeader('Content-Type', 'application/json');
		req.onload = function() {
			// This is called even on 404 etc
			// so check the status
			if (req.status == 200) {
				// Resolve the promise with the response text
				resolve(req.response);
			} else {
				// Otherwise reject with the status text
				// which will hopefully be a meaningful error
				statusBad('Disconnected, server error');
				reject(Error(req.statusText));
			}
		};

		// Handle network errors
		req.onerror = function() {
			statusBad('Disconnected, network error');
			reject(Error('Network Error'));
		};

		// Make the request
		req.send();
	});
}

function delay(t) {
	return new Promise(function(resolve) {
		setTimeout(resolve, t);
	});
}

// https://medium.com/adobe-io/how-to-combine-rest-api-calls-with-javascript-promises-in-node-js-or-openwhisk-d96cbc10f299
// https://gist.github.com/trieloff/168312d4dd4d149afdd55cde3d3724cabea
var promiseChain = {
	runChain: function() {
		var timeString = '';
		if (from === null) {
			var d = new Date();
			d.setHours(0, 0, 0, 0);
			d = d.getTime() / 1000;

			if (to === null) {
				to = new Date() / 1000;
			}
			timeString = 'from=' + d + '&to=' + to + '&'; // If from is not set, set to midnight local time in seconds (accurate to the millisecond)
		} else {
			if (to === null) {
				to = new Date() / 1000;
			}
			timeString = 'from=' + from + '&to=' + to + '&';
		}

		var exclusiveStartKeyString = '';
		if (lastEvaluatedKey != undefined && lastEvaluatedKey != 'finished') {
			exclusiveStartKeyString = 'exclusivestartkey=' + lastEvaluatedKey;
		}

		get('/rest/live/read?' + timeString + exclusiveStartKeyString).then(JSON.parse).then(displayPoints).then(function() {
			if (lastEvaluatedKey !== 'finished') {
				promiseChain.runChain();
			} else {
				// Done paginating through data
				from = to + 0.001;
				to = null;
				statusGood('Map connected live');
				return delay(3000).then(function() {
					promiseChain.runChain();
				});
			}
		}, function(error) {
			console.error('callPromise Failed!', error);
			return delay(3000).then(function() {
				promiseChain.runChain();
			});
		}); // Anonymous function
	} // runChain
}; // promiseChain

var midnight = new Date();
midnight.setDate(new Date().getDate()+1);
midnight.setHours(0,0,0,0);
midnight = midnight / 1000;
var now = new Date() / 1000;
midnight = midnight - now;
midnight = midnight + 5; // Add 5 seconds as a cushion
setTimeout(
	midnightTask,
	midnight * 1000
);

function midnightTask() {
	window.location.reload(true);
}

promiseChain.runChain(); // execute function
<commit_msg>Reformat promise spacing<commit_after>var map = null;
var legend = document.getElementById('legend');
var mapStyle = require('./map-style.json');
var pointsStyle = require('./points-style.json');

function initMap() {
	// Create a map object and specify the DOM element for display.
	map = new google.maps.Map(document.getElementById('map'), {
		center: {
			lat: 38.922239,
			lng: -95.794675
		},
		disableDefaultUI: true,
		styles: mapStyle,
		zoom: 5
	});
	map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);

}

var pageviews = 0;
var commitments = 0;

var from = null;
var to = null;

var lastEvaluatedKey;

// Displays the points data provided.
function displayPoints(data) {
	return new Promise(function(resolve) {

		if (data.hasOwnProperty('LastEvaluatedKey')) lastEvaluatedKey = data.LastEvaluatedKey.Id;
		else lastEvaluatedKey = 'finished';

		for (var i = 0; i < data.Items.length; i++) {
			var LatLng = new google.maps.LatLng({
				lat: data.Items[i].Coordinates[0],
				lng: data.Items[i].Coordinates[1]
			});

			var circle = new google.maps.Circle({
				fillColor: pointsStyle[data.Items[i].Action].fillColor,
				fillOpacity: pointsStyle[data.Items[i].Action].fillOpacity,
				map: map,
				center: LatLng,
				radius: pointsStyle[data.Items[i].Action].radius,
				strokeWeight: 0
			});

			if (data.Items[i].Action === 'view') pageviews++;

			if (data.Items[i].Action === 'commitment') commitments++;

		}

		document.getElementById('pageviews').innerText = pageviews;
		document.getElementById('commitments').innerText = commitments;

		resolve('All points processed');

	});
}

function get(url) {
	return new Promise(function(resolve, reject) {

		// Do the usual XHR stuff
		var req = new XMLHttpRequest();
		req.open('GET', url);
		req.setRequestHeader('Content-Type', 'application/json');
		req.onload = function() {
			// This is called even on 404 etc
			// so check the status
			if (req.status == 200) {
				// Resolve the promise with the response text
				resolve(req.response);
			} else {
				// Otherwise reject with the status text
				// which will hopefully be a meaningful error
				statusBad('Disconnected, server error');
				reject(Error(req.statusText));
			}
		};

		// Handle network errors
		req.onerror = function() {
			statusBad('Disconnected, network error');
			reject(Error('Network Error'));
		};

		// Make the request
		req.send();

	});
}

function delay(t) {
	return new Promise(function(resolve) {
		setTimeout(resolve, t);
	});
}

// https://medium.com/adobe-io/how-to-combine-rest-api-calls-with-javascript-promises-in-node-js-or-openwhisk-d96cbc10f299
// https://gist.github.com/trieloff/168312d4dd4d149afdd55cde3d3724cabea
var promiseChain = {
	runChain: function() {
		var timeString = '';
		if (from === null) {
			var d = new Date();
			d.setHours(0, 0, 0, 0);
			d = d.getTime() / 1000;

			if (to === null) {
				to = new Date() / 1000;
			}
			timeString = 'from=' + d + '&to=' + to + '&'; // If from is not set, set to midnight local time in seconds (accurate to the millisecond)
		} else {
			if (to === null) {
				to = new Date() / 1000;
			}
			timeString = 'from=' + from + '&to=' + to + '&';
		}

		var exclusiveStartKeyString = '';
		if (lastEvaluatedKey != undefined && lastEvaluatedKey != 'finished') {
			exclusiveStartKeyString = 'exclusivestartkey=' + lastEvaluatedKey;
		}

		get('/rest/live/read?' + timeString + exclusiveStartKeyString).then(JSON.parse).then(displayPoints).then(function() {
			if (lastEvaluatedKey !== 'finished') {
				promiseChain.runChain();
			} else {
				// Done paginating through data
				from = to + 0.001;
				to = null;
				statusGood('Map connected live');
				return delay(3000).then(function() {
					promiseChain.runChain();
				});
			}
		}, function(error) {
			console.error('callPromise Failed!', error);
			return delay(3000).then(function() {
				promiseChain.runChain();
			});
		}); // Anonymous function
	} // runChain
}; // promiseChain

var midnight = new Date();
midnight.setDate(new Date().getDate()+1);
midnight.setHours(0,0,0,0);
midnight = midnight / 1000;
var now = new Date() / 1000;
midnight = midnight - now;
midnight = midnight + 5; // Add 5 seconds as a cushion
setTimeout(
	midnightTask,
	midnight * 1000
);

function midnightTask() {
	window.location.reload(true);
}

promiseChain.runChain(); // execute function
<|endoftext|>


<commit_before>/* General client code */

Template.login.events({
	'click #venmo-login': function(event) {
		Meteor.loginWithVenmo(function (err, res) {
			if (err){
				console.log(err);
				throw new Meteor.Error("Login failed");
			}
			Meteor.call("after_login", function(err) {
				if (err) {
					throw new Meteor.Error("Unable to update friends.");
				}
			});
			Router.go('/');
		});
	}
});

Template.home.events({
	'click #create': function(event) {
		Router.go('/create');
	}
});

Template.home.helpers({
	'createdPurchases': function(){
		var user = Meteor.user();
		/* Need to check that purchases has been published */
		if (user.purchases) {
			var ids = user.purchases.created;
			return Purchases.find({_id: {$in: ids}});
		} else {
			return null;
		}
	},
	'invitedPurchases': function(){
		var user = Meteor.user();
		/* Need to check that purchases has been published */
		if (user.purchases) {
			var ids = user.purchases.invited;
			return Purchases.find({_id: {$in: ids}});
		} else {
			return null;
		}
	}
});

Template.create.onRendered(function() {
	Session.set("selectedFriends", []);
	var friends = Friends.findOne(Meteor.userId()).venmo_friends;
	var auto_friends = friends.map(function(elem) {
		return {
			/* Venmo id, not app id */
			'id': elem.id,
			'label': elem.display_name,
			'icon': elem.profile_picture_url
		}
	});
	/* jQuery UI autocomplete */
	$("#friends-autocomplete").autocomplete({
		source: auto_friends,
		focus: function( event, ui ) {
			$("#friends-autocomplete").val( ui.item.label );
			return false;
		},
		select: function( event, ui ) {
			var arr = Session.get("selectedFriends");
			arr.push(ui.item);
			Session.set("selectedFriends", arr);
			$("#friends-autocomplete").val('');
			return false;
		}
	});
});

Template.create.events({
	/* Purchase creation via form submission */
	'submit': function(event) {
		event.preventDefault();

		var selected = Session.get("selectedFriends");
		var purch = {};
		purch.title = event.target.title.value;
		purch.description = event.target.description.value;
		purch.cost = Number(event.target.cost.value);
		purch.creator = Meteor.user().services.venmo.id;
		purch.members = selected.map(function(elem){return elem.id});
		purch.accepted = [];
		purch.rejected = [];
		purch.paid = [];
		purch.created_at = new Date();

		var member_names = {};
		selected.forEach(function(elem){
			member_names[elem.id] = elem.label;
		});
		member_names[purch.creator] = Meteor.user().services.venmo.display_name;
		purch.member_names = member_names;

		Meteor.call("check_purchase", purch, function(err, res) {
			if (res.length > 0) {
				alert("ERROR:\n" + res.join("\n"));
			} else {
				/* Add the purchase to the Purchases collection */
				var pid = Purchases.insert(purch);
				/* Add the purchase ID to the creator's list of invited members.
				 * If an invited venmo member isn't a member of ShareCost, then
				 * abort, and remove the purchase. */
				Meteor.call("send_purchase", pid, purch.members, function(error, result) {
					if (error) {
						Purchases.remove(pid);
						alert("Purchase creation failed! Some of the invited friends haven't signed up for ShareCost.");
					} else {
						/* Add the purchase ID to the creator's list of created purchases */
						Meteor.call("own_purchase", pid);
						Router.go('purchase.show', {_id: pid});
					}
				});
			}
		});
	},
	'click .delete-friend': function(event) {
		var id = $(event.target).parents("li").attr("id");
		var arr = Session.get("selectedFriends");
		/* Pretty disgusting way to "delete" something, sorry I was in a hurry. */
		var new_arr = [];
		arr.forEach(function(elem){
			if (elem.id != id) {
				new_arr.push(elem);
			}
		});
		Session.set("selectedFriends", new_arr);
	}
});

Template.create.helpers({
	'selectedFriends': function() {
		return Session.get("selectedFriends");
	}
});

Template.purchaseProposal.helpers({
	'getPurchaseRoute': function() {
		return "/purchase/" + this._id;
	}
});

/*Global Events*/
var events = {
	'click #logout': function(event) {
		Meteor.logout(function(err){
			if (err) {
				throw new Meteor.Error("Logout failed");
			}
			Router.go('/');
		});
	},
	'click #accept-butt': function(event) {
		var purch_id = Template.instance().data._id;
		Meteor.call("accept_purchase", purch_id, function(err, res) {
			if (err) {
				console.log(err);
			}
		});
	},
	'click #reject-butt': function(event) {
		var purch_id = Template.instance().data._id;
		Meteor.call("reject_purchase", purch_id, function(err, res) {
			if (err) {
				console.log(err);
			}
		});
	}
}

Template.BaseLayout.events(events);
Template.ShowPurchase.events(events);
Template.purchaseProposal.events(events);

/*Global Helpers*/
Template.registerHelper('getProfilePictureUrl', function() {
	var user = Meteor.user();
	return user.services.venmo.profile_picture_url
});
Template.registerHelper('getCreatorName', function() {
	return this.member_names[this.creator];
});
/* Returns true if the creator of this purchase is logged in. */
Template.registerHelper('isCreator', function() {
	return this.creator == Meteor.user().services.venmo.id;
});
Template.registerHelper('getAcceptedNames', function() {
	var purch = this;
	return purch.accepted.map(function(elem){
		return purch.member_names[elem];
	});
});
Template.registerHelper('getRejectedNames', function() {
	var purch = this;
	return purch.rejected.map(function(elem){
		return purch.member_names[elem];
	});
});
Template.registerHelper('getPendingNames', function() {
	var purch = this;
	var a = purch.accepted;
	var r = purch.rejected;
	var pending = [];
	/* Disgusting rendition of a set difference */
	purch.members.forEach(function(member) {
		if (a.indexOf(member) == -1 && r.indexOf(member) == -1) {
			pending.push(member);
		}
	});
	return pending.map(function(elem){
		return purch.member_names[elem];
	});
});<commit_msg>checks that user has venmo information<commit_after>/* General client code */

Template.login.events({
	'click #venmo-login': function(event) {
		Meteor.loginWithVenmo(function (err, res) {
			if (err){
				console.log(err);
				throw new Meteor.Error("Login failed");
			}
			Meteor.call("after_login", function(err) {
				if (err) {
					throw new Meteor.Error("Unable to update friends.");
				}
			});
			Router.go('/');
		});
	}
});

Template.home.events({
	'click #create': function(event) {
		Router.go('/create');
	}
});

Template.home.helpers({
	'createdPurchases': function(){
		var user = Meteor.user();
		/* Need to check that purchases has been published */
		if (user.purchases) {
			var ids = user.purchases.created;
			return Purchases.find({_id: {$in: ids}});
		} else {
			return null;
		}
	},
	'invitedPurchases': function(){
		var user = Meteor.user();
		/* Need to check that purchases has been published */
		if (user.purchases) {
			var ids = user.purchases.invited;
			return Purchases.find({_id: {$in: ids}});
		} else {
			return null;
		}
	}
});

Template.create.onRendered(function() {
	Session.set("selectedFriends", []);
	var friends = Friends.findOne(Meteor.userId()).venmo_friends;
	var auto_friends = friends.map(function(elem) {
		return {
			/* Venmo id, not app id */
			'id': elem.id,
			'label': elem.display_name,
			'icon': elem.profile_picture_url
		}
	});
	/* jQuery UI autocomplete */
	$("#friends-autocomplete").autocomplete({
		source: auto_friends,
		focus: function( event, ui ) {
			$("#friends-autocomplete").val( ui.item.label );
			return false;
		},
		select: function( event, ui ) {
			var arr = Session.get("selectedFriends");
			arr.push(ui.item);
			Session.set("selectedFriends", arr);
			$("#friends-autocomplete").val('');
			return false;
		}
	});
});

Template.create.events({
	/* Purchase creation via form submission */
	'submit': function(event) {
		event.preventDefault();

		var selected = Session.get("selectedFriends");
		var purch = {};
		purch.title = event.target.title.value;
		purch.description = event.target.description.value;
		purch.cost = Number(event.target.cost.value);
		purch.creator = Meteor.user().services.venmo.id;
		purch.members = selected.map(function(elem){return elem.id});
		purch.accepted = [];
		purch.rejected = [];
		purch.paid = [];
		purch.created_at = new Date();

		var member_names = {};
		selected.forEach(function(elem){
			member_names[elem.id] = elem.label;
		});
		member_names[purch.creator] = Meteor.user().services.venmo.display_name;
		purch.member_names = member_names;

		Meteor.call("check_purchase", purch, function(err, res) {
			if (res.length > 0) {
				alert("ERROR:\n" + res.join("\n"));
			} else {
				/* Add the purchase to the Purchases collection */
				var pid = Purchases.insert(purch);
				/* Add the purchase ID to the creator's list of invited members.
				 * If an invited venmo member isn't a member of ShareCost, then
				 * abort, and remove the purchase. */
				Meteor.call("send_purchase", pid, purch.members, function(error, result) {
					if (error) {
						Purchases.remove(pid);
						alert("Purchase creation failed! Some of the invited friends haven't signed up for ShareCost.");
					} else {
						/* Add the purchase ID to the creator's list of created purchases */
						Meteor.call("own_purchase", pid);
						Router.go('purchase.show', {_id: pid});
					}
				});
			}
		});
	},
	'click .delete-friend': function(event) {
		var id = $(event.target).parents("li").attr("id");
		var arr = Session.get("selectedFriends");
		/* Pretty disgusting way to "delete" something, sorry I was in a hurry. */
		var new_arr = [];
		arr.forEach(function(elem){
			if (elem.id != id) {
				new_arr.push(elem);
			}
		});
		Session.set("selectedFriends", new_arr);
	}
});

Template.create.helpers({
	'selectedFriends': function() {
		return Session.get("selectedFriends");
	}
});

Template.purchaseProposal.helpers({
	'getPurchaseRoute': function() {
		return "/purchase/" + this._id;
	}
});

/*Global Events*/
var events = {
	'click #logout': function(event) {
		Meteor.logout(function(err){
			if (err) {
				throw new Meteor.Error("Logout failed");
			}
			Router.go('/');
		});
	},
	'click #accept-butt': function(event) {
		var purch_id = Template.instance().data._id;
		Meteor.call("accept_purchase", purch_id, function(err, res) {
			if (err) {
				console.log(err);
			}
		});
	},
	'click #reject-butt': function(event) {
		var purch_id = Template.instance().data._id;
		Meteor.call("reject_purchase", purch_id, function(err, res) {
			if (err) {
				console.log(err);
			}
		});
	}
}

Template.BaseLayout.events(events);
Template.ShowPurchase.events(events);
Template.purchaseProposal.events(events);

/*Global Helpers*/
Template.registerHelper('getProfilePictureUrl', function() {
	var user = Meteor.user();
	if(user.services.venmo)
		return user.services.venmo.profile_picture_url;
});
Template.registerHelper('getCreatorName', function() {
	return this.member_names[this.creator];
});
/* Returns true if the creator of this purchase is logged in. */
Template.registerHelper('isCreator', function() {
	return this.creator == Meteor.user().services.venmo.id;
});
Template.registerHelper('getAcceptedNames', function() {
	var purch = this;
	return purch.accepted.map(function(elem){
		return purch.member_names[elem];
	});
});
Template.registerHelper('getRejectedNames', function() {
	var purch = this;
	return purch.rejected.map(function(elem){
		return purch.member_names[elem];
	});
});
Template.registerHelper('getPendingNames', function() {
	var purch = this;
	var a = purch.accepted;
	var r = purch.rejected;
	var pending = [];
	/* Disgusting rendition of a set difference */
	purch.members.forEach(function(member) {
		if (a.indexOf(member) == -1 && r.indexOf(member) == -1) {
			pending.push(member);
		}
	});
	return pending.map(function(elem){
		return purch.member_names[elem];
	});
});<|endoftext|>


<commit_before>var Router = Backbone.Controller.extend({
    routes: {
        '': 'list',
        'list': 'list',
        'project/:id': 'project'
    },
    list: function() {
        new ProjectListView({ collection: new ProjectList });
    },
    project: function(id) {
        new ProjectView({ model: new Project({ id: id }) });
    },
    error: function() {
        new ErrorView({ message: 'Page not found.' });
    }
});

var App = Backbone.View.extend({
    initialize: function(options) {
        this.settings = this.model;
        this.controller = new Router;

        // Catchall error page requires a regex so we must add its route manually.
        this.controller.route(/^(.*?)/, 'error', Router.prototype.error);

        // Set body ID on each route page.
        // @TODO needs more sanitization.
        this.controller.bind('all', function(page) {
            $('body').attr('id', page.split(':').pop());
        });

        // Bootstrap:
        // 1. Fetch settings object from server.
        // 2. Begin routing.
        this.model.fetch({
            success: function(model) { Backbone.history.start(); },
            error: function(model) { Backbone.history.start(); }
        });
    },
    loading: function(message) {
        this.loadingView = new LoadingView({message: message});
        $(this.el).append(this.loadingView.el);
    },
    done: function() {
        this.loadingView.remove();
    },
    message: function(title, message, type, after) {
        type = type || 'status';
        if (!this.activePopup) {
            this.activePopup = new PopupView({
                title: title,
                content: ich.PopupMessage({
                    message: message,
                    type: type
                }, true),
                after: after
            });
        }
    }
});

$(function() {
    window.app = new App({
        el: $('body'),
        model: new Settings({ id: 'settings' })
    });

   // Use jquery.tipsy for displaying tooltips.
   $('a', this.el).tipsy({
        live:true,
        html:true,
        gravity: function() {
            if ($(this).is('.tipsy-w')) { return 'w'; }
            if ($(this).is('.tipsy-e')) { return 'e'; }
            if ($(this).is('.tipsy-n')) { return 'n'; }
            return 's';
        }
    });
});

<commit_msg>Fetch abilities on app startup.<commit_after>var Router = Backbone.Controller.extend({
    routes: {
        '': 'list',
        'list': 'list',
        'project/:id': 'project'
    },
    list: function() {
        new ProjectListView({ collection: new ProjectList });
    },
    project: function(id) {
        new ProjectView({ model: new Project({ id: id }) });
    },
    error: function() {
        new ErrorView({ message: 'Page not found.' });
    }
});

var App = Backbone.View.extend({
    initialize: function(options) {
        this.settings = this.model;
        this.controller = new Router;
        this.abilities = this.options.abilities;

        // Catchall error page requires a regex so we must add its route manually.
        this.controller.route(/^(.*?)/, 'error', Router.prototype.error);

        // Set body ID on each route page.
        // @TODO needs more sanitization.
        this.controller.bind('all', function(page) {
            $('body').attr('id', page.split(':').pop());
        });

        // Bootstrap:
        // 1. Fetch settings, abilities objects from server.
        // 2. Begin routing.
        this.abilities.fetch();
        this.model.fetch({
            success: function(model) { Backbone.history.start(); },
            error: function(model) { Backbone.history.start(); }
        });
    },
    loading: function(message) {
        this.loadingView = new LoadingView({message: message});
        $(this.el).append(this.loadingView.el);
    },
    done: function() {
        this.loadingView.remove();
    },
    message: function(title, message, type, after) {
        type = type || 'status';
        if (!this.activePopup) {
            this.activePopup = new PopupView({
                title: title,
                content: ich.PopupMessage({
                    message: message,
                    type: type
                }, true),
                after: after
            });
        }
    }
});

$(function() {
    window.app = new App({
        el: $('body'),
        model: new Settings({ id: 'settings' }),
        abilities: new Abilities()
    });

   // Use jquery.tipsy for displaying tooltips.
   $('a', this.el).tipsy({
        live:true,
        html:true,
        gravity: function() {
            if ($(this).is('.tipsy-w')) { return 'w'; }
            if ($(this).is('.tipsy-e')) { return 'e'; }
            if ($(this).is('.tipsy-n')) { return 'n'; }
            return 's';
        }
    });
});

<|endoftext|>


<commit_before>Router.route('/', function () {
  var self = this
  self.render('pageFront')
})

Router.route('/journal', function () {
  var self = this
  var queryDict = self.params.query
  var limit = (!isNaN(queryDict.limit) ? parseInt(queryDict.limit) : 10)
  var skip = (!isNaN(queryDict.skip) ? parseInt(queryDict.skip) : 0)
  Meteor.call('setPagerParameters', limit, skip, function (err, pager) {
    console.log(pager)
    Meteor.call('getPosts', limit, skip, function (err, posts) {
      if(!err) {
        posts = EJSON.parse(posts)
        self.render('pageBlog', {data: {
          posts: posts
        , pager: pager
        }})
      }
    })
  })
})

Router.route('/journal/post/:title', function () {
  var self = this
  var postUrlTitle = self.params.title
  Meteor.call('getSinglePost', postUrlTitle, function (err, post) {
    if(!err) {
      post = EJSON.parse(post)
      self.render('pagePost', {data: post})
    }
  })
})<commit_msg>Add some constrol structure<commit_after>Router.route('/', function () {
  var self = this
  self.render('pageFront')
})

Router.route('/journal', function () {
  var self = this
  var queryDict = self.params.query
  var limit = (!isNaN(queryDict.limit) ? parseInt(queryDict.limit) : 10)
  var skip = (!isNaN(queryDict.skip) ? parseInt(queryDict.skip) : 0)
  Meteor.call('setPagerParameters', limit, skip, function (err, pager) {
    if(!err) {
      Meteor.call('getPosts', limit, skip, function (err, posts) {
        if(!err) {
          posts = EJSON.parse(posts)
          self.render('pageBlog', {data: {
            posts: posts
          , pager: pager
          }})
        }
      })
    }
  })
})

Router.route('/journal/post/:title', function () {
  var self = this
  var postUrlTitle = self.params.title
  Meteor.call('getSinglePost', postUrlTitle, function (err, post) {
    if(!err) {
      post = EJSON.parse(post)
      self.render('pagePost', {data: post})
    }
  })
})<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016, Two Sigma Open Source
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of git-meta nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

const co = require("co");

/**
 * this module is the entrypoint for the `commit` command.
 */

/**
 * help text for the `commit` command
 *
 * @property {String}
 */
exports.helpText = `Commit modifications in local repositories and the
meta-repository to point to these new commits.`;

/**
 * Configure the specified `parser` for the `commit` command.
 *
 * @param {ArgumentParser} parser
 */
exports.configureParser = function (parser) {
    parser.addArgument(["-a", "--all"], {
        defaultValue: false,
        required: false,
        action: "storeConst",
        constant: true,
        help: "commit all changed files",
    });
    parser.addArgument(["-m", "--message"], {
        type: "string",
        defaultValue: null,
        required: false,
        help: "commit message; if not specified will prompt"
    });
    parser.addArgument(["--meta"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `
Include changes to the meta-repo; disabled by default to prevent mistakes.`,
        defaultValue: false,
    });
    parser.addArgument(["--amend"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `\
Amend the last commit, including newly staged chnages and, (if -a is \
specified) modifications.  Will fail unless all submodules changed in HEAD \
have matching commits and have no new commits.`,
    });
    parser.addArgument(["-i", "--interactive"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `\
Interactively choose which meta- and sub-repositories to commit, and what
message to use with each one.`,
    });
    parser.addArgument(["file"], {
        type: "string",
        help: `\
When files are provided, this command ignores changes staged in the index,
and instead records the current content of the listed files (which must already
be known to Git).  Note that this command is incompatible with  the '-a' option
and submodule configuration changes (i.e., those that add, remove, or change
the URL of submodules).`,
        nargs: "*",
    });
};

function abortForNoMessage() {
    console.error("Aborting commit due to empty commit message.");
    process.exit(1);
}

function errorWithStatus(status, relCwd, message) {
    const colors = require("colors");
    const PrintStatusUtil = require("../util/print_status_util");

    process.stderr.write(PrintStatusUtil.printRepoStatus(status, relCwd));
    console.error(colors.yellow(message));
    process.exit(1);
}

function checkForPathIncompatibleSubmodules(repoStatus, relCwd) {

    const Commit = require("../util/commit");

    if (Commit.areSubmodulesIncompatibleWithPathCommits(repoStatus)) {
            errorWithStatus(repoStatus, relCwd, `\
Cannot use path-based commit on submodules with staged commits or
configuration changes.`);
    }
}

const doCommit = co.wrap(function *(args) {
    const path = require("path");

    const Commit          = require("../util/commit");
    const GitUtil         = require("../util/git_util");
    const PrintStatusUtil = require("../util/print_status_util");

    const repo = yield GitUtil.getCurrentRepo();
    const cwd = process.cwd();
    const workdir = repo.workdir();
    const relCwd = path.relative(workdir, cwd);
    const repoStatus = yield Commit.getCommitStatus(repo,
                                                    cwd, {
        showMetaChanges: args.meta,
        all: args.all,
        paths: args.file,
    });

    // Abort if there are uncommittable submodules; we don't want to commit a
    // .gitmodules file with references to a submodule that doesn't have a
    // commit.
    //
    // TODO: potentially do somthing more intelligent like committing a
    // different versions of .gitmodules than what is on disk to omit
    // "uncommittable" submodules.  Considering that this situation should be
    // relatively rare, I don't think it's worth the additional complexity at
    // this time.

    if (repoStatus.areUncommittableSubmodules()) {
        errorWithStatus(
                  repoStatus,
                  relCwd,
                  "Please stage changes in new submodules before committing.");
    }

    // If we're using paths, the status of what we're committing needs to be
    // calculated.  Also, we need to see if there are any submodule
    // configuration changes.

    const usingPaths = 0 !== args.file.length;

    // If we're doing a path based commit, validate that we are in a supported
    // configuration, unless it's interactive, where the user can see that
    // things other than the paths chosen will be committed.

    if (usingPaths && !args.interactive) {
        checkForPathIncompatibleSubmodules(repoStatus, relCwd);
    }

    // If there is nothing possible to commit, exit early.

    if (!Commit.shouldCommit(repoStatus, false, undefined)) {
        process.stdout.write(PrintStatusUtil.printRepoStatus(repoStatus,
                                                             relCwd));
        return;
    }

    let message = args.message;
    let subMessages;

    if (args.interactive) {
        // If 'interactive' mode is requested, ask the user to specify which
        // repos are committed and with what commit messages.

        const prompt = Commit.formatSplitCommitEditorPrompt(repoStatus);
        const userText = yield GitUtil.editMessage(repo, prompt);
        const userData = Commit.parseSplitCommitMessages(userText);
        message = userData.metaMessage;
        subMessages = userData.subMessages;

        // Check if there's actually anything to commit.

        if (!Commit.shouldCommit(repoStatus, message === null, subMessages)) {
            console.log("Nothing to commit.");
            return;
        }
    }
    else if (null === message) {
        // If no message on the command line, prompt for one.

        const initialMessage = Commit.formatEditorPrompt(repoStatus, cwd);
        const rawMessage = yield GitUtil.editMessage(repo, initialMessage);
        message = GitUtil.stripMessage(rawMessage);
    }

    if ("" === message) {
        abortForNoMessage();
    }

    if (usingPaths) {
        yield Commit.commitPaths(repo, repoStatus, message, subMessages);
    }
    else {
        yield Commit.commit(repo, args.all, repoStatus, message, subMessages);
    }
});

const doAmend = co.wrap(function *(args) {
    const colors = require("colors");
    const path   = require("path");

    const Commit          = require("../util/commit");
    const GitUtil         = require("../util/git_util");
    const UserError       = require("../util/user_error");

    const usingPaths = 0 !== args.file.length;

    if (usingPaths) {
        throw new UserError("Paths not supported with amend yet.");
    }

    const repo = yield GitUtil.getCurrentRepo();
    const workdir = repo.workdir();
    const cwd = process.cwd();
    const relCwd = path.relative(workdir, cwd);
    const amendStatus = yield Commit.getAmendStatus(repo, {
        showMetaChanges: args.meta,
        all: args.all,
        paths: args.file,
        cwd: relCwd,
    });

    const status = amendStatus.status;
    const subsToAmend = amendStatus.subsToAmend;

    const head = yield repo.getHeadCommit();
    const defaultSig = repo.defaultSignature();
    const headMeta = Commit.getCommitMetaData(head);
    let message;
    let subMessages;
    if (args.interactive) {
        // If 'interactive' mode is requested, ask the user to specify which
        // repos are committed and with what commit messages.

        const prompt = Commit.formatSplitCommitEditorPrompt(status,
                                                            defaultSig,
                                                            headMeta,
                                                            subsToAmend);
        const userText = yield GitUtil.editMessage(repo, prompt);
        const userData = Commit.parseSplitCommitMessages(userText);
        message = userData.metaMessage;
        subMessages = userData.subMessages;

        // Check if there's actually anything to commit.

        if (!Commit.shouldCommit(status, message === null, subMessages)) {
            console.log("Nothing to commit.");
            return;
        }
    }
    else {
        const mismatched = Object.keys(subsToAmend).filter(name => {
            const meta = subsToAmend[name];
            return !headMeta.equivalent(meta);
        });
        if (0 !== mismatched.length) {
            let error = `\
Cannot amend because the signatures of the affected commits in the
following sub-repos do not match that of the meta-repo:
`;
            mismatched.forEach(name => {
                error += `    ${colors.red(name)}\n`;
            });
            error += `\
You can make this commit using the interactive ('-i') commit option.`;
            throw new UserError(error);
        }

        // If no message, use editor.

        if (null === args.message) {
            const prompt = Commit.formatAmendEditorPrompt(defaultSig,
                                                          headMeta,
                                                          status,
                                                          relCwd);
            const rawMessage = yield GitUtil.editMessage(repo, prompt);
            message = GitUtil.stripMessage(rawMessage);
        }
    }

    if ("" === message) {
        abortForNoMessage();
    }

    // Finally, perform the operation.

    yield Commit.amendMetaRepo(repo,
                               status,
                               Object.keys(subsToAmend),
                               args.all,
                               message,
                               subMessages);
});

/**
 * Exeucte the `commit` command according to the specified `args`.
 *
 * @async
 * @param {Object}  args
 * @param {Boolean} args.all
 * @param {String}  [args.message]
 */
exports.executeableSubcommand = function (args) {
    if (args.all && 0 !== args.file.length) {
        console.error("The use of '-a' and files does not make sense.");
        process.exit(1);
    }
    if (args.message && args.interactive) {
        console.error("The use of '-i' and '-m' does not make sense.");
        process.exit(1);
    }
    if (args.amend) {
        return doAmend(args);                                         // RETURN
    }
    return doCommit(args);
};
<commit_msg>Fix non-interactive `commit --amend`.<commit_after>/*
 * Copyright (c) 2016, Two Sigma Open Source
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of git-meta nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
"use strict";

const co = require("co");

/**
 * this module is the entrypoint for the `commit` command.
 */

// TODO: I need to move the bodies of `doCommit` and `doAmend` into the util
// and create test drivers for them.  I hadn't yet because these methods
// may require user interaction, but I think that's actually pretty easy to
// stub out.

/**
 * help text for the `commit` command
 *
 * @property {String}
 */
exports.helpText = `Commit modifications in local repositories and the
meta-repository to point to these new commits.`;

/**
 * Configure the specified `parser` for the `commit` command.
 *
 * @param {ArgumentParser} parser
 */
exports.configureParser = function (parser) {
    parser.addArgument(["-a", "--all"], {
        defaultValue: false,
        required: false,
        action: "storeConst",
        constant: true,
        help: "commit all changed files",
    });
    parser.addArgument(["-m", "--message"], {
        type: "string",
        defaultValue: null,
        required: false,
        help: "commit message; if not specified will prompt"
    });
    parser.addArgument(["--meta"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `
Include changes to the meta-repo; disabled by default to prevent mistakes.`,
        defaultValue: false,
    });
    parser.addArgument(["--amend"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `\
Amend the last commit, including newly staged chnages and, (if -a is \
specified) modifications.  Will fail unless all submodules changed in HEAD \
have matching commits and have no new commits.`,
    });
    parser.addArgument(["-i", "--interactive"], {
        required: false,
        action: "storeConst",
        constant: true,
        help: `\
Interactively choose which meta- and sub-repositories to commit, and what
message to use with each one.`,
    });
    parser.addArgument(["file"], {
        type: "string",
        help: `\
When files are provided, this command ignores changes staged in the index,
and instead records the current content of the listed files (which must already
be known to Git).  Note that this command is incompatible with  the '-a' option
and submodule configuration changes (i.e., those that add, remove, or change
the URL of submodules).`,
        nargs: "*",
    });
};

function abortForNoMessage() {
    console.error("Aborting commit due to empty commit message.");
    process.exit(1);
}

function errorWithStatus(status, relCwd, message) {
    const colors = require("colors");
    const PrintStatusUtil = require("../util/print_status_util");

    process.stderr.write(PrintStatusUtil.printRepoStatus(status, relCwd));
    console.error(colors.yellow(message));
    process.exit(1);
}

function checkForPathIncompatibleSubmodules(repoStatus, relCwd) {

    const Commit = require("../util/commit");

    if (Commit.areSubmodulesIncompatibleWithPathCommits(repoStatus)) {
            errorWithStatus(repoStatus, relCwd, `\
Cannot use path-based commit on submodules with staged commits or
configuration changes.`);
    }
}

const doCommit = co.wrap(function *(args) {
    const path = require("path");

    const Commit          = require("../util/commit");
    const GitUtil         = require("../util/git_util");
    const PrintStatusUtil = require("../util/print_status_util");

    const repo = yield GitUtil.getCurrentRepo();
    const cwd = process.cwd();
    const workdir = repo.workdir();
    const relCwd = path.relative(workdir, cwd);
    const repoStatus = yield Commit.getCommitStatus(repo,
                                                    cwd, {
        showMetaChanges: args.meta,
        all: args.all,
        paths: args.file,
    });

    // Abort if there are uncommittable submodules; we don't want to commit a
    // .gitmodules file with references to a submodule that doesn't have a
    // commit.
    //
    // TODO: potentially do somthing more intelligent like committing a
    // different versions of .gitmodules than what is on disk to omit
    // "uncommittable" submodules.  Considering that this situation should be
    // relatively rare, I don't think it's worth the additional complexity at
    // this time.

    if (repoStatus.areUncommittableSubmodules()) {
        errorWithStatus(
                  repoStatus,
                  relCwd,
                  "Please stage changes in new submodules before committing.");
    }

    // If we're using paths, the status of what we're committing needs to be
    // calculated.  Also, we need to see if there are any submodule
    // configuration changes.

    const usingPaths = 0 !== args.file.length;

    // If we're doing a path based commit, validate that we are in a supported
    // configuration, unless it's interactive, where the user can see that
    // things other than the paths chosen will be committed.

    if (usingPaths && !args.interactive) {
        checkForPathIncompatibleSubmodules(repoStatus, relCwd);
    }

    // If there is nothing possible to commit, exit early.

    if (!Commit.shouldCommit(repoStatus, false, undefined)) {
        process.stdout.write(PrintStatusUtil.printRepoStatus(repoStatus,
                                                             relCwd));
        return;
    }

    let message = args.message;
    let subMessages;

    if (args.interactive) {
        // If 'interactive' mode is requested, ask the user to specify which
        // repos are committed and with what commit messages.

        const prompt = Commit.formatSplitCommitEditorPrompt(repoStatus);
        const userText = yield GitUtil.editMessage(repo, prompt);
        const userData = Commit.parseSplitCommitMessages(userText);
        message = userData.metaMessage;
        subMessages = userData.subMessages;

        // Check if there's actually anything to commit.

        if (!Commit.shouldCommit(repoStatus, message === null, subMessages)) {
            console.log("Nothing to commit.");
            return;
        }
    }
    else if (null === message) {
        // If no message on the command line, prompt for one.

        const initialMessage = Commit.formatEditorPrompt(repoStatus, cwd);
        const rawMessage = yield GitUtil.editMessage(repo, initialMessage);
        message = GitUtil.stripMessage(rawMessage);
    }

    if ("" === message) {
        abortForNoMessage();
    }

    if (usingPaths) {
        yield Commit.commitPaths(repo, repoStatus, message, subMessages);
    }
    else {
        yield Commit.commit(repo, args.all, repoStatus, message, subMessages);
    }
});

const doAmend = co.wrap(function *(args) {
    const colors = require("colors");
    const path   = require("path");

    const Commit          = require("../util/commit");
    const GitUtil         = require("../util/git_util");
    const UserError       = require("../util/user_error");

    const usingPaths = 0 !== args.file.length;

    if (usingPaths) {
        throw new UserError("Paths not supported with amend yet.");
    }

    const repo = yield GitUtil.getCurrentRepo();
    const workdir = repo.workdir();
    const cwd = process.cwd();
    const relCwd = path.relative(workdir, cwd);
    const amendStatus = yield Commit.getAmendStatus(repo, {
        showMetaChanges: args.meta,
        all: args.all,
        paths: args.file,
        cwd: relCwd,
    });

    const status = amendStatus.status;
    const subsToAmend = amendStatus.subsToAmend;

    const head = yield repo.getHeadCommit();
    const defaultSig = repo.defaultSignature();
    const headMeta = Commit.getCommitMetaData(head);
    let message;
    let subMessages = null;
    if (args.interactive) {
        // If 'interactive' mode is requested, ask the user to specify which
        // repos are committed and with what commit messages.

        const prompt = Commit.formatSplitCommitEditorPrompt(status,
                                                            defaultSig,
                                                            headMeta,
                                                            subsToAmend);
        const userText = yield GitUtil.editMessage(repo, prompt);
        const userData = Commit.parseSplitCommitMessages(userText);
        message = userData.metaMessage;
        subMessages = userData.subMessages;

        // Check if there's actually anything to commit.

        if (!Commit.shouldCommit(status, message === null, subMessages)) {
            console.log("Nothing to commit.");
            return;
        }
    }
    else {
        const mismatched = Object.keys(subsToAmend).filter(name => {
            const meta = subsToAmend[name];
            return !headMeta.equivalent(meta);
        });
        if (0 !== mismatched.length) {
            let error = `\
Cannot amend because the signatures of the affected commits in the
following sub-repos do not match that of the meta-repo:
`;
            mismatched.forEach(name => {
                error += `    ${colors.red(name)}\n`;
            });
            error += `\
You can make this commit using the interactive ('-i') commit option.`;
            throw new UserError(error);
        }

        // If no message, use editor.

        if (null === args.message) {
            const prompt = Commit.formatAmendEditorPrompt(defaultSig,
                                                          headMeta,
                                                          status,
                                                          relCwd);
            const rawMessage = yield GitUtil.editMessage(repo, prompt);
            message = GitUtil.stripMessage(rawMessage);
        }
    }

    if ("" === message) {
        abortForNoMessage();
    }

    // Finally, perform the operation.

    yield Commit.amendMetaRepo(repo,
                               status,
                               Object.keys(subsToAmend),
                               args.all,
                               message,
                               subMessages);
});

/**
 * Exeucte the `commit` command according to the specified `args`.
 *
 * @async
 * @param {Object}  args
 * @param {Boolean} args.all
 * @param {String}  [args.message]
 */
exports.executeableSubcommand = function (args) {
    if (args.all && 0 !== args.file.length) {
        console.error("The use of '-a' and files does not make sense.");
        process.exit(1);
    }
    if (args.message && args.interactive) {
        console.error("The use of '-i' and '-m' does not make sense.");
        process.exit(1);
    }
    if (args.amend) {
        return doAmend(args);                                         // RETURN
    }
    return doCommit(args);
};
<|endoftext|>


<commit_before>/*jshint -W098*/
/*global describe, it, before, beforeEach, after, afterEach, sinon, beforeAll, chai */
var instance, sandbox, notificationsCollection;

var setupFn = function () {
    if (sandbox) {
        //Munit doesn't call the afterEach when a test fails so we do cleanup here to prevent cascading fails
        //See for more info: https://github.com/spacejamio/meteor-munit/issues/24
        tearDownFn();
    }

    instance = Notifications;
    sandbox = sinon.sandbox.create();
    notificationsCollection = instance._getNotificationsCollection();
};

var tearDownFn = function () {
    sandbox.restore();
    instance._notificationTimeout = undefined;
    notificationsCollection.remove({});
    sandbox = undefined;
};

var testNotification, timedNotification, clock, testId;

var expect = chai.expect;

describe('#addNotification', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should call _add', function () {
        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123');
        expect(_add.callCount).to.equal(1);
    });

    it('Should return the _id of the notification', function () {
        var testID = 123;
        var _add = sandbox.stub(instance, '_add').returns(testID);
        expect(instance.addNotification('Title', 'test123')).to.equal(testID);
    });

    it('Should use the defaultOptions to construct the object to pass to _add', function () {
        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = instance.defaultOptions.type;
        expected.userCloseable = instance.defaultOptions.userCloseable;
        expected.clickBodyToClose = instance.defaultOptions.clickBodyToClose;
        expected.closed = undefined;

        delete expected.timeout;

        instance.addNotification(testTitle, testMessage);
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Should use defaultOptionsByType if exists for type', function () {
        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING] = {};
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].userCloseable = false;
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].clickBodyToClose = false;
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].closed = 'blabla';

        delete expected.timeout;
        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = Notifications.TYPES.WARNING;
        expected.userCloseable = false;
        expected.clickBodyToClose = false;
        expected.closed = 'blabla';

        instance.warn(testTitle, testMessage);
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Called with options - it should call _add', function () {
        var testOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false
        };

        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123', testOptions);
        expect(_add.callCount).to.equal(1);
    });

    it('Called with options - Should use the options to construct the object to pass to _add', function () {
        var testOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false,
            clickBodyToClose: true,
            closed: 'test123'
        };

        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = testOptions.type;
        expected.userCloseable = testOptions.userCloseable;
        expected.clickBodyToClose = testOptions.clickBodyToClose;
        expected.closed = 'test123';
        delete expected.timeout;
        instance.addNotification(testTitle, testMessage, _.clone(testOptions));
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Called with options - Options has a timeout - Should add an expires timestamp to the notification given to _add', function () {
        sandbox.useFakeTimers();

        var timedOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false,
            timeout: 2000
        };

        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123', _.clone(timedOptions));
        var expires = _add.args[0][0].expires;
        expect(expires).to.equal(new Date().getTime() + timedOptions.timeout);
    });
});

describe('#getNotificationClass', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should return the className for the given type', function () {
        expect(instance.getNotificationClass(instance.TYPES.ERROR)).to.equal('error');
        expect(instance.getNotificationClass(instance.TYPES.WARNING)).to.equal('warning');
        expect(instance.getNotificationClass(instance.TYPES.INFO)).to.equal('info');
        expect(instance.getNotificationClass(instance.TYPES.SUCCESS)).to.equal('success');
    });
});

describe('#remove', function () {
    var testNotification;

    beforeEach(function () {
        setupFn();
        testNotification = {};
        testNotification.message = 'test100';
        testNotification.type = instance.defaultOptions.type;
        testNotification.userCloseable = instance.defaultOptions.userCloseable;
        instance._add(testNotification);
        instance._add(testNotification);
        instance._add(testNotification);
        testNotification._id = testId = 'unique';
        instance._add(testNotification);
    });

    it('Should remove the notification with the given _id', function () {
        var size = notificationsCollection.find().count();

        instance.remove({_id: testId});

        expect(notificationsCollection.find().count()).to.equal(size - 1);
        expect(notificationsCollection.findOne({_id: testId})).to.equal(undefined);
    });
});

describe('#_add', function () {
    var testNotification, timedNotification;

    beforeEach(function () {
        setupFn();
        testNotification = {
            message: 'test100',
            type: instance.defaultOptions.type,
            userCloseable: instance.defaultOptions.userCloseable
        };

        timedNotification = _.clone(testNotification);
        timedNotification.expires = new Date().getTime() + 2000;
    });

    it('Should call _add', function () {
        var collectionSize = notificationsCollection.find().count();
        instance._add(testNotification);
        expect(notificationsCollection.find().count()).to.equal(collectionSize + 1);
    });

    it('Should call _createTimeout', function () {
        var _createTimeout = sandbox.stub(instance, '_createTimeout');
        instance._add(timedNotification);
        expect(_createTimeout.callCount).to.equal(1);
    });

    it('When given notification has a expires timestamp - When _notifactionTimeout is truthy - Given expires is higher than any existing timestamp - Should NOT remove the existing timeout', function () {
        instance._notificationTimeout = 'test';
        notificationsCollection.insert(timedNotification);
        var slowNotification = _.clone(timedNotification);
        slowNotification.expires += 250;

        instance._add(_.clone(slowNotification));
        expect(instance._notificationTimeout).to.equal('test');
    });

    it('When given notification has a expires timestamp - When _notifactionTimeout is truthy - Given expires timestamp is lower than any existing timestamp - Should call _createTimeout', function () {
        var fastNotification = _.clone(timedNotification);
        fastNotification.expires -= 250;

        instance._notificationTimeout = 'test';
        notificationsCollection.insert(timedNotification);

        var _createTimeout = sandbox.stub(instance, '_createTimeout');
        instance._add(fastNotification);
        expect(_createTimeout.callCount).to.equal(1);
    });
});

describe('#_createTimeout', function () {
    var timedNotification, clock;

    beforeEach(function () {
        setupFn();
        clock = sandbox.useFakeTimers();

        timedNotification = {};
        timedNotification.message = 'test100';
        timedNotification.type = instance.defaultOptions.type;
        timedNotification.userCloseable = instance.defaultOptions.userCloseable;
    });

    it('Should set _notificationTimeout', function () {
        timedNotification.expires = (new Date().getTime()) + 2000;
        notificationsCollection.insert(timedNotification);

        instance._createTimeout();
        expect(instance._notificationTimeout).to.not.equal(undefined);
    });

    it('Should remove the notification when the timeout expires', function () {
        timedNotification.expires = (new Date().getTime()) + 2000;
        notificationsCollection.insert(timedNotification);

        var collectionSize = notificationsCollection.find().count();
        instance._createTimeout();
        clock.tick(1999);
        expect(notificationsCollection.find().count()).to.equal(collectionSize);
        clock.tick(2000);

        expect(notificationsCollection.find().count()).to.equal(collectionSize - 1);
    });

    it('Called with very short timeout - Should remove the notification when the timeout expires', function () {
        timedNotification.expires = (new Date().getTime()) + 10;
        notificationsCollection.insert(timedNotification);

        var collectionSize = notificationsCollection.find().count();
        instance._createTimeout();
        clock.tick(9);
        expect(notificationsCollection.find().count()).to.equal(collectionSize);
        clock.tick(10);

        expect(notificationsCollection.find().count()).to.equal(collectionSize - 1);
    });
});

describe('#getDefaultOptions', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should return defaultOptions when type is not given', function () {
        expect(instance.getDefaultOptions()).to.eql(Notifications.defaultOptions);
    });

    it('Should return defaultOptions of the type when type is given', function () {
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING] = {
            foo: 'bar'
        };

        expect(instance.getDefaultOptions(Notifications.TYPES.WARNING)).to.eql(Notifications.defaultOptionsByType[Notifications.TYPES.WARNING]);
    });

});
<commit_msg>Added unit test for onExpires, and adjusted other tests to allow for the new onExpires field<commit_after>/*jshint -W098*/
/*global describe, it, before, beforeEach, after, afterEach, sinon, beforeAll, chai */
var instance, sandbox, notificationsCollection;

var setupFn = function () {
    if (sandbox) {
        //Munit doesn't call the afterEach when a test fails so we do cleanup here to prevent cascading fails
        //See for more info: https://github.com/spacejamio/meteor-munit/issues/24
        tearDownFn();
    }

    instance = Notifications;
    sandbox = sinon.sandbox.create();
    notificationsCollection = instance._getNotificationsCollection();
};

var tearDownFn = function () {
    sandbox.restore();
    instance._notificationTimeout = undefined;
    notificationsCollection.remove({});
    sandbox = undefined;
};

var testNotification, timedNotification, clock, testId;

var expect = chai.expect;
var assert = chai.assert;

describe('#addNotification', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should call _add', function () {
        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123');
        expect(_add.callCount).to.equal(1);
    });

    it('Should return the _id of the notification', function () {
        var testID = 123;
        var _add = sandbox.stub(instance, '_add').returns(testID);
        expect(instance.addNotification('Title', 'test123')).to.equal(testID);
    });

    it('Should use the defaultOptions to construct the object to pass to _add', function () {
        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = instance.defaultOptions.type;
        expected.userCloseable = instance.defaultOptions.userCloseable;
        expected.clickBodyToClose = instance.defaultOptions.clickBodyToClose;
        expected.closed = undefined;
        expected.onExpires = undefined;

        delete expected.timeout;

        instance.addNotification(testTitle, testMessage);
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Should use defaultOptionsByType if exists for type', function () {
        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING] = {};
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].userCloseable = false;
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].clickBodyToClose = false;
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].closed = 'blabla';
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING].onExpires = undefined;

        delete expected.timeout;
        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = Notifications.TYPES.WARNING;
        expected.userCloseable = false;
        expected.clickBodyToClose = false;
        expected.closed = 'blabla';
        expected.onExpires = undefined;

        instance.warn(testTitle, testMessage);
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Called with options - it should call _add', function () {
        var testOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false
        };

        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123', testOptions);
        expect(_add.callCount).to.equal(1);
    });

    it('Called with options - Should use the options to construct the object to pass to _add', function () {
        var testOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false,
            clickBodyToClose: true,
            closed: 'test123',
            onExpires: 'test456'
        };

        var _add = sandbox.stub(instance, '_add');
        var expected = _.clone(instance.defaultOptions);
        var testMessage = 'test123';
        var testTitle = 'Title';

        expected.title = testTitle;
        expected.message = testMessage;
        expected.type = testOptions.type;
        expected.userCloseable = testOptions.userCloseable;
        expected.clickBodyToClose = testOptions.clickBodyToClose;
        expected.closed = 'test123';
        expected.onExpires = 'test456';

        delete expected.timeout;
        instance.addNotification(testTitle, testMessage, _.clone(testOptions));
        expect(_add).to.have.been.calledWith(expected);
    });

    it('Called with options - Options has a timeout - Should add an expires timestamp to the notification given to _add', function () {
        sandbox.useFakeTimers();

        var timedOptions = {
            type: instance.TYPES.ERROR,
            userCloseable: false,
            timeout: 2000
        };

        var _add = sandbox.stub(instance, '_add');
        instance.addNotification('Title', 'test123', _.clone(timedOptions));
        var expires = _add.args[0][0].expires;
        expect(expires).to.equal(new Date().getTime() + timedOptions.timeout);
    });
});

describe('#getNotificationClass', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should return the className for the given type', function () {
        expect(instance.getNotificationClass(instance.TYPES.ERROR)).to.equal('error');
        expect(instance.getNotificationClass(instance.TYPES.WARNING)).to.equal('warning');
        expect(instance.getNotificationClass(instance.TYPES.INFO)).to.equal('info');
        expect(instance.getNotificationClass(instance.TYPES.SUCCESS)).to.equal('success');
    });
});

describe('#remove', function () {
    var testNotification;

    beforeEach(function () {
        setupFn();
        testNotification = {};
        testNotification.message = 'test100';
        testNotification.type = instance.defaultOptions.type;
        testNotification.userCloseable = instance.defaultOptions.userCloseable;
        instance._add(testNotification);
        instance._add(testNotification);
        instance._add(testNotification);
        testNotification._id = testId = 'unique';
        instance._add(testNotification);
    });

    it('Should remove the notification with the given _id', function () {
        var size = notificationsCollection.find().count();

        instance.remove({_id: testId});

        expect(notificationsCollection.find().count()).to.equal(size - 1);
        expect(notificationsCollection.findOne({_id: testId})).to.equal(undefined);
    });
});

describe('#_add', function () {
    var testNotification, timedNotification;

    beforeEach(function () {
        setupFn();
        testNotification = {
            message: 'test100',
            type: instance.defaultOptions.type,
            userCloseable: instance.defaultOptions.userCloseable
        };

        timedNotification = _.clone(testNotification);
        timedNotification.expires = new Date().getTime() + 2000;
    });

    it('Should call _add', function () {
        var collectionSize = notificationsCollection.find().count();
        instance._add(testNotification);
        expect(notificationsCollection.find().count()).to.equal(collectionSize + 1);
    });

    it('Should call _createTimeout', function () {
        var _createTimeout = sandbox.stub(instance, '_createTimeout');
        instance._add(timedNotification);
        expect(_createTimeout.callCount).to.equal(1);
    });

    it('When given notification has a expires timestamp - When _notifactionTimeout is truthy - Given expires is higher than any existing timestamp - Should NOT remove the existing timeout', function () {
        instance._notificationTimeout = 'test';
        notificationsCollection.insert(timedNotification);
        var slowNotification = _.clone(timedNotification);
        slowNotification.expires += 250;

        instance._add(_.clone(slowNotification));
        expect(instance._notificationTimeout).to.equal('test');
    });

    it('When given notification has a expires timestamp - When _notifactionTimeout is truthy - Given expires timestamp is lower than any existing timestamp - Should call _createTimeout', function () {
        var fastNotification = _.clone(timedNotification);
        fastNotification.expires -= 250;

        instance._notificationTimeout = 'test';
        notificationsCollection.insert(timedNotification);

        var _createTimeout = sandbox.stub(instance, '_createTimeout');
        instance._add(fastNotification);
        expect(_createTimeout.callCount).to.equal(1);
    });
});

describe('#_createTimeout', function () {
    var timedNotification, clock;

    beforeEach(function () {
        setupFn();
        clock = sandbox.useFakeTimers();

        timedNotification = {};
        timedNotification.message = 'test100';
        timedNotification.type = instance.defaultOptions.type;
        timedNotification.userCloseable = instance.defaultOptions.userCloseable;
    });

    it('Should set _notificationTimeout', function () {
        timedNotification.expires = (new Date().getTime()) + 2000;
        notificationsCollection.insert(timedNotification);

        instance._createTimeout();
        expect(instance._notificationTimeout).to.not.equal(undefined);
    });

    it('Should remove the notification when the timeout expires', function () {
        timedNotification.expires = (new Date().getTime()) + 2000;
        notificationsCollection.insert(timedNotification);

        var collectionSize = notificationsCollection.find().count();
        instance._createTimeout();
        clock.tick(1999);
        expect(notificationsCollection.find().count()).to.equal(collectionSize);
        clock.tick(2000);

        expect(notificationsCollection.find().count()).to.equal(collectionSize - 1);
    });

    it('Called with very short timeout - Should remove the notification when the timeout expires', function () {
        timedNotification.expires = (new Date().getTime()) + 10;
        notificationsCollection.insert(timedNotification);

        var collectionSize = notificationsCollection.find().count();
        instance._createTimeout();
        clock.tick(9);
        expect(notificationsCollection.find().count()).to.equal(collectionSize);
        clock.tick(10);

        expect(notificationsCollection.find().count()).to.equal(collectionSize - 1);
    });

    it('Should callback onExpires when the timeout expires', function () {
        timedNotification.expires = (new Date().getTime()) + 2000;

        var flag = null;
        timedNotification.onExpires = function(notification) {
            flag = notification._id;
        }
        var notificationId = notificationsCollection.insert(timedNotification);

        var collectionSize = notificationsCollection.find().count();
        instance._createTimeout();
        clock.tick(1999);
        expect(notificationsCollection.find().count()).to.equal(collectionSize);
        expect(flag).to.equal(null);
        clock.tick(2000);

        expect(notificationsCollection.find().count()).to.equal(collectionSize - 1);
        expect(flag).to.equal(notificationId);
    });
});

describe('#getDefaultOptions', function () {
    beforeEach(function () {
        setupFn();
    });

    it('Should return defaultOptions when type is not given', function () {
        expect(instance.getDefaultOptions()).to.eql(Notifications.defaultOptions);
    });

    it('Should return defaultOptions of the type when type is given', function () {
        Notifications.defaultOptionsByType[Notifications.TYPES.WARNING] = {
            foo: 'bar'
        };

        expect(instance.getDefaultOptions(Notifications.TYPES.WARNING)).to.eql(Notifications.defaultOptionsByType[Notifications.TYPES.WARNING]);
    });

});

describe('#onExpires', function () {
    beforeEach(function () {
        setupFn();
        clock = sandbox.useFakeTimers();
    });

    it('Should call onExpires callback on timeout expiry', function () {
        var flag = null;
        var notificationId = instance.info('Title', 'test123', {
            timeout: 2000,
            onExpires: function(notification) {
                flag = notification._id;
            }
        });

        clock.tick(1999);
        expect(notificationsCollection.find().count()).to.equal(1);
        assert(flag === null, "onExpires fired prematurely");

        clock.tick(2000);
        assert(flag === notificationId,  "onExpires should have fired");
        expect(notificationsCollection.find().count()).to.equal(0);
   });

});

<|endoftext|>