<commit_before>/* jslint node:true */

'use strict';

exports = module.exports = {
    getAll: getAll,
    getAllPublic: getAllPublic,
    getAllLean: getAllLean,
    get: get,
    getPublic: getPublic,
    add: add,
    put: put,
    del: del,
    exp: exp,
    imp: imp,
    extractURLs: extractURLs,
    extractTags: extractTags,
    facelift: facelift,
    cleanupTags: cleanupTags,
    importThings: importThings,

    // TODO remove eventually
    hasOldData: null,
    expOldData: expOldData,
    cleanupOldData: cleanupOldData,

    TYPE_IMAGE: 'image',
    TYPE_UNKNOWN: 'unknown'
};

var assert = require('assert'),
    async = require('async'),
    config = require('./config.js'),
    debug = require('debug')('logic'),
    path = require('path'),
    fs = require('fs'),
    mkdirp = require('mkdirp'),
    url = require('url'),
    tags = require('./database/tags.js'),
    tar = require('tar-fs'),
    things = require('./database/things.js'),
    rimraf = require('rimraf'),
    safe = require('safetydance'),
    superagent = require('superagent');

var PRETTY_URL_LENGTH = 40;

var md = require('markdown-it')({
    breaks: true,
    html: true,
    linkify: true
});

function extractURLs(content) {
    var urls = [];

    // extract links, use markdown-it to avoid collecting code block links
    md.renderer.rules.link_open = function (tokens, idx) {
        // skip links which are already markdown
        if (tokens[idx].markup !== 'linkify') return '';

        var href = tokens[idx].attrs[tokens[idx].attrIndex('href')][1];

        if (href) urls.push(href);

        return '';
    };

    md.render(content);

    // remove duplicates
    return urls.filter(function (item, pos, self) {
        return self.indexOf(item) === pos;
    });
}

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

function extractTags(content) {
    var tagObjects = [];

    // first replace all urls which might contain # with placeholders
    var urls = extractURLs(content);
    urls.forEach(function (u) {
        content = content.replace(new RegExp(escapeRegExp(u), 'gmi'), ' --URL_PLACEHOLDER-- ');
    });

    var md = require('markdown-it')()
        .use(require('markdown-it-hashtag'),{
            hashtagRegExp: '[\u00C0-\u017Fa-zA-Z0-9]+',
            preceding: ''
        });

    md.renderer.rules.hashtag_open  = function(tokens, idx) {
        var tagName = tokens[idx].content.toLowerCase();
        tagObjects.push(tagName);
        return '';
    };

    md.render(content);

    return tagObjects;
}

function extractExternalContent(content, callback) {
    var urls = extractURLs(content);
    var externalContent = [];

    async.each(urls, function (url, callback) {
        superagent.head(url).timeout(20000).end(function (error, result) {
            var obj = { url: url, type: exports.TYPE_UNKNOWN };

            if (error) {
                debug('failed to fetch external content %s', url);
            } else {
                if (result.type.indexOf('image/') === 0) {
                    obj = { url: url, type: exports.TYPE_IMAGE };
                }

                debug('external content type %s - %s', obj.type, obj.url);
            }

            externalContent.push(obj);

            callback(null);
        });
    }, function () {
        callback(null, externalContent);
    });
}

function facelift(userId, thing, callback) {
    var data = thing.content;
    var tagObjects = thing.tags;
    var externalContent = thing.externalContent;
    var attachments = thing.attachments || [];

    function wrapper() {

        // Enrich with tag links
        tagObjects.forEach(function (tag) {
            data = data.replace(new RegExp('#' + tag + '(#|\\s|$)', 'gmi'), '[#' + tag + '](#search?#' + tag + ')$1').trim();
        });

        // Enrich with image links
        externalContent.forEach(function (obj) {
            if (obj.type === exports.TYPE_IMAGE) {
                data = data.replace(new RegExp(escapeRegExp(obj.url), 'gmi'), '![' + obj.url + '](' + obj.url + ')');
            } else {
                // make urls look prettier
                var tmp = url.parse(obj.url);

                var pretty = obj.url;
                if (tmp.protocol) {
                    pretty = obj.url.slice(tmp.protocol.length + 2);
                    if (pretty.length > PRETTY_URL_LENGTH) pretty = pretty.slice(0, PRETTY_URL_LENGTH) + '...';
                }

                data = data.replace(new RegExp(escapeRegExp(obj.url), 'gmi'), '[' + pretty + '](' + obj.url + ')');
            }
        });

        // Enrich with attachments
        attachments.forEach(function (a) {
            if (a.type === exports.TYPE_IMAGE) {
                data = data.replace(new RegExp('\\[' + a.fileName + '\\]', 'gmi'), '![/api/files/' + userId + '/' + a.identifier + '](/api/files/' + userId + '/' + a.identifier + ')');
            } else {
                data = data.replace(new RegExp('\\[' + a.fileName + '\\]', 'gmi'), '[/api/files/' + userId + '/' + a.identifier + '](/api/files/' + userId + '/' + a.identifier + ')');
            }
        });

        callback(null, data);
    }

    if (Array.isArray(externalContent)) return wrapper();

    // old entry extract external content first
    extractExternalContent(thing.content, function (error, result) {
        if (error) {
            console.error('Failed to extract external content:', error);

            externalContent = [];

            return wrapper();
        }

        // set for wrapper()
        externalContent = result;

        debug('update %s with new external content.', thing._id, result);

        things.put(userId, thing._id, thing.content, thing.tags, attachments, result, false, false, false, function (error) {
            if (error) console.error('Failed to update external content:', error);

            wrapper();
        });
    });
}

function getAll(userId, query, skip, limit, callback) {
    things.getAll(userId, query, skip, limit, function (error, result) {
        if (error) return callback(error);
        if (!result) return callback(null, []);

        async.each(result, function (thing, callback) {
            facelift(userId, thing, function (error, data) {
                if (error) console.error('Failed to facelift:', error);

                thing.attachments = thing.attachments || [];
                thing.richContent = data || thing.content;

                callback(null);
            });
        }, function () {
            callback(null, result);
        });
    });
}

function getAllLean(userId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof callback, 'function');

    things.getAllLean(userId, callback);
}

function get(userId, thingId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof callback, 'function');

    things.get(userId, thingId, function (error, result) {
        if (error) return callback(error);

        facelift(userId, result, function (error, data) {
            if (error) console.error('Failed to facelift:', error);

            result.attachments = result.attachments || [];
            result.richContent = data || result.content;

            callback(null, result);
        });
    });
}

function getAllPublic(userId, query, skip, limit, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof query, 'object');
    assert.strictEqual(typeof skip, 'number');
    assert.strictEqual(typeof limit, 'number');
    assert.strictEqual(typeof callback, 'function');

    query.public = true;

    things.getAll(userId, query, skip, limit, function (error, result) {
        if (error) return callback(error);
        if (!result) return callback(null, []);

        async.each(result, function (thing, callback) {
            facelift(userId, thing, function (error, data) {
                if (error) console.error('Failed to facelift:', error);

                thing.attachments = thing.attachments || [];
                thing.richContent = data || thing.content;

                callback(null);
            });
        }, function () {
            callback(null, result);
        });
    });
}

function getPublic(userId, thingId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof callback, 'function');

    get(userId, thingId, function (error, result) {
        if (error) return callback(error);

        if (!result.public && !result.shared) return callback('not allowed');

        callback(null, result);
    });
}

function add(userId, content, attachments, callback) {
    extractExternalContent(content, function (error, result) {
        if (error) return callback(error);

        var doc = {
            content: content,
            createdAt: Date.now(),
            modifiedAt: Date.now(),
            tags: extractTags(content),
            externalContent: result,
            attachments: attachments
        };

        async.eachSeries(doc.tags, tags.update.bind(null, userId), function (error) {
            if (error) return callback(error);

            things.add(userId, doc.content, doc.tags, doc.attachments, doc.externalContent, function (error, result) {
                if (error) return callback(error);
                if (!result) return callback(new Error('no result returned'));

                get(userId, result._id, callback);
            });
        });
    });
}

function put(userId, thingId, content, attachments, isPublic, isShared, isArchived, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof content, 'string');
    assert(Array.isArray(attachments));
    assert.strictEqual(typeof isPublic, 'boolean');
    assert.strictEqual(typeof isShared, 'boolean');
    assert.strictEqual(typeof isArchived, 'boolean');
    assert.strictEqual(typeof callback, 'function');

    var tagObjects = extractTags(content);

    async.eachSeries(tagObjects, tags.update.bind(null, userId), function (error) {
        if (error) return callback(error);

        extractExternalContent(content, function (error, externalContent) {
            if (error) console.error('Failed to extract external content:', error);

            things.put(userId, thingId, content, tagObjects, attachments, externalContent, isPublic, isShared, isArchived, function (error) {
                if (error) return callback(error);

                get(userId, thingId, callback);
            });
        });
    });
}

function del(userId, id, callback) {
    things.del(userId, id, function (error) {
        if (error) return callback(error);
        callback(null);
    });
}

function exp(userId, callback) {
    things.getAllLean(userId, function (error, result) {
        if (error) return callback(error);
        if (!result) return (null, '');

        var out = result.map(function (thing) {
            return {
                createdAt: thing.createdAt,
                modifiedAt: thing.modifiedAt,
                content: thing.content,
                externalContent: thing.externalContent || [],
                attachments: thing.attachments || []
            };
        });

        callback(null, { things: out });
    });
}

