// IMPORTANT: choose one
var RL_LIB = "libreadline";  // NOTE: libreadline is GPL
//var RL_LIB = "libedit";

var HISTORY_FILE = require('path').join(process.env.HOME, '.mal-history');

var rlwrap = {}; // namespace for this module in web context

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

var rllib = ffi.Library(RL_LIB, {
    'readline':    [ 'string', [ 'string' ] ],
    'add_history': [ 'int',    [ 'string' ] ]});

var rl_history_loaded = false;

exports.readline = rlwrap.readline = function(prompt) {
    prompt = prompt || "user> ";

    if (!rl_history_loaded) {
        rl_history_loaded = true;
        var lines = fs.readFileSync(HISTORY_FILE).toString().split("\n");
        // Max of 2000 lines
        lines = lines.slice(Math.max(lines.length - 2000, 0));
        for (var i=0; i<lines.length; i++) {
            if (lines[i]) { rllib.add_history(lines[i]); }
        }
    }

    var line = rllib.readline(prompt);
    if (line) {
        rllib.add_history(line);
        fs.appendFileSync(HISTORY_FILE, line + "\n");
    }

    return line;
};
var readline = exports;

// IMPORTANT: choose one
var RL_LIB = "libreadline";  // NOTE: libreadline is GPL
//var RL_LIB = "libedit";

var HISTORY_FILE = require('path').join(process.env.HOME, '.mal-history');

var rlwrap = {}; // namespace for this module in web context

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

var rllib = ffi.Library(RL_LIB, {
    'readline':    [ 'string', [ 'string' ] ],
    'add_history': [ 'int',    [ 'string' ] ]});

var rl_history_loaded = false;

exports.readline = rlwrap.readline = function(prompt) {
    prompt = prompt || "user> ";

    if (!rl_history_loaded) {
        rl_history_loaded = true;
        var lines = [];
        if (fs.existsSync(HISTORY_FILE)) {
            lines = fs.readFileSync(HISTORY_FILE).toString().split("\n");
        }
        // Max of 2000 lines
        lines = lines.slice(Math.max(lines.length - 2000, 0));
        for (var i=0; i<lines.length; i++) {
            if (lines[i]) { rllib.add_history(lines[i]); }
        }
    }

    var line = rllib.readline(prompt);
    if (line) {
        rllib.add_history(line);
        fs.appendFileSync(HISTORY_FILE, line + "\n");
    }

    return line;
};
var readline = exports;

Correct issue where readline bombs out if there's no existing history file.


import React, { Component } from 'react';
import Promise from 'bluebird';
import FontAwesome from 'react-fontawesome';
import quotes from './quotes.json';
import styles from './jumboQuote.css';

class JumboQuote extends Component {
  constructor(props) {
    super(props);

    this.state = {
      quote: {}
    };
  }

  componentDidMount() {
    // TODO: Pull this from an API with fetch
    Promise.try(
      () => quotes.quotes[Math.floor(Math.random() * quotes.quotes.length)]
    ).then((quote) => {
      this.setState({ quote });
    });
  }

  render() {
    return (

          <FontAwesome name="quote-left" className={styles.quoteIcon} />
          {this.state.quote.body}
          <FontAwesome name="quote-right" className={styles.quoteIcon} />

{this.state.quote.author}

    );
  }
}

export default JumboQuote;

import React, { Component } from 'react';
import Promise from 'bluebird';
import FontAwesome from 'react-fontawesome';
import quotes from './quotes.json';
import styles from './jumboQuote.css';

class JumboQuote extends Component {
  constructor(props) {
    super(props);

    this.state = {
      quote: {}
    };
  }

  componentDidMount() {
    // TODO: Pull this from an API with fetch
    // (kylemh: recommend axios if using promises) source: https://daveceddia.com/ajax-requests-in-react/
    Promise.try(
      () => quotes.quotes[Math.floor(Math.random() * quotes.quotes.length)]
    ).then((quote) => {
      this.setState({ quote });
    });
  }

  render() {
    return (

          <FontAwesome name="quote-left" className={styles.quoteIcon} />
          {this.state.quote.body}
          <FontAwesome name="quote-right" className={styles.quoteIcon} />

{this.state.quote.author}

    );
  }
}

export default JumboQuote;

Add note on TODO for handling quotes via API


import Ember from 'ember';

/**
 * This mixin add support for hasBlock for Ember < 1.13.0
 *
 * @todo Remove in Ember 2.0
 * See also https://github.com/emberjs/ember.js/pull/11313
 */
export default Ember.Mixin.create({
  hasBlock: Ember.computed(function () {
    return !!this.get("template");
  })
});

import Ember from 'ember';

/**
 * This mixin add support for hasBlock for Ember < 1.13.0
 *
 * @todo Remove in Ember 2.0
 * See also https://github.com/emberjs/ember.js/pull/11313
 */
export default Ember.Mixin.create({
  hasBlock: Ember.computed(function () {
    if (typeof this._super.hasBlock === 'undefined') {
      return !!this.get("template");
    }
    return this.hasBlock;
  })
});

Check for hasBlock on super if it exist, otherwise use template.


console.log('The bot is starting');

var Twit = require('twit');
var config = require('./config.js');
var T = new Twit(config);

console.log('The bot is starting');

// Setup node packages
var fs = require('fs');
var twit = require('twit');
var http = require('http');
var parseString = require('xml2js').parseString;

// Setup Twitter API
var twitterAPI = require('./twitterapi.js');
var t = new twit(twitterAPI);

// Setup Behind the Name API
var btnAPI = require('./btnapi.js');
var btnAPIKey = btnAPI.key;
var btnPath = '/api/random.php?gender=' + gender + '&number=1&randomsurname=yes&key=' + btnAPIKey;

// Setup variables
var btnContent = '';
var tweetContent = {
  status: ''
};
var timeOfDeath = 'This morning'; // placeholder
var causeOfDeath = 'heart attack'; // placeholder
var gender;
var forename = '';
var surname = '';
var epitaph = 'Rest in peace.'; // placeholder

// Get random time of death
// TODO

// Get random cause of death
// TODO

// Get random gender
function getRandomGender() {
  var r = Math.floor(Math.random() * 2);
  if (r === 0) {
    gender = 'f';
  } else {
    gender = 'm';
  }
}

// Get random names
function getRandomNames(callback) {
  console.log('Getting names from behindthename.com...');
  return http.get({
    host: 'www.behindthename.com',
    path: btnPath
  }, function(response) {
    response.on('data', function(d) {
      btnContent += d;
      parseString(btnContent, function (err, result) {
        if (err) {
          console.log('Error parsing XML data!');
        } else {
          // Strip ugly XML / JSON structure
          var json = JSON.stringify(result);
          json = JSON.parse(json.substring(30, (json.length - 4)));

          // Get both names from remaining JSON array
          forename = json[0];
          surname = json[1];
        }
      });
    });
    response.on('end', function() {
      callback();
    });
  });
}

// Get random epitaph
// TODO

getRandomGender();
getRandomNames(function() {
  tweetContent = {
    status: timeOfDeath + ', ' + forename + ' ' + surname + ' died of a ' + causeOfDeath + '. ' + epitaph
  };
  console.log(tweetContent);
});

// Send tweet
/*
t.post('statuses/update', tweetContent, tweeted);

function tweeted(err, data, response) {
  if (err) {
    console.log('ERROR! Tweet failed to send.');
  } else {
    console.log('SUCCESS! Tweet sent.');
  }
}
*/

Add random names using behindthename.com API


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

export default Ember.Component.extend({
  layout: layout,
  noDataMessage: 'No data',
  pagination: true,
  selection: Ember.computed.filterBy('content', 'isSelected', true),
  hasMenu: false, // set from inner component, migth fail with nested if
  enableSelection: Ember.computed.oneWay('hasMenu'),
  parsedFields: Ember.computed('fields', function() {
    const fields = this.get('fields');
    if( Ember.typeOf( fields ) == 'string' ) {
      return fields.split(' ');
    } else {
      return fields || []
    }
  }),
});

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

export default Ember.Component.extend({
  layout: layout,
  noDataMessage: 'No data',
  pagination: Ember.computed.bool('content.meta.pagination'),
  selection: Ember.computed.filterBy('content', 'isSelected', true),
  hasMenu: false, // set from inner component, migth fail with nested if
  enableSelection: Ember.computed.oneWay('hasMenu'),
  parsedFields: Ember.computed('fields', function() {
    const fields = this.get('fields');
    if( Ember.typeOf( fields ) == 'string' ) {
      return fields.split(' ');
    } else {
      return fields || []
    }
  }),
});

Set sensible default for the pagination parameter


let createExampleOrg = function() {
  let byEmail = { 'emails.address': process.env.EXAMPLE_ORG_EMAIL };
  if (Meteor.users.find(byEmail).count() === 0) {

    Donations.app.send(new Donations.RegisterOrganization({
      targetId: new Guid(),
      name: 'My Example Organization',
      password: new Password(SHA256(process.env.EXAMPLE_ORG_PASSWORD)),
      country: new Country('AT'),
      contact: new Donations.Contact({
        email: new EmailAddress(process.env.EXAMPLE_ORG_EMAIL),
        name: 'Dominik Guzei',
        phone: '+43 676 9222862'
      })
    }));
  }
};

Donations.setupDevData = function() {
  createExampleOrg();
};

let env = Space.getenv.multi({
  password: ['EXAMPLE_ORG_PASSWORD', '1234', 'string'],
  email: ['EXAMPLE_ORG_EMAIL', 'example@email.com', 'string']
});

let createExampleOrg = function() {
  let byEmail = { 'emails.address': env.email };
  if (Meteor.users.find(byEmail).count() === 0) {

    Donations.app.send(new Donations.RegisterOrganization({
      targetId: new Guid(),
      name: 'My Example Organization',
      password: new Password(SHA256(env.password)),
      country: new Country('AT'),
      contact: new Donations.Contact({
        email: new EmailAddress(env.email),
        name: 'Dominik Guzei',
        phone: '+43 676 9222862'
      })
    }));
  }
};

Donations.setupDevData = function() {
  createExampleOrg();
};

Switch to Space.getenv for default envs


const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'public'),
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
    }),
  ],

  devServer: {
    contentBase: path.join(__dirname, 'public'),
  },
};

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ],
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
    }),
  ],

  devServer: {
    contentBase: path.join(__dirname, 'dist'),
  },
};

Change build directory to `dist`


class Cookie {

	static initialize(prefix) {
		this._prefix = prefix;
	}

	/*
	static set prefix(prefix) {
		this._prefix = prefix;
	}

	static get prefix( ) {
		return this._prefix;
	}
	*/

	static get(cookie) {
		let cookies = decodeURIComponent(document.cookie).split(";");

		for(let i = 0; i < cookies.length; i++) {
			let selection = cookies[i];

			while(selection.charAt(0) === " ") selection = selection.substring(1);

			if(selection.indexOf(this.prefix + "_" + cookie + "=") === 0) {
				return selection.substring((this.prefix + "_" + cookie + "=").length, selection.length);
			}
		}

		return false;
	}

	static set(cookie, value) {
		let date = new Date( );
		date.setTime(date.getTime( ) + 3600 * 3600);

		document.cookie = this.prefix + "_" + cookie + "=" + value + "; " + date.toUTCString( ) + "; path=/";
	}

