var ssh2 = require('ssh2');
var SocketIOFileUpload = require('socketio-file-upload');

var completeFileUpload = function(client, sshCredentials) {
  return function(event) {
    var credentials = sshCredentials(client.instance);
    var connection = ssh2();

    connection.on('end', function() {
    });

    connection.on('ready', function() {
      connection.sftp(function(err, sftp) {
        if (err) {
          console.log("There was an error while connecting via sftp: " + err);
        }
        var stream = sftp.createWriteStream(event.file.name);
        stream.write(client.fileUploadBuffer.toString());
        stream.end(function() {
          connection.end();
        });
        client.fileUploadBuffer = "";
      });
    });

    connection.connect(credentials);
  };
};

module.exports = function(logExceptOnTest, sshCredentials) {
  return {
    attachUploadListenerToSocket: function(client, socket) {
      var uploader = new SocketIOFileUpload();
      uploader.listen(socket);

      uploader.on("error", function(event) {
        console.error("Error in upload " + event);
      });

      uploader.on("start", function(event) {
        client.fileUploadBuffer = "";
        logExceptOnTest('File upload ' + event.file.name);
      });

      uploader.on("progress", function(event) {
        client.fileUploadBuffer += event.buffer;
      });

      uploader.on("complete", completeFileUpload(client, sshCredentials));
    }
  };
};

var ssh2 = require('ssh2');
var SocketIOFileUpload = require('socketio-file-upload');

var completeFileUpload = function(client, sshCredentials) {
  return function(event) {
    var credentials = sshCredentials(client.instance);
    var connection = ssh2();

    connection.on('end', function() {
    });

    connection.on('ready', function() {
      connection.sftp(function(err, sftp) {
        if (err) {
          console.log("There was an error while connecting via sftp: " + err);
        }
        var stream = sftp.createWriteStream(event.file.name);
        stream.write(client.fileUploadBuffer);
        stream.end(function() {
          connection.end();
        });
        client.fileUploadBuffer = "";
      });
    });

    connection.connect(credentials);
  };
};

module.exports = function(logExceptOnTest, sshCredentials) {
  return {
    attachUploadListenerToSocket: function(client, socket) {
      var uploader = new SocketIOFileUpload();
      uploader.listen(socket);

      uploader.on("error", function(event) {
        console.error("Error in upload " + event);
      });

      uploader.on("start", function(event) {
        client.fileUploadBuffer = "";
        logExceptOnTest('File upload name:' + event.file.name);
        logExceptOnTest('File upload encoding: ' + event.file.encoding);
      });

      uploader.on("progress", function(event) {
        client.fileUploadBuffer = event.buffer;
      });

      uploader.on("complete", completeFileUpload(client, sshCredentials));
    }
  };
};

Fix bug with uploading octed data like images


'use strict';

var execall = require('regexp.execall');

module.exports = function (text) {
  return execall(/ \w{1,2} [^ ]/g, text).map(function (match) {
    return match.index + match[0].length - 2;
  });
};

'use strict';

var execall = require('regexp.execall');

module.exports = function (text) {
  return execall(/(^|\s\W*)(\w{1,2})(?= [^ ])/g, text)
    .filter(function (match) {
      return !/\d/.test(match[1]);
    })
    .map(function (match) {
      return match.index + match[0].length;
    });
};

Improve regular expression's overlapping and details


var Backbone = require('backbone');

module.exports = Backbone.View.extend({
  events() {
    return (
      this.model.get('events') || {
        mousedown: 'handleClick'
      }
    );
  },

  attributes() {
    return this.model.get('attributes');
  },

  initialize(opts) {
    this.editor = opts.config.editor;
  },

  handleClick(event) {
    event.preventDefault();
    event.stopPropagation();
    this.execCommand(event);
  },

  execCommand(event) {
    const opts = { event };
    const command = this.model.get('command');
    const editor = this.editor;

    if (typeof command === 'function') {
      command(editor, null, opts);
    }

    if (typeof command === 'string') {
      editor.runCommand(command, opts);
    }
  },

  render() {
    var config = this.editor.getConfig();
    this.el.className += ' ' + config.stylePrefix + 'toolbar-item';
    return this;
  }
});

var Backbone = require('backbone');

module.exports = Backbone.View.extend({
  events() {
    return (
      this.model.get('events') || {
        mousedown: 'handleClick'
      }
    );
  },

  attributes() {
    return this.model.get('attributes');
  },

  initialize(opts) {
    this.editor = opts.config.editor;
  },

  handleClick(event) {
    event.preventDefault();
    event.stopPropagation();
    this.execCommand(event);
  },

  execCommand(event) {
    const opts = { event };
    const command = this.model.get('command');
    const editor = this.editor;

    if (typeof command === 'function') {
      command(editor, null, opts);
    }

    if (typeof command === 'string') {
      editor.runCommand(command, opts);
    }
  },

  render() {
    const { editor, $el, model } = this;
    const label = model.get('label');
    const pfx = editor.getConfig('stylePrefix');
    $el.addClass(`${pfx}toolbar-item`);
    label && $el.append(label);
    return this;
  }
});

Add the possibility to append `label` on component toolbar buttons


const AssignedSchedule = require('../../structs/db/AssignedSchedule.js')
const pruneGuilds = require('./pruneGuilds.js')
const pruneFeeds = require('./pruneFeeds.js')
const pruneFormats = require('./pruneFormats.js')
const pruneFailCounters = require('./pruneFailCounters.js')
const pruneSubscribers = require('./pruneSubscribers.js')
const pruneCollections = require('./pruneCollections.js')
const flushRedis = require('./flushRedis.js')

/**
 * @param {Set<string>} guildIds
 * @param {import('discord.js').Client} bot
 */
async function prunePreInit (guildIds, bot) {
  await Promise.all([
    AssignedSchedule.deleteAll(),
    flushRedis(),
    pruneGuilds(guildIds)
  ])
  await pruneFeeds(guildIds)
  await Promise.all([
    pruneFormats(),
    pruneFailCounters()
  ])
  if (bot) {
    await pruneSubscribers(bot)
  }
  // Prune collections should not be called here until schedules were assigned
}

async function prunePostInit () {
  await pruneCollections()
}

module.exports = {
  flushRedis,
  prunePreInit,
  prunePostInit,
  pruneGuilds,
  pruneFeeds,
  pruneFormats,
  pruneFailCounters,
  pruneSubscribers,
  pruneCollections
}

const AssignedSchedule = require('../../structs/db/AssignedSchedule.js')
const pruneGuilds = require('./pruneGuilds.js')
const pruneFeeds = require('./pruneFeeds.js')
const pruneFormats = require('./pruneFormats.js')
const pruneFailCounters = require('./pruneFailCounters.js')
const pruneSubscribers = require('./pruneSubscribers.js')
const pruneCollections = require('./pruneCollections.js')
const flushRedis = require('./flushRedis.js')
const checkLimits = require('./checkLimits.js')
const checkPermissions = require('./checkPermissions.js')

/**
 * @param {Set<string>} guildIds
 * @param {import('discord.js').Client} bot
 */
async function prunePreInit (guildIds, bot) {
  await Promise.all([
    AssignedSchedule.deleteAll(),
    flushRedis(),
    pruneGuilds(guildIds)
  ])
  await pruneFeeds(guildIds)
  await Promise.all([
    pruneFormats(),
    pruneFailCounters()
  ])
  if (bot) {
    await pruneSubscribers(bot)
  }
  // Prune collections should not be called here until schedules were assigned
}

async function prunePostInit () {
  await pruneCollections()
}

module.exports = {
  flushRedis,
  prunePreInit,
  prunePostInit,
  pruneGuilds,
  pruneFeeds,
  pruneFormats,
  pruneFailCounters,
  pruneSubscribers,
  pruneCollections,
  checkLimits,
  checkPermissions
}

Add missing funcs to util maintenance


import React from 'react';
import PropTypes from 'prop-types';
import Checklist from 'parser/shared/modules/features/Checklist2';
import Rule from 'parser/shared/modules/features/Checklist2/Rule';
import Requirement from 'parser/shared/modules/features/Checklist2/Requirement';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

const DowntimeDescription = () => (
  <>
    You should try to avoid doing nothing during the fight. If you have to move, try casting something instant with range like <SpellLink id={SPELLS.FLAMETONGUE.id} /> or <SpellLink id={SPELLS.ROCKBITER.id} />
  </>
);

class EnhancementShamanChecklist extends React.PureComponent {
  static propTypes = {
    castEfficiency: PropTypes.object.isRequired,
    combatant: PropTypes.shape({
      hasTalent: PropTypes.func.isRequired,
      hasTrinket: PropTypes.func.isRequired,
    }).isRequired,
    thresholds: PropTypes.object.isRequired,
  };

  render() {
    const { combatant, castEfficiency, thresholds } = this.props;

    return (
      <Checklist>
        <Rule
          name="Always be casting"
          description={DowntimeDescription}
        >
          <Requirement name="Downtime" thresholds={thresholds.alwaysBeCasting} />
        </Rule>
      </Checklist>
    );
  }
}

export default EnhancementShamanChecklist;

import React from 'react';
import PropTypes from 'prop-types';
import Checklist from 'parser/shared/modules/features/Checklist2';
import Rule from 'parser/shared/modules/features/Checklist2/Rule';
import Requirement from 'parser/shared/modules/features/Checklist2/Requirement';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';

const DowntimeDescription = () => (
  <>
    You should try to avoid doing nothing during the fight. If you have to move, try casting something instant with range like <SpellLink id={SPELLS.FLAMETONGUE.id} /> or <SpellLink id={SPELLS.ROCKBITER.id} />
  </>
);

class EnhancementShamanChecklist extends React.PureComponent {
  static propTypes = {
    castEfficiency: PropTypes.object.isRequired,
    combatant: PropTypes.shape({
      hasTalent: PropTypes.func.isRequired,
      hasTrinket: PropTypes.func.isRequired,
    }).isRequired,
    thresholds: PropTypes.object.isRequired,
  };

  render() {
    const { combatant, castEfficiency, thresholds } = this.props;

    return (
      <Checklist>
        <Rule
          name="Always be casting"
          description={<DowntimeDescription />}
        >
          <Requirement name="Downtime" thresholds={thresholds.alwaysBeCasting} />
        </Rule>
      </Checklist>
    );
  }
}

export default EnhancementShamanChecklist;

Fix DowntimeDescription not showing on checklist


#!/usr/bin/env node

/*
* Copyright 2013 Eduardo Lundgren, All Rights Reserved.
*
* Code licensed under the BSD License:
* https://github.com/eduardolundgren/blob/master/LICENSE.md
*
* @author Eduardo Lundgren <eduardolundgren@gmail.com>
*/

var async = require('async'),
    fs = require('fs'),
    nopt = require('nopt'),
    base = require('../lib/base'),
    git = require('../lib/git'),
    commandFilePath,
    commandImpl,
    logger,
    operations,
    options,
    parsed,
    remain;

logger = base.logger;
operations = [];
parsed = nopt(process.argv),
remain = parsed.argv.remain;

if (!remain.length) {
    logger.oops('usage: gh [command] [payload] [--flags]');
}

commandFilePath = __dirname + '/../lib/cmds/' + remain[0] + '.js';

if (fs.existsSync(commandFilePath)) {
    commandImpl = require(commandFilePath).Impl;

    options = nopt(
        commandImpl.DETAILS.options,
        commandImpl.DETAILS.shorthands, process.argv, 2);

    operations.push(git.getRepositoryName);
    operations.push(git.getCurrentBranch);

    async.parallel(operations, function(err, results) {
        new commandImpl().run(options, results[0], results[1]);
    });
}
else {
    logger.oops('command not found');
}

