<commit_before>var InstanceConstructor = require("../Instance").Instance;
var Settings            = require("../Settings");
var Property            = require("../Property");

exports.prepare = function (Model, associations) {
	Model.hasMany = function () {
		var name;
		var OtherModel = Model;
		var props = null;
		var opts = {};

		for (var i = 0; i < arguments.length; i++) {
			switch (typeof arguments[i]) {
				case "string":
					name = arguments[i];
					break;
				case "function":
					OtherModel = arguments[i];
					break;
				case "object":
					if (props === null) {
						props = arguments[i];
					} else {
						opts = arguments[i];
					}
					break;
			}
		}
		if (props === null) {
			props = {};
		} else {
			for (var k in props) {
				props[k] = Property.check(props[k]);
			}
		}

		var assocName = opts.name || ucfirst(name);
		var association = {
			name           : name,
			model          : OtherModel || Model,
			props          : props,
			autoFetch      : opts.autoFetch || false,
			autoFetchLimit : opts.autoFetchLimit || 2,
			// I'm not sure the next key is used..
			field          : opts.field || Model.settings.get("properties.association_key").replace("{name}", name),
			mergeTable     : opts.mergeTable || (Model.table + "_" + name),
			mergeId        : opts.mergeId || Model.settings.get("properties.association_key").replace("{name}", Model.table),
			mergeAssocId   : opts.mergeAssocId || Model.settings.get("properties.association_key").replace("{name}", name),
			getAccessor    : opts.getAccessor || ("get" + assocName),
			setAccessor    : opts.setAccessor || ("set" + assocName),
			hasAccessor    : opts.hasAccessor || ("has" + assocName),
			delAccessor    : opts.delAccessor || ("remove" + assocName),
			addAccessor    : opts.addAccessor || ("add" + assocName)
		};
		associations.push(association);

		if (opts.reverse) {
			OtherModel.hasMany(opts.reverse, Model, association.props, {
				reversed       : true,
				mergeTable     : association.mergeTable,
				mergeId        : association.mergeAssocId,
				mergeAssocId   : association.mergeId,
				field          : association.field,
				autoFetch      : association.autoFetch,
				autoFetchLimit : association.autoFetchLimit
			});
		}
		return this;
	};
};

exports.extend = function (Model, Instance, Driver, associations, opts, cb) {
	if (associations.length === 0) {
		return cb();
	}

	var pending = associations.length;
	var extendDone = function extendDone() {
		pending -= 1;

		if (pending === 0) {
			return cb();
		}
	};

	for (var i = 0; i < associations.length; i++) {
		extendInstance(Model, Instance, Driver, associations[i], opts, extendDone);
	}
};