function imp(userId, data, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof data, 'object');
    assert.strictEqual(typeof callback, 'function');

    async.eachSeries(data.things, function (thing, next) {
        var tagObjects = extractTags(thing.content);

        async.eachSeries(tagObjects, tags.update.bind(null, userId), function (error) {
            if (error) return next(error);

            // older exports use strings here
            if (typeof thing.createdAt === 'string') thing.createdAt = (new Date(thing.createdAt)).getTime();
            if (typeof thing.modifiedAt === 'string') thing.modifiedAt = (new Date(thing.modifiedAt)).getTime();

            things.addFull(userId, thing.content, tagObjects, thing.attachments || [], thing.externalContent || [], thing.createdAt, thing.modifiedAt || thing.createdAt, function (error, result) {
                if (error) return next(error);
                if (!result) return next(new Error('no result returned'));

                next(null, result._id);
            });
        });
    }, callback);
}

function cleanupTags() {
    var userIds = things.getAllActiveUserIds();

    async.each(userIds, function (userId, callback) {
        things.getAllLean(userId, function (error, result) {
            if (error) return console.error(new Error(error));

            var activeTags = [];
            result.forEach(function (thing) {
                activeTags = activeTags.concat(extractTags(thing.content));
            });

            tags.get(userId, function (error, result) {
                if (error) return console.error(new Error(error));

                async.each(result, function (tag, callback) {
                    if (activeTags.indexOf(tag.name) !== -1) return callback(null);

                    debug('Cleanup tag', tag.name);

                    tags.del(userId, String(tag._id), callback);
                }, callback);
            });
        });
    }, function (error) {
        if (error) console.error('Cleanup tags failed:', error);
    });
}

function importThings(userId, filePath, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof filePath, 'string');
    assert.strictEqual(typeof callback, 'function');

    var attachmentFolder = path.join(config.attachmentDir, userId);
    mkdirp.sync(attachmentFolder);

    function cleanup() {
        // cleanup things.json
        safe.fs.unlinkSync(path.join(attachmentFolder, 'things.json'));

        // cleanup uploaded file
        safe.fs.unlinkSync(filePath);
    }

    var outStream = fs.createReadStream(filePath);
    var extract = tar.extract(attachmentFolder, {
        map: function (header) {
            var prefix = 'attachments/';

            if (header.name.indexOf(prefix) === 0) header.name = header.name.slice(prefix.length);

            return header;
        }
    });

    extract.on('error', function (error) {
        cleanup();

        callback(error);
    });

    outStream.on('end', function () {
        var data = safe.require(path.join(attachmentFolder, 'things.json'));

        cleanup();

        // very basic sanity check
        if (!data) return callback('content is not JSON');
        if (!Array.isArray(data.things)) return callback('content must have a "things" array');

        imp(userId, data, callback);
    });

    outStream.pipe(extract);
}

function expOldData() {
    var collection = config.db.collection('things');

    collection.find({}).toArray(function (error, result) {
        if (error) return console.error('Failed to export old data:', error);
        if (!result || result.length === 0) return;   // nothing to do

        console.log('Old data found, prepare for import');

        var tmp = result.map(function (thing) {
            return {
                createdAt: thing.createdAt,
                modifiedAt: thing.modifiedAt,
                content: thing.content,
                externalContent: thing.externalContent || [],
                attachments: thing.attachments || []
            };
        });

        var oldAttachmentFolder = '/app/data/attachments';

        // ensure the folder exists in case the user has never uploaded a file
        mkdirp.sync(oldAttachmentFolder);

        var out = tar.pack(oldAttachmentFolder, {
            map: function (header) {
                header.name = 'attachments/' + header.name;
                return header;
            }
        });

        // add the db dump
        out.entry({ name: 'things.json' }, JSON.stringify({ things: tmp }, null, 4));

        out.pipe(fs.createWriteStream('/tmp/old_data_export.tar'));

        out.on('end', function () {
            exports.hasOldData = '/tmp/old_data_export.tar';
            console.log('Old data available at %s', exports.hasOldData);
        });
    });
}

function cleanupOldData(callback) {
    var oldAttachmentFolder = '/app/data/attachments';

    // prevent from further importing
    exports.hasOldData = null;

    rimraf(oldAttachmentFolder, function (error) {
        if (error) console.error(error);

        var collections = [
            config.db.collection('things'),
            config.db.collection('publicLinks'),
            config.db.collection('tags')
        ];

        async.eachSeries(collections, function (collection, callback) {
            collection.drop(function(error) {
                if (error) console.error(error);
                callback();
            });
        }, callback);
    });
}
<commit_msg>show only the file name for non image attachments<commit_after>/* jslint node:true */

'use strict';

exports = module.exports = {
    getAll: getAll,
    getAllPublic: getAllPublic,
    getAllLean: getAllLean,
    get: get,
    getPublic: getPublic,
    add: add,
    put: put,
    del: del,
    exp: exp,
    imp: imp,
    extractURLs: extractURLs,
    extractTags: extractTags,
    facelift: facelift,
    cleanupTags: cleanupTags,
    importThings: importThings,

    // TODO remove eventually
    hasOldData: null,
    expOldData: expOldData,
    cleanupOldData: cleanupOldData,

    TYPE_IMAGE: 'image',
    TYPE_UNKNOWN: 'unknown'
};

var assert = require('assert'),
    async = require('async'),
    config = require('./config.js'),
    debug = require('debug')('logic'),
    path = require('path'),
    fs = require('fs'),
    mkdirp = require('mkdirp'),
    url = require('url'),
    tags = require('./database/tags.js'),
    tar = require('tar-fs'),
    things = require('./database/things.js'),
    rimraf = require('rimraf'),
    safe = require('safetydance'),
    superagent = require('superagent');

var PRETTY_URL_LENGTH = 40;

var md = require('markdown-it')({
    breaks: true,
    html: true,
    linkify: true
});

function extractURLs(content) {
    var urls = [];

    // extract links, use markdown-it to avoid collecting code block links
    md.renderer.rules.link_open = function (tokens, idx) {
        // skip links which are already markdown
        if (tokens[idx].markup !== 'linkify') return '';

        var href = tokens[idx].attrs[tokens[idx].attrIndex('href')][1];

        if (href) urls.push(href);

        return '';
    };

    md.render(content);

    // remove duplicates
    return urls.filter(function (item, pos, self) {
        return self.indexOf(item) === pos;
    });
}

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

function extractTags(content) {
    var tagObjects = [];

    // first replace all urls which might contain # with placeholders
    var urls = extractURLs(content);
    urls.forEach(function (u) {
        content = content.replace(new RegExp(escapeRegExp(u), 'gmi'), ' --URL_PLACEHOLDER-- ');
    });

    var md = require('markdown-it')()
        .use(require('markdown-it-hashtag'),{
            hashtagRegExp: '[\u00C0-\u017Fa-zA-Z0-9]+',
            preceding: ''
        });

    md.renderer.rules.hashtag_open  = function(tokens, idx) {
        var tagName = tokens[idx].content.toLowerCase();
        tagObjects.push(tagName);
        return '';
    };

    md.render(content);

    return tagObjects;
}

function extractExternalContent(content, callback) {
    var urls = extractURLs(content);
    var externalContent = [];

    async.each(urls, function (url, callback) {
        superagent.head(url).timeout(20000).end(function (error, result) {
            var obj = { url: url, type: exports.TYPE_UNKNOWN };

            if (error) {
                debug('failed to fetch external content %s', url);
            } else {
                if (result.type.indexOf('image/') === 0) {
                    obj = { url: url, type: exports.TYPE_IMAGE };
                }

                debug('external content type %s - %s', obj.type, obj.url);
            }

            externalContent.push(obj);

            callback(null);
        });
    }, function () {
        callback(null, externalContent);
    });
}

function facelift(userId, thing, callback) {
    var data = thing.content;
    var tagObjects = thing.tags;
    var externalContent = thing.externalContent;
    var attachments = thing.attachments || [];

    function wrapper() {

        // Enrich with tag links
        tagObjects.forEach(function (tag) {
            data = data.replace(new RegExp('#' + tag + '(#|\\s|$)', 'gmi'), '[#' + tag + '](#search?#' + tag + ')$1').trim();
        });

        // Enrich with image links
        externalContent.forEach(function (obj) {
            if (obj.type === exports.TYPE_IMAGE) {
                data = data.replace(new RegExp(escapeRegExp(obj.url), 'gmi'), '![' + obj.url + '](' + obj.url + ')');
            } else {
                // make urls look prettier
                var tmp = url.parse(obj.url);

                var pretty = obj.url;
                if (tmp.protocol) {
                    pretty = obj.url.slice(tmp.protocol.length + 2);
                    if (pretty.length > PRETTY_URL_LENGTH) pretty = pretty.slice(0, PRETTY_URL_LENGTH) + '...';
                }

                data = data.replace(new RegExp(escapeRegExp(obj.url), 'gmi'), '[' + pretty + '](' + obj.url + ')');
            }
        });

        // Enrich with attachments
        attachments.forEach(function (a) {
            if (a.type === exports.TYPE_IMAGE) {
                data = data.replace(new RegExp('\\[' + a.fileName + '\\]', 'gmi'), '![/api/files/' + userId + '/' + a.identifier + '](/api/files/' + userId + '/' + a.identifier + ')');
            } else {
                data = data.replace(new RegExp('\\[' + a.fileName + '\\]', 'gmi'), '[' + a.identifier + '](/api/files/' + userId + '/' + a.identifier + ')');
            }
        });

        callback(null, data);
    }

    if (Array.isArray(externalContent)) return wrapper();

    // old entry extract external content first
    extractExternalContent(thing.content, function (error, result) {
        if (error) {
            console.error('Failed to extract external content:', error);

            externalContent = [];

            return wrapper();
        }

        // set for wrapper()
        externalContent = result;

        debug('update %s with new external content.', thing._id, result);

        things.put(userId, thing._id, thing.content, thing.tags, attachments, result, false, false, false, function (error) {
            if (error) console.error('Failed to update external content:', error);

            wrapper();
        });
    });
}

