import { A as emberA } from '@ember/array';
import groupBy from 'dummy/utils/group-by';
import { module, test } from 'qunit';

module('Unit | Utility | group by');

test('It returns an empty object if array is empty', function (assert) {
  let group = groupBy(emberA(), '');
  assert.equal(typeof group, 'object');
});

test('It returns a promise proxy', function (assert) {
  let group = groupBy(emberA(), '');
  return group.then(function () {
    assert.ok(group.get('isFulfilled'));
  });
});

import groupBy from 'dummy/utils/group-by';
import { module, setupTest, test } from 'qunit';

module('Unit | Utility | group by', function (hooks) {
  setupTest(hooks);

  test('It returns an empty object if array is empty', function (assert) {
    const group = groupBy([], '');
    assert.equal(typeof group, 'object');
  });

  test('It returns a promise proxy', async function (assert) {
    const group = groupBy([], '');
    const proxy = await group;
    assert.ok(proxy.get('isFulfilled'));
  });
});

Add setup hooks and use async/await


import { Posts } from 'meteor/example-forum';

/*
Let's assign a color to each post (why? cause we want to, that's why).
We'll do that by adding a custom field to the Posts collection.
Note that this requires our custom package to depend on vulcan:posts and vulcan:users.
*/

Posts.addField([
  {
    fieldName: 'soundcloud',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['members'],
    },
  },
  {
    fieldName: 'canBring',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['members'],
    },
  },
  {
    fieldName: 'minimumCharge',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['members'],
    },
  },
])

import { Posts } from 'meteor/example-forum';

/*
Let's assign a color to each post (why? cause we want to, that's why).
We'll do that by adding a custom field to the Posts collection.
Note that this requires our custom package to depend on vulcan:posts and vulcan:users.
*/

Posts.addField([
  {
    fieldName: 'soundcloud',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['guests'],
    },
  },
  {
    fieldName: 'canBring',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['guests'],
    },
  },
  {
    fieldName: 'minimumCharge',
    fieldSchema: {
      type: String,
      control: 'text',
      optional: true,
      insertableBy: ['admins'],
      editableBy: ['admins'],
      viewableBy: ['guests'],
    },
  },
])

Make custom fields viewable by guests


import {LOCATION_CHANGE} from 'react-router-redux';

let previousPathname = "";
// let previousAction = undefined;
// let counter = 0;

export default () => next =>  action => {
    if (action.type === LOCATION_CHANGE) {
        // counter++;
        // console.log("Route change:", counter);
        // console.log("Previous action", previousAction);
        // console.log("Previous path:", previousPathname);
        // console.log("Previous previous path:", previousAction ? previousAction.previousPathname : undefined);
        // console.log("New path:", action.payload.pathname);
        // if (action.payload.action === "REPLACE") {
        //     console.log("RREEEEEPPPLLLLAAAACCCEEEE!!!!");
        // }
        // console.log("-------");
        const newAction = {
            ...action,
            payload: {
                ...action.payload,
                // previousPathname: action.payload.action === "REPLACE" ? previousAction.previousPathname : previousPathname,
                previousPathname,
                // previousAction: previousAction ? {
                //     pathname: previousAction.pathname,
                //     action: previousAction.action,
                //     previousPathname: previousAction ? previousAction.previousPathname : ""
                // } : undefined
            }
        };
        // if (action.payload.action !== "REPLACE") {
        //     previousAction = {
        //         pathname: action.payload.pathname,
        //         action: action.payload.action,
        //         previousPathname: previousAction ? previousAction.pathname : ""
        //     };
        // }
        previousPathname = action.payload.pathname;
        return next(newAction);
    }
    return next(action);
}

import {LOCATION_CHANGE} from 'react-router-redux';

let previousPathname = "";

export default () => next =>  action => {
    if (action.type === LOCATION_CHANGE) {
        const newAction = {
            ...action,
            payload: {
                ...action.payload,
                previousPathname
            }
        };
        previousPathname = action.payload.pathname;
        return next(newAction);
    }
    return next(action);
}

Remove redundant comments from previous-path routing middleware


//@flow
module.exports =
  process.platform === "win32"
    ? "\\\\.\\pipe\\elm_test-" + process.pid
    : "/tmp/elm_test-" + process.pid + ".sock";

//@flow

// Incorporate the process PID into the socket name, so elm-test processes can
// be run parallel without accidentally sharing each others' sockets.
//
// See https://github.com/rtfeldman/node-test-runner/pull/231
module.exports =
  process.platform === "win32"
    ? "\\\\.\\pipe\\elm_test-" + process.pid
    : "/tmp/elm_test-" + process.pid + ".sock";

Add a comment explaining socket naming.


// Is Palindrome Challenge

/*Rules:
  Take any string as an argument
    Return true if palindrome
    return false if not
*/

/*PSEUDOCODE
  1) Split string up to individual characters, strip away punctuation
  2) Create new array with reversed array from original
  3) Compare the two, and return true or false
*/

function isPalindrome(str){
  var originalArr = str.split("");
  var reverseArr = str.split("").reverse();

  return originalArr.join() == reverseArr.join()
}

Write initial attempt at solution. Works, but not for every edge case


// Relevant entry from app/transistions.js
this.transition(
  this.between({childOf: '#liquid-bind-demo > div'}),
  this.use('toUp')
);

// Relevant entry from app/transitions.js
this.transition(
  this.between({childOf: '#liquid-bind-demo > div'}),
  this.use('toUp')
);

Fix typo transistions -> transitions


import NodeType from './node-type'

export default function isNodeSupported (node = {}) {
  return !(
    isTagNameIgnored(node.tagName) ||
    isNodeTypeIgnored(node.nodeType) ||
    isNodeValueIgnored(node.nodeValue)
  )
}

/**
 * Tag names that are not supported.
 * @type {Array}
 */
const IGNORED_TAG_NAMES = ['script']
/**
 * Checks if provided tag name is ignored (not supported).
 * @param {String} tagName
 * @return {Boolean}
 */
const isTagNameIgnored = (tagName = '') => (
  IGNORED_TAG_NAMES.indexOf(tagName.toLowerCase()) > -1
)

/**
 * Node types that are not supported.
 * @type {Array}
 */
const IGNORED_NODE_TYPES = [NodeType.COMMENT]
/**
 * Checks if provided node type is ignored (not supported).
 * @param {Number} nodeType
 * @return {Boolean}
 */
const isNodeTypeIgnored = (nodeType) => (
  IGNORED_NODE_TYPES.indexOf(nodeType) > -1
)

/**
 * Node values that are not supported.
 * @type {RegExp}
 */
const IGNORED_NODE_VALUES = /^\n/
/**
 * Checks if provided node value is ignored (not supported).
 * @param {String} nodeValue
 * @return {Boolean}
 */
const isNodeValueIgnored = (nodeValue) => (
  IGNORED_NODE_VALUES.test(nodeValue)
)

import NodeType from './node-type'

export default function isNodeSupported (node = {}) {
  return !(
    isTagNameIgnored(node.tagName) ||
    isNodeTypeIgnored(node.nodeType) ||
    isNodeValueIgnored(node.nodeValue)
  )
}

/**
 * Tag names that are not supported.
 * @type {Array}
 */
const IGNORED_TAG_NAMES = ['script']
/**
 * Checks if provided tag name is ignored (not supported).
 * @param {String} tagName
 * @return {Boolean}
 */
const isTagNameIgnored = (tagName = '') => (
  IGNORED_TAG_NAMES.indexOf(tagName.toLowerCase()) > -1
)

/**
 * Node types that are not supported.
 * @type {Array}
 */
const IGNORED_NODE_TYPES = [NodeType.COMMENT]
/**
 * Checks if provided node type is ignored (not supported).
 * @param {Number} nodeType
 * @return {Boolean}
 */
const isNodeTypeIgnored = (nodeType) => (
  IGNORED_NODE_TYPES.indexOf(nodeType) > -1
)

/**
 * Node values that are not supported.
 * @type {RegExp}
 */
const IGNORED_NODE_VALUES = /^\n/
/**
 * Checks if provided node value is ignored (not supported).
 * @param {String} nodeValue
 * @return {Boolean}
 */
const isNodeValueIgnored = (nodeValue) => (
  IGNORED_NODE_VALUES.test((nodeValue || '').trim())
)

Fix bug where text nodes beginning with newline would be ignored


const fs = require('fs');

const { learn, getClassifier, } = require('../src/learn');
const intentsFile = require('../src/intents.json').intents;

const natural = require('natural');

const intents = {
  hello: {
    tag: 'hello',
    patterns: [
      'hello!',
    ],
    responses: [
      'hi!',
    ],
  },
};

const classifierFile = __dirname + '/classifier.json';

test('learn: creates a classifier based on an intents object', () => {
  const classifier = learn(intents, classifierFile);

  expect(classifier).toBeInstanceOf(natural.BayesClassifier);
});

test('learn: it saves the classifier into a json file after learning', () => {
  natural.BayesClassifier.load(classifierFile, null, (err, classifier) => {
    expect(classifier).toBeInstanceOf(natural.BayesClassifier);
    expect(err).toEqual(null);
  });

  expect(true);
});

test('getClassifier: loads classifier from a file if it exists', () => {
  getClassifier(classifierFile)
    .then(c => expect(c).toBeInstanceOf(natural.BayesClassifier));
});

test('getClassifier: creates a new classifier from intents.json file if file doesn\'t exists', () => {
  getClassifier(__dirname + '/notafile.json')
    .then(c => {
      expect(c).toEqual(learn(intentsFile));
    });
});

afterAll(() => {
  fs.unlinkSync(classifierFile);
});

const fs = require('fs');

const { learn, getClassifier, } = require('../src/learn');
const intentsFile = require('../src/intents.json').intents;

const natural = require('natural');

const intents = {
  hello: {
    tag: 'hello',
    patterns: [
      'hello!',
    ],
    responses: [
      'hi!',
    ],
  },
};

const classifierFile = __dirname + '/classifier.json';

test('learn: creates a classifier based on an intents object', () => {
  const classifier = learn(intents, classifierFile);

  expect(classifier).toBeInstanceOf(natural.BayesClassifier);
});

test('learn: it saves the classifier into a json file after learning', () => {
  natural.BayesClassifier.load(classifierFile, null, (err, classifier) => {
    expect(classifier).toBeInstanceOf(natural.BayesClassifier);
    expect(err).toEqual(null);
  });
});

test('getClassifier: loads classifier from a file if it exists', (d) => {
  getClassifier(classifierFile)
    .then(c => {
      expect(c).toBeInstanceOf(natural.BayesClassifier);
      d();
    });
});

test('getClassifier: creates a new classifier from intents.json file if file doesn\'t exists', (d) => {
  getClassifier(__dirname + '/notafile.json')
    .then(c => {
      expect(c).toEqual(learn(intentsFile, __dirname + '/notafile.json'));
      d();
    });
});

afterAll(() => {
  fs.unlinkSync(classifierFile);
  fs.unlinkSync(__dirname + '/notafile.json');
});

Fix tests for in promises


module.exports = {
  "extends": ["vue"],
  "plugins": [
    "promise",
    "mocha",
    "chai-expect"
  ],
  "rules": {
    "mocha/no-exclusive-tests": "error"
  }
}

module.exports = {
  "env": {
    "es6": true
    "mocha": true
  },
  "extends": ["vue"],
  "plugins": [
    "promise",
    "mocha",
    "chai-expect"
  ],
  "rules": {
    "mocha/no-exclusive-tests": "error"
  }
}

