const { last } = require('lodash');

const db = require('../database');
const settings = require('../settings');
const { runMigrations } = require('./run_migrations');

// NOTE: Add new migrations that need to traverse entire database, e.g. messages
// store, here. These will only run after attachment migration has completed in
// the background:
const migrations = [
  // {
  //   version: 0,
  //   migrate(transaction, next) {
  //     next();
  //   },
  // },
];

exports.run = async ({ Backbone, database } = {}) => {
  const { canRun } = await exports.getStatus({ database });
  if (!canRun) {
    throw new Error('Cannot run migrations on database without attachment data');
  }

  await runMigrations({ Backbone, database });
};

exports.getStatus = async ({ database } = {}) => {
  const connection = await db.open(database.id, database.version);
  const isAttachmentMigrationComplete =
    await settings.isAttachmentMigrationComplete(connection);
  const hasMigrations = migrations.length > 0;

  const canRun = isAttachmentMigrationComplete && hasMigrations;
  return {
    isAttachmentMigrationComplete,
    hasMigrations,
    canRun,
  };
};

exports.getLatestVersion = () => {
  const lastMigration = last(migrations);
  if (!lastMigration) {
    return null;
  }

  return lastMigration.version;
};

const { last } = require('lodash');

const db = require('../database');
const settings = require('../settings');
const { runMigrations } = require('./run_migrations');

// IMPORTANT: Add new migrations that need to traverse entire database, e.g.
// messages store, below. Whenever we need this, we need to force attachment
// migration on startup:
const migrations = [
  // {
  //   version: 0,
  //   migrate(transaction, next) {
  //     next();
  //   },
  // },
];

exports.run = async ({ Backbone, database } = {}) => {
  const { canRun } = await exports.getStatus({ database });
  if (!canRun) {
    throw new Error('Cannot run migrations on database without attachment data');
  }

  await runMigrations({ Backbone, database });
};

exports.getStatus = async ({ database } = {}) => {
  const connection = await db.open(database.id, database.version);
  const isAttachmentMigrationComplete =
    await settings.isAttachmentMigrationComplete(connection);
  const hasMigrations = migrations.length > 0;

  const canRun = isAttachmentMigrationComplete && hasMigrations;
  return {
    isAttachmentMigrationComplete,
    hasMigrations,
    canRun,
  };
};

exports.getLatestVersion = () => {
  const lastMigration = last(migrations);
  if (!lastMigration) {
    return null;
  }

  return lastMigration.version;
};

Clarify implications of introducing new migrations


jQuery(function ($) {
  function generateChart(el) {
    var url = "daily_reports_chart.json";
    var certname = $(el).attr('data-certname');
    if (typeof certname !== typeof undefined && certname !== false) {
      // truncate /node/certname from URL, to determine path to json
      url = window.location.href.replace(/\/node\/[^/]+$/,'') + "/daily_reports_chart.json?certname=" + certname;
    }
    d3.json(url, function(data) {
      var chart = c3.generate({
        bindto: '#dailyReportsChart',
        data: {
          type: 'bar',
          json: data['result'],
          keys: {
            x: 'day',
            value: ['failed', 'changed', 'unchanged'],
          },
          groups: [
            ['failed', 'changed', 'unchanged']
          ],
          colors: {  // Must match CSS colors
            'failed':'#AA4643',
            'changed':'#4572A7',
            'unchanged':'#89A54E',
          }
        },
        size: {
          height: 160
        },
        axis: {
          x: {
            type: 'category'
          }
        }
      });
    });
  }
  generateChart($("#dailyReportsChart"));
});

jQuery(function ($) {
  let url = "daily_reports_chart.json";
  let certname = $("#dailyReportsChart").data("certname");
  let days = parseInt($("#dailyReportsChart").data("days"));
  let defaultJSON = []

  for (let index = days-1; index >= 0; index--) {
    defaultJSON.push({
      day: moment().startOf('day').subtract(index, 'days').format('YYYY-MM-DD'),
      unchanged: 0,
      changed: 0,
      failed: 0
    })
  }

  let chart = bb.generate({
    bindto: "#dailyReportsChart",
    data: {
      type: "bar",
      json: defaultJSON,
      keys: {
        x: "day",
        value: ["failed", "changed", "unchanged"],
      },
      groups: [["failed", "changed", "unchanged"]],
      colors: {
        // Must match CSS colors
        failed: "#AA4643",
        changed: "#4572A7",
        unchanged: "#89A54E",
      },
    },
    size: {
      height: 160,
    },
    axis: {
      x: {
        type: "category",
      },
    },
  });

  if (typeof certname !== typeof undefined && certname !== false) {
    // truncate /node/certname from URL, to determine path to json
    url =
      window.location.href.replace(/\/node\/[^/]+$/, "") +
      "/daily_reports_chart.json?certname=" +
      certname
  }

  $.getJSON(url, function(data) {
    chart.load({json: data.result})
  });
})

Fix replacement c3 by billboard


steal( // TODO fixtures work with all libraries "./fixture/fixture_test.js",
	"./object/object_test.js",
	"./string/string_test.js")

steal("./fixture/fixture_test.js",
	"./object/object_test.js",
	"./string/string_test.js")

Add fixtures to main tests


'use strict';

// convert an experiment, an array of spectra, to a chart

var types=require('./types.js');

module.exports=function (experiments, channels, index) {
    var channels = channels || 'RGBWT'

    if (! Array.isArray(experiments)) experiments=[experiments];

    var chart = {
        type: "chart",
        value: {
            title: "Open Spectrophotometer results",
            "axis": [
                {
                    "label": "nM"
                },
                {
                    "label": "Y axis"
                }
            ],
            "data": []
        }
    }
    var counter=0;
    for (var i = 0; i < experiments.length; i++) {
        if ((index === undefined) || (index === i)) {
            var experiment=experiments[i];
            for (var key in experiment) {
                if (channels.indexOf(key)>-1) {
                    var data=experiment[key];
                    chart.value.data.push({
                        "x":data.x,
                        "y":data.y,
                        "label":(++counter)+". "+types[key].label+(data.name ? ': '+data.name : ''),
                        xAxis: 0,
                        yAxis: 1,
                        lineWidth: 2,
                        color: 'red'
                    });
                }
            }
        }
    }

    return chart;
}

'use strict';

// convert an experiment, an array of spectra, to a chart

var types=require('./types.js');

module.exports=function (experiments, channels, index) {
    var channels = channels || 'RGBWT'

    if (! Array.isArray(experiments)) experiments=[experiments];

    var chart = {
        type: "chart",
        value: {
            title: "Open Spectrophotometer results",
            "axis": [
                {
                    "label": "nM"
                },
                {
                    "label": "Y axis"
                }
            ],
            "data": []
        }
    }
    var counter=0;
    for (var i = 0; i < experiments.length; i++) {
        if ((index === undefined) || (index === i)) {
            var experiment=experiments[i];
            for (var key in experiment) {
                if (channels.indexOf(key)>-1) {
                    var data=experiment[key];
                    chart.value.data.push({
                        "x":data.x,
                        "y":data.y,
                        "label":(++counter)+". "+types[key].label+(data.name ? ': '+data.name : '')+
                        (data.concentration ? ' ('+data.name+")" : '')
                        ,
                        xAxis: 0,
                        yAxis: 1,
                        lineWidth: 2,
                        color: 'red'
                    });
                }
            }
        }
    }

    return chart;
}

Add concentration in the name


import React, { Component, PropTypes } from 'react'
import Tile from './Tile'
import { sortBy } from 'lodash'

const styles = {
    row: {
        position: 'relative',
        flexDirection: 'row'
    }
}

export default class Board extends Component {

    static propTypes = {
        tiles: PropTypes.array.isRequired,
        playTile: PropTypes.func.isRequired
    }

    render() {
        const { tiles } = this.props;
        return (
            <div style={styles.row}>
            {
                sortBy(tiles, 'index').map((tile) => {
                    return (
                        <Tile
                            type={tile.type}
                            position={tiles.indexOf(tile)}
                            key={tile.index}
                            index={tile.index}
                            isPlayable={tile.isPlayable}
                            onClickCb={this.props.playTile}
                        />
                    )
                })
            }
            </div>
        );
    }
}

import React, { Component, PropTypes } from 'react'
import Tile from './Tile'

const styles = {
    row: {
        position: 'relative',
        flexDirection: 'row'
    }
}

export default class Board extends Component {

    static propTypes = {
        tiles: PropTypes.array.isRequired,
        playTile: PropTypes.func.isRequired
    }

    render() {
        const { tiles } = this.props;
        return (
            <div style={styles.row}>
            {
                tiles.map((tile, idx) => {
                    return (
                        <Tile
                            type={tile.type}
                            position={tile.position}
                            key={idx}
                            index={idx}
                            isPlayable={tile.isPlayable}
                            onClickCb={this.props.playTile}
                        />
                    )
                })
            }
            </div>
        );
    }
}

Use array index instead of .index


'use strict';

var bodyParser = require('body-parser'),
    jsonParser = bodyParser.json(),
    events = require('events');

module.exports = function(app, config) {

    var emitter = new events.EventEmitter();
    var logger = config.logger;

    app.use(jsonParser);

    app.post('/', function(req, res, next) {

        if (req.body.type === 'pokemon') {
            emitter.emit('pokemon', req.body.message);
        }

        next();
    });

    app.listen(config.port, function() {
        logger.info('Express listening on port ' + config.port);
    });

    return emitter;
};

'use strict';

var bodyParser = require('body-parser'),
    jsonParser = bodyParser.json(),
    events = require('events');

module.exports = function(app, config) {

    var emitter = new events.EventEmitter();
    var logger = config.logger;

    app.use(jsonParser);

    app.post('/', function(req, res) {

        if (req.body.type === 'pokemon') {
            emitter.emit('pokemon', req.body.message);
        }

        res.status(200).send();
    });

    app.listen(config.port, function() {
        logger.info('Express listening on port ' + config.port);
    });

    return emitter;
};

Send HTTP 200 to webhook


import mysql from './mysql';
import postgresql from './postgresql';
import sqlserver from './sqlserver';

/**
 * List of supported database clients
 */
export const CLIENTS = [
  { key: 'mysql', name: 'MySQL' },
  { key: 'postgresql', name: 'PostgreSQL', defaultDatabase: 'postgres' },
  { key: 'sqlserver', name: 'Microsoft SQL Server' },
];

export default {
  mysql,
  postgresql,
  sqlserver,
};

import mysql from './mysql';
import postgresql from './postgresql';
import sqlserver from './sqlserver';

/**
 * List of supported database clients
 */
export const CLIENTS = [
  {
    key: 'mysql',
    name: 'MySQL',
    defaultPort: 3306,
  },
  {
    key: 'postgresql',
    name: 'PostgreSQL',
    defaultDatabase: 'postgres',
    defaultPort: 5432,
  },
  {
    key: 'sqlserver',
    name: 'Microsoft SQL Server',
    defaultPort: 1433,
  },
];

export default {
  mysql,
  postgresql,
  sqlserver,
};

Add database default port to consume in client


const exportAllFunctions = (obj) => {
    Object.keys(obj).forEach((key) => {
        window[key] = obj[key];
    });
};

