<commit_before>/* REST calls for RPNow.
   Documentation: https://docs.google.com/spreadsheets/d/1IfC8TYu8kgqLAm1TsC1saRJsD_cxFdgz0C-YMcoYsB0
*/

var express = require('express');
var router = express.Router();
module.exports = router;

var bodyParser = require('body-parser');
router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));

var crypto = require('crypto');

var wordwrap = require('wordwrap');

const PAGE_SIZE = 20;
const REFRESH_MILLIS = 2000;

var rooms = {};

function rpMiddleware(req, res, next) {
   req.rp = rooms[req.params.rpCode];
   if (!req.rp) return res.status(404).json({ error: 'RP not found'});
   return next();
}
router.use('/rps/:rpCode.json', rpMiddleware);
router.use('/rps/:rpCode.txt', rpMiddleware);
router.use('/rps/:rpCode/*', rpMiddleware);

function cleanParams(params) {
   return function(req, res, next) {
      for (var x in params) {
         var optional = Array.isArray(params[x]);
         var requirement = optional? params[x][0]: params[x];

         var string = req.body[x];
         if (string === undefined || string === null) {
            if (optional) continue;
            else return next(new Error(`Missing variable: ${x}`));
         }

         if (typeof(string) !== 'string') return next(new Error(`${x} is not a string.`));
         if (typeof(requirement) === 'number') {
            if (string.length > requirement) return next(new Error(`${x} is longer than ${requirement} characters.`));
            var trimmed = string.trim();
            if (trimmed.length === 0 && !optional) return next(new Error(`${x} is empty or whitespace.`));
            req.body[x] = trimmed;
         }
         else if (requirement instanceof RegExp) {
            if (!string.match(requirement)) return next(new Error(`${x} is not in the correct format: ${requirement.toString()}`));
            req.body[x] = string;
         }

      }
      return next();
   };
}

// REST API

router.post('/rps.json', cleanParams({ 'title':30, 'desc':[255] }), (req, res, next) => {
   var rpCodeCharacters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
   crypto.randomBytes(16, gotBytes);

   function gotBytes(err, buffer) {
      var token = buffer.toString('base64');
      var rpCode = token.match(new RegExp(rpCodeCharacters.split('').join('|'), 'g')).join('').substr(0, 8);
      console.log(token, rpCode);

      // if it generated a bad or duplicate rp code, try again
      if (rpCode.length !== 8 || rooms[rpCode]) {
         crypto.randomBytes(16, gotBytes);
         return;
      }

      rooms[rpCode] = {
         rpCode: rpCode,
         title: req.body.title,
         desc: (req.body.desc || ""),
         msgs: [],
         charas: [],
         updateList: [],
         addUpdateEntry: function() { this.updateList.push({ msgCount: this.msgs.length, charaCount: this.charas.length }); }
      };
      rooms[rpCode].addUpdateEntry();

      res.status(201).json({ rpCode: rpCode });
   }
});

router.get('/rps/:rpCode.json', (req, res, next) => {
   res.status(200).json({
      title: req.rp.title,
      desc: req.rp.desc,
      msgs: req.rp.msgs.slice(-PAGE_SIZE),
      charas: req.rp.charas,
      pageSize: PAGE_SIZE,
      refreshMillis: REFRESH_MILLIS,
      updateCounter: req.rp.updateList.length-1
   });
});

router.get('/rps/:rpCode/updates.json', (req, res, next) => {
   var updateCounter = +req.query.updateCounter;
   if (!(updateCounter >= 0)) return next(new Error('Invalid updateCounter value'));
   if (updateCounter > req.rp.updateList.length-1) return next(new Error('updatecounter larger than latest available update'));
   if (updateCounter === req.rp.updateList.length-1) {
      res.sendStatus(204);
      return;
   }

   var lastClientUpdate = req.rp.updateList[updateCounter]
   res.status(200).json({
      msgs: req.rp.msgs.slice(lastClientUpdate.msgCount),
      charas: req.rp.charas.slice(lastClientUpdate.charaCount),
      updateCounter: req.rp.updateList.length-1
   });
});

router.get('/rps/:rpCode/page/:pageNum.json', (req, res, next) => {
   var pageNum = req.params.pageNum;
   if (!(pageNum >= 0) || pageNum%0) return next(new Error('Bad page number'));
   var numPages = Math.max(Math.ceil(req.rp.msgs.length/PAGE_SIZE), 1);
   var out = {
      title: req.rp.title,
      desc: req.rp.desc,
      numPages: numPages
   };
   if (pageNum > numPages) {
      out.error = `Page not found: ${pageNum}`;
      return res.status(404).json(out);
   }
   var pageStart = (pageNum-1)*PAGE_SIZE;
   out.msgs = req.rp.msgs.slice(pageStart, pageStart+PAGE_SIZE);
   out.charas = out.msgs
      .filter(m=>m.type==='chara')
      .map(m=>m.charaId)
      .reduce((arr,id)=>{ if(!(id in arr)) arr[id] = id; return arr; }, [])
      .map(id=>req.rp.charas[id]);

   res.status(200).json(out);
});

router.post('/rps/:rpCode/msg.json', cleanParams({ content: 10000 }), (req, res, next) => {
   var msg = req.body;
   // message validation
   if (['narrator', 'chara', 'ooc'].indexOf(msg.type) === -1) return next(new Error('Bad message type.'));
   if (msg.type === 'chara') {
      if (!(msg.charaId >= 0)) return next(new Error('Bad or missing chara id.'));
      if (!req.rp.charas[msg.charaId]) return next(new Error('Invalid chara id.'));
   }
   // submit message
   req.rp.msgs.push({
      id: req.rp.msgs.length,
      type: msg.type,
      content: msg.content,
      charaId: msg.charaId,
      timestamp: Date.now() / 1000,
      ipid: crypto.createHash('md5').update(req.ip).digest('hex').substr(0,18)
   });
   req.rp.addUpdateEntry();
   res.status(201).json({ id: req.rp.msgs.length - 1 });
});

router.post('/rps/:rpCode/chara.json', cleanParams({ name: 30, color: /^#[0-9a-f]{6}$/gi }), (req, res, next) => {
   var chara = req.body;
   req.rp.charas.push({
      id: req.rp.charas.length,
      name: chara.name,
      color: chara.color,
      ipid: crypto.createHash('md5').update(req.ip).digest('hex').substr(0,18)
   });
   req.rp.addUpdateEntry();
   res.status(201).json({ id: req.rp.charas.length - 1 });
});

router.get('/rps/:rpCode.txt', (req, res, next) => {
   res.type('.txt');
   res.attachment(req.rp.title + '.txt');

   var out = req.rp.msgs;
   if (!req.query.ooc) out = out.filter(msg=>msg.type !== 'ooc');
   out = out.map(msg=>{
      if(msg.type === 'narrator') {
         return wordwrap(72)(msg.content);
      }
      else if(msg.type === 'ooc') {
         return wordwrap(72)(`(( OOC: ${msg.content} ))`);
      }
      else if(msg.type === 'chara') {
         return `${req.rp.charas[msg.charaId].name.toUpperCase()}:\n`
            + wordwrap(2, 72)(msg.content);
      }
      else {
         throw new Error(`Unexpected message type: ${msg.type}`);
      }
   });
   out.unshift(`${req.rp.title}\n${req.rp.desc}\n----------`);
   res.send(out.map(msg=>msg.replace('\n', '\r\n')).join('\r\n\r\n'));
});

router.all('*', (req, res, next) => {
   next(new Error('unknown API call'));
});

// ERROR HANDLING
router.use((err, req, res, next) => {
   res.status(400).json({ error: err.message });
});
<commit_msg>Removed extraneous console.log statement<commit_after>/* REST calls for RPNow.
   Documentation: https://docs.google.com/spreadsheets/d/1IfC8TYu8kgqLAm1TsC1saRJsD_cxFdgz0C-YMcoYsB0
*/

var express = require('express');
var router = express.Router();
module.exports = router;

var bodyParser = require('body-parser');
router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true }));

var crypto = require('crypto');

var wordwrap = require('wordwrap');

const PAGE_SIZE = 20;
const REFRESH_MILLIS = 2000;

var rooms = {};

function rpMiddleware(req, res, next) {
   req.rp = rooms[req.params.rpCode];
   if (!req.rp) return res.status(404).json({ error: 'RP not found'});
   return next();
}
router.use('/rps/:rpCode.json', rpMiddleware);
router.use('/rps/:rpCode.txt', rpMiddleware);
router.use('/rps/:rpCode/*', rpMiddleware);

function cleanParams(params) {
   return function(req, res, next) {
      for (var x in params) {
         var optional = Array.isArray(params[x]);
         var requirement = optional? params[x][0]: params[x];

         var string = req.body[x];
         if (string === undefined || string === null) {
            if (optional) continue;
            else return next(new Error(`Missing variable: ${x}`));
         }

         if (typeof(string) !== 'string') return next(new Error(`${x} is not a string.`));
         if (typeof(requirement) === 'number') {
            if (string.length > requirement) return next(new Error(`${x} is longer than ${requirement} characters.`));
            var trimmed = string.trim();
            if (trimmed.length === 0 && !optional) return next(new Error(`${x} is empty or whitespace.`));
            req.body[x] = trimmed;
         }
         else if (requirement instanceof RegExp) {
            if (!string.match(requirement)) return next(new Error(`${x} is not in the correct format: ${requirement.toString()}`));
            req.body[x] = string;
         }

      }
      return next();
   };
}