#!/usr/bin/env node

/*
* Copyright 2013 Eduardo Lundgren, All Rights Reserved.
*
* Code licensed under the BSD License:
* https://github.com/eduardolundgren/blob/master/LICENSE.md
*
* @author Eduardo Lundgren <eduardolundgren@gmail.com>
*/

var async = require('async'),
    fs = require('fs'),
    nopt = require('nopt'),
    base = require('../lib/base'),
    git = require('../lib/git'),
    commandFilePath,
    commandImpl,
    logger,
    operations,
    options,
    parsed,
    remain;

logger = base.logger;
operations = [];
parsed = nopt(process.argv),
remain = parsed.argv.remain;

if (!remain.length) {
    logger.oops('usage: gh [command] [payload] [--flags]');
}

commandFilePath = __dirname + '/../lib/cmds/' + remain[0] + '.js';

if (fs.existsSync(commandFilePath)) {
    commandImpl = require(commandFilePath).Impl;

    options = nopt(
        commandImpl.DETAILS.options,
        commandImpl.DETAILS.shorthands, process.argv, 2);

    operations.push(git.getRepositoryName);
    operations.push(git.getCurrentBranch);

    async.parallel(operations, function(err, results) {
        new commandImpl(options, results[0], results[1]).run();
    });
}
else {
    logger.oops('command not found');
}

Change constructor to receive (options, repo, branch)


'use strict';

var express = require('express');
var controller = require('./conversion.controller');

var router = express.Router();

router.post('/import', controller.import);
router.post('/export/:format(cml,pdb,mol,mol2,smiles,hin)', controller.export);

module.exports = router;

'use strict';

var express = require('express');
var controller = require('./conversion.controller');

var router = express.Router();

router.post('/import', controller.import);
router.post('/export/:format(cml|pdb|mol|mol2|smiles|hin)', controller.export);

module.exports = router;

Fix pattern for export formats


$(".container").addClass("tk-proxima-nova");
$(".page-header").addClass("tk-league-gothic");
$("td:nth-child(2)").each(function() {
  $(this).text(moment($.trim($(this).text()), "DD-MMM-YYYY HH:mm").fromNow());
});

Change how Typekit fonts are applied.


Add chapter 9, exercise 3


async function locateScalpel(nest) {
    let curNest = nest.name;
    for (;;) {
        let scalpelLoc = await anyStorage(nest, curNest, "scalpel");
        if (scalpelLoc == curNest) return curNest;
        curNest = scalpelLoc;
    }
}

function locateScalpel2(nest) {
    let next = nest.name;
    function getNext(next) {
        return anyStorage(nest, next, "scalpel")
            .then(val => val == next ? next : getNext(val));
    }
    return getNext(next);
}

locateScalpel(bigOak).then(console.log);
// → Butcher's Shop
locateScalpel2(bigOak).then(console.log);
// → Butcher's Shop

async function locateScalpel(nest) {
	let current = nest.name;

	for (;;) {
		let next = await anyStorage(nest, current, "scalpel");
		if (next == current) {
			return next;
		}

		current = next;
	}
}

function locateScalpel2(nest) {
	function next(current) {
		return anyStorage(nest, current, "scalpel")
			.then(value => value == current ? value : next(value));
	}

	return next(nest.name);
}

Add chapter 11, exercise 1


import Ember from 'ember';
import layout from '../templates/components/simple-table-cell';

export default Ember.Component.extend({
  layout,
  tagName: 'th',

  classNameBindings: ['columnsClass'],
  attributeBindings: ['rowspan', 'colspan', 'columnsStyle:style'],

  columnsClass: Ember.computed.alias('columns.classes'),
  columnsStyle: Ember.computed('columns.style', {
    get() {
      return Ember.String.htmlSafe(this.get('columns.style'));
    }
  }),

  actions: {
    sortBy(key) {
      return this.get('sortAction')(key);
    }
  }

});

import Ember from 'ember';
import layout from '../templates/components/simple-table-cell';

export default Ember.Component.extend({
  layout,
  tagName: 'th',

  classNameBindings: ['columnsClass'],
  attributeBindings: ['rowspan', 'colspan', 'columnsStyle:style'],

  columnsClass: Ember.computed('columns.classes', {
    get() {
      let columns = this.get('columns');
      let column = Ember.isArray(columns) ? columns[0] : columns;
      return Ember.String.htmlSafe(column.classes);
    }
  }),
  columnsStyle: Ember.computed('columns.style', {
    get() {
      let columns = this.get('columns');
      let column = Ember.isArray(columns) ? columns[0] : columns;
      return Ember.String.htmlSafe(column.style);
    }
  }),

  actions: {
    sortBy(key) {
      return this.get('sortAction')(key);
    }
  }

});

Fix styles and classes for grouped columns


'use strict';

var fetch = require('node-fetch');
var URL = require('url').URL;

function wrapFetchForNode(fetch) {
  // Support schemaless URIs on the server for parity with the browser.
  // https://github.com/matthew-andrews/isomorphic-fetch/pull/10
  return function (u, options) {
    if (u instanceof URL) {
      return fetch(u, options);
    }

    if (u instanceof fetch.Request) {
      return fetch(u, options);
    }

    var url = u.slice(0, 2) === '//' ? 'https:' + u : u;

    return fetch(url, options);
  };
}

module.exports = function (context) {
  // This modifies the global `node-fetch` object, which isn't great, since
  // different callers to `fetch-ponyfill` which pass a different Promise
  // implementation would each expect to have their implementation used. But,
  // given the way `node-fetch` is implemented, this is the only way to make
  // it work at all.
  if (context && context.Promise) {
    fetch.Promise = context.Promise;
  }

  return {
    fetch: wrapFetchForNode(fetch),
    Headers: fetch.Headers,
    Request: fetch.Request,
    Response: fetch.Response
  };
};

'use strict';

var fetch = require('node-fetch');
var URL = require('url').URL;

function wrapFetchForNode(fetch) {
  // Support schemaless URIs on the server for parity with the browser.
  // https://github.com/matthew-andrews/isomorphic-fetch/pull/10
  return function (u, options) {
    if (typeof u === 'string' && u.slice(0, 2) === '//') {
      return fetch('https:' + u, options);
    }

    return fetch(u, options);
  };
}

module.exports = function (context) {
  // This modifies the global `node-fetch` object, which isn't great, since
  // different callers to `fetch-ponyfill` which pass a different Promise
  // implementation would each expect to have their implementation used. But,
  // given the way `node-fetch` is implemented, this is the only way to make
  // it work at all.
  if (context && context.Promise) {
    fetch.Promise = context.Promise;
  }

  return {
    fetch: wrapFetchForNode(fetch),
    Headers: fetch.Headers,
    Request: fetch.Request,
    Response: fetch.Response
  };
};

Change to treat the string as a special case


'use strict';
import { t } from '../utils/i18n';

export const fileTypesMatrix = {
  'admin-bounds': {
    display: t('Administrative Boundaries'),
    description: t('A GeoJSON containing polygons with the administrative boundaries.')
  },
  origins: {
    display: t('Population data'),
    description: t('A GeoJSON with population point data. This will be used as the Origin in the analysis.')
  },
  poi: {
    display: t('Points of Interest'),
    description: t('GeoJSON for the Points of Interest (eg. banks or hospitals). These are the destinations in the analysis.')
  },
  'road-network': {
    display: t('Road Network'),
    description: t('The underlying road network in OSM XML format.')
  },
  profile: {
    display: t('Profile'),
    description: t('A lua file with the OSRM Profile.')
  }
};

'use strict';
import { t } from '../utils/i18n';

export const fileTypesMatrix = {
  'admin-bounds': {
    display: t('Administrative Boundaries'),
    description: t('A GeoJSON containing polygons with the administrative boundaries.'),
    helpPath: '/help#administrative-boundaries'
  },
  origins: {
    display: t('Population data'),
    description: t('A GeoJSON with population point data. This will be used as the Origin in the analysis.'),
    helpPath: '/help#population-data'
  },
  poi: {
    display: t('Points of Interest'),
    description: t('GeoJSON for the Points of Interest (eg. banks or hospitals). These are the destinations in the analysis.'),
    helpPath: '/help#points-of-interest'
  },
  'road-network': {
    display: t('Road Network'),
    description: t('The underlying road network in OSM XML format.'),
    helpPath: '/help#road-network'
  },
  profile: {
    display: t('Profile'),
    description: t('A lua file with the OSRM Profile.'),
    helpPath: '/help#profile'
  }
};

Add help path to poi data


import assert from 'assert';

import series from './series';
import * as TYPES from '../../actions/types';

describe('A series reducer for redux state', () => {

	//Test states
	const seriesDefaultState = {};
	const seriesWithTicker = { GOOGL: [] };
	//Test actions
	const testAddSeries = {
		type: TYPES.ADD_SERIES,
		ticker: 'GOOGL',
		data: [],
	}
	const testInvalidAction = {
		type: 'INVALID_ACTION',
	}

	it('should return a default state when no arguments are supplied', () => {
		assert.deepEqual(series(), seriesDefaultState );
	})

	it('should add an empty series attribute if it is missing from the supplied state', () => {
		assert.deepEqual(series({}), seriesDefaultState);
	})

	it('should return an unchanged state when no actions are supplied', () => {
		assert.deepEqual(series(seriesDefaultState), seriesDefaultState);
	})

	it('should return an unchanged state when an invalid action is supplied', () => {
		assert.deepEqual(series(seriesDefaultState, testInvalidAction), seriesDefaultState);
	})

	it('should return a new state based on a passed action', () => {
		assert.deepEqual(series(seriesDefaultState, testAddSeries), seriesWithTicker);
	})

	it('should return a new object', () => {
		assert(series({}) !== {});
	})
})

import assert from 'assert';

import series from './series';
import * as TYPES from '../../actions/types';

describe('A series reducer for redux state', () => {

	//Test states
	const seriesDefaultState = {};
	const seriesWithTicker = { GOOGL: [] };
	//Test actions
	const testAddSeries = {
		type: TYPES.ADD_SERIES,
		ticker: 'GOOGL',
		data: [],
	}
	const testDelSeries = {
		type: TYPES.DEL_SERIES,
		ticker: 'GOOGL',
	}
	const testInvalidAction = {
		type: 'INVALID_ACTION',
	}

	it('should return a default state when no arguments are supplied', () => {
		assert.deepEqual(series(), seriesDefaultState );
	})

	it('should add an empty series attribute if it is missing from the supplied state', () => {
		assert.deepEqual(series({}), seriesDefaultState);
	})

	it('should return an unchanged state when no actions are supplied', () => {
		assert.deepEqual(series(seriesDefaultState), seriesDefaultState);
	})

	it('should return an unchanged state when an invalid action is supplied', () => {
		assert.deepEqual(series(seriesDefaultState, testInvalidAction), seriesDefaultState);
	})

	it('should return a new state based on a ADD_SERIES action', () => {
		assert.deepEqual(series(seriesDefaultState, testAddSeries), seriesWithTicker);
	})

	it('should return a new state based on a DEL_SERIES action', () => {
		assert.deepEqual(series(seriesDefaultState, testDelSeries), seriesDefaultState);
	})

	it('should return a new object', () => {
		assert(series({}) !== {});
	})
})

Test coverage for DEL_SERIES action


/* eslint-env jest */

const { exec } = require('child_process')
const request = require('supertest')
let app

// start a mongo instance loaded with test data using docker
beforeAll(async () => {
  await exec('docker run --name scraper_test_db -d --rm -p 27017:27017 mongo')
  await exec('docker cp dump/ scraper_test_db:/dump')
  await exec('docker exec scraper_test_db mongorestore /dump')
  app = await require('../app')
  await twoSeconds()
})

