<commit_before>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* Constants */
var URL = "ws://localhost:30001/";
var VERSION = 1; /* Note: the extension must always be backward compatible */
var MAXLOGGERLEN = 20;
var RETRY_TIMEOUT = 5;
/* String to copy to the clipboard if it should be empty */
var DUMMY_EMPTYSTRING = "%";

LogLevel = {
    ERROR : "error",
    INFO : "info",
    DEBUG : "debug"
}

/* Global variables */
var clipboardholder_; /* textarea used to hold clipboard content */
var timeout_ = null; /* Set if a timeout is active */
var websocket_ = null; /* Active connection */

/* State variables */
var debug_ = false;
var enabled_ = true; /* true if we are trying to connect */
var active_ = false; /* true if we are connected to a server */
var error_ = false; /* true if there was an error during the last connection */
var dummystr_ = false; /* true if the last string we copied was the dummy string */
var update_ = false; /* true if an update to the extension is available */

var status_ = "";
var logger_ = []; /* Array of status messages: [LogLevel, time, message] */

/* Set the current status string.
 * active is a boolean, true if the WebSocket connection is established. */
function setStatus(status, active) {
    active_ = active;
    status_ = status;

    /* Apply update if the extension is not active */
    if (update_ && !active_)
        chrome.runtime.reload();

    refreshUI();
}

function showHelp() {
    chrome.tabs.create({url: "first.html"});
}

function updateAvailable(version) {
    printLog("A new version of the extension is available (" +
             version + ").", LogLevel.INFO);

    /* Apply update immediately if the extension is not active */
    if (!active_)
        chrome.runtime.reload();
    else
        update_ = true;
}

function checkUpdate() {
    chrome.runtime.requestUpdateCheck(function (status, details) {
        printLog("Update status=" + status, LogLevel.DEBUG);
        if (status == "update_available") {
            updateAvailable(details.version);
        }
    });
}

/* Update the icon, and refresh the popup page */
function refreshUI() {
    if (error_)
        icon = "error"
    else if (!enabled_)
        icon = "disabled"
    else if (active_)
        icon = "connected";
    else
        icon = "disconnected";

    chrome.browserAction.setIcon(
        {path: {'19': icon + '-19.png', '38': icon + '-38.png'}}
    );

    var views = chrome.extension.getViews({type: "popup"});
    for (var i = 0; i < views.length; views++) {
        /* Make sure page is ready */
        if (document.readyState === "complete") {
            /* Update "help" link */
            helplink = views[i].document.getElementById("help");
            helplink.onclick = showHelp;
            /* Update enable/disable link. */
            /* FIXME: Sometimes, there is a little box coming around the link */
            enablelink = views[i].document.getElementById("enable");
            if (enabled_) {
                enablelink.textContent = "Disable";
                enablelink.onclick = function() {
                    console.log("Disable click");
                    enabled_ = false;
                    if (websocket_ != null)
                        websocket_.close();
                    else
                        websocketConnect(); /* Clear timeout and display message */
                    refreshUI();
                }
            } else {
                enablelink.textContent = "Enable";
                enablelink.onclick = function() {
                    console.log("Enable click");
                    enabled_ = true;
                    if (websocket_ == null)
                        websocketConnect();
                    refreshUI();
                }
            }

            /* Update debug mode according to checkbox state. */
            debugcheck = views[i].document.getElementById("debugcheck");
            debugcheck.onclick = function() {
                debug_ = debugcheck.checked;
                refreshUI();
            }
            debugcheck.checked = debug_;

            /* Update status box */
            views[i].document.getElementById("info").textContent = status_;

            /* Update logger table */
            loggertable = views[i].document.getElementById("logger");

            /* FIXME: only update needed rows */
            while (loggertable.rows.length > 0) {
                loggertable.deleteRow(0);
            }

            for (i = 0; i < logger_.length; i++) {
                value = logger_[i];

                if (value[0] == LogLevel.DEBUG && !debug_)
                    continue;

                var row = loggertable.insertRow(-1);
                var cell1 = row.insertCell(0);
                var cell2 = row.insertCell(1);
                var levelclass = value[0];
                cell1.className = "time " + levelclass;
                cell2.className = "value " + levelclass;
                cell1.innerHTML = value[1];
                cell2.innerHTML = value[2];
            }
        }
    }
}

/* Start the extension */
function clipboardStart() {
    printLog("Crouton extension started (" +
             chrome.runtime.getManifest().version + ")!", LogLevel.INFO);
    setStatus("Started...", false);

    clipboardholder_ = document.getElementById("clipboardholder");

    websocketConnect();
}

/* Connect to the server */
function websocketConnect() {
    /* Clear timeout if we were called manually. */
    if (timeout_ != null) {
        clearTimeout(timeout_);
        timeout_ = null;
    }

    if (!enabled_) {
        setStatus("No connection (extension disabled).", false);
        printLog("Extension is disabled.", LogLevel.INFO);
        return;
    }

    if (websocket_ != null) {
        printLog("Socket already open", LogLevel.DEBUG);
        return;
    }

    console.log("websocketConnect: " + websocket_);

    printLog("Opening a web socket", LogLevel.DEBUG);
    error_ = false;
    setStatus("Connecting...", false);
    websocket_ = new WebSocket(URL);
    websocket_.onopen = websocketOpen;
    websocket_.onmessage = websocketMessage;
    websocket_.onclose = websocketClose;
}

/* Connection was established */
function websocketOpen() {
    printLog("Connection established.", LogLevel.INFO);
    setStatus("Connection established: checking version...", false);
}

function readClipboard() {
    clipboardholder_.value = "";
    clipboardholder_.select();
    document.execCommand("Paste");
    return clipboardholder_.value;
}

function writeClipboard(str) {
    clipboardholder_.value = str;
    clipboardholder_.select();
    document.execCommand("Copy");
}

/* Received a message from the server */
function websocketMessage(evt) {
    var received_msg = evt.data;
    var cmd = received_msg[0];
    var payload = received_msg.substring(1);

    printLog("Message is received (" + received_msg + ")", LogLevel.DEBUG);

    /* Only accept version packets until we have received one. */
    if (!active_) {
        if (cmd == 'V') { /* Version */
            if (payload < 1 || payload > VERSION) {
                websocket_.send("EInvalid version (> " + VERSION + ")");
                error("Invalid server version " +
                                payload + " > " + VERSION + ".", false);
            }
            /* Set active_ to true */
            setStatus("Connection established.", true);
            websocket_.send("VOK");
            return;
        } else {
            error("Received frame while waiting for version.", false);
        }
    }

    switch(cmd) {
    case 'W': /* Write */
        var clip = readClipboard();

        /* Do not erase identical clipboard content */
        if (clip != payload) {
             /* We cannot write an empty string: Write DUMMY instead */
            if (payload == "") {
                writeClipboard(DUMMY_EMPTYSTRING);
                dummystr_ = true;
            } else {
                writeClipboard(payload);
                dummystr_ = false;
            }
        } else if (payload == DUMMY_EMPTYSTRING) {
            /* Unlikely case where DUMMY string comes from the other side */
            writeClipboard(payload);
            dummystr_ = false;
        } else {
            printLog("Not erasing content (identical).", LogLevel.DEBUG);
        }

        websocket_.send("WOK");

        break;
    case 'R': /* Read */
        var clip = readClipboard();

        if (clip == DUMMY_EMPTYSTRING && dummystr_) {
            websocket_.send("R");
        } else {
            websocket_.send("R" + clip);
        }

        break;
    case 'U': /* Open an URL */
        /* URL must be absolute: see RFC 3986 for syntax (section 3.1) */
        if (match = (/^([a-z][a-z0-9+-.]*):/i).exec(payload)) {
            /* FIXME: we could blacklist schemes using match[1] here */
            chrome.tabs.create({ url: payload });
            websocket_.send("UOK");
        } else {
            printLog("Received invalid URL: " + payload, LogLevel.ERROR);
            websocket_.send("EError: URL must be absolute.");
        }

        break;
    case 'P': /* Ping */
        websocket_.send(received_msg);
        break;
    case 'E':
        error("Server error: " + payload, 1);
        break;
    default:
        error("Invalid packet from server: " + received_msg, 1);
        break;
    }
}

/* Connection was closed (or never established) */
function websocketClose() {
    if (websocket_ == null) {
        console.log("websocketClose: null!");
        return;
    }

    if (enabled_) {
        setStatus("No connection (retrying in " +
                                    RETRY_TIMEOUT + " seconds)", false);
        printLog("Connection is closed, trying again in " +
                            RETRY_TIMEOUT + " seconds...", LogLevel.INFO);
        /* Retry in RETRY_TIMEOUT seconds */
        if (timeout_ == null) {
            timeout_ = setTimeout(websocketConnect, RETRY_TIMEOUT*1000);
        }
    } else {
        setStatus("No connection (extension disabled).", false);
        printLog("Connection is closed, extension is disabled: not retrying.",
                                                            LogLevel.INFO);
    }

    websocket_ = null;

    /* Check for update on every disconnect */
    checkUpdate();
}

function padstr0(i) {
    var s = i + "";
    if (s.length < 2)
        return "0" + s;
    else
        return s;
}

/* Add a message in the log. */
function printLog(str, level) {
    date = new Date;
    datestr = padstr0(date.getHours()) + ":" +
              padstr0(date.getMinutes()) + ":" +
              padstr0(date.getSeconds());

    if (str.length > 80)
        str = str.substring(0, 77) + "...";
    console.log(datestr + ": " + str);

    /* Add messages to logger */
    if (level != LogLevel.DEBUG || debug_) {
        logger_.unshift([level, datestr, str]);
        if (logger_.length > MAXLOGGERLEN) {
            logger_.pop();
        }
        refreshUI();
    }
}

/* Display an error, and prevent retries if enabled is false */
function error(str, enabled) {
    printLog(str, LogLevel.ERROR);
    enabled_ = enabled;
    error_ = true;
    refreshUI();
    websocket_.close();
    /* Check for extension update (possible reason for the error) */
    checkUpdate();
}

/* On error: disconnect WebSocket, then log errors */
onerror = function(msg, url, line) {
    if (websocket_)
        websocket_.close();
    error("Uncaught JS error: " + msg, false);
    return true;
}

/* Start the extension as soon as the background page is loaded */
document.addEventListener('DOMContentLoaded', clipboardStart);

chrome.runtime.onInstalled.addListener(function(details) {
    if (details.reason == "install") {
        /* Open help tab on first install */
        showHelp();
    }
}
)

