const { get, isEmpty } = require('lodash')

const config = require('../config')
const { authorisedRequest } = require('../lib/authorised-request')

/**
 * Allows users with the given feature flag to trial a new feature.
 *
 * Adding this middleware to a route will set a property / global variable
 * called "isFeatureTesting" on res.locals, this variable can then be used to
 * toggle features in any njk file.
 *
 * The feature name is also added to the query string in the format
 * `?featureTesting={feature}` for tracking via google analytics.
 *
 * Flags can be enabled on a per-user basis through Django.
 *
 * @param {String} feature - the feature to toggle layout for.
 */

module.exports = (feature) => async (req, res, next) => {
  if (!res.locals.userFeatures) {
    const user = await authorisedRequest(req, `${config.apiRoot}/whoami/`)
    res.locals.userFeatures = get(user, 'active_features', [])
  }
  res.locals.isFeatureTesting = res.locals.userFeatures.includes(feature)

  if (res.locals.isFeatureTesting && isEmpty(req.query)) {
    return res.redirect(`${req.originalUrl}?featureTesting=${feature}`)
  } else {
    next()
  }
}

const { get, isEmpty } = require('lodash')

const config = require('../config')
const { authorisedRequest } = require('../lib/authorised-request')

/**
 * Allows users with the given feature flag to trial a new feature.
 *
 * Adding this middleware to a route will set a property / global variable
 * called "isFeatureTesting" on res.locals, this variable can then be used to
 * toggle features in any njk file.
 *
 * The feature name is also added to the query string in the format
 * `?featureTesting={feature}` for tracking via google analytics.
 *
 * Flags can be enabled on a per-user basis through Django.
 *
 * @param {String} feature - the feature to toggle layout for.
 */

module.exports = (feature) => async (req, res, next) => {
  if (!res.locals.userFeatures) {
    const user = await authorisedRequest(req, `${config.apiRoot}/whoami/`)
    res.locals.userFeatures = get(user, 'active_features', [])
  }

  res.locals.isFeatureTesting = res.locals.userFeatures.includes(feature)

  if (res.locals.isFeatureTesting && !req.query.featureTesting) {
    res.redirect(
      `${req.originalUrl}${
        isEmpty(req.query) ? '?' : '&'
      }featureTesting=${feature}`
    )
  } else {
    next()
  }
}

Allow other query params when feature testing


const mkdirp = require('mkdirp');
const fs = require('fs');
const dirname = require('path').dirname;

// Create "this-is-a-post" from "This is a Post"
exports.slugger = str => str.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-');

// Random logo
exports.logoURL = () => `/img/bisnaer${Math.ceil(Math.random() * 31, 10)}.PNG`;

/**
 * Safely write a file to disk
 * @returns {Promise} File written promise
 */
exports.writefile = (filePath, content) => {
  const fileContent = (typeof content === 'object') ? JSON.stringify(content) : content;
  return new Promise((resolve, reject) => {
    mkdirp(dirname(filePath), (err) => {
      if (err) reject(err);

      fs.writeFile(filePath, fileContent, { encoding: 'utf-8' }, resolve);
    });
  });
};

const mkdirp = require('mkdirp');
const fs = require('fs');
const dirname = require('path').dirname;

// Create "this-is-a-post" from "This is a Post"
exports.slugger = str => str
  .toLowerCase()
  .replace(/ä/g, 'ae')
  .replace(/ö/g, 'oe')
  .replace(/ü/g, 'ue')
  .replace(/[^\w ]+/g, ' ')
  .replace(/ +/g, '-');

// Random logo
exports.logoURL = () => `/img/bisnaer${Math.ceil(Math.random() * 31, 10)}.PNG`;

/**
 * Safely write a file to disk
 * @returns {Promise} File written promise
 */
exports.writefile = (filePath, content) => {
  const fileContent = (typeof content === 'object') ? JSON.stringify(content) : content;
  return new Promise((resolve, reject) => {
    mkdirp(dirname(filePath), (err) => {
      if (err) reject(err);

      fs.writeFile(filePath, fileContent, { encoding: 'utf-8' }, resolve);
    });
  });
};

Convert umlaute in slugger urls


var EOL = require('os').EOL;

module.exports = {
  name: 'ember-cli-mocha',

  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  afterInstall: function() {
    var addonContext = this;

    return this.addBowerPackagesToProject([
      { name: 'ember-mocha-adapter',   source: 'ember-mocha-adapter',   target: '~0.3.1' },

    ]).then(function() {
      return addonContext.addPackageToProject('ember-cli-chai', '^0.2.0');

    }).then(function() {
      if ('removePackageFromProject' in addonContext) {
        return addonContext.removePackageFromProject('ember-cli-qunit');
      }
    });
  }
};

var EOL = require('os').EOL;

module.exports = {
  name: 'ember-cli-mocha',

  normalizeEntityName: function() {
    // this prevents an error when the entityName is
    // not specified (since that doesn't actually matter
    // to us
  },

  afterInstall: function() {
    var addonContext = this;

    return this.addBowerPackagesToProject([
      { name: 'ember-mocha-adapter',   source: 'ember-mocha-adapter',   target: '~0.3.1' },

    ]).then(function() {
      return addonContext.addPackageToProject('ember-cli-chai', '^0.3.0');

    }).then(function() {
      if ('removePackageFromProject' in addonContext) {
        return addonContext.removePackageFromProject('ember-cli-qunit');
      }
    });
  }
};

Update auto-installed "ember-cli-chai" to v0.3.0


var Application = require('application');

module.exports = (function() {

    function start() {
        window.app = new Application();
        window.app.start();
    }

    if (location.protocol === 'file:') {
        $.ajax({
            url: 'cordova.js',
            dataType: 'script'
        }).done(function() {
            document.addEventListener('deviceready', start, false);
        }).fail(function() {
            console.error(new Error('Unable to load cordova.js file.'));
        });
    } else {
        $(document).ready(start);
    }

})();

var Application = require('application');

module.exports = (function() {

    function prepareDOM() {
        document.body.innerHTML = '';
        document.body.appendChild(document.createElement('gelato-application'));
        document.body.appendChild(document.createElement('gelato-dialogs'));
        document.body.appendChild(document.createElement('gelato-navbars'));
        document.body.appendChild(document.createElement('gelato-sidebars'));
    }

    function start() {
        prepareDOM();
        window.app = new Application();
        window.app.start();
    }

    if (location.protocol === 'file:') {
        $.ajax({
            url: 'cordova.js',
            dataType: 'script'
        }).done(function() {
            document.addEventListener('deviceready', start, false);
        }).fail(function() {
            console.error(new Error('Unable to load cordova.js file.'));
        });
    } else {
        $(document).ready(start);
    }

})();

Prepare DOM with required custom gelato elements.


/* Filter for finding the next-smallest multiple of input number */
angular.module('sknFloorMultFilter', []).filter('floorMult', function() {
    return function(input, multSize) {
        if(input % multSize === 0) {
            return input;
        }
        else {
            var inputFloor = Math.floor(input);
            if(inputFloor % multSize === 0) {
                return inputFloor;
            }
            else {
                for(var i = 1; i < multSize; ++i) {
                    if((inputFloor - i) % multSize === 0) {
                        return inputFloor - i;
                    }
                }
            }
        return input;
        }
    }
});

/*Filter for month day, year from WP REST API date*/
angular.module('sknDateFilter', []).filter('date', function() {
    return function(input) {
        var months = [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December"
        ];
        var year = input.substr(0, 4);
        var month = months[parseInt(input.substr(5, 2)) - 1];
        var day = input.substr(8, 2);
        var dateString = day + " " + month + " " + year;
        return dateString;
    }
});

/* Filter for finding the next-smallest multiple of input number */
angular.module('sknFloorMultFilter', []).filter('floorMult', function() {
    return function(input, multSize) {
        /* if input is already a multiple of multSize, return */
        if(input % multSize === 0) {
            return input;
        }
        else {
            /* Ensure input is a whole number */
            var inputFloor = Math.floor(input);
            /* if inputFloor is a multiple of multSize, return inputFloor */
            if(inputFloor % multSize === 0) {
                return inputFloor;
            }
            else {
                /* subtract 1 from inputFloor until inputFloor
                is a mulple of multSize. Return modified inputFloor.*/
                for(var i = 1; i < multSize; ++i) {
                    if((inputFloor - i) % multSize === 0) {
                        return inputFloor - i;
                    }
                }
            }
        return input;
        }
    }
});

/*Filter for month day, year from WP REST API date*/
angular.module('sknDateFilter', []).filter('date', function() {
    return function(input) {
        var months = [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December"
        ];
        var year = input.substr(0, 4);
        var month = months[parseInt(input.substr(5, 2)) - 1];
        var day = input.substr(8, 2);
        var dateString = day + " " + month + " " + year;
        return dateString;
    }
});

Add better commenting to floorMult filter.


'use strict';
angular.module('Trendicity')

.controller('IntroCtrl', function($scope, $state, $ionicSlideBoxDelegate, $ionicSideMenuDelegate, $ionicHistory, localStorageService, GeolocationService) {

    //** Settings some default scope variables.
    $scope.fetchingPosition = false;
    $scope.noPosition = false;

  // Clear history since this might have been a forced redirect
  $ionicHistory.clearHistory();

  // Mark intro as seen
  //localStorageService.set('seenIntro', true);

  // Called to navigate to the main app
  $scope.startApp = function() {
    $state.go('app.home.map');
  };

  $scope.next = function() {
    $ionicSlideBoxDelegate.next();
  };

  $scope.previous = function() {
    $ionicSlideBoxDelegate.previous();
  };

    $scope.getCurrentPosition = function () {
        $scope.fetchingPosition = true;
        GeolocationService.getCurrentPosition()
            .then(
                function (position) {
                    $scope.next();
                },
                function (fallbackPosition) {
                    // Show error message
                    $scope.noPosition = true;
                }
            )
            ['finally'](function () {
                $scope.fetchingPosition = false;
            });
    };

  // Called each time the slide changes
  $scope.slideChanged = function(index) {
    $scope.slideIndex = index;
  };
});

'use strict';
angular.module('Trendicity')

.controller('IntroCtrl', function($scope, $state, $ionicSlideBoxDelegate, $ionicSideMenuDelegate, $ionicHistory, localStorageService, GeolocationService) {

    //** Settings some default scope variables.
    $scope.fetchingPosition = false;
    $scope.noPosition = false;

  // Clear history since this might have been a forced redirect
  $ionicHistory.clearHistory();

  $scope.$on('$ionicView.beforeEnter', function() {
    $ionicSlideBoxDelegate.refresh();
  });

  // Mark intro as seen
  localStorageService.set('seenIntro', true);

  // Called to navigate to the main app
  $scope.startApp = function() {
    $state.go('app.home.map');
  };

  $scope.next = function() {
    $ionicSlideBoxDelegate.next();
  };

  $scope.previous = function() {
    $ionicSlideBoxDelegate.previous();
  };

    $scope.getCurrentPosition = function () {
        $scope.fetchingPosition = true;
        GeolocationService.getCurrentPosition()
            .then(
                function (position) {
                    $scope.next();
                },
                function (fallbackPosition) {
                    // Show error message
                    $scope.noPosition = true;
                }
            )
            ['finally'](function () {
                $scope.fetchingPosition = false;
            });
    };

  // Called each time the slide changes
  $scope.slideChanged = function(index) {
    $scope.slideIndex = index;
  };
});