Change enable mocha, es6 eslint env


const OFF = 0;
const ERROR = 2;

module.exports = {
  root: true,
  parser: "babel-eslint",
  plugins: [
    "node",
    "import",
    "react",
    "flowtype",
    "prettier",
  ],
  extends: [
    "eslint:recommended",
    "plugin:import/recommended",
    "plugin:react/recommended",
    "prettier",
    "prettier/flowtype"
  ],
  rules: {
    'prettier/prettier': ERROR,
    'no-console': OFF,
    'import/no-commonjs': ERROR
  },
  overrides: [
    {
      files: ["*.js", "server/**/*"],
      env: {
        node: true
      }
    },
    {
      files: [ "react-client/**/*"],
      env: {
        browser: true
      }
    },
    {
      files: [ "**/__tests__/**", "**.test.js"],
      env: {
        jest: true
      }
    }
  ]
}

const OFF = 0;
const ERROR = 2;

module.exports = {
  root: true,
  parser: "babel-eslint",
  plugins: [
    "node",
    "import",
    "react",
    "flowtype",
    "prettier",
  ],
  extends: [
    "eslint:recommended",
    "plugin:import/recommended",
    "plugin:react/recommended",
    "prettier",
    "prettier/flowtype"
  ],
  rules: {
    'prettier/prettier': ERROR,
    'no-console': OFF,
    'import/no-commonjs': ERROR
  },
  overrides: [
    {
      files: ["*.js", "server/**/*"],
      env: {
        node: true
      }
    },
    {
      files: [ "react-client/**/*"],
      env: {
        browser: true
      }
    },
    {
      files: [ "**/__tests__/**", "**.test.js"],
      env: {
        jest: true
      }
    },
    {
      files: ["webpack.config.js", "ecosystem.config.js"],
      rules: {
        "import/unambiguous": OFF,
        "import/no-commonjs": OFF
      }
    },
  ]
}

Fix lint rules for non module files


/*
 * Copyright (c) 2014-2021 Bjoern Kimminich.
 * SPDX-License-Identifier: MIT
 */

module.exports = {
  extends: 'standard-with-typescript',
  env: {
    browser: true,
    node: true,
    jasmine: true,
    mocha: true,
    protractor: true
  },
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly'
  },
  parserOptions: {
    ecmaVersion: 2018,
    project: './tsconfig.json'
  },
  ignorePatterns: [
    'app/private/**',
    'vagrant/**',
    'frontend/**',
    'dist/**'
  ]
}

/*
 * Copyright (c) 2014-2021 Bjoern Kimminich.
 * SPDX-License-Identifier: MIT
 */

module.exports = {
  extends: 'standard-with-typescript',
  env: {
    browser: true,
    node: true,
    jasmine: true,
    mocha: true,
    protractor: true
  },
  globals: {
    Atomics: 'readonly',
    SharedArrayBuffer: 'readonly'
  },
  parserOptions: {
    ecmaVersion: 2018,
    project: './tsconfig.json'
  },
  ignorePatterns: [
    'app/private/**',
    'vagrant/**',
    'frontend/**',
    'dist/**'
  ],
  overrides: [
    {
      files: ['**/*.ts'],
      parser: '@typescript-eslint/parser',
      rules: {
        '@typescript-eslint/explicit-function-return-type': 'warn',
        '@typescript-eslint/no-dynamic-delete': 'warn',
        '@typescript-eslint/no-floating-promises': 'warn',
        '@typescript-eslint/no-misused-promises': 'warn',
        '@typescript-eslint/no-this-alias': 'warn',
        '@typescript-eslint/no-var-requires': 'warn',
        '@typescript-eslint/prefer-includes': 'warn',
        '@typescript-eslint/prefer-nullish-coalescing': 'warn',
        '@typescript-eslint/prefer-optional-chain': 'warn',
        '@typescript-eslint/promise-function-async': 'warn',
        '@typescript-eslint/restrict-template-expressions': 'warn',
        '@typescript-eslint/restrict-plus-operands': 'warn',
        '@typescript-eslint/return-await': 'warn',
        '@typescript-eslint/strict-boolean-expressions': 'warn'
      }
    }
  ]
}

Change @typescript-eslint errors to warnings


/*jshint node:true */
"use strict";

var fs = require("fs"),
    util = require("util"),
    mp3Parser = require(__dirname + "/../mp3-parser"),
    pathToMp3 = process.argv[2],
    toArrayBuffer = function (buffer) {
        var bufferLength = buffer.length, i = 0,
            uint8Array = new Uint8Array(new ArrayBuffer(bufferLength));

        for (; i < bufferLength; ++i) { uint8Array[i] = buffer[i]; }
        return uint8Array.buffer;
    };

if (!pathToMp3) {
   console.log("please give a path to an mp3 file, i.e. 'node parse.js <file>'");
   process.exit(0);
 }

fs.readFile(pathToMp3, function (error, buffer) {
    if (error) {
        console.log("Oops: " + error);
        process.exit(1);
    }
    buffer = new DataView(toArrayBuffer(buffer));

    var lastFrame = mp3Parser.readLastFrame(buffer),
        tags = mp3Parser.readTags(buffer);

    util.puts("\nTags:\n-----\n" + util.inspect(tags, { depth: 3, colors: true }));
    util.puts("\nLast frame:\n-----------\n" + util.inspect(lastFrame, { depth: 3, colors: true }));
});

/*jshint node:true */
"use strict";

var fs = require("fs"),
    util = require("util"),
    mp3Parser = require(__dirname + "/../mp3-parser"),
    pathToMp3 = process.argv[2],
    toArrayBuffer = function (buffer) {
        var bufferLength = buffer.length, i = 0,
            uint8Array = new Uint8Array(new ArrayBuffer(bufferLength));

        for (; i < bufferLength; ++i) { uint8Array[i] = buffer[i]; }
        return uint8Array.buffer;
    };

if (!pathToMp3) {
   console.log("please give a path to an mp3 file, i.e. 'node parse.js <file>'");
   process.exit(0);
 }

fs.readFile(pathToMp3, function (error, buffer) {
    if (error) {
        console.log("Oops: " + error);
        process.exit(1);
    }
    buffer = new DataView(toArrayBuffer(buffer));

    var lastFrame = mp3Parser.readLastFrame(buffer),
        tags = mp3Parser.readTags(buffer);

    util.puts("\nTags:\n-----\n" + util.inspect(tags, { depth: 4, colors: true }));
    util.puts("\nLast frame:\n-----------\n" + util.inspect(lastFrame, { depth: 3, colors: true }));
});

Modify example to perform deeper JSON-stringifys


import sinon from 'sinon';

beforeEach(function() {
  this.sinon = sinon.sandbox.create();
});

afterEach(function() {
  this.sinon.restore();
});

import sinon from 'sinon';

beforeEach(function() {
  this.sinon = sinon.createSandbox();
});

afterEach(function() {
  this.sinon.restore();
});

Remove deprecated sinon sandbox call.


import React, {PureComponent, PropTypes} from 'react'
import {connect} from 'react-redux'
import {parse} from 'url'

import ButtonItem from './button-item'

function mapStateToProps(state) {
  return {
    title: state.song.playing.title,
    artist: state.song.playing.artist,
    files: state.song.playing.files
  }
}

function mapDispatchToProps(dispatch) {
  return {}
}

@connect(mapStateToProps, mapDispatchToProps)
export default class DownloadButton extends PureComponent {
  static propTypes = {
    title: PropTypes.string,
    artist: PropTypes.string,
    files: PropTypes.array
  }

  render() {
    const {title, artist, files} = this.props
    return (
      <ButtonItem icon='download' hidden={!files}>

Download Song

        {files && files.map(({urls, format, quality}) => (

              {format || 'unknown'}
              {quality && quality.description}:

            {urls.map((url, index) => (

 ·
                <a href={url} target='_blank'
                  type={`audio/${format}`}
                  download={`${artist} - ${title}`}
                  style={{
                    color: 'inherit',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                  }}>
                  {parse(url).host}

            ))}

        ))}
      </ButtonItem>
    )
  }
}

import React, {PureComponent, PropTypes} from 'react'
import {connect} from 'react-redux'
import {parse} from 'url'
import {uniq} from 'lodash'

import ButtonItem from './button-item'

function mapStateToProps(state) {
  return {
    title: state.song.playing.title,
    artist: state.song.playing.artist,
    files: state.song.playing.files
  }
}

function mapDispatchToProps(dispatch) {
  return {}
}

@connect(mapStateToProps, mapDispatchToProps)
export default class DownloadButton extends PureComponent {
  static propTypes = {
    title: PropTypes.string,
    artist: PropTypes.string,
    files: PropTypes.array
  }

  render() {
    const {title, artist, files} = this.props
    return (
      <ButtonItem icon='download' hidden={!files}>

Download Song

        {files && files.map(({urls, format, quality}) => (

              {format || 'unknown'}
              {quality && quality.description}:

            {uniq(urls).map((url, index) => (

 ·
                <a href={url} target='_blank'
                  type={`audio/${format}`}
                  download={`${artist} - ${title}`}
                  style={{
                    color: 'inherit',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                  }}>
                  {parse(url).host}

            ))}

        ))}
      </ButtonItem>
    )
  }
}

Fix duplicate download url, and fix unspecified key problem