function extendInstance(Model, Instance, Driver, association, opts, cb) {
	if (Model.settings.get("instance.cascadeRemove")) {
		Instance.on("beforeRemove", function () {
			Instance[association.delAccessor]();
		});
	}

	Object.defineProperty(Instance, association.hasAccessor, {
		value: function () {
			var Instances = Array.prototype.slice.apply(arguments);
			var cb = Instances.pop();
			var conditions = {}, options = {};

			options.__merge = {
				from: { table: association.mergeTable, field: association.mergeAssocId },
				to: { table: association.model.table, field: association.model.id },
				where: [ association.mergeTable, {} ]
			};
			options.extra = association.props;
			options.extra_info = {
				table: association.mergeTable,
				id: Instance[Model.id],
				id_prop: association.mergeId,
				assoc_prop: association.mergeAssocId
			};

			options.__merge.where[1][association.mergeId] = Instance[Model.id];

			if (Instances.length) {
				options.__merge.where[1][association.mergeAssocId] = [];

				for (var i = 0; i < Instances.length; i++) {
					options.__merge.where[1][association.mergeAssocId].push(Instances[i][association.model.id]);
				}
			}

			association.model.find(conditions, options, function (err, instances) {
				if (err) {
					return cb(err);
				}
				return cb(null, instances.length == Instances.length);
			});
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.getAccessor, {
		value: function () {
			var options    = {};
			var conditions = null;
			var order      = null;
			var cb         = null;

			for (var i = 0; i < arguments.length; i++) {
				switch (typeof arguments[i]) {
					case "function":
						cb = arguments[i];
						break;
					case "object":
						if (Array.isArray(arguments[i])) {
							order = arguments[i];
							order[0] = [ association.model.table, order[0] ];
						} else {
							if (conditions === null) {
								conditions = arguments[i];
							} else {
								options = arguments[i];
							}
						}
						break;
					case "number":
						options.limit = arguments[i];
						break;
				}
			}

			options.__merge = {
				from  : { table: association.mergeTable, field: association.mergeAssocId },
				to    : { table: association.model.table, field: association.model.id },
				where : [ association.mergeTable, {} ]
			};
			options.extra = association.props;
			options.extra_info = {
				table: association.mergeTable,
				id: Instance[Model.id],
				id_prop: association.mergeId,
				assoc_prop: association.mergeAssocId
			};
			if (order !== null) {
				options.order = order;
			}

			if (conditions === null) {
				conditions = {};
			}

			options.__merge.where[1][association.mergeId] = Instance[Model.id];

			if (cb === null) {
				return association.model.find(conditions, options);
			}

			association.model.find(conditions, options, cb);
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.setAccessor, {
		value: function () {
			var Instances = Array.prototype.slice.apply(arguments);
			var cb = (typeof Instances[Instances.length - 1] == "function" ? Instances.pop() : noOperation);

			Instance[association.delAccessor](function (err) {
				if (err) {
					return cb(err);
				}
				Instances.push(cb);
				Instance[association.addAccessor].apply(Instance, Instances);
			});
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.delAccessor, {
		value: function () {
			var Associations = Array.prototype.slice.apply(arguments);
			var cb = (typeof Associations[Associations.length - 1] == "function" ? Associations.pop() : noOperation);
			var conditions = {};
			var associationIds = [];
			var run = function () {
				if (Associations.length === 0) {
					return Driver.remove(association.mergeTable, conditions, cb);
				}

				for (var i = 0; i < Associations.length; i++) {
					if (Associations[i].id) {
						associationIds.push(Associations[i].id);
					}
				}

				if (associationIds.length === 0) {
					return cb(null);
				}

				conditions[association.mergeAssocId] = associationIds;

				Driver.remove(association.mergeTable, conditions, cb);
			};
			conditions[association.mergeId] = Instance[Model.id];

			if (this.saved()) {
				run();
			} else {
				this.save(function (err) {
					if (err) {
						return cb(err);
					}

					return run();
				});
			}
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.addAccessor, {
		value: function () {
			var Associations = [];
			var opts = {};
			var cb = noOperation;
			var run = function () {
				var saveNextAssociation = function () {
					if (Associations.length === 0) {
						return cb();
					}

					var Association = Associations.pop();

					Association.save(function (err) {
						if (err) {
							return cb(err);
						}

						var data = {};
						data[association.mergeId] = Instance[Model.id];
						data[association.mergeAssocId] = Association.id;

						for (var k in opts) {
							data[k] = opts[k];
						}

						Driver.insert(association.mergeTable, data, null, function (err) {
							if (err) {
								return cb(err);
							}

							return saveNextAssociation();
						});
					});
				};

				return saveNextAssociation();
			};

			for (var i = 0; i < arguments.length; i++) {
				switch (typeof arguments[i]) {
					case "function":
						cb = arguments[i];
						break;
					case "object":
						if (arguments[i].isInstance) {
							Associations.push(arguments[i]);
						} else {
							opts = arguments[i];
						}
						break;
				}
			}

			if (this.saved()) {
				run();
			} else {
				this.save(function (err) {
					if (err) {
						return cb(err);
					}

					return run();
				});
			}

			return this;
		},
		enumerable: false
	});

	if (!opts.hasOwnProperty("autoFetchLimit") || typeof opts.autoFetchLimit == "undefined") {
		opts.autoFetchLimit = association.autoFetchLimit;
	}

	if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
		return cb();
	}

	Instance[association.getAccessor]({}, { autoFetchLimit: opts.autoFetchLimit - 1 }, function (err, Assoc) {
		if (!err) {
			Instance[association.name] = Assoc;
		}

		return cb();
	});
}

function ucfirst(text) {
	return text[0].toUpperCase() + text.substr(1);
}

function noOperation() {
}
<commit_msg>Fixes hasMany acessor names to correctly convert prop_name to PropName (underscores)<commit_after>var InstanceConstructor = require("../Instance").Instance;
var Settings            = require("../Settings");
var Property            = require("../Property");

exports.prepare = function (Model, associations) {
	Model.hasMany = function () {
		var name;
		var OtherModel = Model;
		var props = null;
		var opts = {};

		for (var i = 0; i < arguments.length; i++) {
			switch (typeof arguments[i]) {
				case "string":
					name = arguments[i];
					break;
				case "function":
					OtherModel = arguments[i];
					break;
				case "object":
					if (props === null) {
						props = arguments[i];
					} else {
						opts = arguments[i];
					}
					break;
			}
		}
		if (props === null) {
			props = {};
		} else {
			for (var k in props) {
				props[k] = Property.check(props[k]);
			}
		}

		var assocName = opts.name || ucfirst(name);
		var association = {
			name           : name,
			model          : OtherModel || Model,
			props          : props,
			autoFetch      : opts.autoFetch || false,
			autoFetchLimit : opts.autoFetchLimit || 2,
			// I'm not sure the next key is used..
			field          : opts.field || Model.settings.get("properties.association_key").replace("{name}", name),
			mergeTable     : opts.mergeTable || (Model.table + "_" + name),
			mergeId        : opts.mergeId || Model.settings.get("properties.association_key").replace("{name}", Model.table),
			mergeAssocId   : opts.mergeAssocId || Model.settings.get("properties.association_key").replace("{name}", name),
			getAccessor    : opts.getAccessor || ("get" + assocName),
			setAccessor    : opts.setAccessor || ("set" + assocName),
			hasAccessor    : opts.hasAccessor || ("has" + assocName),
			delAccessor    : opts.delAccessor || ("remove" + assocName),
			addAccessor    : opts.addAccessor || ("add" + assocName)
		};
		associations.push(association);

		if (opts.reverse) {
			OtherModel.hasMany(opts.reverse, Model, association.props, {
				reversed       : true,
				mergeTable     : association.mergeTable,
				mergeId        : association.mergeAssocId,
				mergeAssocId   : association.mergeId,
				field          : association.field,
				autoFetch      : association.autoFetch,
				autoFetchLimit : association.autoFetchLimit
			});
		}
		return this;
	};
};

exports.extend = function (Model, Instance, Driver, associations, opts, cb) {
	if (associations.length === 0) {
		return cb();
	}

	var pending = associations.length;
	var extendDone = function extendDone() {
		pending -= 1;

		if (pending === 0) {
			return cb();
		}
	};

	for (var i = 0; i < associations.length; i++) {
		extendInstance(Model, Instance, Driver, associations[i], opts, extendDone);
	}
};

function extendInstance(Model, Instance, Driver, association, opts, cb) {
	if (Model.settings.get("instance.cascadeRemove")) {
		Instance.on("beforeRemove", function () {
			Instance[association.delAccessor]();
		});
	}

	Object.defineProperty(Instance, association.hasAccessor, {
		value: function () {
			var Instances = Array.prototype.slice.apply(arguments);
			var cb = Instances.pop();
			var conditions = {}, options = {};

			options.__merge = {
				from: { table: association.mergeTable, field: association.mergeAssocId },
				to: { table: association.model.table, field: association.model.id },
				where: [ association.mergeTable, {} ]
			};
			options.extra = association.props;
			options.extra_info = {
				table: association.mergeTable,
				id: Instance[Model.id],
				id_prop: association.mergeId,
				assoc_prop: association.mergeAssocId
			};

			options.__merge.where[1][association.mergeId] = Instance[Model.id];

			if (Instances.length) {
				options.__merge.where[1][association.mergeAssocId] = [];

				for (var i = 0; i < Instances.length; i++) {
					options.__merge.where[1][association.mergeAssocId].push(Instances[i][association.model.id]);
				}
			}

			association.model.find(conditions, options, function (err, instances) {
				if (err) {
					return cb(err);
				}
				return cb(null, instances.length == Instances.length);
			});
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.getAccessor, {
		value: function () {
			var options    = {};
			var conditions = null;
			var order      = null;
			var cb         = null;

			for (var i = 0; i < arguments.length; i++) {
				switch (typeof arguments[i]) {
					case "function":
						cb = arguments[i];
						break;
					case "object":
						if (Array.isArray(arguments[i])) {
							order = arguments[i];
							order[0] = [ association.model.table, order[0] ];
						} else {
							if (conditions === null) {
								conditions = arguments[i];
							} else {
								options = arguments[i];
							}
						}
						break;
					case "number":
						options.limit = arguments[i];
						break;
				}
			}

			options.__merge = {
				from  : { table: association.mergeTable, field: association.mergeAssocId },
				to    : { table: association.model.table, field: association.model.id },
				where : [ association.mergeTable, {} ]
			};
			options.extra = association.props;
			options.extra_info = {
				table: association.mergeTable,
				id: Instance[Model.id],
				id_prop: association.mergeId,
				assoc_prop: association.mergeAssocId
			};
			if (order !== null) {
				options.order = order;
			}

			if (conditions === null) {
				conditions = {};
			}

			options.__merge.where[1][association.mergeId] = Instance[Model.id];

			if (cb === null) {
				return association.model.find(conditions, options);
			}

			association.model.find(conditions, options, cb);
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.setAccessor, {
		value: function () {
			var Instances = Array.prototype.slice.apply(arguments);
			var cb = (typeof Instances[Instances.length - 1] == "function" ? Instances.pop() : noOperation);

			Instance[association.delAccessor](function (err) {
				if (err) {
					return cb(err);
				}
				Instances.push(cb);
				Instance[association.addAccessor].apply(Instance, Instances);
			});
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.delAccessor, {
		value: function () {
			var Associations = Array.prototype.slice.apply(arguments);
			var cb = (typeof Associations[Associations.length - 1] == "function" ? Associations.pop() : noOperation);
			var conditions = {};
			var associationIds = [];
			var run = function () {
				if (Associations.length === 0) {
					return Driver.remove(association.mergeTable, conditions, cb);
				}

				for (var i = 0; i < Associations.length; i++) {
					if (Associations[i].id) {
						associationIds.push(Associations[i].id);
					}
				}

				if (associationIds.length === 0) {
					return cb(null);
				}

				conditions[association.mergeAssocId] = associationIds;

				Driver.remove(association.mergeTable, conditions, cb);
			};
			conditions[association.mergeId] = Instance[Model.id];

			if (this.saved()) {
				run();
			} else {
				this.save(function (err) {
					if (err) {
						return cb(err);
					}

					return run();
				});
			}
			return this;
		},
		enumerable: false
	});
	Object.defineProperty(Instance, association.addAccessor, {
		value: function () {
			var Associations = [];
			var opts = {};
			var cb = noOperation;
			var run = function () {
				var saveNextAssociation = function () {
					if (Associations.length === 0) {
						return cb();
					}

					var Association = Associations.pop();

					Association.save(function (err) {
						if (err) {
							return cb(err);
						}

						var data = {};
						data[association.mergeId] = Instance[Model.id];
						data[association.mergeAssocId] = Association.id;

						for (var k in opts) {
							data[k] = opts[k];
						}

						Driver.insert(association.mergeTable, data, null, function (err) {
							if (err) {
								return cb(err);
							}

							return saveNextAssociation();
						});
					});
				};

				return saveNextAssociation();
			};

			for (var i = 0; i < arguments.length; i++) {
				switch (typeof arguments[i]) {
					case "function":
						cb = arguments[i];
						break;
					case "object":
						if (arguments[i].isInstance) {
							Associations.push(arguments[i]);
						} else {
							opts = arguments[i];
						}
						break;
				}
			}

			if (this.saved()) {
				run();
			} else {
				this.save(function (err) {
					if (err) {
						return cb(err);
					}

					return run();
				});
			}

			return this;
		},
		enumerable: false
	});

	if (!opts.hasOwnProperty("autoFetchLimit") || typeof opts.autoFetchLimit == "undefined") {
		opts.autoFetchLimit = association.autoFetchLimit;
	}

	if (opts.autoFetchLimit === 0 || (!opts.autoFetch && !association.autoFetch)) {
		return cb();
	}

	Instance[association.getAccessor]({}, { autoFetchLimit: opts.autoFetchLimit - 1 }, function (err, Assoc) {
		if (!err) {
			Instance[association.name] = Assoc;
		}

		return cb();
	});
}

function ucfirst(text) {
	return text[0].toUpperCase() + text.substr(1).replace(/_([a-z])/, function (m, l) {
		return l.toUpperCase();
	});
}

function noOperation() {
}
<|endoftext|>


<commit_before>var Tuple = require('../lib/tuple')

var gametree = require('../module/gametree')
var uuid = require('../lib/node-uuid')
var fs = require('fs')

var alpha = 'abcdefghijklmnopqrstuvwxyz'

exports.meta = {
    name: 'Smart Game Format',
    extensions: ['sgf']
}

exports.tokenize = function(input) {
    var tokens = []
    var builder = ''
    var propIdentRegex = /^[A-Za-z]+/

    var i = 0
    var inCValueType = false
    var inBackslash = false

    while (i < input.length) {
        if (!inCValueType) {
            builder = ''
            inBackslash = false

            switch (input[i]) {
                case '(':
                    tokens.push(new Tuple('parenthesis', '('))
                    i++
                    break
                case ')':
                    tokens.push(new Tuple('parenthesis', ')'))
                    i++
                    break
                case ';':
                    tokens.push(new Tuple('semicolon', ';'))
                    i++
                    break
                case '[':
                    inCValueType = true
                    i++
                    break
                default:
                    if (/\s/.test(input[i])) {
                        i++
                        break
                    }

                    var match = propIdentRegex.exec(input.slice(i))[0]

                    if (match != null) {
                        tokens.push(new Tuple('prop_ident', match))
                        i += match.length
                    } else {
                        return []
                    }

                    break
            }
        } else {
            if (!inBackslash) {
                switch (input[i]) {
                    case '\\':
                        inBackslash = true
                        break
                    case '\n':
                        builder += '\n'
                        break
                    case ']':
                        inCValueType = false
                        tokens.push(new Tuple('c_value_type', builder))
                        break
                    default:
                        builder += input[i]
                        break
                }
            } else {
                inBackslash = false
                builder += input[i]
            }

            i++
        }
    }

    return tokens
}

exports.parse = function(tokens, start, depth) {
    if (!start) start = [0]
    if (isNaN(depth)) depth = 0

    var i = start[0]
    var node, property, tree = gametree.new()
    tree.collapsed = depth >= setting.get('graph.max_depth')

    while (i < tokens.length) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) break
        if (new Tuple('parenthesis', ')').equals(tokens[i])) return tree

        tokens[i].unpack(function(type, data) {
            if (type == 'semicolon') {
                node = {}
                tree.nodes.push(node)
            } else if (type == 'prop_ident') {
                node[data] = []
                property = node[data]
            } else if (type == 'c_value_type') {
                property.push(data)
            }
        })

        start[0] = ++i
    }

    while (i < tokens.length) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) {
            start[0] = i + 1

            t = exports.parse(tokens, start, depth + Math.min(tree.subtrees.length, 1))
            t.parent = tree
            tree.subtrees.push(t)
            tree.current = 0

            i = start[0]
        } else if (new Tuple('parenthesis', ')').equals(tokens[i])) {
            start[0] = i + 1
            break
        }

        i++
    }

    return tree
}

exports.parseFile = function(filename) {
    var input = fs.readFileSync(filename, { encoding: 'utf8' })
    var tokens = exports.tokenize(input)

    return exports.parse(tokens)
}

exports.point2vertex = function(point) {
    if (point.length != 2) return new Tuple(-1, -1)

    point = point.toLowerCase()
    return new Tuple(alpha.indexOf(point[0]), alpha.indexOf(point[1]))
}

exports.vertex2point = function(tuple) {
    return tuple.unpack(function(x, y) {
        if (x < 0 || y < 0) return ''
        return alpha[x] + alpha[y]
    })
}

exports.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [exports.point2vertex(compressed)]

    var v1 = exports.point2vertex(compressed.slice(0, colon))
    var v2 = exports.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = v1[0]; i <= v2[0]; i++) {
        for (var j = v1[1]; j <= v2[1]; j++) {
            list.push(new Tuple(i, j))
        }
    }

    return list
}

exports.addBoard = function(tree, index, baseboard) {
    if (isNaN(index)) index = 0
    if (index >= tree.nodes.length) return tree

    var node = tree.nodes[index]
    var vertex = null
    var board = null

    if (!baseboard) {
        var prev = gametree.navigate(tree, index, -1)

        if (!prev[0]) {
            var size = 'SZ' in node ? node.SZ[0].toInt() : 19
            baseboard = new Board(size)
        } else {
            var prevNode = prev[0].nodes[prev[1]]

            if (!prevNode.board) exports.addBoard(prev[0], prev[1])
            baseboard = prevNode.board
        }
    }

    if ('B' in node) {
        vertex = exports.point2vertex(node.B[0])
        board = baseboard.makeMove(1, vertex)
    } else if ('W' in node) {
        vertex = exports.point2vertex(node.W[0])
        board = baseboard.makeMove(-1, vertex)
    } else {
        board = baseboard.makeMove(0)
    }

    var ids = ['AW', 'AE', 'AB']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                board.arrangement[exports.point2vertex(value)] = i - 1
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    board.arrangement[vertex] = i - 1
                })
            }
        })
    }

    if (vertex != null) {
        board.overlays[vertex] = new Tuple('point', 0, '')
    }

    var ids = ['CR', 'MA', 'SQ', 'TR']
    var classes = ['circle', 'cross', 'square', 'triangle']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                board.overlays[exports.point2vertex(value)] = new Tuple(classes[i], 0, '')
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    board.overlays[vertex] = new Tuple(classes[i], 0, '')
                })
            }
        })
    }

    if ('LB' in node) {
        node.LB.each(function(composed) {
            var sep = composed.indexOf(':')
            var point = composed.slice(0, sep)
            var label = composed.slice(sep + 1).replace(/\s+/, ' ')
            board.overlays[exports.point2vertex(point)] = new Tuple('label', 0, label)
        })
    }

    node.board = board

    if (index == tree.nodes.length - 1 && tree.subtrees.length > 0) {
        // Add variations

        tree.subtrees.each(function(subtree) {
            if (subtree.nodes.length == 0) return

            var v, sign

            if ('B' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].B[0])
                sign = 1
            } else if ('W' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].W[0])
                sign = -1
            } else {
                return
            }

            if (v in board.overlays)
                board.overlays[v] = board.overlays[v].unpack(function(a, b, c) {
                     return new Tuple(a, sign, c)
                })
            else board.overlays[v] = new Tuple('', sign, '')
        })
    }

    return tree
}

