<commit_before>function RoomCtrl($scope, $routeParams, $window, $location, groove, localStorageService) {
    /*
     * Listeners on the UI
     */
    groove.joinRoom($routeParams.room);

    $scope.users = [];
    $scope.djs = [];
    $scope.currentTab = localStorageService.get("user:tab") || "music";
    $scope.currentOverlay = false;
    $scope.tracks = groove.playlists[groove.activePlaylist];
    $scope.files = [];

    $scope.currentTrack = null;
    $scope.votes = { yes: 0, no: 0 };
    $scope.chat_messages = [];
    $scope.newMessages = false;
    $scope.users.push(groove.me);

    var player = $window.document.createElement("audio");
    window.player = player;
    var activeTrack;
    player.addEventListener("canplay", function() {
        var track = groove.activeTrack;
        if (track == activeTrack) return;
        activeTrack = track;
        console.log("canplay", track && track.currentTime, track);
        if (track) {
            groove.canPlayTrack(player.duration);
            player.play();
        }
    }, false);

    $scope.switchTab = function(tab) {
        if(tab == "chat" && $scope.newMessages) {
            $scope.newMessages = false;
        }

        localStorageService.set("user:tab", tab);
        $scope.currentTab = tab;
    }

    $scope.clickUser = function(user) {
        if(user == groove.me) {
            $scope.setOverlay("change-avatar");
        }
    }

    $scope.isDJ = function(user) {
        return (user.dj == true);
    }

    $scope.isAudience = function(user) {
        return (user.dj != true);
    }

    $scope.becomeDJ = function() {
        if($scope.tracks.length == 0) {
            $scope.setOverlay("no-tracks");
            return;
        }

        if(!groove.me.dj) {
            groove.becomeDJ();
        } else {
            groove.quitDJing();
        }
    }

    $scope.vote = function(direction) {
        if(groove.activeDJ == groove.me) {
            return;
        }

        handleVote(groove.me.vote, direction);
        groove.vote(direction);
    }

    $scope.getJoinText = function() {
        if(groove.me.dj) {
            return "leave";
        } else {
            return "become a dj";
        }
    }

    $scope.$on("$destroy", function() {
        groove.leaveRoom();
    });

    function handleVote(previous, next) {
        if(previous == 1) {
            $scope.votes.yes -= 1;
        } else if(previous == -1) {
            $scope.votes.no -= 1;
        }

        if(next == 1) {
            $scope.votes.yes += 1;
        } else if(next == -1) {
            $scope.votes.no += 1;
        }
    }

    var digest = $scope.$digest.bind($scope);
    function watchUser(user) {
        user.on("name", digest);
        user.on("vote", digest);
        user.on("gravatar", digest);
        user.on("vote", function(previous, next) {
            handleVote(previous, next);
            digest();
        });
    }

    $scope.newMessage = function() {
        var text = $scope.message_text;
        var last = groove.lastChatAuthor;
        if(text && text.trim()) {
            $scope.chat_messages.push({
                from: groove.me,
                text: text,
                isContinuation: last && last == groove.me
            });
            groove.lastChatAuthor = groove.me;
            groove.sendChat(text);
        }
        $scope.message_text = "";
    };

    $scope.$watch("files", function() {
        groove.addFilesToQueue($scope.files);
    });

    $scope.$on("$destroy", function() {
        groove.leaveRoom();
    });

    /*
     * Listeners from the buoy server
     */
    groove.on("chat", function(message) {
        $scope.$apply(function($scope) {
            if($scope.currentTab != "chat") {
                $scope.newMessages = true;
            }

            var last = groove.lastChatAuthor;
            message.isContinuation = (last && last == message.from);
            groove.lastChatAuthor = message.from;
            $scope.chat_messages.push(message);
        });
    });

    groove.on("djs", function(djs) {
        $scope.$apply(function($scope) {
            $scope.djs = djs;
        });
    });

    groove.on("activeDJ", function() {
        $scope.$apply(function($scope) {
            $scope.activeDJ = groove.activeDJ;
        });
    });

    groove.on("activeTrack", function() {
        $scope.$apply(function($scope) {
            $scope.currentTrack = groove.activeTrack;
        });
    });

    groove.on("activeTrackURL", function() {
        var track = groove.activeTrack;
        var url = track && track.url;
        if (url) {
            console.log("got track url", url.length, url.substr(0, 256));
            player.src = url;
            player.play();
        } else {
            player.pause();
        }
    });

    groove.on("emptyPlaylist", function() {
        alert("Add some music to your playlist first.");
    });

    groove.on("queueUpdate", digest);

    groove.on("peerConnected", function(user) {
        $scope.$apply(function($scope) {
            $scope.users.push(user);
            watchUser(user);
        });
    });

    groove.on("peerDisconnected", function(user) {
        $scope.$apply(function($scope) {
            var i =  $scope.users.indexOf(user);
            if(i == -1) {
                return;
            }

            $scope.users.splice(i, 1);
        });
    });

    groove.buoy.on("reconnected", function() {
        $scope.$apply(function($scope) {
            $scope.users = [groove.me];
            groove.me.setName(groove.me.name);
            groove.me.setGravatar(groove.me.gravatar);
            groove.joinRoom($routeParams.room);
        });
    });
}

RoomCtrl.$inject = ["$scope", "$routeParams", "$window", "$location", "groove", "localStorageService"];
<commit_msg>Preventing DJs on reconnect<commit_after>function RoomCtrl($scope, $routeParams, $window, $location, groove, localStorageService) {
    /*
     * Listeners on the UI
     */
    groove.joinRoom($routeParams.room);

    $scope.users = [];
    $scope.djs = [];
    $scope.currentTab = localStorageService.get("user:tab") || "music";
    $scope.currentOverlay = false;
    $scope.tracks = groove.playlists[groove.activePlaylist];
    $scope.files = [];

    $scope.currentTrack = null;
    $scope.votes = { yes: 0, no: 0 };
    $scope.chat_messages = [];
    $scope.newMessages = false;
    $scope.users.push(groove.me);

    var player = $window.document.createElement("audio");
    window.player = player;
    var activeTrack;
    player.addEventListener("canplay", function() {
        var track = groove.activeTrack;
        if (track == activeTrack) return;
        activeTrack = track;
        console.log("canplay", track && track.currentTime, track);
        if (track) {
            groove.canPlayTrack(player.duration);
            player.play();
        }
    }, false);

    $scope.switchTab = function(tab) {
        if(tab == "chat" && $scope.newMessages) {
            $scope.newMessages = false;
        }

        localStorageService.set("user:tab", tab);
        $scope.currentTab = tab;
    }

    $scope.clickUser = function(user) {
        if(user == groove.me) {
            $scope.setOverlay("change-avatar");
        }
    }

    $scope.isDJ = function(user) {
        return (user.dj == true);
    }

    $scope.isAudience = function(user) {
        return (user.dj != true);
    }

    $scope.becomeDJ = function() {
        if($scope.tracks.length == 0) {
            $scope.setOverlay("no-tracks");
            return;
        }

        if(!groove.me.dj) {
            groove.becomeDJ();
        } else {
            groove.quitDJing();
        }
    }

    $scope.vote = function(direction) {
        if(groove.activeDJ == groove.me) {
            return;
        }

        handleVote(groove.me.vote, direction);
        groove.vote(direction);
    }

    $scope.getJoinText = function() {
        if(groove.me.dj) {
            return "leave";
        } else {
            return "become a dj";
        }
    }

    $scope.$on("$destroy", function() {
        groove.leaveRoom();
    });

    function handleVote(previous, next) {
        if(previous == 1) {
            $scope.votes.yes -= 1;
        } else if(previous == -1) {
            $scope.votes.no -= 1;
        }

        if(next == 1) {
            $scope.votes.yes += 1;
        } else if(next == -1) {
            $scope.votes.no += 1;
        }
    }

    var digest = $scope.$digest.bind($scope);
    function watchUser(user) {
        user.on("name", digest);
        user.on("vote", digest);
        user.on("gravatar", digest);
        user.on("vote", function(previous, next) {
            handleVote(previous, next);
            digest();
        });
    }

    $scope.newMessage = function() {
        var text = $scope.message_text;
        var last = groove.lastChatAuthor;
        if(text && text.trim()) {
            $scope.chat_messages.push({
                from: groove.me,
                text: text,
                isContinuation: last && last == groove.me
            });
            groove.lastChatAuthor = groove.me;
            groove.sendChat(text);
        }
        $scope.message_text = "";
    };

    $scope.$watch("files", function() {
        groove.addFilesToQueue($scope.files);
    });

    $scope.$on("$destroy", function() {
        groove.leaveRoom();
    });

    /*
     * Listeners from the buoy server
     */
    groove.on("chat", function(message) {
        $scope.$apply(function($scope) {
            if($scope.currentTab != "chat") {
                $scope.newMessages = true;
            }

            var last = groove.lastChatAuthor;
            message.isContinuation = (last && last == message.from);
            groove.lastChatAuthor = message.from;
            $scope.chat_messages.push(message);
        });
    });

    groove.on("djs", function(djs) {
        $scope.$apply(function($scope) {
            $scope.djs = djs;
        });
    });

    groove.on("activeDJ", function() {
        $scope.$apply(function($scope) {
            $scope.activeDJ = groove.activeDJ;
        });
    });

    groove.on("activeTrack", function() {
        $scope.$apply(function($scope) {
            $scope.currentTrack = groove.activeTrack;
        });
    });

    groove.on("activeTrackURL", function() {
        var track = groove.activeTrack;
        var url = track && track.url;
        if (url) {
            console.log("got track url", url.length, url.substr(0, 256));
            player.src = url;
            player.play();
        } else {
            player.pause();
        }
    });

    groove.on("emptyPlaylist", function() {
        alert("Add some music to your playlist first.");
    });

    groove.on("queueUpdate", digest);

    groove.on("peerConnected", function(user) {
        $scope.$apply(function($scope) {
            $scope.users.push(user);
            watchUser(user);
        });
    });

    groove.on("peerDisconnected", function(user) {
        $scope.$apply(function($scope) {
            var i =  $scope.users.indexOf(user);
            if(i == -1) {
                return;
            }

            $scope.users.splice(i, 1);
        });
    });

    groove.buoy.on("reconnected", function() {
        $scope.$apply(function($scope) {
            $scope.users = [groove.me];
            $scope.djs = [];
            groove.me.setName(groove.me.name);
            groove.me.setGravatar(groove.me.gravatar);
            groove.joinRoom($routeParams.room);
        });
    });
}

