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

function getKeywordCampaigns() {
	fetch('/keyword-campaigns').then(response => response.json()).then(keywordCampaigns => {
        const keywordCampaignList = document.getElementById('keyword-campaigns');

        keywordCampaigns.forEach(keywordCampaign => {
            keywordCampaignList.innerHTML += 'Keyword Campaign ' + keywordCampaign.keywordCampaignId + '';
        });
    });
    console.log('Got keyword campaigns.');
}

function getDSACampaigns(keywordCampaignId) {
    fetch('DSA-campaigns?keywordCampaignId=' + keywordCampaignId).then(response => response.json()).then(DSACampaigns => {
        const DSACampaignsList = document.getElementById('DSA-campaigns');

        DSACampaigns.forEach(DSACampaign => {
            DSACampaignsList.innerHTML += '' + DSACampaign.name + '';
        });
    });
}

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

function getKeywordCampaigns() {
	fetch('/keyword-campaigns').then(response => response.json()).then(keywordCampaigns => {
        const keywordCampaignList = document.getElementById('keyword-campaigns');

        keywordCampaigns.forEach(keywordCampaign => {
            keywordCampaignList.innerHTML += 'Keyword Campaign ' + keywordCampaign.keywordCampaignId + '';
        });
    });
    console.log('Got keyword campaigns.');
}

function getDSACampaigns(keywordCampaignId) {
    const DSACampaignsList = document.getElementById('DSA-campaigns');
    DSACampaignsList.innerHTML = '';

    fetch('DSA-campaigns?keywordCampaignId=' + keywordCampaignId).then(response => response.json()).then(DSACampaigns => {
        DSACampaigns.forEach(DSACampaign => {
            DSACampaignsList.innerHTML += '' + DSACampaign.name + '';
        });
    });
    console.log('Got DSA campaigns.');
}

Reset DSA campaign list with each selection.


'use strict'

var CachedUserItems = require('../../src/GooglePlus/CachedUserItems')
var assert = require('assert')

describe("CachedUserItems", function () {
    describe("_getCacheAgePerUser", function () {
        it("is 24h with as much requests as user feeds", function () {
            var googlePlus = {} // TODO Don't stub
            var cachedUserItems = new CachedUserItems(googlePlus) // TODO Avoid this dependency
            var age = cachedUserItems._getCacheAgePerUser({
                maxDailyUsers: 100,
                dailyRequestsLimit: 100
            })
            assert.equal(age, 24 /* hours */ * 60 * 60 * 1000)
        })
    })
})

'use strict'

var CachedUserItems = require('../../src/GooglePlus/CachedUserItems')
var assert = require('assert')

describe("CachedUserItems", function () {
    describe("_getCacheAgePerUser", function () {
        it("is ~4 hours for current values", function () {
            var googlePlus = {} // TODO Don't stub
            var cachedUserItems = new CachedUserItems(googlePlus) // TODO Avoid this dependency
            var age = cachedUserItems._getCacheAgePerUser({
                maxDailyUsers: 10000,
                dailyRequestsLimit: 50000
            })
            assert.equal(age, 4.8 /* hours */ * 60 * 60 * 1000)
        })
    })
})

Use a more meaningful test


/**
 * Show and hide the map layers
 */
Template.componentPage.events({
    'click .container-map': function(e, template) {
        if ($(e.target).hasClass('natural')) {
            $(e.target).removeClass('natural');
        } else {
            $(e.target).addClass('natural');
        }
    }
});

Template.componentPage.helpers({
    dork: function() {
        return 'whort';
    },
    pages: function() {
        return Pages.find({componentId: this._id});
    }
});

/**
 * Show and hide the map layers
 */
Template.componentPage.events({
    'click .container-map': function(e, template) {
        if ($(e.target).hasClass('natural')) {
            $(e.target).removeClass('natural');
        } else {
            $(e.target).addClass('natural');
        }
    }
});

Template.componentPage.helpers({
    dork: function() {
        return 'whort';
    },
    pages: function() {
        return Pages.find({componentId: this._id}, {sort: {order: 1}});
    }
});

Sort on order to keep items in Lines


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

function fetchRawCompanyListItems(token, id) {
  return authorisedRequest(token, {
    method: 'GET',
    url: `${config.apiRoot}/v4/company-list/${id}/item`,
  })
}

const fetchRawCompanyList = (token) =>
  authorisedRequest(token, {
    method: 'GET',
    url: `${config.apiRoot}/v4/company-list`,
  })

const transformRawCompany = ({ latest_interaction, company }) => ({
  company: pick(company, ['id', 'name']),
  latestInteraction: pick(latest_interaction, ['date', 'id', 'subject']),
  ditParticipants: latest_interaction
    ? latest_interaction.dit_participants.map((participant) => ({
        name: participant.adviser ? participant.adviser.name : '',
        team: participant.team ? participant.team.name : '',
      }))
    : [],
})

const fetchCompanyLists = (token) =>
  fetchRawCompanyList(token).then(({ results }) =>
    Promise.all(
      results.map((list) =>
        fetchRawCompanyListItems(token, list.id).then(({ results }) => ({
          ...pick(list, ['id', 'name']),
          companies: results.map(transformRawCompany),
        }))
      )
    )
  )

module.exports = {
  fetchCompanyLists,
}

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

function fetchRawCompanyListItems(token, id) {
  return authorisedRequest(token, {
    method: 'GET',
    url: `${config.apiRoot}/v4/company-list/${id}/item`,
  })
}

const fetchRawCompanyList = (token) =>
  authorisedRequest(token, {
    method: 'GET',
    url: `${config.apiRoot}/v4/company-list`,
  })

const transformRawCompany = ({ latest_interaction, company }) => ({
  company: pick(company, ['id', 'name']),
  latestInteraction: pick(latest_interaction, ['date', 'id', 'subject']),
  ditParticipants: latest_interaction
    ? latest_interaction.dit_participants.map((participant) => ({
        name: get(participant, 'adviser.name'),
        team: get(participant, 'team.name'),
      }))
    : [],
})

const fetchCompanyLists = (token) =>
  fetchRawCompanyList(token).then(({ results }) =>
    Promise.all(
      results.map((list) =>
        fetchRawCompanyListItems(token, list.id).then(({ results }) => ({
          ...pick(list, ['id', 'name']),
          companies: results.map(transformRawCompany),
        }))
      )
    )
  )

module.exports = {
  fetchCompanyLists,
}

Change guard and use lodash


niclabs.insight.filter.LayerSelector = (function($) {

    /**
     * Construct a layer for the dashboard
     *
     * The layer selector provides an option to switch between layers of the dashboard
     *
     * @class niclabs.insight.filter.LayerSelector
     * @augments niclabs.insight.filter.Filter
     * @param {niclabs.insight.Dashboard} dashboard - dashboard that this filter belongs to
     * @param {Object} options - configuration options for the filter
     */
    var LayerSelector = function(dashboard, options) {
        var view = niclabs.insight.filter.Filter(dashboard, options);

        var layers = {};

        // Configure the view
        var select = $('<select>');

        // Hide the selector if there are no elements
        select.hide();

        select.on('change', function() {
            dashboard.active($(this).val());
        });

        // Add the selector to the view
        view.$.append(select);

        /**
         * Add a layer to the selector
         *
         * @memberof niclabs.insight.filter.LayerSelector
         * @param {string} id - id for the layer
         * @param {name} name - name of the layer
         */
        view.add = function(id, name) {
            layers[id] = name;
            select.append($('<option>').attr('value', id).text(name));

            // Show the selector if there is more than one layer
            if (layers.length > 1)
                select.show();
        };

        return view;
    };

    // Register the handler
    niclabs.insight.handler('layer-selector', 'filter', LayerSelector);

    return LayerSelector;
})(jQuery);

niclabs.insight.filter.LayerSelector = (function($) {

    /**
     * Construct a layer for the dashboard
     *
     * The layer selector provides an option to switch between layers of the dashboard
     *
     * @class niclabs.insight.filter.LayerSelector
     * @augments niclabs.insight.filter.Filter
     * @param {niclabs.insight.Dashboard} dashboard - dashboard that this filter belongs to
     * @param {Object} options - configuration options for the filter
     */
    var LayerSelector = function(dashboard, options) {
        var view = niclabs.insight.filter.Filter(dashboard, options);

        var layers = {};

        // Configure the view
        var select = $('<select>');

        // Hide the selector if there are no elements
        select.hide();

        select.on('change', function() {
            dashboard.active($(this).val());
        });

        // Add the selector to the view
        view.$.append(select);

        /**
         * Add a layer to the selector
         *
         * @memberof niclabs.insight.filter.LayerSelector
         * @param {string} id - id for the layer
         * @param {name} name - name of the layer
         */
        view.add = function(id, name) {
            layers[id] = name;
            select.append($('<option>').attr('value', id).text(name));

            // Show the selector if there is more than one layer
            // Note: layers.length returns undefined
            if (Object.keys(layers).length > 1)
                select.show();
        };

        return view;
    };

    // Register the handler
    niclabs.insight.handler('layer-selector', 'filter', LayerSelector);

    return LayerSelector;
})(jQuery);

Fix layer selector bug not working properly


import sectorGenerator from '../sector-generator';
import { testSystems } from '../generator-data';

describe('SectorGenerator', () => {
  let config;
  beforeEach(() => {
    config = {
      columns: 8,
      rows: 10,
      seed: 'asdfghjkl',
    };
  });

  it('should have a randomly generated name', () => {
    const { name } = sectorGenerator(config);
    expect(name).toEqual('Edena Omega');
  });

  it('should pass the seed, rows, and columns through to the result', () => {
    const testSeed = 'lkjhgfdsa';
    const { seed, rows, columns } = sectorGenerator({
      ...config,
      seed: testSeed,
    });
    expect(seed).toEqual(testSeed);
    expect(rows).toEqual(10);
    expect(columns).toEqual(8);
  });

  it('should generate a sector full of systems from a seed', () => {
    const { systems } = sectorGenerator(config);
    expect(systems).toMatchObject(testSystems);
  });
});

import sectorGenerator from '../sector-generator';
import { testSystems } from '../generator-data';

describe('SectorGenerator', () => {
  let config;
  beforeEach(() => {
    config = {
      columns: 8,
      rows: 10,
      seed: 'asdfghjkl',
      isBuilder: false,
    };
  });

  it('should have a randomly generated name', () => {
    const { name } = sectorGenerator(config);
    expect(name).toEqual('Edena Omega');
  });

  it('should pass the seed, rows, and columns through to the result', () => {
    const testSeed = 'lkjhgfdsa';
    const { seed, rows, columns } = sectorGenerator({
      ...config,
      seed: testSeed,
    });
    expect(seed).toEqual(testSeed);
    expect(rows).toEqual(10);
    expect(columns).toEqual(8);
  });

  it('should generate a sector full of systems from a seed', () => {
    const { systems } = sectorGenerator(config);
    expect(systems).toMatchObject(testSystems);
  });

  it('should generate a sector with no systems if it is initialized in builder mode', () => {
    const { systems } = sectorGenerator({ ...config, isBuilder: true });
    expect(systems).toEqual({});
  });
});