import React from 'react';
import { render } from 'react-dom';
import { compose, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import persistState from 'redux-localstorage';
import Raven from "raven-js";
import createRavenMiddleware from "raven-for-redux";

import Themes from './components/Themes';
import reducer from './reducers';

const RAVEN_DSN = 'https://5672ee5945634745ad84d4a8470d08bc@sentry.io/153480';
Raven.config(RAVEN_DSN).install();

const enhancer = compose(
  persistState(undefined, {
    key: 'loginThemes'
  }),
  applyMiddleware(
      createRavenMiddleware(Raven),
  ),
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

let store = createStore(reducer, enhancer);

render(
  <Provider store={store}>
    <Themes />
  </Provider>,
  document.getElementById('themes')
);

import React from 'react';
import { render } from 'react-dom';
import { compose, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import persistState from 'redux-localstorage';
import Raven from "raven-js";
import createRavenMiddleware from "raven-for-redux";

import Themes from './components/Themes';
import reducer from './reducers';

const RAVEN_DSN = 'https://5672ee5945634745ad84d4a8470d08bc@sentry.io/153480';
Raven.config(RAVEN_DSN).install();

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const enhancer = composeEnhancers(
  persistState(undefined, {
    key: 'loginThemes'
  }),
  applyMiddleware(
      createRavenMiddleware(Raven),
  ),
);

let store = createStore(reducer, enhancer);

render(
  <Provider store={store}>
    <Themes />
  </Provider>,
  document.getElementById('themes')
);

Use correct method to load Redux dev tools middleware


const csv_stream = require('csv-stream');
const EOL = require('os').EOL;

// this CSV parser assumes that:
// - the first line contains column names
// - the delimiter is a comma

const options = {
  escapeChar : '"', // default is an empty string
  enclosedChar : '"', // default is an empty string
  endLine: EOL
};

module.exports.create = function create() {
  const csv_parse_stream = csv_stream.createStream(options);

  // override default encoding which is not set properly for Node.js 8
  // see https://github.com/lbdremy/node-csv-stream/issues/13
  csv_parse_stream._encoding = undefined;

  return csv_parse_stream;
};

const csv_stream = require('csv-stream');
const EOL = require('os').EOL;

// this CSV parser assumes that:
// - the first line contains column names
// - the delimiter is a comma

const options = {
  escapeChar : '"', // default is an empty string
  enclosedChar : '"', // default is an empty string
  endLine: EOL
};

module.exports.create = function create() {
  return csv_stream.createStream(options);
};

Remove monkey patch no longer needed with csv-stream-0.2.0


import React, { PropTypes } from 'react';
import {
	Text,
	View,
	Image
} from 'react-native';

import styles from './styles/Casts';
import { TMDB_IMG_URL } from '../../../constants/api';

const Casts = ({ info, getTabHeight }) => {
	let computedHeight = (80 + 15) * info.casts.cast.length; // (castImage.height + castContainer.marginBottom)
	computedHeight += 447 + 40; // Header height + container ((20 paddingVertical) = 40)

	return (
		<View style={styles.container} onLayout={getTabHeight.bind(this, 'casts', computedHeight)}>
			{
				info.casts.cast.map(item => (
					<View key={item.id} style={styles.castContainer}>
						<Image source={{ uri: `${TMDB_IMG_URL}/w185/${item.profile_path}` }} style={styles.castImage} />
						<View style={styles.characterContainer}>
							<Text style={styles.characterName}>
								{item.name}
							</Text>
							<Text style={styles.asCharacter}>
								{item.character && `as ${item.character}`}
							</Text>
						</View>
					</View>
				))
			}
		</View>
	);
};

Casts.propTypes = {
	info: PropTypes.object.isRequired,
	getTabHeight: PropTypes.func.isRequired
};

export default Casts;

import React, { PropTypes } from 'react';
import {
	Text,
	View,
	Image
} from 'react-native';

import styles from './styles/Casts';
import { TMDB_IMG_URL } from '../../../constants/api';

const Casts = ({ info, getTabHeight }) => {
	let computedHeight = (80 + 15) * info.casts.cast.length; // (castImage.height + castContainer.marginBottom)
	computedHeight += 447 + 40; // Header height + container ((20 paddingVertical) = 40)

	return (
		<View style={styles.container} onLayout={getTabHeight.bind(this, 'casts', computedHeight)}>
			{
				info.casts.cast.map(item => (
					<View key={item.cast_id} style={styles.castContainer}>
						<Image source={{ uri: `${TMDB_IMG_URL}/w185/${item.profile_path}` }} style={styles.castImage} />
						<View style={styles.characterContainer}>
							<Text style={styles.characterName}>
								{item.name}
							</Text>
							<Text style={styles.asCharacter}>
								{item.character && `as ${item.character}`}
							</Text>
						</View>
					</View>
				))
			}
		</View>
	);
};

Casts.propTypes = {
	info: PropTypes.object.isRequired,
	getTabHeight: PropTypes.func.isRequired
};

export default Casts;

Use cast_id instead of id.


// @flow
import { helpers } from 'utils';
import LayoutBehavior from './behaviors/LayoutBehavior';

const classes = {
    CLASS_NAME: 'layout__vertical-layout',
    ITEM: 'layout__vertical-layout-item',
    HIDDEN: 'layout__hidden'
};

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'rows');

        this.rows = options.rows;
    },

    tagName: 'div',

    template: false,

    className() {
        return `${classes.CLASS_NAME} ${this.options.class || ''}`;
    },

    behaviors: {
        LayoutBehavior: {
            behaviorClass: LayoutBehavior
        }
    },

    templateContext() {
        return {
            title: this.options.title
        };
    },

    onRender() {
        this.__rowsCtx = [];
        this.rows.forEach(view => {
            view.on('change:visible', (activeView, visible) => this.__handleChangeVisibility(activeView, visible));

            this.$el.append(view.render().$el);
            this.__rowsCtx.push({
                view
            });
        });
    },

    onAttach() {
        this.rows.forEach(view => {
            view._isAttached = true;
            view.triggerMethod('attach');
        });
    },

    update() {
        this.rows.forEach(view => {
            if (view.update) {
                view.update();
            }
        });
        this.__updateState();
    },

    __handleChangeVisibility(view, visible) {
        view.$el.toggleClass(classes.HIDDEN, !visible);
    },

    onDestroy() {
        this.rows.forEach(view => view.destroy());
    }
});

// @flow
import { helpers } from 'utils';
import LayoutBehavior from './behaviors/LayoutBehavior';

const classes = {
    CLASS_NAME: 'layout__vertical-layout',
    ITEM: 'layout__vertical-layout-item',
    HIDDEN: 'layout__hidden'
};

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'rows');

        this.rows = options.rows;
    },

    tagName: 'div',

    template: false,

    className() {
        return `${classes.CLASS_NAME} ${this.options.class || ''}`;
    },

    behaviors: {
        LayoutBehavior: {
            behaviorClass: LayoutBehavior
        }
    },

    templateContext() {
        return {
            title: this.options.title
        };
    },

    onRender() {
        this.__rowsCtx = [];
        this.rows.forEach(view => {
            view.on('change:visible', (activeView, visible) => this.__handleChangeVisibility(activeView, visible));

            this.$el.append(view.render().$el);
            this.__rowsCtx.push({
                view
            });
        });
        this.__updateState();
    },

    onAttach() {
        this.rows.forEach(view => {
            view._isAttached = true;
            view.triggerMethod('attach');
        });
    },

    update() {
        this.rows.forEach(view => {
            if (view.update) {
                view.update();
            }
        });
        this.__updateState();
    },

    __handleChangeVisibility(view, visible) {
        view.$el.toggleClass(classes.HIDDEN, !visible);
    },

    onDestroy() {
        this.rows.forEach(view => view.destroy());
    }
});

Fix vertical layout visibility update


import React, { PropTypes } from 'react'
import { connect } from 'react-redux'

import { incrementCounter, decrementCounter, incrementCounterAsync } from '../../actions/counter'

import {} from './counter.scss'

let Counter = ({ counter, onIncrementCounter, onDecrementCounter, onIncrementCounterAsync }) => (

{counter}

    <button onClick={onIncrementCounter} className="counter__button">+</button>
    <button onClick={onDecrementCounter} className="counter__button">-</button>
    <button onClick={onIncrementCounterAsync} className="counter__button counter__button--async">async</button>

)

Counter.propTypes = {
  counter: PropTypes.number.isRequired,
  onIncrementCounter: PropTypes.func.isRequired,
  onDecrementCounter: PropTypes.func.isRequired,
  onIncrementCounterAsync: PropTypes.func.isRequired
}

const mapStateToProps = (state) => {
  return {
    counter: state.counter
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onIncrementCounter () {
      dispatch(incrementCounter())
    },
    onDecrementCounter () {
      dispatch(decrementCounter())
    },
    onIncrementCounterAsync () {
      dispatch(incrementCounterAsync())
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

import React, { PropTypes } from 'react'
import { connect } from 'react-redux'

import { incrementCounter, decrementCounter, incrementCounterAsync } from '../../actions/counter'

import {} from './counter.scss'

class Counter {
  render() {
    return (

{this.props.counter}

        <button onClick={this.props.onIncrementCounter}
          className="counter__button">
          +
        </button>

        <button onClick={this.props.onDecrementCounter}
          className="counter__button">
          -
        </button>

        <button onClick={this.props.onIncrementCounterAsync}
          className="counter__button counter__button--async">
          async
        </button>

    )
  }
}

Counter.propTypes = {
  counter: PropTypes.number.isRequired,
  onIncrementCounter: PropTypes.func.isRequired,
  onDecrementCounter: PropTypes.func.isRequired,
  onIncrementCounterAsync: PropTypes.func.isRequired
}

const mapStateToProps = (state) => {
  return {
    counter: state.counter
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onIncrementCounter () {
      dispatch(incrementCounter())
    },
    onDecrementCounter () {
      dispatch(decrementCounter())
    },
    onIncrementCounterAsync () {
      dispatch(incrementCounterAsync())
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

Make containers use class syntax


import * as ActionTypes from '../action-types';
import * as ActionHelpers from '../action-helpers';

const {request, response, fail} = ActionHelpers;

export default function userViews(state = {}, action) {
  switch (action.type) {
    case request(ActionTypes.SUBSCRIBE):
    case request(ActionTypes.UNSUBSCRIBE): {
      const userId = action.payload.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isSubscribing: true}};
    }
    case response(ActionTypes.SUBSCRIBE):
    case response(ActionTypes.UNSUBSCRIBE):
    case fail(ActionTypes.SUBSCRIBE):
    case fail(ActionTypes.UNSUBSCRIBE): {
      const userId = action.request.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isSubscribing: false}};
    }

    case request(ActionTypes.BAN):
    case request(ActionTypes.UNBAN): {
      const userId = action.payload.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isBlocking: true}};
    }
    case response(ActionTypes.BAN):
    case response(ActionTypes.UNBAN):
    case fail(ActionTypes.BAN):
    case fail(ActionTypes.UNBAN): {
      const userId = action.request.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isBlocking: false}};
    }
  }

  return state;
}

import * as ActionTypes from '../action-types';
import * as ActionHelpers from '../action-helpers';

const {request, response, fail} = ActionHelpers;

export default function userViews(state = {}, action) {
  switch (action.type) {
    case request(ActionTypes.SUBSCRIBE):
    case request(ActionTypes.SEND_SUBSCRIPTION_REQUEST):
    case request(ActionTypes.UNSUBSCRIBE): {
      const userId = action.payload.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isSubscribing: true}};
    }
    case response(ActionTypes.SUBSCRIBE):
    case response(ActionTypes.SEND_SUBSCRIPTION_REQUEST):
    case response(ActionTypes.UNSUBSCRIBE):
    case fail(ActionTypes.SUBSCRIBE):
    case fail(ActionTypes.SEND_SUBSCRIPTION_REQUEST):
    case fail(ActionTypes.UNSUBSCRIBE): {
      const userId = action.request.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isSubscribing: false}};
    }

    case request(ActionTypes.BAN):
    case request(ActionTypes.UNBAN): {
      const userId = action.payload.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isBlocking: true}};
    }
    case response(ActionTypes.BAN):
    case response(ActionTypes.UNBAN):
    case fail(ActionTypes.BAN):
    case fail(ActionTypes.UNBAN): {
      const userId = action.request.id;
      const userView = state[userId];
      return {...state, [userId]: {...userView, isBlocking: false}};
    }
  }

  return state;
}

Add a throbber for "sending sub request" in UserCard


angular.module('codebrag.common.directives')

    .directive('reactionMessageSummary', function() {

        return {
            restrict: 'E',
            template: '<span>{{reactionMessage | truncate:50}}</span>',
            replace: true,
            scope: {
                reaction: '='
            },
            link: function(scope, el, attrs) {
                var reaction = scope.reaction;
                if(reaction.message) {
                    scope.reactionMessage = reaction.message;
                } else {
                    scope.reactionMessage = reaction.reactionAuthor + ' liked your code.';
                }
            }
        }
    });

angular.module('codebrag.common.directives')

    .directive('reactionMessageSummary', function($filter) {

        return {
            restrict: 'E',
            template: '',
            replace: true,
            scope: {
                reaction: '='
            },
            link: function(scope, el, attrs) {
                var reaction = scope.reaction;
                if(reaction.message) {
                    scope.reactionMessage = $filter('truncate')(reaction.message, 50);
                } else {
                    scope.reactionMessage = reaction.reactionAuthor + ' liked your code.';
                }
            }
        }
    });

Fix displaying quotes in followups list


// @flow