chrome.runtime.onUpdateAvailable.addListener(function(details) {
    updateAvailable(details.version);
});
<commit_msg>Fix dummy string logic, so that % can be copied from Chromium OS to chroot.<commit_after>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* Constants */
var URL = "ws://localhost:30001/";
var VERSION = 1; /* Note: the extension must always be backward compatible */
var MAXLOGGERLEN = 20;
var RETRY_TIMEOUT = 5;
/* String to copy to the clipboard if it should be empty */
var DUMMY_EMPTYSTRING = "%";

LogLevel = {
    ERROR : "error",
    INFO : "info",
    DEBUG : "debug"
}

/* Global variables */
var clipboardholder_; /* textarea used to hold clipboard content */
var timeout_ = null; /* Set if a timeout is active */
var websocket_ = null; /* Active connection */

/* State variables */
var debug_ = false;
var enabled_ = true; /* true if we are trying to connect */
var active_ = false; /* true if we are connected to a server */
var error_ = false; /* true if there was an error during the last connection */
var dummystr_ = false; /* true if the last string we copied was the dummy string */
var update_ = false; /* true if an update to the extension is available */

var status_ = "";
var logger_ = []; /* Array of status messages: [LogLevel, time, message] */

/* Set the current status string.
 * active is a boolean, true if the WebSocket connection is established. */
function setStatus(status, active) {
    active_ = active;
    status_ = status;

    /* Apply update if the extension is not active */
    if (update_ && !active_)
        chrome.runtime.reload();

    refreshUI();
}

function showHelp() {
    chrome.tabs.create({url: "first.html"});
}

function updateAvailable(version) {
    printLog("A new version of the extension is available (" +
             version + ").", LogLevel.INFO);

    /* Apply update immediately if the extension is not active */
    if (!active_)
        chrome.runtime.reload();
    else
        update_ = true;
}

function checkUpdate() {
    chrome.runtime.requestUpdateCheck(function (status, details) {
        printLog("Update status=" + status, LogLevel.DEBUG);
        if (status == "update_available") {
            updateAvailable(details.version);
        }
    });
}

/* Update the icon, and refresh the popup page */
function refreshUI() {
    if (error_)
        icon = "error"
    else if (!enabled_)
        icon = "disabled"
    else if (active_)
        icon = "connected";
    else
        icon = "disconnected";

    chrome.browserAction.setIcon(
        {path: {'19': icon + '-19.png', '38': icon + '-38.png'}}
    );

    var views = chrome.extension.getViews({type: "popup"});
    for (var i = 0; i < views.length; views++) {
        /* Make sure page is ready */
        if (document.readyState === "complete") {
            /* Update "help" link */
            helplink = views[i].document.getElementById("help");
            helplink.onclick = showHelp;
            /* Update enable/disable link. */
            /* FIXME: Sometimes, there is a little box coming around the link */
            enablelink = views[i].document.getElementById("enable");
            if (enabled_) {
                enablelink.textContent = "Disable";
                enablelink.onclick = function() {
                    console.log("Disable click");
                    enabled_ = false;
                    if (websocket_ != null)
                        websocket_.close();
                    else
                        websocketConnect(); /* Clear timeout and display message */
                    refreshUI();
                }
            } else {
                enablelink.textContent = "Enable";
                enablelink.onclick = function() {
                    console.log("Enable click");
                    enabled_ = true;
                    if (websocket_ == null)
                        websocketConnect();
                    refreshUI();
                }
            }

            /* Update debug mode according to checkbox state. */
            debugcheck = views[i].document.getElementById("debugcheck");
            debugcheck.onclick = function() {
                debug_ = debugcheck.checked;
                refreshUI();
            }
            debugcheck.checked = debug_;

            /* Update status box */
            views[i].document.getElementById("info").textContent = status_;

            /* Update logger table */
            loggertable = views[i].document.getElementById("logger");

            /* FIXME: only update needed rows */
            while (loggertable.rows.length > 0) {
                loggertable.deleteRow(0);
            }

            for (i = 0; i < logger_.length; i++) {
                value = logger_[i];

                if (value[0] == LogLevel.DEBUG && !debug_)
                    continue;

                var row = loggertable.insertRow(-1);
                var cell1 = row.insertCell(0);
                var cell2 = row.insertCell(1);
                var levelclass = value[0];
                cell1.className = "time " + levelclass;
                cell2.className = "value " + levelclass;
                cell1.innerHTML = value[1];
                cell2.innerHTML = value[2];
            }
        }
    }
}

/* Start the extension */
function clipboardStart() {
    printLog("Crouton extension started (" +
             chrome.runtime.getManifest().version + ")!", LogLevel.INFO);
    setStatus("Started...", false);

    clipboardholder_ = document.getElementById("clipboardholder");

    websocketConnect();
}

/* Connect to the server */
function websocketConnect() {
    /* Clear timeout if we were called manually. */
    if (timeout_ != null) {
        clearTimeout(timeout_);
        timeout_ = null;
    }

    if (!enabled_) {
        setStatus("No connection (extension disabled).", false);
        printLog("Extension is disabled.", LogLevel.INFO);
        return;
    }

    if (websocket_ != null) {
        printLog("Socket already open", LogLevel.DEBUG);
        return;
    }

    console.log("websocketConnect: " + websocket_);

    printLog("Opening a web socket", LogLevel.DEBUG);
    error_ = false;
    setStatus("Connecting...", false);
    websocket_ = new WebSocket(URL);
    websocket_.onopen = websocketOpen;
    websocket_.onmessage = websocketMessage;
    websocket_.onclose = websocketClose;
}

/* Connection was established */
function websocketOpen() {
    printLog("Connection established.", LogLevel.INFO);
    setStatus("Connection established: checking version...", false);
}

function readClipboard() {
    clipboardholder_.value = "";
    clipboardholder_.select();
    document.execCommand("Paste");
    return clipboardholder_.value;
}

function writeClipboard(str) {
    clipboardholder_.value = str;
    clipboardholder_.select();
    document.execCommand("Copy");
}

/* Received a message from the server */
function websocketMessage(evt) {
    var received_msg = evt.data;
    var cmd = received_msg[0];
    var payload = received_msg.substring(1);

    printLog("Message is received (" + received_msg + ")", LogLevel.DEBUG);

    /* Only accept version packets until we have received one. */
    if (!active_) {
        if (cmd == 'V') { /* Version */
            if (payload < 1 || payload > VERSION) {
                websocket_.send("EInvalid version (> " + VERSION + ")");
                error("Invalid server version " +
                                payload + " > " + VERSION + ".", false);
            }
            /* Set active_ to true */
            setStatus("Connection established.", true);
            websocket_.send("VOK");
            return;
        } else {
            error("Received frame while waiting for version.", false);
        }
    }

    switch(cmd) {
    case 'W': /* Write */
        var clip = readClipboard();

        dummystr_ = false;

        /* Do not erase identical clipboard content */
        if (clip != payload) {
             /* We cannot write an empty string: Write DUMMY instead */
            if (payload == "") {
                writeClipboard(DUMMY_EMPTYSTRING);
                dummystr_ = true;
            } else {
                writeClipboard(payload);
            }
        } else if (payload == DUMMY_EMPTYSTRING) {
            /* Unlikely case where DUMMY string comes from the other side */
            writeClipboard(payload);
        } else {
            printLog("Not erasing content (identical).", LogLevel.DEBUG);
        }

        websocket_.send("WOK");

        break;
    case 'R': /* Read */
        var clip = readClipboard();

        if (clip == DUMMY_EMPTYSTRING && dummystr_) {
            websocket_.send("R");
        } else {
            websocket_.send("R" + clip);
        }

        break;
    case 'U': /* Open an URL */
        /* URL must be absolute: see RFC 3986 for syntax (section 3.1) */
        if (match = (/^([a-z][a-z0-9+-.]*):/i).exec(payload)) {
            /* FIXME: we could blacklist schemes using match[1] here */
            chrome.tabs.create({ url: payload });
            websocket_.send("UOK");
        } else {
            printLog("Received invalid URL: " + payload, LogLevel.ERROR);
            websocket_.send("EError: URL must be absolute.");
        }

        break;
    case 'P': /* Ping */
        websocket_.send(received_msg);
        break;
    case 'E':
        error("Server error: " + payload, 1);
        break;
    default:
        error("Invalid packet from server: " + received_msg, 1);
        break;
    }
}

/* Connection was closed (or never established) */
function websocketClose() {
    if (websocket_ == null) {
        console.log("websocketClose: null!");
        return;
    }

    if (enabled_) {
        setStatus("No connection (retrying in " +
                                    RETRY_TIMEOUT + " seconds)", false);
        printLog("Connection is closed, trying again in " +
                            RETRY_TIMEOUT + " seconds...", LogLevel.INFO);
        /* Retry in RETRY_TIMEOUT seconds */
        if (timeout_ == null) {
            timeout_ = setTimeout(websocketConnect, RETRY_TIMEOUT*1000);
        }
    } else {
        setStatus("No connection (extension disabled).", false);
        printLog("Connection is closed, extension is disabled: not retrying.",
                                                            LogLevel.INFO);
    }

    websocket_ = null;

    /* Check for update on every disconnect */
    checkUpdate();
}

function padstr0(i) {
    var s = i + "";
    if (s.length < 2)
        return "0" + s;
    else
        return s;
}

/* Add a message in the log. */
function printLog(str, level) {
    date = new Date;
    datestr = padstr0(date.getHours()) + ":" +
              padstr0(date.getMinutes()) + ":" +
              padstr0(date.getSeconds());

    if (str.length > 80)
        str = str.substring(0, 77) + "...";
    console.log(datestr + ": " + str);

    /* Add messages to logger */
    if (level != LogLevel.DEBUG || debug_) {
        logger_.unshift([level, datestr, str]);
        if (logger_.length > MAXLOGGERLEN) {
            logger_.pop();
        }
        refreshUI();
    }
}

/* Display an error, and prevent retries if enabled is false */
function error(str, enabled) {
    printLog(str, LogLevel.ERROR);
    enabled_ = enabled;
    error_ = true;
    refreshUI();
    websocket_.close();
    /* Check for extension update (possible reason for the error) */
    checkUpdate();
}

/* On error: disconnect WebSocket, then log errors */
onerror = function(msg, url, line) {
    if (websocket_)
        websocket_.close();
    error("Uncaught JS error: " + msg, false);
    return true;
}

/* Start the extension as soon as the background page is loaded */
document.addEventListener('DOMContentLoaded', clipboardStart);

chrome.runtime.onInstalled.addListener(function(details) {
    if (details.reason == "install") {
        /* Open help tab on first install */
        showHelp();
    }
}
)