window.$ = window.jQuery = require('jquery');

require('babel-polyfill');
require('promise-polyfill');

// created for handling global onclick
exportAllFunctions(require('./binary/common_functions/attach_dom/handle_click'));
// used by gtm to update page after a new release
exportAllFunctions(require('./binary/common_functions/check_new_release'));

require('event-source-polyfill');
require('./lib/jquery.sparkline.js');
require('./lib/plugins');
require('jquery.scrollto');

require('./binary/components/trackjs_onerror');

const BinaryLoader = require('./binary/base/binary_loader');

$(window).on('load', BinaryLoader.init);
$(window).on('pageshow', (e) => { // Safari doesn't fire load event when using back button
    if (e.originalEvent.persisted) {
        BinaryLoader.init();
    }
});

Fix safari not triggering load event when using back button


// Add your blacklisted sites to this array. Subdomain and TLD are optional and flexible.
var BLACKLIST = [
  'agolo.com',
  'youtube',
  'twitter.com',
  'mail.google',
  'imgur.com',
  'bit.ly',
  'tinyurl',
  'vine'
];

/** Convert the array to an object and export it **/

var empty = {};

// Adapted from: https://egghead.io/lessons/javascript-introducing-reduce-reducing-an-array-into-an-object
var reducer = function(aggregate, element) {
  aggregate[element] = 1;
  return aggregate;
}

module.exports = BLACKLIST.reduce(reducer, empty);

// Add your blacklisted sites to this array. Subdomain and TLD are optional and flexible.
// Should probably be kept in sync with https://github.com/premgane/agolo-twitterbot/blob/master/server.py
var BLACKLIST = [
  'agolo.com',
  'youtube',
  'twitter.com',
  'mail.google',
  'imgur.com',
  'bit.ly',
  'tinyurl',
  'vine'
];

/** Convert the array to an object and export it **/

var empty = {};

// Adapted from: https://egghead.io/lessons/javascript-introducing-reduce-reducing-an-array-into-an-object
var reducer = function(aggregate, element) {
  aggregate[element] = 1;
  return aggregate;
}

module.exports = BLACKLIST.reduce(reducer, empty);

Add comment with a link to agolo-twitterbot


// Video.js
// The set of videos registered in our app.

module.exports = {
	autoPK: false,
	attributes: {
		id :{
			autoIncrement: true,
			primaryKey: true,
			columnName: 'video_id'
		},

		title: {
			type: 'string',
			required: true
		},

	    description: {
	    	type: 'string'
	    },

	    tags: {
	    	collection: 'tag',
	    	via: 'videoWithTags'
	    },

	    views: {
	    	type: 'integer',
	    	defaultsTo: 0
	    },

	    // 0 for self only, 1 for public
	    privacy: {
	    	type: 'integer',
	    	defaultsTo: 1
	    },

	    duration: {
	    	type: 'string'
	    },

	    shares: {
	    	type: 'integer',
	    	defaultsTo: 0
	    },

	    videoDir: {
	    	type: 'string'
	    },

	    mpdDir: {
	    	type: 'array'
	    },

	    thumbnailDir: {
	    	type: 'string'
	    },

	    hasProcessed: {
	    	type: 'string',
	    	enum: ['false', 'true'],
	    	defaultsTo: 'false'
	    },

	    createdAt: {
	    	type: 'datetime',
	    	defaultsTo: function() {return new Date(); }
	    },

	    updatedAt: {
	    	type: 'datetime',
	    	defaultsTo: function() {return new Date(); }
	    }
		embedURL: {
			type: 'string'
		},

	}
}

// Video.js
// The set of videos registered in our app.

module.exports = {
	autoPK: false,
	attributes: {
		id :{
			autoIncrement: true,
			primaryKey: true,
			columnName: 'video_id'
		},

		title: {
			type: 'string',
			required: true
		},

	    description: {
	    	type: 'string'
	    },

	    tags: {
	    	collection: 'tag',
	    	via: 'videoWithTags'
	    },

	    views: {
	    	type: 'integer',
	    	defaultsTo: 0
	    },

	    // 0 for self only, 1 for public
	    privacy: {
	    	type: 'integer',
	    	defaultsTo: 1
	    },

	    duration: {
	    	type: 'string'
	    },

	    shares: {
	    	type: 'integer',
	    	defaultsTo: 0
	    },

	    videoDir: {
	    	type: 'string'
	    },

	    mpdDir: {
	    	type: 'array'
	    },

	    thumbnailDir: {
	    	type: 'string'
	    },

	    hasProcessed: {
	    	type: 'string',
	    	enum: ['false', 'true'],
	    	defaultsTo: 'false'
	    },

	    createdAt: {
	    	type: 'datetime',
	    	defaultsTo: function() {return new Date(); }
	    },

	    updatedAt: {
	    	type: 'datetime',
	    	defaultsTo: function() {return new Date(); }
	    },
		embedURL: {
			type: 'string'
		},

	}
}

Fix error comma not added


/*globals QUnit spade */

require('jquery');
var qunit = require('./qunit');

var packageName = location.search.match(/package=([^&]+)&?/);
packageName = packageName && packageName[1];

var prefix = location.search.match(/prefix=([^&]+)&?/);
prefix = prefix && prefix[1];

if (!packageName) {
  $('#qunit-header').text('Pass package=foo on URL to test package');
} else {
  require(packageName);
  $.extend(window, qunit);

  QUnit.config.autostart = false;
  QUnit.onload();

  QUnit.jsDump.setParser('object', function(obj) {
    return obj.toString();
  });

  var runtime = spade["package"](packageName);
  var files = runtime.files, file;

  for(var i=0, l=files.length; i<l; i++) {
    file = files[i];

    if (file.match(/tests\/.*\.js$/)) {
      if (!prefix || file.indexOf('tests/'+prefix)===0) {
        require(packageName+"/~" + file);
      }
    }
  }

}

/*globals QUnit spade */

require('jquery');
var qunit = require('./qunit');

var packageName = location.search.match(/package=([^&]+)&?/);
packageName = packageName && packageName[1];

var prefix = location.search.match(/prefix=([^&]+)&?/);
prefix = prefix && prefix[1];

if (!packageName) {
  $('#qunit-header').text('Pass package=foo on URL to test package');
} else {
  require(packageName);
  $.extend(window, qunit);

  QUnit.config.autostart = false;
  QUnit.onload();
  $('h1 > a').text(packageName);

  QUnit.jsDump.setParser('object', function(obj) {
    return obj.toString();
  });

  var runtime = spade["package"](packageName);
  var files = runtime.files, file;

  for(var i=0, l=files.length; i<l; i++) {
    file = files[i];

    if (file.match(/tests\/.*\.js$/)) {
      if (!prefix || file.indexOf('tests/'+prefix)===0) {
        require(packageName+"/~" + file);
      }
    }
  }

}

Put package name in QUnit header


var webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    path: './build',
    publicPath: '/',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.vue$/,
        loader: 'vue'
      },
      {
        test: /src\/.+\.js$/,
        loader: 'babel?presets[]=es2015'
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file?name=[name].[ext]?[hash]'
      }
    ]
  }
}

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins = [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
} else {
  module.exports.devtool = '#source-map'
}

var webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    path: './build',
    publicPath: '/',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.vue$/,
        loader: 'vue'
      },
      {
        test: /src\/.+\.js$/,
        loader: 'babel?presets[]=es2015'
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file?name=[name].[ext]?[hash]'
      }
    ]
  }
}

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins = [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
} else {
  module.exports.devtool = '#source-map'
  module.exports.devServer.host = '0.0.0.0'
}

Allow dev server to be accessed from the network.


const DMP = require('diff-match-patch')
const dmp = new DMP()

module.exports = {
  ADDED: 1,
  REMOVED: -1,
  UNCHANGED: 0,

  diffAsShareJsOp(before, after, callback) {
    const diffs = dmp.diff_main(before.join('\n'), after.join('\n'))
    dmp.diff_cleanupSemantic(diffs)

    const ops = []
    let position = 0
    for (const diff of diffs) {
      const type = diff[0]
      const content = diff[1]
      if (type === this.ADDED) {
        ops.push({
          i: content,
          p: position
        })
        position += content.length
      } else if (type === this.REMOVED) {
        ops.push({
          d: content,
          p: position
        })
      } else if (type === this.UNCHANGED) {
        position += content.length
      } else {
        throw new Error('Unknown type')
      }
    }
    callback(null, ops)
  }
}

const DMP = require('diff-match-patch')
const dmp = new DMP()

// Do not attempt to produce a diff for more than 100ms
dmp.Diff_Timeout = 0.1

module.exports = {
  ADDED: 1,
  REMOVED: -1,
  UNCHANGED: 0,

  diffAsShareJsOp(before, after, callback) {
    const diffs = dmp.diff_main(before.join('\n'), after.join('\n'))
    dmp.diff_cleanupSemantic(diffs)

    const ops = []
    let position = 0
    for (const diff of diffs) {
      const type = diff[0]
      const content = diff[1]
      if (type === this.ADDED) {
        ops.push({
          i: content,
          p: position
        })
        position += content.length
      } else if (type === this.REMOVED) {
        ops.push({
          d: content,
          p: position
        })
      } else if (type === this.UNCHANGED) {
        position += content.length
      } else {
        throw new Error('Unknown type')
      }
    }
    callback(null, ops)
  }
}

Set the diff-match-patch timeout to 100ms


'use strict';

const Container = require('./container');

const creationExpression = (parent, options) =>
  new Container(parent, options);

const rootFunction = (options) =>
  new Container(null, options);

const singleton = (generator) => {
  let instance = null;
  return ($container) => {
    if (!instance) {
      instance = $container.build(generator);
    }
    return instance;
  };
};
/**
 * Create a container (Compatability shim)
 * @param {Container} parent      - Parent container, optional.
 * @param {object}    options     - Container options
 */
rootFunction.createContainer = creationExpression;

module.exports = rootFunction;

'use strict';

const Container = require('./container');

const creationExpression = (parent, options) =>
  new Container(parent, options);

const rootFunction = (options) =>
  new Container(null, options);

/**
 * Create a container (Compatability shim)
 * @param {Container} parent      - Parent container, optional.
 * @param {object}    options     - Container options
 */
rootFunction.createContainer = creationExpression;

module.exports = rootFunction;

Remove partial work on singleton.


document.addEventListener('DOMContentLoaded', () => {
  window.setupGol();
  window.navUpdate(true);
  window.setupLaptops();
  // Initialize the top laptop
  window.headerLaptop = new window.Laptop(document.getElementById('header-wrapper').getElementsByClassName('laptop3d')[0], -1);

  window.onscroll();

  setTimeout(window.browserCheck, 1000);

  // Configure scrolling when down indicator clicked
  document.getElementsByClassName('down-indicator')[0].addEventListener('click', () => {
    window.scrollTo({ top: window.innerHeight / 2, behavior: 'smooth' });
  });
});