import * as React from 'react';
import { useDispatch } from 'react-redux';

import {
  backgroundActionType,
  foregroundActionType,
} from 'lib/reducers/foreground-reducer';

import { appBecameInactive } from '../redux/redux-setup';
import { addLifecycleListener } from './lifecycle';

const LifecycleHandler = React.memo<{||}>(() => {
  const dispatch = useDispatch();

  const lastStateRef = React.useRef();
  const onLifecycleChange = React.useCallback(
    (nextState: ?string) => {
      if (!nextState || nextState === 'unknown') {
        return;
      }
      const lastState = lastStateRef.current;
      lastStateRef.current = nextState;
      if (lastState === 'background' && nextState === 'active') {
        dispatch({ type: foregroundActionType, payload: null });
      } else if (lastState !== 'background' && nextState === 'background') {
        dispatch({ type: backgroundActionType, payload: null });
        appBecameInactive();
      }
    },
    [lastStateRef, dispatch],
  );

  React.useEffect(() => {
    const { remove } = addLifecycleListener(onLifecycleChange);
    return remove;
  }, [onLifecycleChange]);

  return null;
});
LifecycleHandler.displayName = 'LifecycleHandler';

export default LifecycleHandler;

// @flow

import * as React from 'react';
import { useDispatch } from 'react-redux';

import {
  backgroundActionType,
  foregroundActionType,
} from 'lib/reducers/foreground-reducer';

import { appBecameInactive } from '../redux/redux-setup';
import { addLifecycleListener } from './lifecycle';

const LifecycleHandler = React.memo<{||}>(() => {
  const dispatch = useDispatch();

  const lastStateRef = React.useRef();
  const onLifecycleChange = React.useCallback(
    (nextState: ?string) => {
      if (!nextState || nextState === 'unknown') {
        return;
      }
      const lastState = lastStateRef.current;
      lastStateRef.current = nextState;
      if (lastState === 'background' && nextState === 'active') {
        dispatch({ type: foregroundActionType, payload: null });
      } else if (lastState !== 'background' && nextState === 'background') {
        dispatch({ type: backgroundActionType, payload: null });
        appBecameInactive();
      }
    },
    [lastStateRef, dispatch],
  );

  React.useEffect(() => {
    const subscription = addLifecycleListener(onLifecycleChange);
    return () => subscription.remove();
  }, [onLifecycleChange]);

  return null;
});
LifecycleHandler.displayName = 'LifecycleHandler';

export default LifecycleHandler;

Fix bug where Lifecycle listener subscription wasn't being torn down properly


'use strict';

const Command = require('../../Command.js');
const FissureEmbed = require('../../embeds/FissureEmbed.js');

/**
 * Displays the currently active Invasions
 */
class Fissures extends Command {
  /**
   * Constructs a callable command
   * @param {Genesis} bot  The bot object
   */
  constructor(bot) {
    super(bot, 'warframe.worldstate.fissures', 'fissures', 'Get the current list of Void Fissure Missions');
  }

  /**
   * Run the command
   * @param {Message} message Message with a command to handle, reply to,
   *                          or perform an action based on parameters.
   */
  run(message) {
    this.bot.settings.getChannelPlatform(message.channel)
      .then(platform => this.bot.worldStates[platform].getData())
      .then((ws) => {
        const fissures = ws.fissures;
        this.messageManager.embed(message,
          new FissureEmbed(this.bot, fissures), true, false);
      })
      .catch(this.logger.error);
  }
}

module.exports = Fissures;

'use strict';

const Command = require('../../Command.js');
const FissureEmbed = require('../../embeds/FissureEmbed.js');

/**
 * Displays the currently active Invasions
 */
class Fissures extends Command {
  /**
   * Constructs a callable command
   * @param {Genesis} bot  The bot object
   */
  constructor(bot) {
    super(bot, 'warframe.worldstate.fissures', 'fissures', 'Get the current list of Void Fissure Missions');
  }

  /**
   * Run the command
   * @param {Message} message Message with a command to handle, reply to,
   *                          or perform an action based on parameters.
   */
  run(message) {
    this.bot.settings.getChannelPlatform(message.channel)
      .then(platform => this.bot.worldStates[platform].getData())
      .then((ws) => {
        const fissures = ws.fissures.sort((a, b) => { return a.tierNum > b.TierNum; });
        this.messageManager.embed(message,
          new FissureEmbed(this.bot, fissures), true, false);
      })
      .catch(this.logger.error);
  }
}

module.exports = Fissures;

Add sorting by fissure tier


const Core = require('./core');

class Discord extends Core {
  constructor() {
    super();
    this.dnsResolver = { resolve: function(address) {return {address: address} } };
  }

  async run(state) {
    this.usedTcp = true;
    const raw = await this.request({
      uri: 'https://discordapp.com/api/guilds/' + this.options.address + '/widget.json',
    });
    const json = JSON.parse(raw);
    state.name = json.name;
    if (json.instant_invite) {
      state.connect = json.instant_invite;
    } else {
      state.connect = 'https://discordapp.com/channels/' + this.options.address
    }
    state.players = json.members.map(v => {
      return {
        name: v.username + '#' + v.discriminator,
        username: v.username,
        discriminator: v.discriminator,
        team: v.status
      }
    });
    state.raw = json;
  }
}

module.exports = Discord;

const Core = require('./core');

class Discord extends Core {
  constructor() {
    super();
    this.dnsResolver = { resolve: function(address) {return {address: address} } };
  }

  async run(state) {
    this.usedTcp = true;
    const raw = await this.request({
      uri: 'https://discordapp.com/api/guilds/' + this.options.address + '/widget.json',
    });
    const json = JSON.parse(raw);
    state.name = json.name;
    if (json.instant_invite) {
      state.connect = json.instant_invite;
    } else {
      state.connect = 'https://discordapp.com/channels/' + this.options.address
    }
    state.players = json.members.map(v => {
      return {
        name: v.username + '#' + v.discriminator,
        username: v.username,
        discriminator: v.discriminator,
        team: v.status
      }
    });
    state.maxplayers = json.presence_count;
    state.raw = json;
  }
}

module.exports = Discord;

Set max players to total amount of members in server


import Ember from 'ember';
import md5 from 'md5';

export default Ember.Component.extend({
  tagName: 'img',
  attributeBindings: ['src', 'alt', 'title'],
  classNames: ['gravatar-image'],
  size: 250,
  email: '',
  title: '',
  default: '',
  secure: false,

  src: Ember.computed('email', 'size', 'default', function() {
    var email = this.get('email'),
        size = this.get('size'),
        def = this.get('default'),
        secure = this.get("secure");

    return (secure? "https" : "http") + '://www.gravatar.com/avatar/' + md5(email) + '?s=' + size + '&d=' + def;
  })
});

import Ember from 'ember';
import md5 from 'md5';

export default Ember.Component.extend({
  tagName: 'img',
  attributeBindings: ['src', 'alt', 'title'],
  classNames: ['gravatar-image'],
  size: 250,
  email: '',
  title: '',
  default: '',
  secure: null,

  src: Ember.computed('email', 'size', 'default', function() {
    var email = this.get('email'),
        size = this.get('size'),
        def = this.get('default'),
        secure = this.get("secure"),
        protocol;
    if(secure===null){
      protocol = ''
    } else if (secure===false){
      protocol = 'http:'
    } else{
      protocol = 'https:'
    }
    return protocol + '//www.gravatar.com/avatar/' + md5(email) + '?s=' + size + '&d=' + def;
  })
});

Allow protocols to be null.


export default assignment => ([
  {
    title: 'Read the article',
    content: 'Read the article and complete the Peer Review Exercise. This will help prepare you for providing feedback.',
    status: 'not_yet_started',
    trainings: [
      {
        title: 'Peer Review Exercise',
        path: '../../../training/students/peer-review',
        external: true
      }
    ]
  },
  {
    title: 'Provide feedback',
    content: 'Assess your classmate\'s proposed contributions.',
    status: 'peer_review_started',
    trainings: [
      {
        title: 'Bibliography',
        path: `${assignment.sandboxUrl}/Bibliograpy`,
        external: true
      },
      {
        title: 'Sandbox',
        path: assignment.sandboxUrl,
        external: true
      },
      {
        title: 'Peer Review',
        path: `${assignment.sandboxUrl}/${assignment.username}_Peer_Review`,
        external: true
      }
    ]
  },
  {
    title: 'Mark as complete',
    content: 'Let your classmate know that you\'ve reviewed their work by posting on their talk page so they can begin to incorporate your feedback into their draft.',
    status: 'peer_review_completed',
    trainings: [
      {
        title: 'Sandbox',
        path: assignment.sandboxUrl,
        external: true
      }
    ]
  }
]);

export default assignment => ([
  {
    title: 'Read the article',
    content: 'Read the article and complete the Peer Review Exercise. This will help prepare you for providing feedback.',
    status: 'not_yet_started',
    trainings: [
      {
        title: 'Peer Review Exercise',
        path: '../../../training/students/peer-review',
        external: true
      }
    ]
  },
  {
    title: 'Provide feedback',
    content: 'Assess your classmate\'s proposed contributions.',
    status: 'peer_review_started',
    trainings: [
      {
        title: 'Bibliography',
        path: `${assignment.sandboxUrl}/Bibliograpy`,
        external: true
      },
      {
        title: 'Sandbox',
        path: assignment.sandboxUrl,
        external: true
      },
      {
        title: 'Peer Review',
        path: `${assignment.sandboxUrl}/${assignment.username}_Peer_Review?veaction=edit&preload=Template:Dashboard.wikiedu.org_peer_review`,
        external: true
      }
    ]
  },
  {
    title: 'Mark as complete',
    content: 'Let your classmate know that you\'ve reviewed their work by posting on their talk page so they can begin to incorporate your feedback into their draft.',
    status: 'peer_review_completed',
    trainings: [
      {
        title: 'Sandbox',
        path: assignment.sandboxUrl,
        external: true
      }
    ]
  }
]);

Add visual editor template to Peer Review on Provide Feedback step


/*
 * Copyright 2015 the Mimu Authors (Dan Bornstein et alia).
 * Licensed AS IS and WITHOUT WARRANTY under the Apache License,
 * Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0>
 */

"use strict";

// Utilities for interacting with styles. Instances are constructed with
// a node from which to get the "original" CSS style. From there, you can
// modify properties of the
// instance and then extract the modified properties, included computed
// properties.
class StyleUtil {
    // Clones the computed style of the given node. The result can be modified
    // freely without affecting the original.
    static cloneComputedStyle(orig) {
        // We make a fresh element, apply the original's style to it by
        // using the text representation (which seems to be the simplest way
        // to achieve that), and then return the style object.
        var doc = orig.ownerDocument;
        var node = doc.createElement("span");

        node.style.cssText = doc.defaultView.getComputedStyle(orig).cssText;
        return node.style;
    }
}

/*
 * Copyright 2015 the Mimu Authors (Dan Bornstein et alia).
 * Licensed AS IS and WITHOUT WARRANTY under the Apache License,
 * Version 2.0. Details: <http://www.apache.org/licenses/LICENSE-2.0>
 */

"use strict";

// Utilities for interacting with styles. Instances are constructed with
// a node from which to get the "original" CSS style. From there, you can
// modify properties of the instance and then extract the modified properties,
// including computed properties.
class StyleUtil {
    // Clones the computed style of the given node. The result can be modified
    // freely without affecting the original.
    static cloneComputedStyle(orig) {
        // We make a fresh element, apply the original's style to it by
        // using the text representation (which seems to be the simplest way
        // to achieve that), and then return the style object.
        var doc = orig.ownerDocument;
        var node = doc.createElement("span");

        node.style.cssText = doc.defaultView.getComputedStyle(orig).cssText;
        return node.style;
    }
}