exports.tree2string = function(tree) {
    var output = ''

    tree.nodes.each(function(node) {
        output += ';'

        Object.each(node, function(values, id) {
            if (id.toUpperCase() != id) return
            output += id

            values.each(function(value) {
                output += '[' + exports.escapeString(value.toString()) + ']'
            })
        })

        output += '\n'
    })

    if (tree.current != null)
        output += '(' + exports.tree2string(tree.subtrees[tree.current]) + ')'

    for (var i = 0; i < tree.subtrees.length; i++) {
        if (i == tree.current) continue
        output += '(' + exports.tree2string(tree.subtrees[i]) + ')'
    }

    return output
}

exports.escapeString = function(input) {
    return input.replace('\\', '\\\\').replace(']', '\\]')
}
<commit_msg>Fix off by one<commit_after>var Tuple = require('../lib/tuple')

var gametree = require('../module/gametree')
var uuid = require('../lib/node-uuid')
var fs = require('fs')

var alpha = 'abcdefghijklmnopqrstuvwxyz'

exports.meta = {
    name: 'Smart Game Format',
    extensions: ['sgf']
}

exports.tokenize = function(input) {
    var tokens = []
    var builder = ''
    var propIdentRegex = /^[A-Za-z]+/

    var i = 0
    var inCValueType = false
    var inBackslash = false

    while (i < input.length) {
        if (!inCValueType) {
            builder = ''
            inBackslash = false

            switch (input[i]) {
                case '(':
                    tokens.push(new Tuple('parenthesis', '('))
                    i++
                    break
                case ')':
                    tokens.push(new Tuple('parenthesis', ')'))
                    i++
                    break
                case ';':
                    tokens.push(new Tuple('semicolon', ';'))
                    i++
                    break
                case '[':
                    inCValueType = true
                    i++
                    break
                default:
                    if (/\s/.test(input[i])) {
                        i++
                        break
                    }

                    var match = propIdentRegex.exec(input.slice(i))[0]

                    if (match != null) {
                        tokens.push(new Tuple('prop_ident', match))
                        i += match.length
                    } else {
                        return []
                    }

                    break
            }
        } else {
            if (!inBackslash) {
                switch (input[i]) {
                    case '\\':
                        inBackslash = true
                        break
                    case '\n':
                        builder += '\n'
                        break
                    case ']':
                        inCValueType = false
                        tokens.push(new Tuple('c_value_type', builder))
                        break
                    default:
                        builder += input[i]
                        break
                }
            } else {
                inBackslash = false
                builder += input[i]
            }

            i++
        }
    }

    return tokens
}

exports.parse = function(tokens, start, depth) {
    if (!start) start = [0]
    if (isNaN(depth)) depth = 0

    var i = start[0]
    var node, property, tree = gametree.new()
    tree.collapsed = depth >= setting.get('graph.max_depth')

    while (i < tokens.length) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) break
        if (new Tuple('parenthesis', ')').equals(tokens[i])) return tree

        tokens[i].unpack(function(type, data) {
            if (type == 'semicolon') {
                node = {}
                tree.nodes.push(node)
            } else if (type == 'prop_ident') {
                node[data] = []
                property = node[data]
            } else if (type == 'c_value_type') {
                property.push(data)
            }
        })

        start[0] = ++i
    }

    while (i < tokens.length) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) {
            start[0] = i + 1

            t = exports.parse(tokens, start, depth + Math.min(tree.subtrees.length, 1))
            t.parent = tree
            tree.subtrees.push(t)
            tree.current = 0

            i = start[0]
        } else if (new Tuple('parenthesis', ')').equals(tokens[i])) {
            start[0] = i
            break
        }

        i++
    }

    return tree
}

exports.parseFile = function(filename) {
    var input = fs.readFileSync(filename, { encoding: 'utf8' })
    var tokens = exports.tokenize(input)

    return exports.parse(tokens)
}

exports.point2vertex = function(point) {
    if (point.length != 2) return new Tuple(-1, -1)

    point = point.toLowerCase()
    return new Tuple(alpha.indexOf(point[0]), alpha.indexOf(point[1]))
}

exports.vertex2point = function(tuple) {
    return tuple.unpack(function(x, y) {
        if (x < 0 || y < 0) return ''
        return alpha[x] + alpha[y]
    })
}