RoomCtrl.$inject = ["$scope", "$routeParams", "$window", "$location", "groove", "localStorageService"];
<|endoftext|>


<commit_before>import http from 'http';
import fs from 'fs-extra';
import path from 'path';
import { promisify } from 'util';
import express from 'express';
import socketio from 'socket.io';
import log from 'apify-shared/log';
import { checkParamOrThrow } from 'apify-client/build/utils';
import { promisifyServerListen } from 'apify-shared/utilities';
import { ENV_VARS, LOCAL_ENV_VARS } from 'apify-shared/consts';
import { addTimeoutToPromise } from '../utils';

const writeFile = promisify(fs.writeFile);
const unlink = promisify(fs.unlink);
const ensureDir = promisify(fs.ensureDir);

/**
 * `LiveViewServer` enables serving of browser snapshots via web sockets. It includes its own client
 * that provides a simple frontend to viewing the captured snapshots. A snapshot consists of three
 * pieces of information, the currently opened URL, the content of the page (HTML) and its screenshot.
 *
 * ```json
 * {
 *     "pageUrl": "https://www.example.com",
 *     "htmlContent": "<html><body> ....",
 *     "screenshotIndex": 3,
 *     "createdAt": "2019-04-18T11:50:40.060Z"
 * }
 * ```
 *
 * `LiveViewServer` is useful when you want to be able to inspect the current browser status on demand.
 * When no client is connected, the webserver consumes very low resources so it should have a close
 * to zero impact on performance. Only once a client connects the server will start serving snapshots.
 * Once no longer needed, it can be disabled again in the client to remove any performance impact.
 *
 * NOTE: Screenshot taking in browser typically takes around 300ms. So having the `LiveViewServer`
 * always serve snapshots will have a significant impact on performance.
 *
 * When using {@link PuppeteerPool}, the `LiveViewServer` can be
 * easily used just by providing the `useLiveView = true` option to the {@link PuppeteerPool}.
 * It can also be initiated via {@link PuppeteerCrawler} `puppeteerPoolOptions`.
 *
 * It will take snapshots of the first page of the latest browser. Taking snapshots of only a
 * single page improves performance and stability dramatically in high concurrency situations.
 *
 * When running locally, it is often best to use a headful browser for debugging, since it provides
 * a better view into the browser, including DevTools, but `LiveViewServer` works too.
 *
 * @param {Object} [options]
 *   All `LiveViewServer` parameters are passed
 *   via an options object with the following keys:
 * @param {string} [options.screenshotDirectoryPath]
 *   By default, the screenshots are saved to
 *   the `live_view` directory in the process' working directory. Provide a different
 *   absolute path to change the settings.
 * @param {number} [options.maxScreenshotFiles=10]
 *   Limits the number of screenshots stored
 *   by the server. This is to prevent using up too much disk space.
 * @param {number} [options.snapshotTimeoutSecs=3]
 *   If a snapshot is not made within the timeout,
 *   its creation will be aborted. This is to prevent
 *   pages from being hung up by a stalled screenshot.
 * @param {number} [options.maxSnapshotFrequencySecs=2]
 *   Use this parameter to further decrease the resource consumption
 *   of `LiveViewServer` by limiting the frequency at which it'll
 *   serve snapshots.
 */
class LiveViewServer {
    constructor(options = {}) {
        const {
            screenshotDirectoryPath = path.resolve('live_view'),
            maxScreenshotFiles = 10,
            snapshotTimeoutSecs = 3,
            maxSnapshotFrequencySecs = 1,
        } = options;

        checkParamOrThrow(screenshotDirectoryPath, 'options.screenshotDirectoryPath', 'String');
        checkParamOrThrow(maxScreenshotFiles, 'options.maxScreenshotFiles', 'Number');
        checkParamOrThrow(maxScreenshotFiles, 'options.snapshotTimeoutSecs', 'Number');
        checkParamOrThrow(maxScreenshotFiles, 'options.maxSnapshotFrequencySecs', 'Number');
        this.screenshotDirectoryPath = screenshotDirectoryPath;
        this.maxScreenshotFiles = maxScreenshotFiles;
        this.snapshotTimeoutMillis = snapshotTimeoutSecs * 1000;
        this.maxSnapshotFrequencyMillis = maxSnapshotFrequencySecs * 1000;

        // Snapshot data
        this.lastSnapshot = null;
        this.lastScreenshotIndex = 0;

        // Server
        this.clientCount = 0;
        this._isRunning = false;
        this.httpServer = null;
        this.socketio = null;
        this.servingSnapshot = false;

        this._setupHttpServer();
    }

    /**
     * Starts the HTTP server with web socket connections enabled.
     * Snapshots will not be created until a client has connected.
     * @return {Promise}
     */
    async start() {
        await ensureDir(this.screenshotDirectoryPath);
        await promisifyServerListen(this.httpServer)(this.port);
        log.info('Live view web server started', { publicUrl: this.liveViewUrl });
        this._isRunning = true;
    }

    /**
     * Prevents the server from receiving more connections. Existing connections
     * will not be terminated, but the server will not prevent a process exit.
     * @return {Promise}
     */
    async stop() {
        this.httpServer.unref();
        return new Promise((resolve) => {
            this.httpServer.close((err) => {
                this._isRunning = false;
                if (err) log.exception(err, 'Live view web server could not be stopped.');
                else log.info('Live view web server stopped.');
                resolve();
            });
        });
    }

    /**
     * Serves a snapshot to all connected clients.
     * Screenshots are not served directly, only their index number
     * which is used by client to retrieve the screenshot.
     *
     * Will time out and throw in `options.snapshotTimeoutSecs`.
     *
     * @param {Page} page
     * @return {Promise}
     */
    async serve(page) {
        if (!this.hasClients()) return;
        // Only serve one snapshot at a time because Puppeteer
        // can't make screenshots in parallel.
        if (this.servingSnapshot) return;

        if (this.lastSnapshot) {
            const lastSnapshotAgeMillis = Date.now() - this.lastSnapshot.createdAt;
            if (lastSnapshotAgeMillis < this.maxSnapshotFrequencyMillis) return;
        }

        try {
            this.servingSnapshot = true;
            const snapshot = await addTimeoutToPromise(
                this._makeSnapshot(page),
                this.snapshotTimeoutMillis,
                'LiveViewServer: Serving of Live View timed out.',
            );
            this._pushSnapshot(snapshot);
        } finally {
            this.servingSnapshot = false;
        }
    }

    /**
     * @return {?Object}
     */
    getLastSnapshot() {
        return this.lastSnapshot;
    }

    /**
     * @return {boolean}
     */
    isRunning() {
        return this._isRunning;
    }

    /**
     * @return {boolean}
     */
    hasClients() {
        // Treat LiveViewServer as a client, until at least one snapshot is made.
        return this.lastSnapshot ? this.clientCount > 0 : true;
    }

    /**
     * Returns an absolute path to the screenshot with the given index.
     * @param {number} screenshotIndex
     * @return {string}
     * @ignore
     */
    _getScreenshotPath(screenshotIndex) {
        return path.join(this.screenshotDirectoryPath, `${screenshotIndex}.jpeg`);
    }

