<commit_before>const path = require('path');
const express = require('express');
const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackConfig = require('../webpack.config.js');
const api = require('./api');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const logger = require('./log.js');
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const mongoskin = require('mongoskin');
const db = mongoskin.db('mongodb://127.0.0.1:27017/throneteki');
const pug = require('pug');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const _ = require('underscore');
const config = require('./config.js');
const crypto = require('crypto');
const raven = require('raven');

const isDeveloping = process.env.NODE_ENV !== 'production';
const port = isDeveloping ? 4000 : process.env.PORT;

const Game = require('./game/game.js');
const Player = require('./game/player.js');
const Spectator = require('./game/spectator.js');
const escapeRegex = require('./util.js').escapeRegex;

var ravenClient = new raven.Client(!isDeveloping && config.sentryDsn);
if(!isDeveloping) {
    ravenClient.patchGlobal();
}

app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(raven.middleware.express.requestHandler(config.sentryDsn));

app.use(session({
    store: new MongoStore({ url: 'mongodb://127.0.0.1:27017/throneteki' }),
    saveUninitialized: false,
    resave: false,
    secret: config.secret,
    cookie: { maxAge: 1000 * 60 * 60 * 24 * 7 } // 7 days
}));
app.use(passport.initialize());
app.use(passport.session());

passport.use(new localStrategy(
    function(username, password, done) {
        db.collection('users').findOne({ username: { '$regex': new RegExp('^' + escapeRegex(username.toLowerCase()), 'i')}},
        function(err, user) {
            if(err) {
                return done(err);
            }

            if(!user) {
                return done(null, false, { message: 'Invalid username/password' });
            }

            bcrypt.compare(password, user.password, function(err, valid) {
                if(err) {
                    return done(err);
                }

                if(!valid) {
                    return done(null, false, { message: 'Invalid username/password' });
                }

                return done(null, { username: user.username, email: user.email, emailHash: user.emailHash, _id: user._id });
            });
        });
    }
));

passport.serializeUser(function(user, done) {
    if(user) {
        done(null, user._id);
    }
});

passport.deserializeUser(function(id, done) {
    db.collection('users').findById(id, function(err, user) {
        if(err) {
            logger.info(err);
        }

        if(!user) {
            return;
        }

        if(!user.emailHash) {
            user.emailHash = crypto.createHash('md5').update(user.email).digest('hex');

            db.collection('users').update({ username: user.username },
                {
                    '$set': {
                        emailHash: user.emailHash
                    }
                });
        }

        done(err, { username: user.username, email: user.email, emailHash: user.emailHash, _id: user._id });
    });
});

api.init(app);

function runServer() {
    if(isDeveloping) {
        const compiler = webpack(webpackConfig);
        const middleware = webpackMiddleware(compiler, {
            hot: true,
            contentBase: 'client',
            publicPath: webpackConfig.output.publicPath,
            stats: {
                colors: true,
                hash: false,
                timings: true,
                chunks: false,
                chunkModules: false,
                modules: false
            },
            historyApiFallback: true
        });

        app.use(middleware);
        app.use(webpackHotMiddleware(compiler, {
            log: logger.info,
            heartbeat: 10 * 1000
        }));
        app.use(express.static(__dirname + '/../public'));
        app.get('*', function response(req, res) {
            var token = undefined;

            if(req.user) {
                token = jwt.sign(req.user, config.secret);
            }

            var html = pug.renderFile('views/index.pug', { basedir: path.join(__dirname, '..', 'views'), user: req.user, token: token });
            middleware.fileSystem.writeFileSync(path.join(__dirname, '..', 'public/index.html'), html);
            res.write(middleware.fileSystem.readFileSync(path.join(__dirname, '..', 'public/index.html')));
            res.end();
        });
    } else {
        app.use(express.static(__dirname + '/../public'));
        app.set('view engine', 'pug');
        app.set('views', path.join(__dirname, '..', 'views'));
        app.get('*', function response(req, res) {
            var token = undefined;

            if(req.user) {
                token = jwt.sign(req.user, config.secret);
            }

            res.render('index', { basedir: path.join(__dirname, '..', 'views'), user: req.user, token: token, production: true });
        });
    }

    server.listen(port, '0.0.0.0', function onStart(err) {
        if(err) {
            logger.error(err);
        }

        logger.info('==> 🌎 Listening on port %s. Open up http://0.0.0.0:%s/ in your browser.', port, port);
    });
}

var games = {};

io.set('heartbeat timeout', 30000);

io.use(function(socket, next) {
    if(socket.handshake.query.token) {
        jwt.verify(socket.handshake.query.token, config.secret, function(err, user) {
            if(!err) {
                socket.request.user = user;
            }
        });
    }

    next();
});

function refreshGameList(socket) {
    var gameSummaries = [];

    _.each(games, game => {
        gameSummaries.push(game.getSummary());
    });

    gameSummaries = _.sortBy(gameSummaries, 'createdAt').reverse();

    if(socket) {
        socket.emit('games', gameSummaries);
    } else {
        io.emit('games', gameSummaries);
    }
}

function refreshUserList(socket) {
    var userList = _.map(users, function(user) {
        return {
            name: user.username,
            emailHash: user.emailHash
        };
    });

    if(socket) {
        socket.emit('users', userList);
    } else {
        io.emit('users', userList);
    }
}

function findGameForPlayer(socketid) {
    var gameToReturn = undefined;
    _.each(games, game => {
        if(game.players[socketid]) {
            gameToReturn = game;
        }

        if(gameToReturn) {
            return;
        }
    });

    return gameToReturn;
}

function sendGameState(game) {
    _.each(game.players, player => {
        io.to(player.id).emit('gamestate', game.getState(player.name));
    });
}

function handleError(game, e) {
    logger.error(e);

    var debugData = {};

    debugData.game = game.getState();

    _.each(game.getPlayers(), player => {
        debugData[player.name] = player.getState(player.name);
    });

    if(!isDeveloping) {
        ravenClient.captureException(e, { extra: debugData });
    }

    if(game) {
        game.addMessage('A Server error has occured processing your game state, apologies.  Your game may now be in an inconsistent state, or you may be able to continue.  The error has been logged.');
    }
}

function runAndCatchErrors(game, func) {
    try {
        func();
    } catch(e) {
        handleError(game, e);

        sendGameState(game);
    }
}

var users = {};

io.on('connection', function(socket) {
    if(socket.request.user) {
        var game = findGameForPlayer(socket.request.user.username);

        if(game) {
            runAndCatchErrors(game, () => {
                if(!game.players[socket.request.user.username].left) {
                    socket.join(game.id);

                    game.players[socket.request.user.disconnected = false];
                    game.reconnect(socket.id, socket.request.user.username);

                    sendGameState(game);
                }
            });
        }
    }

    socket.on('error', function(err) {
        logger.info('socket error', err);
    });

    socket.on('disconnect', function() {
        if(!socket.request.user) {
            return;
        }

        var game = findGameForPlayer(socket.request.user.username);

        delete users[socket.request.user.username];

        refreshUserList();

        if(!game) {
            return;
        }

        game.players[socket.request.user.username].disconnected = true;

        if(_.size(game.getPlayers()) === 1 && _.isEmpty(game.getSpectators()) || _.all(game.players, player => {
            return player.disconnected || player.left;
        })) {
            delete games[game.id];
        }

        game.playerLeave(socket.request.user.username, 'has disconnected');

        refreshGameList();
    });

    socket.on('authenticate', function(token) {
        jwt.verify(token, config.secret, function(err, user) {
            if(!err) {
                socket.request.user = user;
                users[user.username] = user;

                refreshUserList();
            }
        });
    });

    socket.on('newgame', function(gameDetails) {
        if(!socket.request.user) {
            return;
        }

        var game = new Game(socket.request.user.username, gameDetails);

        game.players[socket.request.user.username] = new Player(socket.id, socket.request.user, true, game);

        games[game.id] = game;
        socket.emit('newgame', game.getState(socket.request.user.username));
        socket.join(game.id);

        refreshGameList();
    });

    socket.on('joingame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || game.started || game.getPlayers().length === 2) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.players[socket.request.user.username] = new Player(socket.id, socket.request.user, false, game);
            socket.join(game.id);
        });

        _.each(game.players, player => {
            io.to(player.id).emit('joingame', game.getState(player.name));
        });

        refreshGameList();
    });

    socket.on('watchgame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || !game.allowSpectators) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.players[socket.request.user.username] = new Spectator(socket.id, socket.request.user);
            game.addMessage('{0} has joined the game as a spectator', socket.request.user.username);
            socket.join(game.id);
            _.each(game.players, player => {
                io.to(player.id).emit('joingame', game.getState(player.name));
            });

            sendGameState(game);
        });
    });

    socket.on('selectdeck', function(gameid, deck) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.selectDeck(socket.request.user.username, deck);
        });

        sendGameState(game);
    });

    socket.on('leavegame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = gameid ? games[gameid] : findGameForPlayer(socket.request.user.username);
        if(!game) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.playerLeave(socket.request.user.username, 'has left the game');

            var player = game.players[socket.request.user.username];

            if(!player) {
                return;
            }

            if(game.isSpectator(player)) {
                delete game.players[socket.request.user.username];
                io.to(player.id).emit('gamestate', game.getState(player.name));
            } else {
                game.players[socket.request.user.username].left = true;

                if(game.started && !game.finishedAt) {
                    game.finishedAt = new Date();
                    game.saveGame();
                }
            }

            var listToCheck = game.getPlayers();

            if(_.all(listToCheck, p => {
                return p.left || p.disconnected;
            }) && _.isEmpty(game.getSpectators())) {
                delete games[game.id];
            }

            if(games[game.id]) {
                _.each(game.players, player => {
                    io.to(player.id).emit('gamestate', game.getState(player.name));
                });
            }

            socket.leave(game.id);
        });

        refreshGameList();
    });

    socket.on('startgame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || game.started) {
            return;
        }

        if(_.any(game.getPlayers(), function(player) {
            return !player.deck;
        })) {
            return;
        }

        var player = game.getPlayerByName(socket.request.user.username);
        if(!player || !player.owner) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.started = true;

            game.initialise();
            sendGameState(game);

            refreshGameList();
        });
    });

    socket.on('game', function(command, ...args) {
        if(!socket.request.user) {
            return;
        }

        var game = findGameForPlayer(socket.request.user.username);

        if(!game || (!game.playStarted && command !== 'chat') || !game[command] || !_.isFunction(game[command])) {
            return;
        }

        runAndCatchErrors(game, () => {
            game[command](socket.request.user.username, ...args);

            game.continue();

            sendGameState(game);
        });
    });

    socket.on('lobbychat', function(message) {
        if(!socket.request.user) {
            return;
        }

        var chatMessage = { user: { username: socket.request.user.username, emailHash: socket.request.user.emailHash }, message: message, time: new Date() };
        db.collection('messages').insert(chatMessage);
        io.emit('lobbychat', chatMessage);
    });

    refreshGameList(socket);

    if(socket.request.user) {
        users[socket.request.user.username] = socket.request.user;
    }

    refreshUserList();

    db.collection('messages').find().sort({ time: -1 }).limit(50).toArray((err, messages) => {
        if(err) {
            logger.info(err);
            return;
        }

        socket.emit('lobbymessages', messages.reverse());
    });
});