Fix issue with About slidebox view needing to be refreshed when coming from map view


EpubReflowable.ReflowableCustomizer = Backbone.Model.extend({

    initialize : function (attributes, options) {

        this.$parentEl = $(this.get("parentElement"));
        this.set("customBorder", new EpubReflowable.ReflowableCustomBorder({ targetElement : this.get("readiumFlowingContent") }));
        this.set("customTheme", new EpubReflowable.ReflowableCustomTheme({ iframeElement : this.get("readiumFlowingContent") }));
    },

    // ----- PUBLIC INTERFACE -------------------------------------------------------------------

    setCustomStyle : function (customProperty, styleNameOrCSS) {

        if (customProperty === "border") {
            this.get("customBorder").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "spine-divider") {
            this.get("spineDividerStyleView").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "page-border") {
            this.get("customBorder").setCurrentStyle(styleNameOrCSS);
            this.get("spineDividerStyleView").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "page-theme") {
            this.get("customTheme").setCurrentStyle(styleNameOrCSS);
        }
    }

    // ----- PRIVATE HELPERS -------------------------------------------------------------------
});

EpubReflowable.ReflowableCustomizer = Backbone.Model.extend({

    initialize : function (attributes, options) {

        this.$parentEl = $(this.get("parentElement"));
        this.set("customBorder", new EpubReflowable.ReflowableCustomBorder({ targetElement : this.get("readiumFlowingContent") }));
        this.set("customTheme", new EpubReflowable.ReflowableCustomTheme({ iframeElement : this.get("readiumFlowingContent") }));
    },

    // ----- PUBLIC INTERFACE -------------------------------------------------------------------

    setCustomStyle : function (customProperty, styleNameOrCSS) {

        if (customProperty === "reflowable-epub-border" || customProperty === "epub-border") {
            this.get("customBorder").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "reflowable-spine-divider" || customProperty === "spine-divider") {
            this.get("spineDividerStyleView").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "reflowable-page-border" || customProperty === "page-border") {
            this.get("customBorder").setCurrentStyle(styleNameOrCSS);
            this.get("spineDividerStyleView").setCurrentStyle(styleNameOrCSS);
        }
        else if (customProperty === "reflowable-page-theme") {
            this.get("customTheme").setCurrentStyle(styleNameOrCSS);
        }
    }

    // ----- PRIVATE HELPERS -------------------------------------------------------------------
});

Update the names of custom properties in the epub-reflowable module


#!/usr/bin/env node
const pkg = require('../package.json')

// @todo Figure out a better way to detect if we're running in "development mode"
const serverDevMode = Boolean(pkg._id) === false
const appDevMode = process.argv.indexOf('--dev') !== -1
const port = process.env.PORT || 3910 // eslint-disable-line no-process-env

if (serverDevMode) {
  require('babel-register')
}

const target = appDevMode ? 'devServer' : 'prodServer'
const sourceFolder = serverDevMode ? '../src/' : '../lib/'
const getServer = require(sourceFolder + target).default

getServer().listen(port)

#!/usr/bin/env node
const pkg = require('../package.json')

// @todo Figure out a better way to detect if we're running in "development mode"
const serverDevMode = Boolean(pkg._id) === false
const appDevMode = process.argv.indexOf('--dev') !== -1
const port = process.env.PORT || 3333 // eslint-disable-line no-process-env

if (serverDevMode) {
  require('babel-register')
}

const target = appDevMode ? 'devServer' : 'prodServer'
const sourceFolder = serverDevMode ? '../src/' : '../lib/'
const getServer = require(sourceFolder + target).default

getServer().listen(port)

Change default port to 3333


const ascension = require('ascension')
const sort = ascension([ Number ], entry => [ entry.when ])

const Isochronous = require('isochronous')

class Printer {
    constructor (destructible, write, format, interval) {
        this._write = write
        this._entries = []
        this._format = format
        const isochronous = new Isochronous(interval, true, this._check.bind(this))
        destructible.durable('isochronous', isochronous.start())
        destructible.destruct(() => isochronous.stop())
        destructible.destruct(() => {
            this._entries.sort(sort)
            this._log(this._entries.splice(0))
        })
    }

    say (label, entry) {
        this._entries.push({ when: Date.now(), qualifier: 'prolific', label, ...entry })
    }

    push (envelope) {
        this._entries.push.apply(this._entries, envelope.entries)
    }

    _log (entries) {
        if (entries.length != 0) {
            this._write.call(null, entries.map(this._format).join('\n'))
        }
    }

    _check (status) {
        this._entries.sort(sort)
        const stop = status.scheduled - Math.ceil(status.interval * 1.51)
        let i, I
        for (i = 0, I = this._entries.length; i < I; i++) {
            if (this._entries[i].when > stop) {
                break
            }
        }
        this._log(this._entries.splice(0, i))
    }
}

module.exports = Printer

const ascension = require('ascension')
const sort = ascension([ Number ], entry => [ entry.when ])

const Isochronous = require('isochronous')

class Printer {
    constructor (destructible, write, format, interval) {
        this._write = write
        this._entries = []
        this._format = format
        const isochronous = new Isochronous(interval, true, this._check.bind(this))
        destructible.durable('isochronous', isochronous.start())
        destructible.destruct(() => isochronous.stop())
        destructible.destruct(() => this._log(this._entries.splice(0).sort(sort)))
    }

    say (label, entry) {
        this._entries.push({ when: Date.now(), qualifier: 'prolific', label, ...entry })
    }

    push (envelope) {
        this._entries.push.apply(this._entries, envelope.entries)
    }

    _log (entries) {
        if (entries.length != 0) {
            this._write.call(null, entries.map(this._format).join('\n'))
        }
    }

    _check (status) {
        this._entries.sort(sort)
        const stop = status.scheduled - Math.ceil(status.interval * 1.51)
        let i, I
        for (i = 0, I = this._entries.length; i < I; i++) {
            if (this._entries[i].when > stop) {
                break
            }
        }
        this._log(this._entries.splice(0, i))
    }
}

module.exports = Printer

Convert function to a one-liner.


import Vue from 'vue';
import Router from 'vue-router';
import FileManager from 'components/FileManager';
import Content from 'components/editor-viewer/Content';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'fileManager',
      component: FileManager,
    },
    {
      path: '/content',
      name: 'Content',
      component: Content,
    },
  ],
});

import Vue from 'vue';
import Router from 'vue-router';
import FileManager from 'components/file-manager/FileManager';
import Content from 'components/editor-viewer/Content';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'fileManager',
      component: FileManager,
    },
    {
      path: '/content',
      name: 'Content',
      component: Content,
    },
  ],
});

Edit FileManager path in router


/*global requirejs*/
requirejs.config({
  paths: {
    meiosis: "../lib/meiosis.min",
    meiosisVanillaJs: "../lib/meiosis-vanillajs.min"
  }
});

requirejs(["require", "meiosis", "meiosisVanillaJs", "./model", "./view", "./ready", "./receiveUpdate"],
  function(require) {
    var meiosis = require("meiosis");
    var meiosisVanillaJs = require("meiosisVanillaJs");

    var model = require("./model");
    var view = require("./view");
    var ready = require("./ready");
    var receiveUpdate = require("./receiveUpdate");

    var renderer = meiosisVanillaJs.renderer;
    var Meiosis = meiosis.init(renderer.intoId("app"));

    var createComponent = Meiosis.createComponent;

    var Main = createComponent({
      initialModel: model.initialModel,
      view: view,
      ready: ready,
      receiveUpdate: receiveUpdate
    });

    Meiosis.run(Main);
  }
);

/*global requirejs*/
requirejs.config({
  paths: {
    meiosis: "../lib/meiosis.min",
    meiosisVanillaJs: "../lib/meiosis-vanillajs.min",
    meiosisTracer: "../lib/meiosis-tracer.min"
  }
});

requirejs(["require", "meiosis", "meiosisVanillaJs", "meiosisTracer",
    "./model", "./view", "./ready", "./receiveUpdate"
  ],
  function(require) {
    var meiosis = require("meiosis");
    var meiosisVanillaJs = require("meiosisVanillaJs");
    var meiosisTracer = require("meiosisTracer");

    var model = require("./model");
    var view = require("./view");
    var ready = require("./ready");
    var receiveUpdate = require("./receiveUpdate");

    var renderer = meiosisVanillaJs.renderer;
    var Meiosis = meiosis.init(renderer.intoId("app"));

    var createComponent = Meiosis.createComponent;

    var Main = createComponent({
      initialModel: model.initialModel,
      view: view,
      ready: ready,
      receiveUpdate: receiveUpdate
    });

    var renderRoot = Meiosis.run(Main);

    meiosisTracer(createComponent, renderRoot, "#tracer");
  }
);

Add meiosis-tracer to requirejs example.


import { fail, danger } from 'danger';
import { flatten, intersection, isEmpty } from 'lodash';

const pkg = require('./package.json'); // eslint-disable-line import/newline-after-import
const prLogConfig = pkg['pr-log'];

const Versions = {
  PATCH: 'PATCH',
  MINOR: 'MINOR',
  MAJOR: 'MAJOR',
};

const branchVersion = Versions.PATCH;

const checkRequiredLabels = labels => {
  const forbiddenLabels = flatten([
    'do not merge',
    'in progress',
    branchVersion !== Versions.MAJOR ? 'BREAKING CHANGE' : [],
    branchVersion === Versions.PATCH ? 'feature request' : [],
  ]);

  const requiredLabels = flatten([
    prLogConfig.skipLabels || [],
    Object.keys(prLogConfig.validLabels || {}),
  ]);

  const blockingLabels = intersection(forbiddenLabels, labels);
  if (!isEmpty(blockingLabels)) {
    fail(
      `PR is marked with ${blockingLabels.map(label => `"${label}"`).join(', ')} label${
        blockingLabels.length > 1 ? 's' : ''
      }.`
    );
  }

  const foundLabels = intersection(requiredLabels, labels);
  if (isEmpty(foundLabels)) {
    fail(`PR is not labeled with one of: ${JSON.stringify(requiredLabels)}`);
  }
};

if (prLogConfig) {
  const { labels } = danger.github.issue;
  checkRequiredLabels(labels.map(l => l.name));
}

import { fail, danger } from 'danger';
import { flatten, intersection, isEmpty } from 'lodash';

const pkg = require('./package.json'); // eslint-disable-line import/newline-after-import
const prLogConfig = pkg['pr-log'];

const Versions = {
  PATCH: 'PATCH',
  MINOR: 'MINOR',
  MAJOR: 'MAJOR',
};

const branchVersion = Versions.MAJOR;

