<commit_before>var express = require(`express`);
var path = require(`path`);
var bodyParser = require(`body-parser`);
var WebSocketServer = require('websocket').server;
var crypto = require('crypto');
var mustache = require('mustache');
var _ = require('lodash');
var fs = require('fs');

var colors = ['red', 'blue', 'yellow', 'green', 'purple', 'white', 'grey', 'orange', 'brown'];

var makeEmptyMap = require('./src/map.js');
var tiles = require('./app/public/js/tiles.js');

var app = express();
// needed for heroku to work right
var PORT = process.env.PORT || 3000;

// All dynamic routes need to be added before any static routes.

function sendResponse(response, responseString) {
    response.set("Connection", "close");
    response.write(responseString);
    response.end();
}

var lastActiveZoneId = 'main';

// This route creates the index page from views/index.js and templates/index.mustache.
app.get('/', (request, response, next) => {
    var indexView = require('./app/views/index.js');
    var markup = mustache.render(indexView.getTemplate(), indexView.getMustacheData(lastActiveZoneId));
    sendResponse(response, markup);
});

app.get('/zones', (request, response, next) => {
    var listZonesView = require('./app/views/listZones.js');
    var markup = mustache.render(listZonesView.getTemplate(), listZonesView.getMustacheData());
    sendResponse(response, markup);
});

var zonesInMemory = {};

var writeZoneToFile = (zoneId, map) => {
    if (!fs.existsSync('data')) fs.mkdirSync('data','0777', true);
    if (!fs.existsSync('data/zones')) fs.mkdirSync('data/zones','0777', true);
    // Make sure we can't just write files infinitely to the disk.
    var files = fs.readdirSync('data/zones');
    if (files.length > 100) {
        return;
    }
    fs.writeFile(`data/zones/${zoneId}.json`, JSON.stringify(map.composite), function(err) {
        if(err) {
            return console.log(err);
        }
    });
};

var readZoneFromFile = zoneId => {
    if (!fs.existsSync(`data/zones/${zoneId}.json`)) {
        return makeEmptyMap();
    }
    var grid = JSON.parse(fs.readFileSync(`data/zones/${zoneId}.json`).toString());
    return {
        objects: [],
        tileSize: 32,
        width: grid[0].length,
        height: grid.length,
        composite: grid
    };
}

// Every ten seconds, write all updated files to disk and mark them as not updated.
setInterval(() => {
    for (var zoneId in zonesInMemory) {
        var map = zonesInMemory[zoneId]
        if (map.isDirty) {
            writeZoneToFile(zoneId, map);
            map.isDirty = false;
        }
    }
}, 10000);

var getZone = zoneId => {
    if (!zonesInMemory[zoneId]) {
        zonesInMemory[zoneId] = readZoneFromFile(zoneId);
        zonesInMemory[zoneId].id = zoneId;
        // Remove zones once we hit 20. This may remove an active zone,
        // but active zones will be reloaded so only the inactive ones
        // will stay culled. This obviously won't work well if more
        // than 20 zones are active at once.
        var allLoadedIds = Object.keys(zonesInMemory);
        while (allLoadedIds.length > 20) {
            var idToRemove = allLoadedIds.shift();
            var map = zonesInMemory[idToRemove];
            // Write zone to file before deleting it from memory.
            if (map.isDirty) {
                writeZoneToFile(idToRemove, map);
            }
            delete zonesInMemory[idToRemove];
        }
    }
    return zonesInMemory[zoneId];
}

app.get('/zones/:zoneId', (request, response, next) => {
    var zoneId = request.params.zoneId;
    if (zoneId.length > 32) {
        return sendResponse(response, 'Zone Id must be less than 32 characters.');
    }
    var indexView = require('./app/views/index.js');
    lastActiveZoneId = zoneId;
    var markup = mustache.render(indexView.getTemplate(), indexView.getMustacheData(zoneId));
    sendResponse(response, markup);
})

// local external files
app.use(express.static('app/public'));

// Sets up the Express app to handle data parsing
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text());
app.use(bodyParser.json({ type: "application/vnd.api+json" }));

// call routing code
// htmlRoutes.htmlRoutes(app, data);

// Starts the server to begin listening
// =============================================================
var server = app.listen(PORT, function() {
  console.log("App listening on PORT " + PORT);
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
    // put logic here to detect whether the specified origin is allowed.
    return true;
}

// Maps privateId => publicId
var privateIdMap = {};
// Maps publicId => playerObject
var players = {};
// Maps privateId => web socket connection (used for broadcasting to all players).
var connections = {};

function broadcast(zoneId, data) {
    for (var id in connections) {
        // Only broadcast messages to players in the same zone.
        var player = players[privateIdMap[id]];
        if (player.zoneId !== zoneId) continue;
        connections[id].sendUTF(JSON.stringify(data));
    }
}

var updatePlayer = (playerId, playerData) => {
    for (var key in playerData) players[playerId][key] = playerData[key];
}

// We track which player is it on the server by setting 'taggedId' for that
// zone. The server only uses this to detect when the currently tagged player
// logs out so it can automatically mark another existing player as it.
var tagPlayer = (id) => {
    var player = players[id];
    if (player) getZone(player.zoneId).taggedId = id;
}

wsServer.on('request', function(request) {
    var privateId, publicId;
    if (!originIsAllowed(request.origin)) {
        // Make sure we only accept requests from an allowed origin
        request.reject();
        console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
        return;
    }

    var connection = request.accept(null, request.origin);
    // console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {
        // We only handle utf8 encoded json messages.
        if (message.type !== 'utf8') return;
        // console.log('Received Message: ' + message.utf8Data);
        try {
            var data = JSON.parse(message.utf8Data);
        } catch (e) {
            connection.sendUTF(JSON.stringify({errorMessage: 'Could not parse json'}));
            return;
        }
        if (!data.privateId) {
            do {
                privateId = crypto.createHash('md5').update('' + Math.random()).digest("hex");
            } while (privateIdMap[privateId]);
            do {
                publicId = crypto.createHash('md5').update('' + Math.random()).digest("hex");
            } while (players[publicId]);
            privateIdMap[privateId] = publicId;
            var playersInZone = _.pickBy(players, player => player.zoneId === data.player.zoneId);
            var usedColors = Object.values(playersInZone).map(player => player.color);

            players[publicId] = {
                id: publicId,
                name: data.player.name || 'Incognito' + publicId.substring(0, 6),
                skin: data.player.skin || 0,
                hair: data.player.hair || 0,
                weapon: data.player.weapon || 'sword',
                x: data.player.x || 200,
                y: data.player.y || 800,
                vx: data.player.vx || 0,
                vy: data.player.vy || 0,
                isCrouching: data.player.isCrouching || false,
                zoneId: data.player.zoneId,
                color: _.sample(_.difference(colors, usedColors)),
            };
            // console.log("Added player");
            // console.log(players);
            // When a player first logs in we send them their public/private ids and the full list of player data for the current zone.
            connection.sendUTF(JSON.stringify({
                privateId, publicId, color: players[publicId].color,
                players: playersInZone,
                map: getZone(data.player.zoneId)
            }));
            // Broadcast to all other players that the new player has joined.
            broadcast(data.player.zoneId, {playerJoined: players[publicId]});
            connections[privateId] = connection;
            return;
        }
        if (privateIdMap[data.privateId]) {
            privateId = data.privateId;
            publicId = privateIdMap[data.privateId];
            player = players[publicId];
            // If a new connection is opened for an existing player, close the
            // old connection and store the new one.
            if (connections[privateId] !== connection) {
                // Close the previous connection if it exists.
                if (connections[privateId]) connections[privateId].close();
                connections[privateId] = connection;
            }
            if (data.action === 'attack') {
                broadcast(player.zoneId, {playerAttacked: publicId});
                // console.log(`Player ${publicId} attacked`);
                return;
            }
            if (data.action === 'move') {
                updatePlayer(publicId, data.player);
                broadcast(player.zoneId, {player: players[publicId]});
                // console.log(`Player ${publicId} moved`);
                return;
            }
            var map = getZone(player.zoneId);
            if (data.action === 'updateTile') {
                tiles.applyTileToMap(map, data.tileData, data.position);
                broadcast(player.zoneId, {tileData: data.tileData, position: data.position});
                map.isDirty = true;
                return;
            }
            if (data.action === 'createMapObject') {
                tiles.applyObjectToMap(map, data.mapObject, data.position);
                broadcast(player.zoneId, {mapObject: data.mapObject, position: data.position});
                map.isDirty = true;
                return;
            }
            if (data.action === 'tagged') {
                tagPlayer(data.id);
                broadcast(player.zoneId, {tagged: data.id});
                return;
            }
            return;
        }
        connection.sendUTF(JSON.stringify({errorMessage: `Player id ${data.privateId} not found.`}));
    });
    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
        // If the most recent connection for a private id closes, purge it from memory
        if (connections[privateId] === connection) {
            var player = players[publicId];
            delete connections[privateId];
            delete players[publicId];
            delete privateIdMap[privateId];
            console.log(`Player ${publicId} left`);
            // console.log(players);
            if (player) {
                var message = {playerLeft: publicId}
                // Tag a random player if the player who is it leaves and there are
                // still at least 2 players left in the zone.
                if (publicId === getZone(player.zoneId).taggedId) {
                    var playersInZone = _.filter(players, {zoneId: player.zoneId});
                    if (playersInZone.length > 1) {
                        var taggedPlayer = _.sample(playersInZone);
                        tagPlayer(taggedPlayer.id);
                        message.tagged = taggedPlayer.id;
                    }
                }
                broadcast(player.zoneId, message);
            }
        }
    });
});
<commit_msg>Use _.values, as node doesn't support Object.values<commit_after>var express = require(`express`);
var path = require(`path`);
var bodyParser = require(`body-parser`);
var WebSocketServer = require('websocket').server;
var crypto = require('crypto');
var mustache = require('mustache');
var _ = require('lodash');
var fs = require('fs');