Add a test for the sector generator in builder mode


import React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { fetchPetitions } from 'actions/PetitionActions';
import Petitions from 'components/Petitions';

const PetitionsContainer = React.createClass({
  componentDidMount () {
    this.props.fetchPetitions(this.props);
  },

  render () {
    return (
      <div>
        <Helmet title='Petitions' />
        <Petitions {...this.props} />
      </div>
    );
  }

});

PetitionsContainer.fetchData = ({ store, location, params, history }) => {
  return store.dispatch(fetchPetitions({ location, params, history }));
};

PetitionsContainer.propTypes = {
  petitions: React.PropTypes.array,
  total: React.PropTypes.number,
  currentPage: React.PropTypes.number,
  perPage: React.PropTypes.number,
  fetchPetitions: React.PropTypes.func
};

const mapStateToProps = ({ petitions }) => ({
  petitions: petitions.data || [],
  total: petitions.total,
  currentPage: petitions.currentPage,
  perPage: petitions.perPage
});

// Add dispatchers to the component props,
// for fetching the data _client side_
const mapDispatchToProps = (dispatch) => {
  return { fetchPetitions: (options) => dispatch(fetchPetitions(options)) };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(PetitionsContainer);

import React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { fetchPetitions } from 'actions/PetitionActions';
import Petitions from 'components/Petitions';

const PetitionsContainer = React.createClass({
  componentDidMount () {
    this.props.fetchPetitions(this.props);
  },

  render () {
    return (
      <div>
        <Helmet title='Petitionen' />
        <Petitions {...this.props} />
      </div>
    );
  }

});

PetitionsContainer.fetchData = ({ store, location, params, history }) => {
  return store.dispatch(fetchPetitions({ location, params, history }));
};

PetitionsContainer.propTypes = {
  petitions: React.PropTypes.array,
  total: React.PropTypes.number,
  currentPage: React.PropTypes.number,
  perPage: React.PropTypes.number,
  fetchPetitions: React.PropTypes.func
};

const mapStateToProps = ({ petitions }) => ({
  petitions: petitions.data || [],
  total: petitions.total,
  currentPage: petitions.currentPage,
  perPage: petitions.perPage
});

// Add dispatchers to the component props,
// for fetching the data _client side_
const mapDispatchToProps = (dispatch) => {
  return { fetchPetitions: (options) => dispatch(fetchPetitions(options)) };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(PetitionsContainer);

Set petitions page title to "Petitionen"


/**
 * @author  Hamza Waqas <hamzawaqas@live.com>
 * @since   2/9/14
 */

var linkedin = require('../../')('77pmiwaj7b1ofn', 'EKKZCDTL7IiNUqra')
token = process.env.IN_TOKEN;

jasmine.getEnv().defaultTimeoutInterval = 20000;

linkedin = linkedin.init(token);

describe('API: People Test Suite', function() {

    it('should retrieve profile of current user', function(done ) {
       linkedin.people.me(function(err, data) {
           done();
       });
    });
})

/**
 * @author  Hamza Waqas <hamzawaqas@live.com>
 * @since   2/9/14
 */

var linkedin = require('../../')('75gyccfxufrozz', 'HKwSAPg0z7oGYfh5')
token = process.env.IN_TOKEN;

jasmine.getEnv().defaultTimeoutInterval = 20000;

linkedin = linkedin.init(token);

describe('API: People Test Suite', function() {

    it('should retrieve profile of current user', function(done) {
        linkedin.people.me(function(err, data) {
            done();
        });
    });

    it('should invite someone to connect', function(done) {
        linkedin.people.invite({
            "recipients": {
                "values": [{
                    "person": {
                        "_path": "/people/email=glavin.wiechert@gmail.com",
                        "first-name": "Glavin",
                        "last-name": "Wiechert"
                    }
                }]
            },
            "subject": "Invitation to connect.",
            "body": "Say yes!",
            "item-content": {
                "invitation-request": {
                    "connect-type": "friend"
                }
            }
        }, function(err, data) {
            console.log(err, data);
        });
    });
});

Add spec for new People Invite API.


import React from 'react';
import PropTypes from 'prop-types';
import { Link, withRouter } from 'react-router-dom';

import Result from './Result';

import styles from './ResultList.css';

const ResultList = ({ className = '', style = {}, results = [] }) => (
  <div className={`${styles.root} ${className}`} style={style}>
    {results.map((e, i) => (<Result key={i} data={e} />))}
  </div>
);

ResultList.propTypes = {
  className: PropTypes.string,
  style: PropTypes.object,
  results: PropTypes.arrayOf(PropTypes.object)
};

export default ResultList;

import React from 'react';
import PropTypes from 'prop-types';
import { Link, withRouter } from 'react-router-dom';

import Result from './Result';

import styles from './ResultList.css';

const ResultList = ({ className = '', elementClass = '', style = {}, results = [] }) => (
  <div className={`${styles.root} ${className}`} style={style}>
    {results.map((e, i) => (<Result className={elementClass} key={i} data={e} />))}
  </div>
);

ResultList.propTypes = {
  className: PropTypes.string,
  elementClass: PropTypes.string,
  style: PropTypes.object,
  results: PropTypes.arrayOf(PropTypes.object)
};

export default ResultList;

Add prop for list item class name


const mongoose = require('mongoose')
const Format = require('./Format.js')
const FilterBase = require('./common/FilterBase.js')
const middleware = require('./middleware/Format.js')

/**
 * Override the feed key, removing the unique constraint
 */
const schema = new mongoose.Schema({
  ...Format.schema.obj,
  feed: {
    type: mongoose.Types.ObjectId,
    required: true
  },
  priority: Number
})

schema.add(FilterBase)

schema.pre('validate', middleware.validate)

exports.schema = schema
exports.model = mongoose.model('FilteredFormat', schema)

const mongoose = require('mongoose')
const Format = require('./Format.js')
const FilterBase = require('./common/FilterBase.js')
const middleware = require('./middleware/Format.js')

/**
 * Override the feed key, removing the unique constraint
 */
const schema = new mongoose.Schema({
  ...Format.schema.obj,
  feed: {
    type: mongoose.Types.ObjectId,
    required: true
  },
  priority: Number
})

schema.add(FilterBase)

schema.pre('validate', middleware.validate)

exports.schema = schema
exports.model = mongoose.model('Filtered_Format', schema)

Use underscore in model name for consistency


"use strict";

module.exports = apiLinksMiddleware;

/**
 * If the apiBaseUrl argument is given then configure the models to include
 * links in their output.
 *
 * @param {Object} options The options to configure this function
 * @param {String} options.baseUrl The url where the main app is located
 * @param {String} options.apiBaseUrl The url where the API is mounted
 * @return {Function} Express compatible middleware.
 */
function apiLinksMiddleware(options) {
  return function apiLinks(req, res, next) {
    var schema = req.collection.schema;
    if (options.baseUrl) {
      schema.options.toJSON.baseUrl = options.baseUrl;
    }
    if (options.apiBaseUrl) {
      schema.options.toJSON.apiBaseUrl = options.apiBaseUrl;
    }
    next();
  };
}

"use strict";

module.exports = apiLinksMiddleware;

/**
 * If the apiBaseUrl argument is given then configure the models to include
 * links in their output.
 *
 * @param {Object} options The options to configure this function
 * @param {String} options.baseUrl The url where the main app is located
 * @param {String} options.apiBaseUrl The url where the API is mounted
 * @return {Function} Express compatible middleware.
 */
function apiLinksMiddleware(options) {
  return function apiLinks(req, res, next) {
    var schema = req.collection.schema;
    schema.options.toJSON.baseUrl = options.baseUrl;
    schema.options.toJSON.apiBaseUrl = options.apiBaseUrl;
    if (options.baseUrl) {
      schema.options.toJSON.baseUrl = options.baseUrl;
    }
    if (options.apiBaseUrl) {
      schema.options.toJSON.apiBaseUrl = options.apiBaseUrl;
    }
    next();
  };
}

Reset baseUrl/apiBaseUrl before each request


'use strict';

angular.module('users').controller('AuthenticationController', ['$scope', '$http', '$location', 'Authentication',
	function($scope, $http, $location, Authentication) {
		$scope.authentication = Authentication;

		// If user is signed in then redirect back home
		if ($scope.authentication.user) $location.path('/');

		$scope.signup = function() {
            if ($scope.credentials)
                $scope.convertID();
			$http.post('/auth/signup', $scope.credentials).success(function(response) {
				// If successful we assign the response to the global user model
				$scope.authentication.user = response;

				// And redirect to the index page
				$location.path('/');
			}).error(function(response) {
				$scope.error = response.message;
			});
		};

        $scope.convertID = function() {
            var dashPos = $scope.credentials.ufid.indexOf('-');
            if (dashPos !== -1)
                $scope.credentials.ufid = $scope.credentials.ufid.slice(0,dashPos - 1) + $scope.credentials.slice(dashPos + 1);
        };

		$scope.signin = function() {
			$http.post('/auth/signin', $scope.credentials).success(function(response) {
				// If successful we assign the response to the global user model
				$scope.authentication.user = response;

				// And redirect to the index page
				$location.path('/');
			}).error(function(response) {
				$scope.error = response.message;
			});
		};
	}
]);

'use strict';

angular.module('users').controller('AuthenticationController', ['$scope', '$http', '$location', 'Authentication',
	function($scope, $http, $location, Authentication) {
		$scope.authentication = Authentication;

		// If user is signed in then redirect back home
		if ($scope.authentication.user) $location.path('/');

		$scope.signup = function() {
            if ($scope.credentials)
                $scope.convertID();
			$http.post('/auth/signup', $scope.credentials).success(function(response) {
				// If successful we assign the response to the global user model
				$scope.authentication.user = response;

				// And redirect to the index page
				$location.path('/');
			}).error(function(response) {
				$scope.error = response.message;
			});
		};

        $scope.convertID = function() {
            var dashPos = $scope.credentials.ufid.indexOf('-');
            if (dashPos !== -1)
                $scope.credentials.ufid = $scope.credentials.ufid.slice(0,dashPos) + $scope.credentials.ufid.slice(dashPos + 1);
        };

		$scope.signin = function() {
			$http.post('/auth/signin', $scope.credentials).success(function(response) {
				// If successful we assign the response to the global user model
				$scope.authentication.user = response;

				// And redirect to the index page
				$location.path('/');
			}).error(function(response) {
				$scope.error = response.message;
			});
		};
	}
]);

Fix front-end dash removal method for signup ufid


import actions from '../actions/ArmyActionCreators'
import gameActions from '../actions/GameActionCreators'

const initialState = {
  armies: {
    '0': {
      'id': '0',
      'owner': 'albertoblaz',
      'provinceId': '22',
    },
    '1': {
      'id': '1',
      'owner': 'albertoblaz',
      'provinceId': '23',
    },
    '2': {
      'id': '2',
      'owner': 'adriantom3',
      'provinceId': '32',
    },
  },
}

const handlers = {
  [gameActions.START_GAME]: (state) =>
    Object.assign({}, state, initialState),
}

export default (reducer) =>
  (state = initialState, action) =>
    reducer(handlers, state, action)

// import actions from '../actions/ArmyActionCreators'
import gameActions from '../actions/GameActionCreators'

const initialState = {
  armies: {
    '0': {
      id: '0',
      owner: 'albertoblaz',
      name: 'Grande Armée',
      provinceId: '22',
    },
    '1': {
      id: '1',
      owner: 'albertoblaz',
      name: '2nd Republican Army',
      provinceId: '23',
    },
    '2': {
      id: '2',
      owner: 'adriantom3',
      name: 'Imperial Army',
      provinceId: '32',
    },
  },
}

const handlers = {
  [gameActions.START_GAME]: (state) =>
    Object.assign({}, state, initialState),
}

export default (reducer) =>
  (state = initialState, action) =>
    reducer(handlers, state, action)

Remove commas. Add name to each army


export const EventEmitter = {
    _events: {},
    dispatch: function (event, data) {
        if (!this._events[event]) {
            return; // no one is listening to this event
        }
        for (var i = 0; i < this._events[event].length; i++) {
            this._events[event][i](data);
        }
    },
    subscribe: function (event, callback) {
        if (!this._events[event]) {
            this._events[event] = []; // new event
        }
        this._events[event].push(callback);
    }
};

export const EventEmitter = {
    _events: {},
    dispatch: function (event, data) {
        if (!this._events[event]) {
            return; // no one is listening to this event
        }
        for (var i = 0; i < this._events[event].length; i++) {
            this._events[event][i](data);
        }
    },
    subscribe: function (event, callback) {
        if (!this._events[event]) {
            this._events[event] = []; // new event
        }
        this._events[event].push(callback);
    },
    unsubscribe: function (event, callback) {
        if (!this._events[event]) {
            return; // unsubscribing from an event that doesn't exist
        }
        for (var i = 0; i < this._events[event].length; i++) {
            if (this._events[event][i] === callback) {
                this._events[event].splice(i, 1); // removes it from event stack
            }
        }
    }
};

Add an unsubscribe to EventEmitter module


import React from 'react';
import { View, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  containter: {
    borderRadius: 30,
    position: 'absolute',
    bottom: 16,
    left: 48,
    right: 48,
    paddingVertical: 16,
    minHeight: 60,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'white',
  },
});

class Bubble extends React.PureComponent {
  render () {
    return (
      <View style={styles.containter}>
        {this.props.children}
      </View>
    );
  }
}

export default Bubble;

import React from 'react';
import { View, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  containter: {
    borderRadius: 30,
    position: 'absolute',
    bottom: 16,
    left: 48,
    right: 48,
    paddingVertical: 16,
    minHeight: 60,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'white',
  },
});