    async _makeSnapshot(page) {
        const pageUrl = page.url();
        log.info('Making live view snapshot.', { pageUrl });
        const [htmlContent, screenshot] = await Promise.all([
            page.content(),
            page.screenshot({
                type: 'jpeg',
                quality: 75,
            }),
        ]);

        const screenshotIndex = this.lastScreenshotIndex++;

        await writeFile(this._getScreenshotPath(screenshotIndex), screenshot);
        if (screenshotIndex > this.maxScreenshotFiles - 1) {
            this._deleteScreenshot(screenshotIndex - this.maxScreenshotFiles);
        }

        const snapshot = { pageUrl, htmlContent, screenshotIndex, createdAt: new Date() };
        this.lastSnapshot = snapshot;
        return snapshot;
    }

    _pushSnapshot(snapshot) {
        // Send new snapshot to clients
        log.debug('Sending live view snapshot', { snapshot });
        this.socketio.emit('snapshot', snapshot);
    }

    /**
     * Initiates an async delete and does not wait for it to complete.
     * @param screenshotIndex
     * @ignore
     */
    _deleteScreenshot(screenshotIndex) {
        unlink(this._getScreenshotPath(screenshotIndex))
            .catch(err => log.exception(err, 'Cannot delete live view screenshot.'));
    }

    _setupHttpServer() {
        const containerPort = process.env[ENV_VARS.CONTAINER_PORT] || LOCAL_ENV_VARS[ENV_VARS.CONTAINER_PORT];

        this.port = parseInt(containerPort, 10);
        if (!(this.port >= 0 && this.port <= 65535)) {
            throw new Error('Cannot start LiveViewServer - invalid port specified by the '
                + `${ENV_VARS.CONTAINER_PORT} environment variable (was "${containerPort}").`);
        }
        this.liveViewUrl = process.env[ENV_VARS.CONTAINER_URL] || LOCAL_ENV_VARS[ENV_VARS.CONTAINER_URL];

        this.httpServer = http.createServer();
        const app = express();

        app.use('/', express.static(__dirname));

        // Serves a JS file with the last snapshot, so the the client can immediately show something
        app.get('/init-last-snapshot.js', (req, res) => {
            res.set('Content-Type', 'text/javascript');
            res.send(`window.lastSnapshot = ${JSON.stringify(this.lastSnapshot, null, 2)};`);
        });

        // Serves JPEG with the last screenshot
        app.get('/screenshot/:index', (req, res) => {
            const screenshotIndex = req.params.index;
            const filePath = this._getScreenshotPath(screenshotIndex);
            res.sendFile(filePath);
        });

        app.all('*', (req, res) => {
            res.status(404).send('Nothing here');
        });

        this.httpServer.on('request', app);

        // Socket.io server used to send snapshots to client
        this.socketio = socketio(this.httpServer);
        this.socketio.on('connection', (socket) => {
            this.clientCount++;
            log.info('Live view client connected', { clientId: socket.id });
            socket.on('disconnect', (reason) => {
                this.clientCount--;
                log.info('Live view client disconnected', { clientId: socket.id, reason });
            });
        });
    }
}

export default LiveViewServer;
<commit_msg>Decrease live view snapshot frequency<commit_after>import http from 'http';
import fs from 'fs-extra';
import path from 'path';
import { promisify } from 'util';
import express from 'express';
import socketio from 'socket.io';
import log from 'apify-shared/log';
import { checkParamOrThrow } from 'apify-client/build/utils';
import { promisifyServerListen } from 'apify-shared/utilities';
import { ENV_VARS, LOCAL_ENV_VARS } from 'apify-shared/consts';
import { addTimeoutToPromise } from '../utils';

const writeFile = promisify(fs.writeFile);
const unlink = promisify(fs.unlink);
const ensureDir = promisify(fs.ensureDir);

/**
 * `LiveViewServer` enables serving of browser snapshots via web sockets. It includes its own client
 * that provides a simple frontend to viewing the captured snapshots. A snapshot consists of three
 * pieces of information, the currently opened URL, the content of the page (HTML) and its screenshot.
 *
 * ```json
 * {
 *     "pageUrl": "https://www.example.com",
 *     "htmlContent": "<html><body> ....",
 *     "screenshotIndex": 3,
 *     "createdAt": "2019-04-18T11:50:40.060Z"
 * }
 * ```
 *
 * `LiveViewServer` is useful when you want to be able to inspect the current browser status on demand.
 * When no client is connected, the webserver consumes very low resources so it should have a close
 * to zero impact on performance. Only once a client connects the server will start serving snapshots.
 * Once no longer needed, it can be disabled again in the client to remove any performance impact.
 *
 * NOTE: Screenshot taking in browser typically takes around 300ms. So having the `LiveViewServer`
 * always serve snapshots will have a significant impact on performance.
 *
 * When using {@link PuppeteerPool}, the `LiveViewServer` can be
 * easily used just by providing the `useLiveView = true` option to the {@link PuppeteerPool}.
 * It can also be initiated via {@link PuppeteerCrawler} `puppeteerPoolOptions`.
 *
 * It will take snapshots of the first page of the latest browser. Taking snapshots of only a
 * single page improves performance and stability dramatically in high concurrency situations.
 *
 * When running locally, it is often best to use a headful browser for debugging, since it provides
 * a better view into the browser, including DevTools, but `LiveViewServer` works too.
 *
 * @param {Object} [options]
 *   All `LiveViewServer` parameters are passed
 *   via an options object with the following keys:
 * @param {string} [options.screenshotDirectoryPath]
 *   By default, the screenshots are saved to
 *   the `live_view` directory in the process' working directory. Provide a different
 *   absolute path to change the settings.
 * @param {number} [options.maxScreenshotFiles=10]
 *   Limits the number of screenshots stored
 *   by the server. This is to prevent using up too much disk space.
 * @param {number} [options.snapshotTimeoutSecs=3]
 *   If a snapshot is not made within the timeout,
 *   its creation will be aborted. This is to prevent
 *   pages from being hung up by a stalled screenshot.
 * @param {number} [options.maxSnapshotFrequencySecs=2]
 *   Use this parameter to further decrease the resource consumption
 *   of `LiveViewServer` by limiting the frequency at which it'll
 *   serve snapshots.
 */
class LiveViewServer {
    constructor(options = {}) {
        const {
            screenshotDirectoryPath = path.resolve('live_view'),
            maxScreenshotFiles = 10,
            snapshotTimeoutSecs = 3,
            maxSnapshotFrequencySecs = 2,
        } = options;

        checkParamOrThrow(screenshotDirectoryPath, 'options.screenshotDirectoryPath', 'String');
        checkParamOrThrow(maxScreenshotFiles, 'options.maxScreenshotFiles', 'Number');
        checkParamOrThrow(maxScreenshotFiles, 'options.snapshotTimeoutSecs', 'Number');
        checkParamOrThrow(maxScreenshotFiles, 'options.maxSnapshotFrequencySecs', 'Number');
        this.screenshotDirectoryPath = screenshotDirectoryPath;
        this.maxScreenshotFiles = maxScreenshotFiles;
        this.snapshotTimeoutMillis = snapshotTimeoutSecs * 1000;
        this.maxSnapshotFrequencyMillis = maxSnapshotFrequencySecs * 1000;

        // Snapshot data
        this.lastSnapshot = null;
        this.lastScreenshotIndex = 0;

        // Server
        this.clientCount = 0;
        this._isRunning = false;
        this.httpServer = null;
        this.socketio = null;
        this.servingSnapshot = false;

        this._setupHttpServer();
    }

    /**
     * Starts the HTTP server with web socket connections enabled.
     * Snapshots will not be created until a client has connected.
     * @return {Promise}
     */
    async start() {
        await ensureDir(this.screenshotDirectoryPath);
        await promisifyServerListen(this.httpServer)(this.port);
        log.info('Live view web server started', { publicUrl: this.liveViewUrl });
        this._isRunning = true;
    }

    /**
     * Prevents the server from receiving more connections. Existing connections
     * will not be terminated, but the server will not prevent a process exit.
     * @return {Promise}
     */
    async stop() {
        this.httpServer.unref();
        return new Promise((resolve) => {
            this.httpServer.close((err) => {
                this._isRunning = false;
                if (err) log.exception(err, 'Live view web server could not be stopped.');
                else log.info('Live view web server stopped.');
                resolve();
            });
        });
    }