chrome.runtime.onUpdateAvailable.addListener(function(details) {
    updateAvailable(details.version);
});
<|endoftext|>


<commit_before>'use strict'

const _ = require('lodash')

const type = {
  null: 0,
  bool: 1,
  number: 2,
  string: 3,
  array: 4,
  object: 5
}

function jsonDecode (data, decoder) {
  const typeData = dataToType(data)
  const typeDecoder = decoderToType(decoder)

  if (typeData !== typeDecoder) {
    throw new TypeError(`Expected data type "${typeToString(typeDecoder)}", got data type "${typeToString(typeData)}"`)
  }

  return data
}

function decoderToType (input) {
  switch (input) {
    case null: return type.null
    case Boolean: return type.bool
    case Number: return type.number
    case String: return type.string
    default:
      throw new Error('TODO array')
  }
}

function dataToAst (input) {
  const inputType = dataToType(input)

  switch (inputType) {
    case type.null:
    case type.bool:
    case type.number:
    case type.string:
      return {
        value: input,
        type: inputType,
        children: null
      }
    case type.array:
      return {
        value: input,
        type: inputType,
        children: input.map(dataToAst)
      }
    case type.object:
      return {
        value: input,
        type: inputType,
        children: Object.keys(input).map(key => ({
          key: key,
          value: dataToAst(input[key])
        }))
      }
  }
}

function dataToType (input) {
  if (_.isNull(input)) return type.null
  else if (_.isBoolean(input)) return type.bool
  else if (_.isNumber(input)) return type.number
  else if (_.isString(input)) return type.string
  else if (_.isArray(input)) return type.array
  else if (_.isObject(input)) return type.object

  throw new Error('TODO')
}

function typeToString (type) {
  switch (type) {
    case type.null: return 'null'
    case type.bool: return 'bool'
    case type.number: return 'number'
    case type.string: return 'string'
    case type.array: return 'array'
    case type.object: return 'object'
    default: return 'unknown'
  }
}

module.exports = jsonDecode
<commit_msg>formatting<commit_after>'use strict'

const _ = require('lodash')

const Type = {
  NULL: 0,
  BOOL: 1,
  NUMBER: 2,
  STRING: 3,
  ARRAY: 4,
  OBJECT: 5
}

function jsonDecode (data, decoder) {
  const typeData = dataToType(data)
  const typeDecoder = decoderToType(decoder)

  if (typeData !== typeDecoder) {
    throw new TypeError(`Expected data type "${typeToString(typeDecoder)}", got data type "${typeToString(typeData)}"`)
  }

  return data
}

function decoderToType (input) {
  switch (input) {
    case null: return Type.NULL
    case Boolean: return Type.BOOL
    case Number: return Type.NUMBER
    case String: return Type.STRING
    default:
      throw new Error('TODO array')
  }
}

function dataToAst (input) {
  const inputType = dataToType(input)

  switch (inputType) {
    case Type.NULL:
    case Type.BOOL:
    case Type.NUMBER:
    case Type.STRING:
      return {
        value: input,
        type: inputType,
        children: null
      }
    case Type.ARRAY:
      return {
        value: input,
        type: inputType,
        children: input.map(dataToAst)
      }
    case Type.OBJECT:
      return {
        value: input,
        type: inputType,
        children: Object.keys(input).map(key => ({
          key: key,
          value: dataToAst(input[key])
        }))
      }
  }
}

function dataToType (input) {
  if (_.isNull(input)) return Type.NULL
  else if (_.isBoolean(input)) return Type.BOOL
  else if (_.isNumber(input)) return Type.NUMBER
  else if (_.isString(input)) return Type.STRING
  else if (_.isArray(input)) return Type.ARRAY
  else if (_.isObject(input)) return Type.OBJECT

  throw new Error('TODO')
}

function typeToString (type) {
  switch (type) {
    case Type.NULL: return 'null'
    case Type.BOOL: return 'bool'
    case Type.NUMBER: return 'number'
    case Type.STRING: return 'string'
    case Type.ARRAY: return 'array'
    case Type.OBJECT: return 'object'
    default: return 'unknown'
  }
}

module.exports = jsonDecode
<|endoftext|>


<commit_before>(function() {
  angular.module('notely.notes', [
    'ui.router'
  ])
  .config(notesConfig);

  notesConfig['$inject'] = ['$stateProvider'];
  function notesConfig($stateProvider) {
    $stateProvider

      .state('notes', {
        url: '/notes',
        templateUrl: '/notes/notes.html',
        controller: NotesController
      })

      .state('notes.form', {
        url: '/:noteId',
        templateUrl: '/notes/notes-form.html'
      });
  }

  NotesController['$inject'] = ['$state', '$scope', 'NotesService'];
  function NotesController($state, $scope, NotesService) {
    $scope.note = {};

    $scope.save = function() {
      NotesService.save($scope.note);
    };

    NotesService.fetch().then(function() {
      $scope.notes = NotesService.get();
    });
  }
})();

//
<commit_msg>Resolve notesLoaded promise before loading NotesController.<commit_after>(function() {
  angular.module('notely.notes', [
    'ui.router'
  ])
  .config(notesConfig);

  notesConfig['$inject'] = ['$stateProvider'];
  function notesConfig($stateProvider) {
    $stateProvider

      .state('notes', {
        url: '/notes',
        resolve: {
          notesLoaded: function(NotesService) {
            return NotesService.fetch();
          }
        },
        templateUrl: '/notes/notes.html',
        controller: NotesController
      })

      .state('notes.form', {
        url: '/:noteId',
        templateUrl: '/notes/notes-form.html',
        controller: NotesFormController
      });
  }

  NotesController.$inject = ['$state', '$scope', 'NotesService'];
  function NotesController($state, $scope, NotesService) {
    $scope.note = {};
    $scope.notes = NotesService.get();
  }

  NotesFormController.$inject = ['$scope', '$state', 'NotesService'];
  function NotesFormController($scope, $state, NotesService) {
    $scope.note = NotesService.findById($state.params.noteId);

    $scope.save = function() {
      NotesService.save($scope.note);
    };
  }
})();

//
<|endoftext|>


<commit_before>// This file expects a number of variables to be defined globally.
// Use only in conjunction with GraphView.getView
document.addEventListener("DOMContentLoaded", function(event) {
    try {
        // Match graph size to view size
        var titleHeight = (document.getElementById('chart-title') || { offsetHeight: 0 }).offsetHeight;
        config.size = {
            height: document.body.offsetHeight - titleHeight,
            width: document.body.offsetWidth,
        };

        // Turn off default hover/click behaviors
        config.interaction = { enabled: true };
        config.tooltip = {
            show: true,
            grouped: type === "bar",
            contents: function(data, defaultTitleFormat, defaultValueFormat, color) {
                var html = "";
                for (var i = 0; i < data.length; i++) {
                    if (data.isData[data[i].id]) {
                        var yName = config.data.names[data[i].id];
                        html += "
" + yName + "	" + data[i].value + "
";
                    }
                }
                if (!html) {
                    return "";
                }
                if (type === "bar") {
                    html = "
" + data.barLabels[data[0].x] + "
" + html;
                } else {
                    // TODO: this displays an unecessarily long, English-only string for time charts
                    // "Sat May 02 2015 00:00:00 GMT-0400 (Eastern Daylight Time)"
                    html = "
" + data.xNames[data[0].id] + "	" + data[0].x + "
" + html;
                }
                if (type === "bubble") {
                    html += "
Radius	" + data.radii[d.id][d.index] + "
";
                }
                html = "
" + html + "
";
                html = "
" + html + "
";
                return html;
            },
        };

        // Set point size for bubble charts, and turn points off altogether
        // for other charts (we'll be using custom point shapes)
        config.point = {
            r: function(d) {
                if (data.radii[d.id]) {
                    // Arbitrary max size of 30
                    return 30 * data.radii[d.id][d.index] / data.maxRadii[d.id];
                }
                return 0;
            },
        };

        // Add functions for custom tick label text (where foo-labels was an object).
        // Don't do this if the tick format was already set by Java (which will
        // only happen for time-based graphs that are NOT using custom tick text).
        if (config.axis.x.tick && !config.axis.x.tick.format) {
            config.axis.x.tick.format = function(d) {
                var key = String(d);
                if (type === "time") {
                    var time = key.match(/\d+:\d+:\d+/)[0];
                    key = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " " + time;

                }
                var label = axis.xLabels[key] || d;
                return Math.round(label) || label;
            };
        }
        if (config.axis.y.tick) {
            config.axis.y.tick.format = function(d) {
                return axis.yLabels[String(d)] || Math.round(d);
            };
        }
        if (config.axis.y2.tick) {
            config.axis.y2.tick.format = function(d) {
                return axis.y2Labels[String(d)] || Math.round(d);
            };
        }

        // Hide any system-generated series from legend
        var hideSeries = [];
        for (var yID in config.data.xs) {
            if (!data.isData[yID]) {
                hideSeries.push(yID);
            }
        }
        config.legend.hide = hideSeries;

        // Configure data labels, which we use only to display annotations
        config.data.labels = {
            format: function(value, id, index) {
                return data.annotations[id] || '';
            },
        };

        // Don't use C3's default ordering for stacked series, use the order series are defined
        config.data.order = false;

        // Post-processing
        config.onrendered = function() {
            // For annotations series, nudge text so it appears on top of data point
            d3.selectAll("g.c3-texts text").attr("dy", 10);

            // Support point-style
            for (var yID in data.pointStyles) {
                var symbol = data.pointStyles[yID];
                if (type === "bar") {
                    symbol = "none";
                } else if (type=== "bubble") {
                    symbol = "circle";
                } else {
                    applyPointShape(yID, symbol);
                }
                applyLegendShape(yID, symbol);
            }

            // Configure colors more specifically than C3 allows
            for (var yID in config.data.colors) {
                // Data itself
                if (type === "bar") {
                    var bars = d3.selectAll(".c3-bars-" + yID + " path")[0];
                    for (var i = 0; i < bars.length; i++) {
                        // If there's a bar-specific color, set it
                        if (data.barColors[yID] && data.barColors[yID][i]) {
                            bars[i].style.fill = data.barColors[yID][i];
                        }
                        // Get opacity: bar-specific if it's there, otherwise series-specific
                        var opacity;
                        if (data.barOpacities[yID]) {
                            opacity = data.barOpacities[yID][i];
                        }
                        opacity = opacity || data.lineOpacities[yID];
                        bars[i].style.opacity = opacity;
                    }
                } else {
                    var line = d3.selectAll(".c3-lines-" + yID + " path")[0][0];
                    if (line) {
                        line.style.opacity = data.lineOpacities[yID];
                    }
                }

                // Legend
                var legend = d3.selectAll(".c3-legend-item-" + yID + " path")[0];
                if (!legend.length) {
                    legend = d3.selectAll(".c3-legend-item-" + yID + " line")[0];
                }
                if (legend.length) {
                    legend = legend[0];
                    legend.style.opacity = data.lineOpacities[yID];
                }

                // Point shapes
                var points = d3.selectAll(".c3-circles-" + yID + " path")[0];
                if (!points.length) {
                    points = d3.selectAll(".c3-circles-" + yID + " circle")[0];
                }
                for (var i = 0; i < points.length; i++) {
                    points[i].style.opacity = data.lineOpacities[yID];
                }
            }
            for (var yID in data.areaColors) {
                var area = d3.selectAll(".c3-areas-" + yID + " path")[0][0];
                if (area) {
                    area.style.fill = data.areaColors[yID];
                    area.style.opacity = data.areaOpacities[yID];
                }
            }
        };

        // Generate chart
        c3.generate(config);
    } catch(e) {
        displayError(e);
    }
});