document.addEventListener('DOMContentLoaded', () => {
  setTimeout(window.browserCheck, 1000);

  window.setupGol();
  window.navUpdate(true);
  window.setupLaptops();
  // Initialize the top laptop
  window.headerLaptop = new window.Laptop(document.getElementById('header-wrapper').getElementsByClassName('laptop3d')[0], -1);

  window.onscroll();

  // Configure scrolling when down indicator clicked
  document.getElementsByClassName('down-indicator')[0].addEventListener('click', () => {
    window.scrollTo({ top: window.innerHeight / 2, behavior: 'smooth' });
  });
});

Move browser checking before any other scripts


var storeHelper = require('../../../../server/components/store/helper.js');

describe('validationComponent', function () {

    describe('checkExsits', function () {

        it('should return File doesn\'t exist. if the file can\'t be found', function (done) {

            storeHelper.checkExsits('/home/Abcd123')
                .catch(function(result) {
                    expect(result.message).toBe('File doesn\'t exist.');
                })
                .finally(done);
        });
    });
});

var storeHelper = require('../../../../server/components/store/helper.js');

describe('validationComponent', function () {

    describe('checkExsits', function () {

        it('should return a message (File doesn\'t exist.) if the file can\'t be found', function (done) {
            storeHelper.checkExsits('/home/Abcd123')
                .catch(function(result) {
                    expect(result.message).toBe('File doesn\'t exist.');
                })
                .finally(done);
        });

        it('should return (Path name must be a string) if argument is not a string', function (done) {
            storeHelper.checkExsits(12412313)
                .catch(function(result) {
                    expect(result).toBe('Path name must be a string');
                })
                .finally(done);

            storeHelper.checkExsits(undefined)
                .catch(function(result) {
                    expect(result).toBe('Path name must be a string');
                })
                .finally(done);

            storeHelper.checkExsits(null)
                .catch(function(result) {
                    expect(result).toBe('Path name must be a string');
                })
                .finally(done);
        });
    });

    describe('readFile', function () {

    });
});

Add some more tests to helper spec


import React, { Fragment } from 'react';
import { useAuth } from '../../util/auth';
import Navbar from './../../components/Navbar';
import SignInSection from './../../components/SignInSection';
import BalanceReportSection from '../../components/BalanceReportSection';
import './styles.scss';

function HomePage() {
  const { user } = useAuth();

  return (
    <Fragment>
      <Navbar color="white" spaced={true} />
      {!user ? (
        <SignInSection color="white" title="Welcome" />
      ) : (
        <BalanceReportSection color="white" size="large" />
      )}
    </Fragment>
  );
}

export default HomePage;

import React, { Suspense, Fragment, lazy } from 'react';
import { useAuth } from '../../util/auth';
import Navbar from './../../components/Navbar';
import SignInSection from './../../components/SignInSection';
import './styles.scss';

const BalanceReportSection = lazy(() =>
  import('../../components/BalanceReportSection'),
);

function HomePage() {
  const { user } = useAuth();

  return (
    <Fragment>
      <Navbar color="white" spaced={true} />
      {!user ? (
        <SignInSection color="white" title="Welcome" />
      ) : (
        <Suspense fallback={
loading...
}>
          <BalanceReportSection color="white" size="large" />
        </Suspense>
      )}
    </Fragment>
  );
}

export default HomePage;

Add lazy loading of report section


import parallel from 'async/parallel';
import defaults from 'lodash-es/defaults';
import handleEtag from '../helper/etag';
import keyFactory from '../helper/key';

export default function setList(cache, options = {}) {
  options = defaults({}, options, {
    etag: true,
    list: null,
    total: ['where']
  });

  const write = Boolean(cache.channel());

  return (request, response, next) => {
    const listKey = keyFactory(request, options.list);
    const totalKey = keyFactory(request, options.total);
    const data = request.data();

    const tasks = {
      list: (callback) => cache.set(listKey, data.list, callback),
      total: (callback) => callback(null, null)
    };

    if (response.header('x-total') === null) {
      tasks.total = (callback) => {
        cache.set(totalKey, data.total, callback);
      };
    }

    parallel(tasks, (error, result) => {
      if (error instanceof Error === true) {
        next(error);
        return;
      }

      if (result.total !== null) {
        response.header('x-total', result.total);
      }

      const etag =
        options.etag === true &&
        handleEtag(request, response, result.list, write) === true;

      if (etag === true) {
        return;
      }

      if (write === true) {
        response.write(result.list);
      } else {
        response.end(result.list);
      }
    });
  };
}

import parallel from 'async/parallel';
import defaults from 'lodash-es/defaults';
import handleEtag from '../helper/etag';
import keyFactory from '../helper/key';

export default function setList(cache, options = {}) {
  options = defaults({}, options, {
    etag: true,
    list: null,
    total: ['where']
  });

  const write = Boolean(cache.channel());

  return (request, response, next) => {
    const listKey = keyFactory(request, options.list);
    const totalKey = keyFactory(request, options.total);
    const data = request.data();

    const tasks = {
      list: (callback) => cache.set(listKey, data.list, callback),
      total: (callback) => callback(null, null)
    };

    if (response.header('x-total') === null && data.total !== null) {
      tasks.total = (callback) => {
        cache.set(totalKey, data.total, callback);
      };
    }

    parallel(tasks, (error, result) => {
      if (error instanceof Error === true) {
        next(error);
        return;
      }

      if (result.total !== null) {
        response.header('x-total', result.total);
      }

      const etag =
        options.etag === true &&
        handleEtag(request, response, result.list, write) === true;

      if (etag === true) {
        return;
      }

      if (write === true) {
        response.write(result.list);
      } else {
        response.end(result.list);
      }
    });
  };
}

Check if total is set


import Model from 'components/Model'

import RS_ICON from 'resources/rs_small.png'
import AFG_ICON from 'resources/afg_small.png'

export default class Position extends Model {
	static resourceName = 'Position'
	static listName = 'positionList'

	static schema = {
		name: '',
		type: '',
		code: '',
		status: 'ACTIVE',
		associatedPositions: [],
		organization: {},
		person: {},
		location: {},
	}

	toString() {
		return this.code || this.name
	}

	iconUrl() {
		if (this.type === 'ADVISOR') {
			return RS_ICON
		} else if (this.type === 'PRINCIPAL') {
			return AFG_ICON
		}

		return ''
	}
}

import Model from 'components/Model'

import RS_ICON from 'resources/rs_small.png'
import AFG_ICON from 'resources/afg_small.png'

export default class Position extends Model {
	static resourceName = 'Position'
	static listName = 'positionList'

	static schema = {
		name: '',
		type: '',
		code: '',
		status: 'ACTIVE',
		associatedPositions: [],
		organization: {},
		person: {},
		location: {},
	}

	toString() {
		return this.code || this.name
	}

	iconUrl() {
		if (this.type === 'PRINCIPAL') {
			return AFG_ICON
		}

		return RS_ICON
	}
}

Fix RS icon for non-Advisor NATO members


import {merge, pick} from 'lodash'

const kStorageKey = 'wukong'

export function open(state = {}) {
  try {
    return merge({
      user: {
        preferences: {
          listenOnly: false,
          connection: 0,
          audioQuality: 0,
          theme: 0
        }
      }
    }, JSON.parse(localStorage.getItem(kStorageKey)), state)
  } catch (error) {
    return state
  }
}

export function save(state = {}) {
  try {
    localStorage.setItem(kStorageKey, JSON.stringify(pick(state, [
      'user.preferences',
      'song.playlist',
      'player.volume'
    ])))
  } catch (error) {
    localStorage.removeItem(kStorageKey)
  }
}

import {merge, pick} from 'lodash'

const kStorageKey = 'wukong'

export function open(state = {}) {
  try {
    return merge({
      user: {
        preferences: {
          listenOnly: false,
          connection: 0,
          audioQuality: 2,
          theme: 0
        }
      }
    }, JSON.parse(localStorage.getItem(kStorageKey)), state)
  } catch (error) {
    return state
  }
}

export function save(state = {}) {
  try {
    localStorage.setItem(kStorageKey, JSON.stringify(pick(state, [
      'user.preferences',
      'song.playlist',
      'player.volume'
    ])))
  } catch (error) {
    localStorage.removeItem(kStorageKey)
  }
}

Set audio quality default value


"use strict";

module.exports = function(imports) {

    let express = imports.modules.express;
    let Promise = imports.modules.Promise;
    let fs = require("fs"); // TODO: put this into initImports
    let util = imports.util;
    let handler = util.handler;

    let router = express.Router();

    let pages = {
        signup: "Signup",
        login: "Login",
    };

    for (let page in pages) {

        router.get("/" + page, handler(function*(req, res) {

            res.render("../../morteam-web/src/page.html.ejs", {
                page: pages[page]
            });

        }));

    }

    router.get("/js/:page", handler(function*(req, res) {
        let page = req.params.page;
        let file = "../morteam-web/build/" + page + ".js";
        // TODO: use the package fs-promise
        fs.exists(file, function(exists) {
            if (!exists) {
                return res.end("fail"); // AHHHH
            }
            fs.createReadStream(file).pipe(res);
        });
    }));

    return router;

};

"use strict";

module.exports = function(imports) {

    let express = imports.modules.express;
    let Promise = imports.modules.Promise;
    let fs = require("fs"); // TODO: put this into initImports
    let util = imports.util;
    let handler = util.handler;

    let Team = imports.models.Team;

    let router = express.Router();

    let pages = {
        signup: "Signup",
        login: "Login",
    };

    for (let page in pages) {

        router.get("/" + page, handler(function*(req, res) {

            if (req.user) {
                req.user.team = yield Team.findOne(req.user.team);
            }

            res.render("../../morteam-web/src/page.html.ejs", {
                userInfo: req.user,
                page: pages[page],
            });

        }));

    }

    router.get("/js/:page", handler(function*(req, res) {
        let page = req.params.page;
        let file = "../morteam-web/build/" + page + ".js";
        // TODO: use the package fs-promise
        fs.exists(file, function(exists) {
            if (!exists) {
                return res.end("fail"); // AHHHH
            }
            fs.createReadStream(file).pipe(res);
        });
    }));

    return router;

};

Send user info to client on page load


import { Meteor } from 'meteor/meteor';

const Reports = { columns: {}, rows: {} };

Reports.collection = new Meteor.Collection('Reports');

Reports.add = () => {
  return Reports.collection.insert({ columns: [], rows: [] });
};

Reports.addToTable = (reportId, type, id) => {
  if (type === 'measure') {
    Reports.rows.add(reportId, id);
  } else if (type === 'element') {
    Reports.columns.add(reportId, id);
  }
};

Reports.columns.add = (reportId, elementId) => {
  Reports.collection.update(
    reportId,
    { $push: { columns: { elementId } } }
  );
};

Reports.rows.add = (reportId, measureId) => {
  Reports.collection.update(
    reportId,
    { $push: { rows: { measureId } } }
  );
};

export default Reports;

import { Meteor } from 'meteor/meteor';

const Reports = { columns: {}, rows: {} };

Reports.collection = new Meteor.Collection('Reports');

Reports.add = () => {
  return Reports.collection.insert({ columns: [], rows: [] });
};

Reports.addToTable = (reportId, type, id) => {
  if (type === 'measure') {
    Reports.rows.add(reportId, id);
  } else if (type === 'element') {
    Reports.columns.add(reportId, id);
  }
};