function getAll(userId, query, skip, limit, callback) {
    things.getAll(userId, query, skip, limit, function (error, result) {
        if (error) return callback(error);
        if (!result) return callback(null, []);

        async.each(result, function (thing, callback) {
            facelift(userId, thing, function (error, data) {
                if (error) console.error('Failed to facelift:', error);

                thing.attachments = thing.attachments || [];
                thing.richContent = data || thing.content;

                callback(null);
            });
        }, function () {
            callback(null, result);
        });
    });
}

function getAllLean(userId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof callback, 'function');

    things.getAllLean(userId, callback);
}

function get(userId, thingId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof callback, 'function');

    things.get(userId, thingId, function (error, result) {
        if (error) return callback(error);

        facelift(userId, result, function (error, data) {
            if (error) console.error('Failed to facelift:', error);

            result.attachments = result.attachments || [];
            result.richContent = data || result.content;

            callback(null, result);
        });
    });
}

function getAllPublic(userId, query, skip, limit, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof query, 'object');
    assert.strictEqual(typeof skip, 'number');
    assert.strictEqual(typeof limit, 'number');
    assert.strictEqual(typeof callback, 'function');

    query.public = true;

    things.getAll(userId, query, skip, limit, function (error, result) {
        if (error) return callback(error);
        if (!result) return callback(null, []);

        async.each(result, function (thing, callback) {
            facelift(userId, thing, function (error, data) {
                if (error) console.error('Failed to facelift:', error);

                thing.attachments = thing.attachments || [];
                thing.richContent = data || thing.content;

                callback(null);
            });
        }, function () {
            callback(null, result);
        });
    });
}

function getPublic(userId, thingId, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof callback, 'function');

    get(userId, thingId, function (error, result) {
        if (error) return callback(error);

        if (!result.public && !result.shared) return callback('not allowed');

        callback(null, result);
    });
}

function add(userId, content, attachments, callback) {
    extractExternalContent(content, function (error, result) {
        if (error) return callback(error);

        var doc = {
            content: content,
            createdAt: Date.now(),
            modifiedAt: Date.now(),
            tags: extractTags(content),
            externalContent: result,
            attachments: attachments
        };

        async.eachSeries(doc.tags, tags.update.bind(null, userId), function (error) {
            if (error) return callback(error);

            things.add(userId, doc.content, doc.tags, doc.attachments, doc.externalContent, function (error, result) {
                if (error) return callback(error);
                if (!result) return callback(new Error('no result returned'));

                get(userId, result._id, callback);
            });
        });
    });
}

function put(userId, thingId, content, attachments, isPublic, isShared, isArchived, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof thingId, 'string');
    assert.strictEqual(typeof content, 'string');
    assert(Array.isArray(attachments));
    assert.strictEqual(typeof isPublic, 'boolean');
    assert.strictEqual(typeof isShared, 'boolean');
    assert.strictEqual(typeof isArchived, 'boolean');
    assert.strictEqual(typeof callback, 'function');

    var tagObjects = extractTags(content);

    async.eachSeries(tagObjects, tags.update.bind(null, userId), function (error) {
        if (error) return callback(error);

        extractExternalContent(content, function (error, externalContent) {
            if (error) console.error('Failed to extract external content:', error);

            things.put(userId, thingId, content, tagObjects, attachments, externalContent, isPublic, isShared, isArchived, function (error) {
                if (error) return callback(error);

                get(userId, thingId, callback);
            });
        });
    });
}

function del(userId, id, callback) {
    things.del(userId, id, function (error) {
        if (error) return callback(error);
        callback(null);
    });
}

function exp(userId, callback) {
    things.getAllLean(userId, function (error, result) {
        if (error) return callback(error);
        if (!result) return (null, '');

        var out = result.map(function (thing) {
            return {
                createdAt: thing.createdAt,
                modifiedAt: thing.modifiedAt,
                content: thing.content,
                externalContent: thing.externalContent || [],
                attachments: thing.attachments || []
            };
        });

        callback(null, { things: out });
    });
}

function imp(userId, data, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof data, 'object');
    assert.strictEqual(typeof callback, 'function');

    async.eachSeries(data.things, function (thing, next) {
        var tagObjects = extractTags(thing.content);

        async.eachSeries(tagObjects, tags.update.bind(null, userId), function (error) {
            if (error) return next(error);

            // older exports use strings here
            if (typeof thing.createdAt === 'string') thing.createdAt = (new Date(thing.createdAt)).getTime();
            if (typeof thing.modifiedAt === 'string') thing.modifiedAt = (new Date(thing.modifiedAt)).getTime();

            things.addFull(userId, thing.content, tagObjects, thing.attachments || [], thing.externalContent || [], thing.createdAt, thing.modifiedAt || thing.createdAt, function (error, result) {
                if (error) return next(error);
                if (!result) return next(new Error('no result returned'));

                next(null, result._id);
            });
        });
    }, callback);
}

function cleanupTags() {
    var userIds = things.getAllActiveUserIds();

    async.each(userIds, function (userId, callback) {
        things.getAllLean(userId, function (error, result) {
            if (error) return console.error(new Error(error));

            var activeTags = [];
            result.forEach(function (thing) {
                activeTags = activeTags.concat(extractTags(thing.content));
            });

            tags.get(userId, function (error, result) {
                if (error) return console.error(new Error(error));

                async.each(result, function (tag, callback) {
                    if (activeTags.indexOf(tag.name) !== -1) return callback(null);

                    debug('Cleanup tag', tag.name);

                    tags.del(userId, String(tag._id), callback);
                }, callback);
            });
        });
    }, function (error) {
        if (error) console.error('Cleanup tags failed:', error);
    });
}

function importThings(userId, filePath, callback) {
    assert.strictEqual(typeof userId, 'string');
    assert.strictEqual(typeof filePath, 'string');
    assert.strictEqual(typeof callback, 'function');

    var attachmentFolder = path.join(config.attachmentDir, userId);
    mkdirp.sync(attachmentFolder);

    function cleanup() {
        // cleanup things.json
        safe.fs.unlinkSync(path.join(attachmentFolder, 'things.json'));

        // cleanup uploaded file
        safe.fs.unlinkSync(filePath);
    }

    var outStream = fs.createReadStream(filePath);
    var extract = tar.extract(attachmentFolder, {
        map: function (header) {
            var prefix = 'attachments/';

            if (header.name.indexOf(prefix) === 0) header.name = header.name.slice(prefix.length);

            return header;
        }
    });

    extract.on('error', function (error) {
        cleanup();

        callback(error);
    });

    outStream.on('end', function () {
        var data = safe.require(path.join(attachmentFolder, 'things.json'));

        cleanup();

        // very basic sanity check
        if (!data) return callback('content is not JSON');
        if (!Array.isArray(data.things)) return callback('content must have a "things" array');

        imp(userId, data, callback);
    });

    outStream.pipe(extract);
}

function expOldData() {
    var collection = config.db.collection('things');

    collection.find({}).toArray(function (error, result) {
        if (error) return console.error('Failed to export old data:', error);
        if (!result || result.length === 0) return;   // nothing to do

        console.log('Old data found, prepare for import');

        var tmp = result.map(function (thing) {
            return {
                createdAt: thing.createdAt,
                modifiedAt: thing.modifiedAt,
                content: thing.content,
                externalContent: thing.externalContent || [],
                attachments: thing.attachments || []
            };
        });

        var oldAttachmentFolder = '/app/data/attachments';

        // ensure the folder exists in case the user has never uploaded a file
        mkdirp.sync(oldAttachmentFolder);

        var out = tar.pack(oldAttachmentFolder, {
            map: function (header) {
                header.name = 'attachments/' + header.name;
                return header;
            }
        });

        // add the db dump
        out.entry({ name: 'things.json' }, JSON.stringify({ things: tmp }, null, 4));

        out.pipe(fs.createWriteStream('/tmp/old_data_export.tar'));

        out.on('end', function () {
            exports.hasOldData = '/tmp/old_data_export.tar';
            console.log('Old data available at %s', exports.hasOldData);
        });
    });
}

function cleanupOldData(callback) {
    var oldAttachmentFolder = '/app/data/attachments';

    // prevent from further importing
    exports.hasOldData = null;

    rimraf(oldAttachmentFolder, function (error) {
        if (error) console.error(error);

        var collections = [
            config.db.collection('things'),
            config.db.collection('publicLinks'),
            config.db.collection('tags')
        ];

        async.eachSeries(collections, function (collection, callback) {
            collection.drop(function(error) {
                if (error) console.error(error);
                callback();
            });
        }, callback);
    });
}
<|endoftext|>


<commit_before>// PUT /db/{db}/forms/{id}/filled/{owner}
// Forms / Fill a form.
//
// Alpha @ 0.1.43
//
// `202 Accepted`,
// [Idempotent](/docs/#/concept/idempotent.md).
//
// Contact [`{as}`](/docs/#/concept/as.md) fills in an instance of
// form `{id}` bound to entity `{owner}`.
//
// ### Request format
//     { "data" : { <key> : <value> }
//
// - `data` contains a dictionary mapping field names to arbitrary JSON values.
//   Expected contents depend on the configuration of individual form fields.
//
// ### Response type
//     { "at" : <clock> }
//
// - `at` is a vector clock representing the point in time where the changes
//   performed will be effective.
//

TEST("The response has valid return code and content type.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var token = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,token).result('id');

    var response = Test.query("PUT",["db/",db,"/forms/",id,"/filled/",token.id],data,token).response();

    response.map(function(r) {
	Assert.areEqual(202, r.status).then();
	Assert.isTrue(r.responseJSON, "Response type is JSON").then();
    }).then(next);

});