// REST API

router.post('/rps.json', cleanParams({ 'title':30, 'desc':[255] }), (req, res, next) => {
   var rpCodeCharacters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
   crypto.randomBytes(16, gotBytes);

   function gotBytes(err, buffer) {
      var token = buffer.toString('base64');
      var rpCode = token.match(new RegExp(rpCodeCharacters.split('').join('|'), 'g')).join('').substr(0, 8);

      // if it generated a bad or duplicate rp code, try again
      if (rpCode.length !== 8 || rooms[rpCode]) {
         crypto.randomBytes(16, gotBytes);
         return;
      }

      rooms[rpCode] = {
         rpCode: rpCode,
         title: req.body.title,
         desc: (req.body.desc || ""),
         msgs: [],
         charas: [],
         updateList: [],
         addUpdateEntry: function() { this.updateList.push({ msgCount: this.msgs.length, charaCount: this.charas.length }); }
      };
      rooms[rpCode].addUpdateEntry();

      res.status(201).json({ rpCode: rpCode });
   }
});

router.get('/rps/:rpCode.json', (req, res, next) => {
   res.status(200).json({
      title: req.rp.title,
      desc: req.rp.desc,
      msgs: req.rp.msgs.slice(-PAGE_SIZE),
      charas: req.rp.charas,
      pageSize: PAGE_SIZE,
      refreshMillis: REFRESH_MILLIS,
      updateCounter: req.rp.updateList.length-1
   });
});

router.get('/rps/:rpCode/updates.json', (req, res, next) => {
   var updateCounter = +req.query.updateCounter;
   if (!(updateCounter >= 0)) return next(new Error('Invalid updateCounter value'));
   if (updateCounter > req.rp.updateList.length-1) return next(new Error('updatecounter larger than latest available update'));
   if (updateCounter === req.rp.updateList.length-1) {
      res.sendStatus(204);
      return;
   }

   var lastClientUpdate = req.rp.updateList[updateCounter]
   res.status(200).json({
      msgs: req.rp.msgs.slice(lastClientUpdate.msgCount),
      charas: req.rp.charas.slice(lastClientUpdate.charaCount),
      updateCounter: req.rp.updateList.length-1
   });
});

router.get('/rps/:rpCode/page/:pageNum.json', (req, res, next) => {
   var pageNum = req.params.pageNum;
   if (!(pageNum >= 0) || pageNum%0) return next(new Error('Bad page number'));
   var numPages = Math.max(Math.ceil(req.rp.msgs.length/PAGE_SIZE), 1);
   var out = {
      title: req.rp.title,
      desc: req.rp.desc,
      numPages: numPages
   };
   if (pageNum > numPages) {
      out.error = `Page not found: ${pageNum}`;
      return res.status(404).json(out);
   }
   var pageStart = (pageNum-1)*PAGE_SIZE;
   out.msgs = req.rp.msgs.slice(pageStart, pageStart+PAGE_SIZE);
   out.charas = out.msgs
      .filter(m=>m.type==='chara')
      .map(m=>m.charaId)
      .reduce((arr,id)=>{ if(!(id in arr)) arr[id] = id; return arr; }, [])
      .map(id=>req.rp.charas[id]);

   res.status(200).json(out);
});

router.post('/rps/:rpCode/msg.json', cleanParams({ content: 10000 }), (req, res, next) => {
   var msg = req.body;
   // message validation
   if (['narrator', 'chara', 'ooc'].indexOf(msg.type) === -1) return next(new Error('Bad message type.'));
   if (msg.type === 'chara') {
      if (!(msg.charaId >= 0)) return next(new Error('Bad or missing chara id.'));
      if (!req.rp.charas[msg.charaId]) return next(new Error('Invalid chara id.'));
   }
   // submit message
   req.rp.msgs.push({
      id: req.rp.msgs.length,
      type: msg.type,
      content: msg.content,
      charaId: msg.charaId,
      timestamp: Date.now() / 1000,
      ipid: crypto.createHash('md5').update(req.ip).digest('hex').substr(0,18)
   });
   req.rp.addUpdateEntry();
   res.status(201).json({ id: req.rp.msgs.length - 1 });
});