afterAll(done => exec('docker kill scraper_test_db', done))

describe('courses endpoint', () => {
  test('/courses succeeds', async () => {
    const response = await request(app).get('/courses')
    expect(response.type).toBe('application/json')
    expect(response.statusCode).toBe(200)
  })

  test('/courses returns an Array', async () => {
    const response = await request(app).get('/courses')
    expect(Array.isArray(response.body)).toBeTruthy()
  })
})

function twoSeconds () {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, 2000)
  })
}

/* eslint-env jest */

const util = require('util')
const exec = util.promisify(require('child_process').exec)
const request = require('supertest')
let app

// start a mongo instance loaded with test data using docker
beforeAll(async () => {
  await exec('docker run --name scraper_test_db -d --rm -p 27017:27017 mongo')
  await exec('docker cp __tests__/dump/ scraper_test_db:/dump')
  await exec('docker exec scraper_test_db mongorestore /dump')
  app = require('../app')
  await twoSeconds()
})

afterAll(done => exec('docker kill scraper_test_db', done))

describe('courses endpoint', () => {
  test('/courses succeeds', async () => {
    const response = await request(app).get('/courses')
    expect(response.type).toBe('application/json')
    expect(response.statusCode).toBe(200)
  })

  test('/courses returns an Array', async () => {
    const response = await request(app).get('/courses')
    expect(Array.isArray(response.body)).toBeTruthy()
  })
})

function twoSeconds () {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve()
    }, 2000)
  })
}

Test db should have data now


'use strict';

const fs = require('fs');
const GetEmail = require('../../lib/install/get-email');

describe('GetEmail', () => {
  let step;

  beforeEach(() => {
    step = new GetEmail();
  });

  describe('.start()', () => {
    describe('when the user has a .gitconfig file', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andReturn(`[user]
	email = some.email@company.com`);
      });

      it('returns a promise that is resolved with the user email', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data.email).toEqual('some.email@company.com');
        }));
      });
    });

    describe('when the user has a .gitconfig file without an email', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andReturn('');
      });

      it('returns a promise that is resolved with null', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data).toEqual({email: undefined});
        }));
      });
    });

    describe('when the user has no .gitconfig file', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andCallFake(() => { throw new Error(); });
      });

      it('returns a promise that is resolved with null', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data).toEqual({email: undefined});
        }));
      });
    });
  });
});

'use strict';

const fs = require('fs');
const GetEmail = require('../../lib/install/get-email');

describe('GetEmail', () => {
  let step;

  beforeEach(() => {
    step = new GetEmail();
  });

  describe('.start()', () => {
    afterEach(() => {
      fs.readFileSync.andCallThrough();
    });

    describe('when the user has a .gitconfig file', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andReturn(`[user]
	email = some.email@company.com`);
      });

      it('returns a promise that is resolved with the user email', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data.email).toEqual('some.email@company.com');
        }));
      });
    });

    describe('when the user has a .gitconfig file without an email', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andReturn('');
      });

      it('returns a promise that is resolved with null', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data).toEqual({email: undefined});
        }));
      });
    });

    describe('when the user has no .gitconfig file', () => {
      beforeEach(() => {
        spyOn(fs, 'readFileSync').andCallFake(() => { throw new Error(); });
      });

      it('returns a promise that is resolved with null', () => {
        waitsForPromise(() => step.start().then(data => {
          expect(data).toEqual({email: undefined});
        }));
      });
    });
  });
});

Fix tests relying on fs spies


const fromArray = array => createObservable((push, end) => {
  array.forEach(value => { push(value); });
  end();
});

const fromArray = array => createObservable((push, end) => {
  if (array == null) {
    array = [];
  }
  array.forEach(value => { push(value); });
  end();
});

Allow observable creation with no argument


var interfaceConfig = {
    CANVAS_EXTRA: 104,
    CANVAS_RADIUS: 7,
    SHADOW_COLOR: '#00ccff',
    INITIAL_TOOLBAR_TIMEOUT: 20000,
    TOOLBAR_TIMEOUT: 4000,
    DEFAULT_REMOTE_DISPLAY_NAME: "Fellow Jitster",
    DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME: "Speaker",
    SHOW_JITSI_WATERMARK: true,
    JITSI_WATERMARK_LINK: "http://jitsi.org",
    SHOW_BRAND_WATERMARK: false,
    BRAND_WATERMARK_LINK: "",
    SHOW_POWERED_BY: false,
    GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true
};

var interfaceConfig = {
    CANVAS_EXTRA: 104,
    CANVAS_RADIUS: 7,
    SHADOW_COLOR: '#ffffff',
    INITIAL_TOOLBAR_TIMEOUT: 20000,
    TOOLBAR_TIMEOUT: 4000,
    DEFAULT_REMOTE_DISPLAY_NAME: "Fellow Jitster",
    DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME: "Speaker",
    SHOW_JITSI_WATERMARK: true,
    JITSI_WATERMARK_LINK: "http://jitsi.org",
    SHOW_BRAND_WATERMARK: false,
    BRAND_WATERMARK_LINK: "",
    SHOW_POWERED_BY: false,
    GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true
};

Change the color of the audio levels indicator.


'use babel'

import fs from 'fs-plus'
import path from 'path'

export default class BuilderRegistry {
  getBuilder (filePath) {
    const builders = this.getAllBuilders()
    const candidates = builders.filter((builder) => builder.canProcess(filePath))
    switch (candidates.length) {
      case 0: return null
      case 1: return candidates[0]
    }

    return this.resolveAmbigiousBuilders(candidates)
  }

  getAllBuilders () {
    const moduleDir = path.join(__dirname, 'builders')
    const entries = fs.readdirSync(moduleDir)
    const builders = entries.map((entry) => require(path.join(moduleDir, entry)))

    return builders
  }

  resolveAmbigiousBuilders (builders) {
    const names = builders.map((builder) => builder.name)
    const indexOfLatexmk = names.indexOf('LatexmkBuilder')
    const indexOfTexify = names.indexOf('TexifyBuilder')
    if (names.length === 2 && indexOfLatexmk >= 0 && indexOfTexify >= 0) {
      switch (atom.config.get('latex.builder')) {
        case 'latexmk': return builders[indexOfLatexmk]
        case 'texify': return builders[indexOfTexify]
      }
    }

    throw Error('Unable to resolve ambigous builder registration')
  }
}

'use babel'

import _ from 'lodash'
import fs from 'fs-plus'
import path from 'path'
import MagicParser from './parsers/magic-parser'

export default class BuilderRegistry {
  getBuilder (filePath) {
    const builders = this.getAllBuilders()
    const candidates = builders.filter((builder) => builder.canProcess(filePath))
    switch (candidates.length) {
      case 0: return null
      case 1: return candidates[0]
    }

    return this.resolveAmbiguousBuilders(candidates, this.getBuilderFromMagic(filePath))
  }

  getBuilderFromMagic (filePath) {
    const magic = new MagicParser(filePath).parse()
    if (magic && magic.builder) {
      return magic.builder
    }

    return null
  }

  getAllBuilders () {
    const moduleDir = path.join(__dirname, 'builders')
    const entries = fs.readdirSync(moduleDir)
    const builders = entries.map((entry) => require(path.join(moduleDir, entry)))

    return builders
  }

  resolveAmbiguousBuilders (builders, builderOverride) {
    const name = builderOverride || atom.config.get('latex.builder')
    const namePattern = new RegExp(`^${name}Builder$`, 'i')
    const builder = _.find(builders, builder => builder.name.match(namePattern))

    if (builder) return builder

    latex.log.warning(`Unable to resolve builder named ${name} using fallback ${builders[0].name}.`)
    return builders[0]
  }
}

Use TeX magic to allow bulder override


/*
 * DomainScope module
 * @lastmodifiedBy ChengFuLin(chengfulin0806@gmail.com)
 * @lastmodifiedDate 2015-07-28
 */
var Scope = require('./scope');

/**
 * DomainScope
 * @param {Object} cfg Graph of the domain scope
 * @constructor
 */
function DomainScope(cfg) {
	"use strict";
	Scope.call(this, cfg, Scope.DOMAIN_SCOPE_NAME, Scope.DOMAIN_TYPE, null);
}

DomainScope.prototype = Object.create(Scope.prototype);
Object.defineProperty(DomainScope.prototype, 'constructor', {
	value: DomainScope
});

/* start-public-data-members */
Object.defineProperties(DomainScope.prototype, {
	/**
	 * Array of build-in objects
	 * @type {Array}
	 * @memberof DomainScope.prototype
	 * @inheritdoc
	 */
	builtInObjects: {
		get: function () {
			"use strict";
			/* manual */
			return [
				{name: "localStorage", def: "localStorage"}
			];
		}
	}
});
/* end-public-data-members */

module.exports = DomainScope;

/*
 * DomainScope module
 * @lastmodifiedBy ChengFuLin(chengfulin0806@gmail.com)
 * @lastmodifiedDate 2015-07-28
 */
var Scope = require('./scope');

/**
 * DomainScope
 * @constructor
 */
function DomainScope() {
	"use strict";
	Scope.call(this, null, Scope.DOMAIN_SCOPE_NAME, Scope.DOMAIN_TYPE, null);
}

DomainScope.prototype = Object.create(Scope.prototype);
Object.defineProperty(DomainScope.prototype, 'constructor', {
	value: DomainScope
});

/* start-public-data-members */
Object.defineProperties(DomainScope.prototype, {
	/**
	 * Array of build-in objects
	 * @type {Array}
	 * @memberof DomainScope.prototype
	 * @inheritdoc
	 */
	builtInObjects: {
		get: function () {
			"use strict";
			/* manual */
			return [
				{name: "localStorage", def: "localStorage"}
			];
		}
	}
});
/* end-public-data-members */

module.exports = DomainScope;

Refactor to be constructed without parameters


'use strict';

var generate = require('../generate');

exports.endgames = [
    'nefarious purposes',
    'a Washington takeover',
    'a terrorist plot',
    'world domination',
    'a plot against the queen'
];

exports.get = generate.random(exports.endgames);

'use strict';

var generate = require('../generate');

exports.endgames = [
    'a plot against the queen',
    'a terrorist plot',
    'a Washington takeover',
    'nefarious purposes',
    'world domination',
];

exports.get = generate.random(exports.endgames);

Make sorting and trailing commas consistent


/**
 * Created by Alex on 29/01/2017.
 */
 import Hapi from 'hapi';

 export class Knekt {
 	constructor() {
 		this._server = new Hapi.Server();
 		this.plugins = [];
 	}

 	setConnection(host, port) {
 		this._server.connection({host: host, port: port});
 	}

 	listen() {
 		this._server.start((err) => {
 			if (err) {
 				throw err;
 			}
 			console.log(`Knekt running at ${this._server.info.uri} !`);
 		});
 	}

 	register(plugins) {
 		plugins.forEach((plugin) => {
 			this.plugins.push(new plugin(this));
 		});
 	}

 	addRoute(parameters) {
 		this._server.route(parameters);
 	}
 }

/**
 * Created by Alex on 29/01/2017.
 */
 import Hapi from 'hapi';

 //core routes
 import Descriptor from './routes/descriptor';

 export class Knekt {
 	constructor() {
 		this._server = new Hapi.Server();
 		this.plugins = [];
 	}

 	setConnection(host, port) {
 		this._server.connection({host: host, port: port});
 	}

 	listen() {
 		this._server.start((err) => {
 			if (err) {
 				throw err;
 			}
 			this._registerCoreRoutes();
 			console.log(`Knekt running at ${this._server.info.uri} !`);
 		});
 	}

 	register(plugins) {
 		plugins.forEach((plugin) => {
 			this.plugins.push(new plugin(this).properties);
 		});
 	}

 	addRoute(parameters) {
 		this._server.route(parameters);
 	}

 	_registerCoreRoutes() {
 		this._server.route(Descriptor(this));
 	}
 }