/**
 * Replace C3's default circle points with user-requested symbols.
 * @param yID String ID of y-values to manipulate
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 */
function applyPointShape(yID, symbol) {
    if (type === 'bar' || type === 'bubble') {
        return;
    }

    var circleSet = d3.selectAll(".c3-circles-" + yID);
    var circles = circleSet.selectAll("circle")[0];
    if (!circles) {
        return;
    }

    for (var j = 0; j < circles.length; j++) {
        circles[j].style.opacity = 0;    // hide default circle
        appendSymbol(
            circleSet,
            circles[j].cx.baseVal.value,
            circles[j].cy.baseVal.value,
            symbol,
            circles[j].style.fill
        );
    }
}

/**
 * Make shape displayed in legend match shape used on line.
 * @param yID String ID of y-values to manipulate
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 */
function applyLegendShape(yID, symbol) {
    if (symbol !== "none") {
        var legendItem = d3.selectAll(".c3-legend-item-" + yID);
        var line = legendItem.selectAll("line");    // there will only be one line
        if (!line || !line.length) {
            return;
        }
        line = line[0][0]
        line.style.opacity = 0;    // hide default square
        appendSymbol(
            legendItem,
            line.x1.baseVal.value + 5,
            line.y1.baseVal.value,
            symbol,
            line.style.stroke
        );
    }
}

/**
 * Add symbol to given element.
 * @param parent Element to attach symbol to
 * @param x x-coordinate to draw at
 * @param y y-coordinate to draw at
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 * @param color Color to draw symbol
 */
function appendSymbol(parent, x, y, symbol, color) {
    if (symbol === 'none') {
        return;
    }

    parent.append("path")
        .attr("transform", function (d) {
        return "translate(" + x + ", " + y + ")";
    })
        .attr("class", "symbol")
        .attr("d", d3.svg.symbol()
              .type(symbol)
              .size(50))
        .style("fill", color)
        .style("stroke", color);
}<commit_msg>fix tooltips<commit_after>// This file expects a number of variables to be defined globally.
// Use only in conjunction with GraphView.getView
document.addEventListener("DOMContentLoaded", function(event) {
    try {
        // Match graph size to view size
        var titleHeight = (document.getElementById('chart-title') || { offsetHeight: 0 }).offsetHeight;
        config.size = {
            height: document.body.offsetHeight - titleHeight,
            width: document.body.offsetWidth,
        };

        // Turn off default hover/click behaviors
        config.interaction = { enabled: true };
        var formatXTooltip = function(x) {
            return x;
        }
        if (type === "time") {
            formatXTooltip = d3.time.format(config.axis.x.tick.format);
        }
        config.tooltip = {
            show: true,
            grouped: type === "bar",
            contents: function(yData, defaultTitleFormat, defaultValueFormat, color) {
                var html = "";

                // Add rows for y values
                for (var i = 0; i < yData.length; i++) {
                    if (data.isData[yData[i].id]) {
                        var yName = config.data.names[yData[i].id];
                        html += "
" + yName + "	" + yData[i].value + "
";
                    }
                }
                if (!html) {
                    return "";
                }

                // Add a top row for x value
                if (type === "bar") {
                    html = "
" + data.barLabels[yData[0].x] + "
" + html;
                } else {
                    html = "
" + data.xNames[yData[0].id] + "	"
                            + formatXTooltip(yData[0].x) + "
"
                            + html;
                }

                if (type === "bubble") {
                    html += "
Radius	" + data.radii[d.id][d.index] + "
";
                }

                html = "
" + html + "
";
                html = "
" + html + "
";
                return html;
            },
        };

        // Set point size for bubble charts, and turn points off altogether
        // for other charts (we'll be using custom point shapes)
        config.point = {
            r: function(d) {
                if (data.radii[d.id]) {
                    // Arbitrary max size of 30
                    return 30 * data.radii[d.id][d.index] / data.maxRadii[d.id];
                }
                return 0;
            },
        };

        // Add functions for custom tick label text (where foo-labels was an object).
        // Don't do this if the tick format was already set by Java (which will
        // only happen for time-based graphs that are NOT using custom tick text).
        if (config.axis.x.tick && !config.axis.x.tick.format) {
            config.axis.x.tick.format = function(d) {
                var key = String(d);
                if (type === "time") {
                    var time = key.match(/\d+:\d+:\d+/)[0];
                    key = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " " + time;

                }
                var label = axis.xLabels[key] || d;
                return Math.round(label) || label;
            };
        }
        if (config.axis.y.tick) {
            config.axis.y.tick.format = function(d) {
                return axis.yLabels[String(d)] || Math.round(d);
            };
        }
        if (config.axis.y2.tick) {
            config.axis.y2.tick.format = function(d) {
                return axis.y2Labels[String(d)] || Math.round(d);
            };
        }

        // Hide any system-generated series from legend
        var hideSeries = [];
        for (var yID in config.data.xs) {
            if (!data.isData[yID]) {
                hideSeries.push(yID);
            }
        }
        config.legend.hide = hideSeries;

        // Configure data labels, which we use only to display annotations
        config.data.labels = {
            format: function(value, id, index) {
                return data.annotations[id] || '';
            },
        };

        // Don't use C3's default ordering for stacked series, use the order series are defined
        config.data.order = false;

        // Post-processing
        config.onrendered = function() {
            // For annotations series, nudge text so it appears on top of data point
            d3.selectAll("g.c3-texts text").attr("dy", 10);

            // Support point-style
            for (var yID in data.pointStyles) {
                var symbol = data.pointStyles[yID];
                if (type === "bar") {
                    symbol = "none";
                } else if (type=== "bubble") {
                    symbol = "circle";
                } else {
                    applyPointShape(yID, symbol);
                }
                applyLegendShape(yID, symbol);
            }

            // Configure colors more specifically than C3 allows
            for (var yID in config.data.colors) {
                // Data itself
                if (type === "bar") {
                    var bars = d3.selectAll(".c3-bars-" + yID + " path")[0];
                    for (var i = 0; i < bars.length; i++) {
                        // If there's a bar-specific color, set it
                        if (data.barColors[yID] && data.barColors[yID][i]) {
                            bars[i].style.fill = data.barColors[yID][i];
                        }
                        // Get opacity: bar-specific if it's there, otherwise series-specific
                        var opacity;
                        if (data.barOpacities[yID]) {
                            opacity = data.barOpacities[yID][i];
                        }
                        opacity = opacity || data.lineOpacities[yID];
                        bars[i].style.opacity = opacity;
                    }
                } else {
                    var line = d3.selectAll(".c3-lines-" + yID + " path")[0][0];
                    if (line) {
                        line.style.opacity = data.lineOpacities[yID];
                    }
                }

                // Legend
                var legend = d3.selectAll(".c3-legend-item-" + yID + " path")[0];
                if (!legend.length) {
                    legend = d3.selectAll(".c3-legend-item-" + yID + " line")[0];
                }
                if (legend.length) {
                    legend = legend[0];
                    legend.style.opacity = data.lineOpacities[yID];
                }

                // Point shapes
                var points = d3.selectAll(".c3-circles-" + yID + " path")[0];
                if (!points.length) {
                    points = d3.selectAll(".c3-circles-" + yID + " circle")[0];
                }
                for (var i = 0; i < points.length; i++) {
                    points[i].style.opacity = data.lineOpacities[yID];
                }
            }
            for (var yID in data.areaColors) {
                var area = d3.selectAll(".c3-areas-" + yID + " path")[0][0];
                if (area) {
                    area.style.fill = data.areaColors[yID];
                    area.style.opacity = data.areaOpacities[yID];
                }
            }
        };

        // Generate chart
        c3.generate(config);
    } catch(e) {
        displayError(e);
    }
});

/**
 * Replace C3's default circle points with user-requested symbols.
 * @param yID String ID of y-values to manipulate
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 */
function applyPointShape(yID, symbol) {
    if (type === 'bar' || type === 'bubble') {
        return;
    }

    var circleSet = d3.selectAll(".c3-circles-" + yID);
    var circles = circleSet.selectAll("circle")[0];
    if (!circles) {
        return;
    }

    for (var j = 0; j < circles.length; j++) {
        circles[j].style.opacity = 0;    // hide default circle
        appendSymbol(
            circleSet,
            circles[j].cx.baseVal.value,
            circles[j].cy.baseVal.value,
            symbol,
            circles[j].style.fill
        );
    }
}

/**
 * Make shape displayed in legend match shape used on line.
 * @param yID String ID of y-values to manipulate
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 */
function applyLegendShape(yID, symbol) {
    if (symbol !== "none") {
        var legendItem = d3.selectAll(".c3-legend-item-" + yID);
        var line = legendItem.selectAll("line");    // there will only be one line
        if (!line || !line.length) {
            return;
        }
        line = line[0][0]
        line.style.opacity = 0;    // hide default square
        appendSymbol(
            legendItem,
            line.x1.baseVal.value + 5,
            line.y1.baseVal.value,
            symbol,
            line.style.stroke
        );
    }
}

/**
 * Add symbol to given element.
 * @param parent Element to attach symbol to
 * @param x x-coordinate to draw at
 * @param y y-coordinate to draw at
 * @param symbol string representing symbol: "none", "circle", "cross", etc.
 *  Unknown symbols will be drawn as circles.
 * @param color Color to draw symbol
 */