router.post('/rps/:rpCode/chara.json', cleanParams({ name: 30, color: /^#[0-9a-f]{6}$/gi }), (req, res, next) => {
   var chara = req.body;
   req.rp.charas.push({
      id: req.rp.charas.length,
      name: chara.name,
      color: chara.color,
      ipid: crypto.createHash('md5').update(req.ip).digest('hex').substr(0,18)
   });
   req.rp.addUpdateEntry();
   res.status(201).json({ id: req.rp.charas.length - 1 });
});

router.get('/rps/:rpCode.txt', (req, res, next) => {
   res.type('.txt');
   res.attachment(req.rp.title + '.txt');

   var out = req.rp.msgs;
   if (!req.query.ooc) out = out.filter(msg=>msg.type !== 'ooc');
   out = out.map(msg=>{
      if(msg.type === 'narrator') {
         return wordwrap(72)(msg.content);
      }
      else if(msg.type === 'ooc') {
         return wordwrap(72)(`(( OOC: ${msg.content} ))`);
      }
      else if(msg.type === 'chara') {
         return `${req.rp.charas[msg.charaId].name.toUpperCase()}:\n`
            + wordwrap(2, 72)(msg.content);
      }
      else {
         throw new Error(`Unexpected message type: ${msg.type}`);
      }
   });
   out.unshift(`${req.rp.title}\n${req.rp.desc}\n----------`);
   res.send(out.map(msg=>msg.replace('\n', '\r\n')).join('\r\n\r\n'));
});

router.all('*', (req, res, next) => {
   next(new Error('unknown API call'));
});

// ERROR HANDLING
router.use((err, req, res, next) => {
   res.status(400).json({ error: err.message });
});
<|endoftext|>


<commit_before>'use strict';

var Inherits = require ('Inherits');
var Check = require ('Check');
var ArgException = require ('ArgException');
var ArgTypeException = require ('ArgTypeException');
var QueryParamException = require ('./grokible.queryParamException');

var format = require ('string-format').extend (String.prototype);

var ParamSpec = function (args, spec, opt) {
    var obj = Inherits.superCreateNewIgnored (ParamSpec, Object);
    obj._args = args;
    obj._spec = spec;
    obj._opt = opt;
    return obj;
};

ParamSpec.prototype.getArgs = function () { return this._args }
ParamSpec.prototype.setArgs = function (args) { this._args = args }

ParamSpec.prototype.getSpec = function () { return this._spec }
ParamSpec.prototype.setSpec = function (spec) { this._spec = spec }

ParamSpec.prototype.getOpt = function () { return this._opt }
ParamSpec.prototype.setOpt = function (opt) { this._opt = opt }

ParamSpec.prototype.get = function (name) {
    var v, v2, arg, spec, type, v_type;
    var exCtor, msg;

    var exopt = { spec: this._spec };

    if (this._opt && ('exception' in this._opt))
	exCtor = this._opt ['exception'];
    else
	exCtor = ArgException;

    if (name in this._spec)
	spec = this._spec [name];
    else
	throw exCtor ("No spec '{}' in ParamSpec".format (name), exopt);

    if ('default' in spec)
        v = spec ['default'];

    if (name in this._args) {
	arg = this._args [name];
	v = arg;
    }

    type = ('type' in spec) ? spec ['type'] : 'string';

    // If v is undefined at this point, it means there was no default and
    // no argument passed.  Flags, will be undefined if not passed.

    if (v === undefined && type != 'flag')
        throw exCtor ("No arg found named '{}'".format (name), exopt);

    v_type = typeof v;

    // Coercion (e.g. coerce string to other type by parsing)
    if (v_type === 'string') {
        try {
            if (type === 'string') {
	        v2 = v;
            } else if (type === 'number')
                v2 = parseFloat (v);
            else if (type === 'integer')
                v2 = parseInt (v);
            else if (type === 'flag')
		v2 = true;  // anything with flag should be true
        } catch (e) {
	    msg = "Failed attempting to convert string value '{}' to type {}";
	    msg.format (v, type);
	    throw exCtor (msg, exopt);
        }
    } else if (v_type === 'undefined') {
	if (type === 'flag')
	    v2 = name in this._args;
	else
	    throw ArgException ("Undefined value only allowed on 'flag' " +
                "not type={}".format (type), exopt);
    } else {
	// not a string arg, so no coercion
        v2 = v;
    }

    // v2 now holds the typed thing, check it
    msg = "Expected query param '{}={}' to be a {}.".format (name, arg, type);

    try {
	if (type === 'string')
	    Check.isString (v2, { exception: exCtor, message: msg });
	else if (type === 'number')
	    Check.isNumber (v2, { exception: exCtor, message: msg });
	else if (type === 'integer')
	    Check.isInteger (v2, { exception: exCtor, message: msg });
	else if (type === 'flag') {
	    // Do nothing, as we coerced the value to true/false
	} else
	    throw ArgException ("Spec '{}' unknown type {}".format
                (name, type));
    } catch (e) {
	e.setOpt (exopt);
	throw e;
    }

    return v2;
}

module.exports = ParamSpec;
<commit_msg>paramSpec now accepts undefined args<commit_after>'use strict';

var Inherits = require ('Inherits');
var Check = require ('Check');
var ArgException = require ('ArgException');
var ArgTypeException = require ('ArgTypeException');
var QueryParamException = require ('./grokible.queryParamException');

var format = require ('string-format').extend (String.prototype);

var ParamSpec = function (args, spec, opt) {
    var obj = Inherits.superCreateNewIgnored (ParamSpec, Object);
    obj._args = args === undefined ? {} : args;
    obj._spec = spec;
    obj._opt = opt;
    return obj;
};

ParamSpec.prototype.getArgs = function () { return this._args }
ParamSpec.prototype.setArgs = function (args) { this._args = args }

ParamSpec.prototype.getSpec = function () { return this._spec }
ParamSpec.prototype.setSpec = function (spec) { this._spec = spec }

ParamSpec.prototype.getOpt = function () { return this._opt }
ParamSpec.prototype.setOpt = function (opt) { this._opt = opt }

ParamSpec.prototype.get = function (name) {
    var v, v2, arg, spec, type, v_type;
    var exCtor, msg;

    var exopt = { spec: this._spec };

    if (this._opt && ('exception' in this._opt))
	exCtor = this._opt ['exception'];
    else
	exCtor = ArgException;

    if (name in this._spec)
	spec = this._spec [name];
    else
	throw exCtor ("No spec '{}' in ParamSpec".format (name), exopt);

    if ('default' in spec)
        v = spec ['default'];

    if (name in this._args) {
	arg = this._args [name];
	v = arg;
    }

    type = ('type' in spec) ? spec ['type'] : 'string';

    // If v is undefined at this point, it means there was no default and
    // no argument passed.  Flags, will be undefined if not passed.

    if (v === undefined && type != 'flag')
        throw exCtor ("No arg found named '{}'".format (name), exopt);

    v_type = typeof v;

    // Coercion (e.g. coerce string to other type by parsing)
    if (v_type === 'string') {
        try {
            if (type === 'string') {
	        v2 = v;
            } else if (type === 'number')
                v2 = parseFloat (v);
            else if (type === 'integer')
                v2 = parseInt (v);
            else if (type === 'flag')
		v2 = true;  // anything with flag should be true
        } catch (e) {
	    msg = "Failed attempting to convert string value '{}' to type {}";
	    msg.format (v, type);
	    throw exCtor (msg, exopt);
        }
    } else if (v_type === 'undefined') {
	if (type === 'flag')
	    v2 = name in this._args;
	else
	    throw ArgException ("Undefined value only allowed on 'flag' " +
                "not type={}".format (type), exopt);
    } else {
	// not a string arg, so no coercion
        v2 = v;
    }

    // v2 now holds the typed thing, check it
    msg = "Expected query param '{}={}' to be a {}.".format (name, arg, type);

    try {
	if (type === 'string')
	    Check.isString (v2, { exception: exCtor, message: msg });
	else if (type === 'number')
	    Check.isNumber (v2, { exception: exCtor, message: msg });
	else if (type === 'integer')
	    Check.isInteger (v2, { exception: exCtor, message: msg });
	else if (type === 'flag') {
	    // Do nothing, as we coerced the value to true/false
	} else
	    throw ArgException ("Spec '{}' unknown type {}".format
                (name, type));
    } catch (e) {
	e.setOpt (exopt);
	throw e;
    }

    return v2;
}

module.exports = ParamSpec;
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
 * Google as part of the polymer project is also subject to an additional IP
 * rights grant found at http://polymer.github.io/PATENTS.txt
 */

'use strict';

const depcheck = require('depcheck');
const fs = require('fs-extra');
const gulp = require('gulp');
const mergeStream = require('merge-stream');
const mocha = require('gulp-spawn-mocha');
const path = require('path');
const runSeq = require('run-sequence');
const tslint = require('gulp-tslint');
const typescript = require('gulp-typescript');

const tsProject = typescript.createProject('tsconfig.json');

gulp.task('lint', ['tslint', 'depcheck']);

gulp.task('clean', (done) => {
  fs.remove(path.join(__dirname, 'lib'), done);
});

gulp.task('build', (done) => {
  runSeq('clean', ['compile', 'copy'], done);
});

gulp.task('compile', () => {
  let tsReporter = typescript.reporter.defaultReporter();
  return mergeStream(
             tsProject.src().pipe(tsProject(tsReporter)),
             gulp.src(['src/**/*', '!src/**/*.ts']))
      .pipe(gulp.dest('lib'));
});

gulp.task('copy', () => gulp.src(['src/**/.gitignore']).pipe(gulp.dest('lib')));

gulp.task(
    'test',
    ['build'],
    () => gulp.src('lib/test/unit/**/*_test.js', {read: false}).pipe(mocha({
      ui: 'tdd',
      reporter: 'spec',
    })));

gulp.task(
    'test:integration',
    ['build'],
    () => gulp.src(['lib/test/integration/**/*_test.js'], {read: false})
              .pipe(mocha({
                ui: 'tdd',
                reporter: 'spec',
              })));

gulp.task(
    'tslint',
    () => gulp.src('src/**/*.ts')
              .pipe(tslint({
                configuration: 'tslint.json',
                formatter: 'verbose',
              }))
              .pipe(tslint.report()));

gulp.task('depcheck', ['build'], () => {
  return depcheck(__dirname, {
           // "@types/*" dependencies are type declarations that are
           // automatically
           // loaded by TypeScript during build. depcheck can't detect this
           // so we ignore them here. Same with babel plugins.
           ignoreMatches:
               ['@types/*', 'vinyl', 'babel-plugin-external-helpers']
         })
      .then((result) => {
        let invalidFiles = Object.keys(result.invalidFiles) || [];
        let invalidJsFiles = invalidFiles.filter((f) => f.endsWith('.js'));
        if (invalidJsFiles.length > 0) {
          throw new Error(`Invalid files: ${invalidJsFiles}`);
        }
        if (result.dependencies.length) {
          throw new Error(`Unused dependencies: ${result.dependencies}`);
        }
      });
});
<commit_msg>Exclude cli->polymer-build from depcheck<commit_after>/**
 * @license
 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
 * Google as part of the polymer project is also subject to an additional IP
 * rights grant found at http://polymer.github.io/PATENTS.txt
 */

'use strict';

const depcheck = require('depcheck');
const fs = require('fs-extra');
const gulp = require('gulp');
const mergeStream = require('merge-stream');
const mocha = require('gulp-spawn-mocha');
const path = require('path');
const runSeq = require('run-sequence');
const tslint = require('gulp-tslint');
const typescript = require('gulp-typescript');

const tsProject = typescript.createProject('tsconfig.json');

gulp.task('lint', ['tslint', 'depcheck']);

gulp.task('clean', (done) => {
  fs.remove(path.join(__dirname, 'lib'), done);
});

gulp.task('build', (done) => {
  runSeq('clean', ['compile', 'copy'], done);
});

gulp.task('compile', () => {
  let tsReporter = typescript.reporter.defaultReporter();
  return mergeStream(
             tsProject.src().pipe(tsProject(tsReporter)),
             gulp.src(['src/**/*', '!src/**/*.ts']))
      .pipe(gulp.dest('lib'));
});

gulp.task('copy', () => gulp.src(['src/**/.gitignore']).pipe(gulp.dest('lib')));

gulp.task(
    'test',
    ['build'],
    () => gulp.src('lib/test/unit/**/*_test.js', {read: false}).pipe(mocha({
      ui: 'tdd',
      reporter: 'spec',
    })));

gulp.task(
    'test:integration',
    ['build'],
    () => gulp.src(['lib/test/integration/**/*_test.js'], {read: false})
              .pipe(mocha({
                ui: 'tdd',
                reporter: 'spec',
              })));

gulp.task(
    'tslint',
    () => gulp.src('src/**/*.ts')
              .pipe(tslint({
                configuration: 'tslint.json',
                formatter: 'verbose',
              }))
              .pipe(tslint.report()));

gulp.task('depcheck', ['build'], () => {
  return depcheck(__dirname, {
           // "@types/*" dependencies are type declarations that are
           // automatically
           // loaded by TypeScript during build. depcheck can't detect this
           // so we ignore them here. Same with babel plugins.
           ignoreMatches: [
             '@types/*',
             'vinyl',
             'babel-plugin-external-helpers',
             'polymer-bundler',
            ],
         })
      .then((result) => {
        let invalidFiles = Object.keys(result.invalidFiles) || [];
        let invalidJsFiles = invalidFiles.filter((f) => f.endsWith('.js'));
        if (invalidJsFiles.length > 0) {
          throw new Error(`Invalid files: ${invalidJsFiles}`);
        }
        if (result.dependencies.length) {
          throw new Error(`Unused dependencies: ${result.dependencies}`);
        }
      });
});
<|endoftext|>


<commit_before>/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import React, { Component, PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { connect } from 'react-redux';

import GroupToolbar from '../../components/GroupToolbar/GroupToolbar';
import Link from '../../components/Link/Link';
import TimeShareContainer from '../../components/Timetable/TimeShareContainer';

import s from './Group.css';
import { fetchGroups } from '../../actions/group';
import { fetchFriends } from '../../actions/friend';
import { fetchGroupTimetable } from '../../actions/timeshare';

const title = 'Groups';

class Group extends Component {
  static propTypes = {
    friend: PropTypes.object.isRequired,
    group: PropTypes.object.isRequired,
    isLoggedIn: PropTypes.bool.isRequired,
    semester: PropTypes.string.isRequired,
    year: PropTypes.string.isRequired,
    fetchGroups: PropTypes.func.isRequired,
    fetchGroupTimetable: PropTypes.func.isRequired,
  }

  static contextTypes = {
    setTitle: PropTypes.func.isRequired,
  }

  state = {
    groupShown: null,
    groupId: null,
    date: this.formatDate(new Date()),
  }

  componentWillMount() {
    // Initialize timesharing table
    this.state.groupShown = this.props.group.data[0] || {};
    this.state.groupId = this.state.groupShown.teamId;
    this.props.fetchGroupTimetable({ groupId: this.state.groupId, date: this.state.date });
  }

  componentDidMount() {
    const { year, semester, isLoggedIn, group } = this.props;
    if (!group.isInitialized && isLoggedIn) {
      this.props.fetchGroups({ year, semester });
    }
  }

  formatDate(date) {
    return date.getFullYear() + '-' +  ('00' + (date.getMonth() + 1)).slice(-2) + '-' + date.getDate();
  }

  handleGroupChange = (event, key, groupId) => {
    this.state.groupShown = this.props.group.data.find(d => d.teamId === groupId);
    this.state.groupId = groupId;
    this.props.fetchGroupTimetable({ groupId, date: this.state.date });
  }

  handleDateChange = (event, date) => {
    this.state.date = this.formatDate(date);
    console.log(this.state);
    if (this.state.groupId) {
        this.props.fetchGroupTimetable({ groupId: this.state.groupId, date: this.state.date });
    }
    return { event, date };
  }

  render() {
    if (!this.props.isLoggedIn) {
      return (

            Please
            <Link to="/login">
              login
            </Link>
             to compare your timetable!

      );
    }
    this.context.setTitle(title);

    const noGroupContainer = (

You do not have any groups. Create one!

    );

    /* eslint-disable */
    const dummydata = {
            "createdBy": {
                "userId": "3",
                "name": "Zhi An"
            },
            "teamId": "{id}",
            "teamName": "3216 final project",
            "year": "2016-2017",
            "semester": "1",
            "members": [
                {
                    "userId": "1",
                    "name": "Niko",
                    "acceptInvitation": 1,
                    "timetable": [
                        {
                            "id": 2,
                            "timetableId": 1,
                            "moduleId": 514,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.449Z",
                            "updatedAt": "2016-09-12T08:26:51.449Z",
                            "module": {
                                "id": 514,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1010",
                                "title": "Programming Methodology",
                                "description": "This is CS1010.",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "2-1-1-3-3",
                                "prerequisite": null,
                                "preclusion": "CG1101, CS1010E",
                                "examDate": "2016-11-23T01:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "B3",
                                    "LessonType": "Laboratory",
                                    "WeekText": "Odd Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1400",
                                    "EndTime": "1700",
                                    "Venue": "S12-0402"
                                },
                                "createdAt": "2016-09-12T08:33:46.506Z",
                                "updatedAt": "2016-09-12T08:33:46.507Z"
                            }
                        },
                        {
                            "id": 3,
                            "timetableId": 1,
                            "moduleId": 522,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.450Z",
                            "updatedAt": "2016-09-12T08:26:51.450Z",
                            "module": {
                                "id": 522,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1231",
                                "title": "Discrete Structures",
                                "description": "This is CS1231",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "3-1-0-3-3",
                                "prerequisite": "A-level Mathematics or H2 Mathematics or MA1301 or MA1301FC or MA1301X",
                                "preclusion": "MA1100",
                                "examDate": "2016-11-23T09:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "4",
                                    "LessonType": "Tutorial",
                                    "WeekText": "Every Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1100",
                                    "EndTime": "1200",
                                    "Venue": "COM1-0209"
                                },
                                "createdAt": "2016-09-12T08:33:46.542Z",
                                "updatedAt": "2016-09-12T08:33:46.542Z"
                            }
                        }
                    ]
                },
                {
                    "userId": "3",
                    "name": "Zhi An",
                    "acceptInvitation": 0,
                    "timetable": [
                        {
                            "id": 3,
                            "timetableId": 1,
                            "moduleId": 522,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.450Z",
                            "updatedAt": "2016-09-12T08:26:51.450Z",
                            "module": {
                                "id": 522,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1231",
                                "title": "Discrete Structures",
                                "description": "This is CS1231",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "3-1-0-3-3",
                                "prerequisite": "A-level Mathematics or H2 Mathematics or MA1301 or MA1301FC or MA1301X",
                                "preclusion": "MA1100",
                                "examDate": "2016-11-23T09:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "4",
                                    "LessonType": "Tutorial",
                                    "WeekText": "Every Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1100",
                                    "EndTime": "1200",
                                    "Venue": "COM1-0209"
                                },
                                "createdAt": "2016-09-12T08:33:46.542Z",
                                "updatedAt": "2016-09-12T08:33:46.542Z"
                            }
                        }
                    ]
                }
            ]
        };
    /* eslint-enable */

    var timeShareContainer = noGroupContainer;
    if (!this.props.group.isFetching && this.props.group.isInitialized) {
      if (this.props.group.data.length > 0 && !this.props.timeshare.isFetching) {
        const groupData = this.props.timeshare.data || {};
        timeShareContainer = <TimeShareContainer group={groupData} />;
      }
    }

    return (

        <GroupToolbar
          groupShown={this.state.groupShown}
          groups={this.props.group.data}
          handleGroupChange={this.handleGroupChange}
          handleDateChange={this.handleDateChange}
        />
        {timeShareContainer}

    );
  }
}