Reports.columns.add = (reportId, elementId) => {
  Reports.collection.update(
    reportId,
    { $addToSet: { columns: { elementId } } }
  );
};

Reports.rows.add = (reportId, measureId) => {
  Reports.collection.update(
    reportId,
    { $addToSet: { rows: { measureId } } }
  );
};

export default Reports;

Replace with for no duplicates


import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';

let getUserLanguage = function() {
	// Put here the logic for determining the user language
	return 'fr';
};

if (Meteor.isClient) {
	Meteor.startup(function() {
		TAPi18n.setLanguage(getUserLanguage()).fail(function(error_message) {
			return throwError(error_message);
		});
	});
}

import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/tap:i18n';
import { Bert } from 'meteor/themeteorchef:bert';

let getUserLanguage = () => {
	// Put here the logic for determining the user language
	return 'fr';
};

if (Meteor.isClient) {
	Meteor.startup(() => {
		TAPi18n.setLanguage(getUserLanguage()).fail((error_message) => {
			return Bert.alert(error_message, 'danger', 'growl-top-right');
		});
	});
}

Put code into ES6 mode


'use strict';

var path = process.cwd();

module.exports = function (app) {

	app.route('/')
		.get(function (req, res) {
			res.sendFile(path + '/public/index.html');
		});

	app.route('/:time')
		.get(function(req, res) {
			console.log("Reached the regexp route for", req.url);
			let timeStr = req.params.time;
			console.log(timeStr);
			let timestamp;
			let result = {};

			console.log("Time string:", timeStr);
			// If the time string contains non-numeric characters, attempt to parse it as as time string.
			if (timeStr.match(/\D/)) {
				timestamp = Date.parse(timeStr);
			} else {
				// Question: Do we need to round to the start of the day?
				timestamp = Number(timeStr * 1000);
			}

			if (!isNaN(timestamp)) {
				let theDate = new Date(timestamp);
				result = {
					unix: Math.floor(timestamp / 1000),
					natural: theDate.toDateString()
				}
			}
			res.json(result);
		});
};

'use strict';

var path = process.cwd();

module.exports = function (app) {

	app.route('/')
		.get(function (req, res) {
			res.sendFile(path + '/public/index.html');
		});

	app.route('/:time')
		.get(function(req, res) {
			console.log("Reached the regexp route for", req.url);
			let timeStr = req.params.time;
			console.log(timeStr);
			let timestamp;
			let result = {};

			console.log("Time string:", timeStr);
			// If the time string contains non-numeric characters, attempt to parse it as as time string.
			if (timeStr.match(/\D/)) {
				timestamp = Date.parse(timeStr);
			} else {
				// Question: Do we need to round to the start of the day?
				timestamp = Number(timeStr * 1000);
			}

			if (!isNaN(timestamp)) {
				let theDate = new Date(timestamp);
				result = {
					unix: Math.floor(timestamp / 1000),
					natural: formatDate(theDate)
				}
			}
			res.json(result);
		});
};

// From a given date object, return a string of the form Jan 23, 2013.
function formatDate(aDate) {
	let splitDate = aDate.toDateString().split(' ');
	splitDate[2] = splitDate[2] + ',';
	return splitDate.slice(1).join(' ');
}

Add a function for formatting a date


'use stricts';

var path = process.cwd();
var Controller = require(path + '/app/controller/parserController.js');
var parserController = new Controller();

module.exports = function(app) {

	app.route('/')
		.get(function(req, res){
			res.sendFile(path + "/public/index.html");
		});

	app.route('/whoami')
		.get(function(req, res){
			res.json(parserController.process(req));
		});
};

'use stricts';

var path = process.cwd();
var Controller = require(path + '/app/controller/parserController.js');
var parserController = new Controller();

module.exports = function(app) {

	app.route('/')
		.get(function(req, res){
			res.sendFile(path + "/public/index.html");
		});

	app.route('/whoami')
		.get(function(req, res){
			res.json(parserController.process(req));
		});

	app.route('/*')
		.get(function(req,res){
			res.redirect('/');
		});
};

Make default redirect page in case of no url served


'use strict';

/* global Connector */

Connector.playerSelector = '.player-container';

Connector.artistSelector = '.player-track-name-artist-standard .track-artist';

Connector.getTrack = function() {
	var trackName = $('.player-track-name-artist-standard .primary-title').text();
	var remixedBy = $('.player-track-name-artist-standard .remixed').text();
	if (remixedBy === 'Original Mix') {
		return trackName;
	}
	return trackName + ' (' + remixedBy + ')';
};

Connector.getUniqueID = function() {
	var trackUrl = $('.player-current-track-container a').attr('href');
	var index = trackUrl.lastIndexOf('/');
	return trackUrl.substring(index + 1);
};

Connector.isPlaying = function() {
	return $('.player-controls .play-button').hasClass('pause');
};

'use strict';

/* global Connector, MetadataFilter */

Connector.playerSelector = '.player-container';

Connector.artistSelector = '.player-track-name-artist-standard .track-artist';

Connector.trackSelector = '.player-track-name-artist-standard .primary-title';

Connector.getUniqueID = function() {
	var trackUrl = $('.player-current-track-container a').attr('href');
	var index = trackUrl.lastIndexOf('/');
	return trackUrl.substring(index + 1);
};

Connector.isPlaying = function() {
	return $('.player-controls .play-button').hasClass('pause');
};

Connector.filter = new MetadataFilter({
	all: MetadataFilter.trim, track: removeOriginalMix
});

function removeOriginalMix(track) {
	let remixedBy = $('.player-track-name-artist-standard .remixed').text();
	if (remixedBy === 'Original Mix') {
		return track;
	}

	return `${track} (${remixedBy})`;
}

Use MetadataFilter to modify track title


import SPECS from 'common/SPECS';
import SPEC_ANALYSIS_COMPLETENESS from 'common/SPEC_ANALYSIS_COMPLETENESS';

import CombatLogParser from './CombatLogParser';
import CHANGELOG from './CHANGELOG';

export default {
  spec: SPECS.SUBTLETY_ROGUE,
  maintainer: '@zealk',
  completeness: SPEC_ANALYSIS_COMPLETENESS.NEEDS_MORE_WORK, // When changing this please make a PR with ONLY this value changed, we will do a review of your analysis to find out of it is complete enough.
  changelog: CHANGELOG,
  parser: CombatLogParser,
  path: __dirname, // used for generating a GitHub link directly to your spec
};

import SPECS from 'common/SPECS';
import SPEC_ANALYSIS_COMPLETENESS from 'common/SPEC_ANALYSIS_COMPLETENESS';

import CombatLogParser from './CombatLogParser';
import CHANGELOG from './CHANGELOG';

export default {
  spec: SPECS.SUBTLETY_ROGUE,
  maintainer: '@zealk',
  completeness: SPEC_ANALYSIS_COMPLETENESS.NOT_ACTIVELY_MAINTAINED, // When changing this please make a PR with ONLY this value changed, we will do a review of your analysis to find out of it is complete enough.
  changelog: CHANGELOG,
  parser: CombatLogParser,
  path: __dirname, // used for generating a GitHub link directly to your spec
};

Update Sub Rogue completion to "not actively maintained" since it hasn't been updated in a long time


var app = angular.module('ProductsApp');

app.directive('inplaceEditing', function() {
    return {
        restrict: 'E',
        scope: {
            edit:'='
        },
        controller : 'editmodeController as editCtrl',
        template: "
 <input type='text' id='{{edit.id}}'  ng-model='edit.name' ng-model-options='{ getterSetter: true }' ng-keypress='editCtrl.enterEvent($event)'>
"

    }
});

var app = angular.module('ProductsApp');

app.directive('inplaceEditing', function() {
    return {
        restrict: 'E',
        scope: {
            edit:'='
        },
        controller : 'editmodeController as editCtrl',
        template: "
<input type='text' id='{{edit.id}}'  ng-model='edit.name' ng-model-options='{ getterSetter: true }' ng-keypress='editCtrl.enterEvent($event)' focus='edit.editMode' ng-blur='edit.editMode = false'>
"
    }
});

app.directive('focus', ($timeout) => {
    return {
        restrict: 'A',
        scope: { focus: '=focus' },
        link: (scope, element, attr) => {
            scope.$watch('focus', (cv, pv) => {
                if (scope.focus) {
                    $timeout(() => {
                        console.log(scope.focus);
                        element[0].focus();
                    }, 0);
                }
                else {
                    $timeout(() => {
                        console.log(scope.focus);
                        element[0].blur();
                    }, 0);
                }
            });
        }
    };
});

Add focus directive and implement on inplace-editing template


var cdb = require('cartodb.js');
var RowsView = require('./rows_view');
var ChooseGeometryView = require('./choose_geometry_view');
var ViewFactory = require('../../view_factory');

/**
 * View for the georeference types that requires the two-steps flow.
 * First select columns values, and then the geometry type to use.
 */
module.exports = cdb.core.View.extend({

  initialize: function() {
    this._initBinds();
  },

  render: function() {
    this.clearSubViews();

    if (this.model.get('step') === 1) {
      this._createChooseGeometryView();
    } else {
      this._createHeaderView();
      this._createRowsView();
    }

    return this;
  },

  _createHeaderView: function() {
    this._appendView(
      ViewFactory.createByTemplate('common/dialogs/georeference/default_content_header', {
        title: this.options.title,
        desc: this.options.desc
      })
    );
  },

  _createRowsView: function() {
    this._appendView(
      new RowsView({
        model: this.model
      })
    );
  },

  _createChooseGeometryView: function() {
    this._appendView(
      new ChooseGeometryView({
        model: this.model
      })
    );
  },

  _appendView: function(view) {
    this.addView(view);
    this.$el.append(view.render().$el);
  },

  _initBinds: function() {
    this.model.bind('change:step', this.render, this);
  }

});

var cdb = require('cartodb.js');
var RowsView = require('./rows_view');
var ChooseGeometryView = require('./choose_geometry_view');
var ViewFactory = require('../../view_factory');

/**
 * View for the georeference types that requires the two-steps flow.
 * First select columns values, and then the geometry type to use.
 */
module.exports = cdb.core.View.extend({

  initialize: function() {
    this._initBinds();
  },

  render: function() {
    this.clearSubViews();

    if (this.model.get('step') === 1) {
      this._renderChooseGeometry();
    } else {
      this._renderHeader();
      this._renderRows();
    }

    return this;
  },

  _renderHeader: function() {
    this._appendView(
      ViewFactory.createByTemplate('common/dialogs/georeference/default_content_header', {
        title: this.options.title,
        desc: this.options.desc
      })
    );
  },

  _renderRows: function() {
    this._appendView(
      new RowsView({
        model: this.model
      })
    );
  },

  _renderChooseGeometry: function() {
    this._appendView(
      new ChooseGeometryView({
        model: this.model
      })
    );
  },

  _appendView: function(view) {
    this.addView(view);
    this.$el.append(view.render().$el);
  },

  _initBinds: function() {
    this.model.bind('change:step', this.render, this);
  }

});

Make methods names more representative


/* eslint-env mocha */
const assert = require('assert')
const cm = require('cucumber-messages').io.cucumber.messages
const Gherkin = require('../src/Gherkin')