class Bubble extends React.PureComponent {
  render () {
    return (
      <View style={[styles.containter, this.props.style]}>
        {this.props.children}
      </View>
    );
  }
}

export default Bubble;

Update bubble to accept style props


import React from 'react';
import { Message } from 'retranslate';

import { logo } from '../common';

export const TermsOfUse = () => (

<Message>terms.of.use.title</Message>

<Message>terms.of.use.subheading</Message>

        {[...Array(6)].map((item, index) => (

            <Message>{`terms.of.use.list.item.${index + 1}`}</Message>

        ))}

        <Message>terms.of.use.main.line.1</Message>

        <Message>terms.of.use.main.line.2</Message>

        <Message>terms.of.use.main.line.3</Message>

);

export default TermsOfUse;

Add column to terms of use for padding


'use strict';

// MODULES //

var isObjectLike = require( '@stdlib/utils/is-object-like' );
var isFunction = require( '@stdlib/utils/is-function' );

// IS BUFFER //

/**
* FUNCTION: isBuffer( value )
*	Tests if a value is a Buffer instance.
*
* @param {*} value - value to validate
* @returns {Boolean} boolean indicating if a value is a Buffer instance
*/
function isBuffer( value ) {
	return (
		isObjectLike( value ) &&
		(
			value._isBuffer || // for envs missing Object.prototype.constructor (e.g., Safari 5-7)
			(
				value.constructor &&
				isFunction( value.constructor.isBuffer ) &&
				value.constructor.isBuffer( value )
			)
		)
	);
} // end FUNCTION isBuffer()

// EXPORTS //

module.exports = isBuffer;

'use strict';

// MODULES //

var isObjectLike = require( '@stdlib/utils/is-object-like' );

// IS BUFFER //

/**
* FUNCTION: isBuffer( value )
*	Tests if a value is a Buffer instance.
*
* @param {*} value - value to validate
* @returns {Boolean} boolean indicating if a value is a Buffer instance
*/
function isBuffer( value ) {
	return (
		isObjectLike( value ) &&
		(
			value._isBuffer || // for envs missing Object.prototype.constructor (e.g., Safari 5-7)
			(
				value.constructor &&
				// WARNING: `typeof` is not a foolproof check, as certain envs consider RegExp and NodeList instances to be functions
				typeof value.constructor.isBuffer === 'function' &&
				value.constructor.isBuffer( value )
			)
		)
	);
} // end FUNCTION isBuffer()

// EXPORTS //

module.exports = isBuffer;

Use built-in `typeof` operator and add warning


module.exports = {
  extends: [
    'plugin:flowtype/recommended',
    'wyze/rules/flow',
  ],
  parser: 'babel-eslint',
  plugins: [
    'flowtype',
  ],
}

module.exports = {
  extends: [
    'plugin:flowtype/recommended',
    'wyze/rules/flow',
  ],
  parser: 'babel-eslint',
  plugins: [
    'flowtype',
  ],
  rules: {
    // We disable this so we can import types and non-types from the same file.
    // The `eslint-plugin-import` rule handles this for us as well.
    'no-duplicate-imports': 'off',
  },
}

Fix `no-duplicate-imports` when using Flowtype


// 1. heroku login
// 2. git clone https://git.heroku.com/rekit-portal.git heroku-rekit-portal
// 3. Delete all files except .git folder
// 4. copy files from rekit/packages/rekit-portal to heroku-rekit-portal folder.
// 5. Commit files
// 6. git push origin master

Add instruction about how to deply to heroku.


define([], function() {
    'use strict';

    return withAsyncQueue;

    function withAsyncQueue() {

        this.before('initialize', function() {
            var self = this,
                deferreds = {},
                stacks = {};

            this.setupAsyncQueue = function(name) {

                var stack = stacks[name] = [],
                    objectData;

                self[name + 'Ready'] = function(callback) {
                    if (callback) {
                        // Legacy non-promise
                        if (objectData) {
                            callback.call(self, objectData);
                        } else {
                            stack.push(callback);
                        }
                    } else {
                        return deferreds[name] || (deferreds[name] = $.Deferred());
                    }
                };

                self[name + 'IsReady'] = function() {
                    if (deferreds[name]) {
                        return deferreds[name].isResolved();
                    }

                    return objectData !== undefined;
                };

                self[name + 'MarkReady'] = function(data) {
                    if (!data) throw "No object passed to " + name + "MarkReady";

                    if (deferreds[name]) {
                        deferreds[name].resolve(data);
                    } else {
                        deferreds[name] = $.Deferred();
                        deferreds[name].resolve(data);
                    }

                    objectData = data;

                    stack.forEach(function(c) {
                        c.call(self, objectData);
                    });
                    stack.length = 0;
                };

                self[name + 'Unload'] = function() {
                    deferreds[name] = null;
                    objectData = null;
                };
            };
        });
    }
});

define([], function() {
    'use strict';

    return withAsyncQueue;

    function withAsyncQueue() {

        this.before('initialize', function() {
            var self = this,
                deferreds = {},
                stacks = {};

            this.setupAsyncQueue = function(name) {

                var stack = stacks[name] = [],
                    objectData;

                self[name + 'Ready'] = function(callback) {
                    if (callback) {
                        // Legacy non-promise
                        if (objectData) {
                            callback.call(self, objectData);
                        } else {
                            stack.push(callback);
                        }
                    } else {
                        return deferreds[name] || (deferreds[name] = $.Deferred());
                    }
                };

                self[name + 'IsReady'] = function() {
                    if (deferreds[name]) {
                        return deferreds[name].state() == 'resolved';
                    }

                    return objectData !== undefined;
                };

                self[name + 'MarkReady'] = function(data) {
                    if (!data) throw "No object passed to " + name + "MarkReady";

                    if (deferreds[name]) {
                        deferreds[name].resolve(data);
                    } else {
                        deferreds[name] = $.Deferred();
                        deferreds[name].resolve(data);
                    }

                    objectData = data;

                    stack.forEach(function(c) {
                        c.call(self, objectData);
                    });
                    stack.length = 0;
                };

                self[name + 'Unload'] = function() {
                    deferreds[name] = null;
                    objectData = null;
                };
            };
        });
    }
});

Fix error from removed api after upgrading jquery


App.MockIdealController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {
    requiredFields: ['issuerId'],

    init: function() {
        this._super();
        this.set('model', App.MockIdeal.create());
    }

});

App.MockPaypalController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {

});

App.MockCreditcardController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {
    init: function() {
        this._super();
        this.set('model', App.MockCreditcard.create());
    },

    getIntegrationData: function(){
        return this.get('model');
    }
});

App.MockIdealController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {
    requiredFields: ['issuerId'],

    init: function() {
        this._super();
        this.set('model', App.MockIdeal.create());
    }

});

App.MockPaypalController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {
    getIntegrationData: function(){
        return {};
    }
});

App.MockCreditcardController = App.StandardPaymentMethodController.extend(App.ControllerValidationMixin, {
    init: function() {
        this._super();
        this.set('model', App.MockCreditcard.create());
    },

    getIntegrationData: function(){
        return this.get('model');
    }
});

REturn integration data for MockPal payment


/*
 * Copyright (C) 2017 con terra GmbH (info@conterra.de)
 *
 * 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.
 */
define([
        "dojo/_base/declare",
        "ct/array"
    ],
    function (declare, ct_array) {
        return declare([], {
            _serviceMetadata: null,

            constructor: function (serviceMetadata) {
                this._serviceMetadata = serviceMetadata;
            },

            getRendererForLayer: function (layerId) {
                var metadata = this._serviceMetadata;
                var url = ct_array.arraySearchFirst(metadata.urls, function (url) {
                    return layerId === url.id;
                }).url;
                var details = ct_array.arraySearchFirst(metadata.details, function (detail) {
                    return url === detail.url;
                });
                var id = layerId.split("/")[layerId.split("/").length - 1];
                return details.layers[id] && details.layers[id].drawingInfo.renderer;
            }
        });
    });