Update to register descriptor route


import React from 'react';
import PropTypes from 'prop-types';
import { useTranslator } from '@u-wave/react-translate';
import CircularProgress from '@material-ui/core/CircularProgress';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import ShuffleIcon from '@material-ui/icons/Shuffle';

const { useCallback, useState } = React;
const HARDCODED_LOADING_SIZE = 24; // FIXME derive this from some mui property?

function ShuffleButton({ onShuffle }) {
  const [isLoading, setLoading] = useState(false);
  const { t } = useTranslator();

  const onClick = useCallback(() => {
    setLoading(true);
    onShuffle().finally(() => {
      setTimeout(() => {
        setLoading(false);
      }, 10000);
    });
  }, [onShuffle]);

  return (
    <Tooltip title={t('playlists.shuffle')} placement="top">
      <IconButton
        className="PlaylistMeta-iconButton"
        onClick={onClick}
      >
        {isLoading ? (
          <CircularProgress size={HARDCODED_LOADING_SIZE} />
        ) : (
          <ShuffleIcon />
        )}
      </IconButton>
    </Tooltip>
  );
}

ShuffleButton.propTypes = {
  onShuffle: PropTypes.func.isRequired,
};

export default ShuffleButton;

import React from 'react';
import PropTypes from 'prop-types';
import { useTranslator } from '@u-wave/react-translate';
import CircularProgress from '@material-ui/core/CircularProgress';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import ShuffleIcon from '@material-ui/icons/Shuffle';

const { useCallback, useState } = React;
const HARDCODED_LOADING_SIZE = 24; // FIXME derive this from some mui property?

function ShuffleButton({ onShuffle }) {
  const [isLoading, setLoading] = useState(false);
  const { t } = useTranslator();

  const onClick = useCallback(() => {
    setLoading(true);
    onShuffle().finally(() => {
      setLoading(false);
    });
  }, [onShuffle]);

  return (
    <Tooltip title={t('playlists.shuffle')} placement="top">
      <IconButton
        className="PlaylistMeta-iconButton"
        onClick={onClick}
      >
        {isLoading ? (
          <CircularProgress size={HARDCODED_LOADING_SIZE} />
        ) : (
          <ShuffleIcon />
        )}
      </IconButton>
    </Tooltip>
  );
}

ShuffleButton.propTypes = {
  onShuffle: PropTypes.func.isRequired,
};

export default ShuffleButton;

Remove setTimeout left over from debugging.


import React, { Component, PropTypes } from 'react';
import Relay from 'react-relay';
import TeamRoute from './TeamRoute';
import Can from '../components/Can';
import CheckContext from '../CheckContext';
import { teamSubdomain } from '../helpers';

class TeamMenu extends Component {
  render() {
    const { team } = this.props;
    const history = new CheckContext(this).getContextStore().history;

    return (
      <Can permissions={team.permissions} permission="update Team">

Manage team...

      </Can>
    );
  }
}

TeamMenu.contextTypes = {
  store: React.PropTypes.object
};

const TeamMenuContainer = Relay.createContainer(TeamMenu, {
  fragments: {
    team: () => Relay.QL`
      fragment on Team {
        id,
        dbid,
        name,
        permissions,
      }
    `
  },
});

class TeamMenuRelay extends Component {
  render() {
    if (teamSubdomain()) {
      const route = new TeamRoute({ teamId: '' });
      return (<Relay.RootContainer Component={TeamMenuContainer} route={route} />);
    } else {
      return null;
    }
  }
}

export default TeamMenuRelay;

import React, { Component, PropTypes } from 'react';
import Relay from 'react-relay';
import TeamRoute from './TeamRoute';
import Can from '../components/Can';
import CheckContext from '../CheckContext';
import { teamSubdomain } from '../helpers';

class TeamMenu extends Component {
  render() {
    const { team } = this.props;
    const history = new CheckContext(this).getContextStore().history;

    return (
      <Can permissions={team.permissions} permission="update Team">

Manage team

      </Can>
    );
  }
}

TeamMenu.contextTypes = {
  store: React.PropTypes.object
};

const TeamMenuContainer = Relay.createContainer(TeamMenu, {
  fragments: {
    team: () => Relay.QL`
      fragment on Team {
        id,
        dbid,
        name,
        permissions,
      }
    `
  },
});

class TeamMenuRelay extends Component {
  render() {
    if (teamSubdomain()) {
      const route = new TeamRoute({ teamId: '' });
      return (<Relay.RootContainer Component={TeamMenuContainer} route={route} />);
    } else {
      return null;
    }
  }
}

export default TeamMenuRelay;

Update copy for team management link


var CodeMirror = require('codemirror')
  , bindCodemirror = require('gulf-codemirror')

module.exports = setup
module.exports.consumes = ['editor']
module.exports.provides = []
function setup(plugin, imports, register) {
  var editor = imports.editor

  editor.registerEditor('CodeMirror', 'text', 'An extensible and performant code editor'
  , function(editorEl) {
    var cm = CodeMirror(function(el) {
      editorEl.appendChild(el)

      el.style['height'] = '100%'
      el.style['width'] = '100%'
    })

    editorEl.style['height'] = '100%'

    return bindCodemirror(cm)
  })
  register()
}

var CodeMirror = require('codemirror')
  , bindCodemirror = require('gulf-codemirror')

module.exports = setup
module.exports.consumes = ['editor']
module.exports.provides = []
function setup(plugin, imports, register) {
  var editor = imports.editor

  editor.registerEditor('CodeMirror', 'text', 'An extensible and performant code editor'
  , function(editorEl) {
    var cm = CodeMirror(function(el) {
      editorEl.appendChild(el)

      el.style['height'] = '100%'
      el.style['width'] = '100%'
    })

    editorEl.style['height'] = '100%'

    return Promise.resolve(bindCodemirror(cm))
  })
  register()
}

Fix setup function: Should return promise


define(['Modernizr', 'createElement', 'docElement'], function( Modernizr, createElement, docElement ) {
    // http://www.w3.org/TR/css3-exclusions
    // http://www.w3.org/TR/css3-exclusions/#shapes
    // Examples: http://html.adobe.com/webstandards/cssexclusions
    // Separate test for CSS shapes as WebKit has just implemented this alone
    Modernizr.addTest('shapes', function () {
        var prefixedProperty = Modernizr.prefixed('shapeInside');

        if (!prefixedProperty)
            return false;

        var shapeInsideProperty = prefixedProperty.replace(/([A-Z])/g, function (str, m1) { return '-' + m1.toLowerCase(); }).replace(/^ms-/, '-ms-');

        return Modernizr.testStyles('#modernizr { ' + shapeInsideProperty + ':rectangle(0,0,0,0) }', function (elem) {
            // Check against computed value
            var styleObj = window.getComputedStyle ? getComputedStyle(elem, null) : elem.currentStyle;
            return styleObj[prefixed('shapeInside', docElement.style, false)] == 'rectangle(0px, 0px, 0px, 0px)';
        });
    });
});

define(['Modernizr', 'createElement', 'docElement', 'prefixed', 'testStyles'], function( Modernizr, createElement, docElement, prefixed, testStyles ) {
    // http://www.w3.org/TR/css3-exclusions
    // http://www.w3.org/TR/css3-exclusions/#shapes
    // Examples: http://html.adobe.com/webstandards/cssexclusions
    // Separate test for CSS shapes as WebKit has just implemented this alone
    Modernizr.addTest('shapes', function () {
        var prefixedProperty = prefixed('shapeInside');

        if (!prefixedProperty)
            return false;

        var shapeInsideProperty = prefixedProperty.replace(/([A-Z])/g, function (str, m1) { return '-' + m1.toLowerCase(); }).replace(/^ms-/, '-ms-');

        return testStyles('#modernizr { ' + shapeInsideProperty + ':rectangle(0,0,0,0) }', function (elem) {
            // Check against computed value
            var styleObj = window.getComputedStyle ? getComputedStyle(elem, null) : elem.currentStyle;
            return styleObj[prefixed('shapeInside', docElement.style, false)] == 'rectangle(0px, 0px, 0px, 0px)';
        });
    });
});

Add testStyles and prefixed to the depedencies.


import 'footable';

$(() => {
  $('.participants').footable({
    sorting: {
      enabled: true,
    },
  });
});

import 'footable';

$(() => {
  $('.participants').footable({
    paginate: false,
    sorting: true,
  });
});

Disable pagination in rsvp page