// # Examples
//
// ### Example request
//     PUT /db/0SNQc00211H/forms/0SNQe0032JZ/filled/0SNxd0002JZ?as=0SNxd0002JZ
//     Content-Type: application/json
//
//     { "data" : {
//         "color": "Red",
//         "birth": "1985-04-19" } }
//
// ### Example response
//     202 Accepted
//     Content-Type: application/json
//
//     { "at" : [[5,218]] }

TEST("Correct data is available.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "color" : "Red" };

    var db = Query.mkdb();
    var token = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,token).result('id');

    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",token.id],{"data":data},token).response().then(function(){

	var result = Test.query("GET",["db/",db,"/forms/",id,"/filled/",token.id],token).result();

	var expected = {
	    "owner" : token.id,
	    "data" : data
	};

	Assert.areEqual(expected, result).then(next);

    });

});

// # Errors
//
// ## Returns `404 Not Found`
// - ... if database `{db}` does not exist

TEST("Returns 404 when database does not exist.", function(next) {

    Test.query("PUT",["db/00000000000/forms/00000000001/filled/00000000002"],{"data":{}})
	.error(404).then(next);

});

// - ... if form `{id}` does not exist in database `{db}`

TEST("Returns 404 when form does not exist.", function(next) {

    var db = Query.mkdb();

    Test.query("PUT",["db/",db,"/forms/00000000001/filled/00000000002"],{"data":{}})
	.error(404).then(next);

});

// - ... if contact `{as}` is not allowed to view form `{id}`, to ensure
// [absence equivalence](/docs/#/concept/absence-equivalence.md).

TEST("Returns 404 when form not viewable.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",peon.id],data,peon)
	.error(404).then(next);
});

// ## Returns `401 Unauthorized`
// - ... if the provided token does not grant access as the named
//   contact, or no token was provided

TEST("Returns 401 when token is not valid.", function(next) {

    var example = {
	"owner": "contact",
	"audience": {},
	"fields": []
    };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],example,auth).result("id");

    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":{}},{tok:"0123456789a",id:"0123456789a"})
	.error(401).then(next);

});

// ## Returns `403 Forbidden`
// - ... if contact `{as}` is can view the form, but not fill the requested
//   instance. For instance, without **admin** access, a contact may only
//   fill the instance bound to himself (`{owner} == {as}`), and not to
//   other contacts. Access restrictions are defined for each type of
//   owner.

TEST("Returns 403 when form instance not viewable.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data,peon)
	    .error(403).then(next);
});

TEST("Allow cross-filling when admin", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",peon.id],data,auth)
	.success(202).then(next);
});

// ## Returns `400 Bad Request`
// - ... if a required field was not provided or is `null`, `""`, `[]` or `{}`.

TEST("Missing required field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "json",
	    "label": "Favourite color",
	    "required": true
	} ]
    };

    var data = [
	{ "data": { "color" : null } },
	{ "data": {} },
	{ "data": { "color" : [] } },
	{ "data": { "color" : {} } },
	{ "data": { "color" : "" } }
    ];

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    for (var i = 0; i < data.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data[i],auth)
		   .error(400));

    tests.then(next);
});

// - ... if a provided field does not exist in the form.

TEST("Unknown provided field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "json",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red", "unkown": true } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data,auth)
	.error(400);

});

// - ... A provided field was not in the expected data format.

TEST("Missing required field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "text",
	    "kind": "text",
	    "label": "Text"
	}, {
	    "id": "single",
	    "kind": "single",
	    "choices": [ "A", "B" ],
	    "label": "Single"
	}, {
	    "id": "multi",
	    "kind": "multi",
	    "choices": [ "A", "B" ],
	    "label": "Multiple"
	}, {
	    "id": "time",
	    "kind": "time",
	    "label": "Time"
	}, {
	    "id": "contact",
	    "kind": "contact",
	    "label": "Contact"
	} ]
    };

    var fail = [
	{ "text": [] },
        { "single": [1] },
        { "single": "1" },
        { "single": 2 },
        { "multi": 1 },
        { "multi": [1,2] },
        { "multi": {} },
        { "time": 0 },
        { "time": "2014/01/01" },
        { "contact": "00000000000" },
        { "contact": 123 }
    ];

    var db = Query.mkdb();
    var auth = Query.auth(db);

    var success = [
	{ "text": "Hello" },
        { "single": 1, },
        { "multi": [1] },
        { "multi": [] },
        { "time": "2014-01-01" },
        { "time": "2014-01-01T23:59:59Z" },
        { "contact": auth.id },
	{ "contact": null }
    ];

    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    for (var i = 0; i < fail.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":fail[i]},auth)
		   .error(400));

    for (var i = 0; i < success.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":success[i]},auth)
		   .success(202));

    tests.then(next);
});

// # Access restrictions
//
// Contact must have `fill` [audience](/docs/#/form/audience.md) access to
// the form, and be able to fill the instance.
<commit_msg>Fix tests<commit_after>// PUT /db/{db}/forms/{id}/filled/{owner}
// Forms / Fill a form.
//
// Alpha @ 0.1.43
//
// `202 Accepted`,
// [Idempotent](/docs/#/concept/idempotent.md).
//
// Contact [`{as}`](/docs/#/concept/as.md) fills in an instance of
// form `{id}` bound to entity `{owner}`.
//
// ### Request format
//     { "data" : { <key> : <value> }
//
// - `data` contains a dictionary mapping field names to arbitrary JSON values.
//   Expected contents depend on the configuration of individual form fields.
//
// ### Response type
//     { "at" : <clock> }
//
// - `at` is a vector clock representing the point in time where the changes
//   performed will be effective.
//

TEST("The response has valid return code and content type.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var token = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,token).result('id');

    var response = Test.query("PUT",["db/",db,"/forms/",id,"/filled/",token.id],data,token).response();

    response.map(function(r) {
	Assert.areEqual(202, r.status).then();
	Assert.isTrue(r.responseJSON, "Response type is JSON").then();
    }).then(next);

});

// # Examples
//
// ### Example request
//     PUT /db/0SNQc00211H/forms/0SNQe0032JZ/filled/0SNxd0002JZ?as=0SNxd0002JZ
//     Content-Type: application/json
//
//     { "data" : {
//         "color": "Red",
//         "birth": "1985-04-19" } }
//
// ### Example response
//     202 Accepted
//     Content-Type: application/json
//
//     { "at" : [[5,218]] }

TEST("Correct data is available.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "color" : "Red" };

    var db = Query.mkdb();
    var token = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,token).result('id');

    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",token.id],{"data":data},token).response().then(function(){

	var result = Test.query("GET",["db/",db,"/forms/",id,"/filled/",token.id],token).result();

	var expected = {
	    "owner" : token.id,
	    "data" : data
	};

	Assert.areEqual(expected, result).then(next);

    });

});

// # Errors
//
// ## Returns `404 Not Found`
// - ... if database `{db}` does not exist

TEST("Returns 404 when database does not exist.", function(next) {

    Test.query("PUT",["db/00000000000/forms/00000000001/filled/00000000002"],{"data":{}})
	.error(404).then(next);

});

// - ... if form `{id}` does not exist in database `{db}`

TEST("Returns 404 when form does not exist.", function(next) {

    var db = Query.mkdb();

    Test.query("PUT",["db/",db,"/forms/00000000001/filled/00000000002"],{"data":{}})
	.error(404).then(next);

});

// - ... if contact `{as}` is not allowed to view form `{id}`, to ensure
// [absence equivalence](/docs/#/concept/absence-equivalence.md).

TEST("Returns 404 when form not viewable.", function(next) {

    var form = {
	"owner": "contact",
	"audience": {},
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",peon.id],data,peon)
	.error(404).then(next);
});

// ## Returns `401 Unauthorized`
// - ... if the provided token does not grant access as the named
//   contact, or no token was provided

TEST("Returns 401 when token is not valid.", function(next) {

    var example = {
	"owner": "contact",
	"audience": {},
	"fields": []
    };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],example,auth).result("id");

    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":{}},{tok:"0123456789a",id:"0123456789a"})
	.error(401).then(next);

});

// ## Returns `403 Forbidden`
// - ... if contact `{as}` is can view the form, but not fill the requested
//   instance. For instance, without **admin** access, a contact may only
//   fill the instance bound to himself (`{owner} == {as}`), and not to
//   other contacts. Access restrictions are defined for each type of
//   owner.

TEST("Returns 403 when form instance not viewable.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data,peon)
	    .error(403).then(next);
});

TEST("Allow cross-filling when admin", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "text",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red" } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var peon = Query.auth(db,false,"peon@runorg.com");
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",peon.id],data,auth)
	.success(202).then(next);
});

// ## Returns `400 Bad Request`
// - ... if a required field was not provided or is `null`, `""`, `[]` or `{}`.

TEST("Missing required field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "json",
	    "label": "Favourite color",
	    "required": true
	} ]
    };

    var data = [
	{ "data": { "color" : null } },
	{ "data": {} },
	{ "data": { "color" : [] } },
	{ "data": { "color" : {} } },
	{ "data": { "color" : "" } }
    ];

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    for (var i = 0; i < data.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data[i],auth)
		   .error(400));

    tests.then(next);
});

// - ... if a provided field does not exist in the form.

TEST("Unknown provided field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "color",
	    "kind": "json",
	    "label": "Favourite color"
	} ]
    };

    var data = { "data": { "color" : "Red", "unkown": true } };

    var db = Query.mkdb();
    var auth = Query.auth(db);
    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],data,auth)
	.error(400).then(next);

});