/*
 * Copyright (C) 2017 con terra GmbH (info@conterra.de)
 *
 * 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.
 */
define([
        "dojo/_base/declare",
        "ct/array"
    ],
    function (declare, ct_array) {
        return declare([], {
            _serviceMetadata: null,

            constructor: function (serviceMetadata) {
                this._serviceMetadata = serviceMetadata;
            },

            getRendererForLayer: function (layerId) {
                var metadata = this._serviceMetadata;
                var url = ct_array.arraySearchFirst(metadata.urls, function (url) {
                    return layerId === url.id;
                }).url;
                var details = ct_array.arraySearchFirst(metadata.details, function (detail) {
                    return url === detail.url;
                });
                var id = layerId.split("/")[layerId.split("/").length - 1];
                return ct_array.arraySearchFirst(details.layers, function (metadata) {
                    return metadata.id.toString() === id;
                }).drawingInfo.renderer;
            }
        });
    });

Fix renderer problems in case of non-transit layer ids


import React from 'react';
import * as fdc3 from 'openfin-fdc3';
import { FaChartLine, FaRegListAlt  } from 'react-icons/fa'
import * as PropTypes from 'prop-types';
import { Utils, Intents } from 'stockflux-core';

const handleWatchlistAddClick = (code, name) => {
  fdc3.raiseIntent('WatchlistAdd', {
    type: 'security',
    name,
    id: {
      default: code
    }
  });
};

const SearchResult = ({ code, name }) => (

{Utils.truncate(name)}

{code}

      <button
        type="button"
        className="buttonAction"
        onClick={() => handleWatchlistAddClick(code, name)}
      >
        <FaRegListAlt className="buttonIcon"/>
      </button>
      <button
        type="button"
        className="buttonAction"
        onClick={() => Intents.viewChart(code, name)}
      >
        <FaChartLine className="buttonIcon"/>
      </button>

);

SearchResult.propTypes = {
  code: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired
};

export default SearchResult;

import React from 'react';
import * as fdc3 from 'openfin-fdc3';
import { FaChartLine, FaRegListAlt, FaNewspaper  } from 'react-icons/fa'
import * as PropTypes from 'prop-types';
import { Utils, Intents } from 'stockflux-core';

const handleWatchlistAddClick = (code, name) => {
  fdc3.raiseIntent('WatchlistAdd', {
    type: 'security',
    name,
    id: {
      default: code
    }
  });
};

const handleViewNewsClick = async (code) => {
  Intents.viewNews(code);
}

const SearchResult = ({ code, name }) => (

{Utils.truncate(name)}

{code}

      <button
        type="button"
        className="buttonAction"
        onClick={() => handleViewNewsClick(code)}
      >
        <FaNewspaper className="buttonIcon"/>
      </button>
      <button
        type="button"
        className="buttonAction"
        onClick={() => handleWatchlistAddClick(code, name)}
      >
        <FaRegListAlt className="buttonIcon"/>
      </button>
      <button
        type="button"
        className="buttonAction"
        onClick={() => Intents.viewChart(code, name)}
      >
        <FaChartLine className="buttonIcon"/>
      </button>

);

SearchResult.propTypes = {
  code: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired
};

export default SearchResult;

Add news link to launcher search results


class MCProjectsListComponentController {
    /*@ngInject*/
    constructor($mdEditDialog, mcshow) {
        this.$mdEditDialog = $mdEditDialog;
        this.mcshow = mcshow;
        this.sortOrder = 'name';
    }

    editStatusNote(event, project, index) {
        event.stopPropagation();
        this.$mdEditDialog.small({
            modelValue: project.status_notes[index].note,
            placeholder: 'Add status message...',
            save: function(input) {
                project.status_notes[index].note = input.$modelValue;
                project.update({status_notes: project.status_notes});
            },
            targetEvent: event
        });
    }

    showProjectOverview(project) {
        this.mcshow.projectOverviewDialog(project);
    }
}

angular.module('materialscommons').component('mcProjectsList', {
    templateUrl: 'app/projects/mc-projects-list.html',
    controller: MCProjectsListComponentController,
    bindings: {
        projects: '<',
        query: '='
    }
});

class MCProjectsListComponentController {
    /*@ngInject*/
    constructor(mcshow) {
        this.mcshow = mcshow;
        this.sortOrder = 'name';
    }

    showProjectOverview(project) {
        this.mcshow.projectOverviewDialog(project);
    }
}

angular.module('materialscommons').component('mcProjectsList', {
    templateUrl: 'app/projects/mc-projects-list.html',
    controller: MCProjectsListComponentController,
    bindings: {
        projects: '<',
        query: '='
    }
});