module.exports = runServer;
<commit_msg>refactor/cleanup the client disconnect/leave code.. bug still exists but this is a good first step to fixing it<commit_after>const path = require('path');
const express = require('express');
const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);
const webpack = require('webpack');
const webpackMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackConfig = require('../webpack.config.js');
const api = require('./api');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const logger = require('./log.js');
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const mongoskin = require('mongoskin');
const db = mongoskin.db('mongodb://127.0.0.1:27017/throneteki');
const pug = require('pug');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const _ = require('underscore');
const config = require('./config.js');
const crypto = require('crypto');
const raven = require('raven');

const isDeveloping = process.env.NODE_ENV !== 'production';
const port = isDeveloping ? 4000 : process.env.PORT;

const Game = require('./game/game.js');
const Player = require('./game/player.js');
const Spectator = require('./game/spectator.js');
const escapeRegex = require('./util.js').escapeRegex;

var ravenClient = new raven.Client(!isDeveloping && config.sentryDsn);
if(!isDeveloping) {
    ravenClient.patchGlobal();
}

app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(raven.middleware.express.requestHandler(config.sentryDsn));

app.use(session({
    store: new MongoStore({ url: 'mongodb://127.0.0.1:27017/throneteki' }),
    saveUninitialized: false,
    resave: false,
    secret: config.secret,
    cookie: { maxAge: 1000 * 60 * 60 * 24 * 7 } // 7 days
}));
app.use(passport.initialize());
app.use(passport.session());

passport.use(new localStrategy(
    function(username, password, done) {
        db.collection('users').findOne({ username: { '$regex': new RegExp('^' + escapeRegex(username.toLowerCase()), 'i')}},
        function(err, user) {
            if(err) {
                return done(err);
            }

            if(!user) {
                return done(null, false, { message: 'Invalid username/password' });
            }

            bcrypt.compare(password, user.password, function(err, valid) {
                if(err) {
                    return done(err);
                }

                if(!valid) {
                    return done(null, false, { message: 'Invalid username/password' });
                }

                return done(null, { username: user.username, email: user.email, emailHash: user.emailHash, _id: user._id });
            });
        });
    }
));

passport.serializeUser(function(user, done) {
    if(user) {
        done(null, user._id);
    }
});

passport.deserializeUser(function(id, done) {
    db.collection('users').findById(id, function(err, user) {
        if(err) {
            logger.info(err);
        }

        if(!user) {
            return;
        }

        if(!user.emailHash) {
            user.emailHash = crypto.createHash('md5').update(user.email).digest('hex');

            db.collection('users').update({ username: user.username },
                {
                    '$set': {
                        emailHash: user.emailHash
                    }
                });
        }

        done(err, { username: user.username, email: user.email, emailHash: user.emailHash, _id: user._id });
    });
});

api.init(app);

function runServer() {
    if(isDeveloping) {
        const compiler = webpack(webpackConfig);
        const middleware = webpackMiddleware(compiler, {
            hot: true,
            contentBase: 'client',
            publicPath: webpackConfig.output.publicPath,
            stats: {
                colors: true,
                hash: false,
                timings: true,
                chunks: false,
                chunkModules: false,
                modules: false
            },
            historyApiFallback: true
        });

        app.use(middleware);
        app.use(webpackHotMiddleware(compiler, {
            log: logger.info,
            heartbeat: 10 * 1000
        }));
        app.use(express.static(__dirname + '/../public'));
        app.get('*', function response(req, res) {
            var token = undefined;

            if(req.user) {
                token = jwt.sign(req.user, config.secret);
            }

            var html = pug.renderFile('views/index.pug', { basedir: path.join(__dirname, '..', 'views'), user: req.user, token: token });
            middleware.fileSystem.writeFileSync(path.join(__dirname, '..', 'public/index.html'), html);
            res.write(middleware.fileSystem.readFileSync(path.join(__dirname, '..', 'public/index.html')));
            res.end();
        });
    } else {
        app.use(express.static(__dirname + '/../public'));
        app.set('view engine', 'pug');
        app.set('views', path.join(__dirname, '..', 'views'));
        app.get('*', function response(req, res) {
            var token = undefined;

            if(req.user) {
                token = jwt.sign(req.user, config.secret);
            }

            res.render('index', { basedir: path.join(__dirname, '..', 'views'), user: req.user, token: token, production: true });
        });
    }

    server.listen(port, '0.0.0.0', function onStart(err) {
        if(err) {
            logger.error(err);
        }

        logger.info('==> 🌎 Listening on port %s. Open up http://0.0.0.0:%s/ in your browser.', port, port);
    });
}

var games = {};

io.set('heartbeat timeout', 30000);

io.use(function(socket, next) {
    if(socket.handshake.query.token) {
        jwt.verify(socket.handshake.query.token, config.secret, function(err, user) {
            if(!err) {
                socket.request.user = user;
            }
        });
    }

    next();
});

function refreshGameList(socket) {
    var gameSummaries = [];

    _.each(games, game => {
        gameSummaries.push(game.getSummary());
    });

    gameSummaries = _.sortBy(gameSummaries, 'createdAt').reverse();

    if(socket) {
        socket.emit('games', gameSummaries);
    } else {
        io.emit('games', gameSummaries);
    }
}

function refreshUserList(socket) {
    var userList = _.map(users, function(user) {
        return {
            name: user.username,
            emailHash: user.emailHash
        };
    });

    if(socket) {
        socket.emit('users', userList);
    } else {
        io.emit('users', userList);
    }
}

function findGameForPlayer(socketid) {
    var gameToReturn = undefined;
    _.each(games, game => {
        if(game.players[socketid]) {
            gameToReturn = game;
        }

        if(gameToReturn) {
            return;
        }
    });

    return gameToReturn;
}

function sendGameState(game) {
    _.each(game.players, player => {
        io.to(player.id).emit('gamestate', game.getState(player.name));
    });
}

function handleError(game, e) {
    logger.error(e);

    var debugData = {};

    debugData.game = game.getState();

    _.each(game.getPlayers(), player => {
        debugData[player.name] = player.getState(player.name);
    });

    if(!isDeveloping) {
        ravenClient.captureException(e, { extra: debugData });
    }

    if(game) {
        game.addMessage('A Server error has occured processing your game state, apologies.  Your game may now be in an inconsistent state, or you may be able to continue.  The error has been logged.');
    }
}

function runAndCatchErrors(game, func) {
    try {
        func();
    } catch(e) {
        handleError(game, e);

        sendGameState(game);
    }
}

var users = {};

io.on('connection', function(socket) {
    if(socket.request.user) {
        var game = findGameForPlayer(socket.request.user.username);

        if(game) {
            runAndCatchErrors(game, () => {
                if(!game.players[socket.request.user.username].left) {
                    socket.join(game.id);

                    game.players[socket.request.user.disconnected = false];
                    game.reconnect(socket.id, socket.request.user.username);

                    sendGameState(game);
                }
            });
        }
    }

    socket.on('error', function(err) {
        logger.info('socket error', err);
    });

    socket.on('disconnect', function() {
        if(!socket.request.user) {
            return;
        }

        var game = findGameForPlayer(socket.request.user.username);

        delete users[socket.request.user.username];

        refreshUserList();

        if(!game) {
            return;
        }

        game.players[socket.request.user.username].disconnected = true;
        game.playerLeave(socket.request.user.username, 'has disconnected');

        if(_.all(game.players, player => {
            return player.disconnected || player.left;
        })) {
            delete games[game.id];
        }

        refreshGameList();
    });

    socket.on('authenticate', function(token) {
        jwt.verify(token, config.secret, function(err, user) {
            if(!err) {
                socket.request.user = user;
                users[user.username] = user;

                refreshUserList();
            }
        });
    });

    socket.on('newgame', function(gameDetails) {
        if(!socket.request.user) {
            return;
        }

        var game = new Game(socket.request.user.username, gameDetails);

        game.players[socket.request.user.username] = new Player(socket.id, socket.request.user, true, game);

        games[game.id] = game;
        socket.emit('newgame', game.getState(socket.request.user.username));
        socket.join(game.id);

        refreshGameList();
    });

    socket.on('joingame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || game.started || game.getPlayers().length === 2) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.players[socket.request.user.username] = new Player(socket.id, socket.request.user, false, game);
            socket.join(game.id);
        });

        _.each(game.players, player => {
            io.to(player.id).emit('joingame', game.getState(player.name));
        });

        refreshGameList();
    });

    socket.on('watchgame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || !game.allowSpectators) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.players[socket.request.user.username] = new Spectator(socket.id, socket.request.user);
            game.addMessage('{0} has joined the game as a spectator', socket.request.user.username);
            socket.join(game.id);
            _.each(game.players, player => {
                io.to(player.id).emit('joingame', game.getState(player.name));
            });

            sendGameState(game);
        });
    });

    socket.on('selectdeck', function(gameid, deck) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.selectDeck(socket.request.user.username, deck);
        });

        sendGameState(game);
    });

    socket.on('leavegame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = gameid ? games[gameid] : findGameForPlayer(socket.request.user.username);
        if(!game) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.playerLeave(socket.request.user.username, 'has left the game');

            var player = game.players[socket.request.user.username];

            if(!player) {
                return;
            }

            if(game.isSpectator(player)) {
                delete game.players[socket.request.user.username];
                io.to(player.id).emit('gamestate', game.getState(player.name));
            } else {
                game.players[socket.request.user.username].left = true;

                if(game.started && !game.finishedAt) {
                    game.finishedAt = new Date();
                    game.saveGame();
                }
            }

            if(_.all(game.players, p => {
                return p.left || p.disconnected;
            }) && _.isEmpty(game.getSpectators())) {
                delete games[game.id];
            }

            if(games[game.id]) {
                sendGameState(game);
            }

            socket.leave(game.id);
        });

        refreshGameList();
    });

    socket.on('startgame', function(gameid) {
        if(!socket.request.user) {
            return;
        }

        var game = games[gameid];

        if(!game || game.started) {
            return;
        }

        if(_.any(game.getPlayers(), function(player) {
            return !player.deck;
        })) {
            return;
        }

        var player = game.getPlayerByName(socket.request.user.username);
        if(!player || !player.owner) {
            return;
        }

        runAndCatchErrors(game, () => {
            game.started = true;

            game.initialise();
            sendGameState(game);

            refreshGameList();
        });
    });

    socket.on('game', function(command, ...args) {
        if(!socket.request.user) {
            return;
        }

        var game = findGameForPlayer(socket.request.user.username);

        if(!game || (!game.playStarted && command !== 'chat') || !game[command] || !_.isFunction(game[command])) {
            return;
        }

        runAndCatchErrors(game, () => {
            game[command](socket.request.user.username, ...args);

            game.continue();

            sendGameState(game);
        });
    });

    socket.on('lobbychat', function(message) {
        if(!socket.request.user) {
            return;
        }

        var chatMessage = { user: { username: socket.request.user.username, emailHash: socket.request.user.emailHash }, message: message, time: new Date() };
        db.collection('messages').insert(chatMessage);
        io.emit('lobbychat', chatMessage);
    });

    refreshGameList(socket);

    if(socket.request.user) {
        users[socket.request.user.username] = socket.request.user;
    }

    refreshUserList();

    db.collection('messages').find().sort({ time: -1 }).limit(50).toArray((err, messages) => {
        if(err) {
            logger.info(err);
            return;
        }

        socket.emit('lobbymessages', messages.reverse());
    });
});