App.populator('feed', function (page, data) {
  $(page).find('.app-title').text(data.title);
  $.getJSON('/data/feed/' + data.id + '/articles', function (articles) {
    articles.forEach(function (article) {
      var li = $('
' + article.title + '
');
      li.on('click', function () {
        App.load('article', article);
      });
      $(page).find('.app-list').append(li);
    });
  });
});

App.populator('article', function (page, data) {
  $(page).find('.app-title').text(data.title.substring(0, 20));
  $.getJSON('/data/article/' + data.url, function (article) {
    var content =
    '
' + article.title + '
' +
    '' + article.url + '
' +
    article.content;
    $(page).find('.app-content').html(content);
  });
});

App.populator('feed', function (page, data) {
  $(page).find('.app-title').text(data.title);
  $.getJSON('/data/feed/' + data.id + '/articles', function (articles) {
    articles.forEach(function (article) {
      var li = $('
' + article.title + '
');
      li.on('click', function () {
        App.load('article', article);
      });
      $(page).find('.app-list').append(li);
    });
  });
});

App.populator('article', function (page, data) {
  $(page).find('.app-title').text(data.title.substring(0, 30));
  $.getJSON('/data/article/' + data.url, function (article) {
    var content =
    '
' + article.title + '
' +
    '' + article.url + '
' +
    article.content;
    $(page).find('.app-content').html(content);
  });
});

Change title limit to 30 chars.


import postcss from 'postcss';
import margin from './lib/decl/margin';
import padding from './lib/decl/padding';
import borders from './lib/decl/borders';
import columns from './lib/decl/columns';

const processors = [
    margin,
    padding,
    borders,
    columns,
];

export default postcss.plugin('postcss-merge-longhand', () => {
    return css => {
        let abort = false;
        css.walkRules(rule => {
            processors.forEach(p => {
                const res = p.explode(rule);
                if (res === false) {
                    abort = true;
                }
            });
            if (abort) {
                return;
            }
            processors.slice().reverse().forEach(p => p.merge(rule));
        });
    };
});

import postcss from 'postcss';
import margin from './lib/decl/margin';
import padding from './lib/decl/padding';
import borders from './lib/decl/borders';
import columns from './lib/decl/columns';

const processors = [
    margin,
    padding,
    borders,
    columns,
];

export default postcss.plugin('postcss-merge-longhand', () => {
    return css => {
        css.walkRules(rule => {
            let abort = false;
            processors.forEach(p => {
                const res = p.explode(rule);
                if (typeof res === 'boolean') {
                    abort = true;
                }
            });
            if (abort) {
                return;
            }
            processors.slice().reverse().forEach(p => p.merge(rule));
        });
    };
});

Resolve issue with running plugin on multiple rules.


"use strict";

$(document).on("turbolinks:load", function() { //equivalent of $(document).ready()
  console.log("JS loaded.")
  addSpace();
  var $grid = initMasonry();

  // layout Masonry after each image loads
  $grid.imagesLoaded().progress( function() {
    $grid.masonry('layout');
  });
});

function initMasonry() {
  return $('.masonry-grid').masonry({ //returns the jquery masonry grid to be stored as a variable
    // options
    itemSelector: '.card',
    // columnWidth: 280, //with no columnWidth set, will take size of first element in grid
    horizontalOrder: true,
    isFitWidth: true,  //breaks columns like media queries
    gutter: 20,
    transitionDuration: '0.3s'
  });
}

function addSpace() {
  var spaceBetween = $(".custom-footer").offset().top - $(".navbar").offset().top; //get space between header and footer
  var target = 600;

  if (spaceBetween < target) { //if space between header and footer < x
    var numOfSpaces = (target - spaceBetween) / 35;

    for(var i = 0; i < numOfSpaces; i++) {
      $("#main-container").append("<p>&nbsp</p>"); //add space above footer
    }
  }
}

"use strict";

$(document).on("ready turbolinks:load", function() { //equivalent of $(document).ready()
  console.log("JS loaded.")
  addSpace();
  var $grid = initMasonry();

  // layout Masonry after each image loads
  $grid.imagesLoaded().progress( function() {
    $grid.masonry('layout');
  });
});

function initMasonry() {
  return $('.masonry-grid').masonry({ //returns the jquery masonry grid to be stored as a variable
    // options
    itemSelector: '.card',
    // columnWidth: 280, //with no columnWidth set, will take size of first element in grid
    horizontalOrder: true,
    isFitWidth: true,  //breaks columns like media queries
    gutter: 20,
    transitionDuration: '0.3s'
  });
}

function addSpace() {
  var spaceBetween = $(".custom-footer").offset().top - $(".navbar").offset().top; //get space between header and footer
  var target = 600;

  if (spaceBetween < target) { //if space between header and footer < x
    var numOfSpaces = (target - spaceBetween) / 35;

    for(var i = 0; i < numOfSpaces; i++) {
      $("#main-container").append("<p>&nbsp</p>"); //add space above footer
    }
  }
}

Adjust JS document ready function


/*jslint browser: true, undef: true, white: false, laxbreak: true *//*global Ext,Slate*/
Ext.define('SlateAdmin.model.person.progress.NoteRecipient', {
    extend: 'Ext.data.Model',

    idProperty: 'ID',
    groupField: 'RelationshipGroup',
    fields: [
        'FullName',
        'Email',
        'Label',
        'Status',
        {
            name: 'selected',
            type: 'boolean',
            convert: function (v, record) {
                var selected = !Ext.isEmpty(record.get('Status'));

                return selected;
            }
        }, {
            name: 'PersonID',
            type: 'integer'
        }, {
            name: 'RelationshipGroup',
            convert: function (v) {
                return v ? v : 'Other';
            }
        }, {
            name: 'ID',
            type: 'integer'
        }
    ],
    proxy: {
        type: 'slaterecords',
        api: {
            read: '/notes/progress/recipients',
            update: '/notes/save',
            create: '/notes/save',
            destory: '/notes/save'
        },
        reader: {
            type: 'json',
            rootProperty: 'data'
        },
        writer: {
            type: 'json',
            rootProperty: 'data',
            writeAllFields: false,
            allowSingle: false
        }
    }
});

/*jslint browser: true, undef: true, white: false, laxbreak: true *//*global Ext,Slate*/
Ext.define('SlateAdmin.model.person.progress.NoteRecipient', {
    extend: 'Ext.data.Model',

    idProperty: 'ID',
    groupField: 'RelationshipGroup',
    fields: [
        'FullName',
        'Email',
        'Label',
        'Status',
        {
            name: 'selected',
            type: 'boolean',
            convert: function (v, record) {
                var selected = !Ext.isEmpty(record.get('Status'));

                return selected;
            }
        }, {
            name: 'PersonID',
            type: 'integer'
        }, {
            name: 'RelationshipGroup',
            convert: function (v) {
                return v ? v : 'Other';
            }
        }, {
            name: 'ID',
            type: 'integer'
        }
    ],
    proxy: {
        type: 'slaterecords',
        startParam: null,
        limitParam: null,
        api: {
            read: '/notes/progress/recipients',
            update: '/notes/save',
            create: '/notes/save',
            destory: '/notes/save'
        },
        reader: {
            type: 'json',
            rootProperty: 'data'
        },
        writer: {
            type: 'json',
            rootProperty: 'data',
            writeAllFields: false,
            allowSingle: false
        }
    }
});

Disable start/limit params on recipients request


import requiredArgs from '../_lib/requiredArgs/index.js'

/**
 * @name formatISODuration
 * @category Common Helpers
 * @summary Format a Duration Object according to ISO 8601 Duration standards (https://www.digi.com/resources/documentation/digidocs/90001437-13/reference/r_iso_8601_duration_format.htm)
 *
 * @param {Duration} duration
 *
 * @returns {String} The ISO 8601 Duration string
 * @throws {TypeError} Requires 1 argument
 * @throws {Error} Argument must be an object
 *
 * @example
 * // Get the ISO 8601 Duration between January 15, 1929 and April 4, 1968.
 * const result = formatISODuration({ years: 39, months: 2, days: 20, hours: 7, minutes: 5, seconds: 0 })
 * // => 'P39Y2M20DT0H0M0S'
 */

export default function formatISODuration(duration) {
  requiredArgs(1, arguments)

  if (typeof duration !== 'object')
    throw new Error('Duration must be an object')

  const {
    years = 0,
    months = 0,
    days = 0,
    hours = 0,
    minutes = 0,
    seconds = 0
  } = duration

  return `P${years}Y${months}M${days}DT${hours}H${minutes}M${seconds}S`
}

import requiredArgs from '../_lib/requiredArgs/index.js'

/**
 * @name formatISODuration
 * @category Common Helpers
 * @summary Format a duration object according as ISO 8601 duration string
 *
 * @description
 * Format a duration object according to the ISO 8601 duration standard (https://www.digi.com/resources/documentation/digidocs/90001437-13/reference/r_iso_8601_duration_format.htm)
 *
 * @param {Duration} duration - the duration to format
 *
 * @returns {String} The ISO 8601 duration string
 * @throws {TypeError} Requires 1 argument
 * @throws {Error} Argument must be an object
 *
 * @example
 * // Format the given duration as ISO 8601 string
 * const result = formatISODuration({ years: 39, months: 2, days: 20, hours: 7, minutes: 5, seconds: 0 })
 * // => 'P39Y2M20DT0H0M0S'
 */

export default function formatISODuration(duration) {
  requiredArgs(1, arguments)

  if (typeof duration !== 'object')
    throw new Error('Duration must be an object')

  const {
    years = 0,
    months = 0,
    days = 0,
    hours = 0,
    minutes = 0,
    seconds = 0
  } = duration

  return `P${years}Y${months}M${days}DT${hours}H${minutes}M${seconds}S`
}

Fix formatISODuration documentation (ci skip)


const config = require('../../../knexfile').web
const knex = require('knex')(config)
const orgUnitFinder = require('../helpers/org-unit-finder')

module.exports = function (id, type) {
  var orgUnit = orgUnitFinder('name', type)
  var table = orgUnit.caseProgressView

  return knex(table)
    .where(table + '.id', id)
    .select(table + '.community_last_16_weeks AS communityLast16Weeks',
            table + '.license_last_16_weeks AS licenseLast16Weeks',
            table + '.total_cases AS totalCases',
            table + '.warrants_total AS warrantsTotal',
            table + '.overdue_terminations_total AS overdueTerminationsTotal',
            table + '.unpaid_work_total AS unpaidWorkTotal')
    .then(function (results) {
      return results
    })
}

const config = require('../../../knexfile').web
const knex = require('knex')(config)
const orgUnitFinder = require('../helpers/org-unit-finder')

module.exports = function (id, type) {
  var orgUnit = orgUnitFinder('name', type)
  var table = orgUnit.caseProgressView

  return knex(table)
    .where('id', id)
    .select('name',
            'community_last_16_weeks AS communityLast16Weeks',
            'license_last_16_weeks AS licenseLast16Weeks',
            'total_cases AS totalCases',
            'warrants_total AS warrantsTotal',
            'overdue_terminations_total AS overdueTerminationsTotal',
            'unpaid_work_total AS unpaidWorkTotal')
    .then(function (results) {
      return results
    })
}

Update case progress query to include name


/**
 * SurveyQuestionSingleSelectChoice component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

const SurveyQuestionSingleSelectChoice = () => {
	return
SurveyQuestionSingleSelectChoice
;
};

export default SurveyQuestionSingleSelectChoice;

/**
 * SurveyQuestionSingleSelectChoice component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * Internal dependencies
 */
import Radio from '../Radio';

/* eslint-disable camelcase */

const SurveyQuestionSingleSelectChoice = ( {
	value,
	setValue,
	// writeIn,
	// setWriteIn,
	choice,
} ) => {
	const { answer_ordinal, text } = choice;
	const isChecked = value === answer_ordinal;
	return (
		<Radio
			value={ answer_ordinal }
			checked={ isChecked }
			name={ text }
			onClick={ () => setValue( answer_ordinal ) }
		/>
	);
};

SurveyQuestionSingleSelectChoice.propTypes = {
	value: PropTypes.string.isRequired,
	setValue: PropTypes.func.isRequired,
	writeIn: PropTypes.string.isRequired,
	setWriteIn: PropTypes.func.isRequired,
	choice: PropTypes.shape( {
		answer_ordinal: PropTypes.oneOfType( [
			PropTypes.string,
			PropTypes.number,
		] ),
		text: PropTypes.string,
		write_in: PropTypes.bool,
	} ),
};

export default SurveyQuestionSingleSelectChoice;

Add radio button and link up to state.


var router = require("express").Router();
var db = require("./db");

router.get("/", function (req, res) {
  var o = {
    map: function () {
      if (this.parents.length === 1) {
        for (var i = 0; i < this.files.length; i++) {
          emit(this.author,
            {
              additions: this.files[i].additions,
              deletions: this.files[i].deletions
            });
        }
      }
    },
    reduce: function (key, values) {
      var obj = {additions: 0, deletions: 0};
      values.map(function (value) {
        obj.additions += value.additions;
        obj.deletions += value.deletions;
      });
      return obj;
    }
  };

  db.Commit.mapReduce(o, function (err, results) {
    if (err) {
      res.send(err);
    } else {
      res.send(results);
    }
  });
});

module.exports = router;

var router = require("express").Router();
var db = require("./db");

router.get("/", function (req, res) {
  var o = {
    map: function () {
      if (this.parents.length === 1) {
        for (var i = 0; i < this.files.length; i++) {
          emit(this.author,
            {
              additions: this.files[i].additions,
              deletions: this.files[i].deletions
            });
        }
      }
    },
    reduce: function (key, values) {
      var obj = {additions: 0, deletions: 0};
      values.map(function (value) {
        obj.additions += value.additions;
        obj.deletions += value.deletions;
      });
      return obj;
    },
    out: {
      replace: 'PersonReduce'
    }
  };

  db.Commit.mapReduce(o, function (err, model) {
    if (err) {
      res.send(err);
    } else {
      model.find().populate({
        path: "_id",
        model: "Person"
      }).exec(function (err, results) {
        if (err) {
          res.send(err);
        } else {
          var data = results.map(function (result) {
            return {
              author: result._id.name,
              additions: result.value.additions,
              deletions: result.value.deletions
            };
          });
          res.send(data);
        }
      });
    }
  });
});

module.exports = router;

Return more sane data through API


AccountsAnonymous.login = function (callback) {
  callback = callback || function () {};
  if (Meteor.userId()) {
    callback(new Meteor.Error('accounts-anonymous-already-logged-in',
      "You can't login anonymously while you are already logged in."));
    return;
  }
  Accounts.callLoginMethod({
    methodArguments: [{
      anonymous: true
    }],
    userCallback: function (error, result) {
      if (error) {
        callback && callback(error);
      } else {
        callback && callback();
      }
    }
  });
}

AccountsAnonymous.login = function (callback) {
  callback = callback || function () {};
  if (Meteor.userId()) {
    callback(new Meteor.Error(AccountsAnonymous._ALREADY_LOGGED_IN_ERROR,
      "You can't login anonymously while you are already logged in."));
    return;
  }
  Accounts.callLoginMethod({
    methodArguments: [{
      anonymous: true
    }],
    userCallback: function (error, result) {
      if (error) {
        callback && callback(error);
      } else {
        callback && callback();
      }
    }
  });
}

Use constant for error message.


import Em from 'ember';
import ArraySlice from 'array-slice';
var get = Em.get;
var set = Em.set;

var ArrayPager = ArraySlice.extend({
	// 1-based
	page: function (key, page) {
		var limit = get(this, 'limit');
		var offset;
		// default value
		if (arguments.length <= 1) {
			offset = get(this, 'offset') || 0;
			return Math.ceil(offset / limit) || 1;
		}
		// setter
		// no negative pages
		page = Math.max(page - 1, 0);
		offset = (limit * page) || 0;
		set(this, 'offset', offset);
		return page + 1;
	}.property('offset', 'limit'),

	// 1-based
	pages: function () {
		var limit = get(this, 'limit');
		var length = get(this, 'content.length');
		var pages = Math.ceil(length / limit) || 1;
		if (pages === Infinity) {
			pages = 0;
		}
		return pages;
	}.property('content.length', 'limit')
});

ArrayPager.computed = {
	page: function (prop, page, limit) {
		return Em.computed(prop, function () {
			return ArrayPager.create({
				content: get(this, prop),
				page: page,
				limit: limit
			});
		});
	}
};

export default ArrayPager;

import Em from 'ember';
import ArraySlice from 'array-slice';
var computed = Em.computed;
var get = Em.get;
var set = Em.set;

var ArrayPager = ArraySlice.extend({
	// 1-based
	page: computed('offset', 'limit', function (key, page) {
		var limit = get(this, 'limit');
		var offset;
		// default value
		if (arguments.length <= 1) {
			offset = get(this, 'offset') || 0;
			return Math.ceil(offset / limit) || 1;
		}
		// setter
		// no negative pages
		page = Math.max(page - 1, 0);
		offset = (limit * page) || 0;
		set(this, 'offset', offset);
		return page + 1;
	}),

	// 1-based
	pages: computed('content.length', 'limit', function () {
		var limit = get(this, 'limit');
		var length = get(this, 'content.length');
		var pages = Math.ceil(length / limit) || 1;
		if (pages === Infinity) {
			pages = 0;
		}
		return pages;
	})
});

ArrayPager.computed = {
	page: function (prop, page, limit) {
		return computed(function () {
			return ArrayPager.create({
				content: get(this, prop),
				page: page,
				limit: limit
			});
		});
	}
};

export default ArrayPager;

Use `Ember.computed` instead of `Function.prototype.property`


// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require_tree .

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require angular
//= require angular-rails-templates

//= require angular-app
//= require_tree ./angular-app/templates
//= require_tree ./angular-app/modules
//= require_tree ./angular-app/filters
//= require_tree ./angular-app/directives
//= require_tree ./angular-app/models
//= require_tree ./angular-app/services
//= require_tree ./angular-app/controllers

Add angular assets to asset pipeline


/**
 * @constructor
 */
function Analytics() {
  this.service_ = null;
}

Analytics.prototype.start = function(settings) {
  if (this.service_) {
    throw 'Analytics should be started only once per session.';
  }

  this.service_ = analytics.getService('text_app');
  var propertyId = 'UA-48886257-1';
  if (chrome.runtime.getManifest()['name'].indexOf('Dev') >= 0) {
    propertyId = 'UA-48886257-2';
  }
  this.tracker_ = this.service_.getTracker(propertyId);

  this.reportSettings_(settings);

  this.tracker_.sendAppView('main');
};

Analytics.prototype.reportSettings_ = function(settings) {
  this.tracker_.set('dimension1', settings.get('spacestab').toString());
  this.tracker_.set('dimension2', settings.get('tabsize').toString());
  this.tracker_.set('dimension3',
                    Math.round(settings.get('fontsize')).toString());
  this.tracker_.set('dimension4', settings.get('sidebaropen').toString());
  this.tracker_.set('dimension5', settings.get('theme'));
};

/**
 * @constructor
 */
function Analytics() {
  this.service_ = null;
}

Analytics.prototype.start = function(settings) {
  if (this.service_) {
    throw 'Analytics should be started only once per session.';
  }

  this.service_ = analytics.getService('text_app');
  var propertyId = 'UA-48886257-2';
  if (chrome.runtime.id === 'mmfbcljfglbokpmkimbfghdkjmjhdgbg') {
    propertyId = 'UA-48886257-1';
  }
  this.tracker_ = this.service_.getTracker(propertyId);

  this.reportSettings_(settings);

  this.tracker_.sendAppView('main');
};

Analytics.prototype.reportSettings_ = function(settings) {
  this.tracker_.set('dimension1', settings.get('spacestab').toString());
  this.tracker_.set('dimension2', settings.get('tabsize').toString());
  this.tracker_.set('dimension3',
                    Math.round(settings.get('fontsize')).toString());
  this.tracker_.set('dimension4', settings.get('sidebaropen').toString());
  this.tracker_.set('dimension5', settings.get('theme'));
};

Use chrome.runtime.id instead of name to select Analytics property.


import React, { useState } from 'react';
import { c } from 'ttag';
import PropTypes from 'prop-types';
import { FormModal, PrimaryButton, Label, Alert, TextArea } from 'react-components';

const CustomThemeModal = ({ onSave, theme: initialTheme = '', ...rest }) => {
    const [theme, setTheme] = useState(initialTheme);

    const handleChange = ({ target }) => setTheme(target.value);
    const handleSubmit = async () => {
        await onSave(theme);
        rest.onClose();
    };

    return (
        <FormModal
            submit={<PrimaryButton type="submit">{c('Action').t`Save`}</PrimaryButton>}
            onSubmit={handleSubmit}
            title={c('Title').t`Custom mode`}
            small
            {...rest}
        >
            <Alert type="warning">{c('Warning')
                .t`Custom modes from third parties can potentially betray your privacy. Only use modes from trusted sources.`}</Alert>
            <Label className="mb1" htmlFor="themeTextarea">{c('Label').t`CSS code`}</Label>
            <TextArea
                className="mb1"
                id="themeTextarea"
                value={theme}
                placeholder={c('Action').t`Insert CSS code here`}
                onChange={handleChange}
            />
        </FormModal>
    );
};

CustomThemeModal.propTypes = {
    onSave: PropTypes.func.isRequired,
    theme: PropTypes.string
};

export default CustomThemeModal;

import React, { useState } from 'react';
import { c } from 'ttag';
import PropTypes from 'prop-types';
import { FormModal, PrimaryButton, Label, Alert, TextArea } from 'react-components';

const CustomThemeModal = ({ onSave, theme: initialTheme = '', ...rest }) => {
    const [theme, setTheme] = useState(initialTheme);

    const handleChange = ({ target }) => setTheme(target.value);
    const handleSubmit = async () => {
        await onSave(theme);
        rest.onClose();
    };

    return (
        <FormModal
            submit={<PrimaryButton type="submit">{c('Action').t`Save`}</PrimaryButton>}
            onSubmit={handleSubmit}
            title={c('Title').t`Custom mode`}
            {...rest}
        >
            <Alert type="warning">{c('Warning')
                .t`Custom modes from third parties can potentially betray your privacy. Only use modes from trusted sources.`}</Alert>
            <Label className="bl mb0-5" htmlFor="themeTextarea">{c('Label').t`CSS code`}</Label>
            <TextArea
                className="mb1"
                id="themeTextarea"
                value={theme}
                placeholder={c('Action').t`Insert CSS code here`}
                onChange={handleChange}
            />
        </FormModal>
    );
};

CustomThemeModal.propTypes = {
    onSave: PropTypes.func.isRequired,
    theme: PropTypes.string
};

export default CustomThemeModal;

Use larger modal for Custom CSS


'use strict';

/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */

describe('ng-grid', function() {
  beforeEach(function() {
    browser().navigateTo('../../workbench/index.html');
  });

  describe('templates', function() {
    browser().navigateTo('../../workbench/templating/external.html');

    it('should not cause a $digest or $apply error when fetching the template completes after the data $http request does', function() {

    });

    it('should load external templates before building the grid', function() {
      // todo: rows should have data when external template is defined
    });

    it('should only load the same external template once', function() {
      // todo
    });
  });
});

'use strict';

/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */

describe('ng-grid', function() {

  // beforeEach(function() {
  //   browser().navigateTo('../../workbench/index.html');
  // });

  describe('templates', function() {
    beforeEach(function() {
      browser().navigateTo('../../workbench/templating/external.html');
    });

    it('should not cause a $digest or $apply error when fetching the template completes after the data $http request does', function() {

    });

    it('should load external templates before building the grid', function() {
      // todo: rows should have data when external template is defined
    });

    it('should only load the same external template once', function() {
      // todo
    });
  });
});

Fix for "browser is not defined" error


#!/usr/bin/env node
'use strict';

/**
 * Binary to run the main server.
 * (C) 2015 Alex Fernández.
 */

// requires
var stdio = require('stdio');
var Log = require('log');
var reviewServer = require('../lib/reviewServer');

// globals
var log = new Log('info');
var credentials = {};

// init
try
{
	credentials = require('../credentials.json');
}
catch(exception)
{
	log.info('Please enter default values in a file called credentials.json');
}
var options = stdio.getopt({
	token: {key: 't', args: 1, description: 'Consumer token for Trello'},
	key: {key: 'k', args: 1, description: 'Key for Trello'},
	secret: {key: 's', args: 1, description: 'Secret value to access the server'},
	port: {key: 'p', args: 1, description: 'Port to start the server', default: 6003},
	quiet: {key: 'q', description: 'Do not log any messages'},
	debug: {key: 'd', description: 'Log debug messages'},
});
credentials.overwriteWith(options);

reviewServer.start(credentials);

#!/usr/bin/env node
'use strict';

/**
 * Binary to run the main server.
 * (C) 2015 Alex Fernández.
 */

// requires
var stdio = require('stdio');
var Log = require('log');
var reviewServer = require('../lib/reviewServer');

// globals
var log = new Log('info');
var credentials = {};

// init
try
{
	credentials = require('../credentials.json');
}
catch(exception)
{
	log.info('Please enter default values in a file called credentials.json');
}
var options = stdio.getopt({
	token: {key: 't', args: 1, description: 'Consumer token for Trello'},
	key: {key: 'k', args: 1, description: 'Key for Trello'},
	secret: {key: 's', args: 1, description: 'Secret value to access the server'},
	port: {key: 'p', args: 1, description: 'Port to start the server', default: 7431},
	quiet: {key: 'q', description: 'Do not log any messages'},
	debug: {key: 'd', description: 'Log debug messages'},
});
credentials.overwriteWith(options);

reviewServer.start(credentials);

Change default port to 7431.


#!/usr/bin/env node
'use strict';

/**
 * Binary to run the main server.
 * (C) 2015 Alex Fernández.
 */

// requires
var stdio = require('stdio');
var reviewServer = require('../lib/reviewServer');

// init
var options = stdio.getopt({
	token: {key: 't', args: 1, mandatory: true, description: 'Consumer token for Trello'},
	key: {key: 'k', args: 1, mandatory: true, description: 'Key for Trello'},
	secret: {key: 's', args: 1, mandatory: true, description: 'Secret value to access the server'},
	port: {key: 'p', args: 1, description: 'Port to start the server', default: 6003},
	quiet: {key: 'q', description: 'Do not log any messages'},
	debug: {key: 'd', description: 'Log debug messages'},
});

reviewServer.start(options);

#!/usr/bin/env node
'use strict';

/**
 * Binary to run the main server.
 * (C) 2015 Alex Fernández.
 */

// requires
var stdio = require('stdio');
var Log = require('log');
var reviewServer = require('../lib/reviewServer');

// globals
var log = new Log('info');

// init
try
{
	var credentials = require('./credentials.json');
}
catch(exception)
{
	log.info('Please enter default values in a file called credentials.json');
}
var options = stdio.getopt({
	token: {key: 't', args: 1, description: 'Consumer token for Trello'},
	key: {key: 'k', args: 1, description: 'Key for Trello'},
	secret: {key: 's', args: 1, description: 'Secret value to access the server'},
	port: {key: 'p', args: 1, description: 'Port to start the server', default: 6003},
	quiet: {key: 'q', description: 'Do not log any messages'},
	debug: {key: 'd', description: 'Log debug messages'},
});
credentials.overwriteWith(options);

reviewServer.start(credentials);

Use credentials as default values, complain if not found.


const path = require('path')

module.exports = function engine (config) {
  require('./driver')(config)
  config.engine.on('init', function (processType) {
    // processType will be 'runner','processor', or 'main'
    // Useful for detecting what module you are in
    if (processType === 'main') {
      // if (!config.common.storage.db.users.count()) {
      //   importDB(config)
      // }
    }
    if (processType === 'runtime') {
      patchRuntimeGlobals(config)
    }
  })
}

function patchRuntimeGlobals (config) {
  let pathToModule = path.join(path.dirname(require.main.filename), 'runtime/user-globals.js')
  let userGlobals = require(pathToModule)
  let newUserGlobals = require('./runtime/user-globals.js')
  Object.assign(userGlobals, newUserGlobals)
}

const path = require('path')

module.exports = function (config) {
  if (config.engine.driver) {
    // TODO: For some reason this is missing on runtimes, this needs
    // investigated as it makes on('playerSandbox') never trigger
    require('./driver')(config)
  }
  config.engine.on('init', function (processType) {
    console.log('INIT', processType)
    // processType will be 'runner','processor', or 'main'
    // Useful for detecting what module you are in
    if (processType === 'main') {
      // if (!config.common.storage.db.users.count()) {
      //   importDB(config)
      // }
    }
    if (processType === 'runtime') {
      patchRuntimeGlobals(config)
    }
  })
}

function patchRuntimeGlobals (config) {
  let pathToModule = path.join(path.dirname(require.main.filename), 'user-globals.js')
  let userGlobals = require(pathToModule)
  let newUserGlobals = require('./runtime/user-globals.js')
  Object.assign(userGlobals, newUserGlobals)
}

Fix user global patching. Work around wierd runtime error


console.log('Started', self);
self.addEventListener('install', function(event) {
    self.skipWaiting();
    console.log('Installed', event);
});
self.addEventListener('activate', function(event) {
    console.log('Activated', event);
});
self.addEventListener('push', function(event) {
    console.log('Push message', event);
    var title = 'Push message';
    event.waitUntil(
        self.registration.showNotification(title, {
            body: 'The Message',
            tag: 'my-tag'
        }));
});
self.addEventListener('notificationclick', function(event) {
    console.log('Notification click: tag ', event.notification.tag);
    event.notification.close();
    var url = 'https://www.google.com/';
    event.waitUntil(
        clients.matchAll({
            type: 'window'
        })
            .then(function(windowClients) {
                for (var i = 0; i < windowClients.length; i++) {
                    var client = windowClients[i];
                    if (client.url === url && 'focus' in client) {
                        return client.focus();
                    }
                }
                if (clients.openWindow) {
                    return clients.openWindow(url);
                }
            })
    );
});

console.log('Started', self);
self.addEventListener('install', function(event) {
    self.skipWaiting();
    console.log('Installed', event);
});
self.addEventListener('activate', function(event) {
    console.log('Activated', event);
});
self.addEventListener('push', function(event) {
    var title = 'Push message';
    event.waitUntil(
        self.registration.showNotification(title, {
            body: 'The Message',
            icon: 'assets/launcher-icon-4x.png',
            tag: 'my-tag'
        }));
});
self.addEventListener('notificationclick', function(event) {
    console.log('Notification click: tag ', event.notification.tag);
    event.notification.close();
    var url = 'https://www.google.com/';
    if (clients.openWindow) {
        return clients.openWindow(url);
    }
});

Add notification icon and delete code that didn't work


var _ = require('lodash');
var marked = require('marked');
var textRenderer = require('marked-text-renderer');

function extractFirstNode(nodes, nType) {
    return _.chain(nodes)
    .filter(function(node) {
        return node.type == nType;
    })
    .pluck("text")
    .first()
    .value();
}

function parseReadme(src) {
    var nodes, title, description;
    var renderer = textRenderer();

    // Parse content
    nodes = marked.lexer(src);

    var title = extractFirstNode(nodes, "heading");
    var description = extractFirstNode(nodes, "paragraph");

    var convert = _.compose(
        function(text) {
            return _.unescape(text.replace(/(\r\n|\n|\r)/gm, ""));
        },
        _.partialRight(marked, _.extend({}, marked.defaults, {
            renderer: renderer
        }))
    );

    return {
        title: convert(title),
        description: convert(description)
    };
}

// Exports
module.exports = parseReadme;

var _ = require('lodash');
var marked = require('marked');
var textRenderer = require('marked-text-renderer');

function extractFirstNode(nodes, nType) {
    return _.chain(nodes)
    .filter(function(node) {
        return node.type == nType;
    })
    .pluck("text")
    .first()
    .value();
}

function parseReadme(src) {
    var nodes, title, description;
    var renderer = textRenderer();

    // Parse content
    nodes = marked.lexer(src);

    title = extractFirstNode(nodes, "heading") || '';
    description = extractFirstNode(nodes, "paragraph") || '';

    var convert = _.compose(
        function(text) {
            return _.unescape(text.replace(/(\r\n|\n|\r)/gm, ""));
        },
        function(text) {
            return marked.parse(text, _.extend({}, marked.defaults, {
                renderer: renderer
            }));
        }
    );

    return {
        title: convert(title),
        description: convert(description)
    };
}

// Exports
module.exports = parseReadme;

Fix generate failing on README parsing


var createError = require('createerror');
var httpErrors = require('httperrors');
var socketCodesMap = require('./socketCodesMap');
var _ = require('lodash');

var SocketError = createError({ name: 'SocketError' });

function createSocketError(errorCode) {
    var statusCode = socketCodesMap[errorCode] || 'Unknown';

    var options = _.defaults({
        name: errorCode,
        code: errorCode,
        statusCode: statusCode,
        status: statusCode
    }, _.omit(httpErrors(statusCode), 'message'));

    var socketError = createError(options, SocketError);

    socketErrors[errorCode] = socketError;

    return socketError;
}

var socketErrors = module.exports = function (err) {
    var errorName;

    if (socketErrors.hasOwnProperty(err.code)) {
        errorName = err.code;
    } else {
        errorName = 'NotSocketError';
    }

    return new socketErrors[errorName](err);
};

module.exports.SocketError = SocketError;

// create an Unknown error sentinel
socketErrors.NotSocketError = createSocketError('NotSocketError');

// create a new socket error for each error code
Object.keys(socketCodesMap).forEach(createSocketError);

var createError = require('createerror');
var httpErrors = require('httperrors');
var socketCodesMap = require('./socketCodesMap');
var _ = require('lodash');

var SocketError = createError({ name: 'SocketError' });

function createSocketError(errorCode) {
    var statusCode = socketCodesMap[errorCode] || 'Unknown';

    var options = _.defaults({
        name: errorCode,
        code: errorCode,
        statusCode: statusCode,
        status: statusCode
    }, _.omit(httpErrors(statusCode), 'message'));

    var socketError = createError(options, SocketError);

    socketErrors[errorCode] = socketError;

    return socketError;
}

var socketErrors = module.exports = function (err) {
    var errorName;

    if (socketErrors.hasOwnProperty(err.code)) {
        errorName = err.code;
    } else {
        errorName = 'NotSocketError';
    }

    return new socketErrors[errorName](err);
};

// export the base class
module.exports.SocketError = SocketError;

// create an Unknown error sentinel
socketErrors.NotSocketError = createSocketError('NotSocketError');

// create a new socket error for each error code
Object.keys(socketCodesMap).forEach(createSocketError);

Add a small comment to an export.


import _ from 'lodash';

import {
  OPEN_ALERT_MESSAGE,
  CLOSE_ALERT_MESSAGE,
} from '../actions/actionTypes';

const initialState = {
  show: false,
  messages: {
    th: '',
    en: '',
  },
  technical: {
    message: '',
    code: '',
  },
};
const getInitialState = () => ({
  ...initialState,
});

export default (state = getInitialState(), action) => {
  switch (action.type) {
    case OPEN_ALERT_MESSAGE:
      return {
        ...state,
        show: true,
        // messages: action.messages,
      };
    case CLOSE_ALERT_MESSAGE:
      return getInitialState();
    default:
      return state;
  }
};

import _ from 'lodash';

import {
  OPEN_ALERT_MESSAGE,
  CLOSE_ALERT_MESSAGE,
} from '../actions/actionTypes';

const initialState = {
  show: false,
  title: {
    th: '',
    en: ''
  },
  messages: {
    th: '',
    en: '',
  },
  technical: {
    message: '',
    code: '',
  },
};
const getInitialState = () => ({
  ...initialState,
});

export default (state = getInitialState(), action) => {
  switch (action.type) {
    case OPEN_ALERT_MESSAGE:
      return {
        ...state,
        show: true,
        title: action.data.title,
        messages: action.data.messages,
        technical: action.data.technical,
      };
    case CLOSE_ALERT_MESSAGE:
      return getInitialState();
    default:
      return state;
  }
};

Add alert message reducer format


import SUPPORTED_FORMULAS from '../../src/supported-formulas';

describe('.SUPPORTED_FORMULAS', () => {
  it('should be defined', () => {
    expect(SUPPORTED_FORMULAS.length).to.eq(391);
  });
});

import SUPPORTED_FORMULAS from '../../src/supported-formulas';

describe('.SUPPORTED_FORMULAS', () => {
  it('should be defined', () => {
    expect(SUPPORTED_FORMULAS.length).to.eq(392);
  });
});

Update unit test for supported formula


/**
 * Places.js
 *
 * @description :: TODO: You might write a short summary of how this model works and what it represents here.
 * @docs        :: http://sailsjs.org/documentation/concepts/models-and-orm/models
 */

module.exports = {
  //connection: 'db_server',
  //configurations to disale UpdateAt and CreatedAt Waterline Columns
  tableName: 'places',
  autoCreatedAt: false,
  autoUpdatedAt: false,
  attributes: {

    id: {
      type: 'integer',
      autoIncrement: true,
      unique: true,
      primaryKey: true
    },
    company_id: {
      type: 'integer',
      required: true
    },
    name: {
      type: 'string',
      required: true
    },
    description: {
      type: 'text',
      required: true
    },
    created_at: {
      type: 'datetime'
    },
    updated_at: {
      type: 'datetime'
    },
    updated_by_user: {
      type: 'integer',
      columnName: 'updated_by_user_id',
      required: true
    }

  }
};

/**
 * Places.js
 *
 * @description :: TODO: You might write a short summary of how this model works and what it represents here.
 * @docs        :: http://sailsjs.org/documentation/concepts/models-and-orm/models
 */

module.exports = {
  //connection: 'db_server',
  //configurations to disale UpdateAt and CreatedAt Waterline Columns
  tableName: 'places',
  autoCreatedAt: false,
  autoUpdatedAt: false,
  attributes: {

    id: {
      type: 'integer',
      autoIncrement: true,
      unique: true,
      primaryKey: true
    },
    company_id: {
      type: 'integer',
      required: true
    },
    name: {
      type: 'string',
      required: true
    },
    description: {
      type: 'text'
    },
    created_at: {
      type: 'datetime'
    },
    updated_at: {
      type: 'datetime'
    },
    updated_by_user: {
      type: 'integer',
      columnName: 'updated_by_user_id',
      required: true
    }

  }
};

Refactor [description] property to not require


import { Mongo } from 'meteor/mongo';
import moment from 'moment';

const Payments = new Mongo.Collection('payments');

Payments.refund = (orderId) => {
  if (orderId) {
    const payment = Payments.findOne({ order_id: orderId });
    if (payment && payment.charge.id) {
      import StripeHelper from '../cards/server/stripe_helper';
      StripeHelper.refundCharge(payment.charge.id);
    }
  }
};

Payments.recentPaymentCompleted = email => Payments.findOne({
  email,
  status: 'completed',
  timestamp: {
    $gte: moment().subtract(10, 'minutes').toDate(),
  },
});

Payments.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});