Remove code for editing table entries related to status_notes (status_notes no longer exist. We don't allow editing of reminders here.


var _          = require('underscore');
var moment     = require('moment');
var Promise    = require('promise');
var fetch      = require('./../fetch');
var cache      = require('../cache');
var VideoModel = require('../../db/videos/models/Video');
var saveVideo  = require('../../db/videos/saveVideo');
var getVideos  = require('../../db/videos/getVideos');

module.exports = function (req, res) {
    var query    = req.query;
    var videoIds = query.id.split(',');

    getVideos(videoIds)
        .then(videoResult => {
            var itemsFromDB   = videoResult.itemsFromDB;
            var itemsNotFound = videoResult.itemsNotFound;

            var config = new fetch.Config({
                endpoint: 'videos',
                query: query
            });

            // Update query
            query.id = itemsNotFound.join(',');

            fetch(config).then(result => {
                var fromCache = result.fromCache;
                var items     = result.data.items.concat(itemsFromDB);

                fetch.end(res, items);

                if (!fromCache) {
                    // Save/Update videos in mongoDB
                    _.each(result.data.items, function (videoItem) {
                        videoItem._id     = videoItem.id;
                        videoItem.expires = moment().add(7, 'days').toDate();

                        saveVideo(videoItem);
                    });
                }
            });
        });
};

var _          = require('underscore');
var moment     = require('moment');
var Promise    = require('promise');
var fetch      = require('./../fetch');
var cache      = require('../cache');
var VideoModel = require('../../db/videos/models/Video');
var saveVideo  = require('../../db/videos/saveVideo');
var getVideos  = require('../../db/videos/getVideos');

module.exports = function (req, res) {
    var query    = req.query;
    var videoIds = query.id.split(',');

    getVideos(videoIds)
        .then(videoResult => {
            var itemsFromDB   = videoResult.itemsFromDB;
            var itemsNotFound = videoResult.itemsNotFound;

            var config = new fetch.Config({
                endpoint: 'videos',
                query: query
            });

            // Update query
            query.id = itemsNotFound.join(',');

            if(!query.id) {
                fetch.end(res, itemsFromDB);
                return;
            }

            fetch(config).then(result => {
                var fromCache = result.fromCache;
                var items     = result.data.items.concat(itemsFromDB);

                if (!fromCache) {
                    // Save/Update videos in mongoDB
                    _.each(result.data.items, function (videoItem) {
                        videoItem._id     = videoItem.id;
                        videoItem.expires = moment().add(7, 'days').toDate();

                        saveVideo(videoItem);
                    });
                }
            });
        });
};

Check query.id before fetching data from youtube-api


const {remote} = require('electron');

const Titlebar = React.createClass({

  render: function () {

    const w = {
        min: () => {
            const win = remote.getCurrentWindow()
            win.minimize()
        },
        max: () => {
            const win = remote.getCurrentWindow()
            if (!win.isMaximized()) win.maximize()
            else win.unmaximize()
        },
        quit: () => {
            const win = remote.getCurrentWindow()
            win.close()
        },
    }

    return (

                YouWatch

    );
  }
});

module.exports = Titlebar;

const {remote} = require('electron');

const Titlebar = React.createClass({

  render: function () {

    const w = {
        min: () => {
            const win = remote.getCurrentWindow()
            win.minimize()
        },
        max: () => {
            const win = remote.getCurrentWindow()
            if (!win.isMaximized()) win.maximize()
            else win.unmaximize()
        },
        quit: () => {
            const win = remote.getCurrentWindow()
            win.close()
        },
    }

    let titleBarText;
    if (this.props.platform === 'darwin') {
        titleBarText = (
YouWatch
);
    } else {
        titleBarText = (

                YouWatch

        );
    }

    return (

            {titleBarText}

    );
  }
});

module.exports = Titlebar;

Remove the icon from the title bar on macOS


import css from 'steal/css';
import lessEngine from 'less';

export var instantiate = css.instantiate;

export function translate(load) {
	var pathParts = (load.address+'').split('/');
		pathParts[pathParts.length - 1] = ''; // Remove filename

	var paths = [];
	if (typeof window !== 'undefined') {
		var pathParts = (load.address+'').split('/');
		pathParts[pathParts.length - 1] = ''; // Remove filename
		paths = [pathParts.join('/')];
	}
	return new Promise(function(resolve, reject){
		new (lessEngine.Parser)({
	        optimization: lessEngine.optimization,
	        paths: [pathParts.join('/')]
	    }).parse(load.source, function (e, root) {
			if(e){
				reject(e);
			} else {
				resolve(root.toCSS());
			}
		});
	});

};

export var buildType = "css";

var css = require("steal/css");
var lessEngine = require("less");

exports.instantiate = css.instantiate;

exports.translate = function(load) {
	var pathParts = (load.address+'').split('/');
		pathParts[pathParts.length - 1] = ''; // Remove filename

	var paths = [];
	if (typeof window !== 'undefined') {
		var pathParts = (load.address+'').split('/');
		pathParts[pathParts.length - 1] = ''; // Remove filename
		paths = [pathParts.join('/')];
	}
	return new Promise(function(resolve, reject){
		new (lessEngine.Parser)({
	        optimization: lessEngine.optimization,
	        paths: [pathParts.join('/')]
	    }).parse(load.source, function (e, root) {
			if(e){
				reject(e);
			} else {
				resolve(root.toCSS());
			}
		});
	});
};

exports.buildType = "css";

Convert steal plugin to use CommonJS


export default function composeReducers(reducers) {
  return (state, action) => reducers.reduce(
    (prevState, reducer) => reducer(prevState, action),
    state
  );
}

export default function composeReducers(reducers) {
  return (state, action) => reducers.reduceRight(
    (prevState, reducer) => reducer(prevState, action),
    state
  );
}

Update composeReducers to go right-to-left


'use strict';

module.exports = {
    name: 'singleLinePerSelector',
    nodeTypes: ['ruleset'],
    message: 'Each selector should be on its own line.',

    lint: function singleLinePerSelectorLinter (config, node) {
        var report = false;
        var results = [];
        var self = this;

        node.forEach('delimiter', function (element, index) {
            element = node.get(index + 1);

            // Check if the delimiter is on the next line
            if (!element.is('space')) {
                element = node.get(index - 1);
            }

            switch (config.style) {
                case '18f':
                    node.forEach('selector', function (selector) {
                        selector = selector.first().first('ident').content;

                        if (selector.length >= 5) {
                            report = true;
                            return;
                        }
                    });

                    break;
                default:
                    report = true;

                    break;
            }

            if (report && element.content.indexOf('\n') === -1) {
                results.push({
                    column: element.start.column,
                    line: element.start.line,
                    message: self.message
                });
            }
        });

        if (results.length) {
            return results;
        }
    }
};

'use strict';

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

module.exports = {
    name: 'singleLinePerSelector',
    nodeTypes: ['ruleset'],
    message: 'Each selector should be on its own line.',

    lint: function singleLinePerSelectorLinter (config, node) {
        var report = false;
        var results = [];
        var self = this;

        node.forEach('delimiter', function (element, index) {
            element = helpers.ensureObject(node.get(index + 1));

            // Check if the delimiter is on the next line
            if (element.type !== 'space') {
                element = helpers.ensureObject(node.get(index - 1));
            }

            switch (config.style) {
                case '18f':
                    node.forEach('selector', function (selector) {
                        selector = helpers.ensureObject(selector.first().first('ident'));
                        selector = selector.content;

                        if (selector && selector.length >= 5) {
                            report = true;
                            return;
                        }
                    });

                    break;
                default:
                    report = true;

                    break;
            }

            if (report && element.content.indexOf('\n') === -1) {
                results.push({
                    column: element.start.column,
                    line: element.start.line,
                    message: self.message
                });
            }
        });

        if (results.length) {
            return results;
        }
    }
};

Add some stricter checks in singleLinePerSelector


// Copyright 2015, EMC, Inc.

'use strict';

module.exports = {
    friendlyName: 'Bootstrap Ubuntu',
    injectableName: 'Task.Linux.Bootstrap.Ubuntu',
    implementsTask: 'Task.Base.Linux.Bootstrap',
    options: {
        kernelFile: 'vmlinuz-3.16.0-25-generic',
        initrdFile: 'initrd.img-3.16.0-25-generic',
        kernelUri: '{{ api.server }}/common/{{ options.kernelFile }}',
        initrdUri: '{{ api.server }}/common/{{ options.initrdFile }}',
        basefs: 'common/base.trusty.3.16.0-25-generic.squashfs.img',
        overlayfs: 'common/discovery.overlay.cpio.gz',
        profile: 'linux.ipxe',
        comport: 'ttyS0'
    },
    properties: {
        os: {
            linux: {
                distribution: 'ubuntu',
                release: 'trusty',
                kernel: '3.16.0-25-generic'
            }
        }
    }
};

// Copyright 2015, EMC, Inc.

'use strict';

module.exports = {
    friendlyName: 'Bootstrap Ubuntu',
    injectableName: 'Task.Linux.Bootstrap.Ubuntu',
    implementsTask: 'Task.Base.Linux.Bootstrap',
    options: {
        kernelFile: 'vmlinuz-3.13.0-32-generic',
        initrdFile: 'initrd.img-3.13.0-32-generic',
        kernelUri: '{{ api.server }}/common/{{ options.kernelFile }}',
        initrdUri: '{{ api.server }}/common/{{ options.initrdFile }}',
        basefs: 'common/base.trusty.3.13.0-32-generic.squashfs.img',
        overlayfs: 'common/discovery.overlay.cpio.gz',
        profile: 'linux.ipxe',
        comport: 'ttyS0'
    },
    properties: {
        os: {
            linux: {
                distribution: 'ubuntu',
                release: 'trusty',
                kernel: '3.13.0-32-generic'
            }
        }
    }
};

Revert "Update kernel version of ubuntu microkernel to 3.16.0-25-generic."


const Job = ({ locations, deadline, companyImage, companyName, jobTitle, ingress, jobName }) => {
  if (locations.length >= 2) {
    locations = `${locations.slice(0, -1).join(', ')} and ${locations[locations.length - 1]}`;
  } else if (locations.length === 0) {
    locations = 'Ikke spesifisert';
  }

  return (

          <picture>
            <source srcSet={companyImage.lg} media="(max-width: 992px)" />

          </picture>

          {companyName} - {jobTitle}

{ingress}

Type: {jobName}

Sted: {locations}

Frist: {deadline}

  );
};

export default Job;

const Job = ({ locations, deadline, companyImage, companyName, jobTitle, ingress, jobName }) => {
  if (locations.length >= 2) {
    locations = `${locations.slice(0, -1).join(', ')} og ${locations[locations.length - 1]}`;
  } else if (locations.length === 0) {
    locations = 'Ikke spesifisert';
  }

  return (

          <picture>
            <source srcSet={companyImage.lg} media="(max-width: 992px)" />

          </picture>

          {companyName} - {jobTitle}

{ingress}

Type: {jobName}

Sted: {locations}

Frist: {deadline}

  );
};

export default Job;

Use Norwegian 'og', not 'and'


// @flow
import React, {Component} from 'react'
import {Box, Text} from './'
import {globalStyles, globalColors} from '../styles/style-guide'

import type {Props} from './usernames'

export function usernameText ({type, users, style, inline}: Props) {
  return users.map((u, i) => {
    const userStyle = {
      textDecoration: 'inherit',
      ...style,
    }

    if (u.broken) {
      userStyle.color = globalColors.red
    }

    if (inline) {
      userStyle.display = 'inline-block'
    }

    if (u.you) {
      Object.assign(userStyle, globalStyles.italic)
    }

    return (
      <Text
        key={u.username}
        type={type}
        style={userStyle}>{u.username}
        {
          (i !== users.length - 1) && // Injecting the commas here so we never wrap and have newlines starting with a ,
            <Text type={type} style={{...style, marginRight: 1}}>,</Text>}
      </Text>
    )
  })
}

export default class Usernames extends Component<void, Props, void> {
  render () {
    const containerStyle = this.props.inline ? {display: 'inline'} : {...globalStyles.flexBoxRow, flexWrap: 'wrap'}

    return (
      <Box style={{...containerStyle, textDecoration: 'inherit'}}>
        {usernameText(this.props)}
      </Box>
    )
  }
}

// @flow
import React, {Component} from 'react'
import {Box, Text} from './'
import {globalStyles, globalColors} from '../styles/style-guide'
import {isMobile} from '../constants/platform'

import type {Props} from './usernames'

export function usernameText ({type, users, style, inline}: Props) {
  return users.map((u, i) => {
    const userStyle = {...style}

    if (!isMobile) {
      userStyle.textDecoration = 'inherit'
    }

    if (u.broken) {
      userStyle.color = globalColors.red
    }

    if (inline) {
      userStyle.display = 'inline-block'
    }

    if (u.you) {
      Object.assign(userStyle, globalStyles.italic)
    }

    return (
      <Text
        key={u.username}
        type={type}
        style={userStyle}>{u.username}
        {
          (i !== users.length - 1) && // Injecting the commas here so we never wrap and have newlines starting with a ,
            <Text type={type} style={{...style, marginRight: 1}}>,</Text>}
      </Text>
    )
  })
}

export default class Usernames extends Component<void, Props, void> {
  render () {
    const containerStyle = this.props.inline ? {display: 'inline'} : {...globalStyles.flexBoxRow, flexWrap: 'wrap'}

    return (
      <Box style={{...containerStyle, textDecoration: 'inherit'}}>
        {usernameText(this.props)}
      </Box>
    )
  }
}

Fix unknown textDecoration style prop being passed to React Native


'use strict';

module.exports = function(app) {
  app.route("/").get(function(res, req) {
    req.sendFile(process.cwd() + '/public/index.html');
  });
};

'use strict';

module.exports = function(app) {
  app.route("/").get(function(req, res) {
    res.sendFile(process.cwd() + '/public/index.html');
  });

  app.route("/:query").get(function(req, res) {
    var query = req.params.query;

    if (Number(query)) {
      var unix = Number(query);
      var formattedDate = formatDate(new Date(unix * 1000));
    } else if (new Date(query)) {
      var utcDate = createDateAsUTC(new Date(query));
      var formattedDate = formatDate(utcDate);
      var unix = utcDate.getTime() / 1000;
    }

    if (!unix || !formattedDate) {
      res.sendFile(process.cwd() + '/public/index.html');
    }

    res.send({'unix': unix, 'natural': formattedDate});
  });
};

function formatDate(date) {
  if (!(typeof date === 'object')) {
    return;
  }

  var month = monthIntToMonth(date.getUTCMonth());
  var year = date.getUTCFullYear();
  var date = date.getUTCDate();
  return `${month} ${date}, ${year}`;
}

function monthIntToMonth(num) {
  switch (num) {
    case 0:
      return 'January';
    case 1:
      return 'February';
    case 2:
      return 'March';
    case 3:
      return 'April';
    case 4:
      return 'May';
    case 5:
      return 'June';
    case 6:
      return 'July';
    case 7:
      return 'August';
    case 8:
      return 'September';
    case 9:
      return 'October';
    case 10:
      return 'November';
    case 11:
      return 'December';
  }
};

function createDateAsUTC(date) {
    return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
}

Add logic for the application to correctly parse the query.


/**
 * From where to look for files, starting with the location of this file.
 */
basePath = '../';

/**
 * This is the list of file patterns to load into the browser during testing.
 */
files = [
  JASMINE,
  JASMINE_ADAPTER,
  'vendor/angular/angular.js',
  'vendor/angular/angular-mocks.js',
  'src/**/*.js',
  'dist/tmp/**/*.js'
];

/**
 * How to report, by default.
 */
reporters = 'dots';

/**
 * On which port should the browser connect, on which port is the test runner
 * operating, and what is the URL path for the browser to use.
 */
port = 9018;
runnerPort = 9100;
urlRoot = '/';

/**
 * Log at a very low level, but not quite debug.
 */
logLevel = LOG_INFO;

/**
 * Disable file watching by default.
 */
autoWatch = false;

/**
 * The list of browsers to launch to test on. This is empty by default, so you
 * will need to manually open your browser to http://localhot:9018/ for the
 * tests to work. You can also just add the executable name of your browser(s)
 * here, e.g. 'firefox', 'chrome', 'chromium'.
 */
browsers = [
  'firefox'
];

/**
 * From where to look for files, starting with the location of this file.
 */
basePath = '../';

/**
 * This is the list of file patterns to load into the browser during testing.
 */
files = [
  JASMINE,
  JASMINE_ADAPTER,
  'vendor/angular/angular.js',
  'vendor/angular/angular-mocks.js',
  'src/**/*.js',
  'dist/tmp/**/*.js'
];

/**
 * How to report, by default.
 */
reporters = 'dots';

/**
 * On which port should the browser connect, on which port is the test runner
 * operating, and what is the URL path for the browser to use.
 */
port = 9018;
runnerPort = 9100;
urlRoot = '/';

/**
 * Log at a very low level, but not quite debug.
 */
logLevel = LOG_INFO;

/**
 * Disable file watching by default.
 */
autoWatch = false;

/**
 * The list of browsers to launch to test on. This is empty by default, so you
 * will need to manually open your browser to http://localhot:9018/ for the
 * tests to work. Currently available browser names:
 * Chrome, ChromeCanary, Firefox, Opera, Safari, PhantomJS
 */
browsers = [
  'Firefox'
];

Change browser-name case, add browser list


const spawn = require('cross-spawn');
const script = process.argv[2];
const args = process.argv.slice(3);

switch (script) {
  case 'build':
  case 'start':
  case 'test': {
    const { status } = spawn.sync(
      'node',
      [require.resolve(`../scripts/${script}`)].concat(args),
      { stdio: 'inherit' }
    );
    process.exit(status);
    break;
  }
  case 'eject': {
    const { status } = spawn.sync(
      'node',
      [require.resolve(`./${script}`)].concat(args),
      { stdio: 'inherit' }
    );
    process.exit(status);
    break;
  }
  default:
    console.log(`Unknown script "${script}".`);
    break;
}

const spawn = require('cross-spawn');
const script = process.argv[2];
const args = process.argv.slice(3);

const run = script => {
  const { status } = spawn.sync(
    'node',
    [require.resolve(script)].concat(args),
    { stdio: 'inherit' }
  );
  process.exit(status);
};

switch (script) {
  case 'build':
  case 'start':
  case 'test': {
    run(`../scripts/${script}`);
    break;
  }
  case 'eject': {
    run(`./${script}`);
    break;
  }
  default:
    console.log(`Unknown script "${script}".`);
    break;
}

Use run method to remove duplicate codes


function (doc) {
    if(doc.doc_type === "DataSourceConfiguration") {
        // TODO: Do I need to check for existence of these properties?
        // TODO: Can I get rid of data sources by domain and just use this view?
        emit([
            doc.domain,
            doc.referenced_doc_type,
            doc.meta.build.source_id,
            doc.meta.build.app_id,
            doc.meta.build.app_version
        ], null);
    }
}

function (doc) {
    if(doc.doc_type === "DataSourceConfiguration") {
        // TODO: Can I get rid of the data_sources_by_domain view and just use this?
        var source_id = null;
        var app_id = null;
        var app_version = null;
        if (doc.meta && doc.meta.build){
            source_id = doc.meta.build.source_id;
            app_id = doc.meta.build.app_id;
            app_version = doc.meta.build.app_version;
        }
        emit([
            doc.domain,
            doc.referenced_doc_type,
            source_id,
            app_id,
            app_version
        ], null);
    }
}

Check for existence of attributes in couch view


describe("A leapHelperService", function() {
    beforeEach(module('angular-leap'));
});

describe("A leapHelperService", function() {
    beforeEach(module('angular-leap'));

    it("should offer a timeout function", inject(function(leapHelperService) {
        expect(leapHelperService.timeout).toBeDefined();
    }));

});

Add test for leapHelperService .timeout


import m from 'mithril';
import R from 'ramda';
import Velocity from 'velocity-animate';
import Promise from 'bluebird';
import io from 'socket.io-client';

import { Head, Spotify } from '../components';
import { setPlaylist } from './RestRequests';

const registerSpotify = () => Spotify.getAuthorization();

const hookUpThemSweetSockets = socket => {
  socket.on('addSong', data => Spotify.addSong(data.uriList, data.playlist));

  socket.on('removeSong', data => Spotify.removeSong(data.uris, data.playlist));

  socket.on('makePlaylist', ({ user }) =>
    Spotify.makePlaylist('mix-' + user)
    .then(({ id }) => setPlaylist(user, id)));
};

// VIEW MODEL

// VIEWS

const view = () =>

  ;

// CONTROLLER
const controller = () => {
  const socket = io.connect('http://localhost:3001');
  socket.on('connect', () => console.log('Hooked on sockets worked for me!'));
  hookUpThemSweetSockets(socket);
};

// EXPORT
export default {
  view,
  controller,
};

import m from 'mithril';
import R from 'ramda';
import Velocity from 'velocity-animate';
import Promise from 'bluebird';
import io from 'socket.io-client';

import { Head, Spotify } from '../components';
import { setPlaylist } from './RestRequests';

const registerSpotify = () => Spotify.getAuthorization();

const hookUpThemSweetSockets = socket => {
  socket.on('addSong', data => Spotify.addSong(data.uriList, data.playlist));

  socket.on('removeSong', data => Spotify.removeSong(data.uris, data.playlist));

  socket.on('makePlaylist', ({ user }) =>
    Spotify.makePlaylist('Mixta-' + user)
    .then(({ id }) => setPlaylist(user, id)));
};

// VIEW MODEL

// VIEWS

const view = () =>

  ;

// CONTROLLER
const controller = () => {
  const socket = io.connect('http://localhost:3001');
  socket.on('connect', () => console.log('Hooked on sockets worked for me!'));
  hookUpThemSweetSockets(socket);
};

// EXPORT
export default {
  view,
  controller,
};

Change default playlist name scheme


const Labels = {
  type: 'object',
  title: 'Labels',
  properties: {
    items: {
      type: 'array',
      duplicable: true,
      addLabel: 'Add Label',
      getter(job) {
        let labels = job.getLabels() || {};

        return Object.keys(labels).map(function (key) {
          return {
            key,
            value: labels[key]
          };
        });
      },
      itemShape: {
        properties: {
          key: {
            title: 'Label Name',
            type: 'string'
          },
          value: {
            title: 'Label Value',
            type: 'string'
          }
        }
      }
    }
  }
};

module.exports = Labels;

const Labels = {
  type: 'object',
  title: 'Labels',
  description: 'Attach metadata to jobs to expose additional information to other jobs.',
  properties: {
    items: {
      type: 'array',
      duplicable: true,
      addLabel: 'Add Label',
      getter(job) {
        let labels = job.getLabels() || {};

        return Object.keys(labels).map(function (key) {
          return {
            key,
            value: labels[key]
          };
        });
      },
      itemShape: {
        properties: {
          key: {
            title: 'Label Name',
            type: 'string'
          },
          value: {
            title: 'Label Value',
            type: 'string'
          }
        }
      }
    }
  }
};

module.exports = Labels;

Add job label description equal to service label description


// Copied from https://github.com/facebook/jest/blob/master/packages/jest-config/src/utils.js
// in order to reduce incompatible jest dependencies

const path = require('path');

module.exports = {
  replaceRootDirInPath : (
    rootDir,
    filePath,
  ) => {
    if (!/^<rootDir>/.test(filePath)) {
      return filePath;
    }

    return path.resolve(
      rootDir,
      path.normalize('./' + filePath.substr('<rootDir>'.length)),
    );
  }
}

// Copied from https://github.com/facebook/jest/blob/master/packages/jest-config/src/utils.js
// in order to reduce incompatible jest dependencies

const path = require('path');

module.exports = {
  replaceRootDirInPath : (rootDir,filePath) => {
    if (!/^<rootDir>/.test(filePath)) {
      return filePath;
    }

    return path.resolve(
      rootDir,
      path.normalize('./' + filePath.substr('<rootDir>'.length))
    )
  }
}

Fix issue with node 6 where it cannot handle trailing comma


(function (window, $) {
  'use strict';

  var $resultFields, resultFieldsLength, resultFieldsUrl, updateIntervalId;

  $resultFields = $(".result-field");
  resultFieldsLength = $resultFields.length;
  resultFieldsUrl = window.location.pathname + "/populated_result_fields.json";

  function updateResults() {
    $.getJSON(resultFieldsUrl)
      .done(updateResultFields);
  }

  function updateResultFields(populatedFields) {
    $resultFields.each(function () {
      var $this = $(this),
        isPopulated = (populatedFields.indexOf($this.data("resultFieldName")) != -1);
      $(this).toggleClass("result-field--populated", isPopulated);
    });
    if (populatedFields.length == resultFieldsLength) {
      window.clearInterval(updateIntervalId);
      window.location.reload();
    }
  }

  if (resultFieldsLength > 0) {
    updateIntervalId = window.setInterval(updateResults, 200);
  }
}(this, jQuery));

(function (window, $) {
  'use strict';

  var $resultFields, resultFieldsLength, resultFieldsUrl, updateIntervalId;

  $resultFields = $(".result-field");
  resultFieldsLength = $resultFields.length;
  resultFieldsUrl = window.location.pathname + "/populated_result_fields.json";

  function updateResults() {
    $.getJSON(resultFieldsUrl)
      .done(updateResultFields);
  }

  function updateResultFields(populatedFields) {
    var allFieldsPopulated = true;
    $resultFields.each(function () {
      var $this = $(this),
        isPopulated = (populatedFields.indexOf($this.data("resultFieldName")) != -1);
      $(this).toggleClass("result-field--populated", isPopulated);
      allFieldsPopulated = allFieldsPopulated && isPopulated;
    });
    if (allFieldsPopulated) {
      window.clearInterval(updateIntervalId);
      window.location.reload();
    }
  }

  if (resultFieldsLength > 0) {
    updateIntervalId = window.setInterval(updateResults, 200);
  }
}(this, jQuery));

Improve JS detection of fully populated reports


/*global angular*/
(function () {
    'use strict';

    angular.module('core').directive('qrDeviceInteraction', ["$location", function ($location) {
        var template = '';
        template += '';
        template += '    <qr text="qrConfig.slideUrl" type-number="qrConfig.typeNumber" correction-level="qrConfig.correctionLevel"';
        template += '        size="qrConfig.size" input-mode="qrConfig.inputMode" image="qrConfig.image">';
        template += '    </qr>';
        template += '';

        return {
            restrict: 'E',
            template: template,
        };
    }]);
}());

/*global angular*/
(function () {
    'use strict';

    angular.module('core').directive('qrDeviceInteraction', function () {
        var template = '';
        template += '';
        template += '    <qr text="qrConfig.slideUrl" type-number="qrConfig.typeNumber" correction-level="qrConfig.correctionLevel"';
        template += '        size="qrConfig.size" input-mode="qrConfig.inputMode" image="qrConfig.image">';
        template += '    </qr>';
        template += '';

        return {
            restrict: 'E',
            template: template,
        };
    });
}());

Remove unnecessary $location parameter in qr-device-interaction.


import gql from 'graphql-tag';

export default gql`
  mutation createGroupMutation($name: String!) {
    create_group(input: { name: $name }) {
      group {
        id
        href
      }
    }
  }
`;

import gql from 'graphql-tag';

export default gql`
  mutation createGroupMutation($name: String!, $description: String) {
    create_group(input: { name: $name, description: $description}) {
      group {
        id
        href
      }
    }
  }
`;

Add description to group mutation


'use strict';

ords.controller('projectController', function ($scope, $routeParams, AuthService, Project, User, Member, growl) {

	//
	// This page doesm't make sense to view
	// without being logged in, so redirect
	// back to the home view
	//
	AuthService.check();

	$scope.editMember = function(id){
		growl.warning("Edit Member clicked");
	}

	$scope.removeMember = function(id){
		Member.delete({ id: $routeParams.id, roleid: id},
			function(){
				growl.success("Project member removed");
			},
			function(){
				growl.error("There was a problem removing this project member");
			}
		);

	}

	$scope.project = Project.get({ id: $routeParams.id });

	$scope.members = Member.query({ id: $routeParams.id });
});

'use strict';

ords.controller('projectController', function ($scope, $routeParams, AuthService, Project, User, Member, growl) {

	//
	// This page doesm't make sense to view
	// without being logged in, so redirect
	// back to the home view
	//
	AuthService.check();

	$scope.editMember = function(id){
		growl.warning("Edit Member clicked");
	}

	$scope.removeMember = function(id){
		Member.delete({ id: $routeParams.id, roleid: id},
			function(){
				growl.success("Project member removed");
				$scope.members = Member.query({ id: $routeParams.id });
			},
			function(){
				growl.error("There was a problem removing this project member");
			}
		);

	}

	$scope.project = Project.get({ id: $routeParams.id });

	$scope.members = Member.query({ id: $routeParams.id });
});

Refresh project on deleting a member


import functionalText from "functional-text";

window.addEventListener("DOMContentLoaded", function () {
    let inputNode = document.querySelector("#input");
    let outputHtmlNode = document.querySelector("#output-html");

    let str = window.location.hash.length > 0 ?
        window.location.hash.substr(1) :
        `Sample Text for parsing`.trim();

    inputNode.value = str;
    inputNode.style.height = '200px';
    inputNode.style.width = '100%';

    let savedLog = console.log.bind(console);
    console.log = function (...args) {
        print(...args);
        savedLog(...args);
    };

    function print(...args) {
        outputHtmlNode.innerHTML += `<div>${args.join(" ")}</div>`;
    }

    function runParser() {
        outputHtmlNode.innerHTML = "";

        try {
            let parsedHtml = functionalText(inputNode.value);
            outputHtmlNode.style.borderColor = "black";
            print(parsedHtml.replace(/</g, "<").replace(/>/g, ">"));
        } catch (error) {
            outputHtmlNode.style.borderColor = "red";
            print(error);
        }
    }

    inputNode.addEventListener("input", runParser);
    runParser();
});

import functionalText from "functional-text";

window.addEventListener("DOMContentLoaded", function () {
    let inputNode = document.querySelector("#input");
    let outputHtmlNode = document.querySelector("#output-html");

    let str = window.location.hash.length > 0 ?
        window.location.hash.substr(1) :
        `Sample Text for parsing`.trim();

    inputNode.value = str;
    inputNode.style.height = '200px';
    inputNode.style.width = '100%';

    let savedLog = console.log.bind(console);
    console.log = function (...args) {
        print(...args);
        savedLog(...args);
    };

    function print(...args) {
        outputHtmlNode.innerHTML += `<div>${args.join(" ")}</div>`;
    }

    function runParser() {
        outputHtmlNode.innerHTML = "";

        try {
            let parsedHtml = functionalText(inputNode.value);
            outputHtmlNode.style.borderColor = "black";
            print(parsedHtml.replace(/</g, "<").replace(/>/g, ">"));
        } catch (error) {
            outputHtmlNode.style.borderColor = "red";
            print(error);
        }

        window.location.hash = `#${window.escape(inputNode.value)}`;
    }

    inputNode.addEventListener("input", runParser);
    runParser();
});

Set window hash to the input node's value for easy sharing.


define([
    'underscore',
    'views/proto/paged-collection',
    'views/event/box',
    'text!templates/event-collection.html'
], function (_, PagedCollection, EventBox, html) {
    return PagedCollection.extend({
        template: _.template(html),

        tag: 'div',

        listSelector: '.events-list',

        generateItem: function (model) {
            return new EventBox({ model: model });
        },

        events: {
            "click .filter": "filter"
        },

        filter: function () {
            var types=[];
            $('.filter:checked').each(function() {
                types.push($(this).val());
            });

            var filterString = types.join();

            this.options.types = types;

            this.collection.setTypes(types);

            /* Set queryString to URL */
            var url = '/feed';
            if ( filterString != undefined) {
                url += '?types=' + filterString;
            }

            Backbone.trigger('pp:navigate', url);

        },
        serialize: function() {
            var types = this.options.types || [];

            var filterList = [
                {
                    value: 'add-comment',
                    description: 'add-comment',
                },
                {
                    value: 'add-quest',
                    description: 'add-quest',
                }
            ]

            /* Add status for each filter */
            for( var i = 0; i < filterList.length ; i++ ){
                if( types.indexOf(filterList[i].value) != -1 ){
                    filterList[i].status = 'checked';
                }else{
                    filterList[i].status = '';
                }
            }

            return { filterList: filterList };
        }

    });
});

define([
    'underscore',
    'views/proto/paged-collection',
    'views/event/box',
    'text!templates/event-collection.html'
], function (_, PagedCollection, EventBox, html) {
    return PagedCollection.extend({
        template: _.template(html),

        tag: 'div',

        listSelector: '.events-list',

        generateItem: function (model) {
            return new EventBox({ model: model });
        },

        events: {
            "click .filter": "filter"
        },

        filter: function () {
            var types=[];
            $('.filter:checked').each(function() {
                types.push($(this).val());
            });

            var filterString = types.join();

            this.options.types = types;

            this.collection.setTypes(types);

            /* Set queryString to URL */
            var url = '/feed';
            if ( filterString != undefined) {
                url += '?types=' + filterString;
            }

            Backbone.trigger('pp:navigate', url, { replace: true });

        },
        serialize: function() {
            var types = this.options.types || [];

            var filterList = [
                {
                    value: 'add-comment',
                    description: 'add-comment',
                },
                {
                    value: 'add-quest',
                    description: 'add-quest',
                }
            ]

            /* Add status for each filter */
            for( var i = 0; i < filterList.length ; i++ ){
                if( types.indexOf(filterList[i].value) != -1 ){
                    filterList[i].status = 'checked';
                }else{
                    filterList[i].status = '';
                }
            }

            return { filterList: filterList };
        }

    });
});

Replace URL instead of pushing the new one.


var path = require('path')

module.exports = {
    entry: {
        app: './src/main.js',
        vendors: [
            'vue',
            'vue-resource',
            'vue-router',
        ],
    },
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/themes/{{ name }}/dist/',
        filename: '[name].js',
    },
    resolve: {
        extensions: ['', '.js', '.vue'],
        alias: {
            'src': path.resolve(__dirname, '../src'),
        }
    },
    resolveLoader: {
        root: path.join(__dirname, 'node_modules'),
    },
    module: {
        loaders: [
            {
                test: /\.vue$/,
                loader: 'vue',
            },
            {
                test: /\.js$/,
                loader: 'babel', // babel!eslint
                exclude: /node_modules/,
            },
            {
                test: /\.json$/,
                loader: 'json',
            },
            {
                test: /\.(png|jpg|gif|svg)$/,
                loader: 'url',
                query: {
                    limit: 10000,
                    name: '[name].[ext]?[hash:7]',
                },
            },
        ],
    },
    vue: {
        loaders: {
            js: 'babel', // babel!eslint
        },
    },
    // eslint: {
    //     formatter: require('eslint-friendly-formatter'),
    // },
};