const checkRequiredLabels = labels => {
  const forbiddenLabels = flatten([
    'do not merge',
    'in progress',
    branchVersion !== Versions.MAJOR ? 'BREAKING CHANGE' : [],
    branchVersion === Versions.PATCH ? 'feature request' : [],
  ]);

  const requiredLabels = flatten([
    prLogConfig.skipLabels || [],
    Object.keys(prLogConfig.validLabels || {}),
  ]);

  const blockingLabels = intersection(forbiddenLabels, labels);
  if (!isEmpty(blockingLabels)) {
    fail(
      `PR is marked with ${blockingLabels.map(label => `"${label}"`).join(', ')} label${
        blockingLabels.length > 1 ? 's' : ''
      }.`
    );
  }

  const foundLabels = intersection(requiredLabels, labels);
  if (isEmpty(foundLabels)) {
    fail(`PR is not labeled with one of: ${JSON.stringify(requiredLabels)}`);
  }
};

if (prLogConfig) {
  const { labels } = danger.github.issue;
  checkRequiredLabels(labels.map(l => l.name));
}

Set branch version to MAJOR


Meteor.methods({
  'sendEmail': function (to, from, subject, text) {
    check([to, from, subject, text], [String]);

    // Let other method calls from the same client start running,
    // without waiting for the email sending to complete.
    this.unblock();

    // Construct a message object
    var messageObject = {
      to: to,
      from: from,
      subject: subject,
      text: text
    };

    Meteor.Mailgun.send(messageObject);
  },
  'sendInvitationEmail': function (invitationId) {
    check([invitationId], [String]);
    this.unblock();

    // Get the application absolute URL
    var absoluteUrl = Meteor.absoluteUrl();

    // Get the application base url, without trailing slash
    var baseUrl;

    if (absoluteUrl.substr(-1) === '/') {
        baseUrl = absoluteUrl.substr(0, absoluteUrl.length - 1);
    } else {
      baseUrl = absoluteUrl;
    }

    // Construct an invitation URL
    var invitationUrl = baseUrl + "/invitation/" + invitationId;

    // Construct a from address
    var from = 'contact@' + baseUrl;

    // Create a basic message text
    var message = "You have an invitation at " + invitationUrl;

    // Get invitation
    var invitation = Invitations.findOne(invitationId);

    // Get invitation title
    var title = invitation.title;

    // Get recipient email addresses
    var recipients = invitation.emails;

    for (index in recipients) {
      // Send email to recipient
      Meteor.call('sendEmail', recipients[index], from, title, message);
    };
  }
});

Meteor.methods({
  'sendEmail': function (to, from, subject, html) {
    check([to, from, subject, html], [String]);

    // Let other method calls from the same client start running,
    // without waiting for the email sending to complete.
    this.unblock();

    // Construct a message object
    var messageObject = {
      to: to,
      from: from,
      subject: subject,
      html: html
    };

    Meteor.Mailgun.send(messageObject);
  },
  'sendInvitationEmail': function (invitationId) {
    check([invitationId], [String]);
    this.unblock();

    // Get base URL from settings
    var settings = Settings.findOne({}, {fields: {baseUrl: 1}});

    // Get the application base url, without trailing slash
    var baseUrl = new URI(settings.baseUrl);

    // Get host from base URL
    var host = baseUrl.host();

    // Construct an invitation URL
    var invitationUrl = host + "/invitation/" + invitationId;

    // Construct a from address
    var fromAddress = 'contact@' + host;

    // Get invitation
    var invitation = Invitations.findOne(invitationId);

    // Create a basic message text
    invitation.message = "You have an invitation at " + invitationUrl;

    // Get invitation title
    var subject = invitation.title;

    // Render the html
    var html = SSR.render("invitationEmail", invitation);

    // Get recipient email addresses
    var recipients = invitation.emails;

    for (index in recipients) {
      // Get current recipient
      var recipient = recipients[index];

      // Send email to recipient
      Meteor.call('sendEmail', recipient, fromAddress, subject, html);
    };
  }
});

Switch to HTML with SSR package


var gulp = require('gulp');
var exec = require('child_process').exec;

var IMAGE_TAG = process.env.DOCKER_IMAGE_TAG || 'ottogiron/hapiember';

gulp.task('build-docker', function(callback){

  var dockerBuild = exec('docker build -f docker/Dockerfile -t '+ IMAGE_TAG +' .');

  dockerBuild.stdout.on('data', function (data) {
    console.log(data.toString());
  });

  dockerBuild.stderr.on('data', function (data) {
    console.log(data.toString());
  });

  return callback();

});

var gulp = require('gulp');
var exec = require('child_process').exec;

var IMAGE_TAG = process.env.DOCKER_IMAGE_TAG || 'ottogiron/hapiember';

gulp.task('build-docker', function(callback){

  var dockerBuild = exec('docker build --no-cache -f docker/Dockerfile -t '+ IMAGE_TAG +' .');

  dockerBuild.stdout.on('data', function (data) {
    console.log(data.toString());
  });

  dockerBuild.stderr.on('data', function (data) {
    console.log(data.toString());
  });

  return callback();

});

Add no-cache flag to docker build


module.exports = {
    apps : [
        {
          name: "CaSS",
          script: "./src/main/server.js",
          watch: true,
          ignore_watch: ["logs"],
          instances: 1,
          log_file: 'logs/cass.log',
          env: {
            "CASS_LOOPBACK": "http://localhost:8080/api/",
            "ELASTICSEARCH_ENDPOINT": "http://localhost:9200",
            "PORT": "8080",
            "CASS_BASE": "/cass"
          },
          node_args: [
            "--max-old-space-size=512"
          ]
        }
    ]
  }

module.exports = {
    apps : [
        {
          name: "CaSS",
          script: "./src/main/server.js",
          watch: true,
          ignore_watch: ["logs", "node_modules"],
          instances: 1,
          log_file: 'logs/cass.log',
          env: {
            "CASS_LOOPBACK": "http://localhost:8080/api/",
            "ELASTICSEARCH_ENDPOINT": "http://localhost:9200",
            "PORT": "8080",
            "CASS_BASE": "/cass"
          },
          node_args: [
            "--max-old-space-size=512"
          ]
        }
    ]
  }

Add node modules to ignore watch


exports.id = function(post){
  return post.id || post._id;
};

exports.title = function(post){
  return post.slug;
};

exports.year = function(post){
  return post.date.format('YYYY');
};

exports.month = function(post){
  return post.date.format('MM');
};

exports.day = function(post){
  return post.date.format('DD');
};

exports.category = function(post){
  var categories = post.categories;

  if (categories.length){
    var category = categories[categories.length - 1],
      Category = hexo.model('Category'),
      slug = Category.get(category).slug;

    return slug;
  } else {
    return hexo.config.default_category;
  }
};

exports.id = function(post){
  return post.id || post._id;
};

exports.title = function(post){
  return post.slug;
};

exports.year = function(post){
  return post.date.format('YYYY');
};

exports.month = function(post){
  return post.date.format('MM');
};

exports.day = function(post){
  return post.date.format('DD');
};

exports.i_month = function(post){
  return post.date.format('M');
};

exports.i_day = function(post){
  return post.date.format('D');
};

exports.category = function(post){
  var categories = post.categories;

  if (categories.length){
    var category = categories[categories.length - 1],
      Category = hexo.model('Category'),
      slug = Category.get(category).slug;

    return slug;
  } else {
    return hexo.config.default_category;
  }
};

Add 2 permalink parameters: i_month and i_day


class Timer {

  start(code, delay) {
    this.timeoutObj = setTimeout(function() {
      this.timeoutObj = null;
      code();
    }, delay);

    this.timeoutObj.startTime = Date.now();
  }

  isRunning() {
    return this.timeoutObj ? true : false;
  }

  stop() {
    if(this.timeoutObj) {
      clearTimeout(this.timeoutObj);
      var t = Date.now() - this.timeoutObj.startTime;
      this.timeoutObj = null;
      return t;
    }
    return null;
  }
}

export default Timer;

class Timer {

  start(code, delay) {
    if(!delay) return;

    this.timeoutObj = setTimeout(function() {
      this.timeoutObj = null;
      code();
    }, delay);

    this.timeoutObj.startTime = Date.now();
  }

  isRunning() {
    return this.timeoutObj ? true : false;
  }

  stop() {
    if(this.timeoutObj) {
      clearTimeout(this.timeoutObj);
      var t = Date.now() - this.timeoutObj.startTime;
      this.timeoutObj = null;
      return t;
    }
    return null;
  }
}

export default Timer;

Fix bug in games with no move timeout


$('input.convert').click(convert);

function convert() {
    var input   =   $('input.swatchInput').val();
    var colours =   input.split(' ');
    var output  =   new Array((colours[0] / 255).toFixed(3).replace('.000',''), (colours[1] / 255).toFixed(3).replace('.000',''), (colours[2] / 255).toFixed(3).replace('.000',''));
    $('input.swatchOutput').val(output[0] + ' ' + output[1] + ' ' + output[2]);
}

// Swatch input/output text colourisation
$(function(){
    $('input.swatchInput').keyup(function(){
        // Split the input value into a space-delimited array
        var value = $(this).val().split(' ');

        // If the values haven't been typed yet, default to 0
        if (value[0] == null) { value[0] = 0; }
        if (value[1] == null) { value[1] = 0; }
        if (value[2] == null) { value[2] = 0; }

        // Make 'inputColor' an rgb() swatch using the values typed
        inputColour = 'rgb(' + value[0] + ',' + value[1] + ',' + value[2] + ')';

        // Change the input text colour to the colour being typed
        $(this).css('color', inputColour);
    });
});

Add a function to change text colour of input to colour being typed


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

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

Use the babel-runtime to fix the build


/* eslint-disable no-underscore-dangle */
import last from 'lodash/last';
/**
 * Change the Scope UI theme from normal to high-contrast.
 * This will inject a stylesheet into <head> and override the styles.
 *
 * A window-level variable is written to the .html page during the build process that contains
 * the filename (and content hash) needed to download the file.
 */

function getFilename(href) {
  return last(href.split('/'));
}

export function loadTheme(theme = 'normal') {
  if (window.__WEAVE_SCOPE_THEMES) {
    // Load the pre-built stylesheet.
    const stylesheet = window.__WEAVE_SCOPE_THEMES[theme];
    const head = document.querySelector('head');
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = `${window.__WEAVE_SCOPE_THEMES.publicPath}${stylesheet}`;
    link.onload = () => {
      // Remove the old stylesheet to prevent weird overlapping styling issues
      const oldTheme = theme === 'normal' ? 'contrast' : 'normal';
      const links = document.querySelectorAll('head link');
      for (let i = 0; i < links.length; i += 1) {
        const l = links[i];
        if (getFilename(l.href) === getFilename(window.__WEAVE_SCOPE_THEMES[oldTheme])) {
          head.removeChild(l);
          break;
        }
      }
    };

    head.appendChild(link);
  }
}