exports.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [exports.point2vertex(compressed)]

    var v1 = exports.point2vertex(compressed.slice(0, colon))
    var v2 = exports.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = v1[0]; i <= v2[0]; i++) {
        for (var j = v1[1]; j <= v2[1]; j++) {
            list.push(new Tuple(i, j))
        }
    }

    return list
}

exports.addBoard = function(tree, index, baseboard) {
    if (isNaN(index)) index = 0
    if (index >= tree.nodes.length) return tree

    var node = tree.nodes[index]
    var vertex = null
    var board = null

    if (!baseboard) {
        var prev = gametree.navigate(tree, index, -1)

        if (!prev[0]) {
            var size = 'SZ' in node ? node.SZ[0].toInt() : 19
            baseboard = new Board(size)
        } else {
            var prevNode = prev[0].nodes[prev[1]]

            if (!prevNode.board) exports.addBoard(prev[0], prev[1])
            baseboard = prevNode.board
        }
    }

    if ('B' in node) {
        vertex = exports.point2vertex(node.B[0])
        board = baseboard.makeMove(1, vertex)
    } else if ('W' in node) {
        vertex = exports.point2vertex(node.W[0])
        board = baseboard.makeMove(-1, vertex)
    } else {
        board = baseboard.makeMove(0)
    }

    var ids = ['AW', 'AE', 'AB']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                board.arrangement[exports.point2vertex(value)] = i - 1
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    board.arrangement[vertex] = i - 1
                })
            }
        })
    }

    if (vertex != null) {
        board.overlays[vertex] = new Tuple('point', 0, '')
    }

    var ids = ['CR', 'MA', 'SQ', 'TR']
    var classes = ['circle', 'cross', 'square', 'triangle']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                board.overlays[exports.point2vertex(value)] = new Tuple(classes[i], 0, '')
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    board.overlays[vertex] = new Tuple(classes[i], 0, '')
                })
            }
        })
    }

    if ('LB' in node) {
        node.LB.each(function(composed) {
            var sep = composed.indexOf(':')
            var point = composed.slice(0, sep)
            var label = composed.slice(sep + 1).replace(/\s+/, ' ')
            board.overlays[exports.point2vertex(point)] = new Tuple('label', 0, label)
        })
    }

    node.board = board

    if (index == tree.nodes.length - 1 && tree.subtrees.length > 0) {
        // Add variations

        tree.subtrees.each(function(subtree) {
            if (subtree.nodes.length == 0) return

            var v, sign

            if ('B' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].B[0])
                sign = 1
            } else if ('W' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].W[0])
                sign = -1
            } else {
                return
            }

            if (v in board.overlays)
                board.overlays[v] = board.overlays[v].unpack(function(a, b, c) {
                     return new Tuple(a, sign, c)
                })
            else board.overlays[v] = new Tuple('', sign, '')
        })
    }

    return tree
}

exports.tree2string = function(tree) {
    var output = ''

    tree.nodes.each(function(node) {
        output += ';'

        Object.each(node, function(values, id) {
            if (id.toUpperCase() != id) return
            output += id

            values.each(function(value) {
                output += '[' + exports.escapeString(value.toString()) + ']'
            })
        })

        output += '\n'
    })

    if (tree.current != null)
        output += '(' + exports.tree2string(tree.subtrees[tree.current]) + ')'

    for (var i = 0; i < tree.subtrees.length; i++) {
        if (i == tree.current) continue
        output += '(' + exports.tree2string(tree.subtrees[i]) + ')'
    }

    return output
}

exports.escapeString = function(input) {
    return input.replace('\\', '\\\\').replace(']', '\\]')
}
<|endoftext|>


<commit_before>var Board = require('./board.js')
var Tuple = require('../lib/tuple')

var uuid = require('../lib/node-uuid')
var fs = require('fs')

var alpha = 'abcdefghijklmnopqrstuvwxyz'

exports.tokenize = function(input) {
    var tokens = []
    var builder = ''
    var propIdentRegex = /^[A-Za-z]+/

    var i = 0
    var inCValueType = false
    var inBackslash = false

    while (i < input.length) {
        if (!inCValueType) {
            builder = ''
            inBackslash = false

            switch (input[i]) {
                case '(':
                    tokens.push(new Tuple('parenthesis', '('))
                    i++
                    break
                case ')':
                    tokens.push(new Tuple('parenthesis', ')'))
                    i++
                    break
                case ';':
                    tokens.push(new Tuple('semicolon', ';'))
                    i++
                    break
                case '[':
                    inCValueType = true
                    i++
                    break
                default:
                    if (/\s/.test(input[i])) {
                        i++
                        break
                    }

                    var match = propIdentRegex.exec(input.slice(i))[0]

                    if (match != null) {
                        tokens.push(new Tuple('prop_ident', match))
                        i += match.length
                    } else {
                        return []
                    }

                    break
            }
        } else {
            if (!inBackslash) {
                switch (input[i]) {
                    case '\\':
                        inBackslash = true
                        break
                    case '\n':
                        builder += '\n'
                        break
                    case ']':
                        inCValueType = false
                        tokens.push(new Tuple('c_value_type', builder))
                        break
                    default:
                        builder += input[i]
                        break
                }
            } else {
                inBackslash = false
                builder += input[i]
            }

            i++
        }
    }

    return tokens
}

exports.parse = function(tokens, start, end) {
    if (arguments.length <= 2) end = tokens.length - 1
    if (arguments.length <= 1) start = 0

    var i = start

    var tree = { nodes: [], subtrees: [], parent: null, current: null }
    var node, property

    while (i <= end) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) break
        if (new Tuple('parenthesis', ')').equals(tokens[i])) return null

        tokens[i].unpack(function(type, data) {
            if (type == 'semicolon') {
                node = {}
                tree.nodes.push(node)
            } else if (type == 'prop_ident') {
                node[data] = []
                property = node[data]
            } else if (type == 'c_value_type') {
                property.push(data)
            }
        })

        i++
    }

    var depth = 0
    var newstart = 0

    while (i <= end) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) {
            depth++
            if (depth == 1) newstart = i + 1
        } else if (new Tuple('parenthesis', ')').equals(tokens[i])) {
            depth--
            if (depth == 0) {
                t = exports.parse(tokens, newstart, i - 1)
                t.parent = tree
                tree.subtrees.push(t)
                tree.current = 0
            }
        }

        i++
    }

    return tree
}

exports.parseFile = function(filename) {
    var input = fs.readFileSync(filename, { encoding: 'utf8' })
    var tokens = exports.tokenize(input)

    return exports.parse(tokens)
}

exports.point2vertex = function(point) {
    if (point.length != 2) return new Tuple(-1, -1)

    point = point.toLowerCase()
    return new Tuple(alpha.indexOf(point[0]), alpha.indexOf(point[1]))
}

exports.vertex2point = function(tuple) {
    return tuple.unpack(function(x, y) {
        if (x < 0 || y < 0) return ''
        return alpha[x] + alpha[y]
    })
}

exports.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [exports.point2vertex(compressed)]

    var v1 = exports.point2vertex(compressed.slice(0, colon))
    var v2 = exports.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = v1[0]; i <= v2[0]; i++) {
        for (var j = v1[1]; j <= v2[1]; j++) {
            list.push(new Tuple(i, j))
        }
    }

    return list
}

exports.navigate = function(tree, index, step) {
    if (index + step >= 0 && index + step < tree.nodes.length) {
        return new Tuple(tree, index + step)
    } else if (index + step < 0 && tree.parent) {
        var prev = tree.parent
        index = prev.nodes.length + index + step

        if (index >= 0)
            return new Tuple(prev, index)
        else if (prev.parent)
            return new Tuple(prev.parent, prev.parent.nodes.length - 1)
    } else if (index + step >= tree.nodes.length && tree.current != null) {
        index = index + step - tree.nodes.length
        var next = tree.subtrees[tree.current]

        if (index < next.nodes.length)
            return new Tuple(next, index)
        else if (next.current != null)
            return new Tuple(next.subtrees[next.current], 0)
    }

    return new Tuple(null, 0)
}