const mapState = (state) => ({
  year: state.selection.year,
  semester: state.selection.semester,
  isLoggedIn: !!state.user.data.id,
  group: state.group,
  friend: state.friend,
  timeshare: state.timeshare,
});

const mapDispatch = {
  fetchGroups,
  fetchFriends,
  fetchGroupTimetable,
};

export default connect(mapState, mapDispatch)(withStyles(s)(Group));
<commit_msg>Pad date with leading zero if single digit<commit_after>/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import React, { Component, PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import { connect } from 'react-redux';

import GroupToolbar from '../../components/GroupToolbar/GroupToolbar';
import Link from '../../components/Link/Link';
import TimeShareContainer from '../../components/Timetable/TimeShareContainer';

import s from './Group.css';
import { fetchGroups } from '../../actions/group';
import { fetchFriends } from '../../actions/friend';
import { fetchGroupTimetable } from '../../actions/timeshare';

const title = 'Groups';

class Group extends Component {
  static propTypes = {
    friend: PropTypes.object.isRequired,
    group: PropTypes.object.isRequired,
    isLoggedIn: PropTypes.bool.isRequired,
    semester: PropTypes.string.isRequired,
    year: PropTypes.string.isRequired,
    fetchGroups: PropTypes.func.isRequired,
    fetchGroupTimetable: PropTypes.func.isRequired,
  }

  static contextTypes = {
    setTitle: PropTypes.func.isRequired,
  }

  state = {
    groupShown: null,
    groupId: null,
    date: this.formatDate(new Date()),
  }

  componentWillMount() {
    // Initialize timesharing table
    this.state.groupShown = this.props.group.data[0] || {};
    this.state.groupId = this.state.groupShown.teamId;
    this.props.fetchGroupTimetable({ groupId: this.state.groupId, date: this.state.date });
  }

  componentDidMount() {
    const { year, semester, isLoggedIn, group } = this.props;
    if (!group.isInitialized && isLoggedIn) {
      this.props.fetchGroups({ year, semester });
    }
  }

  formatDate(date) {
    return date.getFullYear() + '-' +  ('00' + (date.getMonth() + 1)).slice(-2) + '-' + ('00' + date.getDate()).slice(-2);
  }

  handleGroupChange = (event, key, groupId) => {
    this.state.groupShown = this.props.group.data.find(d => d.teamId === groupId);
    this.state.groupId = groupId;
    this.props.fetchGroupTimetable({ groupId, date: this.state.date });
  }

  handleDateChange = (event, date) => {
    this.state.date = this.formatDate(date);
    console.log(this.state);
    if (this.state.groupId) {
        this.props.fetchGroupTimetable({ groupId: this.state.groupId, date: this.state.date });
    }
    return { event, date };
  }

  render() {
    if (!this.props.isLoggedIn) {
      return (

            Please
            <Link to="/login">
              login
            </Link>
             to compare your timetable!

      );
    }
    this.context.setTitle(title);

    const noGroupContainer = (

You do not have any groups. Create one!

    );

    /* eslint-disable */
    const dummydata = {
            "createdBy": {
                "userId": "3",
                "name": "Zhi An"
            },
            "teamId": "{id}",
            "teamName": "3216 final project",
            "year": "2016-2017",
            "semester": "1",
            "members": [
                {
                    "userId": "1",
                    "name": "Niko",
                    "acceptInvitation": 1,
                    "timetable": [
                        {
                            "id": 2,
                            "timetableId": 1,
                            "moduleId": 514,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.449Z",
                            "updatedAt": "2016-09-12T08:26:51.449Z",
                            "module": {
                                "id": 514,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1010",
                                "title": "Programming Methodology",
                                "description": "This is CS1010.",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "2-1-1-3-3",
                                "prerequisite": null,
                                "preclusion": "CG1101, CS1010E",
                                "examDate": "2016-11-23T01:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "B3",
                                    "LessonType": "Laboratory",
                                    "WeekText": "Odd Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1400",
                                    "EndTime": "1700",
                                    "Venue": "S12-0402"
                                },
                                "createdAt": "2016-09-12T08:33:46.506Z",
                                "updatedAt": "2016-09-12T08:33:46.507Z"
                            }
                        },
                        {
                            "id": 3,
                            "timetableId": 1,
                            "moduleId": 522,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.450Z",
                            "updatedAt": "2016-09-12T08:26:51.450Z",
                            "module": {
                                "id": 522,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1231",
                                "title": "Discrete Structures",
                                "description": "This is CS1231",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "3-1-0-3-3",
                                "prerequisite": "A-level Mathematics or H2 Mathematics or MA1301 or MA1301FC or MA1301X",
                                "preclusion": "MA1100",
                                "examDate": "2016-11-23T09:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "4",
                                    "LessonType": "Tutorial",
                                    "WeekText": "Every Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1100",
                                    "EndTime": "1200",
                                    "Venue": "COM1-0209"
                                },
                                "createdAt": "2016-09-12T08:33:46.542Z",
                                "updatedAt": "2016-09-12T08:33:46.542Z"
                            }
                        }
                    ]
                },
                {
                    "userId": "3",
                    "name": "Zhi An",
                    "acceptInvitation": 0,
                    "timetable": [
                        {
                            "id": 3,
                            "timetableId": 1,
                            "moduleId": 522,
                            "lessonType": "sectional",
                            "classNumber": 1,
                            "createdAt": "2016-09-12T08:26:51.450Z",
                            "updatedAt": "2016-09-12T08:26:51.450Z",
                            "module": {
                                "id": 522,
                                "year": "2016-2017",
                                "semester": 1,
                                "code": "CS1231",
                                "title": "Discrete Structures",
                                "description": "This is CS1231",
                                "department": "Computer Science",
                                "credit": 4,
                                "workload": "3-1-0-3-3",
                                "prerequisite": "A-level Mathematics or H2 Mathematics or MA1301 or MA1301FC or MA1301X",
                                "preclusion": "MA1100",
                                "examDate": "2016-11-23T09:00:00.000Z",
                                "timetable": {
                                    "ClassNo": "4",
                                    "LessonType": "Tutorial",
                                    "WeekText": "Every Week",
                                    "DayText": "Wednesday",
                                    "StartTime": "1100",
                                    "EndTime": "1200",
                                    "Venue": "COM1-0209"
                                },
                                "createdAt": "2016-09-12T08:33:46.542Z",
                                "updatedAt": "2016-09-12T08:33:46.542Z"
                            }
                        }
                    ]
                }
            ]
        };
    /* eslint-enable */

    var timeShareContainer = noGroupContainer;
    if (!this.props.group.isFetching && this.props.group.isInitialized) {
      if (this.props.group.data.length > 0 && !this.props.timeshare.isFetching) {
        const groupData = this.props.timeshare.data || {};
        timeShareContainer = <TimeShareContainer group={groupData} />;
      }
    }

    return (

        <GroupToolbar
          groupShown={this.state.groupShown}
          groups={this.props.group.data}
          handleGroupChange={this.handleGroupChange}
          handleDateChange={this.handleDateChange}
        />
        {timeShareContainer}

    );
  }
}