Clean up and fix grammar.


'use strict';

/* Controllers */

angular.module('myApp.controllers', [])
  .controller('HomeController', ['$scope', function($scope) {
    $scope.allMusic = [];
    $scope.albums = [];
    $scope.artists = [];
    $scope.songs = [];

    $scope.metadata = {song: '', artist: '', album: ''};

    $scope.saveMusic = function() {
      $scope.allMusic.push($scope.metadata);
      $scope.albums.push($scope.metadata.album);
      $scope.artists.push($scope.metadata.artist);
      $scope.songs.push($scope.metadata.song);

      // Clear out data after it has been saved!
      $scope.metadata = {song: '', artist: '', album: ''};
    };
  }])
  .controller('AlbumsController', ['$scope', function($scope) {
  }])
  .controller('ArtistsController', ['$scope', function($scope) {
  }])
  .controller('SongsController', ['$scope', function($scope) {
  }])
  .controller('FavoritesController', ['$scope', function($scope) {
  }]);

'use strict';

/* Controllers */

angular.module('myApp.controllers', [])
  .controller('HomeController', ['$scope', function($scope) {
    $scope.allMusic = [];
    $scope.albums = [];
    $scope.artists = [];
    $scope.songs = [];

    $scope.metadata = {song: '', artist: '', album: ''};

    $scope.saveMusic = function() {
      $scope.allMusic.push($scope.metadata);
      $scope.albums.push({title: $scope.metadata.album, favorite: false});
      $scope.artists.push({name: $scope.metadata.artist, favorite: false});
      $scope.songs.push({title: $scope.metadata.song, favorite: false});

      // Clear out data after it has been saved!
      $scope.metadata = {song: '', artist: '', album: ''};
    };
  }])
  .controller('AlbumsController', ['$scope', function($scope) {
  }])
  .controller('ArtistsController', ['$scope', function($scope) {
  }])
  .controller('SongsController', ['$scope', function($scope) {
  }])
  .controller('FavoritesController', ['$scope', function($scope) {
  }]);

Store songs, artists, and albums as objects


//@flow

import styled, { css } from "styled-components";

export const Ul = styled.ul`
  ${({ theme }: { theme: Theme }) => css`

    padding: 0 0 0 ${theme.scale.s8()};
    display: flex;
    justify-content: flex-start;
    align-items: flex-end;
    list-style: none;
    margin: 0;

  `}
`;

export const Li = styled.li`
  ${({ theme }: { theme: Theme }) => {
    const textColor = theme.color.black;

    return css`

    ${theme.type.paragraph};
    color: ${textColor};
    font-size: ${theme.scale.s2(-1)};
    line-height: ${theme.scale.s8()};
    position: relative;
    padding: 0 1em;
    transition: color 0.2s;
    z-index: 1;

    &:hover{
      cursor: pointer;
      color: ${theme.effect.darken(0.2, textColor)};
    }

  `;
  }}
`;

//@flow

import styled, { css } from "styled-components";

export const Ul = styled.ul`
  ${({ theme }: { theme: Theme }) => css`

    padding: 0 0 0 ${theme.scale.s8()};
    display: flex;
    justify-content: flex-start;
    align-items: flex-end;
    list-style: none;
    margin: 0;

  `}
`;

export const Li = styled.li`
  ${({ theme }: { theme: Theme }) => {
    const textColor = theme.color.black;

    return css`
    ${theme.type.paragraph};
    font-size: ${theme.scale.s2(-1)};
    line-height: ${theme.scale.s8()};
    position: relative;
    padding: 0 1em;
    transition: color 0.2s;
    z-index: 1;
  `;
  }}
`;

Remove css hover from nav items - moved to Highlight comp


import Ember from 'ember';
import config from '../config/environment';

export default Ember.Controller.extend({

  cordova: Ember.inject.service(),
  subscriptions: Ember.inject.controller(),

  initSubscriptions: Ember.on('init', function() {
    if (this.session.get("isLoggedIn")) {
      this.send('setSubscriptions');
    }
  }),

  supportGCLink: Ember.computed('session.language', function(){
    return this.get('session.language') === 'zh-tw' ? "https://www.goodcity.hk/get_involved_zh.html" : "https://www.goodcity.hk/get_involved.html";
  }),

  appVersion: Ember.computed(function(){
    return config.cordova.enabled ? config.APP.VERSION : null;
  }),

  actions: {
    logMeOut() {
      this.session.clear(); // this should be first since it updates isLoggedIn status
      this.get('subscriptions').send('unwire');
      this.get('subscriptions').send('unloadNotifications');
      this.store.unloadAll();
      var _this = this;
      config.APP.PRELOAD_TYPES.forEach(function(type) {
        _this.store.findAll(type);
      });
      this.transitionToRoute('login');
    },

    logMeIn() {
      this.send('setSubscriptions');
    },

    setSubscriptions() {
      this.get('subscriptions').send('wire');
    }
  }
});

import Ember from 'ember';
import config from '../config/environment';

export default Ember.Controller.extend({

  cordova: Ember.inject.service(),
  subscriptions: Ember.inject.controller(),
  config,

  initSubscriptions: Ember.on('init', function() {
    if (this.session.get("isLoggedIn")) {
      this.send('setSubscriptions');
    }
  }),

  supportGCLink: Ember.computed('session.language', function(){
    return this.get('session.language') === 'zh-tw' ? "https://www.goodcity.hk/get_involved_zh.html" : "https://www.goodcity.hk/get_involved.html";
  }),

  appVersion: Ember.computed(function(){
    return config.cordova.enabled ? config.APP.VERSION : null;
  }),

  actions: {
    logMeOut() {
      this.session.clear(); // this should be first since it updates isLoggedIn status
      this.get('subscriptions').send('unwire');
      this.get('subscriptions').send('unloadNotifications');
      this.store.unloadAll();
      var _this = this;
      config.APP.PRELOAD_TYPES.forEach(function(type) {
        _this.store.findAll(type);
      });
      this.transitionToRoute('login');
    },

    logMeIn() {
      this.send('setSubscriptions');
    },

    setSubscriptions() {
      this.get('subscriptions').send('wire');
    }
  }
});

Fix rate itunes not being hidden on production build (to be enabled later)


"use strict";

var fs = require('fs'),
    path = require('path');

exports.up = function(migrationName) {
};

exports.down = function(migrationName) {
};

exports.create = function(migrationName) {
	var migrationTemplate = [
		'"use strict"',
		'',
		'exports.up = function(mongoose, next) {',
		'    next();',
		'}',
		'',
		'exports.down = function(mongoose, next) {',
		'    next();',
		'}',
		''
	].join('\n');

};

"use strict";

var fs = require('fs'),
    path = require('path');

var baseDirectory = path.join(__dirname, '..', '..');
var relativeMigrationDirectory = path.join('db', 'migrations');
var absoluteMigrationDirectory = path.join(baseDirectory, relativeMigrationDirectory);

function padNumeral(numeral) {
    return Array(7 - numeral.toString().length).join('0') + numeral;
};

function makeNameFilename(migrationName) {
	return migrationName.replace(/\s+/g, '-');
};

exports.up = function(migrationName) {
};

exports.down = function(migrationName) {
};

exports.create = function(migrationName) {
    var migrationTemplate = [
        '"use strict"',
        '',
        'exports.up = function(mongoose, next) {',
        '    next();',
        '}',
        '',
        'exports.down = function(mongoose, next) {',
        '    next();',
        '}',
        ''
    ].join('\n');

    try {
        fs.mkdirSync(absoluteMigrationDirectory, parseInt('0775', 8));
    } catch (err) {
        // ignore error creating directory
    }

	var existingMigrationOrdinals = fs.readdirSync(absoluteMigrationDirectory).map(function(filename) {
        return parseInt(filename.match(/^(\d+)/)[1], 10);
    }).sort(function(a, b) {
        return a - b;
	});

	var nextOrdinal = (existingMigrationOrdinals.pop() || 0) + 1;
    var fileName = padNumeral(nextOrdinal) + "-" + makeNameFilename(migrationName) + ".js";
	var absoluteFileName = path.join(absoluteMigrationDirectory, fileName);
	fs.writeFileSync(absoluteFileName, migrationTemplate, {mode: parseInt('0664', 8)});
	console.log("New migration created: " + path.join(relativeMigrationDirectory, fileName));
};

Add migration creation logic to MM.create()


/* jslint node: true */
'use strict';

var _ = require( 'lodash' );
var Q = require( 'q' );
var dust = require( './dustjsExtended' );
var Joi = require( 'joi' );

function Templater( options ) {

  var self = this;

  // Maybe merge in some defaults here?
  self.settings = _.merge( {
    namespace: ''
  }, options );

  var schema = Joi.object().keys( {
    namespace: Joi.any().optional()
  } );

  Joi.validate( self.settings, schema, function( err ) {
    if ( err ) {
      throw new Error( err );
    }
  } );

  self.compile = function( options2 ) {
    var deferred = Q.defer();
    try {
      var compiled = dust.compile( options2.content, self.settings.namespace + options2.identifier );
      dust.loadSource( compiled );
      deferred.resolve();
    } catch ( e ) {
      deferred.reject( e.message );
    }
    return deferred.promise;
  };

  self.render = function( options2 ) {
    var deferred = Q.defer();

    // Passing the template namespace to the template variable scope
    options2.data.templateNamespace = self.settings.namespace;

    // Render the template
    dust.render( self.settings.namespace + options2.template, options2.data, function( err, out ) {
      if ( err ) {
        deferred.reject( err );
      } else {
        deferred.resolve( out );
      }
    } );
    return deferred.promise;
  };

}

module.exports = exports = Templater;

/* jslint node: true */
'use strict';

var _ = require( 'lodash' );
var Q = require( 'q' );
var dust = require( './dustjsExtended' );
var Joi = require( 'joi' );

function Templater( options ) {

  var self = this;

  // Maybe merge in some defaults here?
  self.settings = _.merge( {
    namespace: ''
  }, options );

  var schema = Joi.object().keys( {
    namespace: Joi.any().optional()
  } );

  Joi.validate( self.settings, schema, function( error ) {
    if ( error ) {
      throw new Error( error );
    }
  } );

  self.compile = function( compileOptions ) {
    var deferred = Q.defer();
    try {
      var compiled = dust.compile( compileOptions.content, self.settings.namespace + compileOptions.identifier );
      dust.loadSource( compiled );
      deferred.resolve();
    } catch ( error ) {
      deferred.reject( error.message );
    }
    return deferred.promise;
  };

  self.render = function( renderOptions ) {
    var deferred = Q.defer();

    // Passing the template namespace to the template variable scope
    renderOptions.data.templateNamespace = self.settings.namespace;

    // Render the template
    dust.render( self.settings.namespace + renderOptions.template, renderOptions.data, function( error, out ) {
      if ( error ) {
        deferred.reject( error );
      } else {
        deferred.resolve( out );
      }
    } );
    return deferred.promise;
  };

}
module.exports = exports = Templater;

Update options to be option that makes sense


import { call } from 'redux-saga/effects'
import { Actions as NavigationActions, ActionConst } from 'react-native-router-flux'
import { merge } from 'ramda'