// - ... A provided field was not in the expected data format.

TEST("Missing required field.", function(next) {

    var form = {
	"owner": "contact",
	"audience": { "fill" : "anyone" },
	"fields": [ {
	    "id": "text",
	    "kind": "text",
	    "label": "Text"
	}, {
	    "id": "single",
	    "kind": "single",
	    "choices": [ "A", "B" ],
	    "label": "Single"
	}, {
	    "id": "multiple",
	    "kind": "multiple",
	    "choices": [ "A", "B" ],
	    "label": "Multiple"
	}, {
	    "id": "time",
	    "kind": "time",
	    "label": "Time"
	}, {
	    "id": "contact",
	    "kind": "contact",
	    "label": "Contact"
	} ]
    };

    var fail = [
	{ "text": [] },
        { "single": [1] },
        { "single": "1" },
        { "single": 2 },
        { "multiple": 1 },
        { "multiple": [1,2] },
        { "multiple": {} },
        { "time": 0 },
        { "time": "2014/01/01" },
        { "contact": "00000000000" },
        { "contact": 123 }
    ];

    var db = Query.mkdb();
    var auth = Query.auth(db);

    var success = [
	{ "text": "Hello" },
        { "single": 1, },
        { "multiple": [1] },
        { "multiple": [] },
        { "time": "2014-01-01" },
        { "time": "2014-01-01T23:59:59Z" },
        { "contact": auth.id },
	{ "contact": null }
    ];

    var id = Test.query("POST",["db/",db,"/forms/create"],form,auth).result("id");

    var tests = [];
    for (var i = 0; i < fail.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":fail[i]},auth)
		   .error(400));

    for (var i = 0; i < success.length; ++i)
	tests.push(Test.query("PUT",["db/",db,"/forms/",id,"/filled/",auth.id],{"data":success[i]},auth)
		   .success(202));

    tests.then(next);
});

// # Access restrictions
//
// Contact must have `fill` [audience](/docs/#/form/audience.md) access to
// the form, and be able to fill the instance.
<|endoftext|>


<commit_before>var BinaryHeap = require('gamejs/utils/binaryheap').BinaryHeap;
qModule('gamejs/utils/binaryheap');

test('PushPop', function() {
   var ITEMS = [10, 3, 4, 8, 2, 9, 7, 1, 2, 6, 5];
   var heap = new BinaryHeap(function(x){return x;});

   ITEMS.forEach(function(item) {
      heap.push(item);
   }, this);

   ITEMS.sort(function(a, b) { return a > b ? 1 : -1; });
   var i = 0;
   while (heap.size() > 0) {
      equal(heap.pop(), ITEMS[i]);
      i++;
   };
});

test('Remove', function() {
   var ITEMS = [10, 3, 4, 8, 2, 9, 7, 1, 2, 6, 5];
   var heap = new BinaryHeap(function(x){return x;});

   ITEMS.forEach(function(item) {
      heap.push(item);
   });

   raises(function() { heap.remove(88); });
   strictEqual(heap.remove(2), undefined);
   strictEqual(heap.remove(2), undefined);
   raises(function() { heap.remove(2); });
});
<commit_msg>fix binaryheap unit tests<commit_after>var BinaryHeap = require('gamejs/utils/binaryheap').BinaryHeap;
qModule('gamejs/utils/binaryheap');

test('PushPop', function() {
   var ITEMS = [10, 3, 4, 8, 2, 9, 7, 1, 2, 6, 5];
   var heap = new BinaryHeap(function(x){return x;});

   ITEMS.forEach(function(item) {
      heap.push(item);
   }, this);

   ITEMS.sort(function(a, b) { return a > b ? 1 : -1; });
   var i = 0;
   while (heap.size() > 0) {
      equal(heap.pop(), ITEMS[i]);
      i++;
   };
});

test('Remove', function() {
   var ITEMS = [10, 3, 4, 8, 2, 9, 7, 1, 2, 6, 5];
   var heap = new BinaryHeap(function(x){return x;});

   ITEMS.forEach(function(item) {
      heap.push(item);
   });

   //raises(function() { heap.remove(88); });
   strictEqual(heap.remove(2), undefined);
   strictEqual(heap.remove(2), undefined);
   //raises(function() { heap.remove(2); });
});
<|endoftext|>


<commit_before>'use strict';

var expect = require('chai').expect;
var Grid = require('../app/scripts/models/grid');
var Player = require('../app/scripts/models/player');
var Game = require('../app/scripts/models/game');

describe('game', function () {

  // Place chips at the given columns in the given order
  function placeChips(args) {
    args.columns.forEach(function (column) {
      args.game.placePendingChip({column: column});
    });
  }

  it('should initialize with no arguments', function () {
    var game = new Game();
    expect(game).to.have.property('grid');
    expect(game.grid).to.have.property('columnCount', 7);
    expect(game.grid).to.have.property('rowCount', 6);
    expect(game).to.have.property('players');
    expect(game.players).to.have.length(0);
    expect(game).to.have.property('currentPlayer', null);
    expect(game).to.have.property('inProgress', false);
    expect(game).to.have.property('pendingChip', null);
    expect(game).to.have.property('lastPlacedChip', null);
    expect(game).to.have.property('winner', null);
  });

  it('should initialize with arguments', function () {
    var game = new Game({
      players: [
        new Player({color: 'blue', name: 'Bob'}),
        new Player({color: 'black', name: 'Larry'})
      ],
      grid: new Grid({columnCount: 9, rowCount: 8})
    });
    expect(game.grid).to.have.property('columnCount', 9);
    expect(game.grid).to.have.property('rowCount', 8);
    expect(game.players).to.have.length(2);
    expect(game.players[0].name).to.equal('Bob');
    expect(game.players[1].name).to.equal('Larry');
    expect(game).to.have.property('currentPlayer', null);
    expect(game).to.have.property('inProgress', false);
    expect(game).to.have.property('pendingChip', null);
    expect(game).to.have.property('lastPlacedChip', null);
    expect(game).to.have.property('winner', null);
  });

  it('should start', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    expect(game.currentPlayer).to.equal(game.players[0]);
    expect(game.inProgress).to.be.true;
  });

  it('should start turn', function () {
    var game = new Game();
    game.startTurn();
    expect(game.pendingChip).not.to.be.null;
  });

  it('should end turn', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.startTurn();
    game.endTurn();
    expect(game.currentPlayer).to.equal(game.players[1]);
  });

  it('should end', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.endGame();
    expect(game.currentPlayer).to.be.null;
    expect(game.inProgress).to.be.false;
    expect(game.pendingChip).to.be.null;
  });

  it('should increment winner\'s score when ending', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.winner = game.players[0];
    expect(game.winner.score).to.equal(0);
    game.endGame();
    expect(game.winner.score).to.equal(1);
  });

  it('should reset grid when resetting', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.placePendingChip({column: 2});
    expect(game.grid.columns[2]).to.have.length(1);
    game.endGame();
    game.resetGame();
    expect(game.grid.columns[2]).to.have.length(0);
  });

  it('should reset winner when resetting', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.winner = game.players[0];
    game.endGame();
    game.resetGame();
    expect(game.winner).to.be.null;
  });

  it('should win horizontally', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [2, 2, 3, 3, 4, 4, 5]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win vertically', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [0, 1, 0, 1, 0, 1, 0]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win diagonally', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [3, 4, 4, 3, 5, 5, 5, 6, 6, 6, 6]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win with two connect-fours at once', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [0, 1, 1, 1, 2, 2, 2, 0, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 3]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win on connections of more than four', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [2, 2, 3, 3, 4, 4, 6, 6, 5]
    });
    expect(game.winner).not.to.be.null;
  });

  it('should end when grid becomes full', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [
        0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0,
        2, 3, 2, 3, 2, 3, 3, 2, 3, 2, 3, 2,
        4, 5, 4, 5, 4, 5, 5, 4, 5, 4, 5, 4,
        6, 6, 6, 6, 6, 6
      ]
    });
    expect(game.winner).to.be.null;
    expect(game.inProgress).to.be.false;
    expect(game.players[0].score).to.equal(0);
    expect(game.players[1].score).to.equal(0);
  });

});
<commit_msg>Fix failing startTurn test<commit_after>'use strict';

var expect = require('chai').expect;
var Grid = require('../app/scripts/models/grid');
var Player = require('../app/scripts/models/player');
var Game = require('../app/scripts/models/game');