    /**
     * Serves a snapshot to all connected clients.
     * Screenshots are not served directly, only their index number
     * which is used by client to retrieve the screenshot.
     *
     * Will time out and throw in `options.snapshotTimeoutSecs`.
     *
     * @param {Page} page
     * @return {Promise}
     */
    async serve(page) {
        if (!this.hasClients()) return;
        // Only serve one snapshot at a time because Puppeteer
        // can't make screenshots in parallel.
        if (this.servingSnapshot) return;

        if (this.lastSnapshot) {
            const lastSnapshotAgeMillis = Date.now() - this.lastSnapshot.createdAt;
            if (lastSnapshotAgeMillis < this.maxSnapshotFrequencyMillis) return;
        }

        try {
            this.servingSnapshot = true;
            const snapshot = await addTimeoutToPromise(
                this._makeSnapshot(page),
                this.snapshotTimeoutMillis,
                'LiveViewServer: Serving of Live View timed out.',
            );
            this._pushSnapshot(snapshot);
        } finally {
            this.servingSnapshot = false;
        }
    }

    /**
     * @return {?Object}
     */
    getLastSnapshot() {
        return this.lastSnapshot;
    }

    /**
     * @return {boolean}
     */
    isRunning() {
        return this._isRunning;
    }

    /**
     * @return {boolean}
     */
    hasClients() {
        // Treat LiveViewServer as a client, until at least one snapshot is made.
        return this.lastSnapshot ? this.clientCount > 0 : true;
    }

    /**
     * Returns an absolute path to the screenshot with the given index.
     * @param {number} screenshotIndex
     * @return {string}
     * @ignore
     */
    _getScreenshotPath(screenshotIndex) {
        return path.join(this.screenshotDirectoryPath, `${screenshotIndex}.jpeg`);
    }

    async _makeSnapshot(page) {
        const pageUrl = page.url();
        log.info('Making live view snapshot.', { pageUrl });
        const [htmlContent, screenshot] = await Promise.all([
            page.content(),
            page.screenshot({
                type: 'jpeg',
                quality: 75,
            }),
        ]);

        const screenshotIndex = this.lastScreenshotIndex++;

        await writeFile(this._getScreenshotPath(screenshotIndex), screenshot);
        if (screenshotIndex > this.maxScreenshotFiles - 1) {
            this._deleteScreenshot(screenshotIndex - this.maxScreenshotFiles);
        }

        const snapshot = { pageUrl, htmlContent, screenshotIndex, createdAt: new Date() };
        this.lastSnapshot = snapshot;
        return snapshot;
    }

    _pushSnapshot(snapshot) {
        // Send new snapshot to clients
        log.debug('Sending live view snapshot', { snapshot });
        this.socketio.emit('snapshot', snapshot);
    }

    /**
     * Initiates an async delete and does not wait for it to complete.
     * @param screenshotIndex
     * @ignore
     */
    _deleteScreenshot(screenshotIndex) {
        unlink(this._getScreenshotPath(screenshotIndex))
            .catch(err => log.exception(err, 'Cannot delete live view screenshot.'));
    }

    _setupHttpServer() {
        const containerPort = process.env[ENV_VARS.CONTAINER_PORT] || LOCAL_ENV_VARS[ENV_VARS.CONTAINER_PORT];

        this.port = parseInt(containerPort, 10);
        if (!(this.port >= 0 && this.port <= 65535)) {
            throw new Error('Cannot start LiveViewServer - invalid port specified by the '
                + `${ENV_VARS.CONTAINER_PORT} environment variable (was "${containerPort}").`);
        }
        this.liveViewUrl = process.env[ENV_VARS.CONTAINER_URL] || LOCAL_ENV_VARS[ENV_VARS.CONTAINER_URL];

        this.httpServer = http.createServer();
        const app = express();

        app.use('/', express.static(__dirname));

        // Serves a JS file with the last snapshot, so the the client can immediately show something
        app.get('/init-last-snapshot.js', (req, res) => {
            res.set('Content-Type', 'text/javascript');
            res.send(`window.lastSnapshot = ${JSON.stringify(this.lastSnapshot, null, 2)};`);
        });

        // Serves JPEG with the last screenshot
        app.get('/screenshot/:index', (req, res) => {
            const screenshotIndex = req.params.index;
            const filePath = this._getScreenshotPath(screenshotIndex);
            res.sendFile(filePath);
        });

        app.all('*', (req, res) => {
            res.status(404).send('Nothing here');
        });

        this.httpServer.on('request', app);

        // Socket.io server used to send snapshots to client
        this.socketio = socketio(this.httpServer);
        this.socketio.on('connection', (socket) => {
            this.clientCount++;
            log.info('Live view client connected', { clientId: socket.id });
            socket.on('disconnect', (reason) => {
                this.clientCount--;
                log.info('Live view client disconnected', { clientId: socket.id, reason });
            });
        });
    }
}

export default LiveViewServer;
<|endoftext|>


<commit_before>/*
 Models
 */

// Maybe we can move this to the currentOrderController?
App.bankList = [
    Ember.Object.create({value:"0081", title: "Fortis"}),
    Ember.Object.create({value:"0021", title: "Rabobank"}),
    Ember.Object.create({value:"0721", title: "ING Bank"}),
    Ember.Object.create({value:"0751", title: "SNS Bank"}),
    Ember.Object.create({value:"0031", title: "ABN Amro Bank"}),
    Ember.Object.create({value:"0761", title: "ASN Bank"}),
    Ember.Object.create({value:"0771", title: "SNS Regio Bank"}),
    Ember.Object.create({value:"0511", title: "Triodos Bank"}),
    Ember.Object.create({value:"0091", title: "Friesland Bank"}),
    Ember.Object.create({value:"0161", title: "Van Lanschot Bankiers"})
];

App.PaymentMethod = DS.Model.extend({
    url: 'fund/paymentmethods',
    name: DS.attr('string'),
    order: DS.belongsTo('App.Order')
});

App.Order = DS.Model.extend({
    url: 'fund/orders',
    amount: DS.attr('number'),
    status: DS.attr('string'),
    recurring: DS.attr('string'),
    payment_method_id: DS.attr('string'),
    payment_submethod_id: DS.attr('string'),
    payment_methods: DS.hasMany('App.PaymentMethod')
});

App.PaymentOrderProfile = DS.Model.extend({
    url: 'fund/paymentorderprofiles',
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    street: DS.attr('string'),
    city: DS.attr('string'),
    country: DS.attr('string'),
    postalCode: DS.attr('string')
});

App.OrderItem = DS.Model.extend({
    url: 'fund/orders/:order_id/items',

    // Model fields
    // FIXME Make the drf2 serializer use the id (or slug) to serialize DS.belongsTo.
    //       This will enable us to remove the project_slug field.
    project: DS.belongsTo('App.Project'),
    project_slug: DS.attr('string'),
    amount: DS.attr('number'),
    status: DS.attr('string'),
    type: DS.attr('string')
});

App.CurrentOrderItem = DS.Model.extend({
    url: 'fund/orders/current/items',
});

App.LatestDonation = App.OrderItem.extend({
    url: 'fund/orders/latest/donations'
});

App.CurrentDonation = App.OrderItem.extend({
    url: 'fund/orders/current/donations'
});

App.Voucher =  App.OrderItem.extend({
    url: 'fund/vouchers',
    receiver_name: DS.attr('string', {defaultValue: ''}),
    receiver_email: DS.attr('string'),
    sender_name: DS.attr('string', {defaultValue: ''}),
    sender_email: DS.attr('string'),
    message: DS.attr('string', {defaultValue: ''}),
    sender_email: DS.attr('string'),
    language: DS.attr('string', {defaultValue: 'en'}),
    amount: DS.attr('number', {defaultValue: 25})
});

App.CurrentVoucher = App.Voucher.extend({
    url: 'fund/orders/current/vouchers',
    order: DS.belongsTo('App.CurrentOrderItem')
});

App.PaymentInfo = DS.Model.extend({
    url: 'fund/paymentinfo',
    payment_method: DS.attr('number'),
    amount: DS.attr('number'),
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    address: DS.attr('string'),
    city: DS.attr('string'),
    country: DS.attr('string'),
    zipCode: DS.attr('string'),
    payment_url: DS.attr('string')
});

App.PaymentMethodInfo = DS.Model.extend({
    url: 'fund/paymentmethodinfo',
    payment_url: DS.attr('string'),
    bank_account_number: DS.attr('string'),
    bank_account_name: DS.attr('string'),
    bank_account_city: DS.attr('string'),
});

App.Payment = DS.Model.extend({
    url: 'fund/payments',
    payment_method: DS.attr('string'),
    amount: DS.attr('number'),
    status: DS.attr('string')
});

/*
 Controllers
 */

App.CurrentOrderItemListController = Em.ArrayController.extend({

    updateOrderItem: function(orderItem, newAmount) {
        var transaction = App.store.transaction();
        transaction.add(orderItem);
        orderItem.set('amount', newAmount);
        transaction.commit();
    },

    deleteOrderItem: function(orderItem) {
        var transaction = App.store.transaction();
        transaction.add(orderItem);
        orderItem.deleteRecord();
        transaction.commit();
    }
});