var path = require('path')

module.exports = {
    entry: {
        app: './src/main.js',
        vendors: [
            'vue',
            'vue-resource',
            'vue-router',
        ],
    },
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/themes/{{ name }}/dist/',
        filename: '[name].js',
    },
    resolve: {
        extensions: ['', '.js', '.vue'],
        alias: {
            'src': path.resolve(__dirname, '../src'),
        }
    },
    resolveLoader: {
        root: path.join(__dirname, 'node_modules'),
    },
    sassLoader: {
        includePaths: [ path.resolve(__dirname, '../src/scss') ],
    },
    module: {
        loaders: [
            {
                test: /\.vue$/,
                loader: 'vue',
            },
            {
                test: /\.js$/,
                loader: 'babel', // babel!eslint
                exclude: /node_modules/,
            },
            {
                test: /\.json$/,
                loader: 'json',
            },
            {
                test: /\.(png|jpg|gif|svg)$/,
                loader: 'url',
                query: {
                    limit: 10000,
                    name: '[name].[ext]?[hash:7]',
                },
            },
        ],
    },
    vue: {
        loaders: {
            js: 'babel', // babel!eslint
        },
    },
    // eslint: {
    //     formatter: require('eslint-friendly-formatter'),
    // },
};

Resolve styles from the scss directory