export default Payments;

import { Mongo } from 'meteor/mongo';
import moment from 'moment';

const Payments = new Mongo.Collection('payments');

Payments.refund = (orderId) => {
  if (orderId) {
    const payment = Payments.findOne({ order_id: orderId });
    if (payment && payment.charge.id) {
      import StripeHelper from '../cards/server/stripe_helper';
      StripeHelper.refundCharge(payment.charge.id);
    }
  }
};

Payments.recentPaymentCompleted = (email) => {
  const recentPayments = Payments.find({
    email,
    status: 'completed',
    timestamp: {
      $gte: moment().subtract(10, 'minutes').toDate(),
    },
  }, {
    sort: {
      timestamp: -1,
    },
  }).fetch();
  return recentPayments ? recentPayments[0] : null;
};

Payments.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});

export default Payments;

Use most recent payment when sending final charged to Shopify


var fs = require('fs'),
	fileModule = require('file'),
	testDir = '/tests',
	testFileName = 'integration_test.js';

process.env.INTEGRATION = true;

describe('endpoint', function() {
	it('should load the server and set everything up properly',function(done){
		this.timeout(1000); //Server should not take more than 1 sek to boot

		var app = require(process.cwd() + '/server');

		app.on('ready',function(){
			fileModule.walkSync('./endpoints', function(dirPath, dirs, files){
		        if (dirPath.indexOf(testDir) < 0) return;
		        files.forEach(function(file){
		            if (file != testFileName) return;
		            var path = dirPath + '/' + file;
		            if (!fs.existsSync(path)) return;
		            require('../../' + path);
		        });
			});
			done();
		});
	});
});