module.exports = runServer;
<|endoftext|>


<commit_before>var currentTestName = "";
var currentSteps = [];
var currentStepIndex = 0;

var testCases = [];
var currentlyExecutingTest = 0;

var testMode = "manual";      // can be "auto"
var isRecursive = false;
var runningRecursive = false;

var snapshotPrefix = "";
var snapshotIndex = 0;

var advanceKey = "n";
var pathSeparator = ".";

TestCase = function (name, path, func) {
    this.name = name;
    this.path = path;
    this.func = func;
}

var currentTestCase = null;
var usePrimaryCamera = false;
var currentRecursiveTestCompleted = false;

//returns n as a string, padded to length characters with the character ch
function pad(n, length, ch) {
  ch = ch || '0';  // default is '0'
  n += '';         // convert n to string

  // returns n as is, if it is too long
  return (n.length >= length) ? n : new Array(length - n.length + 1).join(ch) + n;
}

var runOneStep = function (stepFunctor, stepIndex) {
    print("Running step " + (stepIndex + 1) + "/" + (currentSteps.length) +": " + stepFunctor.name);
    Window.displayAnnouncement("Running step " + (stepIndex + 1) + "/" + (currentSteps.length) +": " + stepFunctor.name);

    if (stepFunctor.func !== undefined) {
        stepFunctor.func();
    }

    // Not quite sure this is the definitive solution here because of the snapshot bug latency issue.
    // but this seems to work ok if the snapshot is a separate step
    if ((stepFunctor.snap !== undefined) && stepFunctor.snap) {
        print("Taking snapshot " + (stepIndex + 1));

        // Image numbers are padded to 5 digits
        // Changing this number requires changing the auto-tester C++ code!
        var NUM_DIGITS = 5;
        var currentSnapshotName = snapshotPrefix + pad(snapshotIndex, NUM_DIGITS, '0');;
        usePrimaryCamera ? Window.takeSnapshot(false, false, 0.0, currentSnapshotName) : Window.takeSecondaryCameraSnapshot(currentSnapshotName);
        ++snapshotIndex;
    }
}

var runNextStep = function () {
    // Run next step and increment only if there is one more
    if (currentStepIndex < currentSteps.length) {
        runOneStep(currentSteps[currentStepIndex], currentStepIndex);
        currentStepIndex++;
    }

    // Return true to go on or false if done
    return (currentStepIndex < currentSteps.length);
}

var testOver = function() {
    if (testMode === "manual") {
        Controller.keyPressEvent.disconnect(onKeyPressEventNextStep);
        Window.displayAnnouncement("Test " + currentTestName + " have been completed");
    }

    currentSteps = [];
    currentStepIndex = 0;
    currentTestName = "";
    currentTestCase = null;

    if (isRecursive) {
        currentRecursiveTestCompleted = true;
    } else {
        // Just stop the script
        Script.stop();
    }
}

var autoTimeStep = 2000;

var onRunAutoNext = function() {
    // run the step...
    if (!runNextStep()) {
        testOver();
        return;
    }

    // and call itself after next timer
    Script.setTimeout(
        onRunAutoNext,
        autoTimeStep
    );
}

var onKeyPressEventNextStep = function (event) {
    if (String.fromCharCode(event.key) == advanceKey.toUpperCase()) {
        if (!runNextStep()) {
            testOver();
        }
    }
}

var onRunManual = function() {
    Window.displayAnnouncement(
        "Ready to run test " + currentTestName + "\n" +
        currentSteps.length + " steps\nPress " + "'" + advanceKey + "'" + " for next steps");

    Controller.keyPressEvent.connect(onKeyPressEventNextStep);
}

var onRunAuto = function() {
    // run the next step after next timer
    Script.setTimeout(
        onRunAutoNext,
        autoTimeStep
    );
}

// Add Steps to the test case
var doAddStep = function (name, stepFunction, snapshot) {
    currentSteps.push({"index": currentSteps.length, "name": name, "func": stepFunction, "snap": snapshot })
    print("PUSHING STEP" + currentSteps.length);
}

// The following are exported methods, accessible to test scripts

// Perform is the main method of a test
//      testName - name of the test
//      testPath - path the test is executing in
//      testMain - a function that creates the test
//
// The method creates a test case in currentTestCase.
// If the test mode is manual or auto then its execution is started
module.exports.perform = function (testName, testPath, testMain) {
    currentTestCase = new TestCase(testName, testPath, testMain);

    // Manual and auto tests are run immediately, recursive tests are stored in a queue
    if (isRecursive) {
        print("Not running yet - in recursive mode");
        testCases.push(currentTestCase);
    } else if (testMode === "manual") {
        print("Begin manual test:" + testName);
        currentTestCase.func("manual");
    } else { // testMode === "auto"
        print("Begin auto test:" + testName);
        currentTestCase.func("auto");
    }
}

module.exports.setupTest = function (primaryCamera) {
    if (currentTestCase === null) {
        return;
    }

    // Clear the test case steps
    currentSteps = [];
    currentStepIndex = 0;
    currentTestName = currentTestCase.name;

    print("Setup test - " + currentTestName);

    // Hide the avatar
    MyAvatar.setEnableMeshVisible(false);

    // Zero the head position
    MyAvatar.bodyYaw =   0.0;
    MyAvatar.bodyPitch = 0.0;
    MyAvatar.bodyRoll =  0.0;
    MyAvatar.headYaw =   0.0;
    MyAvatar.headPitch = 0.0;
    MyAvatar.headRoll =  0.0;

    // resolvePath(".") returns a string that looks like "file:/" + <current folder>
    // We need the current folder
    var path = currentTestCase.path.substring(currentTestCase.path.indexOf(":") + 4);
    var pathParts = path.split("/");

    // Snapshots are saved in the user-selected folder
    // For a test running from D:/GitHub/hifi-tests/tests/content/entity/zone/create/tests.js
    // the tests are named tests.content.entity.zone.create.00000.jpg and so on
    // (assuming pathSeparator is ".")
    // Date and time are not used as part of the name, to keep the path lengths to a minimum
    // (the Windows API limit is 260 characters).
    //
    var testsIndex;
    for (testsIndex = pathParts - 1; pathParts > 0; --pathParts) {
        if (pathParts[testIndex] === "tests") {
            break;
        }
    }

    snapshotPrefix = pathParts[testsIndex];
    for (var i = testsIndex + 1; i < pathParts.length; ++i) {
        snapshotPrefix += pathSeparator + pathParts[i];
    }

    snapshotIndex = 0;

    var spectatorCameraConfig = Render.getConfig("SecondaryCamera");
    spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true);
    spectatorCameraConfig.resetSizeSpectatorCamera(1920, 1080);
    spectatorCameraConfig.vFoV = 45;
    Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 0;
	  Render.getConfig("SecondaryCameraJob.DrawHighlight").enabled = false;

    // Configure the secondary camera
    spectatorCameraConfig.position = {x: MyAvatar.position.x, y: MyAvatar.position.y + 0.6, z: MyAvatar.position.z};
    spectatorCameraConfig.orientation = MyAvatar.orientation;

    usePrimaryCamera = (primaryCamera !== undefined && primaryCamera);

    return spectatorCameraConfig;
}

// Add steps to the test case, take snapshot if 3rd parameter is true
module.exports.addStep = function (name, stepFunction) {
    doAddStep(name, stepFunction, false);
}

// Add steps to the test case, take snapshot
module.exports.addStepSnapshot = function (name, stepFunction) {
    doAddStep(name, stepFunction, true);
}

// The default mode is manual
// The default time between test steps may be modified through these methods
module.exports.enableAuto = function (timeStep) {
    testMode = "auto";
    if (timeStep) {
        autoTimeStep = timeStep;
    }
    print("TEST MODE AUTO SELECTED");
}

module.exports.enableRecursive = function (timeStep) {
    isRecursive = true;
    if (timeStep) {
        autoTimeStep = timeStep;
    }
    print("TEST MODE RECURSIVE SELECTED");
}

// Steps is an array of functions; each function being a test step
module.exports.runTest = function (testType) {
    // In recursive mode, this call is ignored during script load
    if (isRecursive && !runningRecursive) {
        return;
    }

    if (testType  === "auto") {
        onRunAuto();
    } else { // testType === "manual"
        onRunManual();
    }
}

module.exports.runRecursive = function () {
    print("Starting recursive tests");
    runningRecursive = true;

    currentRecursiveTestCompleted = true;
    Script.setInterval(
        function () {
            if (currentRecursiveTestCompleted) {
                currentRecursiveTestCompleted = false;
                if (testCases.length > 0) {
                    currentTestCase = testCases.pop();
                    currentTestCase.func(testMode);
                } else {
                    print("Recursive tests complete");
                    Script.stop();
                }
            }
        },
        1000
    );
}<commit_msg>Typo.<commit_after>var currentTestName = "";
var currentSteps = [];
var currentStepIndex = 0;

var testCases = [];
var currentlyExecutingTest = 0;

var testMode = "manual";      // can be "auto"
var isRecursive = false;
var runningRecursive = false;