describe('Gherkin', () => {
  it('parses gherkin from the file system', async () => {
    const messages = await streamToArray(
      Gherkin.fromPaths(['testdata/good/minimal.feature'])
    )
    assert.strictEqual(messages.length, 3)
  })

  it('parses gherkin from STDIN', async () => {
    const source = cm.Source.fromObject({
      uri: 'test.feature',
      data: `Feature: Minimal

  Scenario: minimalistic
    Given the minimalism
`,
      media: cm.Media.fromObject({
        encoding: 'UTF-8',
        contentType: 'text/x.cucumber.gherkin+plain',
      }),
    })

    const messages = await streamToArray(Gherkin.fromSources([source]))
    assert.strictEqual(messages.length, 3)
  })
})

function streamToArray(readableStream) {
  return new Promise((resolve, reject) => {
    const items = []
    readableStream.on('data', items.push.bind(items))
    readableStream.on('error', reject)
    readableStream.on('end', () => resolve(items))
  })
}

/* eslint-env mocha */
const assert = require('assert')
const cm = require('cucumber-messages').io.cucumber.messages
const Gherkin = require('../src/Gherkin')

describe('Gherkin', () => {
  it('parses gherkin from the file system', async () => {
    const messages = await streamToArray(
      Gherkin.fromPaths(['testdata/good/minimal.feature'])
    )
    assert.strictEqual(messages.length, 3)
  })

  it('parses gherkin from STDIN', async () => {
    const source = cm.Source.fromObject({
      uri: 'test.feature',
      data: `Feature: Minimal

  Scenario: minimalistic
    Given the minimalism
`,
      media: cm.Media.fromObject({
        encoding: 'UTF-8',
        contentType: 'text/x.cucumber.gherkin+plain',
      }),
    })

    const messages = await streamToArray(Gherkin.fromSources([source]))
    console.log('Messages', messages)
    assert.strictEqual(messages.length, 3)
  })
})

function streamToArray(readableStream) {
  return new Promise((resolve, reject) => {
    const items = []
    readableStream.on('data', items.push.bind(items))
    readableStream.on('error', reject)
    readableStream.on('end', () => resolve(items))
  })
}

Add a bit of logging


;(function() {

  var MODULES_PATH = 'https://a.alipayobjects.com/static/arale/'
  //var MODULES_PATH = 'http://modules.seajs.org/'

  if (location.href.indexOf('/~lifesinger/x') > 0) {
    MODULES_PATH = 'http://' + location.host + '/~lifesinger/seajs/spm/modules/'
  }

  // Ref: https://github.com/miohtama/detectmobile.js
  var isMobile = Math.max(screen.availWidth || screen.availHeight) <= 480

  function root(path) {
    return MODULES_PATH + path
  }

  var hash = {
    '$': root(isMobile ? 'zepto/0.8.0/zepto.js' : 'jquery/1.7.2/jquery.js'),
    'coffee': root('coffee/1.3.3/coffee-script.js'),
    'less': root('less/1.3.0/less.js')
  }

  seajs.config({
    alias: hash
  })

  define({ isMobile: isMobile })

})()

;(function() {

  var MODULES_PATH = 'https://a.alipayobjects.com/static/arale/'
  //var MODULES_PATH = 'http://modules.seajs.org/'

  if (location.href.indexOf('/~lifesinger/') > 0) {
    MODULES_PATH = 'http://' + location.host + '/~lifesinger/seajs/spm/modules/'
  }

  // Ref: https://github.com/miohtama/detectmobile.js
  var isMobile = Math.max(screen.availWidth || screen.availHeight) <= 480

  function root(path) {
    return MODULES_PATH + path
  }

  var hash = {
    '$': root(isMobile ? 'zepto/0.8.0/zepto.js' : 'jquery/1.7.2/jquery.js'),
    'coffee': root('coffee/1.3.3/coffee-script.js'),
    'less': root('less/1.3.0/less.js')
  }

  seajs.config({
    alias: hash
  })

  define({ isMobile: isMobile })

})()

Fix modules path in test cases


describe('hasVowels()', function() {
    it('should always return a boolean', function() {
        expect('example string'.hasVowels()).toEqual(jasmine.any(Boolean));
    });

    it('should return true if string has vowels', function() {
        expect('oreofe'.hasVowels()).toBe(true);
        expect(('AnDeLa').hasVowels()).toBe(true);
    });

    it('should return false if string has no vowels', function() {
        expect('why'.hasVowels()).toBe(false);
        expect(('twyndyllyngs').hasVowels()).toBe(false);
    });
});

describe('toUpper()', function() {
    it('should always return a string', function() {
        expect('example string'.toUpper()).toEqual(jasmine.any(String));
    });

    it('should return an upper case of the calling string', function() {
        expect('upper case'.toUpper()).toBe('UPPER CASE');
        expect('camelCase'.toUpper()).toBe('CAMELCASE');
        expect('MiXeD cAsE'.toUpper()).toBe('MIXED CASE');
    });
});

describe('toLower()', function() {
    it('should return a string', function() {
        expect('example string'.toLower()).toEqual(jasmine.any(String));
    });
});

describe('hasVowels()', function() {
    it('should always return a boolean', function() {
        expect('example string'.hasVowels()).toEqual(jasmine.any(Boolean));
    });

    it('should return true if string has vowels', function() {
        expect('oreofe'.hasVowels()).toBe(true);
        expect(('AnDeLa').hasVowels()).toBe(true);
    });

    it('should return false if string has no vowels', function() {
        expect('why'.hasVowels()).toBe(false);
        expect(('twyndyllyngs').hasVowels()).toBe(false);
    });
});

describe('toUpper()', function() {
    it('should always return a string', function() {
        expect('example string'.toUpper()).toEqual(jasmine.any(String));
    });

    it('should return an upper case of the calling string', function() {
        expect('upper case'.toUpper()).toBe('UPPER CASE');
        expect('camelCase'.toUpper()).toBe('CAMELCASE');
        expect('MiXeD cAsE'.toUpper()).toBe('MIXED CASE');
    });
});

describe('toLower()', function() {
    it('should return a string', function() {
        expect('example string'.toLower()).toEqual(jasmine.any(String));
    });

    it('shoudl return a lower case of the calling string', function() {
        expect('LOWER CASE'.toLower()).toBe('lower case');
        expect('PascalCaseD'.toLower()).toBe('pascalcased');
        expect('MiXeDcAsE'.toLower()).toBe('mixedcase');
    });
});

Add spec to test the returned result of toLower


/*
 * Copyright 2008 The Closure Compiler Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview JavaScript Built-Ins that are not part of any specifications
 * but are still needed in some project's build.
 * @externs
 */

var opera = {};

opera.postError;

/** @nosideeffects */
opera.version = function() {};

/** @constructor */
function XSLTProcessor() {}

/**
 * @constructor @extends {Element}
 * @param {*=} opt_text
 * @param {*=} opt_value
 * @param {*=} opt_defaultSelected
 * @param {*=} opt_selected
 */
function Option(opt_text, opt_value, opt_defaultSelected, opt_selected) {}

// The "methods" object is a place to hang arbitrary external
// properties. It is a throwback to pre-typed days, and should
// not be used for any new definitions; it exists only to bridge
// the gap between the old way and the new way.
var methods = {};

/*
 * Copyright 2008 The Closure Compiler Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview JavaScript Built-Ins that are not part of any specifications
 * but are still needed in some project's build.
 * @externs
 */

var opera = {};

opera.postError;

/** @nosideeffects */
opera.version = function() {};

/** @constructor */
function XSLTProcessor() {}

/**
 * @constructor
 * @extends {HTMLOptionElement}
 * @param {*=} opt_text
 * @param {*=} opt_value
 * @param {*=} opt_defaultSelected
 * @param {*=} opt_selected
 */
function Option(opt_text, opt_value, opt_defaultSelected, opt_selected) {}

// The "methods" object is a place to hang arbitrary external
// properties. It is a throwback to pre-typed days, and should
// not be used for any new definitions; it exists only to bridge
// the gap between the old way and the new way.
var methods = {};

Make Option extend HTMLOptionElement instead of Element


define('app/views/key_list', [
    'text!app/templates/key_list.html',
    'ember'
    ],
    /**
     *
     * Key list page
     *
     * @returns Class
     */
    function(key_list_html) {
        return Ember.View.extend({
            tagName: false,

            init: function() {
                this._super();
                this.set('template', Ember.Handlebars.compile(key_list_html));
            },

            deleteKey: function(){
                var keys = this.getSelectedKeys();
                var plural = false;

                if(keys.length == 0){
                    return;
                } else if(keys.length > 1){
                    plural = true;
                }

                Mist.confirmationController.set("title", 'Delete Key' + (plural ? 's' : ''));

                var names = '';

                keys.forEach(function(key){
                    names = names + ' ' + key.name;
                });

                Mist.confirmationController.set("text", 'Are you sure you want to delete' +
                        names +'?');

                Mist.confirmationController.set("callback", function(){
                    keys.forEach(function(key){
                       key.deleteKey();
                       window.history.go(-1);
                    });
                });

                Mist.confirmationController.set("fromDialog", true);
                Mist.confirmationController.show();
            },

            getSelectedKeys: function(){
                var keys = new Array();

                Mist.keysController.forEach(function(key){
                        if(key.selected){
                            keys.push(key);
                        }
                });

                return keys;
            },
        });
    }
);

define('app/views/key_list', [
    'text!app/templates/key_list.html',
    'ember'
    ],
    /**
     *
     * Key list page
     *
     * @returns Class
     */
    function(key_list_html) {
        return Ember.View.extend({
            tagName: false,

            init: function() {
                this._super();
                this.set('template', Ember.Handlebars.compile(key_list_html));
            },

            deleteKey: function(){
                var keys = this.getSelectedKeys();
                var plural = false;

                if(keys.length == 0){
                    return;
                } else if(keys.length > 1){
                    plural = true;
                }

                Mist.confirmationController.set("title", 'Delete Key' + (plural ? 's' : ''));

                var names = '';

                keys.forEach(function(key){
                    names = names + ' ' + key.name;
                });

                Mist.confirmationController.set("text", 'Are you sure you want to delete' +
                        names +'?');

                Mist.confirmationController.set("callback", function(){
                    keys.forEach(function(key){
                       key.deleteKey();
                       window.history.go(-1);
                       $('#keys .keys-footer').fadeOut(200);
                    });
                });

                Mist.confirmationController.set("fromDialog", true);
                Mist.confirmationController.show();
            },

            getSelectedKeys: function(){
                var keys = new Array();

                Mist.keysController.forEach(function(key){
                        if(key.selected){
                            keys.push(key);
                        }
                });

                return keys;
            },
        });
    }
);

Hide actions after key deletion


/**
 * External dependencies
 */

/**
 * Internal dependencies
 */
import SiteKitRegistry from 'assets/js/googlesitekit/data';
import { registerSiteKitStores } from 'assets/js/googlesitekit/data/store';
import { collectReducers } from 'assets/js/googlesitekit/data/utils';

if ( typeof global.googlesitekit === 'undefined' ) {
	global.googlesitekit = {};
}

if ( typeof global.googlesitekit.data === 'undefined' ) {
	global.googlesitekit.data = { collectReducers, registerSiteKitStores, registry: SiteKitRegistry };
	// Register all stores on the default Site Kit registry.
	registerSiteKitStores( global.googlesitekit.data.registry );
}