/* eslint-disable no-underscore-dangle */
import last from 'lodash/last';
/**
 * Change the Scope UI theme from normal to high-contrast.
 * This will inject a stylesheet into <head> and override the styles.
 *
 * A window-level variable is written to the .html page during the build process that contains
 * the filename (and content hash) needed to download the file.
 */

function getFilename(href) {
  return last(href.split('/'));
}

export function loadTheme(theme = 'normal') {
  if (window.__WEAVE_SCOPE_THEMES) {
    // Load the pre-built stylesheet.
    const stylesheet = window.__WEAVE_SCOPE_THEMES[theme];
    const head = document.querySelector('head');
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = stylesheet;
    link.onload = () => {
      // Remove the old stylesheet to prevent weird overlapping styling issues
      const oldTheme = theme === 'normal' ? 'contrast' : 'normal';
      const links = document.querySelectorAll('head link');
      for (let i = 0; i < links.length; i += 1) {
        const l = links[i];
        if (getFilename(l.href) === getFilename(window.__WEAVE_SCOPE_THEMES[oldTheme])) {
          head.removeChild(l);
          break;
        }
      }
    };

    head.appendChild(link);
  }
}

Fix for contrast stylesheet public path


// `window.ActiveXObject` returns undefined in IE11 (as well as non-IE browsers)
// `"ActiveXObject" in window` returns `true` in all IE versions
// only IE11 will pass _both_ of these conditions
export const isIE11 = !window.ActiveXObject && 'ActiveXObject' in window;
export const isIE = 'ActiveXObject' in window;

export const isEdge = navigator.userAgent.indexOf('Edge') >= 0;

// `window.ActiveXObject` is "falsey" in IE11 (but not `undefined` or `false`)
// `"ActiveXObject" in window` returns `true` in all IE versions
// only IE11 will pass _both_ of these conditions
export const isIE11 = !window.ActiveXObject && 'ActiveXObject' in window;
export const isIE = 'ActiveXObject' in window;

export const isEdge = navigator.userAgent.indexOf('Edge') >= 0;

Fix comment around IE11 detection.


import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import BaseTouchable from '../../utils/rnw-compat/BaseTouchable';
import useActionElement from '../../hooks/useActionElement';
import action from '../../utils/action';

const propTypes = {
  // eslint-disable-next-line react/forbid-foreign-prop-types
  ...action.propTypes,
  children: PropTypes.node.isRequired,
  disabled: PropTypes.bool,
  active: PropTypes.bool,
};

const defaultProps = {
  ...action.defaultProps,
  disabled: false,
  active: false,
};

const ActionListGroupItem = React.forwardRef(function ActionListGroupItem(
  props,
  ref,
) {
  const { disabled, active, ...elementProps } = props;

  const classes = cx(
    // constant classes
    'list-group-item',
    'list-group-item-action',
    // variable classes
    disabled && 'disabled',
    active && 'active',
  );

  const createElement = useActionElement(BaseTouchable, {
    disabled,
    elementProps,
    ref,
  });

  return createElement({
    className: classes,
  });
});

ActionListGroupItem.displayName = 'ActionListGroupItem';
ActionListGroupItem.propTypes = propTypes;
ActionListGroupItem.defaultProps = defaultProps;

export default ActionListGroupItem;

import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import BaseTouchable from '../../utils/rnw-compat/BaseTouchable';
import useActionElement from '../../hooks/useActionElement';
import action from '../../utils/action';

const propTypes = {
  // eslint-disable-next-line react/forbid-foreign-prop-types
  ...action.propTypes,
  children: PropTypes.node.isRequired,
  disabled: PropTypes.bool,
  active: PropTypes.bool,
};

const defaultProps = {
  ...action.defaultProps,
  disabled: false,
  active: false,
};

const ActionListGroupItem = React.forwardRef(function ActionListGroupItem(
  props,
  ref,
) {
  const { disabled, active, ...elementProps } = props;

  const classes = cx(
    // constant classes
    'list-group-item',
    'list-group-item-action',
    // variable classes
    disabled && 'disabled',
    active && 'active',
  );

  const createElement = useActionElement(
    BaseTouchable,
    {
      disabled,
      ...elementProps,
    },
    ref,
  );

  return createElement({
    className: classes,
  });
});

ActionListGroupItem.displayName = 'ActionListGroupItem';
ActionListGroupItem.propTypes = propTypes;
ActionListGroupItem.defaultProps = defaultProps;

export default ActionListGroupItem;

Fix touchable list group item


'use babel'

import { dirname, basename } from 'path'

function getPath (path) {
  let [projectPath, filePath] = atom.project.relativizePath(path)

  if (!projectPath) {
    projectPath = dirname(filePath.toString())
    filePath = basename(filePath.toString())
  }

  return { projectPath, filePath }
}

function getActiveFile () {
  let editor = atom.workspace.getActivePaneItem()
    if (editor) {
      if (editor.file) {
        return editor.file.path
      } else if (editor.buffer && editor.buffer.file) {
        return editor.buffer.file.path
      }
    }
}

function getUserConfig (projectPath) {
  try {
    return require(projectPath + '/bs-config.js')
  } catch(err) {
    return {}
  }
}

function onlineConfig (value) {
  switch (value) {
    case 0:
      return false
      break;
    case 1:
      return undefined
      break;
    case 2:
      return true
      break;
  }
}

export { getPath, getActiveFile, getUserConfig, onlineConfig }

'use babel'

import { dirname, basename } from 'path'

function getPath (path) {
  let [projectPath, filePath] = atom.project.relativizePath(path)

  if (!projectPath) {
    projectPath = filePath ? dirname(filePath) : null
    filePath = filePath ? basename(filePath) : null
  }

  return { projectPath, filePath }
}

function getActiveFile () {
  let editor = atom.workspace.getActivePaneItem()
    if (editor) {
      if (editor.file) {
        return editor.file.path
      } else if (editor.buffer && editor.buffer.file) {
        return editor.buffer.file.path
      }
    }
}

function getUserConfig (projectPath) {
  try {
    return require(projectPath + '/bs-config.js')
  } catch(err) {
    return {}
  }
}

function onlineConfig (value) {
  switch (value) {
    case 0:
      return false
      break;
    case 1:
      return undefined
      break;
    case 2:
      return true
      break;
  }
}

export { getPath, getActiveFile, getUserConfig, onlineConfig }

Fix error when activating on empty editor


var HtmlReporter = require('protractor-html-screenshot-reporter');

exports.config = {
  allScriptsTimeout: 11000,

  specs: [
    '*.js'
  ],

  multiCapabilities: [{
      'browserName': 'chrome'
  }, {
      'browserName': 'firefox'
  }],

  baseUrl: 'http://localhost:9001/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  },

  onPrepare: function() {
    // Add a screenshot reporter and store screenshots to `/tmp/screnshots`:
    jasmine.getEnv().addReporter(new HtmlReporter({
      baseDirectory: 'e2e/reports'
    }));
  }
};

var HtmlReporter = require('protractor-html-screenshot-reporter');

exports.config = {
  allScriptsTimeout: 11000,

  specs: [
    '*.js'
  ],

  multiCapabilities: [{
      'browserName': 'chrome'
  // }, {
  //     'browserName': 'firefox'
  }],

  baseUrl: 'http://localhost:9001/',

  framework: 'jasmine',

  jasmineNodeOpts: {
    defaultTimeoutInterval: 30000
  },

  onPrepare: function() {
    // Add a screenshot reporter and store screenshots to `/tmp/screnshots`:
    jasmine.getEnv().addReporter(new HtmlReporter({
      baseDirectory: 'e2e/reports'
    }));
  }
};

Disable firefox for e2e tests.


/*
 * Server
 *
 */

// Define dependencies
const http = require('http');
const payload = require('request-payload');

// Create module object
var server = {};

// Is listening already
server.isListening = false;

// Listen
server.listen = function(){
    if(!server.isListening){
        server.httpServer = http.createServer((req,res) => {
            // Add the payload to the request
            payload(req, function(body) {
                req.body = body;
                // Pass the request to the request processor
                app.lib.req.process(req,res);
            });
        });
        var port = typeof(app.config.port) == 'number' ? app.config.port : 3000;
        server.httpServer.listen(port);
        server.isListening = true;
        app.lib.log.msg('Petite microservice listening on port '+port);
        return true;
    } else {
        return false;
    }
};

// Stop listening
server.close = function(callback){
    callback = typeof(callback) == 'function' ? callback : false;
    if(server.isListening){
      server.httpServer.close(function () {
        server.isListening = false;
        if(callback){
            callback(true);
        }
      });
    } else {
      if(callback){
        callback(false);
      }
    }
};

// Export the module
module.exports = server;

/*
 * Server
 *
 */

// Define dependencies
const http = require('http');
const StringDecoder = require('string_decoder').StringDecoder;

// Create module object
var server = {};

// Is listening already
server.isListening = false;

// Listen
server.listen = function(){
    if(!server.isListening){
        server.httpServer = http.createServer((req,res) => {

            // Add the payload to the request
            var decoder = new StringDecoder('utf-8');
            var buffer = '';

            // This section inspired by the 'request-payload' npm module
            req.on('data', function(data) {
                buffer += decoder.write(data);
            });
            req.on('end', function() {
                req.body = buffer + decoder.end();
                // Pass the request to the request processor
                app.lib.req.process(req,res);
            });

        });
        var port = typeof(app.config.port) == 'number' ? app.config.port : 3000;
        server.httpServer.listen(port);
        server.isListening = true;
        app.lib.log.msg('Petite microservice listening on port '+port);
        return true;
    } else {
        return false;
    }
};

// Stop listening
server.close = function(callback){
    callback = typeof(callback) == 'function' ? callback : false;
    if(server.isListening){
      server.httpServer.close(function () {
        server.isListening = false;
        if(callback){
            callback(true);
        }
      });
    } else {
      if(callback){
        callback(false);
      }
    }
};

// Export the module
module.exports = server;

Remove request-payload as a dependency


'use strict';

/**
 * An asynchronous bootstrap function that runs before
 * your application gets lifted.
 *
 * This gives you an opportunity to set up your data model,
 * run jobs, or perform some special logic.
 */

module.exports.bootstrap = function (cb) {
  const fixtures = require('../../api/user/config/fixtures/index');

  fixtures.role.create().then(function () {
    return fixtures.route.create();
  })
    .then(function () {
      cb();
    })
    .catch(function (err) {
      strapi.log.error(err);
      cb(err);
    });
};

'use strict';

/**
 * An asynchronous bootstrap function that runs before
 * your application gets lifted.
 *
 * This gives you an opportunity to set up your data model,
 * run jobs, or perform some special logic.
 */

module.exports.bootstrap = function (cb) {
  const fixtures = require('../../api/user/config/fixtures/index');

  fixtures.role.create()
    .then(function () {
      return fixtures.route.create();
    })
    .then(function () {
      cb();
    })
    .catch(function (err) {
      strapi.log.error(err);
      cb(err);
    });
};

Fix indentation on boostrap function


import EventEmitter from 'events';