var fs = require('fs'),
  path = require('path'),
	fileModule = require('file'),
	testDir = '/tests',
	testFileName = 'integration_test.js';

process.env.INTEGRATION = true;

describe('endpoint', function() {
	it('should load the server and set everything up properly',function(done){
		this.timeout(1000); //Server should not take more than 1 sek to boot

		var app = require(process.cwd() + '/server');

		app.on('ready',function(){
			fileModule.walkSync('./endpoints', function(dirPath, dirs, files){
		        if (dirPath.indexOf(testDir) < 0) return;
		        files.forEach(function(file){
		            if (file != testFileName) return;
		            var fullPath = dirPath + '/' + file;
		            if (!fs.existsSync(fullPath)) return;
                if (path.extname(fullPath) !== '.js') return;
		            require('../../' + fullPath);
		        });
			});
			done();
		});
	});
});

Check the file extension before requiring a test.


// include flightplan.js
var plan = require('flightplan');

// Plan to git deploy via ssh remote
plan.remote('gitDeploy', function(remote) {
  var webRoot = plan.runtime.options.webRoot;

  // git pull
  remote.with('cd ' + webRoot, function() {
      remote.exec('git pull');
  });
});

module.exports = function () {

  /*
    @method remotGitDeploy
    Takes a flightplan instance and transport
    @param remote {Object} Flightplan transport instance
    @param webRoot {string} path to run git pull on the remote server
     e.g. /var/www/project
  */
  var remotGitDeploy = function (remote, webRoot) {
    // git pull
    remote.with('cd ' + webRoot, function() {
      remote.exec('git pull');
    });
  };

  /*
    @method addToPlan
    Takes a flightplan instance and adds a task called gitDeploy
    @param plan {Object} Flightplan instance
    @param plan.runtime.options.webRoot {String} Remote path to run git pull
  */
  var addToPlan = function (plan) {
    // Plan to git deploy via ssh remote
    var webRoot = plan.runtime.options.webRoot;

    plan.remote('gitDeploy', function(remote) {
      remoteGitDeploy(remote, webRoot);
    });
  };

  return {
    addToPlan: addToPlan,
    remoteGitDeploy: remoteGitDeploy
  };
}