exports.addBoard = function(tree, index, baseboard) {
    if (index >= tree.nodes.length) return tree

    var node = tree.nodes[index]
    var vertex = null

    if (arguments.length <= 2) {
        if (index != 0) {
            baseboard = tree.nodes[index - 1].board
        } else {
            if (tree.parent == null || tree.parent.nodes.length == 0) {
                var size = 'SZ' in node ? node.SZ[0].toInt() : 19
                baseboard = new Board(size)
            } else {
                baseboard = tree.parent.nodes.getLast().board
            }
        }
    }

    if ('B' in node) {
        vertex = exports.point2vertex(node.B[0])
        baseboard = baseboard.makeMove(1, vertex)
    } else if ('W' in node) {
        vertex = exports.point2vertex(node.W[0])
        baseboard = baseboard.makeMove(-1, vertex)
    } else {
        baseboard = baseboard.makeMove(0)
    }

    var ids = ['AW', 'AE', 'AB']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                baseboard.arrangement[exports.point2vertex(value)] = i - 1
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    baseboard.arrangement[vertex] = i - 1
                })
            }
        })
    }

    if (vertex != null) {
        baseboard.overlays[vertex] = new Tuple('point', 0, '')
    }

    var ids = ['CR', 'MA', 'SQ', 'TR']
    var classes = ['circle', 'cross', 'square', 'triangle']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                baseboard.overlays[exports.point2vertex(value)] = new Tuple(classes[i], 0, '')
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    baseboard.overlays[vertex] = new Tuple(classes[i], 0, '')
                })
            }
        })
    }

    if ('LB' in node) {
        node.LB.each(function(composed) {
            var sep = composed.indexOf(':')
            var point = composed.slice(0, sep)
            var label = composed.slice(sep + 1).replace(/\s+/, ' ')
            baseboard.overlays[exports.point2vertex(point)] = new Tuple('label', 0, label)
        })
    }

    node.board = baseboard

    if (index == tree.nodes.length - 1 && tree.subtrees.length > 0) {
        // Add variations

        tree.subtrees.each(function(subtree) {
            if (subtree.nodes.length == 0) return

            var v, sign

            if ('B' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].B[0])
                sign = 1
            } else if ('W' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].W[0])
                sign = -1
            } else {
                return
            }

            if (v in baseboard.overlays)
                baseboard.overlays[v] = baseboard.overlays[v].unpack(function(a, b, c) {
                     return new Tuple(a, sign, c)
                })
            else baseboard.overlays[v] = new Tuple('', sign, '')
        })
    }

    return tree
}

exports.addBoards = function(tree, baseboard) {
    if (tree.nodes.length == 0) return tree

    for (var j = 0; j < tree.nodes.length; j++) {
        if ('board' in tree.nodes[j]) continue
        exports.addBoard(tree, j)
    }

    if (tree.current == null) return tree

    exports.addBoards(tree.subtrees[tree.current], baseboard)
    return tree
}

exports.splitTree = function(tree, index) {
    if (index < 0 || index >= tree.nodes.length - 1) return tree

    var newnodes = tree.nodes.slice(0, index + 1)
    tree.nodes = tree.nodes.slice(index + 1)

    var newtree = { nodes: newnodes, subtrees: [tree], parent: tree.parent, current: 0 }
    tree.parent = newtree

    if (newtree.parent != null) {
        newtree.parent.subtrees[newtree.parent.subtrees.indexOf(tree)] = newtree
    }

    return newtree
}

exports.tree2string = function(tree) {
    var output = ''

    tree.nodes.each(function(node) {
        output += ';'

        Object.each(node, function(values, id) {
            if (id.toUpperCase() != id) return
            output += id

            values.each(function(value) {
                output += '[' + exports.escapeString(value.toString()) + ']'
            })
        })

        output += '\n'
    })

    if (tree.current != null)
        output += '(' + exports.tree2string(tree.subtrees[tree.current]) + ')'

    for (var i = 0; i < tree.subtrees.length; i++) {
        if (i == tree.current) continue
        output += '(' + exports.tree2string(tree.subtrees[i]) + ')'
    }

    return output
}

exports.escapeString = function(input) {
    return input.replace('\\', '\\\\').replace(']', '\\]')
}

exports.getDepth = function(tree) {
    var depth = 0

    tree.subtrees.each(function(subtree) {
        depth = Math.max(exports.getDepth(subtree), depth)
    })

    return depth + tree.nodes.length
}

exports.getSections = function(tree, n) {
    if (n < tree.nodes.length) return [new Tuple(tree, n)]

    var sections = []

    tree.subtrees.each(function(subtree) {
        sections.combine(exports.getSections(subtree, n - tree.nodes.length))
    })

    return sections
}

exports.tree2graph = function(tree, xshift, yshift, getWidth) {
    if (!xshift) xshift = 0
    if (!yshift) yshift = 0

    var graph = { nodes: [], edges: [] }
    var width = 1

    if (!('id' in tree)) tree.id = uuid.v4()
    var id = tree.id

    for (var i = 0; i < tree.nodes.length; i++) {
        graph.nodes.push({
            'id': id + '-' + i,
            x: xshift * 30,
            y: (yshift + i) * 30,
            size: 4
        })

        if (i != 0) {
            graph.edges.push({
                'id': id + '-e' + i,
                source: id + '-' + (i - 1),
                target: id + '-' + i
            })
        }
    }

    for (var i = 0; i < tree.subtrees.length; i++) {
        var subtree = tree.subtrees[i]
        var result = exports.tree2graph(subtree, xshift, yshift + tree.nodes.length, true)

        result[0].nodes.each(function(node) { graph.nodes.push(node) })
        result[0].edges.each(function(edge) { graph.edges.push(edge) })

        graph.edges.push({
            'id': id + '-b' + i,
            source: id + '-' + (tree.nodes.length - 1),
            target: result[0].nodes[0].id
        })

        xshift += result[1]
        width += result[1]

        // Adjust width
        if (i == 0) width--
        if (i < tree.subtrees.length - 1) {
            var depth = Math.max.apply(this, tree.subtrees.slice(i + 1).map(function(t) {
                return exports.getDepth(t)
            }))

            if (subtree.nodes.length >= depth) {
                xshift -= result[1] - 1
                width -= result[1] - 1
            }
        }
    }

    if (getWidth) return new Tuple(graph, width)
    else return graph
}
<commit_msg>New tree2graph algorithm<commit_after>var Board = require('./board.js')
var Tuple = require('../lib/tuple')

var uuid = require('../lib/node-uuid')
var fs = require('fs')

var alpha = 'abcdefghijklmnopqrstuvwxyz'

exports.tokenize = function(input) {
    var tokens = []
    var builder = ''
    var propIdentRegex = /^[A-Za-z]+/

    var i = 0
    var inCValueType = false
    var inBackslash = false

    while (i < input.length) {
        if (!inCValueType) {
            builder = ''
            inBackslash = false

            switch (input[i]) {
                case '(':
                    tokens.push(new Tuple('parenthesis', '('))
                    i++
                    break
                case ')':
                    tokens.push(new Tuple('parenthesis', ')'))
                    i++
                    break
                case ';':
                    tokens.push(new Tuple('semicolon', ';'))
                    i++
                    break
                case '[':
                    inCValueType = true
                    i++
                    break
                default:
                    if (/\s/.test(input[i])) {
                        i++
                        break
                    }

                    var match = propIdentRegex.exec(input.slice(i))[0]

                    if (match != null) {
                        tokens.push(new Tuple('prop_ident', match))
                        i += match.length
                    } else {
                        return []
                    }

                    break
            }
        } else {
            if (!inBackslash) {
                switch (input[i]) {
                    case '\\':
                        inBackslash = true
                        break
                    case '\n':
                        builder += '\n'
                        break
                    case ']':
                        inCValueType = false
                        tokens.push(new Tuple('c_value_type', builder))
                        break
                    default:
                        builder += input[i]
                        break
                }
            } else {
                inBackslash = false
                builder += input[i]
            }

            i++
        }
    }

    return tokens
}

exports.parse = function(tokens, start, end) {
    if (arguments.length <= 2) end = tokens.length - 1
    if (arguments.length <= 1) start = 0

    var i = start

    var tree = { nodes: [], subtrees: [], parent: null, current: null }
    var node, property

    while (i <= end) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) break
        if (new Tuple('parenthesis', ')').equals(tokens[i])) return null

        tokens[i].unpack(function(type, data) {
            if (type == 'semicolon') {
                node = {}
                tree.nodes.push(node)
            } else if (type == 'prop_ident') {
                node[data] = []
                property = node[data]
            } else if (type == 'c_value_type') {
                property.push(data)
            }
        })

        i++
    }

    var depth = 0
    var newstart = 0

    while (i <= end) {
        if (new Tuple('parenthesis', '(').equals(tokens[i])) {
            depth++
            if (depth == 1) newstart = i + 1
        } else if (new Tuple('parenthesis', ')').equals(tokens[i])) {
            depth--
            if (depth == 0) {
                t = exports.parse(tokens, newstart, i - 1)
                t.parent = tree
                tree.subtrees.push(t)
                tree.current = 0
            }
        }

        i++
    }

    return tree
}

exports.parseFile = function(filename) {
    var input = fs.readFileSync(filename, { encoding: 'utf8' })
    var tokens = exports.tokenize(input)

    return exports.parse(tokens)
}

exports.point2vertex = function(point) {
    if (point.length != 2) return new Tuple(-1, -1)

    point = point.toLowerCase()
    return new Tuple(alpha.indexOf(point[0]), alpha.indexOf(point[1]))
}

exports.vertex2point = function(tuple) {
    return tuple.unpack(function(x, y) {
        if (x < 0 || y < 0) return ''
        return alpha[x] + alpha[y]
    })
}

exports.compressed2list = function(compressed) {
    var colon = compressed.indexOf(':')
    if (colon < 0) return [exports.point2vertex(compressed)]

    var v1 = exports.point2vertex(compressed.slice(0, colon))
    var v2 = exports.point2vertex(compressed.slice(colon + 1))
    var list = []

    for (var i = v1[0]; i <= v2[0]; i++) {
        for (var j = v1[1]; j <= v2[1]; j++) {
            list.push(new Tuple(i, j))
        }
    }

    return list
}