const cable = {
  __proto__: EventEmitter.prototype,

  init(websocket) {
    this.emit('ready', websocket);
  },

  get websocket() {
    return new Promise((resolve, reject) => {
      if (this._websocket) {
        resolve(this._websocket);
      } else {
        this.on('ready', websocket => {
          resolve(this._websocket = this._websocket || websocket);
        });
      }
    });
  }
};

const cp = async (msg, comment, opts) => (await cable.websocket).
  send(JSON.stringify({msg, comment, opts}));

export {
  cable,
  cp
};

import EventEmitter from 'events';

const cable = {
  __proto__: EventEmitter.prototype,

  init(websocket) {
    if (this._websocket) {
      this._websocket.terminate();
    }
    this._websocket = websocket;
    this.emit('ready');
  },

  get websocket() {
    return new Promise((resolve, reject) => {
      if (this._websocket) {
        resolve(this._websocket);
      } else {
        this.on('ready', () => resolve(this._websocket));
      }
    });
  }
};

const cp = async (msg, comment, opts) => (await cable.websocket).
  send(JSON.stringify({msg, comment, opts}));

export {
  cable,
  cp
};

Store the last peek42 websocket reference (and terminate the previous)


var services = {
  'travis' : require('./services/travis'),
  'circle' : require('./services/circle'),
  'buildkite' : require('./services/buildkite'),
  'codeship' : require('./services/codeship'),
  'drone' : require('./services/drone'),
  'appveyor' : require('./services/appveyor'),
  'wercker' : require('./services/wercker'),
  'jenkins' : require('./services/jenkins'),
  'semaphore' : require('./services/semaphore'),
  'snap' : require('./services/snap'),
  'gitlab' : require('./services/gitlab')
};

var detectProvider = function(){
  var config;
  for (var name in services){
    if (services[name].detect()){
      config = services[name].configuration();
      break;
    }
  }
  if (!config){
    var local = require('./services/localGit');
    config = local.configuration();
    if (!config){
      throw new Error("Unknown CI servie provider. Unable to upload coverage.");
    }
  }
  return config;
};

module.exports = detectProvider;

var services = {
  'travis' : require('./services/travis'),
  'circle' : require('./services/circle'),
  'buildkite' : require('./services/buildkite'),
  'codeship' : require('./services/codeship'),
  'drone' : require('./services/drone'),
  'appveyor' : require('./services/appveyor'),
  'wercker' : require('./services/wercker'),
  'jenkins' : require('./services/jenkins'),
  'semaphore' : require('./services/semaphore'),
  'snap' : require('./services/snap'),
  'gitlab' : require('./services/gitlab')
};

var detectProvider = function(){
  var config;
  for (var name in services){
    if (services[name].detect()){
      config = services[name].configuration();
      break;
    }
  }
  if (!config){
    var local = require('./services/localGit');
    config = local.configuration();
    if (!config){
      throw new Error("Unknown CI service provider. Unable to upload coverage.");
    }
  }
  return config;
};

module.exports = detectProvider;

Fix typo in unknown service provider error message


var log4js = require('log4js');
log4js.configure('bin/log4js.json', {reloadSecs: 300});

module.exports = log4js.getLogger();

var log4js = require('log4js');
log4js.configure(__dirname + '/../bin/log4js.json', {reloadSecs: 300});

module.exports = log4js.getLogger();

Fix log4js during the `npm install pulsar-rest-api`


/* jshint node: true */
/* global require, module */

var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

var app = new EmberAddon();

// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.

module.exports = app.toTree();

/* jshint node: true */
/* global require, module */

var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');

var app = new EmberAddon();

// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
app.import('bower_components/bootstrap/dist/css/bootstrap.css');

module.exports = app.toTree();

Add bootstrap to build process for testing


game.PlayScreen = me.ScreenObject.extend({
    /**
     *  action to perform on state change
     */
    onResetEvent: function() {
      // load a level
        me.levelDirector.loadLevel("map2");

        // reset the score
        game.data.score = 0;

        // add our HUD to the game world
        this.HUD = new game.HUD.Container();
        me.game.world.addChild(this.HUD);

        // play some music
        me.audio.playTrack("DST-GameForest");
    },

    /**
     *  action to perform on state change
     */
    onDestroyEvent: function() {

        // remove the HUD from the game world
        me.game.world.removeChild(this.HUD);

        // stop some music
        me.audio.stopTrack("DST-GameForest");
    }
});

game.PlayScreen = me.ScreenObject.extend({
    /**
     *  action to perform on state change
     */
    onResetEvent: function() {
      // load a level
        me.levelDirector.loadLevel("map1");

        // reset the score
        game.data.score = 0;

        // add our HUD to the game world
        this.HUD = new game.HUD.Container();
        me.game.world.addChild(this.HUD);

        // play some music
        me.audio.playTrack("DST-GameForest");
    },

    /**
     *  action to perform on state change
     */
    onDestroyEvent: function() {

        // remove the HUD from the game world
        me.game.world.removeChild(this.HUD);

        // stop some music
        me.audio.stopTrack("DST-GameForest");
    }
});

Undo accidental commit that changed the platformer starting map


Add log level to configuration


(function () {
  'use strict';
  console.log('test');
  var smsElem = $('#invite_sms');
  var emailElem = $('#invite_email');
  var codeElem = $('#invitation_code');
  rogerthat.callbacks.ready(function () {
    console.log('rogerthat is ready');
    var code = rogerthat.user.data.invitation_code;
    var userName = rogerthat.user.data.name || rogerthat.user.name;
    var appId = rogerthat.system.appId;
    var message = 'Hi,\n\n' +
      'I\'d like to invite you to join the ThreeFold community.' +
      ' You can download the Android or iOS app at https://rogerth.at/install?a=' + appId +
      '\n\n' +
      'To become a registered user in the app, you can use my invitation code ' + code + '.' +
      '\n\n' +
      'Welcome on board!\n' + userName;
    var subject = 'Invitation to Threefold community';
    var smsHref = 'sms:?body=' + encodeURIComponent(message);
    var emailHref = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(message);
    smsElem.attr('href', smsHref);
    emailElem.attr('href', emailHref);
    codeElem.text(code);
  });
})();

(function () {
  'use strict';
  console.log('test');
  var smsElem = $('#invite_sms');
  var emailElem = $('#invite_email');
  var codeElem = $('#invitation_code');
  rogerthat.callbacks.ready(function () {
    console.log('rogerthat is ready');
    var code = rogerthat.user.data.invitation_code;
    var userName = rogerthat.user.data.name || rogerthat.user.name;
    var appId = rogerthat.system.appId;
    var message = 'Hi,\n\n' +
      'I\'d like to invite you to join the ThreeFold community.' +
      ' You can download the Android or iOS app at https://rogerth.at/install?a=' + appId +
      '\n\n' +
      'To become a registered user in the app, you can use my invitation code ' + code + '.' +
      '\n\n' +
      'Welcome on board!\n' + userName;
    var subject = 'Invitation to Threefold community';
    var smsHref;
    if (rogerthat.system.os === 'ios') {
      smsHref = 'sms:&body=' + encodeURIComponent(message);
    } else {
      smsHref = 'sms:?body=' + encodeURIComponent(message);
    }
    var emailHref = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(message);
    smsElem.attr('href', smsHref);
    emailElem.attr('href', emailHref);
    codeElem.text(code);
  });
})();

Fix sms link for iOS


function start() {
    console.log( "Start" );
}

function start() {
    const socket = require( './socket_plugin' );
    socket.run( "gulpease", [ "/home/amantova/Projects/documentation/RR/PianoDiProgetto" ], function ( num ) {
	document.getElementById( "example" ).innerHTML = num;
    }
}

Call module gulpease with socket plugin


module.exports = {
  /**
   * Just like parseInt, but fixed to base-10
   */
  Int(str) {
     return parseInt(str, 10);
  },

  /**
   * Cycles through an object and changes all numeric fields to ints
   * where necessary. 'context' is used for exception reporting and can be
   * left unset upon invocation.
   */
  cleanJSON: function cleanJSON(json, context) {
     if (!context)
        context = "root";

     var integerProperties = [
        'x'
       ,'y'
       ,'width'
       ,'height'
       ,'radius'
       ,'strokeWidth'
     ];
     const isIntegerProperty = (property) => integerProperties.indexOf(property) != -1;

     for (var property in json) {
        if (typeof(json[property]) == 'object') {
           cleanJSON(json[property], context + '.' + property);
        }
        else if (isIntegerProperty(property)) {
           if (typeof(json[property]) == 'string') {
            json[property] = Int(json[property]);
              if (isNaN(json[property])) {
                 var msg = "In '" + context + "': property '" + property +
                  "' is not a number.";
                 throw msg;
              }
           }
        }
     }
  }
}

/**
* Just like parseInt, but fixed to base-10
*/
function Int(str) {
  return parseInt(str, 10);
}

/**
* Cycles through an object and changes all numeric fields to ints
* where necessary. 'context' is used for exception reporting and can be
* left unset upon invocation.
*/
function cleanJSON(json, context) {
  if (!context)
     context = "root";

  var integerProperties = [
     'x'
    ,'y'
    ,'width'
    ,'height'
    ,'radius'
    ,'strokeWidth'
  ];
  const isIntegerProperty = (property) => integerProperties.indexOf(property) != -1;

  for (var property in json) {
     if (typeof(json[property]) == 'object') {
        cleanJSON(json[property], context + '.' + property);
     }
     else if (isIntegerProperty(property)) {
        if (typeof(json[property]) == 'string') {
         json[property] = Int(json[property]);
           if (isNaN(json[property])) {
              var msg = "In '" + context + "': property '" + property +
               "' is not a number.";
              throw msg;
           }
        }
     }
  }
}

module.exports = {
   Int,
   cleanJSON,
}

Make Int and cleanJSON plain functions


var exec = require('child_process').exec;
var spawn = require('child_process').spawn;

function GatewayProcessManager() {

  this.processNames = [
    "ripple_rest",
    "deposits",
    "outgoing",
    "incoming",
    "withdrawals",
    "webapp"
  ];

  this.processes = {};

}

GatewayProcessManager.prototype.start = function() {

  var manager = this;

  manager.processNames.forEach(function(name) {

    var process = spawn('node', [__dirname+'/../processes/'+name]);

    manager.processes[name] = process.pid;

    process.stdout.on('data', function(data){
      console.log(name.toUpperCase()+' :', data.toString());
    });

    process.stderr.on('data', function(data){
      console.log(name.toUpperCase()+' :', data.toString());
    });

  });

  console.log(manager.processes);

}

module.exports = GatewayProcessManager;

var exec = require('child_process').exec;
var spawn = require('child_process').spawn;

function GatewayProcessManager() {

  this.processNames = [
    "deposits",
    "outgoing",
    "incoming",
    "withdrawals",
    "webapp"
  ];

  this.processes = {};

}

GatewayProcessManager.prototype.start = function() {

  var manager = this;

  manager.processNames.forEach(function(name) {

    var process = spawn('node', [__dirname+'/../processes/'+name]);

    manager.processes[name] = process.pid;

    process.stdout.on('data', function(data){
      console.log(name.toUpperCase()+' :', data.toString());
    });

    process.stderr.on('data', function(data){
      console.log(name.toUpperCase()+' :', data.toString());
    });

  });

  console.log(manager.processes);

}

module.exports = GatewayProcessManager;

Remove ripple rest fom package.json.


import React, { Component } from 'react';
import { Navbar, Nav, NavItem } from 'react-bootstrap';
import ImgurImage from './ImgurImage';
import { searchGallery } from './../services/imgur';

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {images: [], page: 0};
  }

  loadImages(page = 0) {
    searchGallery(undefined, undefined, page).then((result) => {
      if (this.isMounted()) {
        this.setState({images: result.data, page: page});
      }
    });
  }

  componentDidMount() {
    this.loadImages();
  }

  render() {

    return (

        <Navbar
          brand='React Imgur viewer'
          fixedTop={true}>

            <NavItem
              onClick={() => this.loadImages(this.state.page - 1)}
              disabled={this.state.page <= 0}>
               Previous page
            </NavItem>
            <NavItem onClick={() => this.loadImages(this.state.page + 1)}>
              Next page
            </NavItem>

        </Navbar>

          {this.state.images.map(function(image) {
            return (
              <ImgurImage
                image={image}
                key={image.id} />
            );
          })}

    );
  }
}

