<commit_before>/**
 * Express configuration
 */

'use strict';

import express from 'express';
import favicon from 'serve-favicon';
import morgan from 'morgan';
import compression from 'compression';
import bodyParser from 'body-parser';
import methodOverride from 'method-override';
import cookieParser from 'cookie-parser';
import errorHandler from 'errorhandler';
import path from 'path';
import lusca from 'lusca';
import config from './environment';
import passport from 'passport';
import session from 'express-session';
import connectMongo from 'connect-mongo';
import mongoose from 'mongoose';

var mongoStore = connectMongo(session);
var cors = require('cors');

export default function(app) {
  var env = app.get('env');

  app.set('views', config.root + '/server/views');
  app.engine('html', require('ejs').renderFile);
  app.set('view engine', 'html');
  app.use(compression());
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json());
  app.use(methodOverride());
  app.use(cookieParser());
  app.use(passport.initialize());

  // Persist sessions with mongoStore / sequelizeStore
  // We need to enable sessions for passport-twitter because it's an
  // oauth 1.0 strategy, and Lusca depends on sessions
  app.use(session({
    secret: config.secrets.session,
    saveUninitialized: true,
    resave: false,
    store: new mongoStore({
      mongooseConnection: mongoose.connection,
      db: 'game-platform-server'
    })
  }));

var whitelist = [
    'http://localhost:90',
];
var corsOptions = {
    origin: function(origin, callback){
        var originIsWhitelisted = whitelist.indexOf(origin) !== -1;
        callback(null, originIsWhitelisted);
    },
    credentials: true
};
app.use(cors(corsOptions));

  /**
   * Lusca - express server security
   * https://github.com/krakenjs/lusca

  if ('test' !== env) {
    app.use(lusca({
      csrf: {
        angular: true
      },
      xframe: 'SAMEORIGIN',
      hsts: {
        maxAge: 31536000, //1 year, in seconds
        includeSubDomains: true,
        preload: true
      },
      xssProtection: true
    }));
  }
 */
  app.set('appPath', path.join(config.root, 'client'));

  if ('production' === env) {
    app.use(favicon(path.join(config.root, 'client', 'favicon.ico')));
    app.use(express.static(app.get('appPath')));
    app.use(morgan('dev'));
  }

  if ('development' === env) {
    app.use(require('connect-livereload')());
  }

  if ('development' === env || 'test' === env) {
    app.use(express.static(path.join(config.root, '.tmp')));
    app.use(express.static(app.get('appPath')));
    app.use(morgan('dev'));
    app.use(errorHandler()); // Error handler - has to be last
  }
}
<commit_msg>added allow location for client<commit_after>/**
 * Express configuration
 */

'use strict';

import express from 'express';
import favicon from 'serve-favicon';
import morgan from 'morgan';
import compression from 'compression';
import bodyParser from 'body-parser';
import methodOverride from 'method-override';
import cookieParser from 'cookie-parser';
import errorHandler from 'errorhandler';
import path from 'path';
import lusca from 'lusca';
import config from './environment';
import passport from 'passport';
import session from 'express-session';
import connectMongo from 'connect-mongo';
import mongoose from 'mongoose';

var mongoStore = connectMongo(session);
var cors = require('cors');

export default function(app) {
  var env = app.get('env');

  app.set('views', config.root + '/server/views');
  app.engine('html', require('ejs').renderFile);
  app.set('view engine', 'html');
  app.use(compression());
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json());
  app.use(methodOverride());
  app.use(cookieParser());
  app.use(passport.initialize());

  // Persist sessions with mongoStore / sequelizeStore
  // We need to enable sessions for passport-twitter because it's an
  // oauth 1.0 strategy, and Lusca depends on sessions
  app.use(session({
    secret: config.secrets.session,
    saveUninitialized: true,
    resave: false,
    store: new mongoStore({
      mongooseConnection: mongoose.connection,
      db: 'game-platform-server'
    })
  }));

var whitelist = [
    'http://localhost:90',
    'https://jackorbetter-client.appspot.com'
];
var corsOptions = {
    origin: function(origin, callback){
        var originIsWhitelisted = whitelist.indexOf(origin) !== -1;
        callback(null, originIsWhitelisted);
    },
    credentials: true
};
app.use(cors(corsOptions));

  /**
   * Lusca - express server security
   * https://github.com/krakenjs/lusca

  if ('test' !== env) {
    app.use(lusca({
      csrf: {
        angular: true
      },
      xframe: 'SAMEORIGIN',
      hsts: {
        maxAge: 31536000, //1 year, in seconds
        includeSubDomains: true,
        preload: true
      },
      xssProtection: true
    }));
  }
 */
  app.set('appPath', path.join(config.root, 'client'));

  if ('production' === env) {
    app.use(favicon(path.join(config.root, 'client', 'favicon.ico')));
    app.use(express.static(app.get('appPath')));
    app.use(morgan('dev'));
  }

  if ('development' === env) {
    app.use(require('connect-livereload')());
  }

  if ('development' === env || 'test' === env) {
    app.use(express.static(path.join(config.root, '.tmp')));
    app.use(express.static(app.get('appPath')));
    app.use(morgan('dev'));
    app.use(errorHandler()); // Error handler - has to be last
  }
}
<|endoftext|>


<commit_before>import express from 'express';
import passport from 'passport';
import session from 'express-session';
import bodyParser from 'body-parser';
import path from 'path';
import flash from 'express-flash';
import methodOverride from 'method-override';
import unsupportedMessage from '../db/unsupportedMessage';
import { sessionSecret } from './secrets';
import { DB_TYPE, ENV } from './appConfig';
import { session as dbSession } from '../db';

export default (app) => {
  app.set('port', (process.env.PORT || 3000));

  // X-Powered-By header has no functional value.
  // Keeping it makes it easier for an attacker to build the site's profile
  // It can be removed safely
  app.disable('x-powered-by');
  app.set('views', path.join(__dirname, '..', 'views'));

  app.set('view cache', false);

  // app.use(require('force-ssl-heroku'));
  app.use(bodyParser.json({limit: '50mb'}));
  app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); // for parsing application/x-www-form-urlencoded
  app.use(methodOverride());
  app.use(express.static(path.join(__dirname, '../..', 'public')));

  // I am adding this here so that the Heroku deploy will work
  // Indicates the app is behind a front-facing proxy,
  // and to use the X-Forwarded-* headers to determine the connection and the IP address of the client.
  // NOTE: X-Forwarded-* headers are easily spoofed and the detected IP addresses are unreliable.
  // trust proxy is disabled by default.
  // When enabled, Express attempts to determine the IP address of the client connected through the front-facing proxy, or series of proxies.
  // The req.ips property, then, contains an array of IP addresses the client is connected through.
  // To enable it, use the values described in the trust proxy options table.
  // The trust proxy setting is implemented using the proxy-addr package. For more information, see its documentation.
  // loopback - 127.0.0.1/8, ::1/128
  app.set('trust proxy', 'loopback');
  // Create a session middleware with the given options
  // Note session data is not saved in the cookie itself, just the session ID. Session data is stored server-side.
  // Options: resave: forces the session to be saved back to the session store, even if the session was never
  //                  modified during the request. Depending on your store this may be necessary, but it can also
  //                  create race conditions where a client has two parallel requests to your server and changes made
  //                  to the session in one request may get overwritten when the other request ends, even if it made no
  //                  changes(this behavior also depends on what store you're using).
  //          saveUnitialized: Forces a session that is uninitialized to be saved to the store. A session is uninitialized when
  //                  it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage
  //                  usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with
  //                  race conditions where a client makes multiple parallel requests without a session
  //          secret: This is the secret used to sign the session ID cookie.
  //          name: The name of the session ID cookie to set in the response (and read from in the request).
  //          cookie: Please note that secure: true is a recommended option.
  //                  However, it requires an https-enabled website, i.e., HTTPS is necessary for secure cookies.
  //                  If secure is set, and you access your site over HTTP, the cookie will not be set.
  let sessionStore = null;
  if (!dbSession) {
    console.warn(unsupportedMessage('session'));
  } else {
    sessionStore = dbSession();
  }

  const sess = {
    resave: false,
    saveUninitialized: false,
    secret: sessionSecret,
    proxy: true, // The "X-Forwarded-Proto" header will be used.
    name: 'sessionId',
    // Add HTTPOnly, Secure attributes on Session Cookie
    // If secure is set, and you access your site over HTTP, the cookie will not be set
    cookie: {
      httpOnly: true,
      secure: false
    },
    store: sessionStore
  };

  console.log('--------------------------');
  console.log('===> 😊  Starting Server . . .');
  console.log(`===>  Environment: ${ENV}`);
  console.log(`===>  Listening on port: ${app.get('port')}`);
  console.log(`===>  Using DB TYPE: ${DB_TYPE}`);
  if (ENV === 'production') {
    console.log('===> 🚦  Note: In order for authentication to work in production');
    console.log('===>           you will need a secure HTTPS connection');
    //sess.cookie.secure = true; // Serve secure cookies
  }
  console.log('--------------------------');

  app.use(session(sess));

  app.use(passport.initialize());
  app.use(passport.session());

  app.use(flash());
};
<commit_msg>fix #10 - accidentally removed the ssl redirection<commit_after>import express from 'express';
import passport from 'passport';
import session from 'express-session';
import bodyParser from 'body-parser';
import path from 'path';
import flash from 'express-flash';
import methodOverride from 'method-override';
import unsupportedMessage from '../db/unsupportedMessage';
import { sessionSecret } from './secrets';
import { DB_TYPE, ENV } from './appConfig';
import { session as dbSession } from '../db';