exports.navigate = function(tree, index, step) {
    if (index + step >= 0 && index + step < tree.nodes.length) {
        return new Tuple(tree, index + step)
    } else if (index + step < 0 && tree.parent) {
        var prev = tree.parent
        index = prev.nodes.length + index + step

        if (index >= 0)
            return new Tuple(prev, index)
        else if (prev.parent)
            return new Tuple(prev.parent, prev.parent.nodes.length - 1)
    } else if (index + step >= tree.nodes.length && tree.current != null) {
        index = index + step - tree.nodes.length
        var next = tree.subtrees[tree.current]

        if (index < next.nodes.length)
            return new Tuple(next, index)
        else if (next.current != null)
            return new Tuple(next.subtrees[next.current], 0)
    }

    return new Tuple(null, 0)
}

exports.addBoard = function(tree, index, baseboard) {
    if (index >= tree.nodes.length) return tree

    var node = tree.nodes[index]
    var vertex = null

    if (arguments.length <= 2) {
        if (index != 0) {
            baseboard = tree.nodes[index - 1].board
        } else {
            if (tree.parent == null || tree.parent.nodes.length == 0) {
                var size = 'SZ' in node ? node.SZ[0].toInt() : 19
                baseboard = new Board(size)
            } else {
                baseboard = tree.parent.nodes.getLast().board
            }
        }
    }

    if ('B' in node) {
        vertex = exports.point2vertex(node.B[0])
        baseboard = baseboard.makeMove(1, vertex)
    } else if ('W' in node) {
        vertex = exports.point2vertex(node.W[0])
        baseboard = baseboard.makeMove(-1, vertex)
    } else {
        baseboard = baseboard.makeMove(0)
    }

    var ids = ['AW', 'AE', 'AB']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                baseboard.arrangement[exports.point2vertex(value)] = i - 1
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    baseboard.arrangement[vertex] = i - 1
                })
            }
        })
    }

    if (vertex != null) {
        baseboard.overlays[vertex] = new Tuple('point', 0, '')
    }

    var ids = ['CR', 'MA', 'SQ', 'TR']
    var classes = ['circle', 'cross', 'square', 'triangle']

    for (var i = 0; i < ids.length; i++) {
        if (!(ids[i] in node)) continue

        node[ids[i]].each(function(value) {
            if (value.indexOf(':') < 0) {
                // Single point
                baseboard.overlays[exports.point2vertex(value)] = new Tuple(classes[i], 0, '')
            } else {
                // Compressed point list
                exports.compressed2list(value).each(function(vertex) {
                    baseboard.overlays[vertex] = new Tuple(classes[i], 0, '')
                })
            }
        })
    }

    if ('LB' in node) {
        node.LB.each(function(composed) {
            var sep = composed.indexOf(':')
            var point = composed.slice(0, sep)
            var label = composed.slice(sep + 1).replace(/\s+/, ' ')
            baseboard.overlays[exports.point2vertex(point)] = new Tuple('label', 0, label)
        })
    }

    node.board = baseboard

    if (index == tree.nodes.length - 1 && tree.subtrees.length > 0) {
        // Add variations

        tree.subtrees.each(function(subtree) {
            if (subtree.nodes.length == 0) return

            var v, sign

            if ('B' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].B[0])
                sign = 1
            } else if ('W' in subtree.nodes[0]) {
                v = sgf.point2vertex(subtree.nodes[0].W[0])
                sign = -1
            } else {
                return
            }

            if (v in baseboard.overlays)
                baseboard.overlays[v] = baseboard.overlays[v].unpack(function(a, b, c) {
                     return new Tuple(a, sign, c)
                })
            else baseboard.overlays[v] = new Tuple('', sign, '')
        })
    }

    return tree
}

exports.addBoards = function(tree, baseboard) {
    if (tree.nodes.length == 0) return tree

    for (var j = 0; j < tree.nodes.length; j++) {
        if ('board' in tree.nodes[j]) continue
        exports.addBoard(tree, j)
    }

    if (tree.current == null) return tree

    exports.addBoards(tree.subtrees[tree.current], baseboard)
    return tree
}

exports.splitTree = function(tree, index) {
    if (index < 0 || index >= tree.nodes.length - 1) return tree

    var newnodes = tree.nodes.slice(0, index + 1)
    tree.nodes = tree.nodes.slice(index + 1)

    var newtree = { nodes: newnodes, subtrees: [tree], parent: tree.parent, current: 0 }
    tree.parent = newtree

    if (newtree.parent != null) {
        newtree.parent.subtrees[newtree.parent.subtrees.indexOf(tree)] = newtree
    }

    return newtree
}

exports.tree2string = function(tree) {
    var output = ''

    tree.nodes.each(function(node) {
        output += ';'

        Object.each(node, function(values, id) {
            if (id.toUpperCase() != id) return
            output += id

            values.each(function(value) {
                output += '[' + exports.escapeString(value.toString()) + ']'
            })
        })

        output += '\n'
    })

    if (tree.current != null)
        output += '(' + exports.tree2string(tree.subtrees[tree.current]) + ')'

    for (var i = 0; i < tree.subtrees.length; i++) {
        if (i == tree.current) continue
        output += '(' + exports.tree2string(tree.subtrees[i]) + ')'
    }

    return output
}

exports.escapeString = function(input) {
    return input.replace('\\', '\\\\').replace(']', '\\]')
}

exports.getDepth = function(tree) {
    var depth = 0

    tree.subtrees.each(function(subtree) {
        depth = Math.max(exports.getDepth(subtree), depth)
    })

    return depth + tree.nodes.length
}

exports.getSections = function(tree, n) {
    if (n < tree.nodes.length) return [new Tuple(tree, n)]

    var sections = []

    tree.subtrees.each(function(subtree) {
        sections.combine(exports.getSections(subtree, n - tree.nodes.length))
    })

    return sections
}

exports.tree2matrix = function(tree, matrix, xshift, yshift) {
    if (!matrix) matrix = Array.apply(null, new Array(exports.getDepth(tree))).map(function() { return [] });
    if (!xshift) xshift = 0
    if (!yshift) yshift = 0
    if (!('id' in tree)) tree.id = uuid.v4()

    var hasCollisions = true
    while (hasCollisions) {
        hasCollisions = false

        for (var y = 0; y < Math.min(tree.nodes.length + 1, matrix.length - yshift); y++) {
            if (xshift < matrix[yshift + y].length) {
                hasCollisions = true
                xshift++
                break
            }
        }
    }

    for (var y = 0; y < tree.nodes.length; y++) {
        while (xshift >= matrix[yshift + y].length) {
            matrix[yshift + y].push(null)
        }

        matrix[yshift + y][xshift] = new Tuple(tree, y)
    }

    for (var k = 0; k < tree.subtrees.length; k++) {
        var subtree = tree.subtrees[k]
        exports.tree2matrix(subtree, matrix, xshift, yshift + tree.nodes.length)
    }

    return matrix
}

exports.tree2graph = function(tree) {
    var matrix = exports.tree2matrix(tree)
    var graph = { nodes: [], edges: [] }
    var width = Math.max.apply(null, matrix.map(function(x) { return x.length }))

    for (x = 0; x < width; x++) {
        for (y = 0; y < matrix.length; y++) {
            if (!matrix[y][x]) continue

            var tree = matrix[y][x][0]
            var index = matrix[y][x][1]
            var id = tree.id + '-' + index

            graph.nodes.push({
                'id': id,
                'x': x * 30,
                'y': y * 30,
                'size': 4
            })

            var prev = exports.navigate(tree, index, -1)
            if (!prev[0]) continue

            graph.edges.push({
                'id': id + '-e',
                'source': id,
                'target': prev[0].id + '-' + prev[1]
            })
        }
    }

    return graph
}
<|endoftext|>


<commit_before>/*
    The SerGIS Project - sergis-server

    Copyright (c) 2015, SerGIS Project Contributors. All rights reserved.
    Use of this source code is governed by the MIT License, which can be found
    in the LICENSE.txt file.
 */

// node modules
var fs = require("fs"),
    path = require("path");

// required modules
var mongoose = require("mongoose");

// our modules
var config = require("../config");

/** Functions to call when the database is loaded */
var loadHandlers = [];

/** Models */
var models = exports.models = {};

/** Models directory */
var modelsDir = path.join(__dirname, "models");

// Connect to the MongoDB server
mongoose.connect(config.MONGO_SERVER, {
    server: {
        keepAlive: 1,
        auto_reconnect: true
    }
});

var db = mongoose.connection;

db.on("error", function (err) {
    console.error("Error connecting to MongoDB at " + config.MONGO_SERVER + ": ", err.stack);
    // Just quit...
    process.exit();
});