function appendSymbol(parent, x, y, symbol, color) {
    if (symbol === 'none') {
        return;
    }

    parent.append("path")
        .attr("transform", function (d) {
        return "translate(" + x + ", " + y + ")";
    })
        .attr("class", "symbol")
        .attr("d", d3.svg.symbol()
              .type(symbol)
              .size(50))
        .style("fill", color)
        .style("stroke", color);
}<|endoftext|>


<commit_before>var participants = [];		//people currently running an instance of the hangout
var queue = [];				//list of participant ids waiting to speak. 0 is the current speaker
var timeOut = -1;				//unix time of when the current speaker's turn will end
var currentSpeakerId;		//id of the current speaker; on updates if it doesn't match queue, manager sets new time

var greenDot = gapi.hangout.av.effects.createImageResource('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90BDwMNIxL4LdEAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAuNJREFUeNrtmy2M2mAYx3/tTsxchliWnDi1kAxBhsE0aTBkbkPg0Ljhlp1CYnF3Dj03wbBnSJOaMywTXLKcOkGyICBnJrhsog9Lj7Xlo3y078s/qSx9/z+e9/N5XoMdy3Z5BpwBOaAgTw44B14CM2AC3ANDYCDPEBg5Fo+7bJ+xI9MvgBJQB97H/Lke0AH6jsU0sQBsFwMoAs0tmI6C0QJuHIs/iQAgxsvAFwnpfWgM1IDruCCMmObfAl+B1xxGd0DVsfi+VwC2y3MJxU8kQ22g6Vj83jkA2+UN8AM4IVmaAXnH4nadl8w1zX+Q6Slp5pE2DaWN2wdgu3wEuiRfXWnr9gDYLp+BS9KjS2lz/DFAaKbJvF8Nx+JqYwDSn7qkWxXH4tvaAGS0H6KGcmGzgxExzz8kdLTfdIo8DVonhA2CLYXMz6fI1koRIMvbAWqqsLhsNgI2Nj8PuLbfx94h699ALXaBssLmEW/lwAiQf//XHre0h9IYeDWPAn8EFDUwj3gsBnWBJvqo+aQLyBneBL2UcSym8wgooZ9K/i5Q1xBAHcCQc/sZeurExEta6KozEy9Lo6tyJl6qSlcVjgCOXcDL0uqqcxPIaAwgY6K5TA33AH5NTLzKDF11b6LO0fcmGpqoewC6igZHAMcuACONAYxMqcPraWi+51g8zhdCHQ0BdOYLIYC+hgD6/wBIBWZPs/Cf+iMAQrKniqrl3wvMdYOXNlJdY/H6FIDkymoaAKhFZYev8VLIqupOPBIIQMhUFQZQXSyu/u9ARCoo2gqabwcVVYedCDVRK1s0IyT7HQhAqqnyCgHIh1WSh54JSl1dRQHzlagK8shDUamwbKTYfCOqSnQpAIFwBVyk0PzFsjphWOPCRMqKphurmF8LgEBIQ/F0ZVnYbwxAIOh7ZcY3O5wmbLHUxiuGvl33Re2vzcXKDcqHs8C7PW+lx/LNbBzzsSNgIRr0vDobAkO/y9MRMBJ9ff4vhV3btM/DqFkAAAAASUVORK5CYII=');
var greenDotOverlay = greenDot.createOverlay(
	{'scale':
		{'magnitude': 0.15, 'reference': gapi.hangout.av.effects.ScaleReference.WIDTH}});

var yellowDot = gapi.hangout.av.effects.createImageResource('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90BDwMXCA5pLkoAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAuFJREFUeNrtmyts21AUhj97BiPVCiZLBUVzpAVUKykuqca2gLDgsoVNKwoMDWtZ8NhAFloSXJJpIJXmooJIVkCjkoFbbcAnk5vZzsN52Pfml8yS+P5fzn2ecy3WrMB3XgAHQBk4lqcMHAKvAQU8APfAAOjLMwCGrqee1tk+a02mXwGnwDnwIePPdYE20HM9Nc4tgMB3LOAEaKzAdBqMJnDjeupPLgCI8TPgq4T0JjQCasB1VhBWRvPvgG/AG7ajO6DqeurHRgEEvvNSQvEz+VALaLie+r12AIHvvAV+Ag75kgKOXE/dLvIle0HzH2V6ypt5pE0DaePqAQS+8wnokH91pK2rAxD4zhfgkuLoUtqcfQwQmkUyH1Xd9dTV0gCkP3Uotiqup74vDEBG+wF6qJw0O1gp8/xjTkf7ZafIvbh1QtIg2NTI/GSKbM4VAbK87aOnjqeXzVbMxubXFtf2m9g7lKIbqOkucKaxecTbWWwEyL8fbHBLuy2NAHcSBdEIODHAPOLxJK4LNDBHjWddQM7wHjBL+66nxpMIOMU8nUa7wLmBAM4BLDm3V5gpxyZMWpiqA5swS2OqyjZhqspUHe8A7LpAmKU1VYc2sG8wgH0bw2UbuAeI6sEmrMwwVfc2+hx9L6OBjb4HoPOovwOw6wIwNBjA0JY6vK6B5ruup54mC6G2gQDak4UQQM9AAL1/AKQCs2tY+I+jEQAJ2VNN1YzuBSa6IUwb6a6ReH0OQHJlNQMA1NKyw9eEKWRddSceiQUgZKoaA6hOF1f/dyAiFRQtDc234oqqd0VScZ+WDx5p9O8fJVWSJ54JSl1dRQPzlbQK8tRDUamwrBfYfD2tSnQmAIFwBVwU0PzFrDrhxEEwYWAsUtF0fR7zCwEQCEUonq7MCvulAQgEc6/MRGaHvZwtlloyz98u+kXjr81lyg3Ki0vA+w1vpUfyzlIW85kjYCoazLw6mwDDvMvTKTByfX3+L4KL/xorzXR/AAAAAElFTkSuQmCC');
	var yellowDotOverlay = yellowDot.createOverlay(
	{'scale':
		{'magnitude': 0.15, 'reference': gapi.hangout.av.effects.ScaleReference.WIDTH}});

var buttonDisabled = false;	//true when button is clicked to prevent joining the queue several times

gapi.hangout.onApiReady.add(function(eventObj){
	if (eventObj.isApiReady) {
		buttonDisabled = false;
		participants = gapi.hangout.getEnabledParticipants();
		printParticipants();

		gapi.hangout.onParticipantsChanged.add(onParticipantsChange);
		gapi.hangout.data.onStateChanged.add(onDataChange);

		//setInterval(updateTimeOutText, 200);
	}
});

function onParticipantsChange(eventObj) {
	participants = eventObj.participants;

	onServerUpdate();
};

function onDataChange (eventObj){
	var state = gapi.hangout.data.getState();
	queue = JSON.parse(state.queue);
	try{
		timeOut = JSON.parse((state.timeOut) ? state.timeOut : -1);
	}
	catch (e){
		console.log(e);
		timeOut = -1;
	}

	onServerUpdate();
}

function onServerUpdate(){
	console.log("onServerUpdate called");
	buttonDisabled = false;
	printParticipants();
	clearOverlay();
	var queuePosition = queue.indexOf(gapi.hangout.getParticipantId());
	if (queuePosition == 0){
		placeGreenDot();
		document.getElementById('queueButton').innerHTML = "End Early";
	}
	else if (queuePosition > 0){
		placeYellowDot();
		document.getElementById('queueButton').innerHTML = "Leave Queue";
	}
	else{
		document.getElementById('queueButton').innerHTML = "Raise Hand";
	}

	//lowest order participant manages time updates
	//and checks for disconnected queue memembers
	if (isManager()){
		console.log("managing update...");
		var validSpeaker = false;
		var updateRequired = true;
		while (queue.length > 0 && !validSpeaker){

			var speakerInHangout = false;
			for (var i = 0; i < participants.length; i++){
				if (participants[i].id == queue[0]){
					if (!participants[i].hasAppEnabled){
						console.log("speaker doesn't have app on");
						queue.shift();
						updateRequired = true;
						speakerInHangout = true;
					}
					else{
						console.log("speaker running app");
						validSpeaker = true;
						speakerInHangout = true;
					}
				}
			}
			if (!speakerInHangout){
				console.log("speaker not in hangout");
				queue.shift();
				updateRequire = true;
			}
		}
		if (timeOut == -1 || new Date().getTime() > timeOut){
			updateRequired = true;
		}
		if (updateRequired){
			timeOut = new Date().getTime() + 1000*60;
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue), 'timeOut': JSON.stringify(timeOut)});
		}
		//set time
	}
}

function printParticipants(){
	var members = "";
	if (queue.length > 0){
		document.getElementById('speakerName') = getParticipantNameById(queue[0]);
	}
	for (var i = 1; i < queue.length; i++){
		members = members + getParticipantNameById(queue[i]) + '
';
	}
	for (var i = 0; i < participants.length; i++){
		if (queue.indexOf(participants[i].id) == -1){
			members = members + participants[i].person.displayName + '
';
		}
	}
	document.getElementById('otherParticipants').innerHTML = members;
}

function getParticipantNameById(id){
	for (var i = 0; i < participants.length; i++){
		if (participants.id == id){
			return participants[i].person.displayName;
		}
	}
	return '';
}

function buttonClick(){
	if (!buttonDisabled){
		buttonDisabled = true;
		var queuePosition = queue.indexOf(gapi.hangout.getParticipantId());
		if (queuePosition == 0){
			queue.shift();
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue)});
		}
		else if (queuePosition == -1){
			queue.push(gapi.hangout.getParticipantId());
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue)});
		}
	}
}

function placeGreenDot(){
	greenDotOverlay.setPosition(.4, -.35);
	greenDotOverlay.setVisible(true);
}

function placeYellowDot(){
	yellowDotOverlay.setPosition(.4, -.35);
	yellowDotOverlay.setVisible(true);
}

function clearOverlay(){
	greenDotOverlay.setVisible(false);
	yellowDotOverlay.setVisible(false);
}

function isManager(){
	return (participants[0].id == gapi.hangout.getParticipantId());
}

function updateTimeOutText(){
	if (timeOut != -1){
		document.getElementById('timeLeft').innerHTML = Math.round((timeOut - new Date.getTime()/1000));
	}
	else{
		document.getElementById('timeLeft').innerHTML = '';
	}
}<commit_msg>stopped sending update on bad timeout value<commit_after>var participants = [];		//people currently running an instance of the hangout
var queue = [];				//list of participant ids waiting to speak. 0 is the current speaker
var timeOut = -1;				//unix time of when the current speaker's turn will end
var currentSpeakerId;		//id of the current speaker; on updates if it doesn't match queue, manager sets new time