export default (app) => {
  app.set('port', (process.env.PORT || 3000));

  // X-Powered-By header has no functional value.
  // Keeping it makes it easier for an attacker to build the site's profile
  // It can be removed safely
  app.disable('x-powered-by');
  app.set('views', path.join(__dirname, '..', 'views'));

  app.set('view cache', false);

  app.use(require('force-ssl-heroku'));
  app.use(bodyParser.json({limit: '50mb'}));
  app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); // for parsing application/x-www-form-urlencoded
  app.use(methodOverride());
  app.use(express.static(path.join(__dirname, '../..', 'public')));

  // I am adding this here so that the Heroku deploy will work
  // Indicates the app is behind a front-facing proxy,
  // and to use the X-Forwarded-* headers to determine the connection and the IP address of the client.
  // NOTE: X-Forwarded-* headers are easily spoofed and the detected IP addresses are unreliable.
  // trust proxy is disabled by default.
  // When enabled, Express attempts to determine the IP address of the client connected through the front-facing proxy, or series of proxies.
  // The req.ips property, then, contains an array of IP addresses the client is connected through.
  // To enable it, use the values described in the trust proxy options table.
  // The trust proxy setting is implemented using the proxy-addr package. For more information, see its documentation.
  // loopback - 127.0.0.1/8, ::1/128
  app.set('trust proxy', 'loopback');
  // Create a session middleware with the given options
  // Note session data is not saved in the cookie itself, just the session ID. Session data is stored server-side.
  // Options: resave: forces the session to be saved back to the session store, even if the session was never
  //                  modified during the request. Depending on your store this may be necessary, but it can also
  //                  create race conditions where a client has two parallel requests to your server and changes made
  //                  to the session in one request may get overwritten when the other request ends, even if it made no
  //                  changes(this behavior also depends on what store you're using).
  //          saveUnitialized: Forces a session that is uninitialized to be saved to the store. A session is uninitialized when
  //                  it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage
  //                  usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with
  //                  race conditions where a client makes multiple parallel requests without a session
  //          secret: This is the secret used to sign the session ID cookie.
  //          name: The name of the session ID cookie to set in the response (and read from in the request).
  //          cookie: Please note that secure: true is a recommended option.
  //                  However, it requires an https-enabled website, i.e., HTTPS is necessary for secure cookies.
  //                  If secure is set, and you access your site over HTTP, the cookie will not be set.
  let sessionStore = null;
  if (!dbSession) {
    console.warn(unsupportedMessage('session'));
  } else {
    sessionStore = dbSession();
  }

  const sess = {
    resave: false,
    saveUninitialized: false,
    secret: sessionSecret,
    proxy: true, // The "X-Forwarded-Proto" header will be used.
    name: 'sessionId',
    // Add HTTPOnly, Secure attributes on Session Cookie
    // If secure is set, and you access your site over HTTP, the cookie will not be set
    cookie: {
      httpOnly: true,
      secure: false
    },
    store: sessionStore
  };

  console.log('--------------------------');
  console.log('===> 😊  Starting Server . . .');
  console.log(`===>  Environment: ${ENV}`);
  console.log(`===>  Listening on port: ${app.get('port')}`);
  console.log(`===>  Using DB TYPE: ${DB_TYPE}`);
  if (ENV === 'production') {
    console.log('===> 🚦  Note: In order for authentication to work in production');
    console.log('===>           you will need a secure HTTPS connection');
    //sess.cookie.secure = true; // Serve secure cookies
  }
  console.log('--------------------------');

  app.use(session(sess));

  app.use(passport.initialize());
  app.use(passport.session());

  app.use(flash());
};
<|endoftext|>


<commit_before>// mode: js2

startMonitoringContainer = function(hostId, containerId){
  if (! ensureApi(hostId,"1.17"))
    return;
  if (! _.isUndefined(containerStats[containerId]))
    return;
  if (docker[hostId] === undefined)
    return;

  var container = docker[hostId].getContainer(containerId);
  if (!container)
    return;
  containerStats[containerId] = {status: 2};
  container.stats(Meteor.bindEnvironment(
    function (err, stream) {
      if (err){
        delete containerStats[containerId];
        console.log(err);
        return;
      }

      containerStats[containerId] = {stream: stream, status:1};
      stream.on('data', Meteor.bindEnvironment(
        function(chunk){
          try{
            var stat = JSON.parse(chunk);
            stat.Id = containerId;
            stat._host = hostId;
            stat.read = moment(stat.read).toDate();
            ContainersStats.insert(stat);
          } catch(err){
            console.log('error during stats: ', err);
          }
        })).
        on('end', Meteor.bindEnvironment(function(){
                    delete containerStats[containerId];
                  })).
        on('close', Meteor.bindEnvironment(function(){
                      delete containerStats[containerId];
                    })).
        on('error', Meteor.bindEnvironment(function(){
                      delete containerStats[containerId];
                    }));
    }));
};

startMonitoringContainers = function(){
  Containers.find().forEach(function(container){
    startMonitoringContainer(container._host,container.Id);
  });
};

// Clean old stats
SyncedCron.add({
  name: 'Clean old stats',
  schedule: function(parser) {
    return parser.text('every 2 hours');
  },
  job: function() {
    var query = {read: {$lt: moment().subtract(1, 'days').toDate()}};
    var stats = ContainersStats.find(query);
    ContainersStats.remove(query)
    return stats.count();
  }
});

SyncedCron.config({
  log:false
});

Meteor.startup(function(){
  SyncedCron.start();
});<commit_msg>save stats with upsert<commit_after>// mode: js2

startMonitoringContainer = function(hostId, containerId){
  if (! ensureApi(hostId,"1.17"))
    return;
  if (! _.isUndefined(containerStats[containerId]))
    return;
  if (docker[hostId] === undefined)
    return;

  var container = docker[hostId].getContainer(containerId);
  if (!container)
    return;
  containerStats[containerId] = {status: 2};
  container.stats(Meteor.bindEnvironment(
    function (err, stream) {
      if (err){
        delete containerStats[containerId];
        console.log(err);
        return;
      }

      containerStats[containerId] = {stream: stream, status:1};
      stream.on('data', Meteor.bindEnvironment(
        function(chunk){
          try{
            var stat = JSON.parse(chunk);
            stat.Id = containerId;
            stat._host = hostId;
            stat.read = moment(stat.read).toDate();
            ContainersStats.upsert({Id:stat.Id, _host: stat._host, read: stat.read}, stat);
          } catch(err){
            console.log('error during stats: ', err, chunk.toString());
          }
        })).
        on('end', Meteor.bindEnvironment(function(){
                    delete containerStats[containerId];
                  })).
        on('close', Meteor.bindEnvironment(function(){
                      delete containerStats[containerId];
                    })).
        on('error', Meteor.bindEnvironment(function(){
                      delete containerStats[containerId];
                    }));
    }));
};

startMonitoringContainers = function(){
  Containers.find().forEach(function(container){
    startMonitoringContainer(container._host,container.Id);
  });
};

// Clean old stats
SyncedCron.add({
  name: 'Clean old stats',
  schedule: function(parser) {
    return parser.text('every 2 hours');
  },
  job: function() {
    var query = {read: {$lt: moment().subtract(1, 'days').toDate()}};
    var stats = ContainersStats.find(query);
    ContainersStats.remove(query)
    return stats.count();
  }
});

SyncedCron.config({
  log:false
});

Meteor.startup(function(){
  SyncedCron.start();
});<|endoftext|>


<commit_before>// Copyright (c) 2010 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.

// require: listselectionmodel.js

/**
 * @fileoverview This implements a list control.
 */