const mapState = (state) => ({
  year: state.selection.year,
  semester: state.selection.semester,
  isLoggedIn: !!state.user.data.id,
  group: state.group,
  friend: state.friend,
  timeshare: state.timeshare,
});

const mapDispatch = {
  fetchGroups,
  fetchFriends,
  fetchGroupTimetable,
};

export default connect(mapState, mapDispatch)(withStyles(s)(Group));
<|endoftext|>


<commit_before>/*
 *  asf -- Ada Server Faces
 *  Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015, 2016 Stephane Carrez
 *  Written by Stephane Carrez (Stephane.Carrez@gmail.com)
 *
 *  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.
 */

/**
 * @class Ada Server Faces namespace.
 */
var ASF = {};

(function () {
    ASF.Actions = {
        update: function(id, action) {
            $(id).html(action.data);
        },
        prepend: function(id, action) {
            $(id).prepend(action.data);
        },
        append: function(id, action) {
            $(id).append(action.data);
        },
        show: function(id, action) {
            $(id).show('slow');
        },
        hide: function(id, action) {
            $(id).hide('slow');
        },
        "delete": function(id, action) {
            $(id).remove();
        },
        removeClass: function(id, action) {
            $(id).removeClass(action.data);
        },
        addClass: function(id, action) {
            $(id).addClass(action.data);
        },
        fadeIn: function(id, action) {
            $(id).fadeIn('slow');
        },
        fadeOut: function(id, action) {
            $(id).fadeOut('slow');
        },
        slideUp: function(id, action) {
            $(id).slideUp('slow');
        },
        slideDown: function(id, action) {
            $(id).slideDown('slow');
        },
        closeDialog: function(id, action) {
            $(id).dialog('close');
        },
        closePopup: function(id, action) {
            $(id).popup('close');
        },
        redirect: function(id, action) {
            document.location = action.url;
        },
        message: function(id, action) {
            ASF.Message(node, action.id, action.data);
        },
        notification: function(id, action) {
	        ASF.Message(node, action.id, action.data, 'asf-notification').message('autoClose');
        },
        get: function(id, action) {
            ASF.Update(null, action.url, id);
        },
        script: function(id, action) {
            try {
                eval(action.script);
            } catch (e) {
                alert(e);
            }
        },
        clear: function(id, action) {
	        $(id).each(function() {
                switch (this.type) {
                case 'password':
                case 'select-multiple':
                case 'select-one':
                case 'text':
                case 'textarea':
                    $(this).val('');
                    break;
                case 'checkbox':
                case 'radio':
                    this.checked = false;
                    break;
                }
            });
        }
    };

    /**
     * Execute the AJAX response action represented by the JSON object action.
     *
     * @param node the current node
     * @param action the JSON action object
     */
    ASF.ExecuteOne = function(node, action) {
        var id = action.id ? action.id : node;
        if (action.child) {
            id = $(id).children(action.child);
        }
        if (ASF.Actions.hasOwnProperty(action.action)) {
            var handler = ASF.Actions[action.action];
            handler(id, action);
        }
    };

    ASF.Execute = function(node, data) {
        if (data) {
            for (var i = 0; i < data.length; i++) {
                ASF.ExecuteOne(node, data[i]);
            }
        }
    };

    /**
     * Handle an AJAX operation that failed.
     */
    ASF.AjaxError = function(jqXHDR, status, error, node) {
        var contentType = jqXHDR.getResponseHeader('Content-type');
        if (contentType && contentType.match(/^application\/json(;.*)?$/i)) {
            var data = $.parseJSON(jqXHDR.responseText);
            ASF.Execute(node, data);
        } else {
            ASF.Message(node, null, "Operation failed");
        }
    };

    /**
     * Update the target container with the content of the AJAX GET request
     * on the given URL.  The target parameter is optional.
     *
     * @param node the current element
     * @param url the URL to fetch using an HTTP GET
     * @param target the optional target element
     * @param complete an optional function called when the response is received
     */
    ASF.Update = function(node, url, target, complete) {
        /* Find the container to update */
        var d;
        if (target != null) {
            d = $(target);
        } else {
            d = $(node);
            if (!d.is('.asf-container')) {
                d = d.parent('.asf-container');
            }
        }
        if (d.length > 0) {
            /* Perform the HTTP GET */
            jQuery.ajax({
                url: url,
                context: document.body,
                success: function(data, status, jqXHDR) {
                    var contentType = jqXHDR.getResponseHeader('Content-type');
                    if (contentType == null) {
                        contentType = "text/html";
                    }
                    if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                        d.html(jqXHDR.responseText);

                    } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                        ASF.Execute(d, data);
                    }
                    if (complete != null) {
                        complete(d, jqXHDR);
                    }
               },
               error: function(jqXHDR, status, error) {
                   ASF.AjaxError(jqXHDR, status, error, d);
               }
            });
        }
    };

    /**
     * Submit the current form.  The node element refers to the element being clicked.
     * From that element, we can identify the form to submit.  The element to refresh upon
     * the form submit is an outer element that should have the asf-container class.
     * If not such element as parent node, the parent node of the form is used.
     *
     * After the form is submitted, the response is executed if it is a JSON response.
     * If the response is an HTML content, the container element is refreshed with it.
     *
     * @param node the current element
     * @param url the URL to fetch using an HTTP GET
     * @param target the optional target element
     */
    ASF.Submit = function(node) {
        /* Find the form and container to update */
        var f = $(node).closest("form");
        var d = $(f);
        if (!d.is('.asf-container,ui-dialog-content')) {
            d = d.closest('.asf-container,.ui-dialog-content');
        }
        if (d.length == 0) {
            d = $(f).parent();
        }
        if (d.length > 0) {
            var params;
            var params = $(f).serialize();
            var url = $(f).attr('action');

            if (node.tagName == 'a' || node.tagName == 'A') {
                params = node.id + "=1&" + $(f).serialize();
            } else {
                params = node.name + "=1&" + $(f).serialize();
            }
            /**
             * Send notification on form to indicate a form submit is now in progress.
             */
            $(f).triggerHandler("notifySubmit", d);

            /* Perform the HTTP POST */
            jQuery.ajax({
                type: "POST",
                url: url,
                data: params,
                context: document.body,
                success: function(data, status, jqXHDR) {
                    var contentType = jqXHDR.getResponseHeader('Content-type');
                    if (contentType == null) {
                        contentType = "text/html";
                    }
                    $(f).triggerHandler("successSubmit", d);
                    if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                        d.fadeOut("fast", function() {
                            d.html(jqXHDR.responseText);
                            d.fadeIn("fast");
                        });

                    } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                        ASF.Execute(d, data);
                    }
               },
               error: function(jqXHDR, status, error) {
                   ASF.AjaxError(jqXHDR, status, error, d);
               }
            });
        }
        return false;
    };

    /**
     * Perform an HTTP POST on the given URL.
     *
     * @param node the node element that will be updated by default on the AJAX response.
     * @param url the HTTP URL.
     * @param params the HTTP POST parameters.
     */
    ASF.Post = function(node, url, params) {
        /* Perform the HTTP POST */
        jQuery.ajax({
            type: "POST",
            url: url,
            data: params,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(node).html(jqXHDR.responseText);

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(node, data);
                }
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, node);
            }
        });
        return false;
    };

    /**
     * Open a popup form attached to the node element and which is filled by
     * the given URL.
     *
     * The popup form is created when the page refered by <tt>url</tt> is fetched.
     * It can be referenced by the unique id given in <tt>id</tt>.
     *
     * @param node the node element where the popup is attached to.
     * @param id the popup unique id.
     * @param url the URL to fetch to fill the popup content.
     * @param params the popup creation parameters.
     */
    ASF.Popup = function(node, id, url, params) {
        jQuery.ajax({
            type: "GET",
            url: url,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                var div = $('#' + id);
                if (div.length == 0) {
                    div = document.createElement("div");
                    div.id = id;
                    $(document.body).append($(div));
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(div).html(jqXHDR.responseText);

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(div, data);
                }
                $(div).popup(params).popup('open');
                $(div).find('form:not(.filter) :input:visible:first').focus();
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, node);
            }
        });
        return false;
    };

    /**
     * Open a dialog box and fetch the dialog content from the given URI.
     *
     * @param node the current element
     * @param id the dialog id
     * @param url the URL to fetch using an HTTP GET
     * @param params the optional dialog options
     */
    ASF.OpenDialog = function(node, id, url, params) {
        var div = $(id);
        if (div.length > 0) {
            return false;
        }

        div = document.createElement("div");
        div.id = id;
        div = $(div);

        var d = $(document.body);
        if (d.length == 0) {
            return false;
        }

        $(d).append($(div));

        /* Perform the HTTP GET */
        jQuery.ajax({
            url: url,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(div).dialog({
                        autoOpen: false,
                        show: "blind",
                        hide: "explode",
                        minWidth: 600,
                        close: function() {
                            $(div).dialog('destroy');
                            $(div).remove();
                            if (params && params.close) {
                                params.close(node);
                            }
                        }
                     });

                     $(div).html(jqXHDR.responseText);

                     /**
                      * Extract a title from the inner form to setup the dialog box.
                      */
                     var dTitle, dBox = $(div).children('div');
                     if (dBox.length == 0) {
                        dTitle = $(div).children('h2');
                     } else {
                        dTitle = $(dBox).children('h2');
                     }
                     if (dTitle.length > 0) {
                        $(div).dialog("option", "title", dTitle.html());
                        dTitle.remove();
                     } else {
                        dTitle = $(div).children('div').attr('title');
                        if (dTitle != null) {
                            $(div).dialog("option", "title", dTitle );
                            /* $(div).attr('title', title); */
                        }
                     }
                     $(div).dialog('open');

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(node, data);
                }
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, d);
            }
        });
        return false;
    };

    /**
     * Close the dialog box identified by the given identifier.
     *
     * @param id the dialog box identifier
     */
    ASF.CloseDialog = function(id) {
        $('#' + id).dialog('close');
        return false;
    };

    /**
     * Create and display a popup message near the node element.
     *
     * @param node the element giving the location of the message
     * @param id the message id or null
     * @param message the HTML message content
     * @param css the optional CSS to set on the message content.
     */
    ASF.Message = function(node, id, message, css) {
        if (!id) {
            id = "#asf-message";
        }
        var div = $(id);
        if (div.length == 0) {
            div = document.createElement("div");
            div.id = id.substring(1);
            $(document.body).append($(div));
        } else {
            $(div).removeClass();
        }
        if (css) {
            $(div).addClass(css);
        }
        return $(div).html(message).message({}).message("attachment", node);
    };
})();