var snapshotPrefix = "";
var snapshotIndex = 0;

var advanceKey = "n";
var pathSeparator = ".";

TestCase = function (name, path, func) {
    this.name = name;
    this.path = path;
    this.func = func;
}

var currentTestCase = null;
var usePrimaryCamera = false;
var currentRecursiveTestCompleted = false;

//returns n as a string, padded to length characters with the character ch
function pad(n, length, ch) {
  ch = ch || '0';  // default is '0'
  n += '';         // convert n to string

  // returns n as is, if it is too long
  return (n.length >= length) ? n : new Array(length - n.length + 1).join(ch) + n;
}

var runOneStep = function (stepFunctor, stepIndex) {
    print("Running step " + (stepIndex + 1) + "/" + (currentSteps.length) +": " + stepFunctor.name);
    Window.displayAnnouncement("Running step " + (stepIndex + 1) + "/" + (currentSteps.length) +": " + stepFunctor.name);

    if (stepFunctor.func !== undefined) {
        stepFunctor.func();
    }

    // Not quite sure this is the definitive solution here because of the snapshot bug latency issue.
    // but this seems to work ok if the snapshot is a separate step
    if ((stepFunctor.snap !== undefined) && stepFunctor.snap) {
        print("Taking snapshot " + (stepIndex + 1));

        // Image numbers are padded to 5 digits
        // Changing this number requires changing the auto-tester C++ code!
        var NUM_DIGITS = 5;
        var currentSnapshotName = snapshotPrefix + pad(snapshotIndex, NUM_DIGITS, '0');;
        usePrimaryCamera ? Window.takeSnapshot(false, false, 0.0, currentSnapshotName) : Window.takeSecondaryCameraSnapshot(currentSnapshotName);
        ++snapshotIndex;
    }
}

var runNextStep = function () {
    // Run next step and increment only if there is one more
    if (currentStepIndex < currentSteps.length) {
        runOneStep(currentSteps[currentStepIndex], currentStepIndex);
        currentStepIndex++;
    }

    // Return true to go on or false if done
    return (currentStepIndex < currentSteps.length);
}

var testOver = function() {
    if (testMode === "manual") {
        Controller.keyPressEvent.disconnect(onKeyPressEventNextStep);
        Window.displayAnnouncement("Test " + currentTestName + " have been completed");
    }

    currentSteps = [];
    currentStepIndex = 0;
    currentTestName = "";
    currentTestCase = null;

    if (isRecursive) {
        currentRecursiveTestCompleted = true;
    } else {
        // Just stop the script
        Script.stop();
    }
}

var autoTimeStep = 2000;

var onRunAutoNext = function() {
    // run the step...
    if (!runNextStep()) {
        testOver();
        return;
    }

    // and call itself after next timer
    Script.setTimeout(
        onRunAutoNext,
        autoTimeStep
    );
}

var onKeyPressEventNextStep = function (event) {
    if (String.fromCharCode(event.key) == advanceKey.toUpperCase()) {
        if (!runNextStep()) {
            testOver();
        }
    }
}

var onRunManual = function() {
    Window.displayAnnouncement(
        "Ready to run test " + currentTestName + "\n" +
        currentSteps.length + " steps\nPress " + "'" + advanceKey + "'" + " for next steps");

    Controller.keyPressEvent.connect(onKeyPressEventNextStep);
}

var onRunAuto = function() {
    // run the next step after next timer
    Script.setTimeout(
        onRunAutoNext,
        autoTimeStep
    );
}

// Add Steps to the test case
var doAddStep = function (name, stepFunction, snapshot) {
    currentSteps.push({"index": currentSteps.length, "name": name, "func": stepFunction, "snap": snapshot })
    print("PUSHING STEP" + currentSteps.length);
}

// The following are exported methods, accessible to test scripts

// Perform is the main method of a test
//      testName - name of the test
//      testPath - path the test is executing in
//      testMain - a function that creates the test
//
// The method creates a test case in currentTestCase.
// If the test mode is manual or auto then its execution is started
module.exports.perform = function (testName, testPath, testMain) {
    currentTestCase = new TestCase(testName, testPath, testMain);

    // Manual and auto tests are run immediately, recursive tests are stored in a queue
    if (isRecursive) {
        print("Not running yet - in recursive mode");
        testCases.push(currentTestCase);
    } else if (testMode === "manual") {
        print("Begin manual test:" + testName);
        currentTestCase.func("manual");
    } else { // testMode === "auto"
        print("Begin auto test:" + testName);
        currentTestCase.func("auto");
    }
}

module.exports.setupTest = function (primaryCamera) {
    if (currentTestCase === null) {
        return;
    }

    // Clear the test case steps
    currentSteps = [];
    currentStepIndex = 0;
    currentTestName = currentTestCase.name;

    print("Setup test - " + currentTestName);

    // Hide the avatar
    MyAvatar.setEnableMeshVisible(false);

    // Zero the head position
    MyAvatar.bodyYaw =   0.0;
    MyAvatar.bodyPitch = 0.0;
    MyAvatar.bodyRoll =  0.0;
    MyAvatar.headYaw =   0.0;
    MyAvatar.headPitch = 0.0;
    MyAvatar.headRoll =  0.0;

    // resolvePath(".") returns a string that looks like "file:/" + <current folder>
    // We need the current folder
    var path = currentTestCase.path.substring(currentTestCase.path.indexOf(":") + 4);
    var pathParts = path.split("/");

    // Snapshots are saved in the user-selected folder
    // For a test running from D:/GitHub/hifi-tests/tests/content/entity/zone/create/tests.js
    // the tests are named tests.content.entity.zone.create.00000.jpg and so on
    // (assuming pathSeparator is ".")
    // Date and time are not used as part of the name, to keep the path lengths to a minimum
    // (the Windows API limit is 260 characters).
    //
    // Find location of "tests"
    var testsIndex;
    for (testsIndex = pathParts - 1; pathParts > 0; --pathParts) {
        if (pathParts[testsIndex] === "tests") {
            break;
        }
    }

    snapshotPrefix = pathParts[testsIndex];
    for (var i = testsIndex + 1; i < pathParts.length; ++i) {
        snapshotPrefix += pathSeparator + pathParts[i];
    }

    snapshotIndex = 0;

    var spectatorCameraConfig = Render.getConfig("SecondaryCamera");
    spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true);
    spectatorCameraConfig.resetSizeSpectatorCamera(1920, 1080);
    spectatorCameraConfig.vFoV = 45;
    Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 0;
	  Render.getConfig("SecondaryCameraJob.DrawHighlight").enabled = false;

    // Configure the secondary camera
    spectatorCameraConfig.position = {x: MyAvatar.position.x, y: MyAvatar.position.y + 0.6, z: MyAvatar.position.z};
    spectatorCameraConfig.orientation = MyAvatar.orientation;

    usePrimaryCamera = (primaryCamera !== undefined && primaryCamera);

    return spectatorCameraConfig;
}

// Add steps to the test case, take snapshot if 3rd parameter is true
module.exports.addStep = function (name, stepFunction) {
    doAddStep(name, stepFunction, false);
}

// Add steps to the test case, take snapshot
module.exports.addStepSnapshot = function (name, stepFunction) {
    doAddStep(name, stepFunction, true);
}

// The default mode is manual
// The default time between test steps may be modified through these methods
module.exports.enableAuto = function (timeStep) {
    testMode = "auto";
    if (timeStep) {
        autoTimeStep = timeStep;
    }
    print("TEST MODE AUTO SELECTED");
}

module.exports.enableRecursive = function (timeStep) {
    isRecursive = true;
    if (timeStep) {
        autoTimeStep = timeStep;
    }
    print("TEST MODE RECURSIVE SELECTED");
}

// Steps is an array of functions; each function being a test step
module.exports.runTest = function (testType) {
    // In recursive mode, this call is ignored during script load
    if (isRecursive && !runningRecursive) {
        return;
    }

    if (testType  === "auto") {
        onRunAuto();
    } else { // testType === "manual"
        onRunManual();
    }
}

module.exports.runRecursive = function () {
    print("Starting recursive tests");
    runningRecursive = true;

    currentRecursiveTestCompleted = true;
    Script.setInterval(
        function () {
            if (currentRecursiveTestCompleted) {
                currentRecursiveTestCompleted = false;
                if (testCases.length > 0) {
                    currentTestCase = testCases.pop();
                    currentTestCase.func(testMode);
                } else {
                    print("Recursive tests complete");
                    Script.stop();
                }
            }
        },
        1000
    );
}<|endoftext|>


<commit_before>import Ember from "ember";
import DS from 'ember-data';

var attr = DS.attr,
  belongsTo = DS.belongsTo;

export default DS.Model.extend({
  favourite: attr('boolean'),
  cloudinaryId: attr('string'),
  item: belongsTo('item', { async: false }),
  angle: attr('number'),

  imageUrl: Ember.computed('cloudinaryId', 'angle', function() {
    return this.generateUrl();
  }),

  thumbImageUrl: Ember.computed('cloudinaryId', 'angle', function() {
    return this.generateUrl(120, 120, true);
  }),

  generateUrl: function(width, height, crop) {
    //e.g. cloudinaryId = 1406959628/wjvaksnadntp239n6vwe.png
    var id = this.get('cloudinaryId');
    var angle = this.get('angle') || 0;
    if (!id || id.indexOf("/") === -1) {
      return null;
    }
    var version = id.split("/")[0];
    var filename = id.substring(id.indexOf("/") + 1);
    var options = {
      version: version,
      height: height,
      width: width,
      crop: crop === true ? 'fill' : 'fit',
      flags: "progressive",
      id: id,
      secure: true,
      protocol: 'https:'
    };
    if (angle) { options["angle"] = angle; }
    return Ember.$.cloudinary.url(filename, options);
  }
});
<commit_msg>ItemId and PackageId fields added to image model<commit_after>import Ember from "ember";
import DS from 'ember-data';

var attr = DS.attr,
  belongsTo = DS.belongsTo;

export default DS.Model.extend({
  favourite: attr('boolean'),
  cloudinaryId: attr('string'),
  item: belongsTo('item', { async: false }),
  angle: attr('number'),
  itemId: attr('number'),
  packageId: attr('number'),

  imageUrl: Ember.computed('cloudinaryId', 'angle', function() {
    return this.generateUrl();
  }),

  thumbImageUrl: Ember.computed('cloudinaryId', 'angle', function() {
    return this.generateUrl(120, 120, true);
  }),

  generateUrl: function(width, height, crop) {
    //e.g. cloudinaryId = 1406959628/wjvaksnadntp239n6vwe.png
    var id = this.get('cloudinaryId');
    var angle = this.get('angle') || 0;
    if (!id || id.indexOf("/") === -1) {
      return null;
    }
    var version = id.split("/")[0];
    var filename = id.substring(id.indexOf("/") + 1);
    var options = {
      version: version,
      height: height,
      width: width,
      crop: crop === true ? 'fill' : 'fit',
      flags: "progressive",
      id: id,
      secure: true,
      protocol: 'https:'
    };
    if (angle) { options["angle"] = angle; }
    return Ember.$.cloudinary.url(filename, options);
  }
});
<|endoftext|>