export { collectReducers };

export default SiteKitRegistry;

/**
 * External dependencies
 */

/**
 * Internal dependencies
 */
import SiteKitRegistry from 'assets/js/googlesitekit/data';
import { registerSiteKitStores } from 'assets/js/googlesitekit/data/store';
import { collect, collectReducers } from 'assets/js/googlesitekit/data/utils';

if ( typeof global.googlesitekit === 'undefined' ) {
	global.googlesitekit = {};
}

if ( typeof global.googlesitekit.data === 'undefined' ) {
	global.googlesitekit.data = { collect, collectReducers, registerSiteKitStores, registry: SiteKitRegistry };
	// Register all stores on the default Site Kit registry.
	registerSiteKitStores( global.googlesitekit.data.registry );
}

export { collect, collectReducers };

export default SiteKitRegistry;

Add collect to data exports.


let id = 0;

const identity = arg => arg;

export default function createAction(name, mapper = identity) {
  if (typeof name === 'function') {
    mapper = name;
    name = undefined;
  }

  if (typeof mapper !== 'function') {
    mapper = identity;
  }

  const action = {
    id: ++id,
    type: `[${id}]${name ? ' ' + name : ''}`
  };

  let actionStores = undefined;

  function setupPayload(payload) {
    return {
      id: action.id,
      type: action.type,
      payload: payload
    };
  }

  function actionCreator(...args) {
    const payloaded = setupPayload(mapper.apply(undefined, args));

    if (Array.isArray(actionStores)) {
      return actionStores.map(store=> store.dispatch(payloaded));
    } else if (actionStores && actionStores.dispatch) {
      return actionStores.dispatch(payloaded);
    } else {
      return payloaded;
    }
  }

  actionCreator.toString = ()=> action.id;

  actionCreator.bindTo = (stores)=> {
    actionStores = stores;
    return actionCreator;
  };

  return actionCreator;
}

let id = 0;

const identity = arg => arg;

export default function createAction(name, mapper = identity) {
  if (typeof name === 'function') {
    mapper = name;
    name = undefined;
  }

  if (typeof mapper !== 'function') {
    mapper = identity;
  }

  const action = {
    id: ++id,
    type: `[${id}]${name ? ' ' + name : ''}`
  };

  let actionStores = undefined;

  function setupPayload(payload) {
    return {
      id: action.id,
      type: action.type,
      payload: payload
    };
  }

  function actionCreator(...args) {
    const payloaded = setupPayload(mapper.apply(undefined, args));

    if (Array.isArray(actionStores)) {
      return actionStores.map(store=> store.dispatch(payloaded));
    } else if (actionStores) {
      return actionStores.dispatch(payloaded);
    } else {
      return payloaded;
    }
  }

  actionCreator.toString = ()=> action.id;

  actionCreator.bindTo = (stores)=> {
    actionStores = stores;
    return actionCreator;
  };

  return actionCreator;
};

Remove dispatch check since there is none for store array


//= require ./fixtures

describe('StudentProfileHeader', function() {
  var dom = window.shared.ReactHelpers.dom;
  var createEl = window.shared.ReactHelpers.createEl;
  var merge = window.shared.ReactHelpers.merge;

  var StudentProfileHeader = window.shared.StudentProfileHeader;
  var SpecSugar = window.shared.SpecSugar;
  var Fixtures = window.shared.Fixtures;

  var helpers = {
    renderInto: function(el, props) {
      var mergedProps = merge(props || {}, {
        student: Fixtures.studentProfile.student
      });
      return ReactDOM.render(createEl(StudentProfileHeader, mergedProps), el);
    }
  };

  SpecSugar.withTestEl('high-level integration tests', function() {
    it('renders note-taking area', function() {
      var el = this.testEl;
      helpers.renderInto(el);

      expect(el).toContainText('Daisy Poppins');
      expect(el).toContainText('Arthur D Healey');
      expect($(el).find('a.homeroom-link')).toContainText('102');
    });
  });
});

//= require ./fixtures

describe('StudentProfileHeader', function() {
  var dom = window.shared.ReactHelpers.dom;
  var createEl = window.shared.ReactHelpers.createEl;
  var merge = window.shared.ReactHelpers.merge;

  var StudentProfileHeader = window.shared.StudentProfileHeader;
  var SpecSugar = window.shared.SpecSugar;
  var Fixtures = window.shared.Fixtures;

  var helpers = {
    renderActiveStudent: function(el, props) {
      var mergedProps = merge(props || {}, { student: Fixtures.studentProfile.student });
      return ReactDOM.render(createEl(StudentProfileHeader, mergedProps), el);
    },

    renderTransferredStudent: function(el, props) {
      var this_student = Fixtures.studentProfile.student;
      this_student['enrollment_status'] = 'Transferred';

      var mergedProps = merge(props || {}, { student: this_student });
      return ReactDOM.render(createEl(StudentProfileHeader, mergedProps), el);
    }

  };

  SpecSugar.withTestEl('active enrolled student', function() {
    it('renders note-taking area with homeroom', function() {
      var el = this.testEl;
      helpers.renderActiveStudent(el);

      expect(el).toContainText('Daisy Poppins');
      expect(el).toContainText('Arthur D Healey');
      expect($(el).find('a.homeroom-link')).toContainText('102');
    });
  });

  SpecSugar.withTestEl('non-active Transferred student', function() {
    it('renders note-taking area with Transferred status', function() {
      var el = this.testEl;
      helpers.renderTransferredStudent(el);

      expect(el).toContainText('Daisy Poppins');
      expect(el).toContainText('Arthur D Healey');
      expect(el).toContainText('Transferred');
    });
  });

});

Add test case for non-Active student


/*************************************************
 * Copyright (c) 2017 Ansible, Inc.
 *
 * All Rights Reserved
 *************************************************/

export default ['$scope', 'QuerySet', 'InventoryHostsStrings',
    function($scope, qs, InventoryHostsStrings) {
        $scope.hostFilterTags = [];

        $scope.filterTooltip = InventoryHostsStrings.get('smartinventories.TOOLTIP');

        $scope.$watch('hostFilter', function(){
            $scope.hostFilterTags = [];

            if($scope.hostFilter && $scope.hostFilter !== '') {
                let hostFilterCopy = angular.copy($scope.hostFilter);

                let searchParam = hostFilterCopy.host_filter.split('%20and%20');
                delete hostFilterCopy.host_filter;

                $.each(searchParam, function(index, param) {
                    let paramParts = decodeURIComponent(param).split(/=(.+)/);
                    paramParts[0] = paramParts[0].replace(/__icontains(_DEFAULT)?/g, "");
                    paramParts[0] = paramParts[0].replace(/__search(_DEFAULT)?/g, "");
                    let reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]);
                    $scope.hostFilterTags.push(reconstructedSearchString);
                });

                $scope.hostFilterTags = $scope.hostFilterTags.concat(qs.stripDefaultParams(hostFilterCopy));
            }
        });
    }
];

/*************************************************
 * Copyright (c) 2017 Ansible, Inc.
 *
 * All Rights Reserved
 *************************************************/

export default ['$scope', 'QuerySet', 'InventoryHostsStrings',
    function($scope, qs, InventoryHostsStrings) {
        $scope.hostFilterTags = [];

        $scope.filterTooltip = $scope.fieldIsDisabled ? '' : InventoryHostsStrings.get('smartinventories.TOOLTIP');

        $scope.$watch('hostFilter', function(){
            $scope.hostFilterTags = [];

            if($scope.hostFilter && $scope.hostFilter !== '') {
                let hostFilterCopy = angular.copy($scope.hostFilter);

                let searchParam = hostFilterCopy.host_filter.split('%20and%20');
                delete hostFilterCopy.host_filter;

                $.each(searchParam, function(index, param) {
                    let paramParts = decodeURIComponent(param).split(/=(.+)/);
                    paramParts[0] = paramParts[0].replace(/__icontains(_DEFAULT)?/g, "");
                    paramParts[0] = paramParts[0].replace(/__search(_DEFAULT)?/g, "");
                    let reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]);
                    $scope.hostFilterTags.push(reconstructedSearchString);
                });

                $scope.hostFilterTags = $scope.hostFilterTags.concat(qs.stripDefaultParams(hostFilterCopy));
            }
        });
    }
];

Hide filter tooltip when host filter is disabled


var FileSystem = require('fs');
var RequestHash = require('./request_hash');

var CacheClient = function(cacheDir) {
  this.cacheDir = cacheDir;
}

CacheClient.prototype.isCached = function (request) {
  var self = this;
  return new Promise(function(resolve, reject) {
    var mode = FileSystem.F_OK | FileSystem.R_OK | FileSystem.W_OK;
    FileSystem.access(self.path(request), mode, function (error) {
      if (error) { // No Access, File Does Not Exist.
        resolve(false);
      } else { // File Exists and is accessable.
        resolve(true);
      }
    });
  });
}