App.CurrentOrderVoucherListController = Em.ArrayController.extend({

    count: function(){
        return this.get('content.length') -1;
    }.property('content.length'),

    amount: function(){
        // Calculate the total amount of Vouchers that are added to the list, not the one that's being edited in the form.
        var amount = 0;
        this.forEach(function(item){
            if (! item.get('isDirty')) {
                amount += item.get('amount');
            }
        });
        return amount;
    }.property('content.length'),

    deleteOrderItem: function(item) {
        var transaction = App.store.transaction();
        transaction.add(item);
        item.deleteRecord();
        transaction.commit();
    }
});

App.CurrentOrderVoucherAddController = Em.ObjectController.extend({

    createNewVoucher: function() {
        var transaction = App.store.transaction();
        var voucher =  transaction.createRecord(App.CurrentVoucher);
        voucher.set('sender_name', App.userController.get('content.full_name'));
        voucher.set('sender_email', App.userController.get('content.email'));
        this.set('content', voucher);
        this.set('transaction', transaction);

    },

    addVoucher: function(){
        var voucher = this.get('content');
        var controller = this;
        voucher.on('didCreate', function(record) {
            controller.createNewVoucher();
            controller.set('content.sender_name', record.get('sender_name'));
            controller.set('content.sender_email', record.get('sender_email'));
        });
        voucher.on('becameInvalid', function(record) {
            controller.get('content').set('errors', record.get('errors'));
        });
        this.get('transaction').commit();

    }

});

App.PaymentOrderProfileController = Em.ObjectController.extend({

    initTransaction: function(){
        var transaction = App.store.transaction();
        this.set('transaction', transaction);
        transaction.add(this.get('content'));
    }.observes('content'),

    updateProfile: function(){
        var profile = this.get('content');
        var controller = this;
        // We should at least have an email address
        if (!profile.get('isDirty') && profile.get('email')) {
            // No changes. No need to commit.
            controller.transitionTo('currentPaymentMethodInfo');
        }
        this.get('transaction').commit();
        profile.on('didUpdate', function(record) {
            controller.transitionTo('currentPaymentMethodInfo');
        });
        // TODO: Validate data and return errors here
        profile.on('becameInvalid', function(record) {
            controller.get('content').set('errors', record.get('errors'));
        });
    }
});

App.CurrentOrderController = Em.ObjectController.extend({

    isIdeal: function(){
        return (this.get('content.payment_method_id') == 'dd-ideal');
    }.property('content.payment_method_id'),

    isDirectDebit: function(){
        return (this.get('content.payment_method_id') == 'dd-direct-debit');
    }.property('content.payment_method_id'),

    initTransaction: function(){
        var order = this.get('content');
        var transaction = App.get('store').transaction();
        this.set('transaction', transaction);
        transaction.add(order);
    }.observes('content'),

    updateOrder: function(){
        if (this.get('content.isDirty')) {
            var controller = this;
            var order = this.get('content');
            this.get('transaction').commit();
            order.on('didUpdate', function(record){
                // Init a new private transaction.
                controller.initTransaction();
            });

        }
    }.observes('content.isDirty')
});

App.CurrentOrderPaymentController = Em.ObjectController.extend({
    contentBinding: App.CurrentOrderController.content
});

App.CurrentPaymentMethodInfoController = Em.ObjectController.extend({

});

/*
 Views
 */

App.CurrentOrderView = Em.View.extend({
    templateName: 'current_order'
});

App.PaymentOrderProfileView = Em.View.extend({
    templateName: 'payment_order_profile',
    tagName: 'form',
    submit: function(e){
        e.preventDefault();
        this.controller.updateProfile();
    }
});

App.CurrentOrderItemListView = Em.View.extend({
    templateName: 'currentorderitem_form',
    tagName: 'form'
});

App.CurrentOrderVoucherListView = Em.View.extend({
    templateName: 'current_order_voucher_list',
    tagName: 'div',
    classNames: ['content']
});

App.FinalOrderItemListView = Em.View.extend({
    templateName: 'final_order_item_list',
    tagName: 'div'
});

App.CurrentOrderItemView = Em.View.extend({
    templateName: 'currentorderitem',
    tagName: 'li',
    classNames: 'donation-project',

    neededAfterDonation: function() {
        return this.get('content.project.money_needed_natural') - this.get('content.amount');
    }.property('content.amount', 'content.project.money_needed_natural'),

    change: function(e){
        this.get('controller').updateOrderItem(this.get('content'), Em.get(e, 'target.value'));
    },

    delete: function(item){
        var controller = this.get('controller');
        this.$().slideUp(500, function(){controller.deleteOrderItem(item)});
    },
    submit: function(e){
        e.preventDefault();
    }

});

App.CurrentOrderVoucherView = Em.View.extend({
    templateName: 'current_order_voucher',
    tagName: 'li',
    classNames: ['voucher-item'],

    delete: function(){
        var controller = this.get('controller');
        var item = this.get('content');
        this.$().slideUp(500, function(){controller.deleteOrderItem(item)});
    },
    submit: function(e){
        e.preventDefault();
    }

});

App.CurrentOrderVoucherAddView = Em.View.extend({
    templateName: 'current_order_voucher_add',
    tagName: 'form',
    classNames: ['labeled']
});

App.OrderNavView = Ember.View.extend({
    tagName: 'li',

    didInsertElement: function () {
        this._super();
        if (this.get('childViews.firstObject.active')) {
            this.setOrderProgress();
        }
    },

    childBecameActive: function(sender, key) {
        if (this.get(key) && this.state == "inDOM") {
            this.setOrderProgress()
        }
    }.observes('childViews.firstObject.active'),

    setOrderProgress: function() {
        var highlightClassName = 'is-selected';
        this.$().prevAll().addClass(highlightClassName);
        this.$().nextAll().removeClass(highlightClassName);
        this.$().addClass(highlightClassName);
    }

});

App.CurrentOrderPaymentView = Em.View.extend({
    tagName: 'div',
    classNames: ['content'],
    templateName: 'order_payment'
});

App.CurrentPaymentMethodInfoView = Em.View.extend({
    tagName: 'div',
    templateName: 'payment_method_info'
});

App.IdealPaymentMethodInfoView = Em.View.extend({
    tagName: 'form',
    templateName: 'ideal_payment_method_info'
});

App.DirectDebitPaymentMethodInfoView = Em.View.extend({
    tagName: 'form',
    templateName: 'direct_debit_payment_method_info',

    submit: function(e){
        e.preventDefault();
    }
});

App.VoucherStartView = Em.View.extend({
    tagName: 'div',
    templateName: 'vouchers_start'
});

<commit_msg>Remove duplicate field in Voucher ember model.<commit_after>/*
 Models
 */

// Maybe we can move this to the currentOrderController?
App.bankList = [
    Ember.Object.create({value:"0081", title: "Fortis"}),
    Ember.Object.create({value:"0021", title: "Rabobank"}),
    Ember.Object.create({value:"0721", title: "ING Bank"}),
    Ember.Object.create({value:"0751", title: "SNS Bank"}),
    Ember.Object.create({value:"0031", title: "ABN Amro Bank"}),
    Ember.Object.create({value:"0761", title: "ASN Bank"}),
    Ember.Object.create({value:"0771", title: "SNS Regio Bank"}),
    Ember.Object.create({value:"0511", title: "Triodos Bank"}),
    Ember.Object.create({value:"0091", title: "Friesland Bank"}),
    Ember.Object.create({value:"0161", title: "Van Lanschot Bankiers"})
];

App.PaymentMethod = DS.Model.extend({
    url: 'fund/paymentmethods',
    name: DS.attr('string'),
    order: DS.belongsTo('App.Order')
});

App.Order = DS.Model.extend({
    url: 'fund/orders',
    amount: DS.attr('number'),
    status: DS.attr('string'),
    recurring: DS.attr('string'),
    payment_method_id: DS.attr('string'),
    payment_submethod_id: DS.attr('string'),
    payment_methods: DS.hasMany('App.PaymentMethod')
});

App.PaymentOrderProfile = DS.Model.extend({
    url: 'fund/paymentorderprofiles',
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    email: DS.attr('string'),
    street: DS.attr('string'),
    city: DS.attr('string'),
    country: DS.attr('string'),
    postalCode: DS.attr('string')
});

App.OrderItem = DS.Model.extend({
    url: 'fund/orders/:order_id/items',

    // Model fields
    // FIXME Make the drf2 serializer use the id (or slug) to serialize DS.belongsTo.
    //       This will enable us to remove the project_slug field.
    project: DS.belongsTo('App.Project'),
    project_slug: DS.attr('string'),
    amount: DS.attr('number'),
    status: DS.attr('string'),
    type: DS.attr('string')
});