<commit_before>/*!
 * Module dependencies.
 */

var _ = require('underscore'),
	keystone = require('../../'),
	querystring = require('querystring'),
	https = require('https'),
	util = require('util'),
	utils = require('../utils'),
	super_ = require('../field');

var RADIUS_KM = 6371,
	RADIUS_MILES = 3959;

/**
 * Location FieldType Constructor
 * @extends Field
 * @api public
 */

function location(list, path, options) {
	this._underscoreMethods = ['format', 'list', 'googleLookup', 'kmFrom', 'milesFrom'];
	// TODO: implement filtering, hard-coded as disabled for now
	options.nofilter = true;
	this.enableMapsAPI = keystone.get('google api key') ? true : false;
	if (!options.defaults) {
		options.defaults = {};
	}
	if (options.required) {
		if (Array.isArray(options.required)) {
			this.requiredPaths = options.required;
		} else if ('string' == typeof options.required) {
			this.requiredPaths = options.required.split(' ');
		} else {
			this.requiredPaths = ['street1', 'suburb'];
		}
		options.required = true;
	} else {
		this.requiredPaths = [];
	}
	location.super_.call(this, list, path, options);
};

/*!
 * Inherit from Field
 */

util.inherits(location, super_);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * @api public
 */

location.prototype.addToSchema = function() {

	var field = this,
		schema = this.list.schema,
		options = this.options;

	var paths = this.paths = {
		number: this._path.append('.number'),
		name: this._path.append('.name'),
		street1: this._path.append('.street1'),
		street2: this._path.append('.street2'),
		suburb: this._path.append('.suburb'),
		state: this._path.append('.state'),
		postcode: this._path.append('.postcode'),
		country: this._path.append('.country'),
		geo: this._path.append('.geo'),
		geo_lat: this._path.append('.geo_lat'),
		geo_lng: this._path.append('.geo_lng'),
		serialised: this._path.append('.serialised'),
		improve: this._path.append('_improve'),
		overwrite: this._path.append('_improve_overwrite')
	};

	var getFieldDef = function(type, key) {
		var def = { type: type };
		if (options.defaults[key]) {
			def.default = options.defaults[key];
		}
		return def;
	}

	schema.nested[this.path] = true;
	schema.add({
		number: getFieldDef(String, 'number'),
		name: getFieldDef(String, 'name'),
		street1: getFieldDef(String, 'street1'),
		street2: getFieldDef(String, 'street2'),
		street3: getFieldDef(String, 'street3'),
		suburb: getFieldDef(String, 'suburb'),
		state: getFieldDef(String, 'state'),
		postcode: getFieldDef(String, 'postcode'),
		country: getFieldDef(String, 'country'),
		geo: { type: [Number], index: '2dsphere' }
	}, this.path + '.');

	schema.virtual(paths.serialised).get(function() {
		return _.compact([
			this.get(paths.number),
			this.get(paths.name),
			this.get(paths.street1),
			this.get(paths.street2),
			this.get(paths.suburb),
			this.get(paths.state),
			this.get(paths.postcode),
			this.get(paths.country)
		]).join(', ');
	});

	// pre-save hook to fix blank geo fields
	// see http://stackoverflow.com/questions/16388836/does-applying-a-2dsphere-index-on-a-mongoose-schema-force-the-location-field-to
	schema.pre('save', function(next) {
		var obj = field._path.get(this);
		if (Array.isArray(obj.geo) && 0 === obj.geo.length) {
			obj.geo = undefined;
		}
		next();
	});

	this.bindUnderscoreMethods();

}

/**
 * Formats the field value
 *
 * @api public
 */

location.prototype.format = function(item) {
	return item.get(this.paths.serialised);
}

/**
 * Outputs a list of the values of specified sub-paths. Only paths that
 * have values will be included.
 *
 * Sub-paths should be specified as a space-separated list.
 *
 * Delimiter defaults to `', '`.
 *
 * @api public
 */

location.prototype.list = function(item, values, delimiter) {

	var paths = this.paths;

	values = paths.split(' ').map(function(i) {
		return item.get(paths[i]);
	});

	return _.compact(values).join(delimiter || ', ');

}

/**
 * Detects whether the field has been modified
 *
 * @api public
 */

location.prototype.isModified = function(item) {
	return item.isModified(this.paths.number)
		|| item.isModified(this.paths.name)
		|| item.isModified(this.paths.street1)
		|| item.isModified(this.paths.street2)
		|| item.isModified(this.paths.suburb)
		|| item.isModified(this.paths.state)
		|| item.isModified(this.paths.postcode)
		|| item.isModified(this.paths.country)
		|| item.isModified(this.paths.geo);
}

/**
 * Validates that a value for this field has been provided in a data object
 *
 * options.required specifies an array or space-delimited list of paths that
 * are required (defaults to street1, suburb)
 *
 * @api public
 */

location.prototype.validateInput = function(data, required) {
	if (required) {
		var paths = this.paths,
			valid = true;
		this.requiredPaths.forEach(function(path) {
			if (!data[paths[path]]) {
				valid = false;
			}
		});
		return valid;
	} else {
		return true;
	}
}

/**
 * Updates the value for this field in the item from a data object
 *
 * @api public
 */

location.prototype.updateItem = function(item, data) {

	var paths = this.paths;

	var setValue = function(key) {
		if (paths[key] in data && data[paths[key]] != item.get(paths[key])) {
			item.set(paths[key], data[paths[key]] || null);
		}
	}

	_.each(['number', 'name', 'street1', 'street2', 'suburb', 'state', 'postcode', 'country'], setValue);

	if (paths.geo in data) {

		var oldGeo = item.get(paths.geo),
			newGeo = data[paths.geo];

		if (!Array.isArray(newGeo) || newGeo.length != 2) {
			newGeo = [];
		}

		if (newGeo[0] != oldGeo[0] || newGeo[1] != oldGeo[1]) {
			item.set(paths.geo, newGeo);
		}

	} else if (paths.geo_lat in data && paths.geo_lng in data) {

		var lat = utils.number(data[paths.geo_lat]),
			lng = utils.number(data[paths.geo_lng]);

		if (lat && lng) {
			item.set(paths.geo, [lng, lat]);
		}

	}

}

/**
 * Returns a callback that handles a standard form submission for the field
 *
 * Handles:
 * - `field.paths.improve` in `req.body` - improves data via `.googleLookup()`
 * - `field.paths.overwrite` in `req.body` - in conjunction with `improve`, overwrites existing data
 *
 * @api public
 */

location.prototype.getRequestHandler = function(item, req, callback) {

	var field = this;

	callback = callback || function() {};

	return function() {

		var update = req.body[field.paths.overwrite] ? 'overwrite' : true;

		if (req.body && req.body[field.paths.improve]) {
			field.googleLookup(item, false, update, function() {
				callback();
			});
		} else {
			callback();
		}

	}

}

/**
 * Immediately handles a standard form submission for the field (see `getRequestHandler()`)
 *
 * @api public
 */

location.prototype.handleRequest = function(item, req, callback) {
	this.getRequestHandler(item, req, callback)();
}

/**
 * Internal Google geocode request method
 *
 * @api private
 */

function doGoogleGeocodeRequest(address, region, callback) {

	// https://developers.google.com/maps/documentation/geocoding/
	// Use of the Google Geocoding API is subject to a query limit of 2,500 geolocation requests per day, except with an enterprise license.
	// Note: the Geocoding API may only be used in conjunction with a Google map; geocoding results without displaying them on a map is prohibited.
	// Please make sure your Keystone app complies with the Google Maps API License.

	var options = {
		sensor: false,
		language: 'en',
		address: address
	};

	if (arguments.length == 2 && 'function' == typeof region) {
		callback = region;
		region = null;
	}

	if (region) {
		options.region = region;
	}

	var endpoint = 'https://maps.googleapis.com/maps/api/geocode/json?' + querystring.stringify(options);

	https.get(endpoint, function(res) {
		var data = [];
		res.on('data', function(chunk) {
				data.push(chunk);
			})
			.on('end', function() {
				var dataBuff = data.join('').trim();
				var result;
				try {
					result = JSON.parse(dataBuff);
				}
				catch (exp) {
					result = {'status_code': 500, 'status_text': 'JSON Parse Failed', 'status': 'UNKNOWN_ERROR'};
				}
				callback(null, result);
			});
	})
	.on('error', function(err) {
		callback(err);
	});
};

/**
 * Autodetect the full address and lat, lng from the stored value.
 *
 * Uses Google's Maps API and may only be used in conjunction with a Google map.
 * Geocoding results without displaying them on a map is prohibited.
 * Please make sure your Keystone app complies with the Google Maps API License.
 *
 * Internal status codes mimic the Google API status codes.
 *
 * @api private
 */

location.prototype.googleLookup = function(item, region, update, callback) {

	if ('function' == typeof update) {
		callback = update;
		update = false;
	}

	var field = this,
		stored = item.get(this.path),
		address = item.get(this.paths.serialised);

	if (address.length == 0) {
		return callback({'status_code': 500, 'status_text': 'No address to geocode', 'status': 'NO_ADDRESS'});
	}

	doGoogleGeocodeRequest(address, region || keystone.get('default region'), function(err, geocode){

		if (err || geocode.status != 'OK') {
			return callback(err);
		}

		// use the first result
		// if there were no results in the array, status would be ZERO_RESULTS
		var result = geocode.results[0];

		// parse the address components into a location object

		var location = {};

		_.each(result.address_components, function(val,key){
			if ( _.indexOf(val.types,'street_number') >= 0 ) {
				location.street1 = location.street1 || [];
				location.street1.push(val.long_name);
			}
			if ( _.indexOf(val.types,'route') >= 0 ) {
				location.street1 = location.street1 || [];
				location.street1.push(val.short_name);
			}
			// in some cases, you get suburb, city as locality - so only use the first
			if ( _.indexOf(val.types,'locality') >= 0 && !location.suburb) {
				location.suburb = val.long_name;
			}
			if ( _.indexOf(val.types,'administrative_area_level_1') >= 0 ) {
				location.state = val.short_name;
			}
			if ( _.indexOf(val.types,'country') >= 0 ) {
				location.country = val.long_name;
			}
			if ( _.indexOf(val.types,'postal_code') >= 0 ) {
				location.postcode = val.short_name;
			}
		});

		if (Array.isArray(location.street1))
			location.street1 = location.street1.join(' ');

		location.geo = [
			result.geometry.location.lng,
			result.geometry.location.lat
		];

		//console.log('------ Google Geocode Results ------');
		//console.log(address);
		//console.log(result);
		//console.log(location);

		if (update == 'overwrite') {
			item.set(field.path, location);
		} else if (update) {
			_.each(location, function(value, key) {
				if (key == 'geo') {
					return;
				}
				if (!stored[key]) {
					item.set(field.paths[key], value);
				}
			});
			if (!stored.geo[0] || !stored.geo[1]) {
				item.set(field.paths.geo, location.geo);
			}
		}

		callback(null, location, result);

	});
};