var colors = ['red', 'blue', 'yellow', 'green', 'purple', 'white', 'grey', 'orange', 'brown'];

var makeEmptyMap = require('./src/map.js');
var tiles = require('./app/public/js/tiles.js');

var app = express();
// needed for heroku to work right
var PORT = process.env.PORT || 3000;

// All dynamic routes need to be added before any static routes.

function sendResponse(response, responseString) {
    response.set("Connection", "close");
    response.write(responseString);
    response.end();
}

var lastActiveZoneId = 'main';

// This route creates the index page from views/index.js and templates/index.mustache.
app.get('/', (request, response, next) => {
    var indexView = require('./app/views/index.js');
    var markup = mustache.render(indexView.getTemplate(), indexView.getMustacheData(lastActiveZoneId));
    sendResponse(response, markup);
});

app.get('/zones', (request, response, next) => {
    var listZonesView = require('./app/views/listZones.js');
    var markup = mustache.render(listZonesView.getTemplate(), listZonesView.getMustacheData());
    sendResponse(response, markup);
});

var zonesInMemory = {};

var writeZoneToFile = (zoneId, map) => {
    if (!fs.existsSync('data')) fs.mkdirSync('data','0777', true);
    if (!fs.existsSync('data/zones')) fs.mkdirSync('data/zones','0777', true);
    // Make sure we can't just write files infinitely to the disk.
    var files = fs.readdirSync('data/zones');
    if (files.length > 100) {
        return;
    }
    fs.writeFile(`data/zones/${zoneId}.json`, JSON.stringify(map.composite), function(err) {
        if(err) {
            return console.log(err);
        }
    });
};

var readZoneFromFile = zoneId => {
    if (!fs.existsSync(`data/zones/${zoneId}.json`)) {
        return makeEmptyMap();
    }
    var grid = JSON.parse(fs.readFileSync(`data/zones/${zoneId}.json`).toString());
    return {
        objects: [],
        tileSize: 32,
        width: grid[0].length,
        height: grid.length,
        composite: grid
    };
}

// Every ten seconds, write all updated files to disk and mark them as not updated.
setInterval(() => {
    for (var zoneId in zonesInMemory) {
        var map = zonesInMemory[zoneId]
        if (map.isDirty) {
            writeZoneToFile(zoneId, map);
            map.isDirty = false;
        }
    }
}, 10000);

var getZone = zoneId => {
    if (!zonesInMemory[zoneId]) {
        zonesInMemory[zoneId] = readZoneFromFile(zoneId);
        zonesInMemory[zoneId].id = zoneId;
        // Remove zones once we hit 20. This may remove an active zone,
        // but active zones will be reloaded so only the inactive ones
        // will stay culled. This obviously won't work well if more
        // than 20 zones are active at once.
        var allLoadedIds = Object.keys(zonesInMemory);
        while (allLoadedIds.length > 20) {
            var idToRemove = allLoadedIds.shift();
            var map = zonesInMemory[idToRemove];
            // Write zone to file before deleting it from memory.
            if (map.isDirty) {
                writeZoneToFile(idToRemove, map);
            }
            delete zonesInMemory[idToRemove];
        }
    }
    return zonesInMemory[zoneId];
}

app.get('/zones/:zoneId', (request, response, next) => {
    var zoneId = request.params.zoneId;
    if (zoneId.length > 32) {
        return sendResponse(response, 'Zone Id must be less than 32 characters.');
    }
    var indexView = require('./app/views/index.js');
    lastActiveZoneId = zoneId;
    var markup = mustache.render(indexView.getTemplate(), indexView.getMustacheData(zoneId));
    sendResponse(response, markup);
})

// local external files
app.use(express.static('app/public'));

// Sets up the Express app to handle data parsing
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text());
app.use(bodyParser.json({ type: "application/vnd.api+json" }));

// call routing code
// htmlRoutes.htmlRoutes(app, data);

// Starts the server to begin listening
// =============================================================
var server = app.listen(PORT, function() {
  console.log("App listening on PORT " + PORT);
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
    // put logic here to detect whether the specified origin is allowed.
    return true;
}

// Maps privateId => publicId
var privateIdMap = {};
// Maps publicId => playerObject
var players = {};
// Maps privateId => web socket connection (used for broadcasting to all players).
var connections = {};

function broadcast(zoneId, data) {
    for (var id in connections) {
        // Only broadcast messages to players in the same zone.
        var player = players[privateIdMap[id]];
        if (player.zoneId !== zoneId) continue;
        connections[id].sendUTF(JSON.stringify(data));
    }
}

var updatePlayer = (playerId, playerData) => {
    for (var key in playerData) players[playerId][key] = playerData[key];
}

// We track which player is it on the server by setting 'taggedId' for that
// zone. The server only uses this to detect when the currently tagged player
// logs out so it can automatically mark another existing player as it.
var tagPlayer = (id) => {
    var player = players[id];
    if (player) getZone(player.zoneId).taggedId = id;
}

wsServer.on('request', function(request) {
    var privateId, publicId;
    if (!originIsAllowed(request.origin)) {
        // Make sure we only accept requests from an allowed origin
        request.reject();
        console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
        return;
    }

    var connection = request.accept(null, request.origin);
    // console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {
        // We only handle utf8 encoded json messages.
        if (message.type !== 'utf8') return;
        // console.log('Received Message: ' + message.utf8Data);
        try {
            var data = JSON.parse(message.utf8Data);
        } catch (e) {
            connection.sendUTF(JSON.stringify({errorMessage: 'Could not parse json'}));
            return;
        }
        if (!data.privateId) {
            do {
                privateId = crypto.createHash('md5').update('' + Math.random()).digest("hex");
            } while (privateIdMap[privateId]);
            do {
                publicId = crypto.createHash('md5').update('' + Math.random()).digest("hex");
            } while (players[publicId]);
            privateIdMap[privateId] = publicId;
            var playersInZone = _.pickBy(players, player => player.zoneId === data.player.zoneId);
            var usedColors = _.values(playersInZone).map(player => player.color);

            players[publicId] = {
                id: publicId,
                name: data.player.name || 'Incognito' + publicId.substring(0, 6),
                skin: data.player.skin || 0,
                hair: data.player.hair || 0,
                weapon: data.player.weapon || 'sword',
                x: data.player.x || 200,
                y: data.player.y || 800,
                vx: data.player.vx || 0,
                vy: data.player.vy || 0,
                isCrouching: data.player.isCrouching || false,
                zoneId: data.player.zoneId,
                color: _.sample(_.difference(colors, usedColors)),
            };
            // console.log("Added player");
            // console.log(players);
            // When a player first logs in we send them their public/private ids and the full list of player data for the current zone.
            connection.sendUTF(JSON.stringify({
                privateId, publicId, color: players[publicId].color,
                players: playersInZone,
                map: getZone(data.player.zoneId)
            }));
            // Broadcast to all other players that the new player has joined.
            broadcast(data.player.zoneId, {playerJoined: players[publicId]});
            connections[privateId] = connection;
            return;
        }
        if (privateIdMap[data.privateId]) {
            privateId = data.privateId;
            publicId = privateIdMap[data.privateId];
            player = players[publicId];
            // If a new connection is opened for an existing player, close the
            // old connection and store the new one.
            if (connections[privateId] !== connection) {
                // Close the previous connection if it exists.
                if (connections[privateId]) connections[privateId].close();
                connections[privateId] = connection;
            }
            if (data.action === 'attack') {
                broadcast(player.zoneId, {playerAttacked: publicId});
                // console.log(`Player ${publicId} attacked`);
                return;
            }
            if (data.action === 'move') {
                updatePlayer(publicId, data.player);
                broadcast(player.zoneId, {player: players[publicId]});
                // console.log(`Player ${publicId} moved`);
                return;
            }
            var map = getZone(player.zoneId);
            if (data.action === 'updateTile') {
                tiles.applyTileToMap(map, data.tileData, data.position);
                broadcast(player.zoneId, {tileData: data.tileData, position: data.position});
                map.isDirty = true;
                return;
            }
            if (data.action === 'createMapObject') {
                tiles.applyObjectToMap(map, data.mapObject, data.position);
                broadcast(player.zoneId, {mapObject: data.mapObject, position: data.position});
                map.isDirty = true;
                return;
            }
            if (data.action === 'tagged') {
                tagPlayer(data.id);
                broadcast(player.zoneId, {tagged: data.id});
                return;
            }
            return;
        }
        connection.sendUTF(JSON.stringify({errorMessage: `Player id ${data.privateId} not found.`}));
    });
    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
        // If the most recent connection for a private id closes, purge it from memory
        if (connections[privateId] === connection) {
            var player = players[publicId];
            delete connections[privateId];
            delete players[publicId];
            delete privateIdMap[privateId];
            console.log(`Player ${publicId} left`);
            // console.log(players);
            if (player) {
                var message = {playerLeft: publicId}
                // Tag a random player if the player who is it leaves and there are
                // still at least 2 players left in the zone.
                if (publicId === getZone(player.zoneId).taggedId) {
                    var playersInZone = _.filter(players, {zoneId: player.zoneId});
                    if (playersInZone.length > 1) {
                        var taggedPlayer = _.sample(playersInZone);
                        tagPlayer(taggedPlayer.id);
                        message.tagged = taggedPlayer.id;
                    }
                }
                broadcast(player.zoneId, message);
            }
        }
    });
});
<|endoftext|>


<commit_before>// Config
var Habitat = require("habitat");
Habitat.load();
var env = new Habitat();
var config = env.get("SERVER");
config.port = env.get("PORT");

var Boom = require("boom");
var Hapi = require("hapi");
var pg = require('pg');
var pgNative = pg.native;

if(pgNative) {
  pg = pgNative;
}