CacheClient.prototype.fetch = function (request) {
  var self = this;
  return new Promise(function(resolve, reject) {
    FileSystem.readFile(self.path(request), function (err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

CacheClient.prototype.path = function (request) {
  var requestHash = new RequestHash(request).toString();
  return this.cacheDir + '/' + requestHash;
}

module.exports = CacheClient

var FileSystem = require('fs');
var RequestHash = require('./request_hash');

var CacheClient = function(cacheDir) {
  this.cacheDir = cacheDir;
}

CacheClient.prototype.isCached = function (request) {
  var self = this;
  var mode = FileSystem.F_OK | FileSystem.R_OK | FileSystem.W_OK;
  var error;
  try {
    error = FileSystem.accessSync(self.path(request), mode);
    return true;
  } catch (error) {
    return false;
  }
}

CacheClient.prototype.fetch = function (request) {
  var self = this;
  return new Promise(function(resolve, reject) {
    FileSystem.readFile(self.path(request), function (err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

CacheClient.prototype.record = function (request, response) {
  var self = this;
  return new Promise(function(resolve, reject) {
    FileSystem.writeFile(self.path(request), response, function (err) {
      if (err) {
        reject(err);
      } else {
        resolve(response);
      }
    });
  });
}

CacheClient.prototype.path = function (request) {
  var requestHash = new RequestHash(request).toString();
  return this.cacheDir + '/' + requestHash;
}

module.exports = CacheClient

Add Record Logic to Recall Responses


import equal from 'deep-equal';

export default class Feed {
  constructor(api, path, params) {
    this.api = api;
    this.path = path;
    this.params = params;

    this.nextURL = undefined;
    this.items = [];
  }

  sameAs(other) {
    return this.path == other.path && equal(this.params, other.params);
  }

  fetchAll() {
    return new Promise((resolve, reject) => {
      const next = () => {
        if (this.hasMore()) {
          this.fetchNext().then(next).catch(reject);
        } else {
          resolve(this.items);
        }
      }
      next();
    });
  }

  fetchNext() {
    return this._fetchNextData()
      .then(data => {
        const items = data.results;
        this.nextURL = data.next;
        this.items = this.items.concat(items);
        return items;
      });
  }

  hasAnything() {
    return this.items.length !== 0;
  }

  hasMore() {
    return this.nextURL !== null;  // undefined means we don't know yet
  }

  _fetchNextData() {
    if (this.nextURL) {
      return this.api.get(this.nextURL);
    } else {
      return this.api.get(this.path, this.params);
    }
  }
}

import equal from 'deep-equal';

export default class Feed {
  constructor(api, path, params) {
    this.api = api;
    this.path = path;
    this.params = params;

    this.nextURL = undefined;
    this.items = [];
  }

  fetchAll() {
    return new Promise((resolve, reject) => {
      const next = () => {
        if (this.hasMore()) {
          this.fetchNext().then(next).catch(reject);
        } else {
          resolve(this.items);
        }
      }
      next();
    });
  }

  fetchNext() {
    return this._fetchNextData()
      .then(data => {
        const items = data.results;
        this.nextURL = data.next;
        this.items = this.items.concat(items);
        return items;
      });
  }

  hasAnything() {
    return this.items.length !== 0;
  }

  hasMore() {
    return this.nextURL !== null;  // undefined means we don't know yet
  }

  _fetchNextData() {
    if (this.nextURL) {
      return this.api.get(this.nextURL);
    } else {
      return this.api.get(this.path, this.params);
    }
  }
}

Remove an unneeded method from Feed class


'use strict';

const { logEvent } = require('../../log');

// Error handler, which sends final response, if server-side errors
const errorHandler = async function ({
  error,
  protocolAdapter,
  config,
  mInput,
}) {
  // Make sure a response is sent, even empty, or the socket will hang
  await protocolAdapter.send({ content: '', contentLength: 0 });

  const mInputA = { ...DEFAULT_MINPUT, ...mInput };

  // Report any exception thrown
  await logEvent({
    mInput: mInputA,
    event: 'failure',
    phase: 'request',
    level: 'error',
    params: { error },
    config,
  });
};

const DEFAULT_MINPUT = {
  status: 'SERVER_ERROR',
};

module.exports = {
  errorHandler,
};

'use strict';

const { logEvent } = require('../../log');

// Error handler, which sends final response, if server-side errors
const errorHandler = async function ({
  error,
  protocolAdapter,
  config,
  mInput,
}) {
  // Make sure a response is sent, even empty, or the socket will hang
  await protocolAdapter.send({ content: '', contentLength: 0 });

  // In case an error happened during final layer
  const mInputA = { ...mInput, status: 'SERVER_ERROR' };

  // Report any exception thrown
  await logEvent({
    mInput: mInputA,
    event: 'failure',
    phase: 'request',
    level: 'error',
    params: { error },
    config,
  });
};

module.exports = {
  errorHandler,
};

Make sure server-side errors do not have status SUCCESS


var PokeApi = PokeApi || {};
PokeApi.apiUrl = "http://pokeapi.co/api/";
PokeApi.apiVersion = "v2";

PokeApi.getResource = function getResource(resource) {
    return new Promise(function(resolve, reject) {
        fetch(PokeApi.apiUrl + PokeApi.apiVersion + "/" + resource).then(function(response) {
            // handle HTTP response
            if(response.ok){
                response.json().then(function(responseJson){resolve(responseJson)});
            } else{
                reject(response.status + " " + response.statusText)
            }

        }, function(error) {
            // handle network error
            reject(error.message);
        })
    })
}

PokeApi.getBerry = function getBerry(id) {
    return new Promise(function(resolve, reject) {
        PokeApi.getResource("berry/" + id).then(function(response) {
            resolve(response);
        }).catch(function(error){
            reject(error);
        })
    })
}

var PokeApi = PokeApi || {};
PokeApi.apiUrl = "https://pokeapi.co/api/";
PokeApi.apiVersion = "v2";

PokeApi.getResource = function getResource(resource) {
    return new Promise(function(resolve, reject) {
        fetch(PokeApi.apiUrl + PokeApi.apiVersion + "/" + resource).then(function(response) {
            // handle HTTP response
            if(response.ok){
                response.json().then(function(responseJson){resolve(responseJson)});
            } else{
                reject(response.status + " " + response.statusText)
            }

        }, function(error) {
            // handle network error
            reject(error.message);
        })
    })
}

PokeApi.getBerry = function getBerry(id) {
    return new Promise(function(resolve, reject) {
        PokeApi.getResource("berry/" + id).then(function(response) {
            resolve(response);
        }).catch(function(error){
            reject(error);
        })
    })
}

Change pokeapi url to use https


var drawLine = function(sx, sy, dx, dy) {
    gfx.ctx.beginPath();
    gfx.ctx.moveTo(sx, sy);
    gfx.ctx.lineTo(dx, dy);
    gfx.ctx.stroke();
}

var drawWorld = function(world) {

    drawGrid(world);
}

var drawGrid = function(world) {

    for (var x = 0; x <= world.getWidth(); x++) {
        var cell_x = (camera.getWidth() / world.getWidth()) * x;
        drawLine(cell_x, 0, cell_x, camera.getHeight());
    }

    for (var y = 0; y <= world.getHeight(); y++) {
        var cell_y = (camera.getHeight() / world.getHeight()) * y;
        drawLine(0, cell_y, camera.getWidth(), cell_y);
    }
}

var drawLine = function(sx, sy, dx, dy) {
    gfx.ctx.beginPath();
    gfx.ctx.moveTo(sx, sy);
    gfx.ctx.lineTo(dx, dy);
    gfx.ctx.stroke();
}

var drawWorld = function(world) {

    drawGrid(world);
}

    gfx.ctx.strokeStyle = "#000000";
    for (var x = 0; x <= world.getWidth(); x++) {
        var draw_x = camera.transformToCameraSpace(x, 0).cam_x;
        var top = camera.transformToCameraSpace(x, 0).cam_y;
        var bot = camera.transformToCameraSpace(x, world.getHeight()).cam_y;
        drawLine(draw_x, top, draw_x, bot);
    }

    for (var y = 0; y <= world.getHeight(); y++) {
        var draw_y = camera.transformToCameraSpace(0, y).cam_y;
        var left = camera.transformToCameraSpace(0, y).cam_x;
        var right = camera.transformToCameraSpace(world.getWidth(), y).cam_x;
        drawLine(left, draw_y, right, draw_y);
    }
}

Improve grid rendering with new camera methods


/* global process */
module.exports = function (config) {
    config.set({
        browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ],
        singleRun: true,
        frameworks: [ 'mocha' ],
        files: [
            'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-with-addons.js',
            'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js',
            './__tests__/tests.webpack.js'
        ],
        preprocessors: {
            './__tests__/tests.webpack.js': [ 'webpack', 'sourcemap' ]
        },
        reporters: [ 'dots' ],
        webpack: {
            devtool: 'inline-source-map',
            module: {
                loaders: [
                    {
                        test: /\.jsx?$/,
                        loaders: [
                            'jsx-loader?stripTypes',
                            'babel?stage=0'
                        ],
                        exclude: /node_modules/
                    }
                ]
            },
            externals : {
                'react'     : 'React',
                'react-dom' : 'ReactDOM'
            }
        },
        webpackServer: {
            noInfo: true
        }
    });
};

/* global process */
module.exports = function (config) {
    config.set({
        browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ],
        singleRun: !!process.env.CONTINUOUS_INTEGRATION,
        frameworks: [ 'mocha' ],
        files: [
            'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-with-addons.js',
            'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js',
            './__tests__/tests.webpack.js'
        ],
        preprocessors: {
            './__tests__/tests.webpack.js': [ 'webpack', 'sourcemap' ]
        },
        reporters: [ 'dots' ],
        webpack: {
            devtool: 'inline-source-map',
            module: {
                loaders: [
                    {
                        test: /\.jsx?$/,
                        loaders: [
                            'jsx-loader?stripTypes',
                            'babel?stage=0'
                        ],
                        exclude: /node_modules/
                    }
                ]
            },
            externals : {
                'react'     : 'React',
                'react-dom' : 'ReactDOM'
            }
        },
        webpackServer: {
            noInfo: true
        }
    });
};

Use multiple run for local karma


'use strict';

var extendPrototypeWithThese = function (prototype, extendThese) {
    /*
        Helper method to implement a simple inheritance model for object prototypes.
    */

    var outp = prototype;

    if (extendThese) {

        for (var i = extendThese.length - 1; i >= 0; i--) {
            // I need to create the object in order to copy the prototype functions
            var tmpObj = extendThese[i].prototype;
            for (var key in tmpObj) {
                if (key == '_implements') {
                    // Implements should be extended with later coming before earlier
                    // TODO: Filer so we remove duplicates from existing list (order makes difference)
                    outp.prototype._implements = tmpObj._implements.concat(outp.prototype._implements);
                } else {
                    // All others added and lower indexes override higher
                   outp.prototype[key] = tmpObj[key];
                }
            }
        }

    }

    return outp;
}
module.exports.extendPrototypeWithThese = extendPrototypeWithThese;

var addMembers = function (outp, params) {
    /*
        Helper method to add each item in params dictionary to the prototype of outp.
    */

    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            outp.prototype[key] = params[key];
        }
    }

    return outp;
}
module.exports.addMembers = addMembers;

'use strict';

var extendPrototypeWithThese = function (prototype, extendThese) {
    /*
        Helper method to implement a simple inheritance model for object prototypes.
    */

    var outp = prototype;

    if (extendThese) {

        for (var i = extendThese.length - 1; i >= 0; i--) {
            // I need to create the object in order to copy the prototype functions
            var tmpObj = extendThese[i].prototype;
            for (var key in tmpObj) {
                if (key == '_implements') {
                    // Implements should be extended with later coming before earlier
                    // TODO: Filer so we remove duplicates from existing list (order makes difference)
                    outp.prototype._implements = tmpObj._implements.concat(outp.prototype._implements);
                } else {
                    // All others added and lower indexes override higher
                   outp.prototype[key] = tmpObj[key];
                }
            }
        }

    }

    return outp;
}
module.exports.extendPrototypeWithThese = extendPrototypeWithThese;

var addMembers = function (outp, params) {
    /*
        Helper method to add each item in params dictionary to the prototype of outp.
    */

    for (var key in params) {
        if (params.hasOwnProperty(key)) {
            if (typeof outp.prototype[key] === 'function') {
                // Store inherited functions that are overridden in the _super property
                if (!outp.prototype._super) {
                    outp.prototype._super = {};
                }
                outp.prototype._super[key] = outp.prototype[key];
            }
            outp.prototype[key] = params[key];
        }
    }

    return outp;
}
module.exports.addMembers = addMembers;

Store overridden methods in _super


var Path = require('path');

var config = global.Config = module.exports = require('nconf');
var package = require('../package.json');

config.argv();
config.file(Path.resolve(__dirname, '../config/site.json'));

config.defaults({
  name: package.name,
  version: package.version,
  service: {
    listen: 8080
  },
  db: {
    cluster: ['127.0.0.1:2379']
  }
});

var Path = require('path');

var config = global.Config = module.exports = require('nconf');
var package = require('../package.json');

config.argv();
config.file(Path.resolve(__dirname, '../config/site.json'));

config.defaults({
  name: package.name,
  version: package.version,
  service: {
    listen: 2381
  },
  db: {
    cluster: ['127.0.0.1:2379']
  }
});

Change default service port to 2381


import {mapValues} from 'lodash';
import shallowEquals from 'shallow-equal/objects';

// Simplified version of reselect
const createSelector = (select, inputEquals) => {
    let lastInput = null;
    let lastResult = null;

    return (...input) => {
        if (!lastInput || !inputEquals(lastInput, input)) {
            lastResult = select(...input);
            lastInput = input;
        }

        return lastResult;
    };
};

const composeCallback = (customCallback, ownCallback) => e => {
    ownCallback(e);
    customCallback(); // `customCallback` is already bound with `arg`
};

export const createEventsBindingSelector = ownEvents => {
    const ownEventEntries = Object.entries(ownEvents);

    return createSelector(
        // Bind args of all event callbacks to given input
        (events, arg) => {
            const customEvents = mapValues(events, fn => () => fn(arg));
            return ownEventEntries.reduce(
                (events, [name, ownCallback]) => {
                    const customCallback = events[name];
                    const callback = customCallback ? composeCallback(customCallback, ownCallback) : ownCallback;

                    return {
                        ...events,
                        [name]: callback
                    };
                },
                customEvents
            );
        },
        // [events, {change, side}]
        (prev, next) => shallowEquals(prev[0], next[0]) && shallowEquals(prev[1], next[1])
    );
};

import {mapValues} from 'lodash';
import shallowEquals from 'shallow-equal/objects';
import arrayShallowEquals from 'shallow-equal/arrays';

// Simplified version of reselect
export const createSelector = (select, inputEquals = arrayShallowEquals) => {
    let lastInput = null;
    let lastResult = null;

    return (...input) => {
        if (!lastInput || !inputEquals(lastInput, input)) {
            lastResult = select(...input);
            lastInput = input;
        }

        return lastResult;
    };
};

const composeCallback = (customCallback, ownCallback) => e => {
    ownCallback(e);
    customCallback(); // `customCallback` is already bound with `arg`
};

export const createEventsBindingSelector = (ownEvents = {}) => {
    const ownEventEntries = Object.entries(ownEvents);

    return createSelector(
        // Bind args of all event callbacks to given input
        (events, arg) => {
            const customEvents = mapValues(events, fn => () => fn(arg));
            return ownEventEntries.reduce(
                (events, [name, ownCallback]) => {
                    const customCallback = events[name];
                    const callback = customCallback ? composeCallback(customCallback, ownCallback) : ownCallback;

                    return {
                        ...events,
                        [name]: callback
                    };
                },
                customEvents
            );
        },
        // [events, {change, side}]
        (prev, next) => shallowEquals(prev[0], next[0]) && shallowEquals(prev[1], next[1])
    );
};

Fix event binding selector in unified mode


Template.singleHomeActivityStatus.helpers({
  "activityLevelCounts": function () {
    var instance = this;

    // Get ID of current home
    var homeId = this._id;

    return ReactiveMethod.call("getHomeActivityLevelCounts", homeId);
  }
});

Template.singleHomeActivityStatus.onCreated(function () {
    var instance = this;

    // Get ID of current home
    var homeId = instance.data.home._id;

    // Add variable to hold activity counts
    instance.activityLevelCounts = new ReactiveVar();

    instance.autorun(function () {
      // Retrieve home resident activity level counts from server
      const activityLevelCounts = ReactiveMethod.call("getHomeActivityLevelCounts", homeId);

      // Update the reactive variable, to trigger the graph to render
      instance.activityLevelCounts.set(activityLevelCounts);
    });
  });

Set up activityLevelCounts instance reactive var


/**
 * Migrates the database from one version to another.
 */
function migrateStorage() {
	var extensionVersion = getExtensionVersion();

	getStorageVersion(function(storageVersion) {
		if (!versionsAreEqual(extensionVersion, storageVersion)) {
			console.log("Extension and storage versions are not equal", extensionVersion, storageVersion);
			setStorageVersion(extensionVersion);

			if (storageVersion.major == 1) {
				if (extensionVersion.major == 2) {
					// Migrate from a database from 1.x.x to 2.x.x
					migrate_1_to_2(function() {
						console.log("Migration from 1.x.x to 2.x.x complete");
					});
				}
			}
		}
	});
}

/**
 * Migrates the database from one version to another.
 */
function migrateStorage() {
	var extensionVersion = getExtensionVersion().major;

	getStorageVersion(function(storageVersion) {
		if (extensionVersion != storageVersion) {
			console.log("Extension and storage versions are not equal", extensionVersion, storageVersion);
			setStorageVersion(extensionVersion);

			if (storageVersion == 1) {
				if (extensionVersion.major == 2) {
					// Migrate from a database from 1.x.x to 2.x.x
					migrate_1_to_2(function() {
						console.log("Migration from 1.x.x to 2.x.x complete");
					});
				}
			}
		}
	});
}

Fix comparison of extension and storage versions.


var httpProxy = require('http-proxy');
var http = require('http');
var morgan = require('morgan');
var logger = morgan('short');
var proxy = httpProxy.createProxyServer({
    target: 'https://accountview.net',
    secure: false
  })
  .on('proxyRes', function (proxyRes, req, res) {
    proxyRes.headers['access-control-allow-origin'] = '*';
    proxyRes.headers['access-control-max-age'] = 3600;
  });

var listener = http.createServer(
  function (req, res) {
    console.log(JSON.stringify(req.headers));

    logger(req, res, function (err) {
      // if (err) return done(err)
      proxy.web(req, res);
    })
  }
).listen(process.env.PORT || 3000, function () {
  console.log('Your app is listening on port ' + listener.address().port);
});

var httpProxy = require('http-proxy');
var http = require('http');
var morgan = require('morgan');
var logger = morgan('short');
var proxy = httpProxy.createProxyServer({
    target: 'https://accountview.net',
    secure: false
  })
  .on('proxyRes', function (proxyRes, req, res) {
    proxyRes.headers['access-control-allow-origin'] = '*';
    proxyRes.headers['access-control-allow-headers'] = 'origin, authorization, x-company';
    proxyRes.headers['access-control-max-age'] = 3600;
  });

var listener = http.createServer(
  function (req, res) {
    console.log(JSON.stringify(req.headers));

    logger(req, res, function (err) {
      // if (err) return done(err)
      proxy.web(req, res);
    })
  }
).listen(process.env.PORT || 3000, function () {
  console.log('Your app is listening on port ' + listener.address().port);
});

Fix access-control-allow-headers value on response


import frux from 'frux';
import content from './modules/content';
import { browserHistory } from 'react-router';
import { initializeRouter } from './router';

export const { actions, getters } = frux.initialize({
  options: { debug: true },
  content
});

export function initialize(options) {
  const history = browserHistory;
  const router = initializeRouter({ history });

  frux.mount(router);
};

import frux from 'frux';
import content from './modules/content';
import { browserHistory } from 'react-router';
import { initializeRouter } from './router';

export const { actions, getters } = frux.initialize({
  options: { debug: process.env.NODE_ENV === 'development' },
  content
});

export function initialize(options) {
  const history = browserHistory;
  const router = initializeRouter({ history });

  frux.mount(router);
};

Add debug true only if in development mode


// Configuration for require.js
require.config({
    baseUrl: "/src",
    paths : {
    	"text": '../vendor/text'
    },
    waitSeconds: 5
});

require(['modules/log', 'views/mainView'], function(log, MainView) {

	var mainView = null;

	log.info('MAIN', "Application start");

	var lang = navigator.language || navigator.userLanguage || 'en-US';

    log.debug('POPUP', 'Detected language', lang);

    //Moment settings
    moment.locale(lang, {week: {dow: 1, doy: 4}});

    //Language initialization
    i18n.init({ lng: lang}, function() {
        log.info('POPUP', 'I18n initialized');

        log.info('POPUP', 'Display main view');
        Backbone.Mediator.subscribe('main-settings', function() {
            displayConfig(onLoad, this);
        });

        Backbone.Mediator.subscribe('main-about', function() {

        });

        mainView = new MainView();
        $('#main-elt').append(mainView.render().el);

        // Initialize the extension
        //init();

        // Load the extension
        //onLoad();
    });

});

// Configuration for require.js
require.config({
    baseUrl: "/src",
    paths : {
    	"text": '../vendor/text'
    },
    waitSeconds: 5
});

require(['modules/log', 'modules/acsConnector', 'modules/credentials', 'views/mainView'], function(log, acs, credentials, MainView) {

	var mainView = null;

    var user = null;

	log.info('MAIN', "Application start");

	var lang = navigator.language || navigator.userLanguage || 'en-US';

    log.debug('POPUP', 'Detected language', lang);

    //Moment settings
    moment.locale(lang, {week: {dow: 1, doy: 4}});

    //Language initialization
    i18n.init({ lng: lang}, function() {
        log.info('POPUP', 'I18n initialized');

        // Display main view
        Backbone.Mediator.subscribe('main-settings', function() {
            displayConfig(onLoad, this);
        });

        Backbone.Mediator.subscribe('main-about', function() {

        });

        mainView = new MainView();
        $('#main-elt').append(mainView.render().el);

        // Try to log to the ACS server
        credentials.load(function(user_param) {
            user = user_param;
            log.debug("Main", "User found", user);

            if(user.host.length > 0 && user.login.length > 0 && user.password.length > 0) {
                acs.loginToACS(user.host, user.login, user.password, function() {

                }, function() {

                });
            }
            else {
                Backbone.Mediator.publish('error:popup');
            }
        }, function() {
            Backbone.Mediator.publish('error:popup');
        }, this);

    });

});

Allow to log to ACS


var parse = require('./parse')

/**
 * Returns array of dates withtin specified range.
 * @param {date|string} dirtyStart
 * @param {date|string} dirtyEnd
 * @returns {date[]}
 */
var eachDay = function(dirtyStart, dirtyEnd) {
  var endTime = parse(dirtyEnd).getTime()
  var dates = []
  var tmpDate

  var curDate = parse(dirtyStart)
  curDate.setHours(0, 0, 0, 0)

  while (curDate.getTime() <= endTime) {
    dates.push(new Date(curDate))

    curDate.setDate(curDate.getDate() + 1)

    /**
     * add additional 5 hours to get next day,
     * because of possible troubles with daylight savings dates
     */
    tmpDate = new Date(curDate.setTime(curDate.getTime() + 5 * 60 * 60 * 1000))
    tmpDate = new Date(tmpDate.setHours(0, 0, 0, 0))
    curDate = tmpDate
  }

  return dates
}

module.exports = eachDay

var parse = require('./parse')

/**
 * Returns array of dates withtin specified range.
 * @param {date|string} dirtyStart
 * @param {date|string} dirtyEnd
 * @returns {date[]}
 */
var eachDay = function(dirtyStart, dirtyEnd) {
  var endTime = parse(dirtyEnd).getTime()
  var dates = []

  var currentDate = parse(dirtyStart)
  currentDate.setHours(0, 0, 0, 0)

  while (currentDate.getTime() <= endTime) {
    dates.push(new Date(currentDate))
    currentDate.setDate(currentDate.getDate() + 1)
  }

  return dates
}

module.exports = eachDay

Remove DST fix in `eachDay` function