// Process OPEN_SCREEN actions, intended for use with Reactotron to open a screen via a direct dispatch
export function * openScreen (action) {
  const {screen, options} = action
  // Always reset the nav stack when opening a screen via Reactotron
  // You can override the RESET type in the options passed to the OPEN_SCREEN dispatch
  const mergedOptions = merge({type: ActionConst.RESET}, options)
  yield call(NavigationActions[screen], mergedOptions)
}

import { call } from 'redux-saga/effects'
import { Actions as NavigationActions, ActionConst } from 'react-native-router-flux'
import { merge } from 'ramda'

// Process OPEN_SCREEN actions
export function * openScreen (action) {
  const {screen, options} = action
  // Always reset the nav stack when opening a screen by default
  // You can override the RESET type in the options passed to the OPEN_SCREEN dispatch
  const mergedOptions = merge({type: ActionConst.RESET}, options)
  yield call(NavigationActions[screen], mergedOptions)
}

Remove references to Reactotron in the comments


$(document).ready(function() {
  $('a.menu').click(function() {
    $('.site-header nav').slideToggle(100);
    return false;
  });

  $(window).resize(function(){
    var w = $(window).width();
    var menu = $('.site-header nav');
    if(w > 680 && menu.is(':hidden')) {
      menu.removeAttr('style');
    }
  });

  $('article.post iframe').wrap('<div class="video-container" />');

});

$(document).ready(function() {
    var vpH = $(window).height();
    var vH = vpH - 350;
    $('.overlay').css("height", vH);
    $('.featured-image').css("height", vH);
});

$(function(){
  $('<img>').attr('src',function(){
      var imgUrl = $('div.featured-image').css('background-image');
      if (!imgUrl) {
        return;
      }
      imgUrl = imgUrl.substring(4, imgUrl.length-1);
      return imgUrl;
  }).load(function(){
    $('img.loading').fadeOut(500);
    $('div.overlay').fadeTo("slow", 0.6);
  });
});

$(function(){
    $('.post-list li').each(function(i){
        var t = $(this);
        setTimeout(function(){ t.addClass('slider'); }, (i+1) * 330);
    });
});

$(document).ready(function() {
  $('a.menu').click(function() {
    $('.site-header nav').slideToggle(100);
    return false;
  });

  $(window).resize(function() {
    var w = $(window).width();
    var menu = $('.site-header nav');
    if (w > 680 && menu.is(':hidden')) {
      menu.removeAttr('style');
    }
  });

  $('article.post iframe').wrap('<div class="video-container" />');

});

$(document).ready(function() {
  var vpH = $(window).height();
  var vH = vpH - 350;
  $('.overlay').css("height", vH);
  $('.featured-image').css("height", vH);
});