	static remove(cookie) {
		let date = new Date( );
		date.setTime(date.getTime( ) - 3600 * 3600);

		document.cookie = this.prefix + "_" + cookie + "= ; " + date.toUTCString( ) + "; path=/";
	}

}

class Cookie {

	static initialize(prefix) {
		this._prefix = prefix;
	}

	/*
	static set prefix(prefix) {
		this._prefix = prefix;
	}

	static get prefix( ) {
		return this._prefix;
	}
	*/

	static get(cookie) {
		let cookies = decodeURIComponent(document.cookie).split(";");

		for(let i = 0; i < cookies.length; i++) {
			let selection = cookies[i];

			while(selection.charAt(0) === " ") selection = selection.substring(1);

			if(selection.indexOf(this._prefix + "_" + cookie + "=") === 0) {
				return selection.substring((this._prefix + "_" + cookie + "=").length, selection.length);
			}
		}

		return false;
	}

	static set(cookie, value) {
		let date = new Date( );
		date.setTime(date.getTime( ) + 3600 * 3600);

		document.cookie = this._prefix + "_" + cookie + "=" + value + "; " + date.toUTCString( ) + "; path=/";
	}

	static remove(cookie) {
		let date = new Date( );
		date.setTime(date.getTime( ) - 3600 * 3600);

		document.cookie = this._prefix + "_" + cookie + "= ; " + date.toUTCString( ) + "; path=/";
	}

}

Update references to fix Language selection


const React = require('react');
const importJsx = require('import-jsx');
const { render } = require('ink');

const App = importJsx('./app');

const renderInteractive = taskRunner => {
  const { unmount } = render(<App taskRunner={taskRunner} />);
  return unmount;
};

module.exports = { renderInteractive };

const React = require('react');
const importJsx = require('import-jsx');
const { render } = require('ink');

const App = importJsx('./App');

const renderInteractive = taskRunner => {
  const { unmount } = render(<App taskRunner={taskRunner} />);
  return unmount;
};

module.exports = { renderInteractive };

Fix import of interactive renderer App


import { getBaseDestination } from '../storage/storage';

const youtubedl = require('youtube-dl');
const path = require('path');
const fs = require('fs');
const { remote } = require('electron');

export var init = function () { };

export var downloadVideo = function (link, onInfo, onError, onEnd) {
  let filePath;

  const baseDestination = getBaseDestination();
  if (! fs.existsSync(baseDestination)) {
    fs.mkdirSync(baseDestination);
  }

  const video = youtubedl(
    link,
    ['--format=18'],
    { cwd: baseDestination }
  );

  video.on('info', function (info) {
    onInfo(info);

    const destination = getDestination(baseDestination, info);
    if (! fs.existsSync(destination)) {
      fs.mkdirSync(destination);
    }

    filePath = path.join(destination, info._filename);
    video.pipe(fs.createWriteStream(filePath));
  });

  video.on('error', onError);
  video.on('end', () => {
    console.log('finished downloading!');
    onEnd(filePath);
  });
};

function getDestination(baseDestination, info) {
  return path.join(baseDestination, info.uploader);
}

import { getBaseDestination } from '../storage/storage';

const youtubedl = require('youtube-dl');
const path = require('path');
const fs = require('fs');
const { remote } = require('electron');

export var init = function () { };

export var downloadVideo = function (link, onInfo, onError, onEnd) {
  let filePath;

  const baseDestination = getBaseDestination();
  if (! fs.existsSync(baseDestination)) {
    fs.mkdirSync(baseDestination);
  }

  const video = youtubedl(
    link,
    ['--format=18'],
    { cwd: baseDestination }
  );

  video.on('info', function (info) {
    onInfo(info);

    const destination = getDestination(baseDestination, info);
    if (! fs.existsSync(destination)) {
      fs.mkdirSync(destination);
    }

    filePath = getFilePath(destination, info);
    video.pipe(fs.createWriteStream(filePath));
  });

  video.on('error', onError);
  video.on('end', () => {
    console.log('finished downloading!');
    onEnd(filePath);
  });
};

function getDestination(baseDestination, info) {
  if (info.playlist === null || info.playlist === 'NA') {
    return path.join(baseDestination, info.uploader);
  }

  return path.join(baseDestination, info.uploader, info.playlist);
}

function getFilePath(destination, info) {
  if (info.playlist_index === null || info.playlist_index === 'NA') {
    return path.join(destination, info._filename);
  }

  return path.join(destination, info.playlist_index + ' - ' + info._filename);
}

Add playlist folder and playlist index in title if there is one


import React from 'react';
import connect from 'react-redux';
import { fetchPost } from '../actions';

class PostsShow extends React.Component {
  componentDidMount() {
    const { id } = this.props.match.params;
    this.props.fetchPost(id);
  }

  render() {
    const { post } = this.props;

    return (

{post.title}

Categories: {post.categories}

{post.content}

    )
  }
}

function mapStateToProps({ posts }, ownProps) {
  return { post: posts[ownProps.match.params.id] };
}

export default connect(mapStateToProps, { fetchPost })(PostsShow);

import React from 'react';
import connect from 'react-redux';
import { fetchPost } from '../actions';

class PostsShow extends React.Component {
  componentDidMount() {
    const { id } = this.props.match.params;
    this.props.fetchPost(id);
  }

  render() {
    const { post } = this.props;

    if (!post) {
      return (

Loading ...

      );
    }

    return (

{post.title}

Categories: {post.categories}

{post.content}

    )
  }
}

function mapStateToProps({ posts }, ownProps) {
  return { post: posts[ownProps.match.params.id] };
}

export default connect(mapStateToProps, { fetchPost })(PostsShow);

Add check to see if post exists


'use strict';

const Joi = require('joi');
const Boom = require('boom');

exports.addTicket = {
    description: 'Add new support ticket',
    validate: {
        payload: {
            subject: Joi.string().max(500).allow(''),
            email: Joi.string().max(100).allow(''),
            description: Joi.string().max(5000).allow('')
        },
        failAction: function(request, reply, source, error) {
            // Boom bad request
            return reply.reply(Boom.badRequest(error));
        }
    },
    auth: {
        mode: 'try',
        strategy: 'standard'
    },
    plugins: {
        'hapi-auth-cookie': {
            redirectTo: false
        }
    },
    handler: function(request, reply) {
        var ticket = {
            subject: request.payload.subject,
            email: request.payload.email,
            description: request.payload.description
        };
        console.log(ticket);

        var freshDesk = request.server.plugins.freshdesk;

        freshDesk.newTicket(ticket, function() {
            return reply({
                statusCode: 200,
                message: 'Successfully created ticket.'
            });
        });
    }
};

'use strict';

const Joi = require('joi');
const Boom = require('boom');

exports.addTicket = {
    description: 'Add new support ticket',
    validate: {
        payload: {
            subject: Joi.string().max(500).allow(''),
            email: Joi.string().max(100),
            description: Joi.string().max(5000).allow('')
        },
        failAction: function(request, reply, source, error) {
            // Boom bad request
            return reply.reply(Boom.badRequest(error));
        }
    },
    auth: {
        mode: 'try',
        strategy: 'standard'
    },
    plugins: {
        'hapi-auth-cookie': {
            redirectTo: false
        }
    },
    handler: function(request, reply) {
        var ticket = {
            subject: request.payload.subject,
            email: request.payload.email,
            description: request.payload.description
        };

        var freshDesk = request.server.plugins.freshdesk;

        freshDesk.newTicket(ticket, function() {
            return reply({
                statusCode: 200,
                message: 'Successfully created ticket.'
            });
        });
    }
};

Remove console logging and make email required


let service;
let map;

const MTV = new google.maps.LatLng(37.4220, 122.0841);

function findNearbyPlaces() {
    map = new google.maps.Map(document.getElementById('map'), {
        center: MTV,
        zoom: 15
    });

    let request = {
        location: MTV,
        radius: '5000',
        rankBy: google.maps.places.RankBy.PROMINENCE,
        type: ['restaurant']
    };

    service = new google.maps.places.PlacesService(map);
    service.nearbySearch(request, callback);
}

function callback(results, status) {
    if(status == google.maps.places.PlacesService.OK) {
        for(let i = 0; i < 5; i++) {
            console.log(results[i].name);
        }
    }
}

let service;
let map;

const MTV = new google.maps.LatLng(37.4220, 122.0841);

function findNearbyPlaces() {
    map = new google.maps.Map(document.getElementById('map'), {
        center: MTV,
        zoom: 15
    });

    let request = {
        location: MTV,
        radius: '5000',
        type: ['restaurant']
    };

    service = new google.maps.places.PlacesService(map);
    service.nearbySearch(request, callback);
}

function callback(results, status) {
    if(status == google.maps.places.PlacesService.OK) {
        for(let i = 0; i < 5; i++) {
            console.log(results[i].name);
        }
    }
}

Remove rankBy parameter in request


import React from 'react';

import Icon from '../components/Icon';

class UniversePage extends React.Component {
  render() {
    return this.props.children;
  }
}

UniversePage.routeConfig = {
  label: 'Packages',
  icon: <Icon id="packages-inverse" size="small" family="product" />,
  matches: /^\/universe/
};

module.exports = UniversePage;

import React from 'react';

import Icon from '../components/Icon';

class UniversePage extends React.Component {
  render() {
    return this.props.children;
  }
}

UniversePage.routeConfig = {
  label: 'Universe',
  icon: <Icon id="packages-inverse" size="small" family="product" />,
  matches: /^\/universe/
};

module.exports = UniversePage;

Update 'Packages' sidebar label to 'Universe'


const gulp = require('gulp');
const del = require('del');
const paths = require('vinyl-paths');
const rev = require('gulp-rev');
const through = require('through2');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const addToManifest = require('./revision').addToManifest;

gulp.task('clean-css', () =>
    gulp.src(['./www/css/*-*.css', '!./www/css/bundle-*', 'static/css/*.css*']).pipe(paths(del))
);

gulp.task('sass', ['clean-css'], () =>
    gulp
        .src(['static/css/bot.scss', 'static/css/index.scss'])
        .pipe(sourcemaps.init())
        .pipe(sass().on('error', sass.logError))
        .pipe(rev())
        .pipe(through.obj(addToManifest))
        .pipe(sourcemaps.write('./'))
        .pipe(gulp.dest('./static/css'))
);

gulp.task('static-css', ['sass'], () => gulp.src('static/css/*.css*').pipe(gulp.dest('./www/css')));

gulp.task('static', ['static-css'], () => gulp.src(['static/**', '!static/css/*']).pipe(gulp.dest('./www')));

gulp.task('copy-jquery-img', () => gulp.src('node_modules/jquery-ui-css/images/*').pipe(gulp.dest('www/css/images')));

gulp.task('copy-binary-css', () => gulp.src('node_modules/binary-style/binary.css').pipe(gulp.dest('www/css')));

Copy binary.css from binary-style node_module


const path = require('path');