/**
 * Internal Distance calculation function
 *
 * See http://en.wikipedia.org/wiki/Haversine_formula
 *
 * @api private
 */

function calculateDistance(point1, point2) {

	var dLng = (point2[0] - point1[0]) * Math.PI / 180;
	var dLat = (point2[1] - point1[1]) * Math.PI / 180;
	var lat1 = (point1[1]) * Math.PI / 180;
	var lat2 = (point2[1]) * Math.PI / 180;

	var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLng/2) * Math.sin(dLng/2) * Math.cos(lat1) * Math.cos(lat2);
	var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

	return c;

}

/**
 * Returns the distance from a [lat,lng] point in kilometres
 *
 * @api public
 */

location.prototype.kmFrom = function(item, point) {
	return calculateDistance(this.get(this.paths.geo), point) * RADIUS_KM;
}

/**
 * Returns the distance from a [lat,lng] point in miles
 *
 * @api public
 */

location.prototype.milesFrom = function(item, point) {
	return calculateDistance(this.get(this.paths.geo), point) * RADIUS_MILES;
}

/*!
 * Export class
 */

exports = module.exports = location;
<commit_msg>Rolling new list method for location fields into format method<commit_after>/*!
 * Module dependencies.
 */

var _ = require('underscore'),
	keystone = require('../../'),
	querystring = require('querystring'),
	https = require('https'),
	util = require('util'),
	utils = require('../utils'),
	super_ = require('../field');

var RADIUS_KM = 6371,
	RADIUS_MILES = 3959;

/**
 * Location FieldType Constructor
 * @extends Field
 * @api public
 */

function location(list, path, options) {
	this._underscoreMethods = ['format', 'googleLookup', 'kmFrom', 'milesFrom'];
	// TODO: implement filtering, hard-coded as disabled for now
	options.nofilter = true;
	this.enableMapsAPI = keystone.get('google api key') ? true : false;
	if (!options.defaults) {
		options.defaults = {};
	}
	if (options.required) {
		if (Array.isArray(options.required)) {
			this.requiredPaths = options.required;
		} else if ('string' == typeof options.required) {
			this.requiredPaths = options.required.split(' ');
		} else {
			this.requiredPaths = ['street1', 'suburb'];
		}
		options.required = true;
	} else {
		this.requiredPaths = [];
	}
	location.super_.call(this, list, path, options);
};

/*!
 * Inherit from Field
 */

util.inherits(location, super_);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * @api public
 */

location.prototype.addToSchema = function() {

	var field = this,
		schema = this.list.schema,
		options = this.options;

	var paths = this.paths = {
		number: this._path.append('.number'),
		name: this._path.append('.name'),
		street1: this._path.append('.street1'),
		street2: this._path.append('.street2'),
		suburb: this._path.append('.suburb'),
		state: this._path.append('.state'),
		postcode: this._path.append('.postcode'),
		country: this._path.append('.country'),
		geo: this._path.append('.geo'),
		geo_lat: this._path.append('.geo_lat'),
		geo_lng: this._path.append('.geo_lng'),
		serialised: this._path.append('.serialised'),
		improve: this._path.append('_improve'),
		overwrite: this._path.append('_improve_overwrite')
	};

	var getFieldDef = function(type, key) {
		var def = { type: type };
		if (options.defaults[key]) {
			def.default = options.defaults[key];
		}
		return def;
	}

	schema.nested[this.path] = true;
	schema.add({
		number: getFieldDef(String, 'number'),
		name: getFieldDef(String, 'name'),
		street1: getFieldDef(String, 'street1'),
		street2: getFieldDef(String, 'street2'),
		street3: getFieldDef(String, 'street3'),
		suburb: getFieldDef(String, 'suburb'),
		state: getFieldDef(String, 'state'),
		postcode: getFieldDef(String, 'postcode'),
		country: getFieldDef(String, 'country'),
		geo: { type: [Number], index: '2dsphere' }
	}, this.path + '.');

	schema.virtual(paths.serialised).get(function() {
		return _.compact([
			this.get(paths.number),
			this.get(paths.name),
			this.get(paths.street1),
			this.get(paths.street2),
			this.get(paths.suburb),
			this.get(paths.state),
			this.get(paths.postcode),
			this.get(paths.country)
		]).join(', ');
	});

	// pre-save hook to fix blank geo fields
	// see http://stackoverflow.com/questions/16388836/does-applying-a-2dsphere-index-on-a-mongoose-schema-force-the-location-field-to
	schema.pre('save', function(next) {
		var obj = field._path.get(this);
		if (Array.isArray(obj.geo) && 0 === obj.geo.length) {
			obj.geo = undefined;
		}
		next();
	});

	this.bindUnderscoreMethods();

}

/**
 * Formats a list of the values stored by the field. Only paths that
 * have values will be included.
 *
 * Optionally provide a space-separated list of values to include.
 *
 * Delimiter defaults to `', '`.
 *
 * @api public
 */

location.prototype.format = function(item, values, delimiter) {

	if (!values) {
		return item.get(this.paths.serialised);
	}

	var paths = this.paths;

	values = values.split(' ').map(function(i) {
		return item.get(paths[i]);
	});

	return _.compact(values).join(delimiter || ', ');

}

/**
 * Detects whether the field has been modified
 *
 * @api public
 */

location.prototype.isModified = function(item) {
	return item.isModified(this.paths.number)
		|| item.isModified(this.paths.name)
		|| item.isModified(this.paths.street1)
		|| item.isModified(this.paths.street2)
		|| item.isModified(this.paths.suburb)
		|| item.isModified(this.paths.state)
		|| item.isModified(this.paths.postcode)
		|| item.isModified(this.paths.country)
		|| item.isModified(this.paths.geo);
}

/**
 * Validates that a value for this field has been provided in a data object
 *
 * options.required specifies an array or space-delimited list of paths that
 * are required (defaults to street1, suburb)
 *
 * @api public
 */

location.prototype.validateInput = function(data, required) {
	if (required) {
		var paths = this.paths,
			valid = true;
		this.requiredPaths.forEach(function(path) {
			if (!data[paths[path]]) {
				valid = false;
			}
		});
		return valid;
	} else {
		return true;
	}
}

/**
 * Updates the value for this field in the item from a data object
 *
 * @api public
 */

location.prototype.updateItem = function(item, data) {

	var paths = this.paths;

	var setValue = function(key) {
		if (paths[key] in data && data[paths[key]] != item.get(paths[key])) {
			item.set(paths[key], data[paths[key]] || null);
		}
	}

	_.each(['number', 'name', 'street1', 'street2', 'suburb', 'state', 'postcode', 'country'], setValue);

	if (paths.geo in data) {

		var oldGeo = item.get(paths.geo),
			newGeo = data[paths.geo];

		if (!Array.isArray(newGeo) || newGeo.length != 2) {
			newGeo = [];
		}

		if (newGeo[0] != oldGeo[0] || newGeo[1] != oldGeo[1]) {
			item.set(paths.geo, newGeo);
		}

	} else if (paths.geo_lat in data && paths.geo_lng in data) {

		var lat = utils.number(data[paths.geo_lat]),
			lng = utils.number(data[paths.geo_lng]);

		if (lat && lng) {
			item.set(paths.geo, [lng, lat]);
		}

	}

}

/**
 * Returns a callback that handles a standard form submission for the field
 *
 * Handles:
 * - `field.paths.improve` in `req.body` - improves data via `.googleLookup()`
 * - `field.paths.overwrite` in `req.body` - in conjunction with `improve`, overwrites existing data
 *
 * @api public
 */

location.prototype.getRequestHandler = function(item, req, callback) {

	var field = this;

	callback = callback || function() {};

	return function() {

		var update = req.body[field.paths.overwrite] ? 'overwrite' : true;

		if (req.body && req.body[field.paths.improve]) {
			field.googleLookup(item, false, update, function() {
				callback();
			});
		} else {
			callback();
		}

	}

}

/**
 * Immediately handles a standard form submission for the field (see `getRequestHandler()`)
 *
 * @api public
 */

location.prototype.handleRequest = function(item, req, callback) {
	this.getRequestHandler(item, req, callback)();
}

/**
 * Internal Google geocode request method
 *
 * @api private
 */

function doGoogleGeocodeRequest(address, region, callback) {

	// https://developers.google.com/maps/documentation/geocoding/
	// Use of the Google Geocoding API is subject to a query limit of 2,500 geolocation requests per day, except with an enterprise license.
	// Note: the Geocoding API may only be used in conjunction with a Google map; geocoding results without displaying them on a map is prohibited.
	// Please make sure your Keystone app complies with the Google Maps API License.

	var options = {
		sensor: false,
		language: 'en',
		address: address
	};

	if (arguments.length == 2 && 'function' == typeof region) {
		callback = region;
		region = null;
	}

	if (region) {
		options.region = region;
	}

	var endpoint = 'https://maps.googleapis.com/maps/api/geocode/json?' + querystring.stringify(options);

	https.get(endpoint, function(res) {
		var data = [];
		res.on('data', function(chunk) {
				data.push(chunk);
			})
			.on('end', function() {
				var dataBuff = data.join('').trim();
				var result;
				try {
					result = JSON.parse(dataBuff);
				}
				catch (exp) {
					result = {'status_code': 500, 'status_text': 'JSON Parse Failed', 'status': 'UNKNOWN_ERROR'};
				}
				callback(null, result);
			});
	})
	.on('error', function(err) {
		callback(err);
	});
};

/**
 * Autodetect the full address and lat, lng from the stored value.
 *
 * Uses Google's Maps API and may only be used in conjunction with a Google map.
 * Geocoding results without displaying them on a map is prohibited.
 * Please make sure your Keystone app complies with the Google Maps API License.
 *
 * Internal status codes mimic the Google API status codes.
 *
 * @api private
 */