import React, { Component } from 'react';
import { Navbar, Nav, NavItem } from 'react-bootstrap';
import ImgurImage from './ImgurImage';
import { searchGallery } from './../services/imgur';

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {images: [], page: 0};
  }

  loadImages(page = 0) {
    searchGallery(undefined, undefined, page).then((result) => {
      this.setState({images: result.data, page: page});
    });
  }

  componentDidMount() {
    this.loadImages();
  }

  render() {

    return (

        <Navbar
          brand='React Imgur viewer'
          fixedTop={true}>

            <NavItem
              onClick={() => this.loadImages(this.state.page - 1)}
              disabled={this.state.page <= 0}>
               Previous page
            </NavItem>
            <NavItem onClick={() => this.loadImages(this.state.page + 1)}>
              Next page
            </NavItem>

        </Navbar>

          {this.state.images.map(function(image) {
            return (
              <ImgurImage
                image={image}
                key={image.id} />
            );
          })}

    );
  }
}

Revert "Check if app is mounted"


'use strict';

/*global $*/

describe('ACP edit user group', function () {
  it('should save changes', function (done) {
    let user;
    let adminGroupId;

    TEST.browser
      .do.auth('admin_users_usergroups_edit', usr => {
        user = usr;
      })
      .fn(cb => TEST.N.models.users.UserGroup.findIdByName('administrators')
                  .then(res => {
                    adminGroupId = res;
                    user.usergroups.push(adminGroupId);
                    return user.save();
                  })
                  .asCallback(cb)
      )
      .do.open(() => TEST.N.router.linkTo('admin.users.usergroups.edit', { _id: adminGroupId }))
      .do.click('#setting_can_see_deleted_users')
      .do.click('button.btn-primary[type="submit"]')
      .do.wait('.alert-info')
      .do.reload()
      .test.evaluate(function () {
        return $('#setting_can_see_deleted_users').is(':checked') === false;
      })
      // Set setting back
      .do.click('#setting_can_see_deleted_users')
      .do.click('button.btn-primary[type="submit"]')
      .do.wait('.alert-info')
      .close()
      .run(true, done);
  });
});

'use strict';

/*global $*/

describe('ACP edit user group', function () {
  it('should save changes', function (done) {
    let user;
    let adminGroupId;

    TEST.browser
      .do.auth('admin_users_usergroups_edit', usr => {
        user = usr;
      })
      .fn(cb => TEST.N.models.users.UserGroup.findIdByName('administrators')
                  .then(res => {
                    adminGroupId = res;
                    user.usergroups.push(adminGroupId);
                    return user.save();
                  })
                  .asCallback(cb)
      )
      .do.open(() => TEST.N.router.linkTo('admin.users.usergroups.edit', { _id: adminGroupId }))
      .do.click('#setting_can_see_deleted_users')
      .do.click('button.btn-primary[type="submit"]')
      .do.wait('.notification-info')
      .do.reload()
      .test.evaluate(function () {
        return $('#setting_can_see_deleted_users').is(':checked') === false;
      })
      // Set setting back
      .do.click('#setting_can_see_deleted_users')
      .do.click('button.btn-primary[type="submit"]')
      .do.wait('.notification-info')
      .close()
      .run(true, done);
  });
});

Fix test after notification classes change


import passport from 'passport';
import passportLocal from 'passport-local';
import bcrypt from 'bcrypt';
import user from '../models';
import helpers from '../helpers';

const LocalStrategy = passportLocal.Strategy;

passport.serializeUser((sessionUser, done) => {
  done(null, sessionUser.id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, sessionUser) => done(err, sessionUser));
});

passport.use(new LocalStrategy({ passReqToCallback: true }, (req, username, password, done) => {
  user.User.findOne({
    where: {
      username
    }
  }).then((users) => {

    if (!users) {
      return done(null, false, { message: 'Incorrect username' });
    }
    // Compare database salt to the hash of incoming password
    const reqPasswordHash = bcrypt.hashSync(req.body.password, users.salt);
    console.log('The request password hash', reqPasswordHash);
    console.log('The database password is' , users.password);
    if (users.password === reqPasswordHash) {
      return done(null, users);
    }
    return done(null, false, { message: ' Incorrect password.' });

  }).catch(err => done(err));
}
));

module.exports = passport;

import passport from 'passport';
import passportLocal from 'passport-local';
import bcrypt from 'bcrypt';
import user from '../models';
import helpers from '../helpers';

const LocalStrategy = passportLocal.Strategy;

passport.serializeUser((sessionUser, done) => {
  done(null, sessionUser.id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, sessionUser) => done(err, sessionUser));
});

passport.use(new LocalStrategy({ passReqToCallback: true }, (req, username, password, done) => {
  user.User.findOne({
    where: {
      username
    }
  }).then((users) => {
    if (!users) {
      return done(null, false, { message: 'Incorrect username' });
    }
    // Compare database salt to the hash of incoming password
    const reqPasswordHash = bcrypt.hashSync(req.body.password, users.salt);

    if (users.password === reqPasswordHash) {
      return done(null, users);
    }
    return done(null, false, { message: ' Incorrect password.' });
  }).catch(err => done(err));
}
));

module.exports = passport;

Edit code to fix bugs


var express = require('express');
var cors = require('cors');
var app = express();
app.use(cors);

var winston = require('winston');
var logger = new (winston.Logger) ({
  transports: [
    new (winston.transports.Console) ({ level: 'info' }),
    new (winston.transports.File) ({
      filename: 'server-debug.log',
      level: 'debug'
    })
  ]
});

var persons = require('./person-mocks');

exports.createServer = function (port) {
  app.get('/', function(req, res) {
    res.status(501);
    res.set('Access-Control-Allow-Origin:', '*');
    res.send(JSON.stringify({ message: 'not implemented'}));
  });

  app.get('/persons', function(req, res) {
    res.status(200);
    res.set('Content-Type', 'application/json');
    res.send(JSON.stringify(persons));
  });

  var server = app.listen(port, function() {
    var host = server.address().address;
    var port = server.address().port;

    logger.debug('Server running at http://%s:%s', host, port);
  });

  return server;
};

var express = require('express');
var cors = require('cors');
var app = express();

var winston = require('winston');
var logger = new (winston.Logger) ({
  transports: [
    new (winston.transports.Console) ({ level: 'info' }),
    new (winston.transports.File) ({
      filename: 'server-debug.log',
      level: 'debug'
    })
  ]
});

var persons = require('./person-mocks');

exports.createServer = function (port) {
  app.get('/', function(req, res) {
    res.status(501);
    res.set('Access-Control-Allow-Origin:', '*');
    res.send(JSON.stringify({ message: 'not implemented'}));
  });

  app.get('/persons', cors(), function(req, res) {
    res.status(200);
    res.set('Content-Type', 'application/json');
    res.send(JSON.stringify(persons));
  });

  var server = app.listen(port, function() {
    var host = server.address().address;
    var port = server.address().port;

    logger.debug('Server running at http://%s:%s', host, port);
  });

  return server;
};

Set CORS to be used in specific responses


const http = require('http');
const mongoose = require('mongoose');
const { logger } = require('./middleware/logger');
const app = require('./app');

const PORT = process.env.PORT || 6673;
const SERVER = http.createServer(app.callback());

// Gracefully close Mongo connection
const gracefulShutdown = () => {
  mongoose.connection.close(false, () => {
    logger.info('Mongo closed');
    SERVER.close(() => {
      logger.info('Shutting down...');
      process.exit();
    });
  });
};

// Server start
app.on('ready', () => {
  SERVER.listen(PORT, '0.0.0.0', () => {
    logger.info(`Running on port: ${PORT}`);

    // Handle kill commands
    process.on('SIGTERM', gracefulShutdown);

    // Prevent dirty exit on code-fault crashes:
    process.on('uncaughtException', gracefulShutdown);

    // Prevent promise rejection exits
    process.on('unhandledRejection', gracefulShutdown);
  });
});

const http = require('http');
const mongoose = require('mongoose');
const { logger } = require('./middleware/logger');
const app = require('./app');

const PORT = process.env.PORT || 6673;
const SERVER = http.createServer(app.callback());

// Gracefully close Mongo connection
const gracefulShutdown = (msg) => {
  logger.info(`Shutdown initiated: ${msg}`);
  mongoose.connection.close(false, () => {
    logger.info('Mongo closed');
    SERVER.close(() => {
      logger.info('Shutting down...');
      process.exit();
    });
  });
};

// Server start
app.on('ready', () => {
  SERVER.listen(PORT, '0.0.0.0', () => {
    logger.info(`Running on port: ${PORT}`);

    // Handle kill commands
    process.on('SIGTERM', gracefulShutdown);

    // Handle interrupts
    process.on('SIGINT', gracefulShutdown);

    // Prevent dirty exit on uncaught exceptions:
    process.on('uncaughtException', gracefulShutdown);

    // Prevent dirty exit on unhandled promise rejection
    process.on('unhandledRejection', gracefulShutdown);
  });
});

Add logging for graceful shutdown errors


import React from 'react';
import { formatRelative } from 'date-fns';
import './Projects.css';

const Projects = ({ projects, toggle, update }) => {
  return (

      {projects.map(project => (

            <input
              checked={project.selected}
              className="ToggleProjects__checkbox"
              type="checkbox"
              onChange={toggle.bind(this, project.id)}
            />
            {project.company} — {project.name} (ID: {project.id})

            <button
              className="btn-link btn-sm"
              onClick={update.bind(this, project.id)}
              type="button"
              disabled={project.updating}
            >
              {!project.updating ? 'Update Tickets' : 'Updating Tickets...'}
            </button>

            {project.lastUpdated ? (
              Updated {formatRelative(project.lastUpdated, new Date())}
            ) : null}

      ))}

  );
};

export default Projects;