module.exports = {
  sessionOptions: {
    secret: process.env.SESSION_SECRET_KEY,
    saveUninitialized: true, // save new sessions
    resave: false, // do not automatically write to the session store
    cookie: {
      httpOnly: true,
      maxAge: 2419200000
    } // TODO set secure to true when https is used
  },
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT
  },
  aws: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    accessKeySecret: process.env.AWS_ACCESS_KEY_SECRET,
    bucketName: process.env.AWS_S3_BUCKET_NAME,
    region: process.env.AWS_S3_REGION,
    expirationTime: 300
  },
  postmark: {
    serverKey: process.env.POSTMARK_SECRET_KEY,
    validRecipient: (email) => {
      return (email.split('@').pop() === 'uprise.org'); // only send to @uprise.org email accounts for now
    },
    from: 'notifications@uprise.org',
    contactEmail: 'help@uprise.org'
  },
  urls: {
    api: process.env.SERVER_BASE_URL,
    client: process.env.CLIENT_BASE_URL
  },
  paths: {
    base: path.resolve(__dirname, '..')
  },
  sentry: {
    dsn: process.env.SENTRY_PRIVATE_DSN
  }
}

const path = require('path');

module.exports = {
  sessionOptions: {
    secret: process.env.SESSION_SECRET_KEY,
    saveUninitialized: true, // save new sessions
    resave: false, // do not automatically write to the session store
    cookie: {
      httpOnly: true,
      maxAge: 2419200000
    } // TODO set secure to true when https is used
  },
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT
  },
  aws: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    accessKeySecret: process.env.AWS_ACCESS_KEY_SECRET,
    bucketName: process.env.AWS_S3_BUCKET_NAME,
    region: process.env.AWS_S3_REGION,
    expirationTime: 300
  },
  postmark: {
    serverKey: process.env.POSTMARK_SECRET_KEY,
    validRecipient: (email) => {
      return process.env.NODE_ENV === 'production' || (email.split('@').pop() === 'uprise.org'); // only send to @uprise.org email accounts for non production
    },
    from: 'notifications@uprise.org',
    contactEmail: 'help@uprise.org'
  },
  urls: {
    api: process.env.SERVER_BASE_URL,
    client: process.env.CLIENT_BASE_URL
  },
  paths: {
    base: path.resolve(__dirname, '..')
  },
  sentry: {
    dsn: process.env.SENTRY_PRIVATE_DSN
  }
}

Send emails to non @uprise.org accounts in production settings.


// @flow
import Raven from 'raven-js';

function addUserData(data) {
  if (!window.champaign) return;
  if (!window.champaign.personalization) return;
  const { location, member } = window.champaign.personalization;
  data.user = {
    id: member ? member.id : undefined,
    username: member ? member.name : undefined,
    ip: location ? location.ip : undefined,
  };
}

function addReduxState(data) {
  if (window.champaign && window.champaign.store) {
    data.extra = {
      ...data.extra,
      reduxState: window.champaign.store.getState(),
    };
  }
}

Raven.config(process.env.SENTRY_DSN, {
  release: process.env.CIRCLE_SHA1,
  environment: process.env.SENTRY_ENVIRONMENT || 'development',
  dataCallback: data => {
    addUserData(data);
    addReduxState(data);
    return data;
  },
}).install();

window.Raven = Raven;

// @flow
import Raven from 'raven-js';

function addUserData(data) {
  if (!window.champaign) return;
  if (!window.champaign.personalization) return;
  const { location, member } = window.champaign.personalization;
  data.user = {
    id: member ? member.id : undefined,
    username: member ? member.name : undefined,
    ip: location ? location.ip : undefined,
  };
}

function addReduxState(data) {
  if (window.champaign && window.champaign.store) {
    data.extra = {
      ...data.extra,
      reduxState: window.champaign.store.getState(),
    };
  }
}

Raven.config(process.env.SENTRY_DSN, {
  release: new Date().toISOString(),
  environment: process.env.SENTRY_ENVIRONMENT || 'development',
  dataCallback: data => {
    addUserData(data);
    addReduxState(data);
    return data;
  },
}).install();

window.Raven = Raven;

Set ISO date as the Sentry release tag


import _extend from 'lodash-es/extend';

import { geoExtent } from '../geo';

export function osmNote() {
    if (!(this instanceof osmNote)) {
        return (new osmNote()).initialize(arguments);
    } else if (arguments.length) {
        this.initialize(arguments);
    }
}

osmNote.id = function() {
    return osmNote.id.next--;
};

osmNote.id.next = -1;

_extend(osmNote.prototype, {

    type: 'note',

    initialize: function(sources) {
        for (var i = 0; i < sources.length; ++i) {
            var source = sources[i];
            for (var prop in source) {
                if (Object.prototype.hasOwnProperty.call(source, prop)) {
                    if (source[prop] === undefined) {
                        delete this[prop];
                    } else {
                        this[prop] = source[prop];
                    }
                }
            }
        }

        if (!this.id) {
            this.id = osmNote.id();
        }

        return this;
    },

    extent: function() {
        return new geoExtent(this.loc);
    },

    getID: function() {
        return this.id;
    },

    getType: function() {
        return this.type;
    },

    getComments: function() {
        return this.comments;
    }
});

import _extend from 'lodash-es/extend';

import { geoExtent } from '../geo';

export function osmNote() {
    if (!(this instanceof osmNote)) {
        return (new osmNote()).initialize(arguments);
    } else if (arguments.length) {
        this.initialize(arguments);
    }
}

osmNote.id = function() {
    return osmNote.id.next--;
};

osmNote.id.next = -1;

_extend(osmNote.prototype, {

    type: 'note',

    initialize: function(sources) {
        for (var i = 0; i < sources.length; ++i) {
            var source = sources[i];
            for (var prop in source) {
                if (Object.prototype.hasOwnProperty.call(source, prop)) {
                    if (source[prop] === undefined) {
                        delete this[prop];
                    } else {
                        this[prop] = source[prop];
                    }
                }
            }
        }

        if (!this.id) {
            this.id = osmNote.id();
        }

        return this;
    },

    extent: function() {
        return new geoExtent(this.loc);
    },

    update: function(attrs) {
        return osmNote(this, attrs, {v: 1 + (this.v || 0)});
    }
});

Add update method, remove getters.. This makes the osmNote work a bit more like other osm objects in iD.


'use strict';

var FormManager = require('./formmanager');
var Rules = require('./rules');
var TableController = require('./table');
var translate = require('./translate');

var rulesContainer;
var deleteRule;

var addButton = document.querySelector('#add-button');

addButton.addEventListener('click', () => FormManager.show('add'));

FormManager.on('add', (rule) => rulesContainer.add(rule));
FormManager.on('save', (rule) => rulesContainer.edit(deleteRule, rule));
FormManager.on('hide', () => addButton.classList.remove('hidden'));
FormManager.on('show', () => addButton.classList.add('hidden'));

TableController.on('remove', (number) => rulesContainer.remove(number));

TableController.on('click', (number) => {
    deleteRule = number;
    FormManager.fill(rulesContainer.storage.rules[number]);
    FormManager.show('edit');
});

window.addEventListener('load', () => {
    translate.translateHTML();

    chrome.storage.sync.get('rules', obj => {
        rulesContainer = new Rules(obj.rules || []);

        TableController.table(rulesContainer.storage.rules);

        if (rulesContainer.storage.rules.length === 0) {
            addButton.click();
        }

        chrome.storage.onChanged.addListener(() => {
            TableController.table(rulesContainer.storage.rules);
        });
    });
});

'use strict';

var FormManager = require('./formmanager');
var Rules = require('./rules');
var TableController = require('./table');
var translate = require('./translate');

var rulesContainer;
var currentRule;

var addButton = document.querySelector('#add-button');

addButton.addEventListener('click', () => FormManager.show('add'));

FormManager.on('add', (rule) => rulesContainer.add(rule));
FormManager.on('save', (rule) => rulesContainer.edit(currentRule, rule));
FormManager.on('hide', () => addButton.classList.remove('hidden'));
FormManager.on('show', () => addButton.classList.add('hidden'));

TableController.on('remove', (number) => rulesContainer.remove(number));

TableController.on('click', (number) => {
    currentRule = number;
    FormManager.fill(rulesContainer.storage.rules[number]);
    FormManager.show('edit');
});

window.addEventListener('load', () => {
    translate.translateHTML();

    chrome.storage.sync.get('rules', obj => {
        rulesContainer = new Rules(obj.rules || []);

        TableController.table(rulesContainer.storage.rules);

        if (rulesContainer.storage.rules.length === 0) {
            addButton.click();
        }

        chrome.storage.onChanged.addListener(() => {
            TableController.table(rulesContainer.storage.rules);
        });
    });
});

Rename deleteRule variable to currentRule


import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchWeather } from '../actions/index';

class SearchBar extends Component {
  constructor(props) {
    super(props);

    this.state = { term: '' };

    this.onInputChange = this.onInputChange.bind(this);
    this.onFormSubmit = this.onFormSubmit.bind(this);
  }

  onInputChange(event) {
    console.log(event.target.value);
    this.setState({term: event.target.value})
  }

  onFormSubmit(event) {
    e.preventDefault();

    // need to fetch weather data
    this.props.fetchWeather(this.state.term);
    this.setState({ term: '' });
  }

  render() {
    return (
      <form onSubmit={this.onFormSubmit} className="input-group">
        <input
          placeholder="Get a five day forecast in your favorite cities"
          className="form-control"
          value={this.state.term}
          onChange={this.onInputChange}
          />
        <span className="input-group-btn">
          <button type="submit" className="btn btn-secondary">Submit</button>
        </span>
      </form>
    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ fetchWeather }, dispatch);
}

export default connect(null, mapDispatchToProps)(SearchBar);

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchWeather } from '../actions/index';

class SearchBar extends Component {
  constructor(props) {
    super(props);

    this.state = { term: '' };

    this.onInputChange = this.onInputChange.bind(this);
    this.onFormSubmit = this.onFormSubmit.bind(this);
  }

  onInputChange(event) {
    console.log(event.target.value);
    this.setState({term: event.target.value})
  }

  onFormSubmit(event) {
    event.preventDefault();

    // need to fetch weather data
    this.props.fetchWeather(this.state.term);
    this.setState({ term: '' });
  }