<commit_msg>Fix and improvement of the 'notification' and 'message' actions to allow the definition of the message/notification attachment node<commit_after>/*
 *  asf -- Ada Server Faces
 *  Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015, 2016 Stephane Carrez
 *  Written by Stephane Carrez (Stephane.Carrez@gmail.com)
 *
 *  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.
 */

/**
 * @class Ada Server Faces namespace.
 */
var ASF = {};

(function () {
    ASF.Actions = {
        update: function(id, action) {
            $(id).html(action.data);
        },
        prepend: function(id, action) {
            $(id).prepend(action.data);
        },
        append: function(id, action) {
            $(id).append(action.data);
        },
        show: function(id, action) {
            $(id).show('slow');
        },
        hide: function(id, action) {
            $(id).hide('slow');
        },
        "delete": function(id, action) {
            $(id).remove();
        },
        removeClass: function(id, action) {
            $(id).removeClass(action.data);
        },
        addClass: function(id, action) {
            $(id).addClass(action.data);
        },
        fadeIn: function(id, action) {
            $(id).fadeIn('slow');
        },
        fadeOut: function(id, action) {
            $(id).fadeOut('slow');
        },
        slideUp: function(id, action) {
            $(id).slideUp('slow');
        },
        slideDown: function(id, action) {
            $(id).slideDown('slow');
        },
        closeDialog: function(id, action) {
            $(id).dialog('close');
        },
        closePopup: function(id, action) {
            $(id).popup('close');
        },
        redirect: function(id, action) {
            document.location = action.url;
        },
        message: function(id, action, node) {
            ASF.Message(action.attach ? action.attach : node, action.id, action.data);
        },
        notification: function(id, action, node) {
	        ASF.Message(action.attach ? action.attach : node,
	                    action.id, action.data, 'asf-notification').message('autoClose');
        },
        get: function(id, action) {
            ASF.Update(null, action.url, id);
        },
        script: function(id, action) {
            try {
                eval(action.script);
            } catch (e) {
                alert(e);
            }
        },
        clear: function(id, action) {
	        $(id).each(function() {
                switch (this.type) {
                case 'password':
                case 'select-multiple':
                case 'select-one':
                case 'text':
                case 'textarea':
                    $(this).val('');
                    break;
                case 'checkbox':
                case 'radio':
                    this.checked = false;
                    break;
                }
            });
        }
    };

    /**
     * Execute the AJAX response action represented by the JSON object action.
     *
     * @param node the current node
     * @param action the JSON action object
     */
    ASF.ExecuteOne = function(node, action) {
        var id = action.id ? action.id : node;
        if (action.child) {
            id = $(id).children(action.child);
        }
        if (ASF.Actions.hasOwnProperty(action.action)) {
            var handler = ASF.Actions[action.action];
            handler(id, action);
        }
    };

    ASF.Execute = function(node, data) {
        if (data) {
            for (var i = 0; i < data.length; i++) {
                ASF.ExecuteOne(node, data[i]);
            }
        }
    };

    /**
     * Handle an AJAX operation that failed.
     */
    ASF.AjaxError = function(jqXHDR, status, error, node) {
        var contentType = jqXHDR.getResponseHeader('Content-type');
        if (contentType && contentType.match(/^application\/json(;.*)?$/i)) {
            var data = $.parseJSON(jqXHDR.responseText);
            ASF.Execute(node, data);
        } else {
            ASF.Message(node, null, "Operation failed");
        }
    };

    /**
     * Update the target container with the content of the AJAX GET request
     * on the given URL.  The target parameter is optional.
     *
     * @param node the current element
     * @param url the URL to fetch using an HTTP GET
     * @param target the optional target element
     * @param complete an optional function called when the response is received
     */
    ASF.Update = function(node, url, target, complete) {
        /* Find the container to update */
        var d;
        if (target != null) {
            d = $(target);
        } else {
            d = $(node);
            if (!d.is('.asf-container')) {
                d = d.parent('.asf-container');
            }
        }
        if (d.length > 0) {
            /* Perform the HTTP GET */
            jQuery.ajax({
                url: url,
                context: document.body,
                success: function(data, status, jqXHDR) {
                    var contentType = jqXHDR.getResponseHeader('Content-type');
                    if (contentType == null) {
                        contentType = "text/html";
                    }
                    if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                        d.html(jqXHDR.responseText);

                    } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                        ASF.Execute(d, data);
                    }
                    if (complete != null) {
                        complete(d, jqXHDR);
                    }
               },
               error: function(jqXHDR, status, error) {
                   ASF.AjaxError(jqXHDR, status, error, d);
               }
            });
        }
    };

    /**
     * Submit the current form.  The node element refers to the element being clicked.
     * From that element, we can identify the form to submit.  The element to refresh upon
     * the form submit is an outer element that should have the asf-container class.
     * If not such element as parent node, the parent node of the form is used.
     *
     * After the form is submitted, the response is executed if it is a JSON response.
     * If the response is an HTML content, the container element is refreshed with it.
     *
     * @param node the current element
     * @param url the URL to fetch using an HTTP GET
     * @param target the optional target element
     */
    ASF.Submit = function(node) {
        /* Find the form and container to update */
        var f = $(node).closest("form");
        var d = $(f);
        if (!d.is('.asf-container,ui-dialog-content')) {
            d = d.closest('.asf-container,.ui-dialog-content');
        }
        if (d.length == 0) {
            d = $(f).parent();
        }
        if (d.length > 0) {
            var params;
            var params = $(f).serialize();
            var url = $(f).attr('action');

            if (node.tagName == 'a' || node.tagName == 'A') {
                params = node.id + "=1&" + $(f).serialize();
            } else {
                params = node.name + "=1&" + $(f).serialize();
            }
            /**
             * Send notification on form to indicate a form submit is now in progress.
             */
            $(f).triggerHandler("notifySubmit", d);

            /* Perform the HTTP POST */
            jQuery.ajax({
                type: "POST",
                url: url,
                data: params,
                context: document.body,
                success: function(data, status, jqXHDR) {
                    var contentType = jqXHDR.getResponseHeader('Content-type');
                    if (contentType == null) {
                        contentType = "text/html";
                    }
                    $(f).triggerHandler("successSubmit", d);
                    if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                        d.fadeOut("fast", function() {
                            d.html(jqXHDR.responseText);
                            d.fadeIn("fast");
                        });

                    } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                        ASF.Execute(d, data);
                    }
               },
               error: function(jqXHDR, status, error) {
                   ASF.AjaxError(jqXHDR, status, error, d);
               }
            });
        }
        return false;
    };

    /**
     * Perform an HTTP POST on the given URL.
     *
     * @param node the node element that will be updated by default on the AJAX response.
     * @param url the HTTP URL.
     * @param params the HTTP POST parameters.
     */
    ASF.Post = function(node, url, params) {
        /* Perform the HTTP POST */
        jQuery.ajax({
            type: "POST",
            url: url,
            data: params,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(node).html(jqXHDR.responseText);

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(node, data);
                }
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, node);
            }
        });
        return false;
    };

    /**
     * Open a popup form attached to the node element and which is filled by
     * the given URL.
     *
     * The popup form is created when the page refered by <tt>url</tt> is fetched.
     * It can be referenced by the unique id given in <tt>id</tt>.
     *
     * @param node the node element where the popup is attached to.
     * @param id the popup unique id.
     * @param url the URL to fetch to fill the popup content.
     * @param params the popup creation parameters.
     */
    ASF.Popup = function(node, id, url, params) {
        jQuery.ajax({
            type: "GET",
            url: url,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                var div = $('#' + id);
                if (div.length == 0) {
                    div = document.createElement("div");
                    div.id = id;
                    $(document.body).append($(div));
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(div).html(jqXHDR.responseText);

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(div, data);
                }
                $(div).popup(params).popup('open');
                $(div).find('form:not(.filter) :input:visible:first').focus();
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, node);
            }
        });
        return false;
    };

    /**
     * Open a dialog box and fetch the dialog content from the given URI.
     *
     * @param node the current element
     * @param id the dialog id
     * @param url the URL to fetch using an HTTP GET
     * @param params the optional dialog options
     */
    ASF.OpenDialog = function(node, id, url, params) {
        var div = $(id);
        if (div.length > 0) {
            return false;
        }

        div = document.createElement("div");
        div.id = id;
        div = $(div);

        var d = $(document.body);
        if (d.length == 0) {
            return false;
        }

        $(d).append($(div));

        /* Perform the HTTP GET */
        jQuery.ajax({
            url: url,
            context: document.body,
            success: function(data, status, jqXHDR) {
                var contentType = jqXHDR.getResponseHeader('Content-type');
                if (contentType == null) {
                    contentType = "text/html";
                }
                if (contentType.match(/^text\/(html|xml)(;.*)?$/i)) {
                    $(div).dialog({
                        autoOpen: false,
                        show: "blind",
                        hide: "explode",
                        minWidth: 600,
                        close: function() {
                            $(div).dialog('destroy');
                            $(div).remove();
                            if (params && params.close) {
                                params.close(node);
                            }
                        }
                     });

                     $(div).html(jqXHDR.responseText);

                     /**
                      * Extract a title from the inner form to setup the dialog box.
                      */
                     var dTitle, dBox = $(div).children('div');
                     if (dBox.length == 0) {
                        dTitle = $(div).children('h2');
                     } else {
                        dTitle = $(dBox).children('h2');
                     }
                     if (dTitle.length > 0) {
                        $(div).dialog("option", "title", dTitle.html());
                        dTitle.remove();
                     } else {
                        dTitle = $(div).children('div').attr('title');
                        if (dTitle != null) {
                            $(div).dialog("option", "title", dTitle );
                            /* $(div).attr('title', title); */
                        }
                     }
                     $(div).dialog('open');

                } else if (contentType.match(/^application\/json(;.*)?$/i)) {
                    ASF.Execute(node, data);
                }
            },
            error: function(jqXHDR, status, error) {
                ASF.AjaxError(jqXHDR, status, error, d);
            }
        });
        return false;
    };

    /**
     * Close the dialog box identified by the given identifier.
     *
     * @param id the dialog box identifier
     */
    ASF.CloseDialog = function(id) {
        $('#' + id).dialog('close');
        return false;
    };

    /**
     * Create and display a popup message near the node element.
     *
     * @param node the element giving the location of the message
     * @param id the message id or null
     * @param message the HTML message content
     * @param css the optional CSS to set on the message content.
     */
    ASF.Message = function(node, id, message, css) {
        if (!id) {
            id = "#asf-message";
        }
        var div = $(id);
        if (div.length == 0) {
            div = document.createElement("div");
            div.id = id.substring(1);
            $(document.body).append($(div));
        } else {
            $(div).removeClass();
        }
        if (css) {
            $(div).addClass(css);
        }
        return $(div).html(message).message({}).message("attachment", node);
    };
})();