location.prototype.googleLookup = function(item, region, update, callback) {

	if ('function' == typeof update) {
		callback = update;
		update = false;
	}

	var field = this,
		stored = item.get(this.path),
		address = item.get(this.paths.serialised);

	if (address.length == 0) {
		return callback({'status_code': 500, 'status_text': 'No address to geocode', 'status': 'NO_ADDRESS'});
	}

	doGoogleGeocodeRequest(address, region || keystone.get('default region'), function(err, geocode){

		if (err || geocode.status != 'OK') {
			return callback(err);
		}

		// use the first result
		// if there were no results in the array, status would be ZERO_RESULTS
		var result = geocode.results[0];

		// parse the address components into a location object

		var location = {};

		_.each(result.address_components, function(val,key){
			if ( _.indexOf(val.types,'street_number') >= 0 ) {
				location.street1 = location.street1 || [];
				location.street1.push(val.long_name);
			}
			if ( _.indexOf(val.types,'route') >= 0 ) {
				location.street1 = location.street1 || [];
				location.street1.push(val.short_name);
			}
			// in some cases, you get suburb, city as locality - so only use the first
			if ( _.indexOf(val.types,'locality') >= 0 && !location.suburb) {
				location.suburb = val.long_name;
			}
			if ( _.indexOf(val.types,'administrative_area_level_1') >= 0 ) {
				location.state = val.short_name;
			}
			if ( _.indexOf(val.types,'country') >= 0 ) {
				location.country = val.long_name;
			}
			if ( _.indexOf(val.types,'postal_code') >= 0 ) {
				location.postcode = val.short_name;
			}
		});

		if (Array.isArray(location.street1))
			location.street1 = location.street1.join(' ');

		location.geo = [
			result.geometry.location.lng,
			result.geometry.location.lat
		];

		//console.log('------ Google Geocode Results ------');
		//console.log(address);
		//console.log(result);
		//console.log(location);

		if (update == 'overwrite') {
			item.set(field.path, location);
		} else if (update) {
			_.each(location, function(value, key) {
				if (key == 'geo') {
					return;
				}
				if (!stored[key]) {
					item.set(field.paths[key], value);
				}
			});
			if (!stored.geo[0] || !stored.geo[1]) {
				item.set(field.paths.geo, location.geo);
			}
		}

		callback(null, location, result);

	});
};

/**
 * Internal Distance calculation function
 *
 * See http://en.wikipedia.org/wiki/Haversine_formula
 *
 * @api private
 */

function calculateDistance(point1, point2) {

	var dLng = (point2[0] - point1[0]) * Math.PI / 180;
	var dLat = (point2[1] - point1[1]) * Math.PI / 180;
	var lat1 = (point1[1]) * Math.PI / 180;
	var lat2 = (point2[1]) * Math.PI / 180;

	var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLng/2) * Math.sin(dLng/2) * Math.cos(lat1) * Math.cos(lat2);
	var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

	return c;

}

/**
 * Returns the distance from a [lat,lng] point in kilometres
 *
 * @api public
 */

location.prototype.kmFrom = function(item, point) {
	return calculateDistance(this.get(this.paths.geo), point) * RADIUS_KM;
}

/**
 * Returns the distance from a [lat,lng] point in miles
 *
 * @api public
 */

location.prototype.milesFrom = function(item, point) {
	return calculateDistance(this.get(this.paths.geo), point) * RADIUS_MILES;
}

/*!
 * Export class
 */

exports = module.exports = location;
<|endoftext|>


<commit_before>import Component from '@ember/component';
import { computed } from 'ember-decorators/object';
import { alias, equal } from 'ember-decorators/object/computed';

import moment from 'moment';

const NOVEMBER_2017_RETIREMENT = '2017-11-28T12:00:00-08:00';
const LATEST_TRUSTY_RELEASE = '2017-12-10T00:00:00-00:00';

export default Component.extend({
  @alias('job.queue') queue: null,
  @alias('job.config') jobConfig: null,

  @computed('job.isFinished')
  conjugatedRun(isFinished) {
    return isFinished ? 'ran' : 'is running';
  },

  @equal('queue', 'builds.linux') isLegacyInfrastructure: null,
  @equal('queue', 'builds.ec2') isTrustySudoFalse: null,

  @computed('job.startedAt', 'queue', 'job.config.dist')
  isTrustySudoRequired(startedAt, queue, dist) {
    if (queue === 'builds.gce' && dist === 'trusty') {
      console.log('!!!! Before date condition.');
      const jobRanAfterReleaseDate = Date.parse(startedAt) > Date.parse(LATEST_TRUSTY_RELEASE);
      if (jobRanAfterReleaseDate) {
        console.log('!!!!! Ran after release date!');
        return true;
      }
    }

    console.log('!!!!! Ran before release date!');
    return false;
  },

  @equal('queue', 'builds.macstadium6') isMacStadium6: null,

  @computed('queue', 'job.config')
  isPreciseEOL(queue, config = {}) {
    if (queue === 'builds.gce' && config.dist === 'precise') {
      if (config.language !== 'android') {
        return true;
      }
    }
  },

  @alias('jobConfig.osx_image') macOSImage: null,

  deprecatedXcodeImages: ['xcode8', 'xcode8.1', 'xcode8.2'],

  imageToRetirementDate: {
    'xcode8': NOVEMBER_2017_RETIREMENT,
    'xcode8.1': NOVEMBER_2017_RETIREMENT,
    'xcode8.2': NOVEMBER_2017_RETIREMENT
  },

  imageToNewImage: {
    'xcode8': 'xcode8.3',
    'xcode8.1': 'xcode8.3',
    'xcode8.2': 'xcode8.3'
  },

  newImageStrings: {
    'xcode8.3': 'Xcode 8.3',
    'xcode7.3': 'Xcode 7.3.1',
    'xcode6.4': 'Xcode 6.4'
  },

  @computed('isMacStadium6', 'macOSImage')
  isDeprecatedOrRetiredMacImage(isMacStadium6, macOSImage) {
    return isMacStadium6 && this.get('deprecatedXcodeImages').includes(macOSImage);
  },

  // eslint-disable-next-line
  @computed('job.startedAt', 'macOSImage', 'job.isFinished', 'conjugatedRun', 'isDeprecatedOrRetiredMacImage')
  deprecatedOrRetiredMacImageMessage(startedAt, image, isFinished, conjugatedRun) {
    const retirementDate = Date.parse(this.get('imageToRetirementDate')[image]);

    const newImage = this.get('imageToNewImage')[image];
    const newImageString = this.get('newImageStrings')[newImage];
    const newImageAnchor = newImageString.replace(' ', '-');
    const newImageURLString = `<a href='https://docs.travis-ci.com/user/reference/osx/#${newImageAnchor}'>${newImageString}</a>`;
    const imageRetirementAnnouncementURL = 'https://blog.travis-ci.com/2017-11-21-xcode8-3-default-image-announce';

    const jobRanBeforeRetirementDate = Date.parse(startedAt) < retirementDate;
    const retirementDateIsInTheFuture = retirementDate > new Date();

    const formattedRetirementDate = moment(retirementDate).format('MMMM D, YYYY');

    const retirementLink =
      `
      ${retirementDateIsInTheFuture ? 'will be retired' : 'was retired'}
      on ${formattedRetirementDate}`;

    let retirementSentence, routingSentence;

    if (retirementDateIsInTheFuture) {
      retirementSentence = `This job ${conjugatedRun} on an OS X image that ${retirementLink}.`;
    } else {
      retirementSentence = `
        This job ${isFinished ? 'was configured to run on' : 'is configured to run on'}
        an OS X image that ${retirementLink}.`;
    }

    if (retirementDateIsInTheFuture) {
      routingSentence =
        `After that, it will route to our ${newImageURLString} image.`;
    } else if (jobRanBeforeRetirementDate) {
      routingSentence = `New jobs will route to our ${newImageURLString} image.`;
    } else {
      routingSentence = `It was routed to our ${newImageURLString} image.`;
    }

    return `${retirementSentence} ${routingSentence}`;
  }
});
<commit_msg>Removed console.log statement.<commit_after>import Component from '@ember/component';
import { computed } from 'ember-decorators/object';
import { alias, equal } from 'ember-decorators/object/computed';

import moment from 'moment';

const NOVEMBER_2017_RETIREMENT = '2017-11-28T12:00:00-08:00';
const LATEST_TRUSTY_RELEASE = '2017-12-10T00:00:00-00:00';

export default Component.extend({
  @alias('job.queue') queue: null,
  @alias('job.config') jobConfig: null,

  @computed('job.isFinished')
  conjugatedRun(isFinished) {
    return isFinished ? 'ran' : 'is running';
  },

  @equal('queue', 'builds.linux') isLegacyInfrastructure: null,
  @equal('queue', 'builds.ec2') isTrustySudoFalse: null,

  @computed('job.startedAt', 'queue', 'job.config.dist')
  isTrustySudoRequired(startedAt, queue, dist) {
    if (queue === 'builds.gce' && dist === 'trusty') {
      const jobRanAfterReleaseDate = Date.parse(startedAt) > Date.parse(LATEST_TRUSTY_RELEASE);
      if (jobRanAfterReleaseDate) {
        return true;
      }
    }

    return false;
  },

  @equal('queue', 'builds.macstadium6') isMacStadium6: null,

  @computed('queue', 'job.config')
  isPreciseEOL(queue, config = {}) {
    if (queue === 'builds.gce' && config.dist === 'precise') {
      if (config.language !== 'android') {
        return true;
      }
    }
  },

  @alias('jobConfig.osx_image') macOSImage: null,

  deprecatedXcodeImages: ['xcode8', 'xcode8.1', 'xcode8.2'],

  imageToRetirementDate: {
    'xcode8': NOVEMBER_2017_RETIREMENT,
    'xcode8.1': NOVEMBER_2017_RETIREMENT,
    'xcode8.2': NOVEMBER_2017_RETIREMENT
  },

  imageToNewImage: {
    'xcode8': 'xcode8.3',
    'xcode8.1': 'xcode8.3',
    'xcode8.2': 'xcode8.3'
  },

  newImageStrings: {
    'xcode8.3': 'Xcode 8.3',
    'xcode7.3': 'Xcode 7.3.1',
    'xcode6.4': 'Xcode 6.4'
  },

  @computed('isMacStadium6', 'macOSImage')
  isDeprecatedOrRetiredMacImage(isMacStadium6, macOSImage) {
    return isMacStadium6 && this.get('deprecatedXcodeImages').includes(macOSImage);
  },

  // eslint-disable-next-line
  @computed('job.startedAt', 'macOSImage', 'job.isFinished', 'conjugatedRun', 'isDeprecatedOrRetiredMacImage')
  deprecatedOrRetiredMacImageMessage(startedAt, image, isFinished, conjugatedRun) {
    const retirementDate = Date.parse(this.get('imageToRetirementDate')[image]);

    const newImage = this.get('imageToNewImage')[image];
    const newImageString = this.get('newImageStrings')[newImage];
    const newImageAnchor = newImageString.replace(' ', '-');
    const newImageURLString = `<a href='https://docs.travis-ci.com/user/reference/osx/#${newImageAnchor}'>${newImageString}</a>`;
    const imageRetirementAnnouncementURL = 'https://blog.travis-ci.com/2017-11-21-xcode8-3-default-image-announce';

    const jobRanBeforeRetirementDate = Date.parse(startedAt) < retirementDate;
    const retirementDateIsInTheFuture = retirementDate > new Date();

    const formattedRetirementDate = moment(retirementDate).format('MMMM D, YYYY');

    const retirementLink =
      `
      ${retirementDateIsInTheFuture ? 'will be retired' : 'was retired'}
      on ${formattedRetirementDate}`;

    let retirementSentence, routingSentence;

    if (retirementDateIsInTheFuture) {
      retirementSentence = `This job ${conjugatedRun} on an OS X image that ${retirementLink}.`;
    } else {
      retirementSentence = `
        This job ${isFinished ? 'was configured to run on' : 'is configured to run on'}
        an OS X image that ${retirementLink}.`;
    }

    if (retirementDateIsInTheFuture) {
      routingSentence =
        `After that, it will route to our ${newImageURLString} image.`;
    } else if (jobRanBeforeRetirementDate) {
      routingSentence = `New jobs will route to our ${newImageURLString} image.`;
    } else {
      routingSentence = `It was routed to our ${newImageURLString} image.`;
    }

    return `${retirementSentence} ${routingSentence}`;
  }
});
<|endoftext|>