  render() {
    return (
      <form onSubmit={this.onFormSubmit} className="input-group">
        <input
          placeholder="Get a five day forecast in your favorite cities"
          className="form-control"
          value={this.state.term}
          onChange={this.onInputChange}
          />
        <span className="input-group-btn">
          <button type="submit" className="btn btn-secondary">Submit</button>
        </span>
      </form>
    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({ fetchWeather }, dispatch);
}

export default connect(null, mapDispatchToProps)(SearchBar);

Use event instead of e


require.config({
  baseUrl: window.baseSiteUrl + 'Scripts/post-mortem/',
  paths: {
    'underscore': 'libs/underscore/underscore-debug',
    'backbone': 'libs/backbone/backbone-debug',
    'jquery': 'libs/jquery/2.2.3/jquery-debug',
    'velocity': 'libs/velocity/velocity',
    'date-format': 'libs/date-format/date.format',
    'text': 'libs/text/text',

    'jquery.ui.tagit': 'libs/tag-it/tag-it-debug',
    //'jquery.ui.datepicker': 'libs/jquery-ui/datepicker',
    'jquery.ui.widget': 'libs/jquery-ui/widget',
    //'jquery.ui.slider': 'libs/jquery-ui/slider',
    //'jquery.ui.mouse': 'libs/jquery-ui/mouse',
    //'jquery.ui.keycode': 'libs/jquery-ui/keycode',
    //'jquery.ui.version': 'libs/jquery-ui/version',

  },

  shim: {
    'jquery.ui.tagit': ['jquery.ui.widget', 'jquery.ui.version'],
    'jquery.ui.mouse': ['jquery.ui.widget', 'jquery.ui.version'],
    //'jquery.ui.slider': ['jquery', 'jquery.ui.mouse', 'jquery.ui.keycode', 'jquery.ui.version', 'jquery.ui.widget']
  }

});

require(['jquery', 'src/app'], function ($, App) {
  // Avoid jQuery conflict when import multiple version.
  // Use jQuery import by requirejs.
  var j = jQuery.noConflict(true);

  App.initialize();
});

require.config({
  baseUrl: window.baseSiteUrl + 'Scripts/post-mortem/',
  paths: {
    'underscore': 'libs/underscore/underscore-debug',
    'backbone': 'libs/backbone/backbone-debug',
    'jquery': 'libs/jquery/2.2.3/jquery-debug',
    'velocity': 'libs/velocity/velocity',
    'date-format': 'libs/date-format/date.format',
    'text': 'libs/text/text',

    'jquery.ui.tagit': 'libs/tag-it/tag-it-debug',
    'jquery.ui.widget': 'libs/jquery-ui/widget',
  },

  shim: {
    'jquery.ui.tagit': ['jquery.ui.widget'],
  }

});

require(['jquery', 'src/app'], function ($, App) {
  // Avoid jQuery conflict when import multiple version.
  // Use jQuery import by requirejs.
  var j = jQuery.noConflict(true);

  App.initialize();
});

Remove path config of not used dependencies


// We don't want everyone to be synchronized, so add a random amount to the interval
const NEW_VERSION_INTERVAL = (10 + Math.random() * 5) * 60 * 1000;
module.exports = ['$interval', '$http', '$rootScope', ($interval, $http, $rootScope) => {
  let ignoreNewVersion = false;

  $rootScope.ignoreNewVersion = () => {
    ignoreNewVersion = true;
    $rootScope.newVersionAvailable = false;
    $interval.cancel(versionCheck);
  };

  let versionCheck = $interval(() => {
    $http
      .get('currentVersion?' + new Date().getTime())
        .success(version => {
          // clVersion is generate at build time and injected into index.html
          // it is a global value!
          $rootScope.newVersionAvailable = !ignoreNewVersion && clVersion != version;
        })
        .error(error => {

        });
  }, NEW_VERSION_INTERVAL);
}];

// We don't want everyone to be synchronized, so add a random amount to the interval
const NEW_VERSION_INTERVAL = (10 + Math.random() * 5) * 60 * 1000;
module.exports = ['$interval', '$http', '$rootScope', ($interval, $http, $rootScope) => {
  let ignoreNewVersion = false;

  $rootScope.ignoreNewVersion = () => {
    ignoreNewVersion = true;
    $rootScope.newVersionAvailable = false;
    $interval.cancel(versionCheck);
  };

  let versionCheck = $interval(checkForNewVersion, NEW_VERSION_INTERVAL);

  function checkForNewVersion() {
    $http
      .get('currentVersion?' + new Date().getTime())
        .success(version => {
          // clVersion is generate at build time and injected into index.html
          // it is a global value!
          $rootScope.newVersionAvailable = !ignoreNewVersion && clVersion != version;
        })
        .error(error => {

        });
  }
}];

Check for new version on load. We should probably do this even earlier and force reload immediately.


import Component from '@ember/component';
import Metrics from 'shared/mixins/metrics';
import layout from './template';

export default Component.extend(Metrics, {
  layout,

  filters: { resourceType: 'istioproject' },

  projectScope:  true,
  istio:        true,
});

import Component from '@ember/component';
import Metrics from 'shared/mixins/metrics';
import layout from './template';
import { observer, setProperties } from '@ember/object';
import { inject as service } from '@ember/service';

export default Component.extend(Metrics, {
  scope: service(),
  layout,

  filters: { resourceType: 'istioproject' },

  projectScope:  true,
  istio:        true,

  projectDidChange: observer('scope.currentProject.id', function() {
    setProperties(this, {
      'state.noGraphs': true,
      graphs:           [],
      single:           []
    });

    this.send('query', false);
  })
});

Fix the issue that switch project doesn't update the metrics


const justFocus = (_, { id, ...props }) =>
  document.getElementById(props.id).focus(props)

const justBlur = (_, id) => document.getElementById(id).blur()

export const focus = (id, { preventScroll } = {}) => [
  justFocus,
  { id, preventScroll },
]

export const blur = (id) => [justBlur, id]

const justFocus = (_, { id, ...props }) =>
  document.getElementById(id).focus(props)

const justBlur = (_, id) => document.getElementById(id).blur()

export const focus = (id, { preventScroll } = {}) => [
  justFocus,
  { id, preventScroll },
]

export const blur = (id) => [justBlur, id]

Fix bug in focus effect accessing null prop


module.exports = function linkListShortCode (data) {
    return `
${data.host == undefined ? '' : `<h2>${data.host}</h2>`}
<ol reversed>
${data.list.map(entry =>
    `<li>
        ${entry.replaced == undefined ? '' : `<span style="text-decoration:line-through;">${entry.replaced}</span> `}
        ${entry.url == undefined ? `${entry.title}` : `<a href="${entry.url}/">${entry.title}</a>`}
    </li>`
).join('\n')}
</ol>`;
}

module.exports = function linkListShortCode (data) {
    return `
${data.host == undefined ? '' : `<h2>${data.host}</h2>`}
<ol reversed>
${data.list.map(entry =>
    `<li>
        ${entry.replaced == undefined ? '' : `<span style="text-decoration:line-through;">${entry.replaced}</span> `}
        ${entry.url == undefined ? `${entry.title}` : `<a href="${entry.url}/">${entry.title}</a> `}
        ${entry.comment == undefined ? '' : ` - ${entry.comment}`}
    </li>`
).join('\n')}
</ol>`;
}

Add comment to link list shortcode


import * as types from '../constants/actionTypes';
import * as dashboardActions from '../actions/dashboardActions';
import { takeEvery, takeLatest } from 'redux-saga';
import { take, put, call, fork, select } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import * as sidebarIndexTypes from '../constants/sidebarIndexTypes';

function* updateURL(action) {
  const { slug } = action;
  yield put(push('/dashboard/${slug}'));
}

function* updateSidebar(action) {
  const { sidebarIndex } = action;
  switch (sidebarIndex) {
    case sidebarIndexTypes.HOME:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.HOME));
      break;
    case sidebarIndexTypes.MESSAGES:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.MESSAGES));
      break;
    default:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.HOME));
  }
}

export function* dashboardMessagesFlow() {
  yield takeEvery(types.SHOW_MESSAGES, updateURL);
}

export function* dashboardHomeFlow() {
  yield takeEvery(types.SHOW_HOME, updateURL);
}

export function* updateSidebarFromURL() {
  yield takeEvery(types.DASHBOARD_SIDEBAR_CHANGED, updateSidebar);
}

import * as types from '../constants/actionTypes';
import * as dashboardActions from '../actions/dashboardActions';
import { takeEvery, takeLatest } from 'redux-saga';
import { take, put, call, fork, select } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import * as sidebarIndexTypes from '../constants/sidebarIndexTypes';

function* updateURL(action) {
  const { slug } = action;
  yield put(push(`/dashboard/${slug}`));
}

function* updateSidebar(action) {
  const { sidebarIndex } = action;
  switch (sidebarIndex) {
    case sidebarIndexTypes.HOME:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.HOME));
      break;
    case sidebarIndexTypes.MESSAGES:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.MESSAGES));
      break;
    default:
      yield put(dashboardActions.pageSwitcher(sidebarIndexTypes.HOME));
  }
}

export function* dashboardMessagesFlow() {
  yield takeEvery(types.SHOW_MESSAGES, updateURL);
}

export function* dashboardHomeFlow() {
  yield takeEvery(types.SHOW_HOME, updateURL);
}

export function* updateSidebarFromURL() {
  yield takeEvery(types.DASHBOARD_SIDEBAR_CHANGED, updateSidebar);
}

Fix string interpolation in dashboard.


module.exports = function(access) {
    'use strict';

    let _ = require('lodash');
    let projectAccessCache = {};
    return {
        find: find,
        clear: clear,
        validateAccess: validateAccess
    };

    // find looks up a given project in the project cache.
    // If the projectAccessCache hasn't been loaded it loads the
    // cache.
    function *find(project_id) {
        if (! _.isEmpty(projectAccessCache)) {
            return projectAccessCache[project_id];
        }

        projectAccessCache = yield access.allByProject();
        return projectAccessCache[project_id];
    }

    // clear will clear the current project cache. This is useful
    // when project permissions have been updated or a new project
    // has been created.
    function clear() {
        projectAccessCache = {};
    }

    // validateAccess checks if the user has access to the
    // given project. This method assumes that find was called
    // first so that the projectAccessCache was preloaded. If the
    // projectAccessCache is empty then it returns false (no access).
    function validateAccess(project_id, user) {
        if (_.isEmpty(projectAccessCache)) {
            return false;
        }
        if (!(project_id in projectAccessCache)) {
            return false;
        }
        let index = _.indexOf(projectAccessCache[project_id], function(a) {
            return a.user_id == user.id;
        });

        // when index !== -1 we found the given user in the project.
        return index !== -1;
    }
};

module.exports = function(access) {
    'use strict';

    let _ = require('lodash');
    let projectAccessCache = {};
    let adminUsersCache = {};
    return {
        find: find,
        clear: clear,
        validateAccess: validateAccess
    };

    // find looks up a given project in the project cache.
    // If the projectAccessCache hasn't been loaded it loads the
    // cache.
    function *find(project_id) {
        if (_.isEmpty(adminUsersCache)) {
            let adminUsers = yield access.adminUsers();
            adminUsers.forEach(function(user) {
                adminUsersCache[user.id] = user;
            });
        }

        if (! _.isEmpty(projectAccessCache)) {
            return projectAccessCache[project_id];
        }

        projectAccessCache = yield access.allByProject();
        return projectAccessCache[project_id];
    }

    // clear will clear the current project cache. This is useful
    // when project permissions have been updated or a new project
    // has been created.
    function clear() {
        projectAccessCache = {};
    }

    // validateAccess checks if the user has access to the
    // given project. This method assumes that find was called
    // first so that the projectAccessCache was preloaded. If the
    // projectAccessCache is empty then it returns false (no access).
    function validateAccess(project_id, user) {
        if (user.id in adminUsersCache) {
            return true;
        }

        if (_.isEmpty(projectAccessCache)) {
            return false;
        }

        if (!(project_id in projectAccessCache)) {
            return false;
        }
        let index = _.indexOf(projectAccessCache[project_id], function(a) {
            return a.user_id == user.id;
        });

        // when index !== -1 we found the given user in the project.
        return index !== -1;
    }
};

Load and test for admin users.


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

const apickli = require("apickli");
const { Before: before } = require("cucumber");

before(function () {
  this.apickli = new apickli.Apickli(
    "https",
    process.env.APIGEE_ORG +
    "-" +
    process.env.APIGEE_ENV +
    ".apigee.net" +
    process.env.PROXY_BASE_PATH || '/js-callout/v1'
  );

  this.apickli.addRequestHeader("Cache-Control", "no-cache");
});

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