#!/usr/bin/env node

// Sets the release date of the current release in the changelog.
// This is run automatically when npm version is run.

const fs = require('fs');
const cp = require('child_process');

const suffix = process.env.PRERELEASE_SUFFIX || 'rc';

const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');

// The changelog entry to be updated looks like this:
// ## Unreleased
// We need to add the version and release date in a YYYY-MM-DD format, so that it looks like this:
// ## 2.5.3 (2019-04-25)

const pkg = require('../../package.json');
const version = pkg.version.replace(new RegExp('-' + suffix + '\\..*'), '');

const unreleased = /^## Unreleased$/im;
const released = new RegExp(`^## ${version} \\([-\\d]*\\)$`, 'm');

if (released.test(changelog)) {
  process.exit(0);
}

if (!unreleased.test(changelog)) {
  console.error('Missing changelog entry');
  process.exit(1);
}

fs.writeFileSync('CHANGELOG.md', changelog.replace(
  unreleased,
  `## ${version} (${new Date().toISOString().split('T')[0]})`),
);

cp.execSync('git add CHANGELOG.md', { stdio: 'inherit' });

#!/usr/bin/env node

// Sets the release date of the current release in the changelog.
// This is run automatically when npm version is run.

const fs = require('fs');
const cp = require('child_process');

const suffix = process.env.PRERELEASE_SUFFIX || 'rc';

const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');

// The changelog entry to be updated looks like this:
// ## Unreleased
// We need to add the version and release date in a YYYY-MM-DD format, so that it looks like this:
// ## 2.5.3 (2019-04-25)

const pkg = require('../../package.json');
const version = pkg.version.replace(new RegExp('-' + suffix + '\\..*'), '');

const header = new RegExp(`^## (Unreleased|${version})$`, 'm');

if (!header.test(changelog)) {
  console.error('Missing changelog entry');
  process.exit(1);
}

const newHeader = pkg.version.indexOf(suffix) === -1
  ? `## ${version} (${new Date().toISOString().split('T')[0]})`
  : `## ${version}`;

fs.writeFileSync('CHANGELOG.md',
  changelog.replace(header, newHeader)
);

cp.execSync('git add CHANGELOG.md', { stdio: 'inherit' });

Fix changelog versioning to include date only after final release


const DrawCard = require('../../drawcard.js');