cr.define('cr.ui', function() {
  const ListSelectionModel = cr.ui.ListSelectionModel;
  const ListSelectionController = cr.ui.ListSelectionController;
  const ArrayDataModel = cr.ui.ArrayDataModel;

  /**
   * Whether a mouse event is inside the element viewport. This will return
   * false if the mouseevent was generated over a border or a scrollbar.
   * @param {!HTMLElement} el The element to test the event with.
   * @param {!Event} e The mouse event.
   * @param {boolean} Whether the mouse event was inside the viewport.
   */
  function inViewport(el, e) {
    var rect = el.getBoundingClientRect();
    var x = e.clientX;
    var y = e.clientY;
    return x >= rect.left + el.clientLeft &&
           x < rect.left + el.clientLeft + el.clientWidth &&
           y >= rect.top + el.clientTop &&
           y < rect.top + el.clientTop + el.clientHeight;
  }

  /**
   * Creates an item (dataModel.item(0)) and measures its height.
   * @param {!List} list The list to create the item for.
   * @return {number} The height of the item, taking margins into account.
   */
  function measureItem(list) {
    var dataModel = list.dataModel;
    if (!dataModel || !dataModel.length)
      return 0;
    var item = list.createItem(dataModel.item(0));
    list.appendChild(item);
    var cs = getComputedStyle(item);
    var mt = parseFloat(cs.marginTop);
    var mb = parseFloat(cs.marginBottom);
    var h = item.offsetHeight;

    // Handle margin collapsing.
    if (mt < 0 && mb < 0) {
      h += Math.min(mt, mb);
    } else if (mt >= 0 && mb >= 0) {
      h += Math.max(mt, mb);
    } else {
      h += mt + mb;
    }

    list.removeChild(item);
    return Math.max(0, h);
  }

  function getComputedStyle(el) {
    return el.ownerDocument.defaultView.getComputedStyle(el);
  }

  /**
   * Creates a new list element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLUListElement}
   */
  var List = cr.ui.define('list');

  List.prototype = {
    __proto__: HTMLUListElement.prototype,

    /**
     * The height of list items. This is lazily calculated the first time it is
     * needed.
     * @type {number}
     * @private
     */
    itemHeight_: 0,

    dataModel_: null,

    /**
     * The data model driving the list.
     * @type {ListDataModel}
     */
    set dataModel(dataModel) {
      if (this.dataModel_ != dataModel) {
        if (!this.boundHandleDataModelSplice_) {
          this.boundHandleDataModelSplice_ =
              this.handleDataModelSplice_.bind(this);
          this.boundHandleDataModelChange_ =
              this.handleDataModelChange_.bind(this);
        }

        if (this.dataModel_) {
          this.dataModel_.removeEventListener('splice',
                                              this.boundHandleDataModelSplice_);
          this.dataModel_.removeEventListener('change',
                                              this.boundHandleDataModelChange_);
        }

        this.dataModel_ = dataModel;

        this.cachedItems_ = {};
        this.selectionModel.clear();
        if (dataModel)
          this.selectionModel.adjust(0, 0, dataModel.length);

        if (this.dataModel_) {
          this.dataModel_.addEventListener('splice',
                                           this.boundHandleDataModelSplice_);
          this.dataModel_.addEventListener('change',
                                           this.boundHandleDataModelChange_);
        }

        this.redraw();
      }
    },

    get dataModel() {
      return this.dataModel_;
    },

    /**
     * The selection model to use.
     * @type {cr.ui.ListSelectionModel}
     */
    get selectionModel() {
      return this.selectionModel_;
    },
    set selectionModel(sm) {
      var oldSm = this.selectionModel_;
      if (oldSm == sm)
        return;

      if (!this.boundHandleOnChange_) {
        this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
        this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this);
      }

      if (oldSm) {
        oldSm.removeEventListener('change', this.boundHandleOnChange_);
        oldSm.removeEventListener('leadIndexChange',
                                  this.boundHandleLeadChange_);
      }

      this.selectionModel_ = sm;
      this.selectionController_ = this.createSelectionController(sm);

      if (sm) {
        sm.addEventListener('change', this.boundHandleOnChange_);
        sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
      }
    },

    /**
     * Convenience alias for selectionModel.selectedItem
     * @type {cr.ui.ListItem}
     */
    get selectedItem() {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = this.selectionModel.selectedIndex;
        if (index != -1)
          return dataModel.item(index);
      }
      return null;
    },
    set selectedItem(selectedItem) {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = this.dataModel.indexOf(selectedItem);
        this.selectionModel.selectedIndex = index;
      }
    },

    /**
     * Convenience alias for selectionModel.selectedItems
     * @type {!Array<cr.ui.ListItem>}
     */
    get selectedItems() {
      var indexes = this.selectionModel.selectedIndexes;
      var dataModel = this.dataModel;
      if (dataModel) {
        return indexes.map(function(i) {
          return dataModel.item(i);
        });
      }
      return [];
    },

    /**
     * The HTML elements representing the items. This is just all the element
     * children but subclasses may override this to filter out certain elements.
     * @type {HTMLCollection}
     */
    get items() {
      return this.children;
    },

    batchCount_: 0,

    /**
     * When making a lot of updates to the list, the code could be wrapped in
     * the startBatchUpdates and finishBatchUpdates to increase performance. Be
     * sure that the code will not return without calling endBatchUpdates or the
     * list will not be correctly updated.
     */
    startBatchUpdates: function() {
      this.batchCount_++;
    },

    /**
     * See startBatchUpdates.
     */
    endBatchUpdates: function() {
      this.batchCount_--;
      if (this.batchCount_ == 0)
        this.redraw();
    },

    /**
     * Initializes the element.
     */
    decorate: function() {
      // Add fillers.
      this.beforeFiller_ = this.ownerDocument.createElement('div');
      this.afterFiller_ = this.ownerDocument.createElement('div');
      this.beforeFiller_.className = 'spacer';
      this.afterFiller_.className = 'spacer';
      this.appendChild(this.beforeFiller_);
      this.appendChild(this.afterFiller_);

      var length = this.dataModel ? this.dataModel.length : 0;
      this.selectionModel = new ListSelectionModel(length);

      this.addEventListener('mousedown', this.handleMouseDownUp_);
      this.addEventListener('mouseup', this.handleMouseDownUp_);
      this.addEventListener('keydown', this.handleKeyDown);
      this.addEventListener('scroll', this.redraw.bind(this));

      // Make list focusable
      if (!this.hasAttribute('tabindex'))
        this.tabIndex = 0;
    },

    /**
     * Callback for mousedown and mouseup events.
     * @param {Event} e The mouse event object.
     * @private
     */
    handleMouseDownUp_: function(e) {
      var target = e.target;

      // If the target was this element we need to make sure that the user did
      // not click on a border or a scrollbar.
      if (target == this && !inViewport(target, e))
        return;

      while (target && target.parentNode != this) {
        target = target.parentNode;
      }

      if (!target) {
        this.selectionController_.handleMouseDownUp(e, -1);
      } else {
        var cs = getComputedStyle(target);
        var top = target.offsetTop -
                  parseFloat(cs.marginTop);
        var index = Math.floor(top / this.itemHeight_);
        this.selectionController_.handleMouseDownUp(e, index);
      }
    },

    /**
     * Handle a keydown event.
     * @param {Event} e The keydown event.
     * @return {boolean} Whether the key event was handled.
     */
    handleKeyDown: function(e) {
      return this.selectionController_.handleKeyDown(e);
    },

    /**
     * Callback from the selection model. We dispatch {@code change} events
     * when the selection changes.
     * @param {!cr.Event} e Event with change info.
     * @private
     */
    handleOnChange_: function(ce) {
      ce.changes.forEach(function(change) {
        var listItem = this.getListItemByIndex(change.index);
        if (listItem)
          listItem.selected = change.selected;
      }, this);

      cr.dispatchSimpleEvent(this, 'change');
    },

    /**
     * Handles a change of the lead item from the selection model.
     * @property {Event} pe The property change event.
     * @private
     */
    handleLeadChange_: function(pe) {
      var element;
      if (pe.oldValue != -1) {
        if ((element = this.getListItemByIndex(pe.oldValue)))
          element.lead = false;
      }

      if (pe.newValue != -1) {
        this.scrollIndexIntoView(pe.newValue);
        if ((element = this.getListItemByIndex(pe.newValue)))
          element.lead = true;
      }
    },

    handleDataModelSplice_: function(e) {
      this.selectionModel.adjust(e.index, e.removed.length, e.added.length);
      // Remove the cache of everything above index.
      for (var index in this.cachedItems_) {
        if (index >= e.index)
          delete this.cachedItems_[index];
      }
      this.redraw();
    },

    handleDataModelChange_: function(e) {
      if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) {
        delete this.cachedItems_;
        this.redraw();
      }
    },

    /**
     * Ensures that a given index is inside the viewport.
     * @param {number} index The index of the item to scroll into view.
     * @return {boolean} Whether any scrolling was needed.
     */
    scrollIndexIntoView: function(index) {
      var dataModel = this.dataModel;
      if (!dataModel || index < 0 || index >= dataModel.length)
        return false;

      var itemHeight = this.itemHeight_;
      var scrollTop = this.scrollTop;
      var top = index * itemHeight;

      if (top < scrollTop) {
        this.scrollTop = top;
        return true;
      } else {
        var clientHeight = this.clientHeight;
        var cs = getComputedStyle(this);
        var paddingY = parseInt(cs.paddingTop, 10) +
                       parseInt(cs.paddingBottom, 10);

        if (top + itemHeight > scrollTop + clientHeight - paddingY) {
          this.scrollTop = top + itemHeight - clientHeight + paddingY;
          return true;
        }
      }

      return false;
    },

    /**
     * @return {!ClientRect} The rect to use for the context menu.
     */
    getRectForContextMenu: function() {
      // TODO(arv): Add trait support so we can share more code between trees
      // and lists.
      var index = this.selectionModel.selectedIndex;
      var el = this.getListItemByIndex(index);
      if (el)
        return el.getBoundingClientRect();
      return this.getBoundingClientRect();
    },

    /**
     * Takes a value from the data model and finds the associated list item.
     * @param {*} value The value in the data model that we want to get the list
     *     item for.
     * @return {ListItem} The first found list item or null if not found.
     */
    getListItem: function(value) {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = dataModel.indexOf(value);
        return this.getListItemByIndex(index);
      }
      return null;
    },

    /**
     * Find the list item element at the given index.
     * @param {number} index The index of the list item to get.
     * @return {ListItem} The found list item or null if not found.
     */
    getListItemByIndex: function(index) {
      if (index < this.firstIndex_ || index >= this.lastIndex_)
        return null;

      return this.children[index - this.firstIndex_ + 1];
    },

    /**
     * Creates a new list item.
     * @param {*} value The value to use for the item.
     * @return {!ListItem} The newly created list item.
     */
    createItem: function(value) {
      return new cr.ui.ListItem({label: value});
    },

    /**
     * Creates the selection controller to use internally.
     * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
     * @return {!cr.ui.ListSelectionModel} The newly created selection
     *     controller.
     */
    createSelectionController: function(sm) {
      return new ListSelectionController(sm);
    },

    /**
     * Redraws the viewport.
     */
    redraw: function() {
      if (this.batchCount_ != 0)
        return;

      var dataModel = this.dataModel;
      if (!dataModel) {
        this.textContent = '';
        return;
      }

      console.time('redraw');
      var scrollTop = this.scrollTop;
      var clientHeight = this.clientHeight;

      if (!this.itemHeight_) {
        this.itemHeight_ = measureItem(this);
      }

      var itemHeight = this.itemHeight_;

      // We cache the list items since creating the DOM nodes is the most
      // expensive part of redrawing.
      var cachedItems = this.cachedItems_ || {};
      var newCachedItems = {};

      var desiredScrollHeight = dataModel.length * itemHeight;

      var firstIndex = Math.floor(scrollTop / itemHeight);
      var itemsInViewPort = Math.min(dataModel.length - firstIndex,
          Math.ceil((scrollTop + clientHeight - firstIndex * itemHeight) /
                    itemHeight));
      var lastIndex = firstIndex + itemsInViewPort;

      this.textContent = '';

      var oldFirstIndex = this.firstIndex_ || 0;
      var oldLastIndex = this.lastIndex_ || 0;

      this.beforeFiller_.style.height = firstIndex * itemHeight + 'px';
      this.appendChild(this.beforeFiller_);

      var sm = this.selectionModel;
      var leadIndex = sm.leadIndex;

      for (var y = firstIndex; y < lastIndex; y++) {
        var dataItem = dataModel.item(y);
        var listItem = cachedItems[y] || this.createItem(dataItem);
        if (y == leadIndex) {
          listItem.lead = true;
        }
        if (sm.getIndexSelected(y)) {
          listItem.selected = true;
        }
        this.appendChild(listItem);
        newCachedItems[y] = listItem;
      }

      this.afterFiller_.style.height =
          (dataModel.length - firstIndex - itemsInViewPort) * itemHeight + 'px';
      this.appendChild(this.afterFiller_);

      this.firstIndex_ = firstIndex;
      this.lastIndex_ = lastIndex;

      this.cachedItems_ = newCachedItems;

      console.timeEnd('redraw');
    },

    /**
     * Redraws a single item
     * @param {number} index The row index to redraw.
     */
    redrawItem: function(index) {
      if (index >= this.firstIndex_ && index < this.lastIndex_) {
        delete this.cachedItems_[index];
        this.redraw();
      }
    }
  };

  return {
    List: List
  }
});
<commit_msg>Bookmark manager: Fix issue where we cache the height of a list item.<commit_after>// Copyright (c) 2010 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.