const apickli = require("apickli");
const { Before: before } = require("cucumber");

before(function () {
  this.apickli = new apickli.Apickli(
    "https",
    process.env.APIGEE_ORG +
    "-" +
    process.env.APIGEE_ENV +
    ".apigee.net/js-callout/v1"
  );

  this.apickli.addRequestHeader("Cache-Control", "no-cache");
});

Remove configurable Base Path for js callout proxy


'use strict';

import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);
let expect = chai.expect;

describe('Board page (desktop)', () => {

    before(() => {
        browser.driver.manage().window().setSize(1280, 768);
    });

    beforeEach(() => {
        browser.get('/');
    });

    describe('static content', () => {
        it('should have right page title', () => {
            expect(browser.getTitle()).to.eventually.equal('BoardOfShame Список мошенников');
        });

        it('should have right h1 caption', () => {
            expect(element(by.css('.board-caption')).getText()).to.eventually.equal('Поиск');
        });

        it('should have right active menu item', () => {
            expect(element(by.css('.navbar .active')).getText()).to.eventually.equal('Список');
        });
    });

    describe('board body', () => {
        it('should contain a lot of board items', () => {
            expect(element.all(by.css('.board-item')).count()).to.eventually.be.above(50);
        });
    });

});

'use strict';

import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);
let expect = chai.expect;

describe('Board page (desktop)', () => {

    before(() => {
        browser.driver.manage().window().setSize(1280, 768);
    });

    beforeEach(() => {
        browser.get('/');
    });

    describe('static content', () => {
        it('should have right page title', () => {
            expect(browser.getTitle()).to.eventually.equal('BoardOfShame Список мошенников');
        });

        it('should have right h1 caption', () => {
            expect(element(by.css('.board-caption')).getText()).to.eventually.equal('Поиск');
        });

        it('should have right active menu item', () => {
            expect(element(by.css('.navbar .active')).getText()).to.eventually.equal('Список');
        });
    });

    describe('board body', () => {
        it('should contain 11 board items (test table)', () => {
            expect(element.all(by.css('.board-item')).count()).to.eventually.equal(11);
        });
    });

    describe('board footer info', () => {
        it('should contain right info about items', () => {
            expect(element(by.css('.board-footer')).getText()).to.eventually.equal('Всего найдено: 11');
        });
    });

});

Add tests for board body and footer


import { HTMLExporter, DefaultDOMElement } from 'substance'

export default class ArticleHTMLExporter extends HTMLExporter {
  constructor (articleConfig) {
    super({
      converters: articleConfig.getConverters('html'),
      idAttribute: 'data-id',
      elementFactory: DefaultDOMElement.createDocument('html')
    })
  }

  /*
    Customised annotatedText method, that takes a document and $$ to be
    compatible with other rendering contexts
  */
  annotatedText (path, doc, $$) {
    if (doc) {
      this.state.doc = doc
    }
    if ($$) {
      this.$$ = $$
    }
    return super.annotatedText(path)
  }
}

import { HTMLExporter, DefaultDOMElement } from 'substance'

export default class ArticleHTMLExporter extends HTMLExporter {
  constructor (articleConfig) {
    super({
      converters: articleConfig.getConverters('html'),
      idAttribute: 'data-id',
      elementFactory: DefaultDOMElement.createDocument('html')
    })
  }

  /*
    Customised annotatedText method, that takes a document and $$ to be
    compatible with other rendering contexts
  */
  annotatedText (path, doc, $$) {
    if (doc) {
      this.state.doc = doc
    }
    if ($$) {
      this.$$ = $$
    }
    return super.annotatedText(path)
  }

  getDefaultBlockConverter () {
    return defaultBlockConverter // eslint-disable-line no-use-before-define
  }
}

// TODO: the default converter is used for nodes that do not have a converter registered
const defaultBlockConverter = {
  export: function (node, el, converter) {
    el.attr('data-type', node.type)
    const nodeSchema = node.getSchema()
    for (let prop of nodeSchema) {
      const name = prop.name
      if (name === 'id' || name === 'type') continue
      // using RDFa like attributes
      let propEl = converter.$$('div').attr('property', name)
      let value = node[name]
      if (prop.isText()) {
        propEl.append(converter.annotatedText([node.id, name]))
      } else if (prop.isReference()) {
        if (prop.isOwned()) {
          value = node.resolve(name)
          if (prop.isArray()) {
            propEl.append(value.map(child => converter.convertNode(child)))
          } else {
            propEl.append(converter.convertNode(value))
          }
        } else {
          // TODO: what to do with relations? maybe create a link pointing to the real one?
          // or render a label of the other
          // For now, we skip such props
          continue
        }
      } else {
        propEl.append(value)
      }
      el.append(propEl)
    }
  }
}

Improve default HTML block node converter.


'use strict';

import React, {PropTypes} from 'react';
import Comment from './Comment.component.js';
import {isEmpty, take} from 'lodash';

class CommentsContainerComponent extends React.Component {
  constructor() {
    super();
    this.toggleExpand = this.toggleExpand.bind(this);
    this.state = {isExpanded: true};
  }

  toggleExpand() {
    this.setState({isExpanded: !this.state.isExpanded});
  }

  render() {

    const expandButtonText = this.state.isExpanded ? 'Se flere kommentarer' : 'Vis færre kommentarer';

    const allComments = this.props.comments;

    const visibleComments = this.state.isExpanded ? take(allComments, 2) : allComments;

    const comments = !(isEmpty(visibleComments)) ? visibleComments.map((val) => {
      return (
        <Comment
          commentContent={val.content}
          key={val.id}
          ownerEmail={val.owner.email}
          ownerImageUrl={val.owner.imageUrl ? val.owner.imageUrl : '/dummy.jpg'}
          />
      );
    }) : (
);

    return (

{comments.length} Kommentarer

        <button className='tiny' onClick={this.toggleExpand}>{expandButtonText}</button>

        {comments}

    );
  }
}

CommentsContainerComponent.displayName = 'CommentsContainer.component';
CommentsContainerComponent.propTypes = {
  comments: PropTypes.array.isRequired
};

export default CommentsContainerComponent;

'use strict';

import React, {PropTypes} from 'react';
import Comment from './Comment.component.js';
import {isEmpty, take} from 'lodash';

class CommentsContainerComponent extends React.Component {
  constructor() {
    super();
    this.toggleExpand = this.toggleExpand.bind(this);
    this.state = {isExpanded: true};
  }

  toggleExpand() {
    this.setState({isExpanded: !this.state.isExpanded});
  }

  render() {

    const expandButtonText = this.state.isExpanded ? 'Se flere kommentarer' : 'Vis færre kommentarer';

    const allComments = this.props.comments;

    const visibleComments = this.state.isExpanded ? take(allComments, 2) : allComments;

    const comments = !(isEmpty(visibleComments)) ? visibleComments.map((val) => {
      return (
        <Comment
          commentContent={val.content}
          key={val.id}
          ownerEmail={val.owner.email}
          ownerImageUrl={val.owner.imageUrl ? val.owner.imageUrl : '/dummy.jpg'}
          />
      );
    }) : (
);

    return (

{allComments.length} Kommentarer

        <button className='tiny' onClick={this.toggleExpand}>{expandButtonText}</button>

        {comments}

    );
  }
}

CommentsContainerComponent.displayName = 'CommentsContainer.component';
CommentsContainerComponent.propTypes = {
  comments: PropTypes.array.isRequired
};

export default CommentsContainerComponent;

Change comment counter in CommentsContainer


import React from 'react'
import {
  Image,
  Text,
  View,
} from 'react-native'
import { connect } from 'react-redux'

class ProfileScreen extends React.Component {
  render() {
    return (
      <View style={{ alignItems: 'center' }}>
        <Image
          source={{ uri: this.props.profile.picture.data.url }}
          style={{ width: 100, height: 100, borderRadius: 50 }}
        />
        <Text style={{ fontSize: 20 }}>{this.props.profile.name}</Text>
        <Text>ID: {this.props.profile.id}</Text>
      </View>
    )
  }
}

const mapStateToProps = state => {
  return {
    profile: state.profile,
  }
}

export default connect(
  mapStateToProps,
)(ProfileScreen)

import React from 'react'
import {
  Image,
  Text,
  View,
} from 'react-native'
import { inject, observer } from 'mobx-react'

@inject('profileStore')
@observer
export default class ProfileScreen extends React.Component {
  render() {
    return (
      <View style={{ alignItems: 'center' }}>
        <Image
          source={{ uri: this.props.profileStore.profile.picture.data.url }}
          style={{ width: 100, height: 100, borderRadius: 50 }}
        />
        <Text style={{ fontSize: 20 }}>{this.props.profileStore.profile.name}</Text>
        <Text>ID: {this.props.profileStore.profile.id}</Text>
      </View>
    )
  }
}

Replace state connection by store injection and observer decorator


var Joi = require('joi');

var VALID_MODELS = ['room', 'thing', 'device'];

exports.ObjectId = Joi.string().regex(/^[a-zA-Z0-9\-_]+$/);
exports.UserRecord = Joi.object().keys({id: exports.ObjectId}).unknown(true);
exports.ModelName = Joi.string().valid(VALID_MODELS);

var Joi = require('joi');

var VALID_MODELS = ['room', 'thing', 'device', 'channel'];

exports.ObjectId = Joi.string().regex(/^[a-zA-Z0-9\-_]+$/);
exports.UserRecord = Joi.object().keys({id: exports.ObjectId}).unknown(true);
exports.ModelName = Joi.string().valid(VALID_MODELS);

Add channel as a separate sync-able object model


export const PROJECT_INFO_INITIAL_STATE = {};
export const PROJECT_PREFERENCES_INITIAL_STATE = {
  filters: [],
};
export const PROJECT_ATTRIBUTES_DELIMITER = '$';

export const FETCH_PROJECT_SUCCESS = 'fetchProjectSuccess';
export const FETCH_PROJECT_PREFERENCES_SUCCESS = 'fetchProjectPreferencesSuccess';
export const UPDATE_CONFIGURATION_ATTRIBUTES = 'updateConfigurationAttributes';
export const UPDATE_NOTIFICATIONS_CONFIG = 'updateNotificationsConfig';
export const UPDATE_NOTIFICATIONS_CONFIG_SUCCESS = 'updateNotificationsConfigSuccess';
export const UPDATE_DEFECT_SUBTYPE = 'updateDefectSubType';
export const UPDATE_DEFECT_SUBTYPE_SUCCESS = 'updateDefectSubTypeSuccess';
export const ADD_DEFECT_SUBTYPE = 'addDefectSubType';
export const ADD_DEFECT_SUBTYPE_SUCCESS = 'addDefectSubTypeSuccess';
export const DELETE_DEFECT_SUBTYPE = 'deleteDefectSubType';
export const DELETE_DEFECT_SUBTYPE_SUCCESS = 'deleteDefectSubTypeSuccess';

export const ANALYZER_ATTRIBUTE_PREFIX = 'analyzer';
export const JOB_ATTRIBUTE_PREFIX = 'job';

export const PROJECT_INFO_INITIAL_STATE = {};
export const PROJECT_PREFERENCES_INITIAL_STATE = {
  filters: [],
};
export const PROJECT_ATTRIBUTES_DELIMITER = '.';