import React from 'react';
import { formatRelative } from 'date-fns';
import './Projects.css';

const Projects = ({ projects, toggle, update }) => {
  return (

      {projects.map(project => (

            <input
              checked={project.selected}
              className="ToggleProjects__checkbox"
              type="checkbox"
              onChange={toggle.bind(this, project.id)}
            />
            {project.company} — {project.name} (ID: {project.id})

            <button
              className="btn-link btn-sm"
              onClick={update.bind(this, project.id)}
              type="button"
            >
              {!project.updating ? 'Update Tickets' : 'Updating Tickets...'}
            </button>

            {project.lastUpdated ? (
              Updated {formatRelative(project.lastUpdated, new Date())}
            ) : null}

      ))}

  );
};

export default Projects;

Stop disabling update button while updating


/*
Copyright 2017 Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';

const CreateRoomButton = function(props) {
    const ActionButton = sdk.getComponent('elements.ActionButton');
    return (
        <ActionButton action="view_create_room"
            label="Create new room"
            iconPath="img/icons-create-room.svg"
            size={props.size}
            tooltip={props.tooltip}
        />
    );
};

CreateRoomButton.propTypes = {
    size: PropTypes.string,
    tooltip: PropTypes.bool,
};

export default CreateRoomButton;

/*
Copyright 2017 Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';

const CreateRoomButton = function(props) {
    const ActionButton = sdk.getComponent('elements.ActionButton');
    return (
        <ActionButton action="view_create_chat"
            label="Create new room"
            iconPath="img/icons-create-room.svg"
            size={props.size}
            tooltip={props.tooltip}
        />
    );
};

CreateRoomButton.propTypes = {
    size: PropTypes.string,
    tooltip: PropTypes.bool,
};

export default CreateRoomButton;

Revert "Fix Create Room button"


module.exports = function karma(config) {
  config.set({
    browsers: ['PhantomJS'],
    frameworks: ['mocha', 'chai', 'sinon'],
    files: ['tests.webpack.js'],
    plugins: [
      'karma-phantomjs-launcher',
      'karma-mocha',
      'karma-chai',
      'karma-sinon',
      'karma-sourcemap-loader',
      'karma-webpack',
      'karma-coverage',
      'karma-mocha-reporter'
    ],
    preprocessors: {
      'tests.webpack.js': ['webpack', 'sourcemap']
    },
    reporters: ['mocha', 'coverage'],
    webpack: {
      devtool: 'inline-source-map',
      module: {
        preLoaders: [
          {
            test: /\.js$/,
            exclude: /(__tests__|node_modules)\//,
            loader: 'isparta-instrumenter'
          }
        ],
        loaders: [
          {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /(node_modules|lib)\//
          }
        ]
      },
      externals: {
        cheerio: 'window',
        'react/addons': true,
        'react/lib/ReactContext': true,
        'react/lib/ExecutionEnvironment': true
      }
    },
    webpackServer: {
      noInfo: true
    },
    coverageReporter: {
      reporters: [
        {
          type: 'lcovonly',
          dir: 'coverage/',
          subdir: '.',
          file: 'lcov.info'
        },
        {
          type: 'text'
        }
      ]
    }
  });
};

module.exports = function karma(config) {
  config.set({
    browsers: ['PhantomJS'],
    frameworks: ['mocha', 'chai', 'sinon'],
    files: ['tests.webpack.js'],
    plugins: [
      'karma-phantomjs-launcher',
      'karma-mocha',
      'karma-chai',
      'karma-sinon',
      'karma-sourcemap-loader',
      'karma-webpack',
      'karma-coverage',
      'karma-mocha-reporter'
    ],
    preprocessors: {
      'tests.webpack.js': ['webpack', 'sourcemap']
    },
    reporters: ['mocha', 'coverage'],
    webpack: {
      devtool: 'inline-source-map',
      module: {
        preLoaders: [
          {
            test: /\.js$/,
            exclude: /(__tests__|node_modules)\//,
            loader: 'isparta-instrumenter'
          }
        ],
        loaders: [
          {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /(node_modules|lib)\//
          }
        ]
      },
      externals: {
        cheerio: 'window',
        'react/addons': true,
        'react/lib/ReactContext': true,
        'react/lib/ExecutionEnvironment': true
      }
    },
    webpackServer: {
      noInfo: true
    },
    coverageReporter: {
      reporters: [
        {
          type: 'lcovonly',
          dir: 'coverage/',
          subdir: '.',
          file: 'lcov.info'
        },
        {
          type: 'text-summary'
        }
      ]
    }
  });
};

Change full coverage report to short text summary


import { Router } from 'express';
import * as SubmissionController from '../controllers/submission.controller';
const router = new Router();

// Gets all submissions for a contest
router.route('/submissions/:contestId').get(SubmissionController.getSubmissions);

// Gets a single submission
router.route('/submissions/:submissionId').get(SubmissionController.getSubmission);

// Posts feedback for a submission
router.route('/submissions/feedback/:submissionId').post(SubmissionController.sendFeedback);

// Posts feedback for a submission
router.route('/submissions/:submissionId').delete(SubmissionController.deleteSubmission);

export default router;

import { Router } from 'express';
import * as SubmissionController from '../controllers/submission.controller';
const router = new Router();

// Gets all submissions for a contest
router.route('/submissions/:contestId/add').get(SubmissionController.getSubmissions);

// Gets a single submission
router.route('/submissions/:submissionId').get(SubmissionController.getSubmission);

// Posts feedback for a submission
router.route('/submissions/feedback/:submissionId').post(SubmissionController.sendFeedback);

// Posts feedback for a submission
router.route('/submissions/:submissionId').delete(SubmissionController.deleteSubmission);

export default router;

Change get all submissions route


module.exports = function(config) {
    config.set({
        frameworks: ['mocha'],
        browsers: ['Firefox', 'PhantomJS'],
        files: [require.resolve('es5-shim'), 'build/test.js'],
        reporters: ['dots']
    });

    if (process.env.CI && process.env.SAUCE_ACCESS_KEY) {
        var customLaunchers = {
            sauceLabsFirefox: {
                base: 'SauceLabs',
                browserName: 'firefox'
            },
            sauceLabsChrome: {
                base: 'SauceLabs',
                browserName: 'chrome'
            }
        };

        config.set({
            browsers: Object.keys(customLaunchers),
            reporters: ['dots', 'saucelabs'],
            captureTimeout: 120000,
            sauceLabs: {
                testName: 'Loud'
            },
            customLaunchers: customLaunchers
        });
    }
};

module.exports = function(config) {
    config.set({
        frameworks: ['mocha'],
        browsers: ['Firefox', 'PhantomJS'],
        files: [require.resolve('es5-shim'), 'build/test.js'],
        reporters: ['dots']
    });

    if (process.env.CI && process.env.SAUCE_ACCESS_KEY) {
        var customLaunchers = {
            sauceLabsFirefox: {
                base: 'SauceLabs',
                browserName: 'firefox'
            },
            sauceLabsChrome: {
                base: 'SauceLabs',
                browserName: 'chrome'
            },
            sauceLabsIE11: {
                base: 'SauceLabs',
                browserName: 'internet explorer',
                version: 11
            },
            sauceLabsIE10: {
                base: 'SauceLabs',
                browserName: 'internet explorer',
                version: 10
            },
            sauceLabsIE9: {
                base: 'SauceLabs',
                browserName: 'internet explorer',
                version: 9
            },
            sauceLabsIE8: {
                base: 'SauceLabs',
                browserName: 'internet explorer',
                version: 8
            }
        };

        config.set({
            browsers: Object.keys(customLaunchers),
            reporters: ['dots', 'saucelabs'],
            captureTimeout: 120000,
            sauceLabs: {
                testName: 'Loud'
            },
            customLaunchers: customLaunchers
        });
    }
};

Test in Internet Explorer in SauceLabs


const SIX_MONTHS_IN_SECONDS = 15552000;

const STATIC_ASSET_PATHS = [
  '/public/font/font-v1.css',
  '/public/font/v1/300.woff',
  '/public/font/v1/300i.woff',
  '/public/font/v1/400.woff',
  '/public/font/v1/400i.woff',
  '/public/font/v1/700.woff',
  '/public/font/v1/700i.woff',
  '/public/favicon.png',
  '/public/img/busy.gif',
  '/public/img/fileRenderPlaceholder.png',
  '/public/img/gerbil-happy.png',
  '/public/img/gerbil-sad.png',
  '/public/img/placeholder.png',
  '/public/img/placeholderTx.gif',
  '/public/img/thumbnail-broken.png',
  '/public/img/thumbnail-missing.png',
  '/public/img/total-background.png',
];

async function redirectMiddleware(ctx, next) {
  const {
    request: { url },
  } = ctx;

  if (STATIC_ASSET_PATHS.includes(url) || (url.startsWith('/public/ui-') && url.endsWith('.js'))) {
    ctx.set('Cache-Control', `public, max-age=${SIX_MONTHS_IN_SECONDS}`);
  }

  return next();
}

module.exports = redirectMiddleware;

const SIX_MONTHS_IN_SECONDS = 15552000;

const STATIC_ASSET_PATHS = [
  '/public/font/font-v1.css',
  '/public/font/v1/300.woff',
  '/public/font/v1/300i.woff',
  '/public/font/v1/400.woff',
  '/public/font/v1/400i.woff',
  '/public/font/v1/700.woff',
  '/public/font/v1/700i.woff',
  '/public/favicon.png',
  '/public/img/busy.gif',
  '/public/img/fileRenderPlaceholder.png',
  '/public/img/gerbil-happy.png',
  '/public/img/gerbil-sad.png',
  '/public/img/placeholder.png',
  '/public/img/placeholderTx.gif',
  '/public/img/thumbnail-broken.png',
  '/public/img/thumbnail-missing.png',
  '/public/img/total-background.png',
];

async function redirectMiddleware(ctx, next) {
  const {
    request: { url },
  } = ctx;

  const HASHED_JS_REGEX = /^\/public\/.*[a-fA-F0-9]{12}\.js$/i;

  if (STATIC_ASSET_PATHS.includes(url) || HASHED_JS_REGEX.test(url)) {
    ctx.set('Cache-Control', `public, max-age=${SIX_MONTHS_IN_SECONDS}`);
  }

  return next();
}

module.exports = redirectMiddleware;

Improve cache for hashed js files


'use strict';

//====================================================================

var hashy = require('./');

//--------------------------------------------------------------------

var expect = require('chai').expect;
var Bluebird = require('bluebird');

//====================================================================

var data = [
  {
    value: 'password',
    hash: '$2a$10$3F2S0bh8CO8aVzW/tqyjI.iVQnLNea1YIpNSpS8dmJwUVNXP3D4/y',
    info: {
      algo: hashy.BCRYPT,
      algoName: 'bcrypt',
      options: {
        cost: 10,
      },
    }
  },
];

//====================================================================

describe('hash()', function () {
  var hash = hashy.hash;

  it('can return a promise', function () {
    return hash('test');
  });

  it('can work with callback', function (done) {
    hash('test', done);
  });

  it('does not creates the same hash twice', function () {
    return Bluebird.all([
      hash('test'),
      hash('test'),
    ]).spread(function (hash1, hash2) {
      expect(hash1).to.not.equal(hash2);
    });
  });
});

describe('getInfo()', function () {
  var getInfo = hashy.getInfo;

  it('returns the algorithm and options', function () {
    data.forEach(function (datum) {
      expect(getInfo(datum.hash)).to.deep.equal(datum.info);
    });
  });
});

describe('needsRehash()', function () {
  var needsRehash = hashy.needsRehash;

  // TODO
});

'use strict';

//====================================================================

var hashy = require('./');

//--------------------------------------------------------------------

var expect = require('chai').expect;
var Bluebird = require('bluebird');
Bluebird.longStackTraces();

//====================================================================

var data = [
  {
    value: 'password',
    hash: '$2a$10$3F2S0bh8CO8aVzW/tqyjI.iVQnLNea1YIpNSpS8dmJwUVNXP3D4/y',
    info: {
      algo: hashy.BCRYPT,
      algoName: 'bcrypt',
      options: {
        cost: 10,
      },
    }
  },
];

//====================================================================

describe('hash()', function () {
  var hash = hashy.hash;

  it('can return a promise', function () {
    return hash('test');
  });

  it('can work with callback', function (done) {
    hash('test', done);
  });

  it('does not creates the same hash twice', function () {
    return Bluebird.all([
      hash('test'),
      hash('test'),
    ]).spread(function (hash1, hash2) {
      expect(hash1).to.not.equal(hash2);
    });
  });
});

describe('getInfo()', function () {
  var getInfo = hashy.getInfo;

  it('returns the algorithm and options', function () {
    data.forEach(function (datum) {
      expect(getInfo(datum.hash)).to.deep.equal(datum.info);
    });
  });
});

describe('needsRehash()', function () {
  var needsRehash = hashy.needsRehash;

  // TODO
});

Use long stack traces in tests.


#! /usr/bin/env node

echo('Cleaning up dist directory');
rm('-rf', 'dist/*');

echo('Optmizing SVG icons');
exec('npm run svgo');

echo('Creating socialshares.js');
exec('npm run build');

echo('Copying socialshares.js and socialshares.js.map to dist');
cp('build/socialshares.js', 'dist/socialshares.js');
cp('build/socialshares.js.map', 'dist/socialshares.js.map');
sed('-i', '{VERSION}', env.npm_package_version, 'dist/socialshares.js');

echo('Creating socialshares.min.js');
exec('npm run build-min');

echo('Copying socialshares.min.js to dist');
cp('build/socialshares.js', 'dist/socialshares.min.js');
sed('-i', '{VERSION}', env.npm_package_version, 'dist/socialshares.min.js');

echo('Copying index.html to dist');
cp('build/index.html', 'dist/index.html');
sed('-i', './socialshares.js', './socialshares.min.js', 'dist/index.html');

echo('The dist directory is now ready!');

#! /usr/bin/env node

echo('Cleaning up dist directory');
rm('-f', 'dist');
mkdir('dist');

echo('Optmizing SVG icons');
exec('npm run svgo');

echo('Creating socialshares.js');
exec('npm run build');

echo('Copying socialshares.js and socialshares.js.map to dist');
cp('build/socialshares.js', 'dist/socialshares.js');
cp('build/socialshares.js.map', 'dist/socialshares.js.map');
sed('-i', '{VERSION}', env.npm_package_version, 'dist/socialshares.js');

echo('Creating socialshares.min.js');
exec('npm run build-min');

echo('Copying socialshares.min.js to dist');
cp('build/socialshares.js', 'dist/socialshares.min.js');
sed('-i', '{VERSION}', env.npm_package_version, 'dist/socialshares.min.js');

echo('Copying index.html to dist');
cp('build/index.html', 'dist/index.html');
sed('-i', './socialshares.js', './socialshares.min.js', 'dist/index.html');

echo('The dist directory is now ready!');

Delete and re-create dist directory


import React from 'react';
import {render} from 'react-dom';
import { MemoryRouter, Switch, Route } from 'react-router';
import SingleCharacterRolling from './components/single-character-rolling';
import Homepage from './components/Homepage';

const Root = () => {
return (
    <MemoryRouter>
        <div>
            <Switch>
                <Route path="/" component={Homepage} />
            </Switch>
        </div>
    </MemoryRouter>
)
}

render(<Root/>, document.querySelector('#app'))

import React from 'react';
import {render} from 'react-dom';
import { MemoryRouter, Switch, Route } from 'react-router';
import { Link } from 'react-router-dom';
import SingleCharacterRolling from './components/single-character-rolling';
import Homepage from './components/Homepage';

const Root = () => {
return (
  <MemoryRouter
    initialEntries={[ '/', '/game']}
    initialIndex={0}>
    <div>
    <Link to="/"> homepage </Link>
    <Link to="/game"> game </Link>
      <Switch>
        <Route exact path="/" component={Homepage} />
        <Route path="/game" component={SingleCharacterRolling} />
      </Switch>
    </div>
  </MemoryRouter>
)
}

render(<Root/>, document.querySelector('#app'))

Use react router to change main page


'use babel';

import React from 'react';
import TodoItem from './TodoItem';

const TodoList = ({ todos, setTodoStatus, deleteTodo, onTitleClick }) => {
  return (
    <div>
      {
        todos.map(todo => (
          <TodoItem
            key={todo._id}
            todo={todo}
            setTodoStatus={setTodoStatus}
            deleteTodo={deleteTodo}
            onTitleClick={onTitleClick}
          />
        ))
      }
    </div>
  );
};

export default TodoList;

'use babel';

import React from 'react';
import TodoItem from './TodoItem';

const TodoListBackgroundMessage = () => (
  <background-tips>

        Newly added todos will be listed here

  </background-tips>
);

const TodoList = ({ todos, setTodoStatus, deleteTodo, onTitleClick }) => {
  return (

      {
        todos.map(todo => (
          <TodoItem
            key={todo._id}
            todo={todo}
            setTodoStatus={setTodoStatus}
            deleteTodo={deleteTodo}
            onTitleClick={onTitleClick}
          />
        ))
      }
      { (!todos || todos.length==0) && <TodoListBackgroundMessage /> }

  );
};

export default TodoList;

Add todo list tip when no todos exist


let _where,
_select,
_from,
_orderBy,
_groupBy,
_limit,
_skip,
_join,
_insert,
_delete,
_update,
_command,
_table,
_option,
_params

module.exports = {
  select: function (columns) {
    if (Array.isArray(columns)) {
      _select = `SELECT ${columns.join(', ')}`
    } else if (columns.length > 0) {
      _select = `SELECT ${columns}`
    } else {
      throw new Error('The columns is not available in correct format!')
    }
    _option = 'select'
    return this
  },
  from: function (table) {
    if (table.length > 0) {
      _table = ` FROM ${table}`
    } else {
      throw new Error('The table variable is required to be string!')
    }
    return this
  },
  addSelect: function (columns) {

  }
}

let _where,
_select,
_from,
_orderBy,
_groupBy,
_limit,
_skip,
_join,
_insert,
_delete,
_update,
_command,
_table,
_option,
_params

module.exports = {
  select: function (columns) {
    if (Array.isArray(columns)) {
      _select = `SELECT ${columns.join(', ')}`
    } else if (columns.length > 0) {
      _select = `SELECT ${columns}`
    } else {
      throw new Error('The columns is not available in correct format!')
    }
    _option = 'select'
    return this
  },
  from: function (table) {
    if (table.length > 0) {
      _table = ` FROM ${table}`
    } else {
      throw new Error('The table variable is required to be string!')
    }
    return this
  },
  addSelect: function (columns) {
    if (!_select) {
      throw new Error('Please first use SELECT method before add new column on select method!')
    } else if (Array.isArray(columns)) {
      _select += ` ${columns.join(', ')}`
    } else if (columns.length > 0) {
      _select += ` ${columns}`
    } else {
      throw new Error('The columns is not available in correct format!')
    }
    return this
  },
  add: function (columns) {
    return this
  }
}

Add addSelect function on query builder and start to make add function


define(["jquery"], function($) {

  function ESGlue() {

  }

  ESGlue.query = function(options, onDone) {
    $.get("http://10.251.33.248:9200/reviews/review",
      '{' +
        '"from":"reviews",' +
        '"select":{"name":"num_bugs","value":"bug_id","aggregate":"count"},' +
        '"edges":["product","component"],' +
        '"esfilter":{"term":{"product":"firefox"}}' +
       '}', onDone, "json");
  };

  return ESGlue;

});

define(["jquery"], function($) {

  function ESGlue() {

  }

  ESGlue.query = function(options, onDone) {
    $.post("http://10.251.33.248:9200/reviews/review",
      '{' +
        '"from":"reviews",' +
        '"select":{"name":"num_bugs","value":"bug_id","aggregate":"count"},' +
        '"edges":["product","component"],' +
        '"esfilter":{"term":{"product":"firefox"}}' +
       '}', onDone, "json");
  };

  return ESGlue;

});

Change get to post in esglue.


var domain = require('domain');

module.exports = function (app, templateVarService, appUtil) {
    return {
        setup: function () {
            app.use(function (req, res) {
                templateVarService.setupLocals(req, res);
                res.status(404).render('not-found');
            });
            app.use(function (err, req, res, next) {
                templateVarService.setupLocals(req, res);
                if (err instanceof appUtil.ValidationError) {
                    res.status(403).json(err.fieldNameToMessage);
                } else if (err) {
                    console.error(err.stack);
                    res.locals.error = err;
                    if (req.url.indexOf('/api/') === 0) {
                        res.status(500).send(err.stack.toString());
                    } else {
                        res.status(500).render('error');
                    }
                }
            })
        }
    };
};

var domain = require('domain');
var _ = require('lodash');

module.exports = function (app, templateVarService, appUtil) {
    return {
        setup: function () {
            app.use(function (req, res) {
                templateVarService.setupLocals(req, res);
                res.status(404).render('not-found');
            });
            app.use(function (err, req, res, next) {
                templateVarService.setupLocals(req, res);
                if (err instanceof appUtil.ValidationError) {
                    res.status(403).json(err.fieldNameToMessage);
                } else if (err) {
                    console.error(err.stack);
                    res.locals.error = err;
                    if (req.url.indexOf('/api/') === 0) {
                        if (_.isError(err)) {
                            res.status(500).send(err.stack.toString());
                        } else {
                            res.status(500).send(err);
                        }
                    } else {
                        res.status(500).render('error');
                    }
                }
            })
        }
    };
};

Fix handling of errors that are not the Error


$(document).ready(function() {
  $('div.results div.result').on('click', function() {
    var redirect = '/detail'
    location.href = redirect
  })
})

$(document).ready(function() {
  $('div.results div.result').on('click', function(e, el) {
    var redirect = $(this).find('h4 a')[0].href
    location.href = redirect
  })
})

Make the whole element clickable, so users don't have to peck on mobile