// require: listselectionmodel.js

/**
 * @fileoverview This implements a list control.
 */

cr.define('cr.ui', function() {
  const ListSelectionModel = cr.ui.ListSelectionModel;
  const ListSelectionController = cr.ui.ListSelectionController;
  const ArrayDataModel = cr.ui.ArrayDataModel;

  /**
   * Whether a mouse event is inside the element viewport. This will return
   * false if the mouseevent was generated over a border or a scrollbar.
   * @param {!HTMLElement} el The element to test the event with.
   * @param {!Event} e The mouse event.
   * @param {boolean} Whether the mouse event was inside the viewport.
   */
  function inViewport(el, e) {
    var rect = el.getBoundingClientRect();
    var x = e.clientX;
    var y = e.clientY;
    return x >= rect.left + el.clientLeft &&
           x < rect.left + el.clientLeft + el.clientWidth &&
           y >= rect.top + el.clientTop &&
           y < rect.top + el.clientTop + el.clientHeight;
  }

  /**
   * Creates an item (dataModel.item(0)) and measures its height.
   * @param {!List} list The list to create the item for.
   * @param {ListItem=} opt_item The list item to use to do the measuring. If
   *     this is not provided an item will be created based on the first value
   *     in the model.
   * @return {number} The height of the item, taking margins into account.
   */
  function measureItem(list, opt_item) {
    var dataModel = list.dataModel;
    if (!dataModel || !dataModel.length)
      return 0;
    var item = opt_item || list.createItem(dataModel.item(0));
    if (!opt_item)
      list.appendChild(item);

    var cs = getComputedStyle(item);
    var mt = parseFloat(cs.marginTop);
    var mb = parseFloat(cs.marginBottom);
    var h = item.offsetHeight;

    // Handle margin collapsing.
    if (mt < 0 && mb < 0) {
      h += Math.min(mt, mb);
    } else if (mt >= 0 && mb >= 0) {
      h += Math.max(mt, mb);
    } else {
      h += mt + mb;
    }

    if (!opt_item)
      list.removeChild(item);
    return Math.max(0, h);
  }

  function getComputedStyle(el) {
    return el.ownerDocument.defaultView.getComputedStyle(el);
  }

  /**
   * Creates a new list element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLUListElement}
   */
  var List = cr.ui.define('list');

  List.prototype = {
    __proto__: HTMLUListElement.prototype,

    /**
     * The height of list items. This is lazily calculated the first time it is
     * needed.
     * @type {number}
     * @private
     */
    itemHeight_: 0,

    dataModel_: null,

    /**
     * The data model driving the list.
     * @type {ListDataModel}
     */
    set dataModel(dataModel) {
      if (this.dataModel_ != dataModel) {
        if (!this.boundHandleDataModelSplice_) {
          this.boundHandleDataModelSplice_ =
              this.handleDataModelSplice_.bind(this);
          this.boundHandleDataModelChange_ =
              this.handleDataModelChange_.bind(this);
        }

        if (this.dataModel_) {
          this.dataModel_.removeEventListener('splice',
                                              this.boundHandleDataModelSplice_);
          this.dataModel_.removeEventListener('change',
                                              this.boundHandleDataModelChange_);
        }

        this.dataModel_ = dataModel;

        this.cachedItems_ = {};
        this.selectionModel.clear();
        if (dataModel)
          this.selectionModel.adjust(0, 0, dataModel.length);

        if (this.dataModel_) {
          this.dataModel_.addEventListener('splice',
                                           this.boundHandleDataModelSplice_);
          this.dataModel_.addEventListener('change',
                                           this.boundHandleDataModelChange_);
        }

        this.redraw();
      }
    },

    get dataModel() {
      return this.dataModel_;
    },

    /**
     * The selection model to use.
     * @type {cr.ui.ListSelectionModel}
     */
    get selectionModel() {
      return this.selectionModel_;
    },
    set selectionModel(sm) {
      var oldSm = this.selectionModel_;
      if (oldSm == sm)
        return;

      if (!this.boundHandleOnChange_) {
        this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
        this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this);
      }

      if (oldSm) {
        oldSm.removeEventListener('change', this.boundHandleOnChange_);
        oldSm.removeEventListener('leadIndexChange',
                                  this.boundHandleLeadChange_);
      }

      this.selectionModel_ = sm;
      this.selectionController_ = this.createSelectionController(sm);

      if (sm) {
        sm.addEventListener('change', this.boundHandleOnChange_);
        sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
      }
    },

    /**
     * Convenience alias for selectionModel.selectedItem
     * @type {cr.ui.ListItem}
     */
    get selectedItem() {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = this.selectionModel.selectedIndex;
        if (index != -1)
          return dataModel.item(index);
      }
      return null;
    },
    set selectedItem(selectedItem) {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = this.dataModel.indexOf(selectedItem);
        this.selectionModel.selectedIndex = index;
      }
    },

    /**
     * Convenience alias for selectionModel.selectedItems
     * @type {!Array<cr.ui.ListItem>}
     */
    get selectedItems() {
      var indexes = this.selectionModel.selectedIndexes;
      var dataModel = this.dataModel;
      if (dataModel) {
        return indexes.map(function(i) {
          return dataModel.item(i);
        });
      }
      return [];
    },

    /**
     * The HTML elements representing the items. This is just all the element
     * children but subclasses may override this to filter out certain elements.
     * @type {HTMLCollection}
     */
    get items() {
      return this.children;
    },

    batchCount_: 0,

    /**
     * When making a lot of updates to the list, the code could be wrapped in
     * the startBatchUpdates and finishBatchUpdates to increase performance. Be
     * sure that the code will not return without calling endBatchUpdates or the
     * list will not be correctly updated.
     */
    startBatchUpdates: function() {
      this.batchCount_++;
    },

    /**
     * See startBatchUpdates.
     */
    endBatchUpdates: function() {
      this.batchCount_--;
      if (this.batchCount_ == 0)
        this.redraw();
    },

    /**
     * Initializes the element.
     */
    decorate: function() {
      // Add fillers.
      this.beforeFiller_ = this.ownerDocument.createElement('div');
      this.afterFiller_ = this.ownerDocument.createElement('div');
      this.beforeFiller_.className = 'spacer';
      this.afterFiller_.className = 'spacer';
      this.appendChild(this.beforeFiller_);
      this.appendChild(this.afterFiller_);

      var length = this.dataModel ? this.dataModel.length : 0;
      this.selectionModel = new ListSelectionModel(length);

      this.addEventListener('mousedown', this.handleMouseDownUp_);
      this.addEventListener('mouseup', this.handleMouseDownUp_);
      this.addEventListener('keydown', this.handleKeyDown);
      this.addEventListener('scroll', this.redraw.bind(this));

      // Make list focusable
      if (!this.hasAttribute('tabindex'))
        this.tabIndex = 0;
    },

    /**
     * Callback for mousedown and mouseup events.
     * @param {Event} e The mouse event object.
     * @private
     */
    handleMouseDownUp_: function(e) {
      var target = e.target;

      // If the target was this element we need to make sure that the user did
      // not click on a border or a scrollbar.
      if (target == this && !inViewport(target, e))
        return;

      while (target && target.parentNode != this) {
        target = target.parentNode;
      }

      if (!target) {
        this.selectionController_.handleMouseDownUp(e, -1);
      } else {
        var cs = getComputedStyle(target);
        var top = target.offsetTop -
                  parseFloat(cs.marginTop);
        var index = Math.floor(top / this.itemHeight_);
        this.selectionController_.handleMouseDownUp(e, index);
      }
    },

    /**
     * Handle a keydown event.
     * @param {Event} e The keydown event.
     * @return {boolean} Whether the key event was handled.
     */
    handleKeyDown: function(e) {
      return this.selectionController_.handleKeyDown(e);
    },

    /**
     * Callback from the selection model. We dispatch {@code change} events
     * when the selection changes.
     * @param {!cr.Event} e Event with change info.
     * @private
     */
    handleOnChange_: function(ce) {
      ce.changes.forEach(function(change) {
        var listItem = this.getListItemByIndex(change.index);
        if (listItem)
          listItem.selected = change.selected;
      }, this);

      cr.dispatchSimpleEvent(this, 'change');
    },

    /**
     * Handles a change of the lead item from the selection model.
     * @property {Event} pe The property change event.
     * @private
     */
    handleLeadChange_: function(pe) {
      var element;
      if (pe.oldValue != -1) {
        if ((element = this.getListItemByIndex(pe.oldValue)))
          element.lead = false;
      }

      if (pe.newValue != -1) {
        this.scrollIndexIntoView(pe.newValue);
        if ((element = this.getListItemByIndex(pe.newValue)))
          element.lead = true;
      }
    },

    handleDataModelSplice_: function(e) {
      this.selectionModel.adjust(e.index, e.removed.length, e.added.length);
      // Remove the cache of everything above index.
      for (var index in this.cachedItems_) {
        if (index >= e.index)
          delete this.cachedItems_[index];
      }
      this.redraw();
    },

    handleDataModelChange_: function(e) {
      if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) {
        delete this.cachedItems_;
        this.redraw();
      }
    },

    /**
     * Ensures that a given index is inside the viewport.
     * @param {number} index The index of the item to scroll into view.
     * @return {boolean} Whether any scrolling was needed.
     */
    scrollIndexIntoView: function(index) {
      var dataModel = this.dataModel;
      if (!dataModel || index < 0 || index >= dataModel.length)
        return false;

      var itemHeight = this.itemHeight_;
      var scrollTop = this.scrollTop;
      var top = index * itemHeight;

      if (top < scrollTop) {
        this.scrollTop = top;
        return true;
      } else {
        var clientHeight = this.clientHeight;
        var cs = getComputedStyle(this);
        var paddingY = parseInt(cs.paddingTop, 10) +
                       parseInt(cs.paddingBottom, 10);

        if (top + itemHeight > scrollTop + clientHeight - paddingY) {
          this.scrollTop = top + itemHeight - clientHeight + paddingY;
          return true;
        }
      }

      return false;
    },

    /**
     * @return {!ClientRect} The rect to use for the context menu.
     */
    getRectForContextMenu: function() {
      // TODO(arv): Add trait support so we can share more code between trees
      // and lists.
      var index = this.selectionModel.selectedIndex;
      var el = this.getListItemByIndex(index);
      if (el)
        return el.getBoundingClientRect();
      return this.getBoundingClientRect();
    },

    /**
     * Takes a value from the data model and finds the associated list item.
     * @param {*} value The value in the data model that we want to get the list
     *     item for.
     * @return {ListItem} The first found list item or null if not found.
     */
    getListItem: function(value) {
      var dataModel = this.dataModel;
      if (dataModel) {
        var index = dataModel.indexOf(value);
        return this.getListItemByIndex(index);
      }
      return null;
    },

    /**
     * Find the list item element at the given index.
     * @param {number} index The index of the list item to get.
     * @return {ListItem} The found list item or null if not found.
     */
    getListItemByIndex: function(index) {
      if (index < this.firstIndex_ || index >= this.lastIndex_)
        return null;

      return this.children[index - this.firstIndex_ + 1];
    },

    /**
     * Creates a new list item.
     * @param {*} value The value to use for the item.
     * @return {!ListItem} The newly created list item.
     */
    createItem: function(value) {
      return new cr.ui.ListItem({label: value});
    },

    /**
     * Creates the selection controller to use internally.
     * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
     * @return {!cr.ui.ListSelectionModel} The newly created selection
     *     controller.
     */
    createSelectionController: function(sm) {
      return new ListSelectionController(sm);
    },

    /**
     * Redraws the viewport.
     */
    redraw: function() {
      if (this.batchCount_ != 0)
        return;

      var dataModel = this.dataModel;
      if (!dataModel) {
        this.textContent = '';
        return;
      }

      console.time('redraw');
      var scrollTop = this.scrollTop;
      var clientHeight = this.clientHeight;

      if (!this.itemHeight_)
        this.itemHeight_ = measureItem(this);

      var itemHeight = this.itemHeight_;

      // We cache the list items since creating the DOM nodes is the most
      // expensive part of redrawing.
      var cachedItems = this.cachedItems_ || {};
      var newCachedItems = {};

      var desiredScrollHeight = dataModel.length * itemHeight;

      var firstIndex = Math.floor(scrollTop / itemHeight);
      var itemsInViewPort = Math.min(dataModel.length - firstIndex,
          Math.ceil((scrollTop + clientHeight - firstIndex * itemHeight) /
                    itemHeight));
      var lastIndex = firstIndex + itemsInViewPort;

      this.textContent = '';

      var oldFirstIndex = this.firstIndex_ || 0;
      var oldLastIndex = this.lastIndex_ || 0;

      this.beforeFiller_.style.height = firstIndex * itemHeight + 'px';
      this.appendChild(this.beforeFiller_);

      var sm = this.selectionModel;
      var leadIndex = sm.leadIndex;
      var listItem;

      for (var y = firstIndex; y < lastIndex; y++) {
        var dataItem = dataModel.item(y);
        listItem = cachedItems[y] || this.createItem(dataItem);
        if (y == leadIndex) {
          listItem.lead = true;
        }
        if (sm.getIndexSelected(y)) {
          listItem.selected = true;
        }
        this.appendChild(listItem);
        newCachedItems[y] = listItem;
      }

      this.afterFiller_.style.height =
          (dataModel.length - firstIndex - itemsInViewPort) * itemHeight + 'px';
      this.appendChild(this.afterFiller_);

      this.firstIndex_ = firstIndex;
      this.lastIndex_ = lastIndex;

      this.cachedItems_ = newCachedItems;

      console.timeEnd('redraw');

      // Measure again in case the item height has change due to a page zoom.
      //
      // The measure above is only done the first time but this measure is done
      // after every redraw. It is done in a timeout so it will not trigger
      // a reflow (which made the redraw speed 3 times slower on my system).
      // By using a timeout the measuring will happen later when there is no
      // need for a reflow.
      if (listItem) {
        var list = this;
        window.setTimeout(function() {
          if (listItem.parentNode == list)
            list.itemHeight_ = measureItem(list, listItem);
        });
      }
    },

    /**
     * Redraws a single item
     * @param {number} index The row index to redraw.
     */
    redrawItem: function(index) {
      if (index >= this.firstIndex_ && index < this.lastIndex_) {
        delete this.cachedItems_[index];
        this.redraw();
      }
    }
  };

  return {
    List: List
  }
});
<|endoftext|>