export const FETCH_PROJECT_SUCCESS = 'fetchProjectSuccess';
export const FETCH_PROJECT_PREFERENCES_SUCCESS = 'fetchProjectPreferencesSuccess';
export const UPDATE_CONFIGURATION_ATTRIBUTES = 'updateConfigurationAttributes';
export const UPDATE_NOTIFICATIONS_CONFIG = 'updateNotificationsConfig';
export const UPDATE_NOTIFICATIONS_CONFIG_SUCCESS = 'updateNotificationsConfigSuccess';
export const UPDATE_DEFECT_SUBTYPE = 'updateDefectSubType';
export const UPDATE_DEFECT_SUBTYPE_SUCCESS = 'updateDefectSubTypeSuccess';
export const ADD_DEFECT_SUBTYPE = 'addDefectSubType';
export const ADD_DEFECT_SUBTYPE_SUCCESS = 'addDefectSubTypeSuccess';
export const DELETE_DEFECT_SUBTYPE = 'deleteDefectSubType';
export const DELETE_DEFECT_SUBTYPE_SUCCESS = 'deleteDefectSubTypeSuccess';

export const ANALYZER_ATTRIBUTE_PREFIX = 'analyzer';
export const JOB_ATTRIBUTE_PREFIX = 'job';

Revert project attributes delimiter to dot


const Boom = require('boom');
const bookshelf = require('../lib/bookshelf');

exports.register = function (server, pluginOptions, next) {
  server.auth.scheme('email_token', (server, schemeOptions) => {
    return {
      async authenticate(request, reply) {
        try {
          var token = await schemeOptions.findToken(request.query.token);
          var user = token ? await schemeOptions.findUser(token.get('user_id')) : null;
        } catch(e) {
          reply(e);
        }

        if (token && user) {
          reply.continue({
            credentials: {
              user,
              token,
            }
          });
        } else {
          reply(Boom.forbidden());
        }
      }

    };
  });
  next();
};

exports.register.attributes = {
  name: 'email-token-scheme',
  version: '1.0.0'
};

const Boom = require('boom');
const bookshelf = require('../lib/bookshelf');

exports.register = function (server, pluginOptions, next) {
  server.auth.scheme('email_token', (server, schemeOptions) => {
    return {
      async authenticate(request, reply) {
        try {
          var token = await schemeOptions.findToken(request.query.token);
          if (token.name === 'TypeError') {
            reply(Boom.forbidden())
          }
          var user = token ? await schemeOptions.findUser(token.get('user_id')) : null;
        } catch(e) {
          reply(e);
        }

        if (token && user) {
          reply.continue({
            credentials: {
              user,
              token,
            }
          });
        } else {
          reply(Boom.forbidden());
        }
      }

    };
  });
  next();
};

exports.register.attributes = {
  name: 'email-token-scheme',
  version: '1.0.0'
};

Check if token is an error


import { GraphQLObjectType, GraphQLString } from 'graphql';

function resolveUrl(field) {
  return async function({ url }, args, { loaders }) {
    const urlEntry = await loaders.urlLoader.load(url);
    return urlEntry[field];
  };
}

const Hyperlink = new GraphQLObjectType({
  name: 'Hyperlink',
  description: 'Data behind a hyperlink',
  fields: {
    url: { type: GraphQLString },
    title: { type: GraphQLString },
    summary: { type: GraphQLString },
    topImageUrl: { type: GraphQLString, resolve: resolveUrl('topImageUrl') },
    fetchedAt: { type: GraphQLString, resolve: resolveUrl('fetchedAt') },
    status: { type: GraphQLString, resolve: resolveUrl('status') },
  },
});

export default Hyperlink;

import { GraphQLObjectType, GraphQLString } from 'graphql';

function resolveUrl(field) {
  return async function({ url }, args, { loaders }) {
    const urlEntry = await loaders.urlLoader.load(url);
    return urlEntry[field];
  };
}

const Hyperlink = new GraphQLObjectType({
  name: 'Hyperlink',
  description: 'Data behind a hyperlink',
  fields: {
    url: { type: GraphQLString },
    title: { type: GraphQLString },
    summary: { type: GraphQLString },
    topImageUrl: { type: GraphQLString, resolve: resolveUrl('topImageUrl') },
    fetchedAt: { type: GraphQLString, resolve: resolveUrl('fetchedAt') },
    status: { type: GraphQLString, resolve: resolveUrl('status') },
    error: { type: GraphQLString, resolve: resolveUrl('error') },
  },
});

export default Hyperlink;

Add error field to reveal fetch error


'use strict';