var greenDot = gapi.hangout.av.effects.createImageResource('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90BDwMNIxL4LdEAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAuNJREFUeNrtmy2M2mAYx3/tTsxchliWnDi1kAxBhsE0aTBkbkPg0Ljhlp1CYnF3Dj03wbBnSJOaMywTXLKcOkGyICBnJrhsog9Lj7Xlo3y078s/qSx9/z+e9/N5XoMdy3Z5BpwBOaAgTw44B14CM2AC3ANDYCDPEBg5Fo+7bJ+xI9MvgBJQB97H/Lke0AH6jsU0sQBsFwMoAs0tmI6C0QJuHIs/iQAgxsvAFwnpfWgM1IDruCCMmObfAl+B1xxGd0DVsfi+VwC2y3MJxU8kQ22g6Vj83jkA2+UN8AM4IVmaAXnH4nadl8w1zX+Q6Slp5pE2DaWN2wdgu3wEuiRfXWnr9gDYLp+BS9KjS2lz/DFAaKbJvF8Nx+JqYwDSn7qkWxXH4tvaAGS0H6KGcmGzgxExzz8kdLTfdIo8DVonhA2CLYXMz6fI1koRIMvbAWqqsLhsNgI2Nj8PuLbfx94h699ALXaBssLmEW/lwAiQf//XHre0h9IYeDWPAn8EFDUwj3gsBnWBJvqo+aQLyBneBL2UcSym8wgooZ9K/i5Q1xBAHcCQc/sZeurExEta6KozEy9Lo6tyJl6qSlcVjgCOXcDL0uqqcxPIaAwgY6K5TA33AH5NTLzKDF11b6LO0fcmGpqoewC6igZHAMcuACONAYxMqcPraWi+51g8zhdCHQ0BdOYLIYC+hgD6/wBIBWZPs/Cf+iMAQrKniqrl3wvMdYOXNlJdY/H6FIDkymoaAKhFZYev8VLIqupOPBIIQMhUFQZQXSyu/u9ARCoo2gqabwcVVYedCDVRK1s0IyT7HQhAqqnyCgHIh1WSh54JSl1dRQHzlagK8shDUamwbKTYfCOqSnQpAIFwBVyk0PzFsjphWOPCRMqKphurmF8LgEBIQ/F0ZVnYbwxAIOh7ZcY3O5wmbLHUxiuGvl33Re2vzcXKDcqHs8C7PW+lx/LNbBzzsSNgIRr0vDobAkO/y9MRMBJ9ff4vhV3btM/DqFkAAAAASUVORK5CYII=');
var greenDotOverlay = greenDot.createOverlay(
	{'scale':
		{'magnitude': 0.15, 'reference': gapi.hangout.av.effects.ScaleReference.WIDTH}});

var yellowDot = gapi.hangout.av.effects.createImageResource('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90BDwMXCA5pLkoAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAuFJREFUeNrtmyts21AUhj97BiPVCiZLBUVzpAVUKykuqca2gLDgsoVNKwoMDWtZ8NhAFloSXJJpIJXmooJIVkCjkoFbbcAnk5vZzsN52Pfml8yS+P5fzn2ecy3WrMB3XgAHQBk4lqcMHAKvAQU8APfAAOjLMwCGrqee1tk+a02mXwGnwDnwIePPdYE20HM9Nc4tgMB3LOAEaKzAdBqMJnDjeupPLgCI8TPgq4T0JjQCasB1VhBWRvPvgG/AG7ajO6DqeurHRgEEvvNSQvEz+VALaLie+r12AIHvvAV+Ag75kgKOXE/dLvIle0HzH2V6ypt5pE0DaePqAQS+8wnokH91pK2rAxD4zhfgkuLoUtqcfQwQmkUyH1Xd9dTV0gCkP3Uotiqup74vDEBG+wF6qJw0O1gp8/xjTkf7ZafIvbh1QtIg2NTI/GSKbM4VAbK87aOnjqeXzVbMxubXFtf2m9g7lKIbqOkucKaxecTbWWwEyL8fbHBLuy2NAHcSBdEIODHAPOLxJK4LNDBHjWddQM7wHjBL+66nxpMIOMU8nUa7wLmBAM4BLDm3V5gpxyZMWpiqA5swS2OqyjZhqspUHe8A7LpAmKU1VYc2sG8wgH0bw2UbuAeI6sEmrMwwVfc2+hx9L6OBjb4HoPOovwOw6wIwNBjA0JY6vK6B5ruup54mC6G2gQDak4UQQM9AAL1/AKQCs2tY+I+jEQAJ2VNN1YzuBSa6IUwb6a6ReH0OQHJlNQMA1NKyw9eEKWRddSceiQUgZKoaA6hOF1f/dyAiFRQtDc234oqqd0VScZ+WDx5p9O8fJVWSJ54JSl1dRQPzlbQK8tRDUamwrBfYfD2tSnQmAIFwBVwU0PzFrDrhxEEwYWAsUtF0fR7zCwEQCEUonq7MCvulAQgEc6/MRGaHvZwtlloyz98u+kXjr81lyg3Ki0vA+w1vpUfyzlIW85kjYCoazLw6mwDDvMvTKTByfX3+L4KL/xorzXR/AAAAAElFTkSuQmCC');
	var yellowDotOverlay = yellowDot.createOverlay(
	{'scale':
		{'magnitude': 0.15, 'reference': gapi.hangout.av.effects.ScaleReference.WIDTH}});

var buttonDisabled = false;	//true when button is clicked to prevent joining the queue several times

gapi.hangout.onApiReady.add(function(eventObj){
	if (eventObj.isApiReady) {
		buttonDisabled = false;
		participants = gapi.hangout.getEnabledParticipants();
		printParticipants();

		gapi.hangout.onParticipantsChanged.add(onParticipantsChange);
		gapi.hangout.data.onStateChanged.add(onDataChange);

		//setInterval(updateTimeOutText, 200);
	}
});

function onParticipantsChange(eventObj) {
	participants = eventObj.participants;

	onServerUpdate();
};

function onDataChange (eventObj){
	var state = gapi.hangout.data.getState();
	queue = JSON.parse(state.queue);
	try{
		timeOut = JSON.parse((state.timeOut) ? state.timeOut : -1);
	}
	catch (e){
		console.log(e);
		timeOut = -1;
	}

	onServerUpdate();
}

function onServerUpdate(){
	console.log("onServerUpdate called");
	buttonDisabled = false;
	printParticipants();
	clearOverlay();
	var queuePosition = queue.indexOf(gapi.hangout.getParticipantId());
	if (queuePosition == 0){
		placeGreenDot();
		document.getElementById('queueButton').innerHTML = "End Early";
	}
	else if (queuePosition > 0){
		placeYellowDot();
		document.getElementById('queueButton').innerHTML = "Leave Queue";
	}
	else{
		document.getElementById('queueButton').innerHTML = "Raise Hand";
	}

	//lowest order participant manages time updates
	//and checks for disconnected queue memembers
	if (isManager()){
		console.log("managing update...");
		var validSpeaker = false;
		var updateRequired = true;
		while (queue.length > 0 && !validSpeaker){

			var speakerInHangout = false;
			for (var i = 0; i < participants.length; i++){
				if (participants[i].id == queue[0]){
					if (!participants[i].hasAppEnabled){
						console.log("speaker doesn't have app on");
						queue.shift();
						updateRequired = true;
						speakerInHangout = true;
					}
					else{
						console.log("speaker running app");
						validSpeaker = true;
						speakerInHangout = true;
					}
				}
			}
			if (!speakerInHangout){
				console.log("speaker not in hangout");
				queue.shift();
				updateRequire = true;
			}
		}
		if (timeOut == -1 || new Date().getTime() > timeOut){
			console.log('new timeout value required');
			//updateRequired = true;
		}
		if (updateRequired){
			console.log("sending new update");
			timeOut = new Date().getTime() + 1000*60;
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue), 'timeOut': JSON.stringify(timeOut)});
		}
		//set time
	}
}

function printParticipants(){
	var members = "";
	if (queue.length > 0){
		document.getElementById('speakerName') = getParticipantNameById(queue[0]);
	}
	for (var i = 1; i < queue.length; i++){
		members = members + getParticipantNameById(queue[i]) + '
';
	}
	for (var i = 0; i < participants.length; i++){
		if (queue.indexOf(participants[i].id) == -1){
			members = members + participants[i].person.displayName + '
';
		}
	}
	document.getElementById('otherParticipants').innerHTML = members;
}

function getParticipantNameById(id){
	for (var i = 0; i < participants.length; i++){
		if (participants.id == id){
			return participants[i].person.displayName;
		}
	}
	return '';
}

function buttonClick(){
	if (!buttonDisabled){
		buttonDisabled = true;
		var queuePosition = queue.indexOf(gapi.hangout.getParticipantId());
		if (queuePosition == 0){
			queue.shift();
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue)});
		}
		else if (queuePosition == -1){
			queue.push(gapi.hangout.getParticipantId());
			gapi.hangout.data.submitDelta({'queue':JSON.stringify(queue)});
		}
	}
}

function placeGreenDot(){
	greenDotOverlay.setPosition(.4, -.35);
	greenDotOverlay.setVisible(true);
}

function placeYellowDot(){
	yellowDotOverlay.setPosition(.4, -.35);
	yellowDotOverlay.setVisible(true);
}

function clearOverlay(){
	greenDotOverlay.setVisible(false);
	yellowDotOverlay.setVisible(false);
}

function isManager(){
	return (participants[0].id == gapi.hangout.getParticipantId());
}

function updateTimeOutText(){
	if (timeOut != -1){
		document.getElementById('timeLeft').innerHTML = Math.round((timeOut - new Date.getTime()/1000));
	}
	else{
		document.getElementById('timeLeft').innerHTML = '';
	}
}<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var chai = require('chai');
var expect = chai.expect;

var Database = require('../../lib/database');
var FakeAdapter = require('../fakes/adapter');

require('../helpers/model');

var db,
  adapter,
  Article,
  User,
  user,
  articleObjects;