<commit_before>'use strict';

describe('publish queue', function() {

    var subscribers = {'_items': [
        {
            '_created': '2015-05-11T06:00:48+0000',
            '_etag': '8d27862ca97a8741aaf8ac38f1de3605e085062c',
            '_id': '555045901d41c80c5804501a',
            '_links': {
                'self': {
                    'href': 'subscribers/555045901d41c80c5804501a',
                    'title': 'Subscriber'
                }
            },
            '_updated': '2015-05-12T01:01:47+0000',
            'destinations': [
                {
                    'config': {
                        'recipients': 'test@aap.com.au'
                    },
                    'delivery_type': 'email',
                    'name': 'test'
                }
            ],
            'is_active': true,
            'name': 'test'
        }
    ]};

    var output_channels = {'_items': [
        {
            '_created': '2015-05-20T04:14:15+0000',
            '_etag': '947ac937ed08ef5679eb48ff3ddef1d0d82bb5fc',
            '_id': '555c0a171d41c83fd826b801',
            '_links': {
                'self': {
                    'href': 'output_channels/555c0a171d41c83fd826b801',
                    'title': 'Output_channel'
                }
            },
            '_updated': '2015-05-20T04:14:15+0000',
            'channel_type': 'address',
            'description': 'Digital',
            'destinations': [
                '555045901d41c80c5804501a'
            ],
            'format': 'nitf',
            'is_active': true,
            'is_digital': true,
            'name': 'Digital',
            'sequence_num_settings': {}
        },
        {
            '_created': '2015-05-11T06:01:19+0000',
            '_etag': 'a508d504362ef67d130597b0553a32dfe4836e24',
            '_id': '555045af1d41c80c593e7656',
            '_links': {
                'self': {
                    'href': 'output_channels/555045af1d41c80c593e7656',
                    'title': 'Output_channel'
                }
            },
            '_updated': '2015-05-11T06:01:26+0000',
            'channel_type': 'address',
            'description': 'Email',
            'destinations': [
                '555045901d41c80c5804501a'
            ],
            'format': 'nitf',
            'is_active': true,
            'name': 'Email',
            'sequence_num_settings': {}
        }
    ]};

    var publish_queue = {'_items': [
            {
            '_created': '2015-05-15T06:27:13+0000',
            '_etag': 'd18bf9f762b03815acc189fdcef633bf67f8e222',
            '_id': '555591c11d41c830e91b1f9f',
            '_links': {
                'self': {
                    'href': 'publish_queue/555591c11d41c830e91b1f9f',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T04:18:23+0000',
            'completed_at': '2015-05-19T04:18:23+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555591c11d41c830e91b1f9e',
            'headline': 'RL:Souths win but Dugan stars in NRL',
            'item_id': 'tag:localhost:2015:a613f9b5-c7c4-4980-a3e8-0a90586ba59e',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 11,
            'publishing_action': 'published',
            'selector_codes': [
                'T12',
                'TTT',
                'XXX'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T04:18:23+0000',
            'unique_name': '#55861'
        },
        {
            '_created': '2015-05-19T05:13:29+0000',
            '_etag': '88602d040ee9e67feb25d928d18325d2f8c24830',
            '_id': '555ac6791d41c8105b514cf0',
            '_links': {
                'self': {
                    'href': 'publish_queue/555ac6791d41c8105b514cf0',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T05:13:34+0000',
            'completed_at': '2015-05-19T05:13:34+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555ac6791d41c8105b514cef',
            'headline': 'Angle Park Greyhound NSW TAB Divs 1-8 Monday',
            'item_id': 'tag:localhost:2015:15982dd3-0ab2-4da1-b2a2-b8f322e8a612',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 15,
            'publishing_action': 'corrected',
            'selector_codes': [
                'TTT',
                'XXX',
                'T12'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T05:13:34+0000',
            'unique_name': '#55860'
        },
        {
            '_created': '2015-05-19T08:56:43+0000',
            '_etag': '3cd58463412d08d2d776a931f04f997282025e7a',
            '_id': '555afacb1d41c817984405b5',
            '_links': {
                'self': {
                    'href': 'publish_queue/555afacb1d41c817984405b5',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T08:56:49+0000',
            'completed_at': '2015-05-19T08:56:49+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555afacb1d41c817984405b4',
            'headline': 'NSW:Man quizzed over suspicious Sydney death',
            'item_id': 'tag:localhost:2015:7466da05-56d2-47d4-a401-b79ed2af08a2',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 16,
            'publishing_action': 'published',
            'selector_codes': [
                'XXX',
                'TTT',
                'T12'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T08:56:49+0000',
            'unique_name': '#57537'
        }
    ]};

    var $scope, controller;

    beforeEach(module('superdesk.authoring'));
    beforeEach(module('superdesk.users'));
    beforeEach(module('superdesk.publish'));
    beforeEach(module('superdesk.mocks'));

    beforeEach(inject(function($rootScope, $controller, adminPublishSettingsService, $q, api) {
        spyOn(adminPublishSettingsService, 'fetchSubscribers').and.returnValue($q.when(subscribers));
        spyOn(adminPublishSettingsService, 'fetchOutputChannels').and.returnValue($q.when(output_channels));
        spyOn(api.publish_queue, 'query').and.returnValue($q.when(publish_queue));
        $scope = $rootScope.$new();
        controller = $controller('publishQueueCtrl',
            {
                $scope: $scope,
                'adminPublishSettingsService': adminPublishSettingsService,
                '$q': $q,
                'api': api
            }
        );
    }));

    it('can load items from publish_queue', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        _.each($scope.publish_queue, function(item) {
            expect(item.selected).toBe(false);
        });
        expect($scope.showResendBtn).toBe(false);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can select multiple queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        expect($scope.multiSelectCount).toBe(1);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can deselect queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(1);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[0].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        expect($scope.multiSelectCount).toBe(0);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can deselect all queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        $scope.cancelSelection();
        expect($scope.multiSelectCount).toBe(0);
    }));

    it('can pass values to filter function', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.selectedFilterChannel = null;
        $scope.selectedFilterSubscriber = null;
        $scope.filterSchedule($scope.publish_queue[0], 'channel');
        expect($scope.selectedFilterChannel).toEqual($scope.publish_queue[0]);
        $scope.filterSchedule($scope.publish_queue[0], 'subscriber');
        expect($scope.selectedFilterSubscriber).toEqual($scope.publish_queue[0]);
    }));

    it('can resend single publish queue item', inject(function($rootScope, api, $q) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        spyOn(api.publish_queue, 'save').and.callFake(function () {
            publish_queue._items.push($scope.buildNewSchedule($scope.publish_queue[0]));
            return $q.when();
        });
        $scope.scheduleToSend($scope.publish_queue[0]);
        expect($scope.publish_queue.length).toBe(4);
    }));
});
<commit_msg>feat(publish queue page): Added unit test for filter function<commit_after>'use strict';

describe('publish queue', function() {

    var subscribers = {'_items': [
        {
            '_created': '2015-05-11T06:00:48+0000',
            '_etag': '8d27862ca97a8741aaf8ac38f1de3605e085062c',
            '_id': '555045901d41c80c5804501a',
            '_links': {
                'self': {
                    'href': 'subscribers/555045901d41c80c5804501a',
                    'title': 'Subscriber'
                }
            },
            '_updated': '2015-05-12T01:01:47+0000',
            'destinations': [
                {
                    'config': {
                        'recipients': 'test@aap.com.au'
                    },
                    'delivery_type': 'email',
                    'name': 'test'
                }
            ],
            'is_active': true,
            'name': 'test'
        }
    ]};

    var output_channels = {'_items': [
        {
            '_created': '2015-05-20T04:14:15+0000',
            '_etag': '947ac937ed08ef5679eb48ff3ddef1d0d82bb5fc',
            '_id': '555c0a171d41c83fd826b801',
            '_links': {
                'self': {
                    'href': 'output_channels/555c0a171d41c83fd826b801',
                    'title': 'Output_channel'
                }
            },
            '_updated': '2015-05-20T04:14:15+0000',
            'channel_type': 'address',
            'description': 'Digital',
            'destinations': [
                '555045901d41c80c5804501a'
            ],
            'format': 'nitf',
            'is_active': true,
            'is_digital': true,
            'name': 'Digital',
            'sequence_num_settings': {}
        },
        {
            '_created': '2015-05-11T06:01:19+0000',
            '_etag': 'a508d504362ef67d130597b0553a32dfe4836e24',
            '_id': '555045af1d41c80c593e7656',
            '_links': {
                'self': {
                    'href': 'output_channels/555045af1d41c80c593e7656',
                    'title': 'Output_channel'
                }
            },
            '_updated': '2015-05-11T06:01:26+0000',
            'channel_type': 'address',
            'description': 'Email',
            'destinations': [
                '555045901d41c80c5804501a'
            ],
            'format': 'nitf',
            'is_active': true,
            'name': 'Email',
            'sequence_num_settings': {}
        }
    ]};

    var publish_queue = {'_items': [
            {
            '_created': '2015-05-15T06:27:13+0000',
            '_etag': 'd18bf9f762b03815acc189fdcef633bf67f8e222',
            '_id': '555591c11d41c830e91b1f9f',
            '_links': {
                'self': {
                    'href': 'publish_queue/555591c11d41c830e91b1f9f',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T04:18:23+0000',
            'completed_at': '2015-05-19T04:18:23+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555591c11d41c830e91b1f9e',
            'headline': 'RL:Souths win but Dugan stars in NRL',
            'item_id': 'tag:localhost:2015:a613f9b5-c7c4-4980-a3e8-0a90586ba59e',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 11,
            'publishing_action': 'published',
            'selector_codes': [
                'T12',
                'TTT',
                'XXX'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T04:18:23+0000',
            'unique_name': '#55861'
        },
        {
            '_created': '2015-05-19T05:13:29+0000',
            '_etag': '88602d040ee9e67feb25d928d18325d2f8c24830',
            '_id': '555ac6791d41c8105b514cf0',
            '_links': {
                'self': {
                    'href': 'publish_queue/555ac6791d41c8105b514cf0',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T05:13:34+0000',
            'completed_at': '2015-05-19T05:13:34+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555ac6791d41c8105b514cef',
            'headline': 'Angle Park Greyhound NSW TAB Divs 1-8 Monday',
            'item_id': 'tag:localhost:2015:15982dd3-0ab2-4da1-b2a2-b8f322e8a612',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 15,
            'publishing_action': 'corrected',
            'selector_codes': [
                'TTT',
                'XXX',
                'T12'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T05:13:34+0000',
            'unique_name': '#55860'
        },
        {
            '_created': '2015-05-19T08:56:43+0000',
            '_etag': '3cd58463412d08d2d776a931f04f997282025e7a',
            '_id': '555afacb1d41c817984405b5',
            '_links': {
                'self': {
                    'href': 'publish_queue/555afacb1d41c817984405b5',
                    'title': 'Publish_queue'
                }
            },
            '_updated': '2015-05-19T08:56:49+0000',
            'completed_at': '2015-05-19T08:56:49+0000',
            'content_type': 'text',
            'destination': {
                'config': {
                    'recipients': 'test@aap.com.au'
                },
                'delivery_type': 'email',
                'name': 'test'
            },
            'formatted_item_id': '555afacb1d41c817984405b4',
            'headline': 'NSW:Man quizzed over suspicious Sydney death',
            'item_id': 'tag:localhost:2015:7466da05-56d2-47d4-a401-b79ed2af08a2',
            'output_channel_id': '555045af1d41c80c593e7656',
            'publish_schedule': null,
            'published_seq_num': 16,
            'publishing_action': 'published',
            'selector_codes': [
                'XXX',
                'TTT',
                'T12'
            ],
            'state': 'success',
            'subscriber_id': '555045901d41c80c5804501a',
            'transmit_started_at': '2015-05-19T08:56:49+0000',
            'unique_name': '#57537'
        }
    ]};

    var $scope, controller;

    beforeEach(module('superdesk.authoring'));
    beforeEach(module('superdesk.users'));
    beforeEach(module('superdesk.publish'));
    beforeEach(module('superdesk.mocks'));

    beforeEach(inject(function($rootScope, $controller, adminPublishSettingsService, $q, api) {
        spyOn(adminPublishSettingsService, 'fetchSubscribers').and.returnValue($q.when(subscribers));
        spyOn(adminPublishSettingsService, 'fetchOutputChannels').and.returnValue($q.when(output_channels));
        spyOn(api.publish_queue, 'query').and.returnValue($q.when(publish_queue));
        $scope = $rootScope.$new();
        controller = $controller('publishQueueCtrl',
            {
                $scope: $scope,
                'adminPublishSettingsService': adminPublishSettingsService,
                '$q': $q,
                'api': api
            }
        );
    }));

    it('can load items from publish_queue', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        _.each($scope.publish_queue, function(item) {
            expect(item.selected).toBe(false);
        });
        expect($scope.showResendBtn).toBe(false);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can select multiple queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        expect($scope.multiSelectCount).toBe(1);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can deselect queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(1);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[0].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        expect($scope.multiSelectCount).toBe(0);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
    }));

    it('can deselect all queue items', inject(function($rootScope) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        $scope.publish_queue[0].selected = true;
        $scope.publish_queue[1].selected = true;
        $scope.selectQueuedItem($scope.publish_queue[0]);
        $scope.selectQueuedItem($scope.publish_queue[1]);
        expect($scope.multiSelectCount).toBe(2);
        expect($scope.showResendBtn).toBe(true);
        expect($scope.showCancelBtn).toBe(false);
        $scope.publish_queue[1].selected = false;
        $scope.selectQueuedItem($scope.publish_queue[1]);
        $scope.cancelSelection();
        expect($scope.multiSelectCount).toBe(0);
    }));

    it('sets the selected filter channel', inject(function($rootScope) {
        var channelValue = {foo: 'bar'};
        $scope.selectedFilterChannel = null;
        $scope.filterSchedule(channelValue, 'channel');
        expect($scope.selectedFilterChannel).toEqual(channelValue);
    }));

    it('sets the selected filter subscriber', inject(function($rootScope) {
        var subscriberValue = {foo: 'bar'};
        $scope.selectedFilterSubscriber = null;
        $scope.filterSchedule(subscriberValue, 'subscriber');
        expect($scope.selectedFilterSubscriber).toEqual(subscriberValue);
    }));

    it('can resend single publish queue item', inject(function($rootScope, api, $q) {
        $rootScope.$digest();
        expect($scope.publish_queue.length).toBe(3);
        spyOn(api.publish_queue, 'save').and.callFake(function () {
            publish_queue._items.push($scope.buildNewSchedule($scope.publish_queue[0]));
            return $q.when();
        });
        $scope.scheduleToSend($scope.publish_queue[0]);
        expect($scope.publish_queue.length).toBe(4);
    }));
});
<|endoftext|>


<commit_before>'use strict';

describe('Controller: UserProfileCtrl', function () {

  // // load the controller's module
  beforeEach(module('openstorefrontApp'));

  var UserProfileCtrl,
    scope;

  // Initialize the controller and a mock scope
  beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();
    UserProfileCtrl = $controller('UserProfileCtrl', {
      $scope: scope
    });
  }));

  it('should have the correct initializations', function () {
    expect(scope.total).toEqual({});
    expect(scope.userProfileForm).toEqual({});
    expect(scope._scopename).toEqual('userprofile');  //NOTE this is set to 'userprofile' when scoped!
    expect(scope.pageTitle).toEqual('DI2E Clearinghouse');  //NOTE this is set to 'DI2E Clearinghouse' when scoped!
    expect(scope.defaultTitle).toEqual('Browse Categories');
    expect(scope.untilDate.toString()).toEqual(new Date().toString());
    expect(scope.review).toEqual(null);
    expect(scope.user).toEqual({});
    expect(scope.nav).toEqual({
      'current': null,
      'bars': [
          { 'title': 'User Profile', 'include': 'views/userprofiletab.html' },
          { 'title': 'Watches', 'include': 'views/watchestab.html' },
          { 'title': 'Component Reviews', 'include': 'views/feedbacktab.html' },
          { 'title': 'Submissions', 'include': 'views/submissions.html' }
      ]
    });
  });

  it('should validate functions that return values', function() {
    expect(scope.validateEmail('complete_email@whatever.com')).toBeTruthy();
    expect(scope.validateEmail('incomplete_email')).toBeFalsy();
    expect(scope.validateEmail('wrongEmail@@wrong.com')).toBeFalsy();
    expect(scope.validateEmail('blah@whatevercom')).toBeFalsy();

    expect(scope.getDate(1440186375456)).toEqual('8/21/2015');
    expect(scope.isNewer(1440186375456,1440101702555)).toEqual(true);
    expect(scope.isNewer(1440101702555,1440186375456)).toEqual(false);
  });

  it('should validate properly using spies', function() {
    spyOn(scope, 'sendTestEmail');
    spyOn(scope, 'saveUserProfile');
    spyOn(scope, 'checkTagsList');
    spyOn(scope, 'cancelUserProfile');
    spyOn(scope, 'revertProfileChanges');
    spyOn(scope, 'removeFromWatches');
    spyOn(scope, 'updateWatch');
    spyOn(scope, 'resetWatches');

    scope.sendTestEmail();
    scope.saveUserProfile();
    scope.checkTagsList();
    scope.cancelUserProfile();
    scope.revertProfileChanges();
    scope.removeFromWatches();
    scope.updateWatch();
    scope.resetWatches();

    expect(scope.sendTestEmail).toHaveBeenCalled();
    expect(scope.saveUserProfile).toHaveBeenCalled();
    expect(scope.checkTagsList).toHaveBeenCalled();
    expect(scope.cancelUserProfile).toHaveBeenCalled();
    expect(scope.revertProfileChanges).toHaveBeenCalled();
    expect(scope.removeFromWatches).toHaveBeenCalled();
    expect(scope.updateWatch).toHaveBeenCalled();
    expect(scope.resetWatches).toHaveBeenCalled();
  });

});
<commit_msg>Cleanup<commit_after>'use strict';

describe('Controller: UserProfileCtrl', function () {

  // // load the controller's module
  beforeEach(module('openstorefrontApp'));

  var UserProfileCtrl,
    scope;

  // Initialize the controller and a mock scope
  beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();
    UserProfileCtrl = $controller('UserProfileCtrl', {
      $scope: scope
    });
  }));

  it('should have the correct initializations', function () {
    expect(scope.total).toEqual({});
    expect(scope.userProfileForm).toEqual({});
    expect(scope._scopename).toEqual('userprofile');
    expect(scope.pageTitle).toEqual('DI2E Clearinghouse');
    expect(scope.defaultTitle).toEqual('Browse Categories');
    expect(scope.untilDate.toString()).toEqual(new Date().toString());
    expect(scope.review).toEqual(null);
    expect(scope.user).toEqual({});
    expect(scope.nav).toEqual({
      'current': null,
      'bars': [
          { 'title': 'User Profile', 'include': 'views/userprofiletab.html' },
          { 'title': 'Watches', 'include': 'views/watchestab.html' },
          { 'title': 'Component Reviews', 'include': 'views/feedbacktab.html' },
          { 'title': 'Submissions', 'include': 'views/submissions.html' }
      ]
    });
  });

  it('should validate functions that return values', function() {
    expect(scope.validateEmail('complete_email@whatever.com')).toBeTruthy();
    expect(scope.validateEmail('incomplete_email')).toBeFalsy();
    expect(scope.validateEmail('wrongEmail@@wrong.com')).toBeFalsy();
    expect(scope.validateEmail('blah@whatevercom')).toBeFalsy();

    expect(scope.getDate(1440186375456)).toEqual('8/21/2015');
    expect(scope.isNewer(1440186375456,1440101702555)).toEqual(true);
    expect(scope.isNewer(1440101702555,1440186375456)).toEqual(false);
  });

  it('should validate properly using spies', function() {
    spyOn(scope, 'sendTestEmail');
    spyOn(scope, 'saveUserProfile');
    spyOn(scope, 'checkTagsList');
    spyOn(scope, 'cancelUserProfile');
    spyOn(scope, 'revertProfileChanges');
    spyOn(scope, 'removeFromWatches');
    spyOn(scope, 'updateWatch');
    spyOn(scope, 'resetWatches');

    scope.sendTestEmail();
    scope.saveUserProfile();
    scope.checkTagsList();
    scope.cancelUserProfile();
    scope.revertProfileChanges();
    scope.removeFromWatches();
    scope.updateWatch();
    scope.resetWatches();

    expect(scope.sendTestEmail).toHaveBeenCalled();
    expect(scope.saveUserProfile).toHaveBeenCalled();
    expect(scope.checkTagsList).toHaveBeenCalled();
    expect(scope.cancelUserProfile).toHaveBeenCalled();
    expect(scope.revertProfileChanges).toHaveBeenCalled();
    expect(scope.removeFromWatches).toHaveBeenCalled();
    expect(scope.updateWatch).toHaveBeenCalled();
    expect(scope.resetWatches).toHaveBeenCalled();
  });

});
<|endoftext|>


<commit_before>"use strict";

var _ = require("underscore");
var Article = require("substance-article");
var ImporterError = require("./converter_errors").ImporterError;

var State = function() {
  // an id generator for different types
  var ids = {};
  this.nextId = function(type) {
    ids[type] = ids[type] || 0;
    ids[type]++;
    return type +"_"+ids[type];
  };

  var stack = [];

  this.current = function() {
    return stack[stack.length-1];
  };

  this.push = function(node) {
    stack.push(node);
  };

  this.pop = function() {
    return stack.pop();
  };

  this.annotations = [];
};

var Annotations = {
  "Emph": "emphasis",
  "Strong": "strong",
  "Link": "link",
  "Code": "code"
};

var _isAnnotation = function(item) {
  for(var id in Annotations) {
    if(item[id] !== undefined) {
      return true;
    }
  }
  return false;
};

var _getAnnotationData = function(item) {
  for(var id in Annotations) {
    if(item[id] !== undefined) {
      return {type: Annotations[id], fragments: item[id]};
    }
  }
};

var PandocImporter = function() {
};

PandocImporter.Prototype = function() {

  this.import = function(input) {
    var state = new State();
    return this.document(state, input);
  };

  this.document = function(state, input) {
    var meta = input[0];
    var doc = new Article({"id": meta.doc_id});
    var idx;

    state.doc = doc;

    // all nodes on this level are inserted and shown
    var nodes = input[1];
    for (idx = 0; idx < nodes.length; idx++) {
      var node = this.topLevelNode(state, nodes[idx]);
      if(node) doc.show("content", node.id, idx);
    }

    // we are creating the annotations afterwards
    // to be sure that the annotated nodes are registered already
    for (idx = 0; idx < state.annotations.length; idx++) {
      doc.create(state.annotations[idx]);
    }

    return doc;
  };

  this.topLevelNode = function(state, input) {
    if (input == 'HorizontalRule') {
      return false;
    }
    var type = Object.keys(input)[0];

    switch(type) {
      case "Header":
        return this.header(state, input["Header"]);
      case "Para":
        // TODO: why is that?
        if (!_.isUndefined(input["Para"][0].Image)) {
          return this.figure(state, input["Para"][0].Image);
        }
        else {
          return this.paragraph(state, input["Para"]);
        }
        break;
      case "CodeBlock":
        return this.codeblock(state, input["CodeBlock"]);
      case 'RawBlock':
        return this.rawblock(state, input["RawBlock"]);
      case 'BlockQuote':
        return this.blockquote(state, input["BlockQuote"]);
      case "BulletList":
        return this.list(state, input["BulletList"], false);
      case "OrderedList":
        return this.list(state, input["OrderedList"], true);
      case "Image":
        return this.figure(state, input["Image"]);
      default:
        throw new ImporterError("Node not supported: " + type);
    }

  };

  /*
  Example:
    {
        "Header": [
            1,
            [
                "heading",
                [],
                []
            ],
            [
                {
                    "Str": "Heading"
                }
            ]
        ]
    },
  */
  this.header = function(state, input) {
    var doc = state.doc;

    var level = input[0];
    var id = state.nextId("header");
    var node = {
      id: id,
      type: "heading",
      level: level,
      content: null
    };

    state.push(node);
    node.content = this.text(state, input[2]);
    state.pop();

    return doc.create(node);
  };

  /*
  Example:
  {
      "Para": [
          {
              "Str": "And"
          },
          "Space",
          {
              "Str": "a"
          },
          "Space",
          {
              "Str": "paragraph"
          }
      ]
  }
  */
  this.paragraph = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("paragraph");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };

    state.push(node);
    node.content = this.text(state, input);
    state.pop();

    return doc.create(node);
  };

  this.rawblock = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("paragraph");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };

    state.push(node);
    node.content = input[1];
    state.pop();

    return doc.create(node);
  };

  this.codeblock = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("codeblock");
    var node = {
      id: id,
      "type": "codeblock",
      content: null
    };

    state.push(node);
    node.content = input[1];
    state.pop();

    return doc.create(node);
  };

  this.blockquote = function(state, input) {
    var doc = state.doc;
    for (var idx = 0; idx < input.length; idx++) {
      var itemInput = input[idx];
      var quote;
      if (itemInput["Para"]) {
        quote = this.paragraph(state, itemInput["Para"]);
        doc.show("content", quote.id, -1);
      }
      else if (itemInput["BlockQuote"]) {
        this.blockquote(state, itemInput["BlockQuote"]);
      }
      else {
        throw new ImporterError("Node not supported as blockquote: " + JSON.stringify(quote));
      }
    }

    return false;
  };

  this.figure = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("figure");
    var node = {
      id: id,
      type: "figure",
      image: null,
      caption: null,
    };

    id = state.nextId("image");
    var url = input[1][0];
    var img = {
      id: id,
      type: "image",
      url: url
    };
    doc.create(img);
    node.image = img.id;

    state.push(node);
    if (!_.isEmpty(input[0])){
      node.caption = this.caption(state, input[0]);
    }
    state.pop();

    return doc.create(node);
  };

  this.caption = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("caption");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };
    state.push(node);
    node.content = this.text(state, input);
    state.pop();

    doc.create(node);
    return id;
  };

  this.list = function(state, input, ordered) {
    var doc = state.doc;

    var id = state.nextId("list");
    var node = {
      id: id,
      type: "list",
      items: [],
      ordered: ordered
    };

    state.push(node);
    if (ordered) {
      input = input[1];
    }
    for (var idx = 0; idx < input.length; idx++) {
      var itemInput = input[idx];
      if (itemInput.length !== 1) {
        throw new ImporterError("Oops. Not ready for that. Can only handle one item per list item");
      }
      // TODO: find out why this is provided as array
      itemInput = itemInput[0];

      var listItem;

      if (itemInput["Plain"]) {
        listItem = this.paragraph(state, itemInput["Plain"]);
      }
      else {
        throw new ImporterError("Node not supported as list item: " + JSON.stringify(itemInput));
      }
      node.items.push(listItem.id);
    }
    state.pop();

    return doc.create(node);
  };

  // Retrieves a text block from an array of textish fragments
  // and creates annotations on the fly.
  // --------
  //

  this.text = function(state, textFragments, startPos) {
    var result = [];
    var pos = startPos || 0;

    for (var i = 0; i < textFragments.length; i++) {
      var item = textFragments[i];

      if (item === "Space") {
        result.push(" ");
        pos++;
      }
      else if (item["Str"]) {
        var str = item["Str"];
        result.push(str);
        pos += str.length;
      }
      else if (_isAnnotation(item)) {
        var content = this.annotation(state, item, pos);
        result.push(content);
        pos += content.length;
      }
      else {
        throw new ImporterError("Unsupported fragment for textish: " + item);
      }
    }

    return result.join("");
  };

  // Create an annotation that begins at the given startPos
  // --------
  //

  this.annotation = function(state, input, startPos) {
    var targetNode = state.current();
    if (targetNode === undefined) {
      throw new ImporterError("No target for annotation available");
    }
    var options = {};

    var data = _getAnnotationData(input);

    var type = data.type;

    var fragments, content;

    if(type == 'link') {
      fragments = data.fragments[0];
      options.url = data.fragments[1][0];
      content = this.text(state, fragments, startPos);
    }
    else if(type == 'code') {
      fragments = data.fragments[1];
      content = fragments;
    }
    else {
      fragments = data.fragments;
      content = this.text(state, fragments, startPos);
    }

    var endPos = startPos + content.length;

    var id = state.nextId(type);
    var annotation = _.extend({
      id: id,
      type: type,
      path: [targetNode.id, "content"],
      range: [startPos, endPos]
    },options);

    state.annotations.push(annotation);

    return content;
  };

};