$(function() {
  $('<img>').attr('src', function() {
    var imgUrl = $('div.featured-image').css('background-image');
    if (!imgUrl) {
      return;
    }
    imgUrl = imgUrl.substring(4, imgUrl.length - 1).replace(/\"/g, '');
    return imgUrl;
  }).load(function() {
    $('img.loading').fadeOut(500);
    $('div.overlay').fadeTo("slow", 0.6);
  });
});

$(function() {
  $('.post-list li').each(function(i) {
    var t = $(this);
    setTimeout(function() { t.addClass('slider'); }, (i + 1) * 330);
  });
});

Fix this janky script and autoformat.


import createRouter from 'router5';
import loggerPlugin from 'router5/plugins/logger';
import listenersPlugin from 'router5/plugins/listeners';
import browserPlugin from 'router5/plugins/browser';
import routes from './routes';

export default function configureRouter(useListenersPlugin = false) {
    const router = createRouter(routes, {
            defaultRoute: 'inbox'
        })
        // Plugins
        .usePlugin(loggerPlugin)
        .usePlugin(browserPlugin({
            useHash: true
        }));

    if (useListenersPlugin) {
        router.usePlugin(listenersPlugin());
    }

    return router;
}

export default configureRouter;

Remove duplicated `export default` statement


//Load common code that includes config
require(['./require.config'], function (common) {
  require([
    'jquery',
    'bootstrap',
    'knockout',
    'viewmodel'
    ], function($, _bootstrap, ko, HelloViewModel){
      'use strict';
      // this is where all the site code should begin

      $(function() {
        $('.lead').html('Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.');
      });

      ko.applyBindings(new HelloViewModel("Planet", "Earth")); // This makes Knockout get to work

  });
});

// load requirejs config file
require(['./require.config'], function (common) {

  // test jquery by appending HTML content
  require([
    'jquery',
    ], function($){
      'use strict';
      // this is where the code goes
      $(function() {
        $('.lead').html('Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.');
      });
  });

  // test knockout by binding hello world view model
  require([
    'knockout',
    'viewmodel'
    ], function(ko, HelloViewModel){
      'use strict';
      // this is where the code goes
      ko.applyBindings(new HelloViewModel("Planet", "Earth")); // This makes Knockout get to work
  });

  // add bootstrap javascript
  require([
    'jquery',
    'bootstrap'
    ], function($, bootstrap){
      'use strict';
      // any JavaScript that requires bootstrap should go here
      //
      //
      //
  });

});

Put knockout, bootstrap and jquery examples into separate requirejs modules


angular.module('smartCampUZApp', ['ui.router', 'base64', 'angular-jwt'])

    .config(function ($stateProvider, $urlRouterProvider) {
        $stateProvider

            //starter screen
            .state('starter', {
                url: "/starter",
                templateUrl: "templates/starter.html",
                controller: "starterCtrl",
                onEnter: function ($state, auth) {
                    if (auth.isAuthenticated()) {
                        $state.go('admin');
                    }
                }
            })

            //starter screen
            .state('admin', {
                url: "/admin",
                templateUrl: "templates/admin.html",
                controller: "adminCtrl",
                onEnter: function ($state, auth) {
                    if (!auth.isAuthenticated()) {
                        $state.go('starter');
                    }
                }
            });

        $urlRouterProvider.otherwise('starter');
    });

angular.module('smartCampUZApp', ['ui.router', 'base64', 'angular-jwt'])

    .config(function ($stateProvider, $urlRouterProvider) {
        $stateProvider

            //starter screen
            .state('starter', {
                url: "/starter",
                templateUrl: "templates/starter.html",
                controller: "starterCtrl",
                onEnter: function ($state, auth) {
                    if (auth.isAuthenticated() && (auth.getType() == 'admin')) {
                        $state.go('admin');
                    } else if (auth.isAuthenticated() && (auth.getType() == 'maintenance')) {
                        $state.go('maintenance');
                    }
                }
            })

            //starter screen
            .state('admin', {
                url: "/admin",
                templateUrl: "templates/admin.html",
                controller: "adminCtrl",
                onEnter: function ($state, auth) {
                    if (!auth.isAuthenticated() || auth.isAuthenticated() && (auth.getType() == 'profesor')) {
                        $state.go('starter');
                    } else if (auth.isAuthenticated() && (auth.getType() == 'maintenance')) {
                        $state.go('maintenance');
                    }
                }
            });

        $urlRouterProvider.otherwise('starter');
    });

Add auth flow onEnter states


'use strict';

const hardwareModules = new Set([
    require('./triones'),
]);

function createFor(peripheral) {
    for (let module of hardwareModules) {
        if (module.isCompatibleWith(peripheral.advertisement)) {
            return module.createFor(peripheral);
        }
    }

    return Promise.resolve(null);
}

module.exports = {
    'createFor': createFor,
};

'use strict';

const noble = require('noble');

const hardwareModules = new Set([
    require('./triones'),
]);

function createFor(peripheral) {
    for (let module of hardwareModules) {
        if (module.isCompatibleWith(peripheral.advertisement)) {
            // Stop scanning: some BT devices do not support
            // scanning and connecting to a peripheral at
            // the same time.
            noble.stopScanning();

            // connect to the device
            return module.createFor(peripheral);
        }
    }

    return Promise.resolve(null);
}

module.exports = {
    'createFor': createFor,
};

Fix connecting to LE device not working on Linux


function createDoc(browser, docType) {
  const devServer = browser.globals.devServerURL;

  browser.url(`${devServer}/`);
  browser.expect.element('#app').to.be.visible.before(5000);

  browser.execute(() => {
    const className = `.${docType}`;
    const numDocs = document.querySelectorAll(className).length;

    return numDocs;
  }, [docType], (numDocs) => {
    try {
      const createButton = `#button-create-${docType}`;
      const numDocsAfterCreate = numDocs.value + 1;

      browser
        .click(createButton)
        .pause(500);

      browser
        .expect(numDocs.value).to.equal(numDocsAfterCreate);
    } catch (error) {
      console.log(error);
    }
  });
}
describe('FileManager\'s create file/folder button', function() {
  after((browser, done) => {
    browser.end(() => done());
  });

  it('should create a new file', (browser) => {
    createDoc(browser, 'file');
  });

  it('should create a new folder', (browser) => {
    createDoc(browser, 'folder');
  });
});

function createDoc(browser, docType) {
  const devServer = browser.globals.devServerURL;

  browser.url(`${devServer}/`);
  browser.expect.element('#app').to.be.visible.before(5000);

  browser.execute((data) => {
    const className = `.${data}`;
    const currentNumDocs = document.querySelectorAll(className).length;

    return currentNumDocs;
  }, [docType], (result) => {
    const className = `.${docType}`;
    const createButton = `#button-create-${docType}`;
    const previousNumDocs = result.value;
    const expectedNumDocs = previousNumDocs + 1;

    browser
        .click(createButton)
        .pause(500);

    browser
      .assert.elementCount(className, expectedNumDocs);
  });
}
describe('FileManager\'s create file/folder button', function() {
  after((browser, done) => {
    browser.end(() => done());
  });

  it('should create a new file', (browser) => {
    createDoc(browser, 'file');
  });

  it('should create a new folder', (browser) => {
    createDoc(browser, 'folder');
  });
});

Add e2e test case for create file/folder


var mongoose = require('mongoose')

var userSchema = new mongoose.Schema({
  id: { type: String, index: { unique: true } },
  twitter_id: String,
  twitter_credentials: mongoose.Schema.Types.Mixed,
  login_tokens: [ String ],
  data: mongoose.Schema.Types.Mixed,
  created_at: Date,
  updated_at: Date,
  last_street_id: Number
})

userSchema.pre('save', function (next) {
  var now = new Date()
  this.updated_at = now
  this.created_at = this.created_at || now
  next()
})

userSchema.methods.asJson = function (options, cb) {
  options = options || {}

  var json = {
    id: this.id
  }

  if (options.auth) {
    json.data = this.data
    json.createdAt = this.created_at
    json.updatedAt = this.updated_at
  }

  cb(null, json)
}

module.exports = mongoose.model('User', userSchema)

var mongoose = require('mongoose')

var userSchema = new mongoose.Schema({
  id: { type: String, index: { unique: true } },
  auth0_id: String,
  auth0_refesh_token: String,
  login_tokens: [ String ],
  data: mongoose.Schema.Types.Mixed,
  created_at: Date,
  updated_at: Date,
  last_street_id: Number
})

userSchema.pre('save', function (next) {
  var now = new Date()
  this.updated_at = now
  this.created_at = this.created_at || now
  next()
})

userSchema.methods.asJson = function (options, cb) {
  options = options || {}

  var json = {
    id: this.id
  }

  if (options.auth) {
    json.data = this.data
    json.createdAt = this.created_at
    json.updatedAt = this.updated_at
  }

  cb(null, json)
}

module.exports = mongoose.model('User', userSchema)

Update User model to match with Auth0 data


define(function(require, exports, module) {
  "use strict";

  var combyne = require("../../lib/index");

  describe("Delimiters", function() {
    it("can be changed on a template", function() {
      var tmpl = combyne("[[test]]", { test: "prop" });

      tmpl.setDelimiters({ START_PROP: "[[", END_PROP: "]]" });

      var output = tmpl.render();

      expect(output).to.equal("prop");
    });

    it("can be incomplete and mixed", function() {
      var tmpl = combyne("[[test}}", { test: "prop" });

      tmpl.setDelimiters({ START_PROP: "[[" });

      var output = tmpl.render();

      expect(output).to.equal("prop");
    });

    it("do not remember previous delimiters", function() {
      var tmpl = combyne("[[one]] {{two}}", { one: "prop", two: "tes" });

      tmpl.setDelimiters({ START_PROP: "[[", END_PROP: "]]" });

      var output = tmpl.render();

      expect(output).to.equal("prop {{two}}");
    });
  });
});

define(function(require, exports, module) {
  "use strict";

  var combyne = require("../../lib/index");

  describe("Delimiters", function() {
    it("will throw an error if not passed an object", function() {
      var tmpl = combyne("[[test]]", { test: "prop" });

      expect(tmpl.setDelimiters).to.throw(Error);
    });

    it("can be changed on a template", function() {
      var tmpl = combyne("[[test]]", { test: "prop" });

      tmpl.setDelimiters({ START_PROP: "[[", END_PROP: "]]" });

      var output = tmpl.render();

      expect(output).to.equal("prop");
    });

    it("can be incomplete and mixed", function() {
      var tmpl = combyne("[[test}}", { test: "prop" });

      tmpl.setDelimiters({ START_PROP: "[[" });

      var output = tmpl.render();

      expect(output).to.equal("prop");
    });

    it("do not remember previous delimiters", function() {
      var tmpl = combyne("[[one]] {{two}}", { one: "prop", two: "tes" });

      tmpl.setDelimiters({ START_PROP: "[[", END_PROP: "]]" });

      var output = tmpl.render();

      expect(output).to.equal("prop {{two}}");
    });
  });
});

Test ensuring setDelimiters requires an object.


import React, {PropTypes} from 'react';
import classNames from 'classnames';

import styles from './styles.css';

function ListSeparator(props) {
  const mainStyles = [`separator`, styles.separator];
  const textStyles = [`separator-text`, styles.separatorText];
  const informativeClass = `informative`;
  if (props.isInformative) {
    mainStyles.push(informativeClass);
    mainStyles.push(styles[informativeClass]);
    textStyles.push(informativeClass);
    textStyles.push(styles[informativeClass]);
  }
  return (

{props.primaryText}

  );
}

ListSeparator.propTypes = {
  isInformative: PropTypes.boolean,
  primaryText: PropTypes.object
};

export default ListSeparator;

import React, {PropTypes} from 'react';
import classNames from 'classnames';

import styles from './styles.css';

function ListSeparator(props) {
  const mainStyles = [`separator`, styles.separator];
  const textStyles = [`separator-text`, styles.separatorText];
  const informativeClass = `informative`;
  if (props.isInformative) {
    mainStyles.push(informativeClass);
    mainStyles.push(styles[informativeClass]);
    textStyles.push(informativeClass);
    textStyles.push(styles[informativeClass]);
  }
  return (

{props.primaryText}

  );
}

ListSeparator.propTypes = {
  isInformative: PropTypes.bool,
  primaryText: PropTypes.object
};

export default ListSeparator;

Fix proptype error with info list separator


angular.module('ngCordova.plugins.deviceOrientation', [])

.factory('$cordovaDeviceOrientation', ['$q', function($q) {

  return {
    watchHeading: function(options) {
      var q = $q.defer();

      navigator.compass.watchHeading(function(result) {
        q.resolve(result);
      }, function(err) {
        q.reject(err);
      }, options);

      return q.promise;
    }
  }
}]);

angular.module('ngCordova.plugins.deviceOrientation', [])

.factory('$cordovaDeviceOrientation', ['$q', function($q) {

  return {
    watchHeading: function(options) {
      var q = $q.defer();

      navigator.compass.watchHeading(function(result) {
        q.notify(result);
      }, function(err) {
        q.reject(err);
      }, options);

      return q.promise;
    }
  }
}]);

Use `notify` not `resolve` with `watchHeading`


/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var http = require('http');
var path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var http = require('http');
var path = require('path');
var fs = require('fs');

var app = express();

var json = fs.readFileSync(__dirname + '/../config.json', 'utf8');
var fakeminder_config = JSON.parse(json);

// all environments
app.set('port', fakeminder_config.target_site.port);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

Read port number for target site from JSON config


var express  = require('express');
var router   = express.Router();
var mongoose = require('../models/mongoose');
const config = require('../config');

// Redis
const Redis  = require('ioredis');
var redis_client = new Redis(config.redis_connection);

// routes that end with jobs
// ----------------------------------------------------
// These are redis-based queries
// route to return all current requests (GET http://localhost:3000/api/requests)
router.route('/jobs/submit')

  // add a request for a process to launch (accessed at PUT api/requests)
  .put(function(req, res) {

    let request = req.body.request;

    // Add _id to process is not present
    if (! request.process._id) {
      request.process._id = new mongoose.mongo.ObjectId();
    }

    console.log("REQUEST", request);

    redis_client.lpush('RAPD_JOBS', JSON.stringify(request), function(err, queue_length) {
      if (err) {
        console.error(err);
        res.status(500).json({
          success: false,
          error: err
        });
      } else {
        console.log('Job added to RAPD_JOBS queue length:', queue_length);
        res.status(200).json({
          success: true,
          queue_length: queue_length
        });
      }
    });
  }); // End .put(function(req,res) {

module.exports = router;

var express  = require('express');
var router   = express.Router();
var mongoose = require('../models/mongoose');
const config = require('../config');

// Redis
const Redis  = require('ioredis');
var redis_client = new Redis(config.redis_connection);

// routes that end with jobs
// ----------------------------------------------------
// These are redis-based queries
// route to return all current requests (GET http://localhost:3000/api/requests)
router.route('/jobs/submit')

  // add a request for a process to launch (accessed at PUT api/requests)
  .put(function(req, res) {

    let request = req.body.request;

    // Add _id to process is not present
    if (! request.process.result_id) {
      request.process.result_id = new mongoose.mongo.ObjectId();
    }

    console.log("REQUEST", request);

    redis_client.lpush('RAPD_JOBS', JSON.stringify(request), function(err, queue_length) {
      if (err) {
        console.error(err);
        res.status(500).json({
          success: false,
          error: err
        });
      } else {
        console.log('Job added to RAPD_JOBS queue length:', queue_length);
        res.status(200).json({
          success: true,
          queue_length: queue_length
        });
      }
    });
  }); // End .put(function(req,res) {

module.exports = router;

Adjust route to add result_id, not _id...


import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';

checkNpmVersions({
  'es6-enum': '1.x',
  express: '4.x',
  helmet: '3.x',
  i18next: '6.0.x',
  'i18next-node-remote-backend': '0.0.x',
  'i18next-xhr-backend': '1.3.x',
  lodash: '4.x',
  logatim: '0.x',
  moment: '2.x',
  'react-dom': '15.x',
  'react-helmet': '4.x',
  'react-i18next': '2.0.x',
  'react-redux': '5.x',
  receptacle: '1.x',
  redux: '3.x',
  useragent: '2.1.x',
  winston: '2.x',
}, 'ssrwpo:ssr');

import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';

checkNpmVersions({
  express: '4.x',
  helmet: '3.x',
  i18next: '6.0.x',
  'i18next-node-remote-backend': '0.0.x',
  'i18next-xhr-backend': '1.3.x',
  lodash: '4.x',
  logatim: '0.x',
  moment: '2.x',
  'react-dom': '15.x',
  'react-helmet': '4.x',
  'react-i18next': '2.0.x',
  'react-redux': '5.x',
  receptacle: '1.x',
  redux: '3.x',
  useragent: '2.1.x',
  winston: '2.x',
}, 'ssrwpo:ssr');

Remove es6-enum as peer dependency


jQuery.fn.uniform = function(settings) {
  settings = jQuery.extend({
    valid_class    : 'valid',
    invalid_class  : 'invalid',
    focused_class  : 'focused',
    holder_class   : 'ctrlHolder',
    field_selector : 'input, select, textarea'
  }, settings);

  return this.each(function() {
    var form = jQuery(this);

    // Focus specific control holder
    var focusControlHolder = function(element) {
      element.parents("." + settings.holder_class).addClass(settings.focused_class)
    };

    // Select form fields and attach them higlighter functionality
    form.find(settings.field_selector).focus(function() {
      form.find('.' + settings.focused_class).removeClass(settings.focused_class);
      focusControlHolder(jQuery(this));
    }).blur(function() {
      form.find('.' + settings.focused_class).removeClass(settings.focused_class);
    });
  });
};

// Auto set on page load...
$(document).ready(function() {
  jQuery('form.uniForm').uniform();
});

jQuery.fn.uniform = function(settings) {
  settings = jQuery.extend({
    valid_class    : 'valid',
    invalid_class  : 'invalid',
    focused_class  : 'focused',
    holder_class   : 'ctrlHolder',
    field_selector : 'input, select, textarea'
  }, settings);

  return this.each(function() {
    var form = jQuery(this);

    // Focus specific control holder
    var focusControlHolder = function(element) {
      element.parents("." + settings.holder_class).addClass(settings.focused_class)
    };

    // Select form fields and attach them higlighter functionality
    form.find(settings.field_selector).focus(function() {
      form.find('.' + settings.focused_class).removeClass(settings.focused_class);
      focusControlHolder(jQuery(this));
    }).blur(function() {
      form.find('.' + settings.focused_class).removeClass(settings.focused_class);
    });
  });
};

// Auto set on page load...
$(document).ready(function() {
  jQuery('form.uniForm').uniform();
  $(document.activeElement).focus() //safari doesn't support this and has no alternative
});

Set the focus highlight correctly if an element has focus while the page is being loaded.


const Command = require('../Command');

const messages = {
    success: 'I am currently operating off commit `{key}`.\nGitHub: https://github.com/Shotbow/ChatBot/commit/{key}',
    error: 'I do not know what version I am currently running.',
};

module.exports = Command.extend({
    commandName: 'debug',
    commandAliases: ['commit', 'hash'],
    advertisable: false,
    proc: null,
    version: null,
    dependencies: {
        'child_process': 'proc'
    },

    initialize: function (dependencyGraph) {
        this._super(dependencyGraph);

        this.proc.exec('git rev-parse HEAD', function (err, stdout) {
            if (stdout) {
                this.version = stdout.trim();
            }
        }.bind(this))
    },

    processMessage: function (message, tokens) {
        if (this.version) {
            return message.channel.send(this.i18n.__mf(messages.success, {key: this.version}));
        } else {
            return message.channel.send(this.i18n.__mf(messages.error));
        }
    }
});

const Command = require('../Command');

const messages = {
    success: 'I am currently operating off commit `{key}`.\nGitHub: <https://github.com/Shotbow/ChatBot/commit/{key}>',
    error: 'I do not know what version I am currently running.',
};

module.exports = Command.extend({
    commandName: 'debug',
    commandAliases: ['commit', 'hash'],
    advertisable: false,
    proc: null,
    version: null,
    dependencies: {
        'child_process': 'proc'
    },

    initialize: function (dependencyGraph) {
        this._super(dependencyGraph);

        this.proc.exec('git rev-parse HEAD', function (err, stdout) {
            if (stdout) {
                this.version = stdout.trim();
            }
        }.bind(this))
    },

    processMessage: function (message, tokens) {
        if (this.version) {
            return message.channel.send(this.i18n.__mf(messages.success, {key: this.version}));
        } else {
            return message.channel.send(this.i18n.__mf(messages.error));
        }
    }
});

Remove link preview from !debug


class CurrentTask {
    constructor() {
        this.currentTask = null;
    }

    get() {
        return this.currentTask;
    }

    set(task) {
        this.currentTask = task;
    }
}

angular.module('materialscommons').service('currentTask', CurrentTask);

class CurrentTask {
    constructor() {
        this.currentTask = null;
        this.onChangeFN = null;
    }

    setOnChange(fn) {
        this.onChangeFN = fn;
    }

    get() {
        return this.currentTask;
    }

    set(task) {
        this.currentTask = task;
        if (this.onChangeFN) {
            this.onChangeFN();
        }
    }
}

angular.module('materialscommons').service('currentTask', CurrentTask);

Add setOnChange that allows a controller to set a function to call when the currentTask changes.


$(document).ready(function() {

    // Init action when user select sildeshow to see
    $('.slideshow-item').on('click', function(event) {

        // Cancel event
        global.cancelEvent(event);

        // Select new slideshow item
        $('.slideshow-item').removeClass('activ');
        $(this).addClass('activ');

        // Extract slideshow identifier and display slideshow
        var slideshowId = String($(this).attr('data-slideshow-id'));
        displaySlideshow(slideshowId);
    });

    // Select first slideshow per default
    if ($('.slideshow-item').length > 0)
        $('.slideshow-item')[0].click();
});

/**
 * Allows to display slideshow using Ajax.
 * @param event Click event to prevent.
 * @param slideshowItem Slideshow item that have been selected.
 */
function displaySlideshow(slideshowId) {

    // Load slideshow content and display it
    var ajaxBaseUrl = global.basePath + '/ajax/slideshow/' + slideshowId;

    $.ajax({
        url: ajaxBaseUrl,
        timeout: 10000,
        success: function(msg) {

            // Add new content and initialize it (carousel + slideshow)
            $('#section-content').html(msg);
            initAjaxSlideshow();
            initThumbnailAction();
        },
        error: function(msg) {
            alert("Impossible de charger le diaporama");
        }
    });
}

/**
 * Allows to initialize action when user click on thumbnails.
 */
function initThumbnailAction() {

    $('.thumbnail-item').on('click', function() {

        var index = $(this).attr('data-index');
        $('#picture-list').slickGoTo(index);
    });
}

$(document).ready(function() {

    // Init action when user select sildeshow to see
    $('.slideshow-item').on('click', function(event) {

        // Cancel event
        global.cancelEvent(event);

        // Select new slideshow item
        $('.slideshow-item').removeClass('activ');
        $(this).addClass('activ');

        // Extract slideshow identifier and display slideshow
        var slideshowId = String($(this).attr('data-slideshow-id'));
        displaySlideshow(slideshowId);
    });

    // Select first slideshow per default
    if ($('.slideshow-item').length > 0)
        $('.slideshow-item')[0].click();
});

/**
 * Allows to display slideshow using Ajax.
 * @param event Click event to prevent.
 * @param slideshowItem Slideshow item that have been selected.
 */
function displaySlideshow(slideshowId) {

    // Load slideshow content and display it
    var ajaxBaseUrl = global.basePath + '/ajax/slideshow/' + slideshowId;

    $.ajax({
        url: ajaxBaseUrl,
        timeout: 10000,
        success: function(msg) {

            // Add new content and initialize it (carousel + slideshow)
            $('#section-content').html(msg);
            initAjaxSlideshow();
            initThumbnailAction();
        },
        error: function(msg) {
            alert("Impossible de charger le diaporama");
        }
    });
}

/**
 * Allows to initialize action when user click on thumbnails.
 */
function initThumbnailAction() {

    $('.thumbnail-item').on('click', function() {

        var index = $(this).attr('data-index');
        $('#picture-list').slick('slickGoTo', index);
    });
}

Fix JS bug on thumbnail


'use strict';

const collectionValidator = require('../validators/collection'),
  repo = require('../repositories/collection'),
  restify = require('restify');

module.exports = {
  post: (req, res, next) => {
    let collection = req.body;
    let username = req.user.username;
    let errors = collectionValidator(collection);

    if (errors && errors.length) {
      let msg = errors[0].message;
      return next(new restify.errors.BadRequestError(msg));
    }

    collection.username = username;
    collection.created = new Date();

    let docSource = repo
      .get(username, collection.name)
      .do(doc => {
        if (doc) {
          let msg = `user ${username} already has a collection named ${collection.name}`;
          throw new restify.errors.ConflictError(msg);
        }
      })
      .flatMap(repo.create(collection))
      .subscribe(data => res.send(data), next, next);
  }
};

'use strict';

const collectionValidator = require('../validators/collection'),
  repo = require('../repositories/collection'),
  restify = require('restify');

module.exports = {
  post: (req, res, next) => {
    let collection = req.body;
    let username = req.user.username;
    let errors = collectionValidator(collection);

    if (errors && errors.length) {
      let msg = errors[0].message;
      return next(new restify.errors.BadRequestError(msg));
    }

    collection.username = username;

    let docSource = repo
      .get(username, collection.name)
      .do(doc => {
        if (doc) {
          let msg = `user ${username} already has a collection named ${collection.name}`;
          throw new restify.errors.ConflictError(msg);
        }
      })
      .flatMap(repo.create(collection))
      .subscribe(data => res.send(data), next, next);
  }
};

Remove redundant created on value


'use strict';

/**
 * Utility method to permit Array#reduce-like operations over objects
 *
 * This is likely to be slightly more inefficient than using lodash.reduce,
 * but results in ~50kb less size in the resulting bundled code before
 * minification and ~12kb of savings with minification.
 *
 * @module object-reduce
 * @private
 * @param {Object} obj An object of key-value pairs
 * @param {Function} iterator A function to use to reduce the object
 * @param {*} initialState The initial value to pass to the reducer function
 * @returns The result of the reduction operation
 */
module.exports = function( obj, iterator, initialState ) {
	return Object.keys( obj ).reduce( function( memo, key ) {
		return iterator( memo, obj[ key ], key );
	}, initialState );
};

'use strict';

/**
 * Utility method to permit Array#reduce-like operations over objects
 *
 * This is likely to be slightly more inefficient than using lodash.reduce,
 * but results in ~50kb less size in the resulting bundled code before
 * minification and ~12kb of savings with minification.
 *
 * Unlike lodash.reduce(), the iterator and initial value properties are NOT
 * optional: this is done to simplify the code, this module is not intended to
 * be a full replacement for lodash.reduce and instead prioritizes simplicity
 * for a specific common case.
 *
 * @module object-reduce
 * @private
 * @param {Object} obj An object of key-value pairs
 * @param {Function} iterator A function to use to reduce the object
 * @param {*} initialState The initial value to pass to the reducer function
 * @returns The result of the reduction operation
 */
module.exports = function( obj, iterator, initialState ) {
	return Object.keys( obj ).reduce( function( memo, key ) {
		return iterator( memo, obj[ key ], key );
	}, initialState );
};

Document rationale for the discrepancy from lodash.reduce signature


const micro = require('micro');
const fs = require('fs');
const path = require('path');

const document = path.join(__dirname, 'index.html');
const html = fs.readFileSync(document);

const server = micro(async (req, res) => {
	console.log('Serving index.html');
	res.end(html);
});

const io = require('socket.io')(server);

// socket-io handlers are in websocket-server.js
require('./websocket-server.js')(io);

console.log('Listening on localhost:4000');
server.listen(4000);

const micro = require('micro');
const fs = require('fs');
const path = require('path');

const document = path.join(__dirname, 'index.html');
const html = fs.readFileSync(document);

const server = micro(async (req, res) => {
	console.log('Serving index.html');
	res.end(html);
});

const io = require('socket.io')(server);

// socket-io handlers are in websocket-server.js
require('./websocket-server.js')(io);

server.listen(4000, () => console.log('Listening on localhost:4000'));

Move the hint message to the callback


// @flow
/**
 * Part of GDL gdl-frontend.
 * Copyright (C) 2017 GDL
 *
 * See LICENSE
 */

import styled from 'styled-components';

// TODO: Fix text-shadow
const Button = styled.button`
  color: #fff;
  border-radius: 4px;
  background: linear-gradient(180deg, #5cbc80 0%, #359258 100%);
  line-height: 1.1;
  padding: 10px 30px;
  font-size: 1.4rem;
  font-weight: 600;
  min-width: 150px;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
  &:hover,
  &:active {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  }
`;

export default Button;

// @flow
/**
 * Part of GDL gdl-frontend.
 * Copyright (C) 2017 GDL
 *
 * See LICENSE
 */

import styled from 'styled-components';

// TODO: Fix text-shadow
const Button = styled.button`
  color: #fff;
  border-radius: 4px;
  background: linear-gradient(180deg, #5cbc80 0%, #359258 100%);
  line-height: 1.1;
  padding: 10px 30px;
  font-size: 1.4rem;
  font-weight: 600;
  min-width: 150px;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
  border-style: none;
  &:hover,
  &:active {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  }
`;

export default Button;

Remove border on Read button


import url from 'url';
import {
  formatOpenURL,
  parse,
} from 'spotify-uri';

export const getCountryParam = (urlString) => {
  const parsed = url.parse(urlString);
  if (parsed.query && parsed.query.country) {
    return parsed.query.country;
  }
  return 'us';
};

export const getUrl = (playlist) => {
  if (!playlist || !playlist.identifier) {
    return null;
  }
  const id = playlist.identifier;
  switch (playlist.provider) {
    case 'YouTube':
      return `https://www.youtube.com/playlist?list=${id}`;
    case 'SoundCloud':
      return `https://soundcloud.com/playlists/${id}`;
    case 'Spotify':
      return formatOpenURL(parse(playlist.url));
    case 'AppleMusic': {
      const country = getCountryParam(playlist.url);
      return `http://tools.applemusic.com/embed/v1/playlist/pl.${id}?country=${country}`;
    }
    default:
      return playlist.url;
  }
};

import url from 'url';
import {
  formatOpenURL,
  parse,
} from 'spotify-uri';

export const getCountryParam = (urlString) => {
  const parsed = url.parse(urlString);
  if (parsed.query && parsed.query.country) {
    return parsed.query.country;
  }
  return 'us';
};

export const getOwnerName = playlist => playlist.owner_name || playlist.owner_id;

export const getUrl = (playlist) => {
  if (!playlist || !playlist.identifier) {
    return null;
  }
  const id = playlist.identifier;
  switch (playlist.provider) {
    case 'YouTube':
      return `https://www.youtube.com/playlist?list=${id}`;
    case 'SoundCloud':
      return `https://soundcloud.com/playlists/${id}`;
    case 'Spotify':
      return formatOpenURL(parse(playlist.url));
    case 'AppleMusic': {
      const country = getCountryParam(playlist.url);
      return `http://tools.applemusic.com/embed/v1/playlist/pl.${id}?country=${country}`;
    }
    default:
      return playlist.url;
  }
};

Use owner_id if owner_name of playlist doesn't exist