describe('Model.hasMany', function() {
  beforeEach(function() {
    adapter = FakeAdapter.create({});
    db = Database.create({ adapter: adapter });

    var hasMany = db.hasMany;
    var attr = db.attr;

    Article = db.model('article').reopen({
      title: attr(),
      authorKey: attr('author_num') // easy access to foreign key attr
    });
    User = db.model('user').reopen({
      username: attr(),
      articles: hasMany(Article, { foreignKey: 'author_num' })
    });
  });

  beforeEach(function() {
    user = User.fresh({ id: 1 });
    articleObjects = user.articleObjects;
  });

  beforeEach(function() {
    adapter.intercept(/select.*from "users"/i, {
      fields: ['id', 'username'],
      rows: [{ id: 1, username: 'wbyoung' }]
    });
    adapter.intercept(/select.*from "articles"/i, {
      fields: ['id', 'title'],
      rows: [{ id: 1, title: 'Journal', 'author_num': 1 }]
    });
    adapter.intercept(/insert into "articles"/i, {
      fields: ['id'],
      rows: [{ id: 23 }]
    });
  });

  describe('definition', function() {
    it('does not need to provide name', function() {
      User.reopen({
        books: db.Model.hasMany()
      });
      expect(user.booksRelation._relatedModel).to.eql(db.model('book'));
    });

    it('calculates foreign key from inverse', function() {
      User.reopen({
        books: db.Model.hasMany({ inverse: 'writer' })
      });
      expect(user.booksRelation.foreignKey).to.eql('writer_id');
    });
  });

  it('has related methods', function() {
    expect(User.__class__.prototype).to.have.ownProperty('articles');
    expect(user).to.have.property('articleObjects');
    expect(user).to.respondTo('createArticle');
    expect(user).to.respondTo('createArticle');
    expect(user).to.respondTo('addArticle');
    expect(user).to.respondTo('addArticles');
    expect(user).to.respondTo('removeArticle');
    expect(user).to.respondTo('removeArticles');
    expect(user).to.respondTo('clearArticles');
  });

  describe('pre-fetch', function() {
    it('executes multiple queries', function(done) {
      User.objects.with('articles').fetch().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "users"', []],
          ['SELECT * FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('caches related objects', function(done) {
      User.objects.with('articles').fetch().get('0').then(function(foundUser) {
        expect(foundUser.id).to.eql(1);
        expect(foundUser.username).to.eql('wbyoung');
        expect(foundUser.articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
      })
      .done(done, done);
    });

    it('works with multiple models each having multiple related objects', function(done) {
      var usersRegex = /select.*from "users".*order by "id"/i;
      var articlesRegex =
        /select.*from "articles" where "author_num" in \(\?, \?, \?\)/i;
      adapter.intercept(usersRegex, {
        fields: ['id', 'username'],
        rows: [
          { id: 1, username: 'wbyoung' },
          { id: 2, username: 'kate' },
          { id: 4, username: 'sam' },
        ]
      });
      adapter.intercept(articlesRegex, {
        fields: ['id', 'title', 'author_num'],
        rows: [
          { id: 3, title: 'Announcing Azul', 'author_num': 1 },
          { id: 5, title: 'Node.js ORM', 'author_num': 1 },
          { id: 9, title: 'Delicious Pancakes', 'author_num': 2 },
          { id: 8, title: 'Awesome Margaritas', 'author_num': 2 },
          { id: 4, title: 'Tasty Kale Salad', 'author_num': 2 },
          { id: 6, title: 'The Bipartisan System', 'author_num': 4 },
        ]
      });

      User.objects.with('articles').orderBy('id').fetch().then(function(users) {
        expect(users[0].username).to.eql('wbyoung');
        expect(users[1].username).to.eql('kate');
        expect(users[2].username).to.eql('sam');
        expect(_.map(users[0].articles, 'title')).to.eql([
          'Announcing Azul', 'Node.js ORM'
        ]);
        expect(_.map(users[1].articles, 'title')).to.eql([
          'Delicious Pancakes', 'Awesome Margaritas', 'Tasty Kale Salad'
        ]);
        expect(_.map(users[2].articles, 'title')).to.eql([
          'The Bipartisan System'
        ]);
      })
      .done(done, done);
    });
  });

  describe('relation', function() {

    it('fetches related objects', function(done) {
      articleObjects.fetch().then(function(articles) {
        expect(articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('caches the related objects query', function() {
      expect(user.articleObjects).to.equal(articleObjects);
    });

    it('throws when attempting to access un-loaded collection', function() {
      expect(function() {
        user.articles;
      }).to.throw(/articles.*not yet.*loaded/i);
    });

    it('does not consider relation loaded when fetched on a duplicated query', function(done) {
      articleObjects.clone().fetch().then(function() {
        return user.articles;
      })
      .throw(new Error('Expected access to relation to fail.'))
      .catch(function(e) {
        expect(e.message).to.match(/articles.*not yet.*loaded/i);
      })
      .done(done, done);
    });

    it('allows access loaded collection', function(done) {
      articleObjects.fetch().then(function() {
        expect(user.articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
      })
      .done(done, done);
    });

    it('can be filtered', function(done) {
      articleObjects.where({ title: 'Azul' }).fetch().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "articles" WHERE ("author_num" = ?) AND ' +
           '"title" = ?', [1, 'Azul']]
        ]);
      })
      .done(done, done);
    });

    it('allows update', function(done) {
      articleObjects.update({ title: 'Azul' }).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "title" = ? ' +
           'WHERE "author_num" = ?', ['Azul', 1]]
        ]);
      })
      .done(done, done);
    });

    it('allows delete', function(done) {
      articleObjects.delete().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['DELETE FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });
  });

  describe('helpers', function() {
    it('allows create', function() {
      var article = user.createArticle({ title: 'Hello' });
      expect(article.authorKey).to.eql(user.id);
      expect(article).to.to.be.an.instanceOf(Article.__class__);
    });

    it('does not create collection cache during create', function() {
      var article = user.createArticle({ title: 'Hello' });
      expect(function() {
        user.articles;
      }).to.throw(/articles.*not yet.*loaded/i);
      expect(article).to.exist;
    });

    it('updates collection cache during create', function(done) {
      var article;
      user.articleObjects.fetch().then(function() {
        article = user.createArticle({ title: 'Hello' });
      })
      .then(function() {
        expect(user.articles).to.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during create', function() {
      var articleObjects = user.articleObjects;
      var article = user.createArticle({ title: 'Hello' });
      expect(user.articleObjects).to.not.equal(articleObjects);
      expect(article).to.exist;
    });

    it('allows add with existing objects', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      user.addArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" = ?', [1, 5]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with multiple existing objects', function(done) {
      var article1 = Article.fresh({ id: 5, title: 'Hello' });
      var article2 = Article.fresh({ id: 8, title: 'Hello' });
      user.addArticles(article1, article2).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" IN (?, ?)', [1, 5, 8]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with unsaved objects', function(done) {
      var article = Article.fresh({ id: 12, title: 'Hello' });
      article.title = 'Renamed';
      user.addArticle(article).then(function() {
        // note that this expectation depends on ordering of object properties
        // which is not guaranteed to be a stable ordering.
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "title" = ?, "author_num" = ? ' +
           'WHERE "id" = ?', ['Renamed', 1, 12]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with created objects', function(done) {
      var article = Article.create({ title: 'Hello' });
      user.addArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['INSERT INTO "articles" ("title", "author_num") VALUES (?, ?) ' +
           'RETURNING "id"', ['Hello', 1]]
        ]);
      })
      .done(done, done);
    });

    it('updates collection cache during add', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      user.articleObjects.fetch().then(function() {
        return user.addArticle(article);
      })
      .then(function() {
        expect(user.articles).to.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during add', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.addArticle(article);
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });

    it('allows remove with existing objects', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello', authorKey: user.id });
      user.removeArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" = ?', [undefined, 5]]
        ]);
      })
      .done(done, done);
    });

    it('allows remove with multiple existing objects', function(done) {
      var article1 = Article.fresh({ id: 5, title: 'Hello' });
      var article2 = Article.fresh({ id: 8, title: 'Hello' });
      user.removeArticles(article1, article2).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" IN (?, ?)', [undefined, 5, 8]]
        ]);
      })
      .done(done, done);
    });

    it.skip('allows remove with unsaved objects', function() {

    });

    it('updates collection cache during remove', function(done) {
      var article;
      user.articleObjects.fetch().then(function() {
        article = user.articles[0];
        return user.removeArticle(article);
      })
      .then(function() {
        expect(user.articles).to.not.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during remove', function(done) {
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.removeArticle(user.articles[0]);
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });

    it('allows clear', function(done) {
      user.clearArticles().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['DELETE FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('updates collection cache during clear', function(done) {
      user.articleObjects.fetch().then(function() {
        return user.clearArticles();
      })
      .then(function() {
        expect(user.articles).to.eql([]);
      })
      .done(done, done);
    });

    it('clears query cache during clear', function(done) {
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.clearArticles();
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });
  });
});
<commit_msg>Pending tests for removing created objects.<commit_after>'use strict';

var _ = require('lodash');
var chai = require('chai');
var expect = chai.expect;

var Database = require('../../lib/database');
var FakeAdapter = require('../fakes/adapter');

require('../helpers/model');

var db,
  adapter,
  Article,
  User,
  user,
  articleObjects;