describe('game', function () {

  // Place chips at the given columns in the given order
  function placeChips(args) {
    args.columns.forEach(function (column) {
      args.game.placePendingChip({column: column});
    });
  }

  it('should initialize with no arguments', function () {
    var game = new Game();
    expect(game).to.have.property('grid');
    expect(game.grid).to.have.property('columnCount', 7);
    expect(game.grid).to.have.property('rowCount', 6);
    expect(game).to.have.property('players');
    expect(game.players).to.have.length(0);
    expect(game).to.have.property('currentPlayer', null);
    expect(game).to.have.property('inProgress', false);
    expect(game).to.have.property('pendingChip', null);
    expect(game).to.have.property('lastPlacedChip', null);
    expect(game).to.have.property('winner', null);
  });

  it('should initialize with arguments', function () {
    var game = new Game({
      players: [
        new Player({color: 'blue', name: 'Bob'}),
        new Player({color: 'black', name: 'Larry'})
      ],
      grid: new Grid({columnCount: 9, rowCount: 8})
    });
    expect(game.grid).to.have.property('columnCount', 9);
    expect(game.grid).to.have.property('rowCount', 8);
    expect(game.players).to.have.length(2);
    expect(game.players[0].name).to.equal('Bob');
    expect(game.players[1].name).to.equal('Larry');
    expect(game).to.have.property('currentPlayer', null);
    expect(game).to.have.property('inProgress', false);
    expect(game).to.have.property('pendingChip', null);
    expect(game).to.have.property('lastPlacedChip', null);
    expect(game).to.have.property('winner', null);
  });

  it('should start', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    expect(game.currentPlayer).to.equal(game.players[0]);
    expect(game.inProgress).to.be.true;
  });

  it('should start turn', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.startTurn();
    expect(game.pendingChip).not.to.be.null;
  });

  it('should end turn', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.startTurn();
    game.endTurn();
    expect(game.currentPlayer).to.equal(game.players[1]);
  });

  it('should end', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.endGame();
    expect(game.currentPlayer).to.be.null;
    expect(game.inProgress).to.be.false;
    expect(game.pendingChip).to.be.null;
  });

  it('should increment winner\'s score when ending', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.winner = game.players[0];
    expect(game.winner.score).to.equal(0);
    game.endGame();
    expect(game.winner.score).to.equal(1);
  });

  it('should reset grid when resetting', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.placePendingChip({column: 2});
    expect(game.grid.columns[2]).to.have.length(1);
    game.endGame();
    game.resetGame();
    expect(game.grid.columns[2]).to.have.length(0);
  });

  it('should reset winner when resetting', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    game.winner = game.players[0];
    game.endGame();
    game.resetGame();
    expect(game.winner).to.be.null;
  });

  it('should win horizontally', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [2, 2, 3, 3, 4, 4, 5]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win vertically', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [0, 1, 0, 1, 0, 1, 0]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win diagonally', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [3, 4, 4, 3, 5, 5, 5, 6, 6, 6, 6]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win with two connect-fours at once', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [0, 1, 1, 1, 2, 2, 2, 0, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 3]
    });
    expect(game.winner).not.to.be.null;
    expect(game.winner.name).to.equal('Player 1');
  });

  it('should win on connections of more than four', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [2, 2, 3, 3, 4, 4, 6, 6, 5]
    });
    expect(game.winner).not.to.be.null;
  });

  it('should end when grid becomes full', function () {
    var game = new Game();
    game.setPlayers(2);
    game.startGame();
    placeChips({
      game: game,
      columns: [
        0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0,
        2, 3, 2, 3, 2, 3, 3, 2, 3, 2, 3, 2,
        4, 5, 4, 5, 4, 5, 5, 4, 5, 4, 5, 4,
        6, 6, 6, 6, 6, 6
      ]
    });
    expect(game.winner).to.be.null;
    expect(game.inProgress).to.be.false;
    expect(game.players[0].score).to.equal(0);
    expect(game.players[1].score).to.equal(0);
  });

});
<|endoftext|>


<commit_before>'use strict';

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

export const EVENTS = {
	DOM: {
		ADDED: {
			CHAT_ITEM: 'dom.added.chatItem',
			CHAT_ITEM_MESSAGE: 'dom.added.chatItemMessage',
			CHAT_ITEM_FIELD_BODY: 'dom.added.chatItemFieldBody',
			CHAT_HEADER: 'dom.added.chatHeader',
			CHAT_INPUT_CONTAINER: 'dom.added.chatInputContainer',
			CHAPTER: 'dom.added.chapter',
			CHAPTER_BUTTON_CONTROLS: 'dom.added.chapterButtonControls',
			STORY: 'dom.added.storyItem',
			CHAT_MODAL: 'dom.added.chatModal',
			MAIN_MENU: 'dom.added.mainMenu'
		}
	}
};

export default class ObserverDOM {
	constructor(eventEmitter) {
		this._eventEmitter = eventEmitter;

		// jQuery already present on page
		$(document).ready(() => {
			this._observeBody();
		});
	}

	node(type) {
		switch (type) {
			case 'mainMenu':
				return document.getElementById('mainMenu');
			default:
				return null;
		}
	}

	nodes(type) {
		switch (type) {
			case 'chapterButtonControls':
				return document.querySelectorAll('.chapter .secondRow');
			case 'chatHeader':
				return document.querySelectorAll('.chatHeader');
			case 'storyItem':
				return document.querySelectorAll('.storyItem');
			case 'chapter':
				return document.querySelectorAll('.chapter');
			case 'logItem':
				return document.querySelectorAll('.logItem');
			case 'message':
				return document.querySelectorAll('.logItem .message');
			case 'mainMenu':
				return [document.getElementById('mainMenu')];
			default:
				return [];
		}
	}

	static _observe(node, callback, config) {
		const observer = new MutationObserver(callback);
		observer.observe(node, config);
	}

	_observeBody() {
		ObserverDOM._observe(document.body, this._observerBodyFunction.bind(this), {
			childList: true,
			subtree: true
		});
	}

	_observerBodyFunction(mutations) {
		for (let mutation of mutations) {
			for (let node of mutation.addedNodes) {
				// console.log(node);
				if (node.classList) {
					if (node.classList.contains('logItem')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, node);
						let nodeMessage = node.querySelector('.message');
						if (nodeMessage) {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
						}
					}
					if (node.classList.contains('message')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, node);
					}
					if (node.classList.contains('jadeRepeat')) {
						node.querySelectorAll('.logItem').forEach(nodeLogItem => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, nodeLogItem);
							let nodeMessage = nodeLogItem.querySelector('.message');
							if (nodeMessage) {
								this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
							}
						});
						node.querySelectorAll('.chapter').forEach(nodeChapter => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER, nodeChapter);
						});
					}
					if (node.classList.contains('chapter')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER, node);
					}
					if (node.classList.contains('fieldBody')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_FIELD_BODY, node);
					}
					if (node.classList.contains('storyItem')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.STORY, node);
					}
					if (node.classList.contains('chatContainer')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_HEADER, node.querySelector('.chatHeader'));
					}
					if (node.classList.contains('secondRow')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER_BUTTON_CONTROLS, node);
					}
					if (node.classList.contains('chatLight')) {
						node.querySelectorAll('.logItem').forEach(nodeLogItem => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, nodeLogItem);
							let nodeMessage = nodeLogItem.querySelector('.message');
							if (nodeMessage) {
								this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
							}
						});
					}
					if (node.classList.contains('chatItemDetail')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_MODAL, node);
						node.querySelectorAll('.chatHeader').forEach(nodeChatHeader => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_HEADER, nodeChatHeader);
						})
					}
					if (node.classList.contains('chatInputContainer')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_INPUT_CONTAINER, node);
					}
				}
			}
		}
	}
}
<commit_msg>Fixed bug where vote reply modal weren't being styled correctly<commit_after>'use strict';

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

export const EVENTS = {
	DOM: {
		ADDED: {
			CHAT_ITEM: 'dom.added.chatItem',
			CHAT_ITEM_MESSAGE: 'dom.added.chatItemMessage',
			CHAT_ITEM_FIELD_BODY: 'dom.added.chatItemFieldBody',
			CHAT_HEADER: 'dom.added.chatHeader',
			CHAT_INPUT_CONTAINER: 'dom.added.chatInputContainer',
			CHAPTER: 'dom.added.chapter',
			CHAPTER_BUTTON_CONTROLS: 'dom.added.chapterButtonControls',
			STORY: 'dom.added.storyItem',
			CHAT_MODAL: 'dom.added.chatModal',
			MAIN_MENU: 'dom.added.mainMenu'
		}
	}
};

export default class ObserverDOM {
	constructor(eventEmitter) {
		this._eventEmitter = eventEmitter;

		// jQuery already present on page
		$(document).ready(() => {
			this._observeBody();
		});
	}

	node(type) {
		switch (type) {
			case 'mainMenu':
				return document.getElementById('mainMenu');
			default:
				return null;
		}
	}

	nodes(type) {
		switch (type) {
			case 'chapterButtonControls':
				return document.querySelectorAll('.chapter .secondRow');
			case 'chatHeader':
				return document.querySelectorAll('.chatHeader');
			case 'storyItem':
				return document.querySelectorAll('.storyItem');
			case 'chapter':
				return document.querySelectorAll('.chapter');
			case 'logItem':
				return document.querySelectorAll('.logItem');
			case 'message':
				return document.querySelectorAll('.logItem .message');
			case 'mainMenu':
				return [document.getElementById('mainMenu')];
			default:
				return [];
		}
	}

	static _observe(node, callback, config) {
		const observer = new MutationObserver(callback);
		observer.observe(node, config);
	}

	_observeBody() {
		ObserverDOM._observe(document.body, this._observerBodyFunction.bind(this), {
			childList: true,
			subtree: true
		});
	}

	_observerBodyFunction(mutations) {
		for (let mutation of mutations) {
			for (let node of mutation.addedNodes) {
				// console.log(node);
				if (node.classList) {
					if (node.classList.contains('logItem')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, node);
						let nodeMessage = node.querySelector('.message');
						if (nodeMessage) {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
						}
					}
					if (node.classList.contains('message')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, node);
					}
					if (node.classList.contains('jadeRepeat')) {
						node.querySelectorAll('.logItem').forEach(nodeLogItem => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, nodeLogItem);
							let nodeMessage = nodeLogItem.querySelector('.message');
							if (nodeMessage) {
								this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
							}
						});
						node.querySelectorAll('.chapter').forEach(nodeChapter => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER, nodeChapter);
						});
					}
					if (node.classList.contains('chapter')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER, node);
					}
					if (node.classList.contains('fieldBody')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_FIELD_BODY, node);
					}
					if (node.classList.contains('storyItem')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.STORY, node);
					}
					if (node.classList.contains('chatContainer')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_HEADER, node.querySelector('.chatHeader'));
					}
					if (node.classList.contains('secondRow')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAPTER_BUTTON_CONTROLS, node);
					}
					if (node.classList.contains('chatLight')) {
						node.querySelectorAll('.logItem').forEach(nodeLogItem => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM, nodeLogItem);
							let nodeMessage = nodeLogItem.querySelector('.message');
							if (nodeMessage) {
								this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_ITEM_MESSAGE, nodeMessage);
							}
						});
					}
					if (node.classList.contains('chatItemDetail') || node.classList.contains('choiceReply')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_MODAL, node);
						node.querySelectorAll('.chatHeader').forEach(nodeChatHeader => {
							this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_HEADER, nodeChatHeader);
						})
					}
					if (node.classList.contains('chatInputContainer')) {
						this._eventEmitter.emit(EVENTS.DOM.ADDED.CHAT_INPUT_CONTAINER, node);
					}
				}
			}
		}
	}
}
<|endoftext|>