PandocImporter.prototype = new PandocImporter.Prototype();

module.exports = PandocImporter;
<commit_msg>Create images instead of figures.<commit_after>"use strict";

var _ = require("underscore");
var Article = require("substance-article");
var ImporterError = require("./converter_errors").ImporterError;

var State = function() {
  // an id generator for different types
  var ids = {};
  this.nextId = function(type) {
    ids[type] = ids[type] || 0;
    ids[type]++;
    return type +"_"+ids[type];
  };

  var stack = [];

  this.current = function() {
    return stack[stack.length-1];
  };

  this.push = function(node) {
    stack.push(node);
  };

  this.pop = function() {
    return stack.pop();
  };

  this.annotations = [];
};

var Annotations = {
  "Emph": "emphasis",
  "Strong": "strong",
  "Link": "link",
  "Code": "code"
};

var _isAnnotation = function(item) {
  for(var id in Annotations) {
    if(item[id] !== undefined) {
      return true;
    }
  }
  return false;
};

var _getAnnotationData = function(item) {
  for(var id in Annotations) {
    if(item[id] !== undefined) {
      return {type: Annotations[id], fragments: item[id]};
    }
  }
};

var PandocImporter = function() {
};

PandocImporter.Prototype = function() {

  this.import = function(input) {
    var state = new State();
    return this.document(state, input);
  };

  this.document = function(state, input) {
    var meta = input[0];
    var doc = new Article({"id": meta.doc_id});
    var idx;

    state.doc = doc;

    // all nodes on this level are inserted and shown
    var nodes = input[1];
    for (idx = 0; idx < nodes.length; idx++) {
      var node = this.topLevelNode(state, nodes[idx]);
      if(node) doc.show("content", node.id, idx);
    }

    // we are creating the annotations afterwards
    // to be sure that the annotated nodes are registered already
    for (idx = 0; idx < state.annotations.length; idx++) {
      doc.create(state.annotations[idx]);
    }

    return doc;
  };

  this.topLevelNode = function(state, input) {
    if (input == 'HorizontalRule') {
      return false;
    }
    var type = Object.keys(input)[0];

    switch(type) {
      case "Header":
        return this.header(state, input["Header"]);
      case "Para":
        // TODO: why is that?
        if (!_.isUndefined(input["Para"][0].Image)) {
          return this.figure(state, input["Para"][0].Image);
        }
        else {
          return this.paragraph(state, input["Para"]);
        }
        break;
      case "CodeBlock":
        return this.codeblock(state, input["CodeBlock"]);
      case 'RawBlock':
        return this.rawblock(state, input["RawBlock"]);
      case 'BlockQuote':
        return this.blockquote(state, input["BlockQuote"]);
      case "BulletList":
        return this.list(state, input["BulletList"], false);
      case "OrderedList":
        return this.list(state, input["OrderedList"], true);
      case "Image":
        return this.figure(state, input["Image"]);
      default:
        throw new ImporterError("Node not supported: " + type);
    }

  };

  /*
  Example:
    {
        "Header": [
            1,
            [
                "heading",
                [],
                []
            ],
            [
                {
                    "Str": "Heading"
                }
            ]
        ]
    },
  */
  this.header = function(state, input) {
    var doc = state.doc;

    var level = input[0];
    var id = state.nextId("header");
    var node = {
      id: id,
      type: "heading",
      level: level,
      content: null
    };

    state.push(node);
    node.content = this.text(state, input[2]);
    state.pop();

    return doc.create(node);
  };

  /*
  Example:
  {
      "Para": [
          {
              "Str": "And"
          },
          "Space",
          {
              "Str": "a"
          },
          "Space",
          {
              "Str": "paragraph"
          }
      ]
  }
  */
  this.paragraph = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("paragraph");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };

    state.push(node);
    node.content = this.text(state, input);
    state.pop();

    return doc.create(node);
  };

  this.rawblock = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("paragraph");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };

    state.push(node);
    node.content = input[1];
    state.pop();

    return doc.create(node);
  };

  this.codeblock = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("codeblock");
    var node = {
      id: id,
      "type": "codeblock",
      content: null
    };

    state.push(node);
    node.content = input[1];
    state.pop();

    return doc.create(node);
  };

  this.blockquote = function(state, input) {
    var doc = state.doc;
    for (var idx = 0; idx < input.length; idx++) {
      var itemInput = input[idx];
      var quote;
      if (itemInput["Para"]) {
        quote = this.paragraph(state, itemInput["Para"]);
        doc.show("content", quote.id, -1);
      }
      else if (itemInput["BlockQuote"]) {
        this.blockquote(state, itemInput["BlockQuote"]);
      }
      else {
        throw new ImporterError("Node not supported as blockquote: " + JSON.stringify(quote));
      }
    }

    return false;
  };

  this.figure = function(state, input) {
    var doc = state.doc;
    var url = input[1][0];

    var id = state.nextId("image");
    var node = {
      id: id,
      type: "image",
      url: url
    };
    return doc.create(node);
  };

  this.caption = function(state, input) {
    var doc = state.doc;

    var id = state.nextId("caption");
    var node = {
      id: id,
      type: "paragraph",
      content: null
    };
    state.push(node);
    node.content = this.text(state, input);
    state.pop();

    doc.create(node);
    return id;
  };

  this.list = function(state, input, ordered) {
    var doc = state.doc;

    var id = state.nextId("list");
    var node = {
      id: id,
      type: "list",
      items: [],
      ordered: ordered
    };

    state.push(node);
    if (ordered) {
      input = input[1];
    }
    for (var idx = 0; idx < input.length; idx++) {
      var itemInput = input[idx];
      if (itemInput.length !== 1) {
        throw new ImporterError("Oops. Not ready for that. Can only handle one item per list item");
      }
      // TODO: find out why this is provided as array
      itemInput = itemInput[0];

      var listItem;

      if (itemInput["Plain"]) {
        listItem = this.paragraph(state, itemInput["Plain"]);
      }
      else {
        throw new ImporterError("Node not supported as list item: " + JSON.stringify(itemInput));
      }
      node.items.push(listItem.id);
    }
    state.pop();

    return doc.create(node);
  };

  // Retrieves a text block from an array of textish fragments
  // and creates annotations on the fly.
  // --------
  //

  this.text = function(state, textFragments, startPos) {
    var result = [];
    var pos = startPos || 0;

    for (var i = 0; i < textFragments.length; i++) {
      var item = textFragments[i];

      if (item === "Space") {
        result.push(" ");
        pos++;
      }
      else if (item["Str"]) {
        var str = item["Str"];
        result.push(str);
        pos += str.length;
      }
      else if (_isAnnotation(item)) {
        var content = this.annotation(state, item, pos);
        result.push(content);
        pos += content.length;
      }
      else {
        throw new ImporterError("Unsupported fragment for textish: " + item);
      }
    }

    return result.join("");
  };

  // Create an annotation that begins at the given startPos
  // --------
  //

  this.annotation = function(state, input, startPos) {
    var targetNode = state.current();
    if (targetNode === undefined) {
      throw new ImporterError("No target for annotation available");
    }
    var options = {};

    var data = _getAnnotationData(input);

    var type = data.type;

    var fragments, content;

    if(type == 'link') {
      fragments = data.fragments[0];
      options.url = data.fragments[1][0];
      content = this.text(state, fragments, startPos);
    }
    else if(type == 'code') {
      fragments = data.fragments[1];
      content = fragments;
    }
    else {
      fragments = data.fragments;
      content = this.text(state, fragments, startPos);
    }

    var endPos = startPos + content.length;

    var id = state.nextId(type);
    var annotation = _.extend({
      id: id,
      type: type,
      path: [targetNode.id, "content"],
      range: [startPos, endPos]
    },options);

    state.annotations.push(annotation);

    return content;
  };

};

PandocImporter.prototype = new PandocImporter.Prototype();

module.exports = PandocImporter;
<|endoftext|>