App.CurrentOrderItem = DS.Model.extend({
    url: 'fund/orders/current/items',
});

App.LatestDonation = App.OrderItem.extend({
    url: 'fund/orders/latest/donations'
});

App.CurrentDonation = App.OrderItem.extend({
    url: 'fund/orders/current/donations'
});

App.Voucher =  App.OrderItem.extend({
    url: 'fund/vouchers',
    receiver_name: DS.attr('string', {defaultValue: ''}),
    receiver_email: DS.attr('string'),
    sender_name: DS.attr('string', {defaultValue: ''}),
    sender_email: DS.attr('string'),
    message: DS.attr('string', {defaultValue: ''}),
    language: DS.attr('string', {defaultValue: 'en'}),
    amount: DS.attr('number', {defaultValue: 25})
});

App.CurrentVoucher = App.Voucher.extend({
    url: 'fund/orders/current/vouchers',
    order: DS.belongsTo('App.CurrentOrderItem')
});

App.PaymentInfo = DS.Model.extend({
    url: 'fund/paymentinfo',
    payment_method: DS.attr('number'),
    amount: DS.attr('number'),
    firstName: DS.attr('string'),
    lastName: DS.attr('string'),
    address: DS.attr('string'),
    city: DS.attr('string'),
    country: DS.attr('string'),
    zipCode: DS.attr('string'),
    payment_url: DS.attr('string')
});

App.PaymentMethodInfo = DS.Model.extend({
    url: 'fund/paymentmethodinfo',
    payment_url: DS.attr('string'),
    bank_account_number: DS.attr('string'),
    bank_account_name: DS.attr('string'),
    bank_account_city: DS.attr('string'),
});

App.Payment = DS.Model.extend({
    url: 'fund/payments',
    payment_method: DS.attr('string'),
    amount: DS.attr('number'),
    status: DS.attr('string')
});

/*
 Controllers
 */

App.CurrentOrderItemListController = Em.ArrayController.extend({

    updateOrderItem: function(orderItem, newAmount) {
        var transaction = App.store.transaction();
        transaction.add(orderItem);
        orderItem.set('amount', newAmount);
        transaction.commit();
    },

    deleteOrderItem: function(orderItem) {
        var transaction = App.store.transaction();
        transaction.add(orderItem);
        orderItem.deleteRecord();
        transaction.commit();
    }
});

App.CurrentOrderVoucherListController = Em.ArrayController.extend({

    count: function(){
        return this.get('content.length') -1;
    }.property('content.length'),

    amount: function(){
        // Calculate the total amount of Vouchers that are added to the list, not the one that's being edited in the form.
        var amount = 0;
        this.forEach(function(item){
            if (! item.get('isDirty')) {
                amount += item.get('amount');
            }
        });
        return amount;
    }.property('content.length'),

    deleteOrderItem: function(item) {
        var transaction = App.store.transaction();
        transaction.add(item);
        item.deleteRecord();
        transaction.commit();
    }
});

App.CurrentOrderVoucherAddController = Em.ObjectController.extend({

    createNewVoucher: function() {
        var transaction = App.store.transaction();
        var voucher =  transaction.createRecord(App.CurrentVoucher);
        voucher.set('sender_name', App.userController.get('content.full_name'));
        voucher.set('sender_email', App.userController.get('content.email'));
        this.set('content', voucher);
        this.set('transaction', transaction);

    },

    addVoucher: function(){
        var voucher = this.get('content');
        var controller = this;
        voucher.on('didCreate', function(record) {
            controller.createNewVoucher();
            controller.set('content.sender_name', record.get('sender_name'));
            controller.set('content.sender_email', record.get('sender_email'));
        });
        voucher.on('becameInvalid', function(record) {
            controller.get('content').set('errors', record.get('errors'));
        });
        this.get('transaction').commit();

    }

});

App.PaymentOrderProfileController = Em.ObjectController.extend({

    initTransaction: function(){
        var transaction = App.store.transaction();
        this.set('transaction', transaction);
        transaction.add(this.get('content'));
    }.observes('content'),

    updateProfile: function(){
        var profile = this.get('content');
        var controller = this;
        // We should at least have an email address
        if (!profile.get('isDirty') && profile.get('email')) {
            // No changes. No need to commit.
            controller.transitionTo('currentPaymentMethodInfo');
        }
        this.get('transaction').commit();
        profile.on('didUpdate', function(record) {
            controller.transitionTo('currentPaymentMethodInfo');
        });
        // TODO: Validate data and return errors here
        profile.on('becameInvalid', function(record) {
            controller.get('content').set('errors', record.get('errors'));
        });
    }
});

App.CurrentOrderController = Em.ObjectController.extend({

    isIdeal: function(){
        return (this.get('content.payment_method_id') == 'dd-ideal');
    }.property('content.payment_method_id'),

    isDirectDebit: function(){
        return (this.get('content.payment_method_id') == 'dd-direct-debit');
    }.property('content.payment_method_id'),

    initTransaction: function(){
        var order = this.get('content');
        var transaction = App.get('store').transaction();
        this.set('transaction', transaction);
        transaction.add(order);
    }.observes('content'),

    updateOrder: function(){
        if (this.get('content.isDirty')) {
            var controller = this;
            var order = this.get('content');
            this.get('transaction').commit();
            order.on('didUpdate', function(record){
                // Init a new private transaction.
                controller.initTransaction();
            });

        }
    }.observes('content.isDirty')
});

App.CurrentOrderPaymentController = Em.ObjectController.extend({
    contentBinding: App.CurrentOrderController.content
});

App.CurrentPaymentMethodInfoController = Em.ObjectController.extend({

});

/*
 Views
 */

App.CurrentOrderView = Em.View.extend({
    templateName: 'current_order'
});

App.PaymentOrderProfileView = Em.View.extend({
    templateName: 'payment_order_profile',
    tagName: 'form',
    submit: function(e){
        e.preventDefault();
        this.controller.updateProfile();
    }
});

App.CurrentOrderItemListView = Em.View.extend({
    templateName: 'currentorderitem_form',
    tagName: 'form'
});

App.CurrentOrderVoucherListView = Em.View.extend({
    templateName: 'current_order_voucher_list',
    tagName: 'div',
    classNames: ['content']
});

App.FinalOrderItemListView = Em.View.extend({
    templateName: 'final_order_item_list',
    tagName: 'div'
});

App.CurrentOrderItemView = Em.View.extend({
    templateName: 'currentorderitem',
    tagName: 'li',
    classNames: 'donation-project',

    neededAfterDonation: function() {
        return this.get('content.project.money_needed_natural') - this.get('content.amount');
    }.property('content.amount', 'content.project.money_needed_natural'),

    change: function(e){
        this.get('controller').updateOrderItem(this.get('content'), Em.get(e, 'target.value'));
    },

    delete: function(item){
        var controller = this.get('controller');
        this.$().slideUp(500, function(){controller.deleteOrderItem(item)});
    },
    submit: function(e){
        e.preventDefault();
    }

});

App.CurrentOrderVoucherView = Em.View.extend({
    templateName: 'current_order_voucher',
    tagName: 'li',
    classNames: ['voucher-item'],

    delete: function(){
        var controller = this.get('controller');
        var item = this.get('content');
        this.$().slideUp(500, function(){controller.deleteOrderItem(item)});
    },
    submit: function(e){
        e.preventDefault();
    }

});

App.CurrentOrderVoucherAddView = Em.View.extend({
    templateName: 'current_order_voucher_add',
    tagName: 'form',
    classNames: ['labeled']
});

App.OrderNavView = Ember.View.extend({
    tagName: 'li',

    didInsertElement: function () {
        this._super();
        if (this.get('childViews.firstObject.active')) {
            this.setOrderProgress();
        }
    },

    childBecameActive: function(sender, key) {
        if (this.get(key) && this.state == "inDOM") {
            this.setOrderProgress()
        }
    }.observes('childViews.firstObject.active'),

    setOrderProgress: function() {
        var highlightClassName = 'is-selected';
        this.$().prevAll().addClass(highlightClassName);
        this.$().nextAll().removeClass(highlightClassName);
        this.$().addClass(highlightClassName);
    }

});

App.CurrentOrderPaymentView = Em.View.extend({
    tagName: 'div',
    classNames: ['content'],
    templateName: 'order_payment'
});

App.CurrentPaymentMethodInfoView = Em.View.extend({
    tagName: 'div',
    templateName: 'payment_method_info'
});

App.IdealPaymentMethodInfoView = Em.View.extend({
    tagName: 'form',
    templateName: 'ideal_payment_method_info'
});

App.DirectDebitPaymentMethodInfoView = Em.View.extend({
    tagName: 'form',
    templateName: 'direct_debit_payment_method_info',

    submit: function(e){
        e.preventDefault();
    }
});