var paypal_total_query = `SELECT SUM(settle_amount)::numeric FROM paypal WHERE timestamp > $1 AND timestamp < $2
                          AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed')
                          OR type = 'Temporary Hold');`;
var stripe_total_query = `SELECT SUM(settle_amount)::numeric FROM stripe WHERE refunded = '0.00'
                          AND status = 'succeeded' AND timestamp > $1 AND timestamp < $2;`;

var bycountry_query = `SELECT country_code, sum(total)::numeric AS total, sum(donors) AS donors FROM (
                       SELECT country_code, sum(amount)::numeric AS total, count(*) AS donors FROM paypal
                       WHERE timestamp > $1 AND timestamp < $2
                       AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed')
                       OR type = 'Temporary Hold') AND country_code IS NOT NULL
                       GROUP BY country_code
                       UNION ALL
                       SELECT country_code, sum(amount)::numeric, count(*) FROM stripe
                       WHERE timestamp > $1 AND timestamp < $2
                       AND refunded = '0.00' AND status = 'succeeded'
                       GROUP BY country_code
                       ) AS bycountry GROUP BY bycountry.country_code ORDER BY bycountry.country_code`;

var byday_query = `SELECT day, SUM(total) AS total FROM
                  (SELECT SUM(settle_amount)::numeric as total, date_trunc('day', timestamp) as day FROM paypal
                    WHERE timestamp > $1 AND timestamp < $2
                    AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed') OR type = 'Temporary Hold')
                    GROUP BY date_trunc('day', timestamp)
                    UNION ALL
                    SELECT
                    SUM(settle_amount)::numeric as total, date_trunc('day', timestamp) as day FROM stripe
                    WHERE timestamp > $1 AND timestamp < $2
                    AND refunded = '0.00' AND status = 'succeeded'
                    GROUP BY date_trunc('day', timestamp)
                  ) as combined GROUP BY day ORDER BY day`;

var server = new Hapi.Server({
  app: {
    stripe_secret: config.stripe_secret
  },
  cache: require("catbox-memory")
});

server.connection({
  host: config.HOST,
  port: config.port
});

server.register(require("hapi-auth-bearer-token"), function(err) {});

  server.auth.strategy("stripe", "bearer-access-token", {
    validateFunc: function(token, callback) {
      // this = request
      callback(null, token === this.server.settings.app.stripe_secret, { token: token });
    },
    accessTokenName: "token"
  });