class WholenessOfTheWorld extends DrawCard {
    setupCardAbilities() {
        this.wouldInterrupt({
            title: 'Keep a claimed ring',
            when: {
                onReturnRing: (event, context) => event.ring.claimedBy === context.player.name
            },
            cannotBeMirrored: true,
            effect: 'prevent {1} from returning to the unclaimed pool',
            effectArgs: context => context.event.ring,
            handler: context => context.cancel()
        });
    }
}

WholenessOfTheWorld.id = 'wholeness-of-the-world';

module.exports = WholenessOfTheWorld;

const DrawCard = require('../../drawcard.js');

class WholenessOfTheWorld extends DrawCard {
    setupCardAbilities(ability) {
        this.wouldInterrupt({
            title: 'Keep a claimed ring',
            when: {
                onReturnRing: (event, context) => event.ring.claimedBy === context.player.name
            },
            cannotBeMirrored: true,
            effect: 'prevent {1} from returning to the unclaimed pool',
            effectArgs: context => context.event.ring,
            handler: context => context.cancel(),
            max: ability.limit.perRound(1)
        });
    }
}

WholenessOfTheWorld.id = 'wholeness-of-the-world';

module.exports = WholenessOfTheWorld;

Fix Wholeness of the World to be max 1 per round


import React from 'react'

import Placeholder from '../Placeholder'

import {decrypt} from '../../utils/crypto'

export default class EncryptedImage extends React.PureComponent {
  state = {
    src: '',
    status: 'fetching',
    error: null
  }

  componentDidMount() {
    this.fetchAndDecryptImage()
  }

  error(status, err) {
    this.setState({status: status, error: err})
    console.error(err)
  }

  fetchAndDecryptImage() {
    fetch(this.props.src)
      .then((response) => {
        if (!response.ok) throw response.status

        this.setState({status: 'decrypting'})

        response.arrayBuffer()
          .then((body) => {
            decrypt(body)
              .then((decrypted) => {
                this.setState({
                  src: `data:image/jpeg;base64,${btoa(decrypted)}`,
                  status: 'ready'
                })
              })
              .catch((err) => {
                this.error('error:decrypting', err)
              })
          })
      })
      .catch((err) => {
        this.error('error:fetching', err)
      })
  }

  render() {
    return this.state.status === 'ready' ? (
      <img
        src={this.state.src}
        title={this.props.title}
        alt={this.props.title}
      />
    ) : (
      <Placeholder
        status={this.state.status}
        title={this.props.title}
        error={this.state.error}
      />
    )
  }
}

import React from 'react'

import Placeholder from '../Placeholder'

import {decrypt} from '../../utils/crypto'

export default class EncryptedImage extends React.PureComponent {
  state = {
    src: '',
    status: 'fetching',
    error: null
  }

  componentDidMount() {
    this.fetchAndDecryptImage()
  }

  error(status, err) {
    this.setState({status: status, error: err})
    console.error(err)
  }

  fetchAndDecryptImage() {
    fetch(this.props.src)
      .then((response) => {
        if (!response.ok) throw response.status

        this.setState({status: 'decrypting'})

        response.arrayBuffer()
          .then((body) => {
            decrypt(body)
              .then((decrypted) => {
                this.setState({
                  src: `data:image/jpeg;base64,${btoa(decrypted)}`,
                  status: 'ready'
                })
              })
              .catch((err) => {
                this.error('error:decrypting', err)
              })
          })
      })
      .catch((err) => {
        this.error('error:fetching', err)
      })
  }

  render() {
    if (this.state.status === 'ready') {
      return (
        <img
          src={this.state.src}
          title={this.props.title}
          alt={this.props.title}
        />
      )
    } else {
      return (
        <Placeholder
          status={this.state.status}
          title={this.props.title}
          error={this.state.error}
        />
      )
    }
  }
}

Remove ternary to fix linting


'use strict';
angular.module('arethusa.table').directive('morphSelector',[
    'state',
    'morph',
    function (state, morph) {
        return {
            restrict: 'A',
            scope: {
                token: "=morphToken"
            },
            link: function(scope, element, attrs) {
                scope.analyses = morph.analyses[scope.token.id].forms;
                scope.form = scope.token.morphology;

                function undoFn(tkn, frm) {
                    var token = tkn;
                    var form = frm;
                    return function() {
                        scope.form = form;
                        state.change(token.id,'morphology',form)
                    }
                }

                function preExecFn(frm) {
                    var form = frm;
                    return function() {
                        scope.form = form;
                    }
                }

                scope.onChange = function() {
                    state.change(scope.token.id, 'morphology', scope.form, undoFn(scope.token,scope.token.morphology), preExecFn(scope.form));
                }

            },
            template: '<select class="no-margin compact" ng-model="form" ng-options="analysis as analysis.lemma+\':\'+analysis.postag for analysis in analyses" ng-change="onChange()"></select>'
        };
}]);

'use strict';
angular.module('arethusa.table').directive('morphSelector',[
    'state',
    'morph',
    function (state, morph) {
        return {
            restrict: 'A',
            scope: {
                token: "=morphToken"
            },
            link: function(scope, element, attrs) {
                scope.analyses = morph.analyses[scope.token.id].forms;
                scope.form = scope.token.morphology;

                scope.longForm = function(form) {
                    var longPostag = Object.keys(form.attributes).filter(function(k) {
                        return /[A-Za-z0-9]/.test(form.attributes[k]);
                    }).map(function(k) {
                        return morph.longAttributeValue(k,form.attributes[k]);
                    }).join('-');
                    var lemma = form.lemma;
                    return lemma + ':' + longPostag;
                }

                function undoFn(tkn, frm) {
                    var token = tkn;
                    var form = frm;
                    return function() {
                        scope.form = form;
                        state.change(token.id,'morphology',form)
                    }
                }

                function preExecFn(frm) {
                    var form = frm;
                    return function() {
                        scope.form = form;
                    }
                }

                scope.onChange = function() {
                    state.change(scope.token.id, 'morphology', scope.form, undoFn(scope.token,scope.token.morphology), preExecFn(scope.form));
                }

            },
            template: '<select class="no-margin compact" ng-model="form" ng-options="analysis as longForm(analysis) for analysis in analyses" ng-change="onChange()"></select>'
        };
}]);

Convert postag to long form in morph selector


define(['underscore'], function(_) {
   var constructor = function(data) {
      _.extend(this, data);
   };

   _.extend(constructor.prototype, {
      url: function() {
         return 'https://github.com/' + this.head.repo.owner.login + '/' + this.head.repo.name + '/pull/' + this.number;
      },

      update: function(data) {
         _.extend(this, data);
      },

      dev_blocked: function() {
         return this.status.dev_block.length > 0;
      },

      deploy_blocked: function() {
         return this.status.deploy_block.length > 0;
      },

      qa_done: function() {
         return this.status.QA.length >= this.status.qa_req;
      },

      cr_done: function() {
         return this.status.CR.length >= this.status.cr_req;
      },

      is_mine: function() {
         return this.user.login === App.user;
      },

      hasLabel: function(labelName) {
         return !this.labels.every(function(label) {
            return label.title !== labelName;
         });
      }
   });

   return constructor;
});

define(['underscore'], function(_) {
   var constructor = function(data) {
      _.extend(this, data);
   };

   _.extend(constructor.prototype, {
      url: function() {
         return 'https://github.com/' + this.head.repo.owner.login + '/' + this.head.repo.name + '/pull/' + this.number;
      },

      update: function(data) {
         _.extend(this, data);
      },

      dev_blocked: function() {
         return this.status.dev_block.length > 0;
      },

      deploy_blocked: function() {
         return this.status.deploy_block.length > 0;
      },

      qa_done: function() {
         return this.status.QA.length >= this.status.qa_req;
      },

      cr_done: function() {
         return this.status.CR.length >= this.status.cr_req;
      },

      is_mine: function() {
         return this.user.login === App.user;
      },

      hasLabel: function(labelName) {
         return _.some(this.labels, function(label) {
            return label.title === labelName;
         });
      }
   });

   return constructor;
});

Use Underscore function for hasLabel


// CI: Angular - Template Cache.js
// ---
// Create a template cache for AngularJS Projects

var gulp = require("gulp");
var gUtil = require("gulp-util");
var templateCache = require("gulp-angular-templatecache");

// Load the build configuration
var config = require("./../config/config.json");

gulp.task("angular:template-cache", function() {

    // Globbing patterns
    var patterns = [
        config.env.dev.serve.dir + "app/**/*.html"
    ];

    // TemplateCache options
    var options = {};

    if (config["template-cache"]) {
        options = {
            module: config["template-cache"].module
        };
    } else {
        gUtil.log("No angular-template-cache configuration was found. This might be an error, so we\'re letting you know ;-).");
    }

    var destination = config.env.dev.serve.dir + "app/";

    return gulp.src(patterns)
        .pipe(templateCache(options))
        .pipe(gulp.dest(destination));
});

// CI: Angular - Template Cache.js
// ---
// Create a template cache for AngularJS Projects

var gulp = require("gulp");
var gUtil = require("gulp-util");
var templateCache = require("gulp-angular-templatecache");

// Load the build configuration
var config = require("./../config/config.json");

gulp.task("angular:template-cache", function() {

    // Globbing patterns
    var patterns = [
        config.env.dev.scripts.dir + "app/**/*.html"
    ];

    // TemplateCache options
    var options = {};

    if (config["template-cache"]) {
        options = {
            filename: config["template-cache"].filename || "app.templates.js",
            module: config["template-cache"].module
        };
    } else {
        gUtil.log("No angular-template-cache configuration was found. This might be an error, so we\'re letting you know ;-).");
    }

    var destination = config.env.dev.scripts.dir + "app/";

    return gulp.src(patterns)
        .pipe(templateCache(options))
        .pipe(gulp.dest(destination));
});

Allow setting the templateCache exported filename


/**
 *
 *
 * @copyright 2017 Government of Canada
 * @license MIT
 * @author igboyes
 *
 */

import Request from "superagent";

export const accountAPI = {

    get: () => {
        return Request.get("/api/account");
    }

};

/**
 *
 *
 * @copyright 2017 Government of Canada
 * @license MIT
 * @author igboyes
 *
 */

import Request from "superagent";

export const accountAPI = {

    get: () => {
        return Request.get("/api/account");
    },

    logout: () => {
        return Request.get("/api/account/logout");
    }

};

Add logout API function in client


(function(undefined){
    var window = this || (0,eval)('this'),      // Use global 'this'
        document = window['document'],
        navigator = window['navigator'],
        jQuery = window["jQuery"],
        JSON = window["JSON"];

(function(undefined){
    // (0, eval)('this') is a robust way of getting a reference to the global object
    // For details, see http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023
    var window = this || (0, eval)('this'),
        document = window['document'],
        navigator = window['navigator'],
        jQuery = window["jQuery"],
        JSON = window["JSON"];

Add comment to explain (0, eval)('this')