App.VoucherStartView = Em.View.extend({
    tagName: 'div',
    templateName: 'vouchers_start'
});

<|endoftext|>


<commit_before>var stream_ui_updates = (function () {

var exports = {};

exports.update_check_button_for_sub = function (sub) {
    var button = subs.check_button_for_sub(sub);
    if (sub.subscribed) {
        button.addClass("checked");
    } else {
        button.removeClass("checked");
    }
    if (sub.should_display_subscription_button) {
        button.removeClass("disabled");
    } else {
        button.addClass("disabled");
    }
};

exports.initialize_disable_btn_hint_popover = function (btn_wrapper, popover_btn,
                                                        disabled_btn, hint_text) {
    // Disabled button blocks mouse events(hover) from reaching
    // to it's parent div element, so popover don't get triggered.
    // Add css to prevent this.
    disabled_btn.css("pointer-events", "none");
    popover_btn.popover({
        placement: "bottom",
        content: "
%s
".replace(
            '%s', hint_text),
        trigger: "manual",
        html: true,
        animation: false,
    });

    btn_wrapper.on('mouseover', function (e) {
        popover_btn.popover('show');
        e.stopPropagation();
    });

    btn_wrapper.on('mouseout', function (e) {
        popover_btn.popover('hide');
        e.stopPropagation();
    });
};

exports.initialize_cant_subscribe_popover = function (sub) {
    var button_wrapper = stream_edit.settings_for_sub(sub).find('.sub_unsub_button_wrapper');
    var settings_button = subs.settings_button_for_sub(sub);
    exports.initialize_disable_btn_hint_popover(button_wrapper, settings_button, settings_button,
                                                i18n.t("Only stream members can add users to a private stream"));
};

exports.update_settings_button_for_sub = function (sub) {
    var settings_button = subs.settings_button_for_sub(sub);
    if (sub.subscribed) {
        settings_button.text(i18n.t("Unsubscribe")).removeClass("unsubscribed");
    } else {
        settings_button.text(i18n.t("Subscribe")).addClass("unsubscribed");
    }
    if (sub.should_display_subscription_button) {
        settings_button.prop("disabled", false);
        settings_button.popover('destroy');
        settings_button.css("pointer-events", "");
    } else {
        settings_button.attr("title", "");
        exports.initialize_cant_subscribe_popover(sub);
        settings_button.attr("disabled", "disabled");
    }
};

exports.update_regular_sub_settings = function (sub) {
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }
    var $settings = $(".subscription_settings[data-stream-id='" + sub.stream_id + "']");
    if (sub.subscribed) {
        if ($settings.find(".email-address").val().length === 0) {
            // Rerender stream email address, if not.
            $settings.find(".email-address").text(sub.email_address);
            $settings.find(".stream-email-box").show();
        }
        $settings.find(".regular_subscription_settings").addClass('in');
    } else {
        $settings.find(".regular_subscription_settings").removeClass('in');
        // Clear email address widget
        $settings.find(".email-address").html("");
    }
};

exports.update_change_stream_privacy_settings = function (sub) {
    var stream_privacy_btn = $(".change-stream-privacy");

    if (sub.can_change_stream_permissions) {
        stream_privacy_btn.show();
    } else {
        stream_privacy_btn.hide();
    }
};

exports.update_stream_row_in_settings_tab = function (sub) {
    // This function display/hide stream row in stream settings tab,
    // used to display immediate effect of add/removal subscription event.
    // If user is subscribed to stream, it will show sub row under
    // "Subscribed" tab, otherwise if stream is not public hide
    // stream row under tab.
    if (subs.is_subscribed_stream_tab_active()) {
        var sub_row = subs.row_for_stream_id(sub.stream_id);
        if (sub.subscribed) {
            sub_row.removeClass("notdisplayed");
        } else if (sub.invite_only || page_params.is_guest) {
            sub_row.addClass("notdisplayed");
        }
    }
};

exports.update_stream_privacy_type_icon = function (sub) {
    var stream_settings = stream_edit.settings_for_sub(sub);
    var sub_row = subs.row_for_stream_id(sub.stream_id);
    var html = templates.render('subscription_setting_icon', sub);

    if (overlays.streams_open()) {
        sub_row.find('.icon').expectOne().replaceWith($(html));
    }
    if (stream_edit.is_sub_settings_active(sub)) {
        var large_icon = stream_settings.find('.large-icon').expectOne();
        if (sub.invite_only) {
            large_icon.removeClass("hash").addClass("lock")
                .html("");
        } else {
            large_icon.addClass("hash").removeClass("lock").html("");
        }
    }
};

exports.update_stream_privacy_type_text = function (sub) {
    var stream_settings = stream_edit.settings_for_sub(sub);
    var html = templates.render('subscription_type', sub);
    if (stream_edit.is_sub_settings_active(sub)) {
        stream_settings.find('.subscription-type-text').expectOne().html(html);
    }
};

exports.update_subscribers_count = function (sub, just_subscribed) {
    if (!overlays.streams_open()) {
        // If the streams overlay isn't open, we don't need to rerender anything.
        return;
    }
    var stream_row = subs.row_for_stream_id(sub.stream_id);
    if (!sub.can_access_subscribers || just_subscribed && sub.invite_only) {
        var rendered_sub_count = templates.render("subscription_count", sub);
        stream_row.find('.subscriber-count').expectOne().html(rendered_sub_count);
    } else {
        stream_row.find(".subscriber-count-text").expectOne().text(sub.subscriber_count);
    }
};

exports.update_subscribers_list = function (sub) {
    // Render subscriptions only if stream settings is open
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }

    if (!sub.can_access_subscribers) {
        $(".subscriber_list_settings_container").hide();
    } else {
        var emails = stream_edit.get_email_of_subscribers(sub.subscribers);
        var subscribers_list = list_render.get("stream_subscribers/" + sub.stream_id);

        // Changing the data clears the rendered list and the list needs to be re-rendered.
        // Perform re-rendering only when the stream settings form of the corresponding
        // stream is open.
        if (subscribers_list) {
            stream_edit.sort_but_pin_current_user_on_top(emails);
            subscribers_list.data(emails);
            subscribers_list.render();
            ui.update_scrollbar($(".subscriber_list_container"));
        }
        $(".subscriber_list_settings_container").show();
    }
};

exports.update_add_subscriptions_elements = function (sub) {
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }

    if (page_params.is_guest) {
        // For guest users, we just hide the add_subscribers feature.
        $('.add_subscribers_container').hide();
        return;
    }

    // Otherwise, we adjust whether the widgets are disabled based on
    // whether this user is authorized to add subscribers.
    var input_element = $('.add_subscribers_container').find('input[name="principal"]').expectOne();
    var button_element = $('.add_subscribers_container').find('button[name="add_subscriber"]').expectOne();
    var allow_user_to_add_subs = sub.can_add_subscribers;

    if (allow_user_to_add_subs) {
        input_element.removeAttr("disabled");
        button_element.removeAttr("disabled");
        button_element.css('pointer-events', "");
        $('.add_subscribers_container input').popover('destroy');
    } else {
        input_element.attr("disabled", "disabled");
        button_element.attr("disabled", "disabled");

        exports.initialize_disable_btn_hint_popover($('.add_subscribers_container'), input_element, button_element,
                                                    i18n.t("Only stream members can add users to a private stream"));
    }
};

return exports;
}());