db.once("open", function () {
    console.log("Opened MongoDB connection.");

    // Load models from `models` directory
    fs.readdir(modelsDir, function (err, files) {
        if (err) return console.error(err, "reading files from models directory at " + modelsDir);

        // Load all the models
        try {
            for (var i = 0; i < files.length; i++) {
                if (files[i].substr(-3) == ".js") {
                    // "require" this file
                    models[files[i].substring(0, files[i].length - 3)] = require(path.join(modelsDir, files[i]))(mongoose);
                }
            }
        } catch (err) {
            console.error("Error loading model: ", err.stack);

            // Make sure the MongoDB connection is closed
            exports.close().then(function () {
                process.exit();
            }, function (err) {
                console.error("Error closing database: ", err.stack);
                process.exit();
            });

            // And, we're done here
            return;
        }

        // Run load handlers
        for (var i = 0; i < loadHandlers.length; i++) {
            loadHandlers[i]();
        }
        // No more load handlers allowed
        loadHandlers = null;
    });
});

/**
 * Get the current Mongoose connection.
 */
exports.getMongooseConnection = function () {
    return db;
};

/**
 * Add a function to be run once the database is connected (or right away if the
 * database is already connected).
 *
 * @param {Function} callback - The function to call.
 */
exports.addLoadHandler = function (callback) {
    if (Array.isArray(loadHandlers)) {
        // Database connection isn't loaded yet
        loadHandlers.push(callback);
    } else {
        // Loading is already done
        callback();
    }
};

/**
 * Close the MongoDB database connection if opened.
 */
exports.close = function () {
    if (mongoose.connection) {
        console.log("Closing MongoDB connection...");
        mongoose.connection.close(function () {
            // There's a strong chance we'll never get here, because the server
            // will end before it's fully closed.
            console.log("Closed MongoDB connection.");
        });
    }
};

/**
 * To get a session object, in case it's ever needed.
 *
 * @param {string} sessionID - The session ID.
 *
 * @return {Promise.<?object>} The session object, if any.
 */
exports.getSessionByID = function (sessionID) {
    return new Promise(function (resolve, reject) {
        db.collection("sessions").findOne({_id: sessionID}, function (err, session) {
            if (err) {
                return reject(err);
            }

            var sessionObject;
            try {
                sessionObject = JSON.parse(session.session);
            } catch (err) {}
            resolve(sessionObject || undefined);
        });
    });
};

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