<commit_before>//not embedded so we can style it
var bitcoinSymbol = "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 273.6 360' enable-background='new 0 0 273.6 360' xml:space='preserve' class='currencySymbol btc'> <g> <g> <path fill='#000' d='M217.021,167.042c18.631-9.483,30.288-26.184,27.565-54.007c-3.667-38.023-36.526-50.773-78.006-54.404l-0.008-52.741 h-32.139l-0.009,51.354c-8.456,0-17.076,0.166-25.657,0.338L108.76,5.897l-32.11-0.003l-0.006,52.728 c-6.959,0.142-13.793,0.277-20.466,0.277v-0.156l-44.33-0.018l0.006,34.282c0,0,23.734-0.446,23.343-0.013 c13.013,0.009,17.262,7.559,18.484,14.076l0.01,60.083v84.397c-0.573,4.09-2.984,10.625-12.083,10.637 c0.414,0.364-23.379-0.004-23.379-0.004l-6.375,38.335h41.817c7.792,0.009,15.448,0.13,22.959,0.19l0.028,53.338l32.102,0.009 l-0.009-52.779c8.832,0.18,17.357,0.258,25.684,0.247l-0.009,52.532h32.138l0.018-53.249c54.022-3.1,91.842-16.697,96.544-67.385 C266.916,192.612,247.692,174.396,217.021,167.042z M109.535,95.321c18.126,0,75.132-5.767,75.14,32.064 c-0.008,36.269-56.996,32.032-75.14,32.032V95.321z M109.521,262.447l0.014-70.672c21.778-0.006,90.085-6.261,90.094,35.32 C199.638,266.971,131.313,262.431,109.521,262.447z'/> </g> </g> </svg>";

var reward = Template.rewardView.reward = Session.getter("reward");
var updateReward = Session.setter("reward");

var rewardMinimum = Template.rewardView.minimum = function () {
    var myReward = reward();
    return myReward ? ReceiverUtils.minimum(myReward.currency) : new Big(0);
};
var rewardTotal = Template.rewardView.total = function () {
    var myReward = reward();
    return myReward ? myReward.total() : new Big(0);
};
var isValid = Template.rewardView.isValid = function () {
    var myReward = reward();
    return myReward && myReward.validationErrors().length === 0;
};

Template.rewardView.currencySymbol = function () {
    var myReward = reward();
    if (!myReward)
        return "";

    if (myReward.currency === "usd")
        return '$';
    if (myReward.currency === "btc")
        return bitcoinSymbol;
};

Template.rewardView.step = function () {
    var myReward = reward();
    if (!myReward)
        return "1";

    if (myReward.currency === "usd")
        return "0.01";
    if (myReward.currency === "btc")
        return "0.0001";
};

Template.rewardView.receiverTotal = function () {
    var myReward = reward();
    return myReward ? myReward.receiverTotal() : new Big(0);
};
Template.rewardView.receiverHasReward = function () {
    var receiver = this;
    return receiver.getReward().gt(0);
};
Template.rewardView.receiverReward = function (percent) {
    var receiver = this;
    var receiverReward = receiver.getReward();

    if (percent !== true)
        return receiverReward;

    var total = rewardTotal();
    var percent = receiverReward.div(total).times(100);
    return percent.toFixed(0);
};

var rowReceiver = function (reward, row) {
    var index = row.index() - 1;
    var receivers = reward.receivers;
    return receivers[index];
};

var setRowAmount = _.debounce(function (row, amount) {
    var myReward = reward();

    var receiver = rowReceiver(myReward, row);

    if (receiver._reward.eq(amount))
        return;

    var total = rewardTotal();
    var minimum = rewardMinimum();

    //if the new amount is > the max, set the amount to the max
    if (amount.gt(total))
        amount = total;

    //if the new amount is < than the minimum and not 0, set the amount to the minimum
    if (amount.lt(minimum) && !amount.eq(0))
        amount = minimum;

    var numberAmount = parseFloat(amount.toString());
    //need to manually update the slider since it is not bound
    row.find(".rewardSlider").val(numberAmount);

    receiver.setReward(amount);
    updateReward(myReward);
}, 250);

var showError = _.debounce(function (show) {
    if (show)
        $(".validationError").stop(false, true)
            .addClass("invalid")
            .fadeIn("fast");
    else
        $(".validationError").stop(true, false)
            .fadeOut("fast", function () {
                $(this).removeClass("invalid");
            });
}, 250);

Template.rewardView.rendered = function () {
    var myReward = reward();
    if (!myReward)
        return;

    var total = rewardTotal(), totalNumber = parseFloat(total.toString());
    var minimum = rewardMinimum(), minimumNumber = parseFloat(minimum.toString());

    var receivers = myReward.getReceivers();

    //setup the sliders
    $(".contributorRow").each(function (index, row) {
        var receiver = receivers[index];
        row = $(row);

        var receiverReward = receiver.getReward();

        var receiverRewardNumber = parseFloat(receiverReward.toString());
        row.find(".rewardSlider").noUiSlider({
            handles: 1,
            range: [minimumNumber, totalNumber],
            slide: function () {
                var amount = new Big($(this).val());
                if (amount.lt(minimum))
                    return false;

                setRowAmount(row, amount);
            },
            start: receiverRewardNumber
        });

        //enable the row if the reward > 0 & minimum !== total
        var enabled = receiverReward.gt(0) && !minimum.eq(total);
        row.find(".rewardSlider").noUiSlider("disabled", !enabled);
    });

    showError(!isValid());
};

Template.rewardView.events({
    "change .rewardInput": function (event) {
        var element = $(event.srcElement);
        var row = element.parents(".contributorRow");
        var amount = new Big(element.val());
        setRowAmount(row, amount);
    },

    "click .closeButton": function () {
        Messenger.send({event: "closeOverlay"});
    },

    "click .rewardButton": function () {
        var url = Session.get("url");
        var admin = window.url("?admin");
        var reason = window.url("?reason");
        if (reason)
            reason = decodeURIComponent(reason);

        $.blockUI();
        Meteor.call("reward", reward(), admin, reason, function (error, success) {
            $.unblockUI();
            Messenger.send({event: "closeOverlay"});
            if (error) {
                TL.error(error, Modules.Reward);
                return;
            }

            if (success)
                Messenger.send({event: "bountyRewarded"});
        });
    },

    "click .shouldPay": function (event) {
        var checkBox = $(event.srcElement);
        var row = checkBox.parents(".contributorRow");

        var myReward = reward();
        var receiver = rowReceiver(myReward, row);

        //if there is no reward set it to the minimum
        if (receiver._reward.eq(0))
            receiver.setReward(new Big(rewardMinimum()));
        //otherwise set the reward at the minimum
        else
            receiver.setReward(new Big(0));

        //update the session
        updateReward(myReward);
    }
});

Template.rewardView.preserve([".noUiSlider"]);
<commit_msg>Auto check and max reward if there is only one contributor.<commit_after>//not embedded so we can style it
var bitcoinSymbol = "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 273.6 360' enable-background='new 0 0 273.6 360' xml:space='preserve' class='currencySymbol btc'> <g> <g> <path fill='#000' d='M217.021,167.042c18.631-9.483,30.288-26.184,27.565-54.007c-3.667-38.023-36.526-50.773-78.006-54.404l-0.008-52.741 h-32.139l-0.009,51.354c-8.456,0-17.076,0.166-25.657,0.338L108.76,5.897l-32.11-0.003l-0.006,52.728 c-6.959,0.142-13.793,0.277-20.466,0.277v-0.156l-44.33-0.018l0.006,34.282c0,0,23.734-0.446,23.343-0.013 c13.013,0.009,17.262,7.559,18.484,14.076l0.01,60.083v84.397c-0.573,4.09-2.984,10.625-12.083,10.637 c0.414,0.364-23.379-0.004-23.379-0.004l-6.375,38.335h41.817c7.792,0.009,15.448,0.13,22.959,0.19l0.028,53.338l32.102,0.009 l-0.009-52.779c8.832,0.18,17.357,0.258,25.684,0.247l-0.009,52.532h32.138l0.018-53.249c54.022-3.1,91.842-16.697,96.544-67.385 C266.916,192.612,247.692,174.396,217.021,167.042z M109.535,95.321c18.126,0,75.132-5.767,75.14,32.064 c-0.008,36.269-56.996,32.032-75.14,32.032V95.321z M109.521,262.447l0.014-70.672c21.778-0.006,90.085-6.261,90.094,35.32 C199.638,266.971,131.313,262.431,109.521,262.447z'/> </g> </g> </svg>";

var reward = Template.rewardView.reward = Session.getter("reward");
var updateReward = Session.setter("reward");

var rewardMinimum = Template.rewardView.minimum = function () {
    var myReward = reward();
    return myReward ? ReceiverUtils.minimum(myReward.currency) : new Big(0);
};
var rewardTotal = Template.rewardView.total = function () {
    var myReward = reward();
    return myReward ? myReward.total() : new Big(0);
};
var isValid = Template.rewardView.isValid = function () {
    var myReward = reward();
    return myReward && myReward.validationErrors().length === 0;
};