if (typeof module !== 'undefined') {
    module.exports = stream_ui_updates;
}
window.stream_ui_updates = stream_ui_updates;
<commit_msg>subscriptions: Fix error when guest user is subscribed to public stream.<commit_after>var stream_ui_updates = (function () {

var exports = {};

exports.update_check_button_for_sub = function (sub) {
    var button = subs.check_button_for_sub(sub);
    if (sub.subscribed) {
        button.addClass("checked");
    } else {
        button.removeClass("checked");
    }
    if (sub.should_display_subscription_button) {
        button.removeClass("disabled");
    } else {
        button.addClass("disabled");
    }
};

exports.initialize_disable_btn_hint_popover = function (btn_wrapper, popover_btn,
                                                        disabled_btn, hint_text) {
    // Disabled button blocks mouse events(hover) from reaching
    // to it's parent div element, so popover don't get triggered.
    // Add css to prevent this.
    disabled_btn.css("pointer-events", "none");
    popover_btn.popover({
        placement: "bottom",
        content: "
%s
".replace(
            '%s', hint_text),
        trigger: "manual",
        html: true,
        animation: false,
    });

    btn_wrapper.on('mouseover', function (e) {
        popover_btn.popover('show');
        e.stopPropagation();
    });

    btn_wrapper.on('mouseout', function (e) {
        popover_btn.popover('hide');
        e.stopPropagation();
    });
};

exports.initialize_cant_subscribe_popover = function (sub) {
    var button_wrapper = stream_edit.settings_for_sub(sub).find('.sub_unsub_button_wrapper');
    var settings_button = subs.settings_button_for_sub(sub);
    exports.initialize_disable_btn_hint_popover(button_wrapper, settings_button, settings_button,
                                                i18n.t("Only stream members can add users to a private stream"));
};

exports.update_settings_button_for_sub = function (sub) {
    var settings_button = subs.settings_button_for_sub(sub);
    if (sub.subscribed) {
        settings_button.text(i18n.t("Unsubscribe")).removeClass("unsubscribed");
    } else {
        settings_button.text(i18n.t("Subscribe")).addClass("unsubscribed");
    }
    if (sub.should_display_subscription_button) {
        settings_button.prop("disabled", false);
        settings_button.popover('destroy');
        settings_button.css("pointer-events", "");
    } else {
        settings_button.attr("title", "");
        exports.initialize_cant_subscribe_popover(sub);
        settings_button.attr("disabled", "disabled");
    }
};

exports.update_regular_sub_settings = function (sub) {
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }
    var $settings = $(".subscription_settings[data-stream-id='" + sub.stream_id + "']");
    if (sub.subscribed) {
        if ($settings.find(".email-address").val().length === 0) {
            // Rerender stream email address, if not.
            $settings.find(".email-address").text(sub.email_address);
            $settings.find(".stream-email-box").show();
        }
        $settings.find(".regular_subscription_settings").addClass('in');
    } else {
        $settings.find(".regular_subscription_settings").removeClass('in');
        // Clear email address widget
        $settings.find(".email-address").html("");
    }
};

exports.update_change_stream_privacy_settings = function (sub) {
    var stream_privacy_btn = $(".change-stream-privacy");

    if (sub.can_change_stream_permissions) {
        stream_privacy_btn.show();
    } else {
        stream_privacy_btn.hide();
    }
};

exports.update_stream_row_in_settings_tab = function (sub) {
    // This function display/hide stream row in stream settings tab,
    // used to display immediate effect of add/removal subscription event.
    // If user is subscribed to stream, it will show sub row under
    // "Subscribed" tab, otherwise if stream is not public hide
    // stream row under tab.
    if (subs.is_subscribed_stream_tab_active()) {
        var sub_row = subs.row_for_stream_id(sub.stream_id);
        if (sub.subscribed) {
            sub_row.removeClass("notdisplayed");
        } else if (sub.invite_only || page_params.is_guest) {
            sub_row.addClass("notdisplayed");
        }
    }
};

exports.update_stream_privacy_type_icon = function (sub) {
    var stream_settings = stream_edit.settings_for_sub(sub);
    var sub_row = subs.row_for_stream_id(sub.stream_id);
    var html = templates.render('subscription_setting_icon', sub);

    if (overlays.streams_open()) {
        sub_row.find('.icon').expectOne().replaceWith($(html));
    }
    if (stream_edit.is_sub_settings_active(sub)) {
        var large_icon = stream_settings.find('.large-icon').expectOne();
        if (sub.invite_only) {
            large_icon.removeClass("hash").addClass("lock")
                .html("");
        } else {
            large_icon.addClass("hash").removeClass("lock").html("");
        }
    }
};

exports.update_stream_privacy_type_text = function (sub) {
    var stream_settings = stream_edit.settings_for_sub(sub);
    var html = templates.render('subscription_type', sub);
    if (stream_edit.is_sub_settings_active(sub)) {
        stream_settings.find('.subscription-type-text').expectOne().html(html);
    }
};

exports.update_subscribers_count = function (sub, just_subscribed) {
    if (!overlays.streams_open()) {
        // If the streams overlay isn't open, we don't need to rerender anything.
        return;
    }
    var stream_row = subs.row_for_stream_id(sub.stream_id);
    if (!sub.can_access_subscribers || just_subscribed && sub.invite_only || page_params.is_guest) {
        var rendered_sub_count = templates.render("subscription_count", sub);
        stream_row.find('.subscriber-count').expectOne().html(rendered_sub_count);
    } else {
        stream_row.find(".subscriber-count-text").expectOne().text(sub.subscriber_count);
    }
};

exports.update_subscribers_list = function (sub) {
    // Render subscriptions only if stream settings is open
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }

    if (!sub.can_access_subscribers) {
        $(".subscriber_list_settings_container").hide();
    } else {
        var emails = stream_edit.get_email_of_subscribers(sub.subscribers);
        var subscribers_list = list_render.get("stream_subscribers/" + sub.stream_id);

        // Changing the data clears the rendered list and the list needs to be re-rendered.
        // Perform re-rendering only when the stream settings form of the corresponding
        // stream is open.
        if (subscribers_list) {
            stream_edit.sort_but_pin_current_user_on_top(emails);
            subscribers_list.data(emails);
            subscribers_list.render();
            ui.update_scrollbar($(".subscriber_list_container"));
        }
        $(".subscriber_list_settings_container").show();
    }
};

exports.update_add_subscriptions_elements = function (sub) {
    if (!stream_edit.is_sub_settings_active(sub)) {
        return;
    }

    if (page_params.is_guest) {
        // For guest users, we just hide the add_subscribers feature.
        $('.add_subscribers_container').hide();
        return;
    }

    // Otherwise, we adjust whether the widgets are disabled based on
    // whether this user is authorized to add subscribers.
    var input_element = $('.add_subscribers_container').find('input[name="principal"]').expectOne();
    var button_element = $('.add_subscribers_container').find('button[name="add_subscriber"]').expectOne();
    var allow_user_to_add_subs = sub.can_add_subscribers;

    if (allow_user_to_add_subs) {
        input_element.removeAttr("disabled");
        button_element.removeAttr("disabled");
        button_element.css('pointer-events', "");
        $('.add_subscribers_container input').popover('destroy');
    } else {
        input_element.attr("disabled", "disabled");
        button_element.attr("disabled", "disabled");

        exports.initialize_disable_btn_hint_popover($('.add_subscribers_container'), input_element, button_element,
                                                    i18n.t("Only stream members can add users to a private stream"));
    }
};

return exports;
}());

if (typeof module !== 'undefined') {
    module.exports = stream_ui_updates;
}
window.stream_ui_updates = stream_ui_updates;
<|endoftext|>


<commit_before>ajax = function(uri, method, data, is_async, sucess, error) {
	var request = {
		url: uri,
		type: method,
		contentType: "application/json",
		accepts: "application/json",
		cache: false,
		dataType: 'json',
		async: is_async,
		data: JSON.stringify(data),
		success: function() {
			serverDown = false;
		},
		error: function(jqXHR) {
			console.log("ajax error " + jqXHR.status);
			serverDown = true;
		}
	};
	return $.ajax(request);
}

setFanSpeed = function(speed_percentage) {
	ajax("/fan/" + speed_percentage, "PUT", null, true, setFanSpeed_success, default_error);
}

setFanSpeed_success = function() {
	alert("TODO")
}

default_error = function (jqXHR) {
	console.log("ajax error " + jqXHR.status);
	serverDown = true;
}<commit_msg>[FanControl] Refactored.<commit_after>do_nothing = function() {}
default_error = function (jqXHR) {
			console.log("ajax error " + jqXHR.status);
			serverDown = true;
}

ajax = function(uri, method, request_data, is_async, success_callback, error_callback) {
	var request = {
		url: uri,
		type: method,
		contentType: "application/json",
		accepts: "application/json",
		cache: false,
		dataType: 'json',
		async: is_async,
		success: function(data) {
			serverDown = false;
			success_callback(data);
		},
		error: function(jqXHR) {
			default_error();
			error_callback();
		}
	};

	if (request_data != null)
		request.data = JSON.stringify(request_data);

	return $.ajax(request);
}

FanSlider = function() {
	var self = this;
	var self.onChangeCallbackEnabled = true;

	self.initSlider = function () {
		ajax("/fan/", "GET", null, false, do_nothing, do_nothing).done(function(data) {
			// Initilaize slider with value
			$( "#slider" ).slider({
				value: data.speed
			});

			// Set speed with slider
			$( "#slider" ).on( "slidechange", function( event, ui ) {
				if (onChangeCallbackEnabled)
					setFanSpeed($( "#slider" ).slider( "value" ));
			});
		});
	}

	self.setSlider = function(data) {
		console.log("setting slider to " + data.speed);
		onChangeCallbackEnabled = false;
		$( "#slider" ).slider( "option", "value", data.speed);
		onChangeCallbackEnabled = true;
	}

	self.setFanSpeed = function(speed_percentage) {
		ajax("/fan/" + speed_percentage, "PUT", null, true, do_nothing, do_nothing);
	}

	getFanSpeed = function() {
		ajax("/fan/", "GET", null, false, do_nothing, do_nothing).done(function(data) { FanSlider.setSlider(data); });
	}

}

FanSlider.initSlider();
<|endoftext|>