<|endoftext|>


<commit_before>'use strict';

var express = require('express');
var bodyParser = require('body-parser');
var morganLog = require('morgan');
var favicon = require('serve-favicon');

var tracklist = require('./tracklist');
var redis = require('redis').createClient();
var config = require('./config');
var trackBuilder = require('./track_builder');
var socket = require('./socket')();
var playerState = require('./player_state');
var listeners = [];
var logger = require('./logger');

module.exports = function (app, playerManager) {
  playerManager.stream.on('data', _sendChunk);

  app.use(express.static(__dirname + '/../public'));
  app.use(morganLog('dev'));
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(bodyParser.json());
  app.set('views', __dirname + '/../public');
  app.set('view engine', 'ejs');
  app.engine('html', require('ejs').renderFile);
  app.use(favicon(__dirname + '/../public/jukebox.png'));

  var allowedMethods = 'GET,PUT,POST,DELETE,OPTIONS';
  var allowedHeaders = 'Content-Length,Content-Type';
  var allowedHosts = [
    'http://soundcloud.com',
    'https://soundcloud.com',
    'http://youtube.com',
    'https://youtube.com',
    'http://www.youtube.com',
    'https://www.youtube.com'
  ];
  function _allowCrossDomain(req, res, next) {
    if (allowedHosts.indexOf(req.headers.origin) !== -1) {
      res.header('Access-Control-Allow-Origin', req.headers.origin);
      res.header('Access-Control-Allow-Methods', allowedMethods);
      res.header('Access-Control-Allow-Headers', allowedHeaders);
      if ('OPTIONS' === req.method) return res.status(200).end();
    }
    next();
  }

  app.route('/player')
  .get(function (req, res) {
    res.send(playerState);
  })
  .post(function (req, res) {
    if (!playerState.playing) playerManager.start();
    res.status(201).end();
  })
  .delete(function (req, res) {
    if (playerState.playing) playerManager.stop();
    res.status(201).end();
  });

  app.route('/tracks')
  .options(_allowCrossDomain)
  .get(function (req, res) {
    tracklist.get()
    .then(function (list) {
      res.send(list);
    }, function (err) {
      res.status(503).send(err.message);
    });
  })
  .post(_allowCrossDomain, function (req, res) {
    if (!req.body.url) return res.status(400).send('You need to provide a track URL.');
    trackBuilder.fromString(req.body.url)
    .then(tracklist.add)
    .then(function (track) {
      socket.emit('tracks:new');
      res.status(201).send(track);
    }, function (err) {
      res.status(500).send(err.message);
    });
  });

  app.route('/current')
  .get(function (req, res) {
    tracklist.current()
    .then(function (track) {
      if (!track) return res.send({});

      redis.scard('jukebox:votes', function (err, count) {
        if (err) return res.status(500).send(err);
        track.votes = {
          favorable: count,
          total: config.minVotes
        };
        res.send(track);
      });
    }, function (err) {
      res.status(500).send(err.message);
    });
  })
  .post(function (req, res) {
    redis.sadd('jukebox:votes', req.ip, function (err, newCount) {
      if (err) return res.status(500).send(err);
      res.send(201).end();
      if (newCount > 0) {
        socket.emit('current:votes');
        _checkVotesNext();
      }
    });
  })
  .delete(function (req, res) {
    playerManager.stop();
    tracklist.next().then(function () {
      playerManager.start();
    });
    res.status(201).end();
  });

  app.get('/history', function (req, res) {
    tracklist.history()
    .then(function (list) {
      res.send(list);
    }, function (err) {
      res.status(500).send(err.message);
    });
  });

  app.get('/stream', function (req, res) {
    res.set({
      'Content-Type': 'audio/mpeg',
      'Transfer-Encoding': 'chunked'
    });

    _addListener(res);

    function _onEnd() {
      _removeListener(res);
    }

    res.on('close', _onEnd);
    res.on('finish', _onEnd);
  });

  /**
  * Check whether we need to skip the track.
  */
  function _checkVotesNext() {
    redis.scard('jukebox:votes', function (err, count) {
      if (err) return logger.log(err);
      if (count < config.minVotes) return;
      tracklist.next();
      playerManager.stop();
      playerManager.start();
    });
  }
};