server.method("total", function(start_date, end_date, next) {
  pg.connect(config.db_connection_string, function(pool_error, client, pg_done) {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(paypal_total_query, [start_date, end_date], function(paypal_error, paypal_result) {
      if (paypal_error) {
        return next(Boom.badImplementation("A paypal database query error occurred", paypal_error));
      }

      client.query(stripe_total_query, [start_date, end_date], function(stripe_error, stripe_result) {
        pg_done();

        if (stripe_error) {
          return next(Boom.badImplementation("A stripe database query error occurred", stripe_error));
        }

        next(null, {
          sum: parseFloat(paypal_result.rows[0].sum, 10) + parseFloat(stripe_result.rows[0].sum, 10)
        });
      });
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.method("total_by_country", (start_date, end_date, next) => {
  pg.connect(config.db_connection_string, (pool_error, client, pg_done) => {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(bycountry_query, [start_date, end_date], (bycountry_error, bycountry_result) => {
      pg_done();

      if (bycountry_error) {
        return next(Boom.badImplementation("A bycountry database query error occurred", bycountry_error));
      }

      next(null, bycountry_result.rows);
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.method("total_by_day", (start_date, end_date, next) => {
  pg.connect(config.db_connection_string, (pool_error, client, pg_done) => {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(byday_query, [start_date, end_date], (byday_error, byday_result) => {
      pg_done();

      if (byday_error) {
        return next(Boom.badImplementation("A byday database query error occurred", byday_error));
      }

      next(null, byday_result.rows);
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.route({
  method: "GET",
  path: "/",
  handler: function(request, reply) {
    reply({
      total_url: `${request.server.info.uri}/eoy-2014-total`,
      total_bycountry_url: `${request.server.info.uri}/eoy-2014-bycountry`,
      total_byday_url: `${request.server.info.uri}/eoy-byday`
    });
  }
});

server.route({
  method: "GET",
  path: "/eoy-2014-bycountry",
  handler: function(request, reply) {
    server.methods.total_by_country(config.start_date, config.end_date, (query_error, results) => {
      reply(query_error, results.map((r) => {
        return {
          country_code: r.country_code,
          sum: parseFloat(r.total, 10),
          count: parseInt(r.donors, 10)
        };
      }));
    });
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

server.route({
  method: "GET",
  path: "/eoy-byday",
  handler: function(request, reply) {
    server.methods.total_by_day(config.start_date, config.end_date, (query_error, results) => {
      reply(query_error, results.map((r) => {
        return {
          day: r.day,
          sum: parseFloat(r.total, 10)
        };
      }));
    });
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

server.route({
  method: "GET",
  path: "/eoy-2014-total",
  handler: function(request, reply) {
    server.methods.total(config.start_date, config.end_date, reply);
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

// set up webhook url as `https://{server_uri}/stripe/callback?token={stripe_secret}` using the stripe dashboard
server.route({
  method: "POST",
  path: "/stripe/callback",
  config: {
    auth: "stripe"
  },
  handler: require("./stripe.js")
});

server.start(function() {
  console.log('Server running at: %s', server.info.uri);
});
<commit_msg>Fix reply interface usage, fix host variable reference<commit_after>// Config
var Habitat = require("habitat");
Habitat.load();
var env = new Habitat();
var config = env.get("SERVER");
config.port = env.get("PORT");

var Boom = require("boom");
var Hapi = require("hapi");
var pg = require('pg');
var pgNative = pg.native;

if(pgNative) {
  pg = pgNative;
}

var paypal_total_query = `SELECT SUM(settle_amount)::numeric FROM paypal WHERE timestamp > $1 AND timestamp < $2
                          AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed')
                          OR type = 'Temporary Hold');`;
var stripe_total_query = `SELECT SUM(settle_amount)::numeric FROM stripe WHERE refunded = '0.00'
                          AND status = 'succeeded' AND timestamp > $1 AND timestamp < $2;`;

var bycountry_query = `SELECT country_code, sum(total)::numeric AS total, sum(donors) AS donors FROM (
                       SELECT country_code, sum(amount)::numeric AS total, count(*) AS donors FROM paypal
                       WHERE timestamp > $1 AND timestamp < $2
                       AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed')
                       OR type = 'Temporary Hold') AND country_code IS NOT NULL
                       GROUP BY country_code
                       UNION ALL
                       SELECT country_code, sum(amount)::numeric, count(*) FROM stripe
                       WHERE timestamp > $1 AND timestamp < $2
                       AND refunded = '0.00' AND status = 'succeeded'
                       GROUP BY country_code
                       ) AS bycountry GROUP BY bycountry.country_code ORDER BY bycountry.country_code`;

var byday_query = `SELECT day, SUM(total) AS total FROM
                  (SELECT SUM(settle_amount)::numeric as total, date_trunc('day', timestamp) as day FROM paypal
                    WHERE timestamp > $1 AND timestamp < $2
                    AND ((type IN ('Donation', 'Payment', 'Recurring Payment') AND status = 'Completed') OR type = 'Temporary Hold')
                    GROUP BY date_trunc('day', timestamp)
                    UNION ALL
                    SELECT
                    SUM(settle_amount)::numeric as total, date_trunc('day', timestamp) as day FROM stripe
                    WHERE timestamp > $1 AND timestamp < $2
                    AND refunded = '0.00' AND status = 'succeeded'
                    GROUP BY date_trunc('day', timestamp)
                  ) as combined GROUP BY day ORDER BY day`;

var server = new Hapi.Server({
  app: {
    stripe_secret: config.stripe_secret
  },
  cache: require("catbox-memory")
});

server.connection({
  host: config.host,
  port: config.port
});

server.register(require("hapi-auth-bearer-token"), function(err) {});

  server.auth.strategy("stripe", "bearer-access-token", {
    validateFunc: function(token, callback) {
      // this = request
      callback(null, token === this.server.settings.app.stripe_secret, { token: token });
    },
    accessTokenName: "token"
  });

server.method("total", function(start_date, end_date, next) {
  pg.connect(config.db_connection_string, function(pool_error, client, pg_done) {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(paypal_total_query, [start_date, end_date], function(paypal_error, paypal_result) {
      if (paypal_error) {
        return next(Boom.badImplementation("A paypal database query error occurred", paypal_error));
      }

      client.query(stripe_total_query, [start_date, end_date], function(stripe_error, stripe_result) {
        pg_done();

        if (stripe_error) {
          return next(Boom.badImplementation("A stripe database query error occurred", stripe_error));
        }

        next(null, {
          sum: parseFloat(paypal_result.rows[0].sum, 10) + parseFloat(stripe_result.rows[0].sum, 10)
        });
      });
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.method("total_by_country", (start_date, end_date, next) => {
  pg.connect(config.db_connection_string, (pool_error, client, pg_done) => {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(bycountry_query, [start_date, end_date], (bycountry_error, bycountry_result) => {
      pg_done();

      if (bycountry_error) {
        return next(Boom.badImplementation("A bycountry database query error occurred", bycountry_error));
      }

      next(null, bycountry_result.rows);
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.method("total_by_day", (start_date, end_date, next) => {
  pg.connect(config.db_connection_string, (pool_error, client, pg_done) => {
    if (pool_error) {
      return next(Boom.badImplementation("A database pool connection error occurred", pool_error));
    }

    client.query(byday_query, [start_date, end_date], (byday_error, byday_result) => {
      pg_done();

      if (byday_error) {
        return next(Boom.badImplementation("A byday database query error occurred", byday_error));
      }

      next(null, byday_result.rows);
    });
  });
}, {
  cache: {
    expiresIn: 10 * 1000,
    generateTimeout: 2 * 1000
  }
});

server.route({
  method: "GET",
  path: "/",
  handler: function(request, reply) {
    reply({
      total_url: `${request.server.info.uri}/eoy-2014-total`,
      total_bycountry_url: `${request.server.info.uri}/eoy-2014-bycountry`,
      total_byday_url: `${request.server.info.uri}/eoy-byday`
    });
  }
});

server.route({
  method: "GET",
  path: "/eoy-2016-bycountry",
  handler: function(request, reply) {
    server.methods.total_by_country(config.start_date, config.end_date, (query_error, results) => {
      if (query_error) {
        return reply(query_error);
      }

      reply(results.map((r) => {
        return {
          country_code: r.country_code,
          sum: parseFloat(r.total, 10),
          count: parseInt(r.donors, 10)
        };
      }));
    });
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

server.route({
  method: "GET",
  path: "/eoy-byday",
  handler: function(request, reply) {
    server.methods.total_by_day(config.start_date, config.end_date, (query_error, results) => {
      if (query_error) {
        return reply(query_error);
      }

      reply(results.map((r) => {
        return {
          day: r.day,
          sum: parseFloat(r.total, 10)
        };
      }));
    });
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

server.route({
  method: "GET",
  path: "/eoy-2016-total",
  handler: function(request, reply) {
    server.methods.total(config.start_date, config.end_date, (query_error, results) => {
      if (query_error) {
        return reply(query_error);
      }

      reply(results);
    });
  },
  config: {
    cors: true,
    jsonp: "callback"
  }
});

// set up webhook url as `https://{server_uri}/stripe/callback?token={stripe_secret}` using the stripe dashboard
server.route({
  method: "POST",
  path: "/stripe/callback",
  config: {
    auth: "stripe"
  },
  handler: require("./stripe.js")
});

server.start(function() {
  console.log('Server running at: %s', server.info.uri);
});
<|endoftext|>


<commit_before>/**
 *  entry point for table
 */

$(function() {

  var Table = cdb.core.View.extend({

    el: document.body,

    events: {
      'keypress': 'keyPress',
      'keyup': 'keyUp'
    },

    initialize: function() {
      this.user = new cdb.admin.User(this.options.user_data);

      _.bindAll(this, "noGeoRefMapMenu", "geoRefMapMenu");

      this._initModels();
      this._initViews();

      // init data
      // table change will raise the map and columns fetch
      this.table.fetch();
      this.keyBind();
    },
    /**
    * Bind the keystrokes associated with menu actions
    * alt + <- : show right menu
    * alt + -> : hide right menu
    * alt + c : toggle carto
    * alt + s : toggle sql
    * @method keyBind
    * TODO: extract to a better place to allow use in all the views
    */
    keyBind: function() {
      var self = this;
      $('body').bind('keydown', function(e) {
        if(e.altKey) {
          //37 39
          if(e.keyCode == 37) { // left arrow
            e.preventDefault();
            e.stopPropagation();
            self.menu.show()
            return false;
          }
          if(e.keyCode == 39) { // right arrow
            e.preventDefault();
            e.stopPropagation();
            self.menu.hide()
            return false;
          }
          if(e.keyCode == 67) { // c key
            e.preventDefault();
            e.stopPropagation();
            self.menu.getButtonByClass('carto_mod').click(e);
            return false;
          }
          if(e.keyCode == 83) { // s key
            e.preventDefault();
            e.stopPropagation();
            self.menu.getButtonByClass('sql_mod').click(e);
            return false;
          }
        }
      });
    },

    appyQuery: function(sql) {
      var self = this;
      self.sqlView.setSQL(sql);
      self.table.useSQLView(self.sqlView);
      // if there is some error the query is rollbacked
      self.sqlView.fetch();
    },

    _initModels: function() {
      var self = this;
      this.table = new cdb.admin.CartoDBTableMetadata({
        id: this.options.table_id
      });
      this.columns = this.table.data();
      this.map = new cdb.admin.Map();
      this.infowindow = new cdb.geo.ui.InfowindowModel({
        template_name: 'table/views/infowindow_light'
      });
      this.sqlView = new cdb.admin.SQLViewData();
      this.geocoder = new cdb.admin.Geocoding('', this.table);

      //TODO: load this from an initial data file or d
      var layers = [
        {
          url: 'http://tile.stamen.com/toner/{z}/{x}/{y}.png',
          maxZoom: 17
        },
        {
          url: 'http://a.tiles.mapbox.com/v3/mapbox.mapbox-light/{z}/{x}/{y}.png',
          maxZoom: 17
        },
        //'http://tile.stamen.com/terrain/{z}/{x}/{y}.png',
        //'http://tile.stamen.com/watercolor/{z}/{x}/{y}.png',
        {
          url: 'http://a.tiles.mapbox.com/v3/mapbox.mapbox-streets/{z}/{x}/{y}.png',
          maxZoom: 17
        }

      ];

      this.baseLayers = this.user.layers;
      _(layers).map(function(m) {
        self.baseLayers.add(new cdb.admin.TileLayer({
          urlTemplate: m.url,
          read_only: true,
          maxZoom: m.maxZoom
        }));
      });

      // fetch or create map id
      this.map.relatedTo(this.table);

      this.map.bind('change:dataLayer', _.once(function() {

        // cache the value to access it in all the view
        self.dataLayer = self.map.get('dataLayer');

        // when the layer query changes the table should change
        self.dataLayer.bind('change:query', function() {
          var sql = this.get('query');
          if(sql) {
            self.appyQuery(sql);
          } else {
            self.table.useSQLView(null);
          }
        });

        // on error rollback the query in the model
        self.sqlView.bind('error', function() {
          self.dataLayer.save({query: null}, {silent: true});
        });

        // on success and no modify rows save the query!
        self.sqlView.bind('reset', function() {
          if(self.sqlView.modify_rows) {
            self.dataLayer.set({query: null}, {silent: true});
          } else {
            self.dataLayer.save(null, {silent: true});
          }
        });

        // trigger the event to ping all views
        self.dataLayer.trigger('change:query');

        var infowindowData = self.dataLayer.get('infowindow');
        if(infowindowData) {
          self.infowindow.set(infowindowData);
        } else {
          // assign a position from start
          var pos = 0;
          _(self.table.get('schema')).each(function(v) {
            self.infowindow.addField(v[0], pos);
            ++pos;
          });
        }

        // check all the fields in the infowindows are in table schema
        // remove the missing ones
        self.table.bind('change:schema', function() {
          var columns = self.table.columnNames();
          columns = _(columns).filter(function(c) {
            return !_.contains(['the_geom', 'created_at', 'updated_at'], c);
          });

          _(self.infowindow.get('fields')).each(function(c) {
            if(!_.contains(columns, c.name)) {
              self.infowindow.removeField(c.name);
            }
          });

        });

        self.infowindow.bind('change:fields change:template_name', _.debounce(function() {
          // call with silent so the layer is no realoded
          // if some view needs to be changed when infowindow is changed it should be
          // subscribed to infowindow model not to dataLayer
          // (which is only a data container for infowindow)
          self.dataLayer.save({ infowindow: self.infowindow.toJSON() }, { silent: true });
        }, 200));

      }));

    },

    _initViews: function() {
      var self = this;

      this.header = new cdb.admin.Header({
        el: this.$('header'),
        model: this.table,
        user: this.user,
        geocoder: this.geocoder
      });
      this.addView(this.header);

      this.tabs = new cdb.admin.Tabs({
        el: this.$('nav')
      });
      this.addView(this.tabs);

      this.workView = new cdb.ui.common.TabPane({
        el: this.$('.panes')
      });

      this.addView(this.workView);

      this.tableTab = new cdb.admin.TableTab({
        model: this.table,
        sqlView: this.sqlView
      });

      this.mapTab = new cdb.admin.MapTab({
        model: this.map,
        baseLayers: this.baseLayers,
        table: this.table,
        infowindow: this.infowindow
      });
      // the .click must be changed for a better way of reference the action. Ask @jsantana
      this.mapTab.bind('manual', function() { self.$('*[href="#table"]').click();})
      this.mapTab.bind('georeference', function() { self.$('.georeference').click();})

      this.globalError = new cdb.admin.GlobalError({
        el: $('.globalerror')
      });

      this.table.bind('notice', this.globalError.showError, this.globalError);

      this.addView(this.globalError);

      // used to show progress on stuff being done
      var bkg_importer = this.bkg_importer = new cdb.ui.common.BackgroundImporter({
        template_base: 'table/views/geocoder_progress',
        import_: this.geocoder
      });
      this.$el.append(this.bkg_importer.render().el);

      this.geocoder.bind('start', function() {
        self.bkg_importer.changeState(self.geocoder);
      });

      this.geocoder.bind('progress', function() {
        self.bkg_importer.render();
        if(self.geocoder.total_connections%10 == 0) {
          //self.dataLayer.trigger('change');
        }
      });

      this.geocoder.bind('finish', function() {
        self.bkg_importer.changeState(self.geocoder);
      });

      this.menu = new cdb.admin.RightMenu({});
      this.$el.append(this.menu.render().el);
      this.menu.hide();
      this.addView(this.menu);

      this.map.bind('change:dataLayer', _.once(function() {

        self.header.setDataLayer(self.dataLayer);

        self.table.bind('change:name', function() {
          self.dataLayer.set({ table_name: self.table.get('name') });
          self.dataLayer.save();
        });

        self.dataLayer.set({
          table_name: self.table.get('name'),
          user_name: self.user.get("username"),
          sql: sql,
          extra_params: {
            map_key: self.user.get('api_key')
          },
          sql_api_domain: cdb.config.get('sql_api_domain'),
          sql_api_endpoint: cdb.config.get('sql_api_endpoint'),
          sql_api_port: cdb.config.get('sql_api_port'),
          tiler_domain: cdb.config.get('tiler_domain'),
          tiler_port: cdb.config.get('tiler_port')
        });

        // set api key when we have api key and the data layer loaded
        var sql = self.table.isInSQLView() ?
                      self.table.data().getSQL() :
                      undefined;

        // lateral menu modules
        var sql = new cdb.admin.mod.SQL({
          model: self.dataLayer,
          sqlView: self.sqlView,
          table: self.table
        });

        sql.bind('writeSQL', self.appyQuery, self);

        var carto = new cdb.admin.mod.Carto({
          model: self.dataLayer,
          table: self.table
        });

        var infowindow = new cdb.admin.mod.InfoWindow({
          table: self.table,
          model: self.infowindow
        });
        self.menu.addModule(sql.render(), ['table', 'map', 'mapLite']);
        self.menu.addModule(carto.render(), 'map');
        self.menu.addModule(infowindow.render(), 'map');

        var addRow = self.menu.addToolButton('add_row', 'table');
        var addColumn = self.menu.addToolButton('add_column', 'table');
        addColumn.bind('click', function() {
            new cdb.admin.NewColumnDialog({
              table: self.table
            }).appendToBody().open();
        });
        addRow.bind('click', function() {
          self.table.data().addRow({ at: 0});
          self.trigger('createRow');
        });

      }));

      this.workView.addTab('table', this.tableTab.render());
      this.workView.addTab('map', this.mapTab.render());
      this.workView.bind('tabEnabled:map', this.mapTab.enableMap, this.mapTab);

      this.workView.bind('tabEnabled', this.tabs.activate);
      this.workView.active('table');

      // global click
      enableClickOut(this.$el);

      this.mapTab.bind('missingClick', function() {
        self.menu.hide();
      });
      this.tableTab.tableView.bind('cellClick', function() {
        self.menu.hide();
      });

      this.mapTab.bind('noGeoRef', this.noGeoRefMapMenu);
      this.mapTab.bind('geoRef', this.geoRefMapMenu)

      // On resize window...
      $(window).bind("resize", this._onResize);

    },

    // Close all dialogs in window resize
    _onResize: function(e) {
      cdb.god.trigger("closeDialogs");
    },

    keyUp: function(e) {
    },

    keyPress: function(e) {
      /*if(e.which == 19) {
        this.menu.show();
        this.menu.active('sql_mod');
        e.preventDefault();
        return false;
      }*/
    },

    noGeoRefMapMenu: function(e) {
      this.menu.hideMapTools();
    },

    geoRefMapMenu: function(e) {
      this.menu.showMapTools();
    }

  });

    cdb._test = cdb._test || {};
    cdb._test.Table = Table;

    var TableRouter = Backbone.Router.extend({

        initialize: function(table) {
          var self = this;
          this.table = table;
          /*
          this.table.table.bind('change:dataSource', function() {
            if(this.isInSQLView()) {
              var sql = this.data().getSQL();
              if(!this.alterTableData(sql)) {
                self.navigate('table/' + encodeURIComponent(sql));
              }
            } else {
              self.navigate('table');
            }
          });
          */
        },

        routes: {
            '': 'index',
            'table': 'index',
            'table/:sql': 'table_sql',
            'map': 'map'
        },

        index: function() {
          this.table.workView.active('table');
          this.table.menu.showTools('table');
        },

        table_sql: function(sql) {
          this.index();
          if(!this.table.table.alterTableData(sql)) {
            this.table.sqlView.setSQL(decodeURIComponent(sql));
            this.table.table.useSQLView(this.table.sqlView);
          }
        },

        map: function() {
          this.table.workView.active('map');
          window.a = this.table;
          if(!this.table.menu.hiddenMapTools) {
            this.table.menu.showTools('map');
          } else {
            this.table.menu.showTools('mapLite');
          }
        }

    });

    cdb.init(function() {
      cdb.config.set(config);
      cdb.templates.namespace = 'cartodb/';
      var table = new Table({
        table_id: table_id,
        user_data: user_data
      });
      var router = new TableRouter(table);
      // expose to debug
      window.table = table;
      Backbone.history.start();
    });

});
<commit_msg>map features loading delayed until the table is loaded to avoid noGeoRef dialog open always<commit_after>/**
 *  entry point for table
 */

$(function() {

  var Table = cdb.core.View.extend({

    el: document.body,

    events: {
      'keypress': 'keyPress',
      'keyup': 'keyUp'
    },

    initialize: function(options) {
      var self = this;
      this.user = new cdb.admin.User(this.options.user_data);

      _.bindAll(this, "noGeoRefMapMenu", "geoRefMapMenu");

      this._initModels();
      this._initViews();

      // init data
      // table change will raise the map and columns fetch

      this.table.bind('change', function() {
        self.initializated = true;
      })
      this.table.fetch()

      this.keyBind();
    },
    /**
    * Bind the keystrokes associated with menu actions
    * alt + <- : show right menu
    * alt + -> : hide right menu
    * alt + c : toggle carto
    * alt + s : toggle sql
    * @method keyBind
    * TODO: extract to a better place to allow use in all the views
    */
    keyBind: function() {
      var self = this;
      $('body').bind('keydown', function(e) {
        if(e.altKey) {
          //37 39
          if(e.keyCode == 37) { // left arrow
            e.preventDefault();
            e.stopPropagation();
            self.menu.show()
            return false;
          }
          if(e.keyCode == 39) { // right arrow
            e.preventDefault();
            e.stopPropagation();
            self.menu.hide()
            return false;
          }
          if(e.keyCode == 67) { // c key
            e.preventDefault();
            e.stopPropagation();
            self.menu.getButtonByClass('carto_mod').click(e);
            return false;
          }
          if(e.keyCode == 83) { // s key
            e.preventDefault();
            e.stopPropagation();
            self.menu.getButtonByClass('sql_mod').click(e);
            return false;
          }
        }
      });
    },

    appyQuery: function(sql) {
      var self = this;
      self.sqlView.setSQL(sql);
      self.table.useSQLView(self.sqlView);
      // if there is some error the query is rollbacked
      self.sqlView.fetch();
    },

    _initModels: function() {
      var self = this;
      this.table = new cdb.admin.CartoDBTableMetadata({
        id: this.options.table_id
      });
      this.columns = this.table.data();
      this.map = new cdb.admin.Map();
      this.infowindow = new cdb.geo.ui.InfowindowModel({
        template_name: 'table/views/infowindow_light'
      });
      this.sqlView = new cdb.admin.SQLViewData();
      this.geocoder = new cdb.admin.Geocoding('', this.table);

      //TODO: load this from an initial data file or d
      var layers = [
        {
          url: 'http://tile.stamen.com/toner/{z}/{x}/{y}.png',
          maxZoom: 17
        },
        {
          url: 'http://a.tiles.mapbox.com/v3/mapbox.mapbox-light/{z}/{x}/{y}.png',
          maxZoom: 17
        },
        //'http://tile.stamen.com/terrain/{z}/{x}/{y}.png',
        //'http://tile.stamen.com/watercolor/{z}/{x}/{y}.png',
        {
          url: 'http://a.tiles.mapbox.com/v3/mapbox.mapbox-streets/{z}/{x}/{y}.png',
          maxZoom: 17
        }

      ];

      this.baseLayers = this.user.layers;
      _(layers).map(function(m) {
        self.baseLayers.add(new cdb.admin.TileLayer({
          urlTemplate: m.url,
          read_only: true,
          maxZoom: m.maxZoom
        }));
      });

      // fetch or create map id
      this.map.relatedTo(this.table);

      this.map.bind('change:dataLayer', _.once(function() {

        // cache the value to access it in all the view
        self.dataLayer = self.map.get('dataLayer');

        // when the layer query changes the table should change
        self.dataLayer.bind('change:query', function() {
          var sql = this.get('query');
          if(sql) {
            self.appyQuery(sql);
          } else {
            self.table.useSQLView(null);
          }
        });

        // on error rollback the query in the model
        self.sqlView.bind('error', function() {
          self.dataLayer.save({query: null}, {silent: true});
        });

        // on success and no modify rows save the query!
        self.sqlView.bind('reset', function() {
          if(self.sqlView.modify_rows) {
            self.dataLayer.set({query: null}, {silent: true});
          } else {
            self.dataLayer.save(null, {silent: true});
          }
        });

        // trigger the event to ping all views
        self.dataLayer.trigger('change:query');

        var infowindowData = self.dataLayer.get('infowindow');
        if(infowindowData) {
          self.infowindow.set(infowindowData);
        } else {
          // assign a position from start
          var pos = 0;
          _(self.table.get('schema')).each(function(v) {
            self.infowindow.addField(v[0], pos);
            ++pos;
          });
        }

        // check all the fields in the infowindows are in table schema
        // remove the missing ones
        self.table.bind('change:schema', function() {
          var columns = self.table.columnNames();
          columns = _(columns).filter(function(c) {
            return !_.contains(['the_geom', 'created_at', 'updated_at'], c);
          });

          _(self.infowindow.get('fields')).each(function(c) {
            if(!_.contains(columns, c.name)) {
              self.infowindow.removeField(c.name);
            }
          });

        });

        self.infowindow.bind('change:fields change:template_name', _.debounce(function() {
          // call with silent so the layer is no realoded
          // if some view needs to be changed when infowindow is changed it should be
          // subscribed to infowindow model not to dataLayer
          // (which is only a data container for infowindow)
          self.dataLayer.save({ infowindow: self.infowindow.toJSON() }, { silent: true });
        }, 200));

      }));

    },

    _initViews: function() {
      var self = this;

      this.header = new cdb.admin.Header({
        el: this.$('header'),
        model: this.table,
        user: this.user,
        geocoder: this.geocoder
      });
      this.addView(this.header);

      this.tabs = new cdb.admin.Tabs({
        el: this.$('nav')
      });
      this.addView(this.tabs);

      this.workView = new cdb.ui.common.TabPane({
        el: this.$('.panes')
      });

      this.addView(this.workView);

      this.tableTab = new cdb.admin.TableTab({
        model: this.table,
        sqlView: this.sqlView
      });

      this.mapTab = new cdb.admin.MapTab({
        model: this.map,
        baseLayers: this.baseLayers,
        table: this.table,
        infowindow: this.infowindow
      });
      // the .click must be changed for a better way of reference the action. Ask @jsantana
      this.mapTab.bind('manual', function() { self.$('*[href="#table"]').click();})
      this.mapTab.bind('georeference', function() { self.$('.georeference').click();})

      this.globalError = new cdb.admin.GlobalError({
        el: $('.globalerror')
      });

      this.table.bind('notice', this.globalError.showError, this.globalError);

      this.addView(this.globalError);

      // used to show progress on stuff being done
      var bkg_importer = this.bkg_importer = new cdb.ui.common.BackgroundImporter({
        template_base: 'table/views/geocoder_progress',
        import_: this.geocoder
      });
      this.$el.append(this.bkg_importer.render().el);

      this.geocoder.bind('start', function() {
        self.bkg_importer.changeState(self.geocoder);
      });

      this.geocoder.bind('progress', function() {
        self.bkg_importer.render();
        if(self.geocoder.total_connections%10 == 0) {
          //self.dataLayer.trigger('change');
        }
      });

      this.geocoder.bind('finish', function() {
        self.bkg_importer.changeState(self.geocoder);
      });

      this.menu = new cdb.admin.RightMenu({});
      this.$el.append(this.menu.render().el);
      this.menu.hide();
      this.addView(this.menu);

      this.map.bind('change:dataLayer', _.once(function() {

        self.header.setDataLayer(self.dataLayer);

        self.table.bind('change:name', function() {
          self.dataLayer.set({ table_name: self.table.get('name') });
          self.dataLayer.save();
        });

        self.dataLayer.set({
          table_name: self.table.get('name'),
          user_name: self.user.get("username"),
          sql: sql,
          extra_params: {
            map_key: self.user.get('api_key')
          },
          sql_api_domain: cdb.config.get('sql_api_domain'),
          sql_api_endpoint: cdb.config.get('sql_api_endpoint'),
          sql_api_port: cdb.config.get('sql_api_port'),
          tiler_domain: cdb.config.get('tiler_domain'),
          tiler_port: cdb.config.get('tiler_port')
        });

        // set api key when we have api key and the data layer loaded
        var sql = self.table.isInSQLView() ?
                      self.table.data().getSQL() :
                      undefined;

        // lateral menu modules
        var sql = new cdb.admin.mod.SQL({
          model: self.dataLayer,
          sqlView: self.sqlView,
          table: self.table
        });

        sql.bind('writeSQL', self.appyQuery, self);

        var carto = new cdb.admin.mod.Carto({
          model: self.dataLayer,
          table: self.table
        });

        var infowindow = new cdb.admin.mod.InfoWindow({
          table: self.table,
          model: self.infowindow
        });
        self.menu.addModule(sql.render(), ['table', 'map', 'mapLite']);
        self.menu.addModule(carto.render(), 'map');
        self.menu.addModule(infowindow.render(), 'map');

        var addRow = self.menu.addToolButton('add_row', 'table');
        var addColumn = self.menu.addToolButton('add_column', 'table');
        addColumn.bind('click', function() {
            new cdb.admin.NewColumnDialog({
              table: self.table
            }).appendToBody().open();
        });
        addRow.bind('click', function() {
          self.table.data().addRow({ at: 0});
          self.trigger('createRow');
        });

      }));

      this.workView.addTab('table', this.tableTab.render());
      this.workView.addTab('map', this.mapTab.render());
      this.workView.bind('tabEnabled:map', this.mapTab.enableMap, this.mapTab);

      this.workView.bind('tabEnabled', this.tabs.activate);
      this.workView.active('table');

      // global click
      enableClickOut(this.$el);

      this.mapTab.bind('missingClick', function() {
        self.menu.hide();
      });
      this.tableTab.tableView.bind('cellClick', function() {
        self.menu.hide();
      });

      this.mapTab.bind('noGeoRef', this.noGeoRefMapMenu);
      this.mapTab.bind('geoRef', this.geoRefMapMenu)

      // On resize window...
      $(window).bind("resize", this._onResize);

    },

    // Close all dialogs in window resize
    _onResize: function(e) {
      cdb.god.trigger("closeDialogs");
    },

    keyUp: function(e) {
    },

    keyPress: function(e) {
      /*if(e.which == 19) {
        this.menu.show();
        this.menu.active('sql_mod');
        e.preventDefault();
        return false;
      }*/
    },

    noGeoRefMapMenu: function(e) {
      this.menu.hideMapTools();
    },

    geoRefMapMenu: function(e) {
      this.menu.showMapTools();
    }

  });

    cdb._test = cdb._test || {};
    cdb._test.Table = Table;

    var TableRouter = Backbone.Router.extend({

        initialize: function(table) {
          var self = this;
          this.table = table;
          /*
          this.table.table.bind('change:dataSource', function() {
            if(this.isInSQLView()) {
              var sql = this.data().getSQL();
              if(!this.alterTableData(sql)) {
                self.navigate('table/' + encodeURIComponent(sql));
              }
            } else {
              self.navigate('table');
            }
          });
          */
        },

        routes: {
            '': 'index',
            'table': 'index',
            'table/:sql': 'table_sql',
            'map': 'map'
        },

        index: function() {
          this.table.workView.active('table');
          this.table.menu.showTools('table');
        },

        table_sql: function(sql) {
          this.index();
          if(!this.table.table.alterTableData(sql)) {
            this.table.sqlView.setSQL(decodeURIComponent(sql));
            this.table.table.useSQLView(this.table.sqlView);
          }
        },

        map: function() {
          this.table.workView.active('map');
          window.a = this.table;
          if(!this.table.menu.hiddenMapTools) {
            this.table.menu.showTools('map');
          } else {
            this.table.menu.showTools('mapLite');
          }
        }

    });

    cdb.init(function() {
      cdb.config.set(config);
      cdb.templates.namespace = 'cartodb/';

      var table = new Table({
        table_id: table_id,
        user_data: user_data
      });
      window.table = table;
      if(table.initializated) {
        var router = new TableRouter(window.table);
        // expose to debug
        Backbone.history.start();
      } else {
        table.table.bind('change', function() {
          var router = new TableRouter(window.table);
          // expose to debug
          Backbone.history.start();
        })
      }
    });

});
<|endoftext|>


<commit_before>import Promise from 'bluebird';

import statesDataset from '../data/states.json!';

export default class FoodRecallService {

  static get $inject() { return ['openFdaService', 'cacheService']; }

  constructor(openFdaService, cacheService) {
    this.openFdaService = openFdaService;
    this.cacheService = cacheService;

    this.EVENT_PROPERTIES = [
      'event_id', 'recall_number', '@epoch', 'reason_for_recall', 'status', 'classification',
      'recalling_firm', 'city', 'state', 'country', 'report_date', 'recall_initiation_date',
      'voluntary_mandated', 'initial_firm_notification', 'distribution_pattern'
    ];

    this.PRODUCT_PROPERTIES = [
      '@id', 'product_quantity', 'product_type', 'product_description', 'code_info'
    ];
  }

  // startDate and endDate should be moment objects
  getRecallsForPeriod(startDate, endDate) {

    // prepare the search criteria dates
    let start = startDate.format('YYYYMMDD');
    let end = endDate.format('YYYYMMDD');

    // See if the data is cached
    let queryCacheType = 'getRecallsForPeriod';
    let queryCacheKey = `${start}-${end}`;
    let cachedResults = this.cacheService.fetch(queryCacheType, queryCacheKey);
    if (cachedResults) {
      return Promise.resolve(cachedResults);
    }

    // Since it wasn't cached, query for it and process the results
    return this.openFdaService
      .query({
        search: 'report_date:[' + start + ' TO ' + end + ']'
      })
      .then(
        (results) => {
          // Process the results
          let processedResults = this.aggregateIntoEvents(results.results);
          // Cache the results
          this.cacheService.store(queryCacheType, queryCacheKey, processedResults);
          return processedResults;
        },
        (error) => {
          if (error.status === 404) {
            return this.processStatesForRecalls([]);
          }
          return Promise.reject(error);
        });
  }

  // aggregate all recalls that share the same event id into a single objects
  aggregateIntoEvents(data) {

    // Build a map of the events
    let eventMap = data.reduce(
      (events, recall) => {
        // See if we have already created this event
        let event = events[recall.event_id];
        if (!event) {
          event = {};
          // Copy over the event specific properties
          this.EVENT_PROPERTIES.forEach((property) => {
            event[property] = recall[property];
          });
          event.distribution_states = this.getDistributionStates(event); // eslint-disable-line camelcase
          event.products = [];
          events[recall.event_id] = event;
        }

        // Copy over the product specific proeprties
        let product = {};
        this.PRODUCT_PROPERTIES.forEach((property) => {
          product[property] = recall[property];
        });

        // Add the product to the recall event
        event.products.push(product);

        return events;
      }, {});

    // Convert the object property map to an array of the values
    let results = Object.keys(eventMap).map(key => eventMap[key]);

    return results;
  }

  // Based on the events distribution_pattern, this method attempts to build
  // an array of state names.
  getDistributionStates(event) {

    let result = [];

    // for each state, see if it matches
    statesDataset.forEach((state) => {
      if (this.appliesToState(state, event)) {
        // if it does, add it to the recall list
        result.push(state.abbr);
      }
    });

    return result;
  }

  // Basic function to check if a recall applies to a state. This will be
  // updated to apply more sophisticated matching.
  appliesToState(state, recall) {

    if (recall.distribution_pattern) {

      // Check the state name
      let nameExpression = new RegExp(`\\b${state.name.replace(' ', '\\s')}\\b`, 'gi');
      // Do a special check for Virginia since west virginia also includes virginia
      if (state.abbr === 'VA') {
        nameExpression = new RegExp(`^(West)\\b${state.name.replace(' ', '\\s')}\\b`, 'gi');
      }
      if (recall.distribution_pattern.match(nameExpression) !== null) {
        return true;
      }

      // Check for the state abbreviation (must be upper case)
      let abbrExpression = new RegExp(`\\b${state.abbr}\\b`, 'g');
      if (recall.distribution_pattern.match(abbrExpression) !== null) {
        return true;
      }

      // Check for the string nationwide (for all states)
      let nationwideExpression = new RegExp(`\\bnationwide\\b`, 'gi');
      if (recall.distribution_pattern.match(nationwideExpression) !== null) {
        return true;
      }
    }

    // Assume it doesn't match
    return false;
  }

}
<commit_msg>Return empty results instead of calling a non-existing method<commit_after>import Promise from 'bluebird';

import statesDataset from '../data/states.json!';

export default class FoodRecallService {

  static get $inject() { return ['openFdaService', 'cacheService']; }

  constructor(openFdaService, cacheService) {
    this.openFdaService = openFdaService;
    this.cacheService = cacheService;

    this.EVENT_PROPERTIES = [
      'event_id', 'recall_number', '@epoch', 'reason_for_recall', 'status', 'classification',
      'recalling_firm', 'city', 'state', 'country', 'report_date', 'recall_initiation_date',
      'voluntary_mandated', 'initial_firm_notification', 'distribution_pattern'
    ];

    this.PRODUCT_PROPERTIES = [
      '@id', 'product_quantity', 'product_type', 'product_description', 'code_info'
    ];
  }

  // startDate and endDate should be moment objects
  getRecallsForPeriod(startDate, endDate) {

    // prepare the search criteria dates
    let start = startDate.format('YYYYMMDD');
    let end = endDate.format('YYYYMMDD');

    // See if the data is cached
    let queryCacheType = 'getRecallsForPeriod';
    let queryCacheKey = `${start}-${end}`;
    let cachedResults = this.cacheService.fetch(queryCacheType, queryCacheKey);
    if (cachedResults) {
      return Promise.resolve(cachedResults);
    }

    // Since it wasn't cached, query for it and process the results
    return this.openFdaService
      .query({
        search: 'report_date:[' + start + ' TO ' + end + ']'
      })
      .then(
        (results) => {
          // Process the results
          let processedResults = this.aggregateIntoEvents(results.results);
          // Cache the results
          this.cacheService.store(queryCacheType, queryCacheKey, processedResults);
          return processedResults;
        },
        (error) => {
          if (error.status === 404) {
            return [];
          }
          return Promise.reject(error);
        });
  }

  // aggregate all recalls that share the same event id into a single objects
  aggregateIntoEvents(data) {

    // Build a map of the events
    let eventMap = data.reduce(
      (events, recall) => {
        // See if we have already created this event
        let event = events[recall.event_id];
        if (!event) {
          event = {};
          // Copy over the event specific properties
          this.EVENT_PROPERTIES.forEach((property) => {
            event[property] = recall[property];
          });
          event.distribution_states = this.getDistributionStates(event); // eslint-disable-line camelcase
          event.products = [];
          events[recall.event_id] = event;
        }

        // Copy over the product specific proeprties
        let product = {};
        this.PRODUCT_PROPERTIES.forEach((property) => {
          product[property] = recall[property];
        });

        // Add the product to the recall event
        event.products.push(product);

        return events;
      }, {});

    // Convert the object property map to an array of the values
    let results = Object.keys(eventMap).map(key => eventMap[key]);

    return results;
  }

  // Based on the events distribution_pattern, this method attempts to build
  // an array of state names.
  getDistributionStates(event) {

    let result = [];

    // for each state, see if it matches
    statesDataset.forEach((state) => {
      if (this.appliesToState(state, event)) {
        // if it does, add it to the recall list
        result.push(state.abbr);
      }
    });

    return result;
  }

  // Basic function to check if a recall applies to a state. This will be
  // updated to apply more sophisticated matching.
  appliesToState(state, recall) {

    if (recall.distribution_pattern) {

      // Check the state name
      let nameExpression = new RegExp(`\\b${state.name.replace(' ', '\\s')}\\b`, 'gi');
      // Do a special check for Virginia since west virginia also includes virginia
      if (state.abbr === 'VA') {
        nameExpression = new RegExp(`^(West)\\b${state.name.replace(' ', '\\s')}\\b`, 'gi');
      }
      if (recall.distribution_pattern.match(nameExpression) !== null) {
        return true;
      }

      // Check for the state abbreviation (must be upper case)
      let abbrExpression = new RegExp(`\\b${state.abbr}\\b`, 'g');
      if (recall.distribution_pattern.match(abbrExpression) !== null) {
        return true;
      }

      // Check for the string nationwide (for all states)
      let nationwideExpression = new RegExp(`\\bnationwide\\b`, 'gi');
      if (recall.distribution_pattern.match(nationwideExpression) !== null) {
        return true;
      }
    }

    // Assume it doesn't match
    return false;
  }

}
<|endoftext|>


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

// Remove all Node warnings before doing anything else
process.removeAllListeners('warning');

const prettyCLI = require('@tryghost/pretty-cli');
const ui = require('@tryghost/pretty-cli').ui;
const _ = require('lodash');
const chalk = require('chalk');
const gscan = require('../lib');
const ghostVersions = require('../lib/utils').versions;

const levels = {
    error: chalk.red,
    warning: chalk.yellow,
    recommendation: chalk.yellow,
    feature: chalk.green
};

const cliOptions = {
    format: 'cli'
};

prettyCLI
    .configure({
        name: 'gscan'
    })
    .groupOrder([
        'Sources:',
        'Utilities:',
        'Commands:',
        'Arguments:',
        'Required Options:',
        'Options:',
        'Global Options:'
    ])
    .positional('<themePath>', {
        paramsDesc: 'Theme folder or .zip file path',
        mustExist: true
    })
    .boolean('-z, --zip', {
        desc: 'Theme path points to a zip file'
    })
    .boolean('-1, --v1', {
        desc: 'Check theme for Ghost 1.0 compatibility'
    })
    .boolean('-2, --v2', {
        desc: 'Check theme for Ghost 2.0 compatibility'
    })
    .boolean('-3, --v3', {
        desc: 'Check theme for Ghost 3.0 compatibility'
    })
    .boolean('-4, --v4', {
        desc: 'Check theme for Ghost 4.0 compatibility'
    })
    .boolean('-c, --canary', {
        desc: 'Check theme for upcoming Ghost version compatibility'
    })
    .boolean('-f, --fatal', {
        desc: 'Only show fatal errors that prevent upgrading Ghost'
    })
    .boolean('--verbose', {
        desc: 'Output check details'
    })
    .parseAndExit()
    .then((argv) => {
        if (argv.v1) {
            cliOptions.checkVersion = 'v1';
        } else if (argv.v2) {
            cliOptions.checkVersion = 'v2';
        } else if (argv.v3) {
            cliOptions.checkVersion = 'v3';
        } else if (argv.v4) {
            cliOptions.checkVersion = 'v4';
        } else if (argv.canary) {
            cliOptions.checkVersion = 'canary';
        } else {
            cliOptions.checkVersion = ghostVersions.default;
        }

        cliOptions.verbose = argv.verbose;
        cliOptions.onlyFatalErrors = argv.fatal;

        if (cliOptions.onlyFatalErrors) {
            ui.log(chalk.bold('\nChecking theme compatibility (fatal issues only)...'));
        } else {
            ui.log(chalk.bold('\nChecking theme compatibility...'));
        }

        if (argv.zip) {
            gscan.checkZip(argv.themePath, cliOptions)
                .then(theme => outputResults(theme, cliOptions))
                .catch((error) => {
                    ui.log(error);
                });
        } else {
            gscan.check(argv.themePath, cliOptions)
                .then(theme => outputResults(theme, cliOptions))
                .catch(function ENOTDIRPredicate(err) {
                    return err.code === 'ENOTDIR';
                }, function (err) {
                    ui.log(err.message);
                    ui.log('Did you mean to add the -z flag to read a zip file?');
                });
        }
    });

function outputResult(result, options) {
    ui.log(levels[result.level](`- ${_.capitalize(result.level)}:`), result.rule);

    if (options.verbose) {
        ui.log(`${chalk.bold('\nDetails:')} ${result.details}`);
    }

    if (result.failures && result.failures.length) {
        if (options.verbose) {
            ui.log(''); // extra line-break
            ui.log(`${chalk.bold('Files:')}`);
            result.failures.forEach((failure) => {
                ui.log(`${failure.ref} - ${failure.message}`);
            });
        } else {
            ui.log(`${chalk.bold('Files:')} ${_.map(result.failures, 'ref')}`);
        }
    }

    ui.log(''); // extra line-break
}

function getSummary(theme, options) {
    let summaryText = '';
    const errorCount = theme.results.error.length;
    const warnCount = theme.results.warning.length;
    const pluralize = require('pluralize');
    const checkSymbol = '\u2713';

    if (errorCount === 0 && warnCount === 0) {
        if (options.onlyFatalErrors) {
            summaryText = `${chalk.green(checkSymbol)} Your theme has no fatal compatibility issues with Ghost ${theme.checkedVersion}`;
        } else {
            summaryText = `${chalk.green(checkSymbol)} Your theme is compatible with Ghost ${theme.checkedVersion}`;
        }
    } else {
        summaryText = `Your theme has`;

        if (!_.isEmpty(theme.results.error)) {
            summaryText += chalk.red.bold(` ${pluralize('error', theme.results.error.length, true)}`);
        }

        if (!_.isEmpty(theme.results.error) && !_.isEmpty(theme.results.warning)) {
            summaryText += ' and';
        }

        if (!_.isEmpty(theme.results.warning)) {
            summaryText += chalk.yellow.bold(` ${pluralize('warning', theme.results.warning.length, true)}`);
        }

        summaryText += '!';

        // NOTE: had to subtract the number of 'invisible' formating symbols
        //       needs update if formatting above changes
        const hiddenSymbols = 38;
        summaryText += '\n' + _.repeat('-', (summaryText.length - hiddenSymbols));
    }

    return summaryText;
}

function outputResults(theme, options) {
    try {
        theme = gscan.format(theme, options);
    } catch (err) {
        ui.log.error('Error formating result, some results may be missing.');
        ui.log.error(err);
    }

    let errorCount = theme.results.error.length;

    ui.log('\n' + getSummary(theme, options));

    if (!_.isEmpty(theme.results.error)) {
        ui.log(chalk.red.bold('\nErrors'));
        ui.log(chalk.red.bold('------'));
        ui.log(chalk.red('Important to fix, functionality may be degraded.\n'));

        _.each(theme.results.error, rule => outputResult(rule, options));
    }

    if (!_.isEmpty(theme.results.warning)) {
        ui.log(chalk.yellow.bold('\nWarnings'));
        ui.log(chalk.yellow.bold('--------'));

        _.each(theme.results.warning, rule => outputResult(rule, options));
    }

    if (!_.isEmpty(theme.results.recommendation)) {
        ui.log(chalk.yellow.bold('\nRecommendations'));
        ui.log(chalk.yellow.bold('---------------'));

        _.each(theme.results.recommendation, rule => outputResult(rule, options));
    }

    ui.log(`\nGet more help at ${chalk.cyan.underline('https://ghost.org/docs/api/handlebars-themes/')}`);
    ui.log(`You can also check theme compatibility at ${chalk.cyan.underline('https://gscan.ghost.org/')}`);

    // The CLI feature is mainly used to run gscan programatically in tests within themes.
    // Exiting with error code for warnings, causes the test to fail, even tho a theme
    // upload via Ghost Admin would be possible without showing errors/warning.
    // See also here: https://github.com/TryGhost/Blog/pull/41#issuecomment-484525754
    // TODO: make failing for warnings configurable by e. g. passing an option, so we can
    // disable it for the usage with tests
    if (errorCount > 0) {
        process.exit(1);
    } else {
        process.exit(0);
    }
}
<commit_msg>Added labs flag support to the CLI<commit_after>#!/usr/bin/env node

// Remove all Node warnings before doing anything else
process.removeAllListeners('warning');

const prettyCLI = require('@tryghost/pretty-cli');
const ui = require('@tryghost/pretty-cli').ui;
const _ = require('lodash');
const chalk = require('chalk');
const gscan = require('../lib');
const ghostVersions = require('../lib/utils').versions;

const levels = {
    error: chalk.red,
    warning: chalk.yellow,
    recommendation: chalk.yellow,
    feature: chalk.green
};

const cliOptions = {
    format: 'cli'
};

prettyCLI
    .configure({
        name: 'gscan'
    })
    .groupOrder([
        'Sources:',
        'Utilities:',
        'Commands:',
        'Arguments:',
        'Required Options:',
        'Options:',
        'Global Options:'
    ])
    .positional('<themePath>', {
        paramsDesc: 'Theme folder or .zip file path',
        mustExist: true
    })
    .boolean('-z, --zip', {
        desc: 'Theme path points to a zip file'
    })
    .boolean('-1, --v1', {
        desc: 'Check theme for Ghost 1.0 compatibility'
    })
    .boolean('-2, --v2', {
        desc: 'Check theme for Ghost 2.0 compatibility'
    })
    .boolean('-3, --v3', {
        desc: 'Check theme for Ghost 3.0 compatibility'
    })
    .boolean('-4, --v4', {
        desc: 'Check theme for Ghost 4.0 compatibility'
    })
    .boolean('-c, --canary', {
        desc: 'Check theme for upcoming Ghost version compatibility'
    })
    .boolean('-f, --fatal', {
        desc: 'Only show fatal errors that prevent upgrading Ghost'
    })
    .boolean('--verbose', {
        desc: 'Output check details'
    })
    .array('--labs', {
        desc: 'a list of labs flags'
    })
    .parseAndExit()
    .then((argv) => {
        if (argv.v1) {
            cliOptions.checkVersion = 'v1';
        } else if (argv.v2) {
            cliOptions.checkVersion = 'v2';
        } else if (argv.v3) {
            cliOptions.checkVersion = 'v3';
        } else if (argv.v4) {
            cliOptions.checkVersion = 'v4';
        } else if (argv.canary) {
            cliOptions.checkVersion = 'canary';
        } else {
            cliOptions.checkVersion = ghostVersions.default;
        }

        cliOptions.verbose = argv.verbose;
        cliOptions.onlyFatalErrors = argv.fatal;

        if (argv.labs) {
            cliOptions.labs = {};

            argv.labs.forEach((flag) => {
                cliOptions.labs[flag] = true;
            });
        }

        if (cliOptions.onlyFatalErrors) {
            ui.log(chalk.bold('\nChecking theme compatibility (fatal issues only)...'));
        } else {
            ui.log(chalk.bold('\nChecking theme compatibility...'));
        }

        if (argv.zip) {
            gscan.checkZip(argv.themePath, cliOptions)
                .then(theme => outputResults(theme, cliOptions))
                .catch((error) => {
                    ui.log(error);
                });
        } else {
            gscan.check(argv.themePath, cliOptions)
                .then(theme => outputResults(theme, cliOptions))
                .catch(function ENOTDIRPredicate(err) {
                    return err.code === 'ENOTDIR';
                }, function (err) {
                    ui.log(err.message);
                    ui.log('Did you mean to add the -z flag to read a zip file?');
                });
        }
    });

function outputResult(result, options) {
    ui.log(levels[result.level](`- ${_.capitalize(result.level)}:`), result.rule);

    if (options.verbose) {
        ui.log(`${chalk.bold('\nDetails:')} ${result.details}`);
    }

    if (result.failures && result.failures.length) {
        if (options.verbose) {
            ui.log(''); // extra line-break
            ui.log(`${chalk.bold('Files:')}`);
            result.failures.forEach((failure) => {
                ui.log(`${failure.ref} - ${failure.message}`);
            });
        } else {
            ui.log(`${chalk.bold('Files:')} ${_.map(result.failures, 'ref')}`);
        }
    }

    ui.log(''); // extra line-break
}

function getSummary(theme, options) {
    let summaryText = '';
    const errorCount = theme.results.error.length;
    const warnCount = theme.results.warning.length;
    const pluralize = require('pluralize');
    const checkSymbol = '\u2713';

    if (errorCount === 0 && warnCount === 0) {
        if (options.onlyFatalErrors) {
            summaryText = `${chalk.green(checkSymbol)} Your theme has no fatal compatibility issues with Ghost ${theme.checkedVersion}`;
        } else {
            summaryText = `${chalk.green(checkSymbol)} Your theme is compatible with Ghost ${theme.checkedVersion}`;
        }
    } else {
        summaryText = `Your theme has`;

        if (!_.isEmpty(theme.results.error)) {
            summaryText += chalk.red.bold(` ${pluralize('error', theme.results.error.length, true)}`);
        }

        if (!_.isEmpty(theme.results.error) && !_.isEmpty(theme.results.warning)) {
            summaryText += ' and';
        }

        if (!_.isEmpty(theme.results.warning)) {
            summaryText += chalk.yellow.bold(` ${pluralize('warning', theme.results.warning.length, true)}`);
        }

        summaryText += '!';

        // NOTE: had to subtract the number of 'invisible' formating symbols
        //       needs update if formatting above changes
        const hiddenSymbols = 38;
        summaryText += '\n' + _.repeat('-', (summaryText.length - hiddenSymbols));
    }

    return summaryText;
}

function outputResults(theme, options) {
    try {
        theme = gscan.format(theme, options);
    } catch (err) {
        ui.log.error('Error formating result, some results may be missing.');
        ui.log.error(err);
    }

    let errorCount = theme.results.error.length;

    ui.log('\n' + getSummary(theme, options));

    if (!_.isEmpty(theme.results.error)) {
        ui.log(chalk.red.bold('\nErrors'));
        ui.log(chalk.red.bold('------'));
        ui.log(chalk.red('Important to fix, functionality may be degraded.\n'));

        _.each(theme.results.error, rule => outputResult(rule, options));
    }

    if (!_.isEmpty(theme.results.warning)) {
        ui.log(chalk.yellow.bold('\nWarnings'));
        ui.log(chalk.yellow.bold('--------'));

        _.each(theme.results.warning, rule => outputResult(rule, options));
    }

    if (!_.isEmpty(theme.results.recommendation)) {
        ui.log(chalk.yellow.bold('\nRecommendations'));
        ui.log(chalk.yellow.bold('---------------'));

        _.each(theme.results.recommendation, rule => outputResult(rule, options));
    }

    ui.log(`\nGet more help at ${chalk.cyan.underline('https://ghost.org/docs/api/handlebars-themes/')}`);
    ui.log(`You can also check theme compatibility at ${chalk.cyan.underline('https://gscan.ghost.org/')}`);

    // The CLI feature is mainly used to run gscan programatically in tests within themes.
    // Exiting with error code for warnings, causes the test to fail, even tho a theme
    // upload via Ghost Admin would be possible without showing errors/warning.
    // See also here: https://github.com/TryGhost/Blog/pull/41#issuecomment-484525754
    // TODO: make failing for warnings configurable by e. g. passing an option, so we can
    // disable it for the usage with tests
    if (errorCount > 0) {
        process.exit(1);
    } else {
        process.exit(0);
    }
}
<|endoftext|>