Update module to return two different methods for adding the deploy script


angular.module('GLClient.themes', [])
  .factory('Templates', function() {
      var selected_theme = 'default';

      return {
        'home': 'templates/' + selected_theme + '/views/home.html',
        'about': 'templates/' + selected_theme + '/views/about.html',
        'status': 'templates/' + selected_theme + '/views/status.html',
        'submission': {
          'form': 'templates/' + selected_theme + '/views/submission/form.html',
          'main': 'templates/' + selected_theme + '/views/submission/main.html'
        }
      };
    }
);

angular.module('GLClient.themes', [])
  .factory('Templates', function() {
      // XXX do not add the "default" string to this file as it is used by
      // build-custom-glclient.sh for matching.
      var selected_theme = 'default';

      return {
        'home': 'templates/' + selected_theme + '/views/home.html',
        'about': 'templates/' + selected_theme + '/views/about.html',
        'status': 'templates/' + selected_theme + '/views/status.html',
        'submission': {
          'form': 'templates/' + selected_theme + '/views/submission/form.html',
          'main': 'templates/' + selected_theme + '/views/submission/main.html'
        }
      };
    }
);

Add note about quirk related to build-custom-glclient.sh


/*global describe, beforeEach, it*/
'use strict';

var path    = require('path');
var helpers = require('yeoman-generator').test;

describe('plugin generator', function () {
    beforeEach(function (done) {
        helpers.testDirectory(path.join(__dirname, 'temp'), function (err) {
            if (err) {
                return done(err);
            }

            this.app = helpers.createGenerator('plugin:app', [
                '../../app'
            ]);
            done();
        }.bind(this));
    });

    it('creates expected files', function (done) {
        var expected = [
            // add files you expect to exist here.
            '.jshintrc',
            '.editorconfig'
        ];

        helpers.mockPrompt(this.app, {
            'someOption': true
        });
        this.app.options['skip-install'] = true;
        this.app.run({}, function () {
            helpers.assertFiles(expected);
            done();
        });
    });
});

/*global describe, beforeEach, it*/
'use strict';

var path    = require('path');
var helpers = require('yeoman-generator').test;

describe('plugin generator', function () {

    beforeEach(function (done) {
        helpers.testDirectory(path.join(__dirname, 'temp'), function (err) {
            if (err) {
                return done(err);
            }

            this.app = helpers.createGenerator('plugin:app', [
                '../../app'
            ]);
            done();
        }.bind(this));
    });

    it('creates expected files', function (done) {

        var expected = [
            // add files you expect to exist here.
            '.jshintrc',
            '.editorconfig',
            '.gitignore',
            '.npmignore',
            'LICENSE-MIT',
            'bower.json',
            'test/main.js',
            'index.js'
        ];

        helpers.mockPrompt(this.app, {
            pluginName: 'myPlugin',
            fullName: 'assemble-plugin-myPlugin',
            description: 'The best plugin ever',
            user: 'assemble',
            stages: ['render:after:pages'],
            homepage: 'https://github.com/assemble/assemble-plugin-myPlugin',
            repositoryUrl: 'https://github.com/assemble/assemble-plugin-myPlugin.git',
            bugUrl: 'https://github.com/assemble/assemble-plugin-myPlugin/issues',
            licenseType: 'MIT',
            licenseUrl: 'https://github.com/assemble/assemble-plugin-myPlugin/blob/master/LICENSE-MIT',
            contributors: 'assemble'
        });

        this.app.options['skip-install'] = true;

        this.app.run({}, function () {
            helpers.assertFile(expected);
            done();
        });
    });
});

Update test mockPromt and expected


var webpack = require('webpack')
module.exports = {
    context: __dirname,
    entry: [
        "./index",
        "webpack-dev-server/client?http://localhost:8080",
        "webpack/hot/only-dev-server",
    ],
    output: {
        path: __dirname + "/public",
        filename: "bundle.js"
    },
    module: {
        loaders: [{
            test: /\.khufu$/,
            loaders: ['babel', '../../src/loader'],
            exclude: /node_modules/,
        }, {
            test: /\.js$/,
            loaders: ['babel'],
            exclude: /node_modules/,
        }],
    },
    resolve: {
        modulesDirectories: ["/usr/local/lib/node_modules", "../.."],
    },
    resolveLoader: {
        modulesDirectories: ["/usr/local/lib/node_modules"],
    },
    devServer: {
        contentBase: '.',
        hot: true,
    },
    khufu: {
        static_attrs: false, // for normal hot reload
    },
    plugins: [
        new webpack.NoErrorsPlugin(),
    ],
}

var webpack = require('webpack')
var DEV = process.env['NODE_ENV'] != 'production';
module.exports = {
    context: __dirname,
    entry: DEV ? [
        "./index",
        "webpack-dev-server/client?http://localhost:8080",
        "webpack/hot/only-dev-server",
    ] : "./index",
    output: {
        path: __dirname + "/public",
        filename: "bundle.js"
    },
    module: {
        loaders: [{
            test: /\.khufu$/,
            loaders: ['babel', '../../src/loader'],
            exclude: /node_modules/,
        }, {
            test: /\.js$/,
            loaders: ['babel'],
            exclude: /node_modules/,
        }],
    },
    resolve: {
        modulesDirectories: ["/usr/local/lib/node_modules", "../.."],
    },
    resolveLoader: {
        modulesDirectories: ["/usr/local/lib/node_modules"],
    },
    devServer: {
        contentBase: '.',
        hot: true,
    },
    khufu: {
        static_attrs: !DEV,
    },
    plugins: [
        new webpack.NoErrorsPlugin(),
    ],
}

Add non-hot-reload build support for playground


'use strict';

const router = require('express').Router();
const temperatureapi = require('../api/temperature');
const requiresApiAuthorization = require('../auth/requiresApiAuthorization');

router.get('/api/temperature/currentTemperature', requiresApiAuthorization(), temperatureapi.getCurrentTemperature);

module.exports = router;

'use strict';

const router = require('express').Router();
const temperatureapi = require('../api/temperature');
const requiresApiAuthorization = require('../auth/requiresApiAuthorization');
const auth = require('../auth/Authentication');

router.get('/api/temperature/currentTemperature', requiresApiAuthorization(), temperatureapi.getCurrentTemperature);

router.get('/api/temperature/', auth.getBearerHandler(), temperatureapi.getCurrentTemperature);

module.exports = router;

Add token authenticated temperature route