// Conditionally load development services if run from CLI
if (
  process.defaultApp
  && process.env.NODE_ENV !== 'production'
  && process.env.NODE_ENV !== 'test'
) {
  // active long stack trace
  require('trace'); // eslint-disable-line global-require
  const chain = require('stack-chain'); // eslint-disable-line global-require
  const { sep } = require('path'); // eslint-disable-line global-require

  // There is no limit for the size of the stack trace (v8 default is 10)
  Error.stackTraceLimit = Infinity;

  chain.filter.attach((error, frames) => {
    return frames.filter((callSite) => {
      const name = callSite && callSite.getFileName();
      const include = !(
        !name
        || name.indexOf(sep) === -1
        || name.match(/internal\//)
        || name.match(/tick/)
        || name.match(/electron-preb/)
      );
      return include;
    });
  });

  /**
  * Load electron-debug, which loads devtron automatically.
  *
  * {@link https://github.com/sindresorhus/electron-debug#usage}
  * {@link https://github.com/electron/devtron}
  */
  require('electron-debug')({ showDevTools: true }); // eslint-disable-line global-require
}

'use strict';

// Conditionally load development services if run from CLI
if (
  process.defaultApp
  && process.env.NODE_ENV !== 'production'
  && process.env.NODE_ENV !== 'test'
) {
  // active long stack trace
  require('trace'); // eslint-disable-line global-require
  const chain = require('stack-chain'); // eslint-disable-line global-require
  const { sep } = require('path'); // eslint-disable-line global-require

  // There is no limit for the size of the stack trace (v8 default is 10)
  Error.stackTraceLimit = Infinity;

  chain.filter.attach((error, frames) => {
    return frames.filter((callSite) => {
      const name = callSite && callSite.getFileName();
      const include = !(
        !name
        || name.indexOf(sep) === -1
        || name.match(/internal\//)
        || name.match(/tick/)
        || name.match(/electron-preb/)
      );
      return include;
    });
  });

  /**
  * Load electron-debug, which loads devtron automatically.
  *
  * {@link https://github.com/sindresorhus/electron-debug#usage}
  * {@link https://github.com/electron/devtron}
  */
  require('electron-debug')({ showDevTools: true, devToolsMode: 'previous' }); // eslint-disable-line global-require
}

Save previously used dev tools dock config


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

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

Remove remnants of Foundation gem from Application.js


"use strict";

angular.module('arethusa').service('treebankRetriever', function($http) {
  // tokens should always be loaded synchronous - the app should
  // not start anything without knowing an initial state

  var xmlToState = function(token) {
    // One could formalize this to real rules that are configurable...
    // We might also need to apply some padding to ids at some point, otherwise
    // we loose the ability to sort them, when treebank xmls use a 'sentence-word'
    // schema such as 1-10.
    //
    // Remember that attributes of the converted xml are prefixed with underscore
    return {
      id: arethusaUtil.formatNumber(token._id, 4),
      string: token._form,
      morphology: {
        lemma: token._lemma,
        postag: token._postag
      },
      relation: {
        label: token._relation
      },
      head: {
        id: token._head
      }
    };
  };

  var parseXml = function(data) {
    /* global xml2json */
    var tokens = {};
    var xml = arethusaUtil.xml2json(data);
    var words = xml.treebank.sentence.word;
    angular.forEach(words, function(xmlToken, i) {
      var token = xmlToState(xmlToken);
      tokens[token.id] = token;
    });
    return tokens;
  };

  this.getData = function(callback) {
    var request = $.ajax({
      url: './static/example_treebank.xml',
      async: false
    });

    request.done(function(data) {
      return callback(parseXml(data));
    });
  };

});

"use strict";

angular.module('arethusa').service('treebankRetriever', function($http) {
  // tokens should always be loaded synchronous - the app should
  // not start anything without knowing an initial state

  var xmlToState = function(token) {
    // One could formalize this to real rules that are configurable...
    // We might also need to apply some padding to ids at some point, otherwise
    // we loose the ability to sort them, when treebank xmls use a 'sentence-word'
    // schema such as 1-10.
    //
    // Remember that attributes of the converted xml are prefixed with underscore
    return {
      id: arethusaUtil.formatNumber(token._id, 4),
      string: token._form,
      morphology: {
        lemma: token._lemma,
        postag: token._postag
      },
      relation: {
        label: token._relation
      },
      head: {
        id: token._head
      }
    };
  };

  var parseXml = function(data) {
    /* global xml2json */
    var tokens = {};
    var xml = arethusaUtil.xml2json(data);
    var words = xml.treebank.sentence.word;
    angular.forEach(words, function(xmlToken, i) {
      var token = xmlToState(xmlToken);
      tokens[token.id] = token;
    });
    return tokens;
  };

  this.getData = function(callback) {
    $http.get('./static/example_treebank.xml').then(function(res) {
      callback(parseXml(res.data));
    });
  };
});

Load treebank data through $http


const LEADERBOARD_SIZE = 10;

class leaderboard {

    constructor () {
        this.db = fbApp.database();
    }

    refresh (callback) {
        this.db.ref('leaderboard').orderByValue().limitToLast(LEADERBOARD_SIZE).once('value', (snapshot) => {
            this.board = [];
            snapshot.forEach((data) => {
                this.board.push({user: data.key, score: data.val()});
            });
            this.board.reverse();
            if (callback)
                callback(this.board);
        });
    }

    update (username, score) {
        this.db.ref('leaderboard/' + username).on('value', (snapshot) => {
            this.db.ref('leaderboard/' + username).set(Math.max(score, snapshot.val()));
        });
    }

}

const LEADERBOARD_SIZE = 10;

class leaderboard {

    constructor () {
        this.db = fbApp.database();
    }

    refreshScores (callback) {
        this.db.ref('leaderboard').orderByValue().limitToLast(LEADERBOARD_SIZE).once('value', (snapshot) => {
            var board = [];
            snapshot.forEach((data) => {
                board.push({user: data.key, score: data.val()});
            });
            board.reverse();
            callback(board);
        });
    }

    updateScore (username, score) {
        this.db.ref('leaderboard/' + username).on('value', (snapshot) => {
            this.db.ref('leaderboard/' + username).set(Math.max(score, snapshot.val()));
        });
    }

    updatePassedLevel (username, levelNumber) {
        this.db.ref('passedLevels/' + username + '/' + levelNumber.toString()).set(1);
    }

    getPassedLevels (username, callback) {
        this.db.ref('passedLevels/' + username + '/').orderByKey().once('value', (snapshot) => {
            var levels = [];
            snapshot.forEach((data) => {
                levels.push(data.key * 1);
            });
            callback(levels);
        });
    }

}

Add storage for "level passed" info


'use strict';

/* Filters */

angular.module('myApp.filters', []).
  filter('interpolate', ['version', function(version) {
    return function(text) {
      return String(text).replace(/\%VERSION\%/mg, version);
    }
  }])
  .filter('mapUrl', function() {
      return function(e) {
          var str = "";
          for (var i=0;i<e.length;i++) {
              str += typeof e[i].name === 'undefined' ||
                e[i].name.localeCompare('') === 0  ?
                e[i].latitude + ',' + e[i].longitude + '%7C' :
                e[i].name + '%7C';
          }
          str = str.replace(/ /g, '%20C');
          return str.replace(/%7C$/, '');
      };
  });

'use strict';

/* Filters */

angular.module('myApp.filters', []).
  filter('interpolate', ['version', function(version) {
    return function(text) {
      return String(text).replace(/\%VERSION\%/mg, version);
    }
  }])
  .filter('mapUrl', function() {
      return function(e) {
          var str = "";
          for (var i=0;i<e.length;i++) {
              str += typeof e[i].name === 'undefined' ||
                e[i].name.localeCompare('') === 0  ?
                e[i].latitude + ',' + e[i].longitude + '%7C' :
                e[i].name + '%7C';
          }
          str = str.replace(/ /g, '%20');
          return str.replace(/%7C$/, '');
      };
  });

Remove extra character from whitespace replacement


import React from 'react';

// From https://gist.github.com/turadg/9bcf08a7279e82a030a645250639fe6e
// Also needed for TypeScript, not just Flow

/**
 * Since React v15.5, there's a warning printed if you access `React.createClass` or `React.PropTypes`
 * https://reactjs.org/blog/2017/04/07/react-v15.5.0.html#new-deprecation-warnings
 *
 * `import * as React from 'react'` is required by Flowtype https://flow.org/en/docs/react/types/ ,
 * but the * causes both those deprecated getters to be called.
 * This is particularly annoying in Jest since every test prints two useless warnings.
 *
 * This file can be used as a Jest setup file to simply delete those features of the `react` package.
 * You don't need the deprecation warning. Your tests will simply fail if you're still using the old ways.
 * https://facebook.github.io/jest/docs/en/configuration.html#setupfiles-array
 */

delete React.createClass;
delete React.PropTypes;

import * as React from 'react';

// From https://gist.github.com/turadg/9bcf08a7279e82a030a645250639fe6e
// Also needed for TypeScript, not just Flow

/**
 * Since React v15.5, there's a warning printed if you access `React.createClass` or `React.PropTypes`
 * https://reactjs.org/blog/2017/04/07/react-v15.5.0.html#new-deprecation-warnings
 *
 * `import * as React from 'react'` is required by Flowtype https://flow.org/en/docs/react/types/ ,
 * but the * causes both those deprecated getters to be called.
 * This is particularly annoying in Jest since every test prints two useless warnings.
 *
 * This file can be used as a Jest setup file to simply delete those features of the `react` package.
 * You don't need the deprecation warning. Your tests will simply fail if you're still using the old ways.
 * https://facebook.github.io/jest/docs/en/configuration.html#setupfiles-array
 */

delete React.createClass;
delete React.PropTypes;

Fix React import for Jest


define([
  'underscore',
  'backbone',
  'models/profile',
  'models/address',
  'collections/item'
], function (_, Backbone, Profile, Address, ItemCollection) {
  'use strict';

  var ResumeModel = Backbone.Model.extend({

    defaults: {
      name: ''
    },

    hasOne: ['profile', 'address'],
    hasMany: ['items'],

    parse: function(response) {
      var r;
      if (response.resume) {
        r = response.resume;
      } else {
        r = response;
      }
      var options = { resumeId: this.id };
      r.address = new Address(r.address, options);
      r.profile = new Profile(r.profile, options);
      r.items = new ItemCollection(r.items, options);

      return r;
    },

    toJSON: function() {
      var json = JSON.parse(JSON.stringify(this.attributes));

      // has one associations
      _.each(this.hasOne, function(assoc) {
        json[assoc + '_id'] = this.get(assoc).id;
        delete json[assoc];
      }, this);

      // has many associations
      _.each(this.hasMany, function(assoc) {
        var singular = assoc.substring(0, assoc.length - 1);
        json[singular + '_ids'] = this.get(assoc).map(function(item) {
          return item.id;
        });
        delete json[assoc];
      }, this);

      return {
        resume: json
      };
    }
  });

  return ResumeModel;
});

define([
  'underscore',
  'backbone',
  'models/profile',
  'models/address',
  'collections/item'
], function (_, Backbone, Profile, Address, ItemCollection) {
  'use strict';

  var ResumeModel = Backbone.Model.extend({

    defaults: {
      name: ''
    },

    hasOne: ['profile', 'address'],
    hasMany: ['items'],

    initialize: function(attributes, options) {
      var options = { resumeId: this.id };
      this.set('address', new Address(attributes.address, options));
      this.set('profile', new Profile(attributes.profile, options));
      this.set('items', new ItemCollection(attributes.items, options));
    },

    parse: function(response) {
      if (response.resume) {
        return response.resume;
      } else {
        return response;
      }
    },

    toJSON: function() {
      var json = JSON.parse(JSON.stringify(this.attributes));

      // has one associations
      _.each(this.hasOne, function(assoc) {
        json[assoc + '_id'] = this.get(assoc).id;
        delete json[assoc];
      }, this);

      // has many associations
      _.each(this.hasMany, function(assoc) {
        var singular = assoc.substring(0, assoc.length - 1);
        json[singular + '_ids'] = this.get(assoc).map(function(item) {
          return item.id;
        });
        delete json[assoc];
      }, this);

      return {
        resume: json
      };
    }
  });

  return ResumeModel;
});

Create and set model association objects in the initialize method,


var ConcatSource = require("webpack/node_modules/webpack-core/lib/ConcatSource");

function ChunkWithStyleTagTemplate(chunkTemplate) {
	this.chunkTemplate = chunkTemplate;
}
module.exports = ChunkWithStyleTagTemplate;

ChunkWithStyleTagTemplate.prototype.updateHash = function(hash) {
	hash.update("ChunkWithStyleTagTemplate");
	hash.update("2");
};

ChunkWithStyleTagTemplate.prototype.render = function(chunkA, chunkB) {
	var chunk = typeof chunkA === "string" ? chunkB : chunkA;
	var source = new ConcatSource();
	if (chunk._chunkStyles) {
		var fnText = insertCss.toString();
		source.add("(("+fnText+")("+JSON.stringify(chunk._chunkStyles)+"));");
	}
	source.add(this.chunkTemplate.render.apply(this.chunkTemplate, arguments));
	source.rendered = true;
	return source;
};

function insertCss(css) {
	var style = document.createElement("style");
	style.setAttribute("type", "text/css");
	style.appendChild(document.createTextNode(css));
	document.getElementsByTagName("head")[0].appendChild(style);
}

var ConcatSource = require("webpack/node_modules/webpack-core/lib/ConcatSource");

function ChunkWithStyleTagTemplate(chunkTemplate) {
	this.chunkTemplate = chunkTemplate;
}
module.exports = ChunkWithStyleTagTemplate;

ChunkWithStyleTagTemplate.prototype.updateHash = function(hash) {
	hash.update("ChunkWithStyleTagTemplate");
	hash.update("2");
};

ChunkWithStyleTagTemplate.prototype.render = function(chunkA, chunkB) {
	var chunk = typeof chunkA === "string" ? chunkB : chunkA;
	var source = new ConcatSource();
	if (chunk._chunkStyles) {
		var fnText = insertCss.toString();
		source.add("(("+fnText+")("+JSON.stringify(chunk._chunkStyles)+"));\n");
		source.add("function ___go_js_go_go_go___() {\n");
	}
	source.add(this.chunkTemplate.render.apply(this.chunkTemplate, arguments));
	if (chunk._chunkStyles) {
		source.add("\n}");
	}
	source.rendered = true;
	return source;
};

function insertCss(css) {
	var link = document.createElement("link");
	link.setAttribute("rel", "stylesheet");
	link.setAttribute("type", "text/css");
	link.setAttribute("href", "data:text/css;base64,"+btoa(css));
	link.addEventListener("load", ___go_js_go_go_go___);
	document.getElementsByTagName("head")[0].appendChild(link);
}

Use a link tag instead of style, to get sourcemaps to work


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

app.configure(function() {
  app.use(express.bodyParser());
  app.set('dirname', __dirname);
  app.use(app.router);
  app.use(express["static"](__dirname + "/public/"));
  app.use(express.errorHandler({
    dumpExceptions: true,
    showStack: true
  }));
  return app.set('views', __dirname + "/views/");
});

app.get("/", function(req, res) {
  return res.render('index.ejs');
});

var port = process.env.PORT || 7001;
app.listen(port);
console.log("Listening on Port '" + port + "'");

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

app.configure(function() {
  app.use(express.bodyParser());
  app.set('dirname', __dirname);
  app.use(app.router);
  app.use(express["static"](__dirname + "/public/"));
  app.use(express.errorHandler({
    dumpExceptions: true,
    showStack: true
  }));
  return app.set('views', __dirname + "/views/");
});

app.get("/", function(req, res) {
  return res.render('index.ejs');
});

var port = process.env.PORT || 5000;
app.listen(port);
console.log("Listening on Port '" + port + "'");

Make sure the presentation listen on the right port


// # Ghost bootloader
// Orchestrates the loading of Ghost
// When run from command line.

var when      = require('when'),
    bootstrap = require('./bootstrap');

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

function startGhost(options) {
    // When we no longer need to require('./server')
    // in a callback this extra deferred object
    // won't be necessary, we'll just be able to return
    // the server object directly.
    var deferred = when.defer();

    options = options || {};

    bootstrap(options.config).then(function () {
        try {
            var ghost = require('./server');
            return ghost(options.app)
                .then(deferred.resolve)
                    .otherwise(function (e) {
                    // We don't return the rejected promise to stop
                    // the propogation of the rejection and just
                    // allow the user to manage what to do.
                    deferred.reject(e);
                });
        } catch (e) {
            deferred.reject(e);
        }
    });

    return deferred.promise;
}

module.exports = startGhost;

// # Ghost bootloader
// Orchestrates the loading of Ghost
// When run from command line.

var when      = require('when'),
    bootstrap = require('./bootstrap');

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

function startGhost(options) {
    // When we no longer need to require('./server')
    // in a callback this extra deferred object
    // won't be necessary, we'll just be able to return
    // the server object directly.
    var deferred = when.defer();

    options = options || {};

    bootstrap(options.config).then(function () {
        try {
            var ghost = require('./server');
            return ghost(options.app)
                .then(deferred.resolve)
                    .otherwise(function (e) {
                    // We don't return the rejected promise to stop
                    // the propagation of the rejection and just
                    // allow the user to manage what to do.
                    deferred.reject(e);
                });
        } catch (e) {
            deferred.reject(e);
        }
    });

    return deferred.promise;
}

module.exports = startGhost;

Fix typo in code comment: propogation errata changed to propagation.


import React from "react/addons";
import times from "lodash.times";

const BATCH_SIZE = 2;

class Columns extends React.Component {
  constructor(props) {
    super(props);
  }

  convertChildren(children) {
    if (children.length === 0) {
      return []
    }

    if (children.length) {
      return children;
    } else {
      return [children];
    }
  }

  componentWillMount() {

  }

  componentWillReceiveProps(newProps) {

  }

  componentDidMount() {

  }

  componentDidUnmount() {

  }

  componentDidUpdate() {

  }

  columnize(number, items) {
    let columns = [];

    times(number, n => {
      columns[n] = [];
    });

    for (var i = 0; i < items.length; i++) {
      let item = items[i];
      columns[i % number].push(item);
    }

    return columns;
  }

  render() {
    let items = this.convertChildren(this.props.children);

    let autoNumber = Math.min(items.length, this.props.number);
    let columnization = this.columnize(autoNumber, items);

    let containerElement = this.props.container ||

    let columnContainers = columnization.map(column => {
        return React.cloneElement(containerElement, {}, column);
      }
    );

    return (

        {columnContainers}

    );
  }
}

export default Columns;

import React from "react/addons";
import times from "lodash.times";

const BATCH_SIZE = 2;

class Columns extends React.Component {
  constructor(props) {
    super(props);
  }

  convertChildren(children) {
    if (children.length === 0) {
      return []
    }

    if (children.length) {
      return children;
    } else {
      return [children];
    }
  }

  componentWillMount() {

  }

  componentWillReceiveProps(newProps) {

  }

  componentDidMount() {

  }

  componentDidUnmount() {

  }

  componentDidUpdate() {

  }

  columnize(number, items) {
    let columns = [];

    times(number, n => {
      columns[n] = [];
    });

    for (var i = 0; i < items.length; i++) {
      let item = items[i];
      columns[i % number].push(item);
    }

    return columns;
  }

  render() {
    let items = this.convertChildren(this.props.children);

    let autoNumber = Math.min(items.length, this.props.number);
    let columnization = this.columnize(autoNumber, items);

    let containerElement = this.props.container ||

    let columnContainers = columnization.map((column, i) => {
        return React.cloneElement(containerElement, {key: i}, column);
      }
    );

    return (

        {columnContainers}

    );
  }
}

export default Columns;

Add key prop to column containers


(function($) {

$(document).ready(function() {
  $('.player-link').click(function(e) {
    $.get($(this).attr('href'));
    e.preventDefault();
  });

  $('#uploader input').change(validateUploadForm);
  $('#uploader input[type="text"]').on('keyup', validateUploadForm);
  $('#uploader input[type="text"]').on('blur', validateUploadForm);

  $('#talk input[type="text"]').on('keyup', validateTalkForm);
  $('#talk input[type="text"]').on('blur', validateTalkForm);
});

var validateUploadForm = function()
{
  validateForm('uploader', function() {
    return
	  $('#input-title').val() != '' &&
      $('#input-image').val() != '' &&
      $('#input-audio').val() != '';
  });
}

var validateTalkForm = function()
{
  validateForm('talk', function() {
    return $('#input-txtmsg').val() != '';
  });
}

var validateForm = function(formName, validityCheckFunc)
{
  $formSubmit = $('#' + formName + '-submit');
  if (validityCheckFunc())
    $formSubmit.removeAttr('disabled');
  else
    $formSubmit.attr('disabled', 'true');
}

})(jQuery);

(function($) {

$(document).ready(function() {
  $('.player-link').click(function(e) {
    $.get($(this).attr('href'));
    e.preventDefault();
  });

  $('#uploader input').change(validateUploadForm);
  $('#uploader input[type="text"]').on('keyup', validateUploadForm);
  $('#uploader input[type="text"]').on('blur', validateUploadForm);

  $('#talk input[type="text"]').on('keyup', validateTalkForm);
  $('#talk input[type="text"]').on('blur', validateTalkForm);
});

var validateUploadForm = function()
{
  validateForm('uploader', function() {
    var valid =
	  $('#input-title').val() != '' &&
      $('#input-image').val() != '' &&
      $('#input-audio').val() != '';

    return valid;
  });
}

var validateTalkForm = function()
{
  validateForm('talk', function() {
    return $('#input-txtmsg').val() != '';
  });
}

var validateForm = function(formName, validityCheckFunc)
{
  $formSubmit = $('#' + formName + '-submit');
  if (validityCheckFunc())
    $formSubmit.removeAttr('disabled');
  else
    $formSubmit.attr('disabled', 'true');
}

})(jQuery);

Fix JS validation. No idea why it doesn't work the other way. Bleh.


function Context( options ) {
  var self = this;

  self.extends = {
    resources: {
      'Person': {
        plugin: function(schema, options) {
          schema.methods.can = self.can;
        }
      }
    }
  };
}

Context.prototype.can = function( action , object ) {
  return true;
};

module.exports = Context;

function Context( options ) {
  var self = this;

  self.extends = {
    resources: {
      'Person': {
        plugin: function(schema, options) {
          schema.methods.can = self.can;
        }
      }
    }
  };
}

Context.prototype.can = function( action , object ) {
  var id = this._id.toString();
  var map = object._show.credits.map(function(x) {
    return x._person.toString();
  });
  return (~map.indexOf( id ));
};

module.exports = Context;

Add real checker to `can` method.


'use strict';

const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-as-promised'));

var stats = require('../../models').Stats;

describe('Stats', function() {
	it('should exist', function() {
		expect(stats).to.exist;
	})

	const TESTING_KEY = 'testing';
	const TESTING_VALUE = 5;

	describe('setting a value', function() {
		it('should not produce an error', function(done) {
			expect(stats.setValue(TESTING_KEY, TESTING_VALUE)).to.eventually.be.ok;
			done();
		})
	})

	describe('fetching it back', function() {
		it('should produce the same value', function() {
			return stats.getValue(TESTING_KEY).then((val) => {
				expect(val).to.equal(TESTING_VALUE);
			})
		})
	})
})

'use strict';

const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-as-promised'));

var stats = require('../../models').Stats;

describe('Stats', function() {
	it('should exist', function() {
		expect(stats).to.exist;
	})

	const TESTING_KEY = 'testing';
	const TESTING_VALUE = 5;

	describe('setting a value', function() {
		it('should not produce an error', function() {
			return stats.setValue(TESTING_KEY, TESTING_VALUE);
		})
	})

	describe('fetching it back', function() {
		it('should produce the same value', function() {
			return stats.getValue(TESTING_KEY).then((val) => {
				expect(val).to.equal(TESTING_VALUE);
			})
		})
	})
})

Fix test (promise was not being resolved the other way)


module.exports = {
  // Prefix identifier that will be used inside storing and loading
  id: 'gjs-',

  // Enable/Disable autosaving
  autosave: 1,

  // Indicates if load data inside editor after init
  autoload: 1,

  // Indicates which storage to use. Available: local | remote
  type: 'local',

  // If autosave enabled, indicates how many steps (general changes to structure)
  // need to be done before save. Useful with remoteStorage to reduce remote calls
  stepsBeforeSave: 1,

  //Enable/Disable components model (JSON format)
  storeComponents: 1,

  //Enable/Disable styles model (JSON format)
  storeStyles: 1,

  //Enable/Disable saving HTML template
  storeHtml: 1,

  //Enable/Disable saving CSS template
  storeCss: 1,

  // ONLY FOR LOCAL STORAGE
  // If enabled, checks if browser supports Local Storage
  checkLocal: 1,

  // ONLY FOR REMOTE STORAGE
  // Custom parameters to pass with the remote storage request, eg. csrf token
  params: {},

  // Custom headers for the remote storage request
  headers: {},

  // Endpoint where to save all stuff
  urlStore: '',

  // Endpoint where to fetch data
  urlLoad: '',

  //Callback before request
  beforeSend(jqXHR, settings) {},

  //Callback after request
  onComplete(jqXHR, status) {},

  // set contentType paramater of $.ajax
  // true: application/json; charset=utf-8'
  // false: 'x-www-form-urlencoded'
  contentTypeJson: false,

  credentials: 'include'
};

module.exports = {
  // Prefix identifier that will be used inside storing and loading
  id: 'gjs-',

  // Enable/Disable autosaving
  autosave: 1,

  // Indicates if load data inside editor after init
  autoload: 1,

  // Indicates which storage to use. Available: local | remote
  type: 'local',

  // If autosave enabled, indicates how many steps (general changes to structure)
  // need to be done before save. Useful with remoteStorage to reduce remote calls
  stepsBeforeSave: 1,

  //Enable/Disable components model (JSON format)
  storeComponents: 1,

  //Enable/Disable styles model (JSON format)
  storeStyles: 1,

  //Enable/Disable saving HTML template
  storeHtml: 1,

  //Enable/Disable saving CSS template
  storeCss: 1,

  // ONLY FOR LOCAL STORAGE
  // If enabled, checks if browser supports Local Storage
  checkLocal: 1,

  // ONLY FOR REMOTE STORAGE
  // Custom parameters to pass with the remote storage request, eg. csrf token
  params: {},

  // Custom headers for the remote storage request
  headers: {},

  // Endpoint where to save all stuff
  urlStore: '',

  // Endpoint where to fetch data
  urlLoad: '',

  //Callback before request
  beforeSend(jqXHR, settings) {},

  //Callback after request
  onComplete(jqXHR, status) {},

  // set contentType paramater of $.ajax
  // true: application/json; charset=utf-8'
  // false: 'x-www-form-urlencoded'
  contentTypeJson: true,

  credentials: 'include'
};

Set `contentTypeJson` default to true


import _request from 'request-promise'

const jar = _request.jar()
const request = _request.defaults({
  headers: {'Content-Type': 'application/json'},
  jar,
})

const SCV='https://www.alza.sk/Services/EShopService.svc/'

export function* login(credentials) {
  return yield request.post(`${SCV}LoginUser`, {body: JSON.stringify(credentials)})
}

export function* addToCart(itemUrl, count) {
  const id = itemUrl.match(/d(\d*)\.htm$/)[1]
  return yield request.post(`${SCV}OrderCommodity`, {body: JSON.stringify(
    {id, count}
  )})
}

import _request from 'request-promise'

const jar = _request.jar()
const request = _request.defaults({
  headers: {'Content-Type': 'application/json'},
  jar,
})

const SCV='https://www.alza.sk/Services/EShopService.svc/'

export function* login(credentials) {
  return yield request.post(`${SCV}LoginUser`, {body: JSON.stringify(credentials)})
}

export function* addToCart({url, count}) {
  const id = url.match(/(d|dq=)(\d*)(\.htm)?$/)[2]
  return yield request.post(`${SCV}OrderCommodity`, {body: JSON.stringify(
    {id, count}
  )})
}

Fix url matching to id.


import baseStyles from '../../../styles/resets/baseStyles.css'
import { BEM } from '../../../utilities/classNames'

const bem = BEM('.c-MessageContent')

const css = `
  ${baseStyles}
  margin-bottom: 20px;

  ${bem.element('content')} {
    max-width: 500px;
    text-align: left;

    &.is-rtl {
      direction: rtl;
      text-align: right;
    }
  }
`

export default css

import baseStyles from '../../../styles/resets/baseStyles.css'
import { BEM } from '../../../utilities/classNames'

const bem = BEM('.c-MessageContent')

const css = `
  ${baseStyles}

  ${bem.element('content')} {
    max-width: 500px;
    text-align: left;

    &.is-rtl {
      direction: rtl;
      text-align: right;
    }
  }
`

export default css

Remove bottom margins for `Message.Content`