function _addListener(res) {
  logger.log('Adding listener');
  listeners.push(res);
}

function _removeListener(res) {
  var idx = listeners.indexOf(res);
  listeners.splice(idx, 1);
  logger.log('Removed listener. ' + listeners.length + ' are left.');
}

// Listeners are 'res' objects
function _sendChunk(chunk) {
  listeners.forEach(function (listener) {
    listener.write(chunk);
  });
}
<commit_msg>Added: no cache headers on the stream<commit_after>'use strict';

var express = require('express');
var bodyParser = require('body-parser');
var morganLog = require('morgan');
var favicon = require('serve-favicon');

var tracklist = require('./tracklist');
var redis = require('redis').createClient();
var config = require('./config');
var trackBuilder = require('./track_builder');
var socket = require('./socket')();
var playerState = require('./player_state');
var listeners = [];
var logger = require('./logger');

module.exports = function (app, playerManager) {
  playerManager.stream.on('data', _sendChunk);

  app.use(express.static(__dirname + '/../public'));
  app.use(morganLog('dev'));
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(bodyParser.json());
  app.set('views', __dirname + '/../public');
  app.set('view engine', 'ejs');
  app.engine('html', require('ejs').renderFile);
  app.use(favicon(__dirname + '/../public/jukebox.png'));

  var allowedMethods = 'GET,PUT,POST,DELETE,OPTIONS';
  var allowedHeaders = 'Content-Length,Content-Type';
  var allowedHosts = [
    'http://soundcloud.com',
    'https://soundcloud.com',
    'http://youtube.com',
    'https://youtube.com',
    'http://www.youtube.com',
    'https://www.youtube.com'
  ];
  function _allowCrossDomain(req, res, next) {
    if (allowedHosts.indexOf(req.headers.origin) !== -1) {
      res.header('Access-Control-Allow-Origin', req.headers.origin);
      res.header('Access-Control-Allow-Methods', allowedMethods);
      res.header('Access-Control-Allow-Headers', allowedHeaders);
      if ('OPTIONS' === req.method) return res.status(200).end();
    }
    next();
  }

  app.route('/player')
  .get(function (req, res) {
    res.send(playerState);
  })
  .post(function (req, res) {
    if (!playerState.playing) playerManager.start();
    res.status(201).end();
  })
  .delete(function (req, res) {
    if (playerState.playing) playerManager.stop();
    res.status(201).end();
  });

  app.route('/tracks')
  .options(_allowCrossDomain)
  .get(function (req, res) {
    tracklist.get()
    .then(function (list) {
      res.send(list);
    }, function (err) {
      res.status(503).send(err.message);
    });
  })
  .post(_allowCrossDomain, function (req, res) {
    if (!req.body.url) return res.status(400).send('You need to provide a track URL.');
    trackBuilder.fromString(req.body.url)
    .then(tracklist.add)
    .then(function (track) {
      socket.emit('tracks:new');
      res.status(201).send(track);
    }, function (err) {
      res.status(500).send(err.message);
    });
  });

  app.route('/current')
  .get(function (req, res) {
    tracklist.current()
    .then(function (track) {
      if (!track) return res.send({});

      redis.scard('jukebox:votes', function (err, count) {
        if (err) return res.status(500).send(err);
        track.votes = {
          favorable: count,
          total: config.minVotes
        };
        res.send(track);
      });
    }, function (err) {
      res.status(500).send(err.message);
    });
  })
  .post(function (req, res) {
    redis.sadd('jukebox:votes', req.ip, function (err, newCount) {
      if (err) return res.status(500).send(err);
      res.send(201).end();
      if (newCount > 0) {
        socket.emit('current:votes');
        _checkVotesNext();
      }
    });
  })
  .delete(function (req, res) {
    playerManager.stop();
    tracklist.next().then(function () {
      playerManager.start();
    });
    res.status(201).end();
  });

  app.get('/history', function (req, res) {
    tracklist.history()
    .then(function (list) {
      res.send(list);
    }, function (err) {
      res.status(500).send(err.message);
    });
  });

  app.get('/stream', function (req, res) {
    res.set({
      'Content-Type': 'audio/mpeg',
      'Transfer-Encoding': 'chunked',
      'Cache-Control': 'private, no-cache, no-store, must-revalidate',
      'Expires': '-1',
      'Pragma': 'no-cache',
    });

    _addListener(res);

    function _onEnd() {
      _removeListener(res);
    }

    res.on('close', _onEnd);
    res.on('finish', _onEnd);
  });

  /**
  * Check whether we need to skip the track.
  */
  function _checkVotesNext() {
    redis.scard('jukebox:votes', function (err, count) {
      if (err) return logger.log(err);
      if (count < config.minVotes) return;
      tracklist.next();
      playerManager.stop();
      playerManager.start();
    });
  }
};

function _addListener(res) {
  logger.log('Adding listener');
  listeners.push(res);
}

function _removeListener(res) {
  var idx = listeners.indexOf(res);
  listeners.splice(idx, 1);
  logger.log('Removed listener. ' + listeners.length + ' are left.');
}

// Listeners are 'res' objects
function _sendChunk(chunk) {
  listeners.forEach(function (listener) {
    listener.write(chunk);
  });
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { render } from 'react-dom';
import CheckList from './CheckList';

class Card extends Component {
    render() {
        return (

{this.props.title}

                    {this.props.description}
                    <CheckList cardId={this.props.id} tasks={this.props.tasks} />

        );
    }
}

export default Card;
<commit_msg>1장 끝 - onClick을 이용한 카드 토글 구현<commit_after>import React, { Component } from 'react';
import { render } from 'react-dom';
import CheckList from './CheckList';

class Card extends Component {

    constructor() {
        super(...arguments);
        this.state = {
            showDetails: false
        };
    }

    render() {
        let cardDetails;

        if(this.state.showDetails) {
            cardDetails = (

                    {this.props.description}
                    <CheckList cardId={this.props.id} tasks={this.props.tasks} />

            );
        }

        return (

this.setState({showDetails: !this.state.showDetails})
                }>{this.props.title}

                {cardDetails}

        );
    }
}

export default Card;
<|endoftext|>