<commit_before>import { KEY_PREFIX, REHYDRATE } from './constants'
import createAsyncLocalStorage from './defaults/asyncLocalStorage'
import purgeStoredState from './purgeStoredState'
import stringify from 'json-stringify-safe'

export default function createPersistor (store, config) {
  // defaults
  const serializer = config.serialize === false ? (data) => data : defaultSerializer
  const deserializer = config.serialize === false ? (data) => data : defaultDeserializer
  const blacklist = config.blacklist || []
  const whitelist = config.whitelist || false
  const transforms = config.transforms || []
  const debounce = config.debounce || false
  const keyPrefix = config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX

  // pluggable state shape (e.g. immutablejs)
  const stateInit = config._stateInit || {}
  const stateIterator = config._stateIterator || defaultStateIterator
  const stateGetter = config._stateGetter || defaultStateGetter
  const stateSetter = config._stateSetter || defaultStateSetter

  // storage with keys -> getAllKeys for localForage support
  let storage = config.storage || createAsyncLocalStorage('local')
  if (storage.keys && !storage.getAllKeys) {
    storage.getAllKeys = storage.keys
  }

  // initialize stateful values
  let lastState = stateInit
  let paused = false
  let storesToProcess = []
  let timeIterator = null

  store.subscribe(() => {
    if (paused) return

    let state = store.getState()

    stateIterator(state, (subState, key) => {
      if (!passWhitelistBlacklist(key)) return
      if (stateGetter(lastState, key) === stateGetter(state, key)) return
      if (storesToProcess.indexOf(key) !== -1) return
      storesToProcess.push(key)
    })

    // time iterator (read: debounce)
    if (timeIterator === null) {
      timeIterator = setInterval(() => {
        if (storesToProcess.length === 0) {
          clearInterval(timeIterator)
          timeIterator = null
          return
        }

        let key = storesToProcess[0]
        let storageKey = createStorageKey(key)
        let endState = transforms.reduce((subState, transformer) => transformer.in(subState, key), stateGetter(store.getState(), key))
        if (typeof endState !== 'undefined') storage.setItem(storageKey, serializer(endState), warnIfSetError(key))
        storesToProcess.shift()
      }, debounce)
    }

    lastState = state
  })

  function passWhitelistBlacklist (key) {
    if (whitelist && whitelist.indexOf(key) === -1) return false
    if (blacklist.indexOf(key) !== -1) return false
    return true
  }

  function adhocRehydrate (incoming, options = {}) {
    let state = {}
    if (options.serial) {
      stateIterator(incoming, (subState, key) => {
        try {
          let data = deserializer(subState)
          let value = transforms.reduceRight((interState, transformer) => {
            return transformer.out(interState, key)
          }, data)
          state = stateSetter(state, key, value)
        } catch (err) {
          if (process.env.NODE_ENV !== 'production') console.warn(`Error rehydrating data for key "${key}"`, subState, err)
        }
      })
    } else state = incoming

    store.dispatch(rehydrateAction(state))
    return state
  }

  function createStorageKey (key) {
    return `${keyPrefix}${key}`
  }

  // return `persistor`
  return {
    rehydrate: adhocRehydrate,
    pause: () => { paused = true },
    resume: () => { paused = false },
    purge: (keys) => purgeStoredState({storage, keyPrefix}, keys)
  }
}

function warnIfSetError (key) {
  return function setError (err) {
    if (err && process.env.NODE_ENV !== 'production') { console.warn('Error storing data for key:', key, err) }
  }
}

function defaultSerializer (data) {
  return stringify(data, null, null, (k, v) => {
    if (process.env.NODE_ENV !== 'production') return null
    throw new Error(`
      redux-persist: cannot process cyclical state.
      Consider changing your state structure to have no cycles.
      Alternatively blacklist the corresponding reducer key.
      Cycle encounted at key "${k}" with value "${v}".
    `)
  })
}

function defaultDeserializer (serial) {
  return JSON.parse(serial)
}

function rehydrateAction (data) {
  return {
    type: REHYDRATE,
    payload: data
  }
}

function defaultStateIterator (collection, callback) {
  return Object.keys(collection).forEach((key) => callback(collection[key], key))
}

function defaultStateGetter (state, key) {
  return state[key]
}

function defaultStateSetter (state, key, value) {
  state[key] = value
  return state
}
<commit_msg>Immediately remove store<commit_after>import { KEY_PREFIX, REHYDRATE } from './constants'
import createAsyncLocalStorage from './defaults/asyncLocalStorage'
import purgeStoredState from './purgeStoredState'
import stringify from 'json-stringify-safe'

export default function createPersistor (store, config) {
  // defaults
  const serializer = config.serialize === false ? (data) => data : defaultSerializer
  const deserializer = config.serialize === false ? (data) => data : defaultDeserializer
  const blacklist = config.blacklist || []
  const whitelist = config.whitelist || false
  const transforms = config.transforms || []
  const debounce = config.debounce || false
  const keyPrefix = config.keyPrefix !== undefined ? config.keyPrefix : KEY_PREFIX

  // pluggable state shape (e.g. immutablejs)
  const stateInit = config._stateInit || {}
  const stateIterator = config._stateIterator || defaultStateIterator
  const stateGetter = config._stateGetter || defaultStateGetter
  const stateSetter = config._stateSetter || defaultStateSetter

  // storage with keys -> getAllKeys for localForage support
  let storage = config.storage || createAsyncLocalStorage('local')
  if (storage.keys && !storage.getAllKeys) {
    storage.getAllKeys = storage.keys
  }

  // initialize stateful values
  let lastState = stateInit
  let paused = false
  let storesToProcess = []
  let timeIterator = null

  store.subscribe(() => {
    if (paused) return

    let state = store.getState()

    stateIterator(state, (subState, key) => {
      if (!passWhitelistBlacklist(key)) return
      if (stateGetter(lastState, key) === stateGetter(state, key)) return
      if (storesToProcess.indexOf(key) !== -1) return
      storesToProcess.push(key)
    })

    // time iterator (read: debounce)
    if (timeIterator === null) {
      timeIterator = setInterval(() => {
        if (storesToProcess.length === 0) {
          clearInterval(timeIterator)
          timeIterator = null
          return
        }

        let key = storesToProcess.shift()
        let storageKey = createStorageKey(key)
        let endState = transforms.reduce((subState, transformer) => transformer.in(subState, key), stateGetter(store.getState(), key))
        if (typeof endState !== 'undefined') storage.setItem(storageKey, serializer(endState), warnIfSetError(key))
      }, debounce)
    }

    lastState = state
  })

  function passWhitelistBlacklist (key) {
    if (whitelist && whitelist.indexOf(key) === -1) return false
    if (blacklist.indexOf(key) !== -1) return false
    return true
  }

  function adhocRehydrate (incoming, options = {}) {
    let state = {}
    if (options.serial) {
      stateIterator(incoming, (subState, key) => {
        try {
          let data = deserializer(subState)
          let value = transforms.reduceRight((interState, transformer) => {
            return transformer.out(interState, key)
          }, data)
          state = stateSetter(state, key, value)
        } catch (err) {
          if (process.env.NODE_ENV !== 'production') console.warn(`Error rehydrating data for key "${key}"`, subState, err)
        }
      })
    } else state = incoming

    store.dispatch(rehydrateAction(state))
    return state
  }

  function createStorageKey (key) {
    return `${keyPrefix}${key}`
  }

  // return `persistor`
  return {
    rehydrate: adhocRehydrate,
    pause: () => { paused = true },
    resume: () => { paused = false },
    purge: (keys) => purgeStoredState({storage, keyPrefix}, keys)
  }
}

function warnIfSetError (key) {
  return function setError (err) {
    if (err && process.env.NODE_ENV !== 'production') { console.warn('Error storing data for key:', key, err) }
  }
}

function defaultSerializer (data) {
  return stringify(data, null, null, (k, v) => {
    if (process.env.NODE_ENV !== 'production') return null
    throw new Error(`
      redux-persist: cannot process cyclical state.
      Consider changing your state structure to have no cycles.
      Alternatively blacklist the corresponding reducer key.
      Cycle encounted at key "${k}" with value "${v}".
    `)
  })
}

function defaultDeserializer (serial) {
  return JSON.parse(serial)
}

function rehydrateAction (data) {
  return {
    type: REHYDRATE,
    payload: data
  }
}

function defaultStateIterator (collection, callback) {
  return Object.keys(collection).forEach((key) => callback(collection[key], key))
}

function defaultStateGetter (state, key) {
  return state[key]
}

function defaultStateSetter (state, key, value) {
  state[key] = value
  return state
}
<|endoftext|>