describe('Model.hasMany', function() {
  beforeEach(function() {
    adapter = FakeAdapter.create({});
    db = Database.create({ adapter: adapter });

    var hasMany = db.hasMany;
    var attr = db.attr;

    Article = db.model('article').reopen({
      title: attr(),
      authorKey: attr('author_num') // easy access to foreign key attr
    });
    User = db.model('user').reopen({
      username: attr(),
      articles: hasMany(Article, { foreignKey: 'author_num' })
    });
  });

  beforeEach(function() {
    user = User.fresh({ id: 1 });
    articleObjects = user.articleObjects;
  });

  beforeEach(function() {
    adapter.intercept(/select.*from "users"/i, {
      fields: ['id', 'username'],
      rows: [{ id: 1, username: 'wbyoung' }]
    });
    adapter.intercept(/select.*from "articles"/i, {
      fields: ['id', 'title'],
      rows: [{ id: 1, title: 'Journal', 'author_num': 1 }]
    });
    adapter.intercept(/insert into "articles"/i, {
      fields: ['id'],
      rows: [{ id: 23 }]
    });
  });

  describe('definition', function() {
    it('does not need to provide name', function() {
      User.reopen({
        books: db.Model.hasMany()
      });
      expect(user.booksRelation._relatedModel).to.eql(db.model('book'));
    });

    it('calculates foreign key from inverse', function() {
      User.reopen({
        books: db.Model.hasMany({ inverse: 'writer' })
      });
      expect(user.booksRelation.foreignKey).to.eql('writer_id');
    });
  });

  it('has related methods', function() {
    expect(User.__class__.prototype).to.have.ownProperty('articles');
    expect(user).to.have.property('articleObjects');
    expect(user).to.respondTo('createArticle');
    expect(user).to.respondTo('createArticle');
    expect(user).to.respondTo('addArticle');
    expect(user).to.respondTo('addArticles');
    expect(user).to.respondTo('removeArticle');
    expect(user).to.respondTo('removeArticles');
    expect(user).to.respondTo('clearArticles');
  });

  describe('pre-fetch', function() {
    it('executes multiple queries', function(done) {
      User.objects.with('articles').fetch().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "users"', []],
          ['SELECT * FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('caches related objects', function(done) {
      User.objects.with('articles').fetch().get('0').then(function(foundUser) {
        expect(foundUser.id).to.eql(1);
        expect(foundUser.username).to.eql('wbyoung');
        expect(foundUser.articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
      })
      .done(done, done);
    });

    it('works with multiple models each having multiple related objects', function(done) {
      var usersRegex = /select.*from "users".*order by "id"/i;
      var articlesRegex =
        /select.*from "articles" where "author_num" in \(\?, \?, \?\)/i;
      adapter.intercept(usersRegex, {
        fields: ['id', 'username'],
        rows: [
          { id: 1, username: 'wbyoung' },
          { id: 2, username: 'kate' },
          { id: 4, username: 'sam' },
        ]
      });
      adapter.intercept(articlesRegex, {
        fields: ['id', 'title', 'author_num'],
        rows: [
          { id: 3, title: 'Announcing Azul', 'author_num': 1 },
          { id: 5, title: 'Node.js ORM', 'author_num': 1 },
          { id: 9, title: 'Delicious Pancakes', 'author_num': 2 },
          { id: 8, title: 'Awesome Margaritas', 'author_num': 2 },
          { id: 4, title: 'Tasty Kale Salad', 'author_num': 2 },
          { id: 6, title: 'The Bipartisan System', 'author_num': 4 },
        ]
      });

      User.objects.with('articles').orderBy('id').fetch().then(function(users) {
        expect(users[0].username).to.eql('wbyoung');
        expect(users[1].username).to.eql('kate');
        expect(users[2].username).to.eql('sam');
        expect(_.map(users[0].articles, 'title')).to.eql([
          'Announcing Azul', 'Node.js ORM'
        ]);
        expect(_.map(users[1].articles, 'title')).to.eql([
          'Delicious Pancakes', 'Awesome Margaritas', 'Tasty Kale Salad'
        ]);
        expect(_.map(users[2].articles, 'title')).to.eql([
          'The Bipartisan System'
        ]);
      })
      .done(done, done);
    });
  });

  describe('relation', function() {

    it('fetches related objects', function(done) {
      articleObjects.fetch().then(function(articles) {
        expect(articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('caches the related objects query', function() {
      expect(user.articleObjects).to.equal(articleObjects);
    });

    it('throws when attempting to access un-loaded collection', function() {
      expect(function() {
        user.articles;
      }).to.throw(/articles.*not yet.*loaded/i);
    });

    it('does not consider relation loaded when fetched on a duplicated query', function(done) {
      articleObjects.clone().fetch().then(function() {
        return user.articles;
      })
      .throw(new Error('Expected access to relation to fail.'))
      .catch(function(e) {
        expect(e.message).to.match(/articles.*not yet.*loaded/i);
      })
      .done(done, done);
    });

    it('allows access loaded collection', function(done) {
      articleObjects.fetch().then(function() {
        expect(user.articles).to.eql([
          Article.fresh({ id: 1, title: 'Journal', authorKey: 1 })
        ]);
      })
      .done(done, done);
    });

    it('can be filtered', function(done) {
      articleObjects.where({ title: 'Azul' }).fetch().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['SELECT * FROM "articles" WHERE ("author_num" = ?) AND ' +
           '"title" = ?', [1, 'Azul']]
        ]);
      })
      .done(done, done);
    });

    it('allows update', function(done) {
      articleObjects.update({ title: 'Azul' }).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "title" = ? ' +
           'WHERE "author_num" = ?', ['Azul', 1]]
        ]);
      })
      .done(done, done);
    });

    it('allows delete', function(done) {
      articleObjects.delete().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['DELETE FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });
  });

  describe('helpers', function() {
    it('allows create', function() {
      var article = user.createArticle({ title: 'Hello' });
      expect(article.authorKey).to.eql(user.id);
      expect(article).to.to.be.an.instanceOf(Article.__class__);
    });

    it('does not create collection cache during create', function() {
      var article = user.createArticle({ title: 'Hello' });
      expect(function() {
        user.articles;
      }).to.throw(/articles.*not yet.*loaded/i);
      expect(article).to.exist;
    });

    it('updates collection cache during create', function(done) {
      var article;
      user.articleObjects.fetch().then(function() {
        article = user.createArticle({ title: 'Hello' });
      })
      .then(function() {
        expect(user.articles).to.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during create', function() {
      var articleObjects = user.articleObjects;
      var article = user.createArticle({ title: 'Hello' });
      expect(user.articleObjects).to.not.equal(articleObjects);
      expect(article).to.exist;
    });

    it('allows add with existing objects', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      user.addArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" = ?', [1, 5]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with multiple existing objects', function(done) {
      var article1 = Article.fresh({ id: 5, title: 'Hello' });
      var article2 = Article.fresh({ id: 8, title: 'Hello' });
      user.addArticles(article1, article2).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" IN (?, ?)', [1, 5, 8]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with unsaved objects', function(done) {
      var article = Article.fresh({ id: 12, title: 'Hello' });
      article.title = 'Renamed';
      user.addArticle(article).then(function() {
        // note that this expectation depends on ordering of object properties
        // which is not guaranteed to be a stable ordering.
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "title" = ?, "author_num" = ? ' +
           'WHERE "id" = ?', ['Renamed', 1, 12]]
        ]);
      })
      .done(done, done);
    });

    it('allows add with created objects', function(done) {
      var article = Article.create({ title: 'Hello' });
      user.addArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['INSERT INTO "articles" ("title", "author_num") VALUES (?, ?) ' +
           'RETURNING "id"', ['Hello', 1]]
        ]);
      })
      .done(done, done);
    });

    it('updates collection cache during add', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      user.articleObjects.fetch().then(function() {
        return user.addArticle(article);
      })
      .then(function() {
        expect(user.articles).to.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during add', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello' });
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.addArticle(article);
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });

    it('allows remove with existing objects', function(done) {
      var article = Article.fresh({ id: 5, title: 'Hello', authorKey: user.id });
      user.removeArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" = ?', [undefined, 5]]
        ]);
      })
      .done(done, done);
    });

    it('allows remove with multiple existing objects', function(done) {
      var article1 = Article.fresh({ id: 5, title: 'Hello' });
      var article2 = Article.fresh({ id: 8, title: 'Hello' });
      user.removeArticles(article1, article2).then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "author_num" = ? ' +
           'WHERE "id" IN (?, ?)', [undefined, 5, 8]]
        ]);
      })
      .done(done, done);
    });

    it.skip('allows remove with unsaved objects', function(done) {
      var article = Article.fresh({ id: 12, title: 'Hello' });
      article.title = 'Renamed';
      user.removeArticle(article).then(function() {
        // note that this expectation depends on ordering of object properties
        // which is not guaranteed to be a stable ordering.
        expect(adapter.executedSQL()).to.eql([
          ['UPDATE "articles" SET "title" = ?, "author_num" = ? ' +
           'WHERE "id" = ?', ['Renamed', undefined, 12]]
        ]);
      })
      .done(done, done);
    });

    it.skip('allows remove with created objects', function(done) {
      var article = Article.create({ title: 'Hello' });
      user.removeArticle(article).then(function() {
        expect(adapter.executedSQL()).to.eql([
        ]);
      })
      .done(done, done);
    });

    it('updates collection cache during remove', function(done) {
      var article;
      user.articleObjects.fetch().then(function() {
        article = user.articles[0];
        return user.removeArticle(article);
      })
      .then(function() {
        expect(user.articles).to.not.contain(article);
      })
      .done(done, done);
    });

    it('clears query cache during remove', function(done) {
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.removeArticle(user.articles[0]);
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });

    it('allows clear', function(done) {
      user.clearArticles().then(function() {
        expect(adapter.executedSQL()).to.eql([
          ['DELETE FROM "articles" WHERE "author_num" = ?', [1]]
        ]);
      })
      .done(done, done);
    });

    it('updates collection cache during clear', function(done) {
      user.articleObjects.fetch().then(function() {
        return user.clearArticles();
      })
      .then(function() {
        expect(user.articles).to.eql([]);
      })
      .done(done, done);
    });

    it('clears query cache during clear', function(done) {
      var articleObjects = user.articleObjects;
      articleObjects.fetch().then(function() {
        return user.clearArticles();
      })
      .then(function() {
        expect(user.articleObjects).to.not.equal(articleObjects);
      })
      .done(done, done);
    });
  });
});
<|endoftext|>


<commit_before>import { store } from '../../util'
import chalk from 'chalk'

const log = console.log

export async function stats () {
  await Promise.all(Object.keys(store.count).map(async counter => {
    const count = store.count[counter].length
    const counterColour = counter === 'added' ? 'greenBright' : counter === 'updated' ? 'blueBright' : 'redBright'
    const counterLabel = counter.toUpperCase()

    if (process.env.TRAVIS) log(`travis_fold:start:${counter}`)
    if (count > 0) log(chalk`{bold.${counterColour} ${counterLabel}} ${count} anime`)

    await store.count[counter]
      .sort((a, b) => a.localeCompare(b, 'en', {'sensitivity': 'base'}))
      .map(anime => {
        log(chalk`  {gray ${anime}}`)
      })

    if (process.env.TRAVIS) log(`travis_fold:end:${counter}\n`)
  }))
}
<commit_msg>fix: log travis folds at correct positions<commit_after>import { store } from '../../util'
import chalk from 'chalk'

const log = console.log
const travis = process.env.TRAVIS

export async function stats () {
  await Promise.all(Object.keys(store.count).map(async counter => {
    const count = store.count[counter].length
    const counterColour = counter === 'added' ? 'greenBright' : counter === 'updated' ? 'blueBright' : 'redBright'
    const counterLabel = counter.toUpperCase()

    if (!travis) log(`travis_fold:start:${counter}`)
    if (count > 0) log(chalk`{bold.${counterColour} ${counterLabel}} ${count} anime`)

    store.count[counter]
      .sort((a, b) => a.localeCompare(b, 'en', {'sensitivity': 'base'}))
      .map(anime => {
        log(chalk`  {gray ${anime}}`)
      })

    if (!travis) log(`travis_fold:end:${counter}`)
  }))
}
<|endoftext|>