Template.rewardView.currencySymbol = function () {
    var myReward = reward();
    if (!myReward)
        return "";

    if (myReward.currency === "usd")
        return '$';
    if (myReward.currency === "btc")
        return bitcoinSymbol;
};

Template.rewardView.step = function () {
    var myReward = reward();
    if (!myReward)
        return "1";

    if (myReward.currency === "usd")
        return "0.01";
    if (myReward.currency === "btc")
        return "0.0001";
};

Template.rewardView.receiverTotal = function () {
    var myReward = reward();
    return myReward ? myReward.receiverTotal() : new Big(0);
};

Template.rewardView.receiverHasReward = function () {
    var receiver = this;
    return receiver.getReward().gt(0);
};

Template.rewardView.receiverReward = function (getPercent) {
    var receiver = this;
    var receiverReward = receiver.getReward();

    if (getPercent !== true)
        return receiverReward;

    var total = rewardTotal();
    var percent = receiverReward.div(total).times(100);
    return percent.toFixed(0);
};

var rowReceiver = function (reward, row) {
    var index = row.index() - 1;
    var receivers = reward.receivers;
    return receivers[index];
};

var setRowAmount = _.debounce(function (row, amount) {
    var myReward = reward();

    var receiver = rowReceiver(myReward, row);

    if (receiver._reward.eq(amount))
        return;

    var total = rewardTotal();
    var minimum = rewardMinimum();

    //if the new amount is > the max, set the amount to the max
    if (amount.gt(total))
        amount = total;

    //if the new amount is < than the minimum and not 0, set the amount to the minimum
    if (amount.lt(minimum) && !amount.eq(0))
        amount = minimum;

    var numberAmount = parseFloat(amount.toString());
    //need to manually update the slider since it is not bound
    row.find(".rewardSlider").val(numberAmount);

    receiver.setReward(amount);
    updateReward(myReward);
}, 250);

var showError = _.debounce(function (show) {
    if (show)
        $(".validationError").stop(false, true)
            .addClass("invalid")
            .fadeIn("fast");
    else
        $(".validationError").stop(true, false)
            .fadeOut("fast", function () {
                $(this).removeClass("invalid");
            });
}, 250);

var singleReceiver = function () {
    var myReward = reward();
    return myReward.receivers.length === 1;
};

Template.rewardView.rendered = function () {
    var myReward = reward();
    if (!myReward)
        return;

    var total = rewardTotal(), totalNumber = parseFloat(total.toString());
    var minimum = rewardMinimum(), minimumNumber = parseFloat(minimum.toString());

    var receivers = myReward.getReceivers();

    //setup the sliders
    $(".contributorRow").each(function (index, row) {
        var receiver = receivers[index];
        row = $(row);

        var receiverReward = receiver.getReward();

        var receiverRewardNumber = parseFloat(receiverReward.toString());
        row.find(".rewardSlider").noUiSlider({
            handles: 1,
            range: [minimumNumber, totalNumber],
            slide: function () {
                var amount = new Big($(this).val());
                if (amount.lt(minimum))
                    return false;

                setRowAmount(row, amount);
            },
            start: receiverRewardNumber
        });

        //enable the row if the reward > 0 & minimum !== total
        var enabled = receiverReward.gt(0) && !minimum.eq(total);
        row.find(".rewardSlider").noUiSlider("disabled", !enabled);
    });

    showError(!isValid());

    if (singleReceiver()) {
        setRowAmount($(".contributorRow"), rewardTotal());
    }
};

Template.rewardView.events({
    "change .rewardInput": function (event) {
        var element = $(event.srcElement);
        var row = element.parents(".contributorRow");
        var amount = new Big(element.val());
        setRowAmount(row, amount);
    },

    "click .closeButton": function () {
        Messenger.send({event: "closeOverlay"});
    },

    "click .rewardButton": function () {
        var url = Session.get("url");
        var admin = window.url("?admin");
        var reason = window.url("?reason");
        if (reason)
            reason = decodeURIComponent(reason);

        $.blockUI();
        Meteor.call("reward", reward(), admin, reason, function (error, success) {
            $.unblockUI();
            Messenger.send({event: "closeOverlay"});
            if (error) {
                TL.error(error, Modules.Reward);
                return;
            }

            if (success)
                Messenger.send({event: "bountyRewarded"});
        });
    },

    "click .shouldPay": function (event) {
        var checkBox = $(event.srcElement);
        var row = checkBox.parents(".contributorRow");

        var myReward = reward();
        var receiver = rowReceiver(myReward, row);

        //if there is no reward set it to the minimum
        if (receiver._reward.eq(0))
            receiver.setReward(new Big(rewardMinimum()));
        //otherwise set the reward at the minimum
        else
            receiver.setReward(new Big(0));

        //update the session
        updateReward(myReward);
    }
});

Template.rewardView.preserve([".noUiSlider"]);
<|endoftext|>


<commit_before>/**
 * @file removeWhitelistDomain command
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

exports.exec = async (Bastion, message, args) => {
  try {
    let index = parseInt(args[0]);
    if (!index || index <= 0) {
      /**
      * The command was ran with invalid parameters.
      * @fires commandUsage
      */
      return Bastion.emit('commandUsage', message, this.help);
    }
    index -= 1;

    let guildSettings = await Bastion.db.get(`SELECT whitelistDomains FROM guildSettings WHERE guildID=${message.guild.id}`);

    if (!guildSettings || guildSettings.whitelistDomains === '[]') {
      /**
      * Error condition is encountered.
      * @fires error
      */
      Bastion.emit('error', Bastion.strings.error(message.guild.language, 'notFound'), Bastion.strings.error(message.guild.language, 'notSet', true, 'whitelist domain'), message.channel);
    }
    else {
      let whitelistDomains = JSON.parse(guildSettings.whitelistDomains);

      if (index >= whitelistDomains.length) {
        /**
        * Error condition is encountered.
        * @fires error
        */
        return Bastion.emit('error', Bastion.strings.error(message.guild.language, 'notFound'), Bastion.strings.error(message.guild.language, 'indexRange', true), message.channel);
      }

      let removedDomain = whitelistDomains[parseInt(args[0]) - 1];
      whitelistDomains.splice(parseInt(args[0]) - 1, 1);

      await Bastion.db.run(`UPDATE guildSettings SET whitelistDomains='${JSON.stringify(whitelistDomains)}' WHERE guildID=${message.guild.id}`);

      message.channel.send({
        embed: {
          color: Bastion.colors.RED,
          description: `I've deleted **${removedDomain}** from whitelisted domains.`
        }
      }).catch(e => {
        Bastion.log.error(e);
      });
    }
  }
  catch (e) {
    Bastion.log.error(e);
  }
};

exports.config = {
  aliases: [],
  enabled: true
};

exports.help = {
  name: 'removeWhitelistDomain',
  botPermission: '',
  userTextPermission: 'MANAGE_GUILD',
  userVoicePermission: '',
  usage: 'removeWhitelistDomain <index>',
  example: [ 'removeWhitelistDomain 3' ]
};
<commit_msg>Implemented new DB - removeWhitelistDomain command.<commit_after>/**
 * @file removeWhitelistDomain command
 * @author Sankarsan Kampa (a.k.a k3rn31p4nic)
 * @license MIT
 */

exports.exec = async (Bastion, message, args) => {
  try {
    let index = parseInt(args[0]);
    if (!index || index <= 0) {
      /**
      * The command was ran with invalid parameters.
      * @fires commandUsage
      */
      return Bastion.emit('commandUsage', message, this.help);
    }
    index -= 1;

    let guildModel = await Bastion.database.models.guild.findOne({
      attributes: [ 'whitelistedDomains' ],
      where: {
        guildID: message.guild.id
      }
    });

    if (!guildModel || guildModel.dataValues.whitelistedDomains === '[]') {
      /**
      * Error condition is encountered.
      * @fires error
      */
      Bastion.emit('error', Bastion.strings.error(message.guild.language, 'notFound'), Bastion.strings.error(message.guild.language, 'notSet', true, 'whitelist domain'), message.channel);
    }
    else {
      let whitelistDomains = JSON.parse(guildModel.dataValues.whitelistedDomains);

      if (index >= whitelistDomains.length) {
        /**
        * Error condition is encountered.
        * @fires error
        */
        return Bastion.emit('error', Bastion.strings.error(message.guild.language, 'notFound'), Bastion.strings.error(message.guild.language, 'indexRange', true), message.channel);
      }

      let removedDomain = whitelistDomains[parseInt(args[0]) - 1];
      whitelistDomains.splice(parseInt(args[0]) - 1, 1);

      await Bastion.database.models.guild.update({
        whitelistedDomains: JSON.stringify(whitelistDomains)
      },
      {
        where: {
          guildID: message.guild.id
        },
        fields: [ 'whitelistedDomains' ]
      });

      message.channel.send({
        embed: {
          color: Bastion.colors.RED,
          description: `I've deleted **${removedDomain}** from whitelisted domains.`
        }
      }).catch(e => {
        Bastion.log.error(e);
      });
    }
  }
  catch (e) {
    Bastion.log.error(e);
  }
};

exports.config = {
  aliases: [],
  enabled: true
};

exports.help = {
  name: 'removeWhitelistDomain',
  botPermission: '',
  userTextPermission: 'MANAGE_GUILD',
  userVoicePermission: '',
  usage: 'removeWhitelistDomain <index>',
  example: [ 'removeWhitelistDomain 3' ]
};
<|endoftext|>