exports.migrateFromPlainMongo = function () {
    var organizationIdsByName = {};
    var userIdsByName = {};
    var gameIdsByName = {};

    return Promise.resolve().then(function () {
        return new Promise(function (resolve, reject) {
            //
            // Migrate sergis-author-games
            //
            db.collection("sergis-author-games").find({}).toArray(function (err, items) {
                if (err) return reject(err);

                var promises = [];
                items.forEach(function (item) {
                    var newItem = new models.AuthorGame({
                        name: item.gameName,
                        name_lowercase: item.gameName.toLowerCase(),
                        owner: userIdsByName[item.gameOwner],
                        jsondata: item.jsondata
                    });
                    promises.push(newItem.save());
                });
                resolve(Promise.all(promises));
            });
        });
    }).then(function () {
        return {
            organizations: organizationIdsByName,
            users: userIdsByName,
            games: gameIdsByName
        };
    });
};
<commit_msg>migration... ==(<commit_after>/*
    The SerGIS Project - sergis-server

    Copyright (c) 2015, SerGIS Project Contributors. All rights reserved.
    Use of this source code is governed by the MIT License, which can be found
    in the LICENSE.txt file.
 */

// node modules
var fs = require("fs"),
    path = require("path");

// required modules
var mongoose = require("mongoose");

// our modules
var config = require("../config");

/** Functions to call when the database is loaded */
var loadHandlers = [];

/** Models */
var models = exports.models = {};

/** Models directory */
var modelsDir = path.join(__dirname, "models");

// Connect to the MongoDB server
mongoose.connect(config.MONGO_SERVER, {
    server: {
        keepAlive: 1,
        auto_reconnect: true
    }
});

var db = mongoose.connection;

db.on("error", function (err) {
    console.error("Error connecting to MongoDB at " + config.MONGO_SERVER + ": ", err.stack);
    // Just quit...
    process.exit();
});

db.once("open", function () {
    console.log("Opened MongoDB connection.");

    // Load models from `models` directory
    fs.readdir(modelsDir, function (err, files) {
        if (err) return console.error(err, "reading files from models directory at " + modelsDir);

        // Load all the models
        try {
            for (var i = 0; i < files.length; i++) {
                if (files[i].substr(-3) == ".js") {
                    // "require" this file
                    models[files[i].substring(0, files[i].length - 3)] = require(path.join(modelsDir, files[i]))(mongoose);
                }
            }
        } catch (err) {
            console.error("Error loading model: ", err.stack);

            // Make sure the MongoDB connection is closed
            exports.close().then(function () {
                process.exit();
            }, function (err) {
                console.error("Error closing database: ", err.stack);
                process.exit();
            });

            // And, we're done here
            return;
        }

        // Run load handlers
        for (var i = 0; i < loadHandlers.length; i++) {
            loadHandlers[i]();
        }
        // No more load handlers allowed
        loadHandlers = null;
    });
});

/**
 * Get the current Mongoose connection.
 */
exports.getMongooseConnection = function () {
    return db;
};

/**
 * Add a function to be run once the database is connected (or right away if the
 * database is already connected).
 *
 * @param {Function} callback - The function to call.
 */
exports.addLoadHandler = function (callback) {
    if (Array.isArray(loadHandlers)) {
        // Database connection isn't loaded yet
        loadHandlers.push(callback);
    } else {
        // Loading is already done
        callback();
    }
};

/**
 * Close the MongoDB database connection if opened.
 */
exports.close = function () {
    if (mongoose.connection) {
        console.log("Closing MongoDB connection...");
        mongoose.connection.close(function () {
            // There's a strong chance we'll never get here, because the server
            // will end before it's fully closed.
            console.log("Closed MongoDB connection.");
        });
    }
};

/**
 * To get a session object, in case it's ever needed.
 *
 * @param {string} sessionID - The session ID.
 *
 * @return {Promise.<?object>} The session object, if any.
 */
exports.getSessionByID = function (sessionID) {
    return new Promise(function (resolve, reject) {
        db.collection("sessions").findOne({_id: sessionID}, function (err, session) {
            if (err) {
                return reject(err);
            }

            var sessionObject;
            try {
                sessionObject = JSON.parse(session.session);
            } catch (err) {}
            resolve(sessionObject || undefined);
        });
    });
};

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

exports.migrateFromPlainMongo = function () {
    var organizationIdsByName = {};
    var userIdsByName = {};

    return Promise.resolve(models.User.find({}).exec()).then(function (users) {
        users.forEach(function (u) {
            userIdsByName[u._id] = u.username_lowercase;
        });
    }).then(function () {
        return new Promise(function (resolve, reject) {
            console.log("DROPPING COLLECTION authorgames: " + db.collection("authorgames").drop());
            //
            // Migrate sergis-author-games
            //
            db.collection("sergis-author-games").find({}).toArray(function (err, items) {
                if (err) return reject(err);

                var promises = [];
                items.forEach(function (item) {
                    var newItem = new models.AuthorGame({
                        name: item.gameName,
                        name_lowercase: item.gameName.toLowerCase(),
                        owner: userIdsByName[item.gameOwner.toLowerCase()],
                        jsondata: item.jsondata
                    });
                    promises.push(newItem.save());
                });
                resolve(Promise.all(promises));
            });
        });
    }).then(function () {
        return "done";
    });
};
<|endoftext|>


<commit_before>(function (require, module) {
  'use strict';

  var Project = require('../schemas/project.js');
  var User = require('../schemas/user.js');
  var moment = require('moment');
  var passport = require('passport');
  var testData = require('../tests/testData.js');
  var config = require('../include/config.js');
  var _ = require('lodash');
  var utils = require('../include/utils.js');

  // Public functions
  module.exports = {

    //
    // Parameters
    //

    pProjectId: function (req, res, next, projectId) {
      var loggedInUser = req.user;

      if (!loggedInUser) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      if (!utils.exists(projectId)) {
        return next();
      }

      // Check for valid object ID
      // Thanks: http://stackoverflow.com/a/14942113/1153203
      if (!projectId.match(/^[0-9a-fA-F]{24}$/)) {
        return next();
      }

      var findProject = Project.findOne({
        '_id': projectId
      });

      findProject.execQ()
        .then(function (project) {
          req.project = project;
          if (project && !(_.contains(project.admins, loggedInUser._id) ||
            _.contains(project.users, loggedInUser._id))) {
            // Need to be a user of the project to see it
            res.send(401, {
              detail: 'You do not have permission to access ' +
                'this project.'
            });
          } else {
            next();
          }
        })
        .catch(function (err) {
          next(err);
        })
        .done();

    },

    //
    // API Methods
    //

    listProjects: function (req, res, next) {
      var minRecord = req.query.minRecord;
      var maxRecord = req.query.maxRecord;

      if (!req.user) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      var getProjects = Project.find({
        $or: [{
          'admins': req.user._id
        }, {
          'users': req.user._id
        }]
      });

      // Project correct fields
      getProjects.select('_id name version admins users description');

      getProjects.sort({
        'name': 'asc'
      });

      var maxRecordCount = config.max_records_per_query;

      // Pagination
      if (minRecord) {
        getProjects.skip(minRecord);
      }
      if (minRecord && maxRecord) {
        if (maxRecord < minRecord) {
          return res.send(400, {
            detail: 'Invalid pagination range'
          });
        }

        maxRecordCount = Math.min(
          maxRecordCount, (maxRecord - minRecord) + 1
        );
      }

      getProjects.limit(maxRecordCount);

      getProjects.execQ()
        .then(function (projects) {
          res.send(200, projects);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    addProject: function (req, res, next) {
      if (!req.user) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      var newProject = new Project({
        name: req.body.name,
        description: req.body.description,
        admins: [req.user._id]
      });

      newProject.saveQ()
        .then(function (data) {
          res.send(201, {
            '_id': data._id
          });
        })
        .catch(function (err) {
          // Duplicate check
          if (err.code === 11000) {
            res.send(409, {
              'detail': 'A project with this name already exists.'
            });
          } else {
            next(err);
          }
        })
        .done();
    },

    getProject: function (req, res, next) {
      var project = req.project;
      if (project) {
        return res.send({
          _id: project._id,
          name: project.name,
          version: project.version,
          admins: project.admins,
          users: project.users,
          description: project.description
        });
      } else {
        return res.send(404, {
          detail: 'Project doesn\'t exist'
        });
      }
    },

    addProjectUser: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (_.contains(project.admins, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user is already an admin of this project.'
        });
      }

      if (_.contains(project.users, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user already added to project users'
        });
      }

      // Add user to users array
      project.users.push(selectedUser._id);

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProjectUser: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (!_.contains(req.project.users, selectedUser._id)) {
        return res.send(404, {
          detail: 'User is not a member of this project'
        });
      }

      // Remove user from users array
      _.pull(project.users, selectedUser._id);
      project.markModified('users');

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    addProjectAdmin: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project admin list'
        });
      }

      if (_.contains(project.users, selectedUser._id)) {
        // Promote user to admin
        _.pull(project.users, selectedUser._id);
        project.markModified('users');
      }

      if (_.contains(project.admins, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user already added to project admins'
        });
      }

      // Add user to admins array
      project.admins.push(selectedUser._id);

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProjectAdmin: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (project.admins.length === 1) {
        return res.send(401, {
          detail: 'A project must have at least one admin'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (!_.contains(req.project.admins, selectedUser._id)) {
        return res.send(404, {
          detail: 'User is not an admin of this project'
        });
      }

      // Remove user from admins array
      _.pull(project.admins, selectedUser._id);
      project.markModified('admins');

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProject: function (req, res, next) {
      var project = req.project;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can delete a project'
        });
      }

      project.removeQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    //
    // Test extensions
    //

    resetTests: function (req, res, next) {
      var testProjects = testData.getTestProjects(),
        projects = [],
        i;

      for (i = 0; i < testProjects.length; i++) {
        projects.push(testProjects[i].name);
      }

      var query = Project.find({
        $or: [{
          'name': {
            $in: projects
          }
        }, {
          'name': /TEST_DATA_DELETE*/
        }]
      });
      query.exec(function (err, projects) {
        if (err) {
          return next(err);
        }
        for (i = 0; i < projects.length; i++) {
          projects[i].remove();
        }
        return res.send(200);
      });
    }

  };

})(require, module);
<commit_msg>Swapped the order of validation checks in remove project admin to make testing easier.<commit_after>(function (require, module) {
  'use strict';

  var Project = require('../schemas/project.js');
  var User = require('../schemas/user.js');
  var moment = require('moment');
  var passport = require('passport');
  var testData = require('../tests/testData.js');
  var config = require('../include/config.js');
  var _ = require('lodash');
  var utils = require('../include/utils.js');

  // Public functions
  module.exports = {

    //
    // Parameters
    //

    pProjectId: function (req, res, next, projectId) {
      var loggedInUser = req.user;

      if (!loggedInUser) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      if (!utils.exists(projectId)) {
        return next();
      }

      // Check for valid object ID
      // Thanks: http://stackoverflow.com/a/14942113/1153203
      if (!projectId.match(/^[0-9a-fA-F]{24}$/)) {
        return next();
      }

      var findProject = Project.findOne({
        '_id': projectId
      });

      findProject.execQ()
        .then(function (project) {
          req.project = project;
          if (project && !(_.contains(project.admins, loggedInUser._id) ||
            _.contains(project.users, loggedInUser._id))) {
            // Need to be a user of the project to see it
            res.send(401, {
              detail: 'You do not have permission to access ' +
                'this project.'
            });
          } else {
            next();
          }
        })
        .catch(function (err) {
          next(err);
        })
        .done();

    },

    //
    // API Methods
    //

    listProjects: function (req, res, next) {
      var minRecord = req.query.minRecord;
      var maxRecord = req.query.maxRecord;

      if (!req.user) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      var getProjects = Project.find({
        $or: [{
          'admins': req.user._id
        }, {
          'users': req.user._id
        }]
      });

      // Project correct fields
      getProjects.select('_id name version admins users description');

      getProjects.sort({
        'name': 'asc'
      });

      var maxRecordCount = config.max_records_per_query;

      // Pagination
      if (minRecord) {
        getProjects.skip(minRecord);
      }
      if (minRecord && maxRecord) {
        if (maxRecord < minRecord) {
          return res.send(400, {
            detail: 'Invalid pagination range'
          });
        }

        maxRecordCount = Math.min(
          maxRecordCount, (maxRecord - minRecord) + 1
        );
      }

      getProjects.limit(maxRecordCount);

      getProjects.execQ()
        .then(function (projects) {
          res.send(200, projects);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    addProject: function (req, res, next) {
      if (!req.user) {
        return res.send(401, {
          detail: 'Not logged in'
        });
      }

      var newProject = new Project({
        name: req.body.name,
        description: req.body.description,
        admins: [req.user._id]
      });

      newProject.saveQ()
        .then(function (data) {
          res.send(201, {
            '_id': data._id
          });
        })
        .catch(function (err) {
          // Duplicate check
          if (err.code === 11000) {
            res.send(409, {
              'detail': 'A project with this name already exists.'
            });
          } else {
            next(err);
          }
        })
        .done();
    },

    getProject: function (req, res, next) {
      var project = req.project;
      if (project) {
        return res.send({
          _id: project._id,
          name: project.name,
          version: project.version,
          admins: project.admins,
          users: project.users,
          description: project.description
        });
      } else {
        return res.send(404, {
          detail: 'Project doesn\'t exist'
        });
      }
    },

    addProjectUser: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (_.contains(project.admins, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user is already an admin of this project.'
        });
      }

      if (_.contains(project.users, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user already added to project users'
        });
      }

      // Add user to users array
      project.users.push(selectedUser._id);

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProjectUser: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (!_.contains(req.project.users, selectedUser._id)) {
        return res.send(404, {
          detail: 'User is not a member of this project'
        });
      }

      // Remove user from users array
      _.pull(project.users, selectedUser._id);
      project.markModified('users');

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    addProjectAdmin: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project admin list'
        });
      }

      if (_.contains(project.users, selectedUser._id)) {
        // Promote user to admin
        _.pull(project.users, selectedUser._id);
        project.markModified('users');
      }

      if (_.contains(project.admins, selectedUser._id)) {
        return res.send(409, {
          detail: 'Specified user already added to project admins'
        });
      }

      // Add user to admins array
      project.admins.push(selectedUser._id);

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProjectAdmin: function (req, res, next) {
      var project = req.project;
      var selectedUser = req.selectedUser;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!selectedUser) {
        return res.send(404, {
          detail: 'Specified user doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can change a project user list'
        });
      }

      if (!_.contains(req.project.admins, selectedUser._id)) {
        return res.send(404, {
          detail: 'User is not an admin of this project'
        });
      }

      if (project.admins.length === 1) {
        return res.send(401, {
          detail: 'A project must have at least one admin'
        });
      }

      // Remove user from admins array
      _.pull(project.admins, selectedUser._id);
      project.markModified('admins');

      project.saveQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    removeProject: function (req, res, next) {
      var project = req.project;
      var loggedInUser = req.user;

      if (!project) {
        return res.send(404, {
          detail: 'Specified project doesn\'t exist'
        });
      }

      if (!_.contains(project.admins, loggedInUser._id)) {
        return res.send(401, {
          detail: 'Only admins can delete a project'
        });
      }

      project.removeQ()
        .then(function () {
          res.send(200);
        })
        .catch(function (err) {
          next(err);
        })
        .done();
    },

    //
    // Test extensions
    //

    resetTests: function (req, res, next) {
      var testProjects = testData.getTestProjects(),
        projects = [],
        i;

      for (i = 0; i < testProjects.length; i++) {
        projects.push(testProjects[i].name);
      }

      var query = Project.find({
        $or: [{
          'name': {
            $in: projects
          }
        }, {
          'name': /TEST_DATA_DELETE*/
        }]
      });
      query.exec(function (err, projects) {
        if (err) {
          return next(err);
        }
        for (i = 0; i < projects.length; i++) {
          projects[i].remove();
        }
        return res.send(200);
      });
    }

  };

})(require, module);
<|endoftext|>