<commit_before>/* eslint-env node */

var FeedParser = require('feedparser'); // to parse feeds
var request = require('requestretry'); // TODO let's abstract this so that we can switch it out (e.g., to fs.readFile )
var Feed = require('rss'); // to write feeds (but not necessary to require here because we should get a Feed object from app.js -- which seems wrong)
var fs = require('fs');
var iconv = require('iconv-lite');
var zlib = require('zlib');
var sanitize = require('sanitize-filename');

var feedFetchTrimmer = function(feedUrl, callback) {
  'use strict';
  // thanks to example from feedparser:
  //  done, maybeDecompress, maybeTranslate, getParams
  var error = {};
  // Define our streams
  function done(err) {
    if (err) {
      error = err;
      console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + error);
      callback();
      // return this.emit('error', new Error('Feedparser ERROR: ' + feedUrl + ' THREW ' + err + '\n'));
    }
    // return;
  }

  function maybeDecompress(res, encoding) {
    var decompress;
    if (encoding.match(/\bdeflate\b/)) {
      decompress = zlib.createInflate();
    } else if (encoding.match(/\bgzip\b/)) {
      decompress = zlib.createGunzip();
    }
    return decompress ? res.pipe(decompress) : res;
  }

  function maybeTranslate(res, charset) {
    // var iconv;
    // Use iconv-lite if its not utf8 already.
    if (!iconv && charset && !/utf-*8/i.test(charset)) {
      try {
        console.log('Feedparser: ' + feedUrl + ': Converting from charset %s to utf-8', charset);
        res = res.pipe(iconv.decodeStream(charset));
      } catch (err) {
        console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + err);
      }
    }
    return res;
  }

  function getParams(str) {
    var params = str.split(';').reduce(function(parameters, param) {
      var parts = param.split('=').map(function(part) {
        return part.trim();
      });
      if (parts.length === 2) {
        parameters[parts[0]] = parts[1];
      }
      return parameters;
    }, {});
    return params;
  }
  // set up request to fetch feed

  var options = {
    url: feedUrl,
    headers: {
      'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36',
      'accept': 'text/html,application/xhtml+xml'
    },
    timeout: 10000,
    //setMaxListeners: 10,
    maxAttempts: 10, // (default) try 5 times
    retryDelay: 10000, // (default) wait for 5s before trying again
    retryStrategy: request.RetryStrategies.HTTPOrNetworkError
  };
  var req = request(options);

  var feedparser = new FeedParser();
  var feedObject;

  // Define our handlers
  req.on('error', done);
  req.on('response', function(res) {
    if (res.statusCode !== 200) {
      return this.emit('error' + req.url, new Error('Bad status code'));
    }
    var encoding = res.headers['content-encoding'] || 'identity',
      charset = getParams(res.headers['content-type'] || '').charset;
    res = maybeDecompress(res, encoding);
    res = maybeTranslate(res, charset);
    res.pipe(feedparser);
  });

  feedparser.on('error', done);
  feedparser.on('end', done);
  feedparser.on('meta', function() {
    var meta = this.meta; // **NOTE** the "meta" is always available in the context of the feedparser instance
    var feedOptions = {
      title: meta.title,
      description: meta.description,
      link: meta.link,
      pubDate: meta.date,
      feed_url: meta.xmlurl,
      // image: meta.image.url,
      copyright: meta.copyright,
      // feedObject.author.email = meta.author.email;
      // author.link: meta.author.link,
      author: meta.author
    };

    feedObject = new Feed(feedOptions);
  });
  feedparser.on('readable', function() {
    // console.log(feedUrl);
    // This is where the action is!
    var stream = this;

    var today = new Date();
    var cutoff = new Date(today.getTime() - 31 * 24 * 60 * 60 * 1000); // only posts from past 30 days (ignoring leap seconds etc.)
    var item;
    while ((item = stream.read())) {
      var itemDate = item.pubdate || item.date;
      // console.log(cutoff, itemDate, cutoff < itemDate);
      if ((cutoff < itemDate) && !(today < itemDate)) {
        var itemOptions = {
          date: itemDate,
          title: item.title,
          url: item.link,
          description: null, // item.description || null
          author: item.author || null
        };
        feedObject.item(itemOptions);
      }
    }
  });
  feedparser.on('finish', function() {
    if (Object.keys(error).length > 0) {
      feedObject.items.sort(function(a, b) { //sort by date for creating pages later
        return b.date - a.date;
      });
      var xml = feedObject.xml({
        indent: true
      });
      // console.log(xml);
      var filename = sanitize(feedUrl) + '.xml';
      // console.log(filename);
      fs.writeFile('./feeds/' + filename, xml, function(err) {
        if (err) {
          console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + err);
        }
        callback();
        // console.log('SUCCESS: "' + filename + '" was saved!');
      });
    }
  });
};

module.exports = feedFetchTrimmer;
<commit_msg>specify 644 for the feed files<commit_after>/* eslint-env node */

var FeedParser = require('feedparser'); // to parse feeds
var request = require('requestretry'); // TODO let's abstract this so that we can switch it out (e.g., to fs.readFile )
var Feed = require('rss'); // to write feeds (but not necessary to require here because we should get a Feed object from app.js -- which seems wrong)
var fs = require('fs');
var iconv = require('iconv-lite');
var zlib = require('zlib');
var sanitize = require('sanitize-filename');

var feedFetchTrimmer = function(feedUrl, callback) {
  'use strict';
  // thanks to example from feedparser:
  //  done, maybeDecompress, maybeTranslate, getParams
  var error = {};
  // Define our streams
  function done(err) {
    if (err) {
      error = err;
      console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + error);
      callback();
      // return this.emit('error', new Error('Feedparser ERROR: ' + feedUrl + ' THREW ' + err + '\n'));
    }
    // return;
  }

  function maybeDecompress(res, encoding) {
    var decompress;
    if (encoding.match(/\bdeflate\b/)) {
      decompress = zlib.createInflate();
    } else if (encoding.match(/\bgzip\b/)) {
      decompress = zlib.createGunzip();
    }
    return decompress ? res.pipe(decompress) : res;
  }

  function maybeTranslate(res, charset) {
    // var iconv;
    // Use iconv-lite if its not utf8 already.
    if (!iconv && charset && !/utf-*8/i.test(charset)) {
      try {
        console.log('Feedparser: ' + feedUrl + ': Converting from charset %s to utf-8', charset);
        res = res.pipe(iconv.decodeStream(charset));
      } catch (err) {
        console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + err);
      }
    }
    return res;
  }

  function getParams(str) {
    var params = str.split(';').reduce(function(parameters, param) {
      var parts = param.split('=').map(function(part) {
        return part.trim();
      });
      if (parts.length === 2) {
        parameters[parts[0]] = parts[1];
      }
      return parameters;
    }, {});
    return params;
  }
  // set up request to fetch feed

  var options = {
    url: feedUrl,
    headers: {
      'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36',
      'accept': 'text/html,application/xhtml+xml'
    },
    timeout: 10000,
    //setMaxListeners: 10,
    maxAttempts: 10, // (default) try 5 times
    retryDelay: 10000, // (default) wait for 5s before trying again
    retryStrategy: request.RetryStrategies.HTTPOrNetworkError
  };
  var req = request(options);

  var feedparser = new FeedParser();
  var feedObject;

  // Define our handlers
  req.on('error', done);
  req.on('response', function(res) {
    if (res.statusCode !== 200) {
      return this.emit('error' + req.url, new Error('Bad status code'));
    }
    var encoding = res.headers['content-encoding'] || 'identity',
      charset = getParams(res.headers['content-type'] || '').charset;
    res = maybeDecompress(res, encoding);
    res = maybeTranslate(res, charset);
    res.pipe(feedparser);
  });

  feedparser.on('error', done);
  feedparser.on('end', done);
  feedparser.on('meta', function() {
    var meta = this.meta; // **NOTE** the "meta" is always available in the context of the feedparser instance
    var feedOptions = {
      title: meta.title,
      description: meta.description,
      link: meta.link,
      pubDate: meta.date,
      feed_url: meta.xmlurl,
      // image: meta.image.url,
      copyright: meta.copyright,
      // feedObject.author.email = meta.author.email;
      // author.link: meta.author.link,
      author: meta.author
    };

    feedObject = new Feed(feedOptions);
  });
  feedparser.on('readable', function() {
    // console.log(feedUrl);
    // This is where the action is!
    var stream = this;

    var today = new Date();
    var cutoff = new Date(today.getTime() - 31 * 24 * 60 * 60 * 1000); // only posts from past 30 days (ignoring leap seconds etc.)
    var item;
    while ((item = stream.read())) {
      var itemDate = item.pubdate || item.date;
      // console.log(cutoff, itemDate, cutoff < itemDate);
      if ((cutoff < itemDate) && !(today < itemDate)) {
        var itemOptions = {
          date: itemDate,
          title: item.title,
          url: item.link,
          description: null, // item.description || null
          author: item.author || null
        };
        feedObject.item(itemOptions);
      }
    }
  });
  feedparser.on('finish', function() {
    if (Object.keys(error).length > 0) {
      feedObject.items.sort(function(a, b) { //sort by date for creating pages later
        return b.date - a.date;
      });
      var xml = feedObject.xml({
        indent: true
      });
      // console.log(xml);
      var filename = sanitize(feedUrl) + '.xml';
      // console.log(filename);
      fs.writeFile('./feeds/' + filename, xml, {mode:0o664}, function(err) {
        if (err) {
          console.log('feedFetchTrimmer: ' + feedUrl + ' : ' + err);
        }
        callback();
        // console.log('SUCCESS: "' + filename + '" was saved!');
      });
    }
  });
};

module.exports = feedFetchTrimmer;
<|endoftext|>


<commit_before>var fs = require('fs');
var sys = require('util');

var assert = require('assert');

var tf2status_function = fs.readFileSync(__dirname + '/../src/tf2stats.js', 'utf8');

eval(tf2status_function); //TODO need to change the filename

var tf2status_xml = fs.readFileSync(__dirname + '/../fixtures/gutomaia-l4d2.xml');

var statsClient = new StatsClient();

var achievements = statsClient.getAchievements('gutomaia', 'l4d2', tf2status_xml);

assert.notEqual(null, achievements);
assert.equal(67, achievements.length);

var achievement = achievements[0];

assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/8d94a8ffcb20fd11de547a6972bcef319fd93bf6.jpg", achievement.iconClosed);
assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/19c4af0f9bb24b719a086cbd3b63961d8348bf2b.jpg", achievement.iconOpen);
assert.equal("ACID REFLEX", achievement.name);
assert.equal("ach_kill_spitter_fast", achievement.apiname);
assert.equal("Kill a Spitter before she is able to spit.", achievement.description);
assert.notEqual(null, achievement.timestamp);
assert.equal(1292701736000, achievement.timestamp);

achievement = achievements[1];

assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b1b772b233b812fd410aee1f175f8b1cb4fe4e43.jpg", achievement.iconClosed);
assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/1921292f1593086aa0dc7d29124e4c0c1119e084.jpg", achievement.iconOpen);
assert.equal("CRASS MENAGERIE", achievement.name);
assert.equal("ach_kill_every_uncommon_infected", achievement.apiname);
assert.equal("Kill one of each Uncommon Infected.", achievement.description);
assert.equal(1292451219000, achievement.timestamp);

achievement = achievements[2];

assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b65a6518c0680448e996e2190d1ed6b00ad14935.jpg", achievement.iconClosed);
assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/95bb3011848b129b4b7483017a970700a0a22bd1.jpg", achievement.iconOpen);
assert.equal("A RIDE DENIED", achievement.name);
assert.equal("ach_save_player_from_jockey_fast", achievement.apiname);
assert.equal("Kill a Jockey within 2 seconds of it jumping on a Survivor.", achievement.description);

achievement = achievements[3];
assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/4eb2b093ce1dd442da4a62c9a0659602fbfc0d1e.jpg", achievement.iconClosed);
assert.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/4fc103dcb52ccc748b5bdea66924d1bf5c9e0705.jpg", achievement.iconOpen);
assert.equal("ARMORY OF ONE", achievement.name);
assert.equal("ach_deploy_ammo_upgrade", achievement.apiname);
assert.equal("Deploy an ammo upgrade and have your team use it.", achievement.description);<commit_msg>changed l4d2 achievements to follow nodeunit standard<commit_after>var fs = require('fs');
var sys = require('util');

var assert = require('assert');

var tf2status_function = fs.readFileSync(__dirname + '/../src/tf2stats.js', 'utf8');

eval(tf2status_function); //TODO need to change the filename

var tf2status_xml = fs.readFileSync(__dirname + '/../fixtures/gutomaia-l4d2.xml');

var statsClient = new StatsClient();

var achievements = statsClient.getAchievements('gutomaia', 'l4d2', tf2status_xml);

exports.testNumberOfAchievements = function(test){
    test.notEqual(null, achievements);
    test.equal(67, achievements.length);
    test.done();
}

exports.testAcidReflexAchievementIconClosed = function(test){
    var achievement = achievements[0];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/8d94a8ffcb20fd11de547a6972bcef319fd93bf6.jpg", achievement.iconClosed);
    test.done();
}

exports.testAcidReflexAchievementIconOpen = function(test){
    var achievement = achievements[0];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/19c4af0f9bb24b719a086cbd3b63961d8348bf2b.jpg", achievement.iconOpen);
    test.done();
}

exports.testAcidReflexAchievementName = function(test){
    var achievement = achievements[0];
    test.equal("ACID REFLEX", achievement.name);
    test.done();
}

exports.testAcidReflexAchievementApiname = function(test){
    var achievement = achievements[0];
    test.equal("ach_kill_spitter_fast", achievement.apiname);
    test.done();
}

exports.testAcidReflexDescription = function(test){
    var achievement = achievements[0];
    test.equal("Kill a Spitter before she is able to spit.", achievement.description);
    test.done();
}

exports.testAcidReflexTimestamp = function(test){
    var achievement = achievements[0];
    test.notEqual(null, achievement.timestamp);
    test.equal(1292701736000, achievement.timestamp);
    test.done();
}

exports.testCrassMenagerieIconOpen = function(test){
    var achievement = achievements[1];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b1b772b233b812fd410aee1f175f8b1cb4fe4e43.jpg", achievement.iconClosed);
    test.done();
}

exports.testCrassMenagerieIconClosed = function(test){
    var achievement = achievements[1];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b1b772b233b812fd410aee1f175f8b1cb4fe4e43.jpg", achievement.iconClosed);
    test.done();
}

exports.testCrassMenagerieIconOpen = function(test){
    var achievement = achievements[1];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/1921292f1593086aa0dc7d29124e4c0c1119e084.jpg", achievement.iconOpen);
    test.done();
}

exports.testCrassMenagerieIconClosed = function(test){
    var achievement = achievements[1];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b1b772b233b812fd410aee1f175f8b1cb4fe4e43.jpg", achievement.iconClosed);
    test.done();
}

exports.testCrassMenagerieName = function(test){
    var achievement = achievements[1];
    test.equal("CRASS MENAGERIE", achievement.name);
    test.done();
}

exports.testCrassMenagerieApiname = function(test){
    var achievement = achievements[1];
    test.equal("ach_kill_every_uncommon_infected", achievement.apiname);
    test.done();
}

exports.testCrassMenagerieDescription = function(test){
    var achievement = achievements[1];
    test.equal("Kill one of each Uncommon Infected.", achievement.description);
    test.done();
}

exports.testCrassMenagerieTimestamp = function(test){
    var achievement = achievements[1];
    test.notEqual(null, achievement.timestamp);
    test.equal(1292451219000, achievement.timestamp);
    test.done();
}

exports.testARideDeniedIconClosed = function(test){
    var achievement = achievements[2];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b65a6518c0680448e996e2190d1ed6b00ad14935.jpg", achievement.iconClosed);
    test.done();
}

var achievement = achievements[2];

exports.testARideDeniedIconOpen = function(test){
    var achievement = achievements[2];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/b65a6518c0680448e996e2190d1ed6b00ad14935.jpg", achievement.iconClosed);
    test.done();
}

exports.testARideDeniedIconClosed = function(test){
    var achievement = achievements[2];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/95bb3011848b129b4b7483017a970700a0a22bd1.jpg", achievement.iconOpen);
    test.done();
}

exports.testARideDeniedName = function(test){
    var achievement = achievements[2];
    test.equal("A RIDE DENIED", achievement.name);
    test.done();
}

exports.testARideDeniedApiname = function(test){
    var achievement = achievements[2];
    test.equal("ach_save_player_from_jockey_fast", achievement.apiname);
    test.done();
}

exports.testARideDeniedDescription = function(test){
    var achievement = achievements[2];
    test.equal("Kill a Jockey within 2 seconds of it jumping on a Survivor.", achievement.description);
    test.done();
}

exports.testArmoryOfOneIconClosed = function(test){
    var achievement = achievements[3];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/4eb2b093ce1dd442da4a62c9a0659602fbfc0d1e.jpg", achievement.iconClosed);
    test.done();
}

exports.testArmoryOfOneIconOpen = function(test){
    var achievement = achievements[3];
    test.equal("http://media.steampowered.com/steamcommunity/public/images/apps/550/4fc103dcb52ccc748b5bdea66924d1bf5c9e0705.jpg", achievement.iconOpen);
    test.done();
}

exports.testArmoryOfOneName = function(test){
    var achievement = achievements[3];
    test.equal("ARMORY OF ONE", achievement.name);
    test.done();
}

exports.testArmoryOfOneApiname = function(test){
    var achievement = achievements[3];
    test.equal("ach_deploy_ammo_upgrade", achievement.apiname);
    test.done();
}

exports.testArmoryOfOneDescription = function(test){
    var achievement = achievements[3];
    test.equal("Deploy an ammo upgrade and have your team use it.", achievement.description);
    test.done();
}<|endoftext|>


<commit_before>'use strict';

// test article api:

var
    _ = require('lodash'),
    fs = require('fs'),
    co = require('co'),
    should = require('should'),
    remote = require('./_remote'),
    constants = require('../constants'),
    roles = constants.role;

describe('#articles', function () {

    var category = null;
    var category2 = null;

    before(remote.setup);

    before(function (done) {
        co(function* () {
            category = yield remote.$post(roles.ADMIN, '/api/categories', {
                name: 'Article Category'
            });
            remote.shouldNoError(category);
            category2 = yield remote.$post(roles.ADMIN, '/api/categories', {
                name: 'Article Category 2'
            });
            remote.shouldNoError(category2);
            return 'ok';
        }).then(function (result) {
            done();
        }, function (err) {
            done(err);
        });
    });

    describe('#api', function () {

        it('should get empty articles', function* () {
            var r = yield remote.$get(roles.CONTRIBUTOR, '/api/articles');
            remote.shouldNoError(r);
            r.page.total.should.equal(0);
            r.articles.should.be.an.Array.and.have.length(0);
        });

        it('get articles failed for no permission', function* () {
            var r = yield remote.$get(roles.SUBSCRIBER, '/api/articles');
            remote.shouldHasError(r, 'permission:denied');
        });

        it('create article by contributor failed', function* () {
            // create article:
            var r1 = yield remote.$post(roles.CONTRIBUTOR, '/api/articles', {
                category_id: category.id,
                name: ' Try create Article  ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldHasError(r1, 'permission:denied');
        });

        it('create by admin and update, delete by editor failed', function* () {
            // create article:
            var r1 = yield remote.$post(roles.ADMIN, '/api/articles', {
                category_id: category.id,
                name: ' Article 1   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);
            r1.category_id.should.equal(category.id);
            r1.name.should.equal('Article 1');
            r1.description.should.equal('blablabla\nhaha...');
            r1.tags.should.equal('aaa,BBB,ccc');
            r1.content.should.equal('Long content...');
            r1.content_id.should.be.ok;
            r1.cover_id.should.be.ok;

            // update by editor:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                name: 'Name Changed  ',
                content: 'Changed?'
            });
            remote.shouldHasError(r2, 'permission:denied');

            // delete by editor:
            var r3 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id + '/delete');
            remote.shouldHasError(r3, 'permission:denied');
        });

        it('create and update article by editor', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'Test Article   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);
            r1.category_id.should.equal(category.id);
            r1.name.should.equal('Test Article');
            r1.description.should.equal('blablabla\nhaha...');
            r1.tags.should.equal('aaa,BBB,ccc');
            r1.content.should.equal('Long content...');
            r1.content_id.should.be.ok;
            r1.cover_id.should.be.ok;

            // check image:
            var dl = yield remote.$download('/files/attachments/' + r1.cover_id + '/l');
            remote.shouldNoError(dl);
            dl.statusCode.should.equal(200);
            dl.headers['content-type'].should.equal('image/jpeg');
            parseInt(dl.headers['content-length'], 10).should.approximately(122826, 10000);

            // update article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                name: 'Name Changed  ',
                content: 'Changed!'
            });
            remote.shouldNoError(r2);
            r2.name.should.equal('Name Changed');
            r2.content.should.equal('Changed!');
            r2.content_id.should.not.equal(r1.content_id);
            r2.cover_id.should.equal(r1.cover_id);
            r2.user_id.should.equal(r1.user_id);

            // query:
            var r3 = yield remote.$get(roles.EDITOR, '/api/articles/' + r1.id);
            r3.name.should.equal(r2.name);
            r3.content.should.equal(r2.content);
            // not updated:
            r3.tags.should.equal(r1.tags);
            r3.description.should.equal(r1.description);
        });

        it('create article then change cover', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'Before Cover Change   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Content not change...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);

            // update article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                image: remote.readFileSync('res-image-2.jpg').toString('base64')
            });
            remote.shouldNoError(r2);
            r2.name.should.equal('Before Cover Change');
            r2.content.should.equal('Content not change...');
            r2.content_id.should.equal(r1.content_id);
            r2.cover_id.should.not.equal(r1.cover_id);

            // check image:
            var dl = yield remote.$download('/files/attachments/' + r2.cover_id + '/l');
            remote.shouldNoError(dl);
            dl.statusCode.should.equal(200);
            dl.headers['content-type'].should.equal('image/jpeg');
            parseInt(dl.headers['content-length'], 10).should.approximately(39368, 10000);
        });

        it('create article with wrong parameter by editor', function* () {
            var
                i, r, params,
                required = ['name', 'description', 'category_id', 'content', 'image'],
                prepared = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: category.id,
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-image.jpg').toString('base64')
                };
            for (i=0; i<required.length; i++) {
                params = _.clone(prepared);
                delete params[required[i]];
                r = yield remote.$post(roles.EDITOR, '/api/articles', params);
                remote.shouldHasError(r, 'parameter:invalid', required[i]);
            }
        });

        it('create article with invalid category_id', function* () {
            var
                r,
                params = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: remote.next_id(),
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-image.jpg').toString('base64')
                };
            r = yield remote.$post(roles.EDITOR, '/api/articles', params);
            remote.shouldHasError(r, 'entity:notfound', 'Category');
        });

        it('create article with invalid image', function* () {
            var
                r,
                params = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: category.id,
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-plain.txt').toString('base64')
                };
            r = yield remote.$post(roles.EDITOR, '/api/articles', params);
            remote.shouldHasError(r, 'parameter:invalid', 'image');
        });

        it('create and delete article by editor', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'To Be Delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Content not change...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);

            // delete article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id + '/delete');
            remote.shouldNoError(r2);
            r2.id.should.equal(r1.id);

            // query:
            var r3 = yield remote.$get(roles.EDITOR, '/api/articles/' + r1.id);
            remote.shouldHasError(r3, 'entity:notfound', 'Article');
        });
    });
});
<commit_msg>add version assertion<commit_after>'use strict';

// test article api:

var
    _ = require('lodash'),
    fs = require('fs'),
    co = require('co'),
    should = require('should'),
    remote = require('./_remote'),
    constants = require('../constants'),
    roles = constants.role;

describe('#articles', function () {

    var category = null;
    var category2 = null;

    before(remote.setup);

    before(function (done) {
        co(function* () {
            category = yield remote.$post(roles.ADMIN, '/api/categories', {
                name: 'Article Category'
            });
            remote.shouldNoError(category);
            category2 = yield remote.$post(roles.ADMIN, '/api/categories', {
                name: 'Article Category 2'
            });
            remote.shouldNoError(category2);
            return 'ok';
        }).then(function (result) {
            done();
        }, function (err) {
            done(err);
        });
    });

    describe('#api', function () {

        it('should get empty articles', function* () {
            var r = yield remote.$get(roles.CONTRIBUTOR, '/api/articles');
            remote.shouldNoError(r);
            r.page.total.should.equal(0);
            r.articles.should.be.an.Array.and.have.length(0);
        });

        it('get articles failed for no permission', function* () {
            var r = yield remote.$get(roles.SUBSCRIBER, '/api/articles');
            remote.shouldHasError(r, 'permission:denied');
        });

        it('create article by contributor failed', function* () {
            // create article:
            var r1 = yield remote.$post(roles.CONTRIBUTOR, '/api/articles', {
                category_id: category.id,
                name: ' Try create Article  ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldHasError(r1, 'permission:denied');
        });

        it('create by admin and update, delete by editor failed', function* () {
            // create article:
            var r1 = yield remote.$post(roles.ADMIN, '/api/articles', {
                category_id: category.id,
                name: ' Article 1   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);
            r1.category_id.should.equal(category.id);
            r1.name.should.equal('Article 1');
            r1.description.should.equal('blablabla\nhaha...');
            r1.tags.should.equal('aaa,BBB,ccc');
            r1.content.should.equal('Long content...');
            r1.content_id.should.be.ok;
            r1.cover_id.should.be.ok;

            // update by editor:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                name: 'Name Changed  ',
                content: 'Changed?'
            });
            remote.shouldHasError(r2, 'permission:denied');

            // delete by editor:
            var r3 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id + '/delete');
            remote.shouldHasError(r3, 'permission:denied');
        });

        it('create and update article by editor', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'Test Article   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Long content...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);
            r1.category_id.should.equal(category.id);
            r1.name.should.equal('Test Article');
            r1.description.should.equal('blablabla\nhaha...');
            r1.tags.should.equal('aaa,BBB,ccc');
            r1.content.should.equal('Long content...');
            r1.content_id.should.be.ok;
            r1.cover_id.should.be.ok;
            r1.version.should.equal(0);

            // check image:
            var dl = yield remote.$download('/files/attachments/' + r1.cover_id + '/l');
            remote.shouldNoError(dl);
            dl.statusCode.should.equal(200);
            dl.headers['content-type'].should.equal('image/jpeg');
            parseInt(dl.headers['content-length'], 10).should.approximately(122826, 10000);

            // update article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                name: 'Name Changed  ',
                content: 'Changed!'
            });
            remote.shouldNoError(r2);
            r2.name.should.equal('Name Changed');
            r2.content.should.equal('Changed!');
            r2.content_id.should.not.equal(r1.content_id);
            r2.cover_id.should.equal(r1.cover_id);
            r2.user_id.should.equal(r1.user_id);
            r2.version.should.equal(1);

            // query:
            var r3 = yield remote.$get(roles.EDITOR, '/api/articles/' + r1.id);
            r3.name.should.equal(r2.name);
            r3.content.should.equal(r2.content);
            r3.version.should.equal(1);
            // not updated:
            r3.tags.should.equal(r1.tags);
            r3.description.should.equal(r1.description);
        });

        it('create article then change cover', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'Before Cover Change   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Content not change...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);

            // update article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id, {
                image: remote.readFileSync('res-image-2.jpg').toString('base64')
            });
            remote.shouldNoError(r2);
            r2.name.should.equal('Before Cover Change');
            r2.content.should.equal('Content not change...');
            r2.content_id.should.equal(r1.content_id);
            r2.cover_id.should.not.equal(r1.cover_id);

            // check image:
            var dl = yield remote.$download('/files/attachments/' + r2.cover_id + '/l');
            remote.shouldNoError(dl);
            dl.statusCode.should.equal(200);
            dl.headers['content-type'].should.equal('image/jpeg');
            parseInt(dl.headers['content-length'], 10).should.approximately(39368, 10000);
        });

        it('create article with wrong parameter by editor', function* () {
            var
                i, r, params,
                required = ['name', 'description', 'category_id', 'content', 'image'],
                prepared = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: category.id,
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-image.jpg').toString('base64')
                };
            for (i=0; i<required.length; i++) {
                params = _.clone(prepared);
                delete params[required[i]];
                r = yield remote.$post(roles.EDITOR, '/api/articles', params);
                remote.shouldHasError(r, 'parameter:invalid', required[i]);
            }
        });

        it('create article with invalid category_id', function* () {
            var
                r,
                params = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: remote.next_id(),
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-image.jpg').toString('base64')
                };
            r = yield remote.$post(roles.EDITOR, '/api/articles', params);
            remote.shouldHasError(r, 'entity:notfound', 'Category');
        });

        it('create article with invalid image', function* () {
            var
                r,
                params = {
                    name: 'Test Params',
                    description: 'blablabla...',
                    category_id: category.id,
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...',
                    image: remote.readFileSync('res-plain.txt').toString('base64')
                };
            r = yield remote.$post(roles.EDITOR, '/api/articles', params);
            remote.shouldHasError(r, 'parameter:invalid', 'image');
        });

        it('create and delete article by editor', function* () {
            // create article:
            var r1 = yield remote.$post(roles.EDITOR, '/api/articles', {
                category_id: category.id,
                name: 'To Be Delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: 'Content not change...',
                image: remote.readFileSync('res-image.jpg').toString('base64')
            });
            remote.shouldNoError(r1);

            // delete article:
            var r2 = yield remote.$post(roles.EDITOR, '/api/articles/' + r1.id + '/delete');
            remote.shouldNoError(r2);
            r2.id.should.equal(r1.id);

            // query:
            var r3 = yield remote.$get(roles.EDITOR, '/api/articles/' + r1.id);
            remote.shouldHasError(r3, 'entity:notfound', 'Article');
        });
    });
});
<|endoftext|>


<commit_before>(function() {
  var hoursEvents = [];
  var hoursPending = {};
  var ratesPending = {};

  // See https://gist.github.com/dperini/729294
  var urlRegex = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i

  var EventItem = React.createClass({
    propTypes: {
      event: React.PropTypes.object.isRequired
    },
    render: function() {
      var event = this.props.event;
      var name = (event.user ? event.user.first_name + ' ' + event.user.last_name : event.args.info);
      if (event.event === 'MarkHours') {
        return React.createElement('li', { key: eventId(event) },
          name + ' marked ' + moment.duration(event.args.duration*1000).humanize() + ' to project ' + event.args.description +
          (event.args.success ? '' : ' FAILED')
        );
      } else if (event.event === 'ProcessPayroll') {
        return React.createElement('li', { key: eventId(event) },
          'Payroll was processed, started new payroll'
        );
      } else {
        return React.createElement('li', { key: eventId(event) },
          'Unknown event ' + event.event
        );
      }
    }
  });

  var EventList = React.createClass({
    propTypes: {
      events: React.PropTypes.array.isRequired
    },
    render: function() {
      var children = this.props.events.map(function(event) {
        return React.createElement(EventItem, { event: event });
      });
      return React.createElement.apply(this, ['ul', {}].concat(children));
    }
  });

  var TxPending = React.createClass({
    propTypes: {
      transactions: React.PropTypes.object.isRequired
    },
    render: function() {
      var txCount = Object.keys(this.props.transactions).length;
      if (txCount > 0) {
        return React.createElement('div', { className: 'pending' },
          txCount + ' transactions pending, please wait');
      } else {
        return React.createElement('div', {},
          'No transactions pending, everything up-to-date');
      }
    }
  });

  var HoursForm = React.createClass({
    getInitialState: function() {
      return {
        hours: 1,
        title: '',
        description: ''
      };
    },
    hoursChanged: function(event) {
      this.state.hours = event.target.value;
      this.forceUpdate();
    },
    isTitleValid: function() {
      return (this.state.title.length <= 32 || urlRegex.test(this.state.title));
    },
    titleChanged: function(event) {
      this.state.title = event.target.value;
      this.forceUpdate();
    },
    descriptionChanged: function(event) {
      this.state.title = event.target.value;
      this.forceUpdate();
    },
    sendMarking: function() {
      postJSON('/api/hours/jvah', {
        duration: parseFloat(this.state.hours)*3600,
        description: this.state.title
      });
    },
    render: function() {
      var titleStyle = {};
      if (!this.isTitleValid()) {
        titleStyle['backgroundColor'] = '#ffcdd2';
      }
      return React.createElement('div', { className: 'hours-form' },
        React.createElement('label', { htmlFor: 'title' }, 'Title:'),
        React.createElement('input', {
          name: 'title',
          type: 'url',
          style: titleStyle,
          placeholder: 'Public title or link URL',
          defaultValue: this.state.title,
          onChange: this.titleChanged
        }),
        React.createElement('br'),
        React.createElement('label', { htmlFor: 'hours' }, 'Hours spent:'),
        React.createElement('input', {
          id: 'hours',
          type: 'number',
          step: 0.25,
          defaultValue: this.state.hours,
          onChange: this.hoursChanged
        }),
        React.createElement('br'),
        React.createElement('label', { htmlFor: 'description' }, 'Description:'),
        React.createElement('textarea', {
          name: 'description',
          placeholder: 'Private description, not shown publicly',
          onChange: this.descriptionChanged
        }),
        React.createElement('br'),
        React.createElement('label'),
        React.createElement('button', {
          id: 'send',
          htmlFor: 'send',
          onClick: this.sendMarking
        }, 'Send')
      );
    }
  });

  function eventComparator(a, b) {
    if (b.blockNumber != a.blockNumber) {
      return (b.blockNumber - a.blockNumber);
    }
    return (b.logIndex - a.logIndex);
  }

  function eventId(e) {
    return e.blockHash + ':' + e.logIndex;
  }

  function addHoursEvent(event) {
    for (var i = 0; i < hoursEvents.length; i++) {
      // If event already exists, do not re-add it
      if (eventId(hoursEvents[i]) === eventId(event)) {
        return;
      }
    }
    hoursEvents.push(event);
    hoursEvents.sort(eventComparator);
  }

  function fetchInitial() {
    getJSON('/api/hours/events', function(err, data) {
      data.forEach(function(event) {
        addHoursEvent(event);
      });
      updateEventList();
    });
    getJSON('/api/hours/pending', function(err, data) {
      data.forEach(function(tx) {
        hoursPending[tx.hash] = tx;
      });
      updateEventList();
    });
  }

  function updateEventList() {
    var hoursForm = React.createElement(HoursForm, {});
    ReactDOM.render(hoursForm, document.getElementById('hours-form'));
    var eventList = React.createElement(EventList, { events: hoursEvents });
    ReactDOM.render(eventList, document.getElementById('event-list'));
    var txPending = React.createElement(TxPending, { transactions: hoursPending });
    ReactDOM.render(txPending, document.getElementById('transaction-status'));
  }

  var spinIcon = _.throttle(function() {
    var icon = document.getElementById('chilicorn-icon');
    icon.className = 'spin';
    setTimeout(function() {
      icon.className = '';
    }, 600);

  }, 1000, {trailing: false});

  fetchInitial();

  var socket = io();
  socket.on('block', function(msg) {
    console.log('block: ' + msg);
  });
  socket.on('rates/pending', function(msg) {
    console.log('rates pending: ' + msg);
    const tx = JSON.parse(msg);
    ratesPending[tx.hash] = tx;
  });
  socket.on('rates/tx', function(msg) {
    console.log('rates tx: ' + msg);
    const tx = JSON.parse(msg);
    delete ratesPending[tx.hash];
  });
  socket.on('hours/pending', function(msg) {
    console.log('hours pending: ' + msg);
    const tx = JSON.parse(msg);
    hoursPending[tx.hash] = tx;
    updateEventList();
  });
  socket.on('hours/tx', function(msg) {
    console.log('hours tx: ' + msg);
    const tx = JSON.parse(msg);
    delete hoursPending[tx.hash];
    updateEventList();
  });
  socket.on('hours/receipt', function(msg) {
    console.log('hours receipt: ' + msg);
  });
  socket.on('hours/event', function(msg) {
    console.log('hours event: ' + msg);
    const event = JSON.parse(msg);
    addHoursEvent(event);
    updateEventList();
    spinIcon();
  });
})();
<commit_msg>Add date to the beginning of events.<commit_after>(function() {
  var hoursEvents = [];
  var hoursPending = {};
  var ratesPending = {};

  // See https://gist.github.com/dperini/729294
  var urlRegex = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i

  var dateFormatter = new Intl.DateTimeFormat({ year: 'numeric', month: '2-digit', day: '2-digit' });

  var EventItem = React.createClass({
    propTypes: {
      event: React.PropTypes.object.isRequired
    },
    render: function() {
      var event = this.props.event;
      var date = dateFormatter.format(event.block ? new Date(event.block.timestamp * 1000) : new Date());
      var name = (event.user ? event.user.first_name + ' ' + event.user.last_name : event.args.info);
      if (event.event === 'MarkHours') {
        return React.createElement('li', { key: eventId(event) },
          date + ' ' + name + ' marked ' + moment.duration(event.args.duration*1000).humanize() + ' to project ' + event.args.description +
          (event.args.success ? '' : ' FAILED')
        );
      } else if (event.event === 'ProcessPayroll') {
        return React.createElement('li', { key: eventId(event) },
          date + ' Payroll was processed, started new payroll'
        );
      } else {
        return React.createElement('li', { key: eventId(event) },
          date + ' Unknown event ' + event.event
        );
      }
    }
  });

  var EventList = React.createClass({
    propTypes: {
      events: React.PropTypes.array.isRequired
    },
    render: function() {
      var children = this.props.events.map(function(event) {
        return React.createElement(EventItem, { event: event });
      });
      return React.createElement.apply(this, ['ul', {}].concat(children));
    }
  });

  var TxPending = React.createClass({
    propTypes: {
      transactions: React.PropTypes.object.isRequired
    },
    render: function() {
      var txCount = Object.keys(this.props.transactions).length;
      if (txCount > 0) {
        return React.createElement('div', { className: 'pending' },
          txCount + ' transactions pending, please wait');
      } else {
        return React.createElement('div', {},
          'No transactions pending, everything up-to-date');
      }
    }
  });

  var HoursForm = React.createClass({
    getInitialState: function() {
      return {
        hours: 1,
        title: '',
        description: ''
      };
    },
    hoursChanged: function(event) {
      this.state.hours = event.target.value;
      this.forceUpdate();
    },
    isTitleValid: function() {
      return (this.state.title.length <= 32 || urlRegex.test(this.state.title));
    },
    titleChanged: function(event) {
      this.state.title = event.target.value;
      this.forceUpdate();
    },
    descriptionChanged: function(event) {
      this.state.title = event.target.value;
      this.forceUpdate();
    },
    sendMarking: function() {
      postJSON('/api/hours/jvah', {
        duration: parseFloat(this.state.hours)*3600,
        description: this.state.title
      });
    },
    render: function() {
      var titleStyle = {};
      if (!this.isTitleValid()) {
        titleStyle['backgroundColor'] = '#ffcdd2';
      }
      return React.createElement('div', { className: 'hours-form' },
        React.createElement('label', { htmlFor: 'title' }, 'Title:'),
        React.createElement('input', {
          name: 'title',
          type: 'url',
          style: titleStyle,
          placeholder: 'Public title or link URL',
          defaultValue: this.state.title,
          onChange: this.titleChanged
        }),
        React.createElement('br'),
        React.createElement('label', { htmlFor: 'hours' }, 'Hours spent:'),
        React.createElement('input', {
          id: 'hours',
          type: 'number',
          step: 0.25,
          defaultValue: this.state.hours,
          onChange: this.hoursChanged
        }),
        React.createElement('br'),
        React.createElement('label', { htmlFor: 'description' }, 'Description:'),
        React.createElement('textarea', {
          name: 'description',
          placeholder: 'Private description, not shown publicly',
          onChange: this.descriptionChanged
        }),
        React.createElement('br'),
        React.createElement('label'),
        React.createElement('button', {
          id: 'send',
          htmlFor: 'send',
          onClick: this.sendMarking
        }, 'Send')
      );
    }
  });

  function eventComparator(a, b) {
    if (b.blockNumber != a.blockNumber) {
      return (b.blockNumber - a.blockNumber);
    }
    return (b.logIndex - a.logIndex);
  }

  function eventId(e) {
    return e.blockHash + ':' + e.logIndex;
  }

  function addHoursEvent(event) {
    for (var i = 0; i < hoursEvents.length; i++) {
      // If event already exists, do not re-add it
      if (eventId(hoursEvents[i]) === eventId(event)) {
        return;
      }
    }
    hoursEvents.push(event);
    hoursEvents.sort(eventComparator);
  }

  function fetchInitial() {
    getJSON('/api/hours/events', function(err, data) {
      data.forEach(function(event) {
        addHoursEvent(event);
      });
      updateEventList();
    });
    getJSON('/api/hours/pending', function(err, data) {
      data.forEach(function(tx) {
        hoursPending[tx.hash] = tx;
      });
      updateEventList();
    });
  }

  function updateEventList() {
    var hoursForm = React.createElement(HoursForm, {});
    ReactDOM.render(hoursForm, document.getElementById('hours-form'));
    var eventList = React.createElement(EventList, { events: hoursEvents });
    ReactDOM.render(eventList, document.getElementById('event-list'));
    var txPending = React.createElement(TxPending, { transactions: hoursPending });
    ReactDOM.render(txPending, document.getElementById('transaction-status'));
  }

  var spinIcon = _.throttle(function() {
    var icon = document.getElementById('chilicorn-icon');
    icon.className = 'spin';
    setTimeout(function() {
      icon.className = '';
    }, 600);

  }, 1000, {trailing: false});

  fetchInitial();

  var socket = io();
  socket.on('block', function(msg) {
    console.log('block: ' + msg);
  });
  socket.on('rates/pending', function(msg) {
    console.log('rates pending: ' + msg);
    const tx = JSON.parse(msg);
    ratesPending[tx.hash] = tx;
  });
  socket.on('rates/tx', function(msg) {
    console.log('rates tx: ' + msg);
    const tx = JSON.parse(msg);
    delete ratesPending[tx.hash];
  });
  socket.on('hours/pending', function(msg) {
    console.log('hours pending: ' + msg);
    const tx = JSON.parse(msg);
    hoursPending[tx.hash] = tx;
    updateEventList();
  });
  socket.on('hours/tx', function(msg) {
    console.log('hours tx: ' + msg);
    const tx = JSON.parse(msg);
    delete hoursPending[tx.hash];
    updateEventList();
  });
  socket.on('hours/receipt', function(msg) {
    console.log('hours receipt: ' + msg);
  });
  socket.on('hours/event', function(msg) {
    console.log('hours event: ' + msg);
    const event = JSON.parse(msg);
    addHoursEvent(event);
    updateEventList();
    spinIcon();
  });
})();
<|endoftext|>


<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 gulp = require('gulp');
const rename = require('gulp-rename');
const chmod = require('gulp-chmod');
const replace = require('gulp-replace');

const version = process.env.npm_package_version.replace(/-.*/, '');

gulp.task('docs', ['components-readme', 'components']);

const components = (path) => gulp.src(path || '../components/**/src/main/docs/*.adoc')
  .pipe(rename({dirname: ''}))
  .pipe(gulp.dest(`content/components/${version}`));

gulp.task('components', () => {
  return components();
});

const componentReadme = () => gulp.src('../components/readme.adoc')
  .pipe(replace(/link:.*\/(.*).adoc(\[.*)/g, `link:components/${version}/$1$2`))
  .pipe(rename('components.adoc'))
  .pipe(gulp.dest('content'));

gulp.task('components-readme', () => {
  return componentReadme();
});

gulp.task('asciidoctor-shim', () => {
  gulp.src('src/scripts/asciidoctor-shim.js')
    .pipe(rename('asciidoctor'))
    .pipe(chmod(0o755))
    .pipe(gulp.dest('node_modules/.bin/'));
});

gulp.task('default', ['docs', 'asciidoctor-shim']);

gulp.task('watch', () => {
  gulp.watch(['../components/**/*.adoc', '!../components/readme.adoc'], (event) => {
    components(event.path);
  });

  gulp.watch('../components/readme.adoc', (event) => {
    componentReadme();
  });
});
<commit_msg>Revert "CAMEL-11492 New Camel website"<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 gulp = require('gulp');
const rename = require('gulp-rename');
const chmod = require('gulp-chmod');
const replace = require('gulp-replace');

const version = process.env.npm_package_version.replace(/-.*/, '');

gulp.task('docs', ['component-doc']);

gulp.task('component-doc', () => {
  gulp.src('../components/readme.adoc')
    .pipe(replace(/link:.*\/(.*).adoc(\[.*)/g, `link:components/${version}/$1$2`))
    .pipe(rename('components.adoc'))
    .pipe(gulp.dest('content'));
  gulp.src('../components/**/src/main/docs/*.adoc')
    .pipe(rename({dirname: ''}))
    .pipe(gulp.dest(`content/components/${version}`));
});

gulp.task('asciidoctor-shim', () => {
  gulp.src('src/scripts/asciidoctor-shim.js')
    .pipe(rename('asciidoctor'))
    .pipe(chmod(0o755))
    .pipe(gulp.dest('node_modules/.bin/'));
});

gulp.task('default', ['docs', 'asciidoctor-shim']);

gulp.task('watch', () => {
  gulp.watch('../**/*.adoc', ['docs']);
});

<|endoftext|>


<commit_before>import Ember from 'ember';
import RSVP from 'rsvp';

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

export default Ember.Component.extend({
    authRequest: Ember.inject.service(),
    internalState: Ember.inject.service(),
    // Controls the state of the publish button
    enablePublish: true,
    // Set the default repository to dev
    selectedRepository: 'Development',
    // Flag set to show the spinner
    publishing: false,
    // Set when publishing has finished
    publishingFinish: false,
    // An array that holds a pair, (fileName, fileSize)
    fileList: [],
    // Holds an array of objects that the user cannot be exclude from their package
    nonOptionalFile: ['tale.yaml', 'environment.zip'],
    // A map that connects the repository dropdown to a url
    repositoryMapping: {'Development': 'https://dev.nceas.ucsb.edu/knb/d1/mn/'},
    // The url for the published tale. This is set after publication succeeds
    packageUrl: '',
    // The jwt token that allows the user to interact with DataONE
    dataoneJWT: '',

    setPublishBtnState(state) {
        this.set('enablePublish', state);
    },

    getTaleFiles() {
        let url = config.apiUrl + '/tale/'+ this.get('modalContext')
        let self = this;
        this.get('authRequest').send(url)
        .then(rep => {
            let folderId = rep['folderId'];
            let queryParams = '?'+[
                'folderId='+folderId,
                'limit=0',
                'sort=lowerName',
                'sortdir=1'
            ].join('&');

            url = config.apiUrl + '/item' + queryParams;
            let options = {
                method: 'GET',
                data: {
                    folderId: folderId,
                    limit: 0,
                    sort: 'lowerName',
                    sortdir: 1
                }};
            self.get('authRequest').send(url)
            .then(rep => {
                let fileList2 = []
                rep.forEach(function(item)
                {
                    let path = item.name;
                    fileList2.push({'name': path, 'size':item.size, 'id': item._id});
                })
                self.set('fileList', fileList2);
            })
        })
    },

    getPath(id) {
            let url = config.apiUrl + '/item/'+ id + '/rootpath';
            let self = this;
            let path = '';
            let hasRoot = false;

            this.get('authRequest').send(url)
                .then(rep => {
                    rep.forEach(function(folder) {
                        let folderName = folder.object['name'];
                        if (folderName === 'Data' || folderName === 'Home') {
                            path += folderName;
                            hasRoot = true;
                        }
                        if (hasRoot) {
                            path += folderName;
                        }
                    })
                });
    },

    getFileParent(file) {
        let self = this;
        let promisedParentMeta;

        if(file.get('_modelType') === 'folder') {
          promisedParentMeta = new RSVP.Promise(resolve => {
            resolve({
              id:   file.get('parentId'),
              type: file.get('parentCollection')
            });
          });
        }
        else {
          let url = config.apiUrl + '/item/' + file.get('id') + '/rootpath';
          promisedParentMeta = this.get('authRequest').send(url)
            .then(response => {
              let parent = response.pop();
              return {
                id:   parent.object._id,
                type: parent.type
              };
            })
          ;
        }

        return promisedParentMeta;
      },

    didInsertElement() {
        this._super(...arguments);

        this.getTaleFiles();
    },

    didRender () {
        // Create the tooltips after the template has been rendered
        this.create_tooltips();
    },

    create_tooltips() {
        // Creates the popup balloons for the info tooltips

        // Create the popup in the main title
        $('.info.circle.blue.icon.main').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.main',
            hoverable: true,
            html: "Get a citeable DOI by publishing your Tale on DataONE. \
            For more information on how to publish and cite your tale, visit the \
            publishing guide."
          });

        // Create the CC0 popup
        $('.info.circle.blue.icon.cc0').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.cc0',
            hoverable: true,
            html: "Place this tale in the public domain and opt out of copyright protection. \
            For more information, visit the CC0 reference page."
          });

        // Create the CCBY3 popup
        $('.info.circle.blue.icon.ccby3').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.ccby3',
            hoverable: true,
            html: "Require that users properly attribute the authors of this tale with the CCBY 3.0 standards. \
            For more information, visit the CCBY3 reference page."
          });

        // Create the CCBY4 popup
        $('.info.circle.blue.icon.ccby4').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.ccby4',
            hoverable: true,
            html: "Require that users properly attribute the authors of this tale with the CCBY 4.0 standards. \
            For more information, visit the CCBY4 reference page."
          });
    },

    joinArray(arr) {
        let joined = String();
        arr.forEach(function(item) {
            joined += "'" +item+"'";
        })
        return joined;
    },

    prepareItemIds() {
        let itemIdList = []
        this.get('fileList').forEach(function(item) {
            itemIdList.push(JSON.stringify(item.id));
        })

        console.log(itemIdList);
        return itemIdList;
    },

    getDataONEJWT() {
        /*
        Queries the DataONE `token` endpoint for the jwt. When a user signs into
        DataONE a cookie is created, which is checked by `token`. If the cookie wasn't
        found, then the response will be empty. Otherwise the jwt is returned.
        */

        // Use the XMLHttpRequest to handle the request
        let xmlHttp = new XMLHttpRequest();
        // Open the request to the the token endpoint, which will return the jwt if logged in
        if(config.dev) {
            xmlHttp.open("GET", 'https://cn-stage-2.test.dataone.org/portal/token', false );
        }
        else {
        xmlHttp.open("GET", 'https://cn.dataone.org/portal/token', false );
        }
        // Set the response content type
        xmlHttp.setRequestHeader("Content-Type", "text/xml");
        // Let XMLHttpRequest know to use cookies
        xmlHttp.withCredentials = true;
        xmlHttp.send(null);

        return xmlHttp.responseText;
    },

    dataoneLogin() {
        /*
        Responsible for opening the login dialog for the user. Ideally, we could
        tell when the user finishes logging in so that we know when to fetch the token
        */
       let url = 'http://cn-stage-2.test.dataone.org/portal/oauth?action=start&target='+config.authRedirect;
       let newwindow=window.open(url,'auth','height=400,width=450');
    },

    loggedIntoDataONE() {
        // Returns true/false if the user is logged into DataONE.
        // Note that this resets the jwt
        if (this.get('dataoneJWT')) {
            return true;
        }
        return false;
    },

    attemptLogin() {
        let jwt = this.getDataONEJWT();
        if (!jwt) {
            return false;
        }
        this.setDataONEJWT(jwt);
        return true;
    },

    setDataONEJWT(dataoneJWT) {
        this.set('dataoneJWT', dataoneJWT);
    },

    publish: function(){
        let self = this;

        // Set the url parameters for the endpoint
        let queryParams = '?'+[
            'itemIds=' + '["+(self.prepareItemIds().join(','))+"]',
            'taleId=' + self.get('modalContext'),
            'repository=' + self.get('repositoryMapping')[self.get('selectedRepository')],
            'jwt=' + self.get('dataoneJWT'),
            'licenseId='+self.getSelectedLicense()
        ].join('&');

        let url = config.apiUrl + '/repository/createPackage' + queryParams;

        this.get('authRequest').send(url)
            .then(rep => {
                // Update the UI state
                self.set('enablePublish', false);
                this.set('publishingFinish', true);
                self.set('packageUrl', rep);
                self.set('publishing', false);
            });
    },

    openPackage: function() {
        let win = window.open(this.get('packageUrl'), '_blank');
        win.focus();
        return;
},

getSelectedLicense() {
    // Returns the id of the selected license
    let selected_radio = $('input[name=license-radio]:checked').parent();
    if (selected_radio.length) {
        return selected_radio[0].id;
    }
    //If we can't find a checked radio, default to 0
    return '0';
  },

    actions: {
        publishedClicked(){
            /*
            Called when the `Publish` button is clicked. It controls the flow
            of logging in and communicating with the `createPackage` endpoint.
            */
           let self = this;
           if (self.get('publishingFinish')) {
            self.openPackage();
           }

           // Disable the button so that it isn't accidentally clicked multiple times
           self.set('enablePublish', false);

           // Let the UI know that the user clicked the `Publish` button.
           self.set('publishing', true);

           let loggedIn = self.attemptLogin()

           if (loggedIn) {
               // If they are, go ahead and publish
               self.publish();
           }
           else {
               // If they aren't logged in, prompt them to do so
               self.dataoneLogin();
               self.setDataONEJWT();
               self.publish();
               self.set('enablePublish', false);
               self.set('publishing', false);
           }

            // Return false so the dlg stays open
           return false;
        },

        cancelPublish: function() {
            console.log('Canceling');
        },

        onNodeChange: function() {
            if(this.get('selectedRepository') != '')
            {
               // setPublishBtnState(true)
            }
            else
            {
                //setPublishBtnState(false)
            }

        }
    },
});
<commit_msg>Make url https<commit_after>import Ember from 'ember';
import RSVP from 'rsvp';

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

export default Ember.Component.extend({
    authRequest: Ember.inject.service(),
    internalState: Ember.inject.service(),
    // Controls the state of the publish button
    enablePublish: true,
    // Set the default repository to dev
    selectedRepository: 'Development',
    // Flag set to show the spinner
    publishing: false,
    // Set when publishing has finished
    publishingFinish: false,
    // An array that holds a pair, (fileName, fileSize)
    fileList: [],
    // Holds an array of objects that the user cannot be exclude from their package
    nonOptionalFile: ['tale.yaml', 'environment.zip'],
    // A map that connects the repository dropdown to a url
    repositoryMapping: {'Development': 'https://dev.nceas.ucsb.edu/knb/d1/mn/'},
    // The url for the published tale. This is set after publication succeeds
    packageUrl: '',
    // The jwt token that allows the user to interact with DataONE
    dataoneJWT: '',

    setPublishBtnState(state) {
        this.set('enablePublish', state);
    },

    getTaleFiles() {
        let url = config.apiUrl + '/tale/'+ this.get('modalContext')
        let self = this;
        this.get('authRequest').send(url)
        .then(rep => {
            let folderId = rep['folderId'];
            let queryParams = '?'+[
                'folderId='+folderId,
                'limit=0',
                'sort=lowerName',
                'sortdir=1'
            ].join('&');

            url = config.apiUrl + '/item' + queryParams;
            let options = {
                method: 'GET',
                data: {
                    folderId: folderId,
                    limit: 0,
                    sort: 'lowerName',
                    sortdir: 1
                }};
            self.get('authRequest').send(url)
            .then(rep => {
                let fileList2 = []
                rep.forEach(function(item)
                {
                    let path = item.name;
                    fileList2.push({'name': path, 'size':item.size, 'id': item._id});
                })
                self.set('fileList', fileList2);
            })
        })
    },

    getPath(id) {
            let url = config.apiUrl + '/item/'+ id + '/rootpath';
            let self = this;
            let path = '';
            let hasRoot = false;

            this.get('authRequest').send(url)
                .then(rep => {
                    rep.forEach(function(folder) {
                        let folderName = folder.object['name'];
                        if (folderName === 'Data' || folderName === 'Home') {
                            path += folderName;
                            hasRoot = true;
                        }
                        if (hasRoot) {
                            path += folderName;
                        }
                    })
                });
    },

    getFileParent(file) {
        let self = this;
        let promisedParentMeta;

        if(file.get('_modelType') === 'folder') {
          promisedParentMeta = new RSVP.Promise(resolve => {
            resolve({
              id:   file.get('parentId'),
              type: file.get('parentCollection')
            });
          });
        }
        else {
          let url = config.apiUrl + '/item/' + file.get('id') + '/rootpath';
          promisedParentMeta = this.get('authRequest').send(url)
            .then(response => {
              let parent = response.pop();
              return {
                id:   parent.object._id,
                type: parent.type
              };
            })
          ;
        }

        return promisedParentMeta;
      },

    didInsertElement() {
        this._super(...arguments);

        this.getTaleFiles();
    },

    didRender () {
        // Create the tooltips after the template has been rendered
        this.create_tooltips();
    },

    create_tooltips() {
        // Creates the popup balloons for the info tooltips

        // Create the popup in the main title
        $('.info.circle.blue.icon.main').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.main',
            hoverable: true,
            html: "Get a citeable DOI by publishing your Tale on DataONE. \
            For more information on how to publish and cite your tale, visit the \
            publishing guide."
          });

        // Create the CC0 popup
        $('.info.circle.blue.icon.cc0').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.cc0',
            hoverable: true,
            html: "Place this tale in the public domain and opt out of copyright protection. \
            For more information, visit the CC0 reference page."
          });

        // Create the CCBY3 popup
        $('.info.circle.blue.icon.ccby3').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.ccby3',
            hoverable: true,
            html: "Require that users properly attribute the authors of this tale with the CCBY 3.0 standards. \
            For more information, visit the CCBY3 reference page."
          });

        // Create the CCBY4 popup
        $('.info.circle.blue.icon.ccby4').popup({
            position : 'right center',
            target   : '.info.circle.blue.icon.ccby4',
            hoverable: true,
            html: "Require that users properly attribute the authors of this tale with the CCBY 4.0 standards. \
            For more information, visit the CCBY4 reference page."
          });
    },

    joinArray(arr) {
        let joined = String();
        arr.forEach(function(item) {
            joined += "'" +item+"'";
        })
        return joined;
    },

    prepareItemIds() {
        let itemIdList = []
        this.get('fileList').forEach(function(item) {
            itemIdList.push(JSON.stringify(item.id));
        })

        console.log(itemIdList);
        return itemIdList;
    },

    getDataONEJWT() {
        /*
        Queries the DataONE `token` endpoint for the jwt. When a user signs into
        DataONE a cookie is created, which is checked by `token`. If the cookie wasn't
        found, then the response will be empty. Otherwise the jwt is returned.
        */

        // Use the XMLHttpRequest to handle the request
        let xmlHttp = new XMLHttpRequest();
        // Open the request to the the token endpoint, which will return the jwt if logged in
        if(config.dev) {
            xmlHttp.open("GET", 'https://cn-stage-2.test.dataone.org/portal/token', false );
        }
        else {
        xmlHttp.open("GET", 'https://cn.dataone.org/portal/token', false );
        }
        // Set the response content type
        xmlHttp.setRequestHeader("Content-Type", "text/xml");
        // Let XMLHttpRequest know to use cookies
        xmlHttp.withCredentials = true;
        xmlHttp.send(null);

        return xmlHttp.responseText;
    },

    dataoneLogin() {
        /*
        Responsible for opening the login dialog for the user. Ideally, we could
        tell when the user finishes logging in so that we know when to fetch the token
        */
       let url = 'https://cn-stage-2.test.dataone.org/portal/oauth?action=start&target='+config.authRedirect;
       let newwindow = window.open(url,'auth','height=400,width=450');
    },

    loggedIntoDataONE() {
        // Returns true/false if the user is logged into DataONE.
        // Note that this resets the jwt
        if (this.get('dataoneJWT')) {
            return true;
        }
        return false;
    },

    attemptLogin() {
        let jwt = this.getDataONEJWT();
        if (!jwt) {
            return false;
        }
        this.setDataONEJWT(jwt);
        return true;
    },

    setDataONEJWT(dataoneJWT) {
        this.set('dataoneJWT', dataoneJWT);
    },

    publish: function(){
        let self = this;

        // Set the url parameters for the endpoint
        let queryParams = '?'+[
            'itemIds=' + '["+(self.prepareItemIds().join(','))+"]',
            'taleId=' + self.get('modalContext'),
            'repository=' + self.get('repositoryMapping')[self.get('selectedRepository')],
            'jwt=' + self.get('dataoneJWT'),
            'licenseId='+self.getSelectedLicense()
        ].join('&');

        let url = config.apiUrl + '/repository/createPackage' + queryParams;

        this.get('authRequest').send(url)
            .then(rep => {
                // Update the UI state
                self.set('enablePublish', false);
                this.set('publishingFinish', true);
                self.set('packageUrl', rep);
                self.set('publishing', false);
            });
    },

    openPackage: function() {
        let win = window.open(this.get('packageUrl'), '_blank');
        win.focus();
        return;
},

getSelectedLicense() {
    // Returns the id of the selected license
    let selected_radio = $('input[name=license-radio]:checked').parent();
    if (selected_radio.length) {
        return selected_radio[0].id;
    }
    //If we can't find a checked radio, default to 0
    return '0';
  },

    actions: {
        publishedClicked(){
            /*
            Called when the `Publish` button is clicked. It controls the flow
            of logging in and communicating with the `createPackage` endpoint.
            */
           let self = this;
           if (self.get('publishingFinish')) {
            self.openPackage();
           }

           // Disable the button so that it isn't accidentally clicked multiple times
           self.set('enablePublish', false);

           // Let the UI know that the user clicked the `Publish` button.
           self.set('publishing', true);

           let loggedIn = self.attemptLogin()

           if (loggedIn) {
               // If they are, go ahead and publish
               self.publish();
           }
           else {
               // If they aren't logged in, prompt them to do so
               self.dataoneLogin();
               self.setDataONEJWT();
               self.publish();
               self.set('enablePublish', false);
               self.set('publishing', false);
           }

            // Return false so the dlg stays open
           return false;
        },

        cancelPublish: function() {
            console.log('Canceling');
        },

        onNodeChange: function() {
            if(this.get('selectedRepository') != '')
            {
               // setPublishBtnState(true)
            }
            else
            {
                //setPublishBtnState(false)
            }

        }
    },
});
<|endoftext|>


<commit_before>import React from 'react';
import { Field, reduxForm } from '../../../../node_modules/redux-form/immutable';
import { Button, Glyphicon } from 'react-bootstrap';
import styles from './styles.css';
import {
  contactName, contactNameWarning,
  contactEmail,
  contactPhone,
  contactLastContacted,
  contactContactFrequency, contactContactFrequencyWarning,
} from '../../utils/validation';

const validate = (values) => ({
  name: contactName(values.get('name')),
  email: contactEmail(values.get('email')),
  phone: contactPhone(values.get('phone')),
  lastContacted: contactLastContacted(values.get('lastContacted')),
  contactFrequency: contactContactFrequency(values.get('contactFrequency')),
});

const warn = (values) => ({
  contactName: contactNameWarning(values.get('name')),
  contactFrequency: contactContactFrequencyWarning(values.get('contactFrequency')),
});

const renderField = ({ input, name, label, type, meta: { touched, error, warning } }) => ( // eslint-disable-line react/prop-types
  <div className={`row ${styles.formFieldRow}`}>
    <div className={`col-sm-3 ${styles.formFieldLabelDiv}`}>
      {label}:

      <input className={`${styles.formField}${(touched && error) ? ` ${styles.formFieldError}` : ''}`} {...input} placeholder={label} type={type} />
      {
        touched &&
        ((error &&
{error}
) ||
          (warning &&
{warning}
))
      }

);

class ContactForm extends React.Component { // eslint-disable-line react/prefer-stateless-function
  render() {
    const { handleSubmit, appStatus, contact } = this.props;
    const contactAppStatus = !!appStatus.get('contacts') &&
      appStatus.get('contacts').get(contact.get('id'));
    const pendingUpdate = !!contactAppStatus && (contactAppStatus.get('updatingStatus') === true);
    const updateError = !!contactAppStatus &&
      (typeof contactAppStatus.get('updatingStatus') === 'string') &&
      contactAppStatus.get('updatingStatus');

    return (

        <form onSubmit={handleSubmit}>
          <Field name="name" label="Name" type="text" component={renderField} />
          <Field name="email" label="Email" type="email" component={renderField} />
          <Field name="phone" label="Phone" type="text" component={renderField} />
          <Field name="lastContacted" label="Last Contacted" type="date" component={renderField} />
          <Field name="contactFrequency" label="Contact Frequency" type="text" component={renderField} />

                {updateError && 'Error: Couldn\'t update contact. Try again in a few seconds.'}

          <div className={`row ${styles.formControls}`}>

              <Button
                type="submit"
                bsStyle="primary"
                disabled={pendingUpdate}
                className={styles.editSubmitButton}
              >
                <Glyphicon glyph="ok" /> Submit
              </Button>
              <Button
                onClick={this.props.onCancelClick}
                bsStyle="danger"
                disabled={pendingUpdate}
                className={`pull-right ${styles.editCancelButton}`}
              >
                <Glyphicon glyph="remove" /> Cancel
              </Button>

        </form>

    );
  }
}

ContactForm.propTypes = {
  handleSubmit: React.PropTypes.func.isRequired,
  onCancelClick: React.PropTypes.func.isRequired,
  appStatus: React.PropTypes.object,
  contact: React.PropTypes.object,
};

export default reduxForm({
  validate,
  warn,
})(ContactForm);
<commit_msg>feat: Update form with bootstrap's form-horizontal styling<commit_after>import React from 'react';
import { Field, reduxForm } from '../../../../node_modules/redux-form/immutable';
import { Button, Glyphicon } from 'react-bootstrap';
import styles from './styles.css';
import {
  contactName, contactNameWarning,
  contactEmail,
  contactPhone,
  contactLastContacted,
  contactContactFrequency, contactContactFrequencyWarning,
} from '../../../utils/validation';

const validate = (values) => ({
  name: contactName(values.get('name')),
  email: contactEmail(values.get('email')),
  phone: contactPhone(values.get('phone')),
  lastContacted: contactLastContacted(values.get('lastContacted')),
  contactFrequency: contactContactFrequency(values.get('contactFrequency')),
});

const warn = (values) => ({
  contactName: contactNameWarning(values.get('name')),
  contactFrequency: contactContactFrequencyWarning(values.get('contactFrequency')),
});

const renderField = ({ input, name, label, type, meta: { touched, error, warning } }) => ( // eslint-disable-line react/prop-types

    {label}

      <input className={`form-control${(touched && error) ? ` ${styles.formFieldError}` : ''}`} {...input} placeholder={label} type={type} />
      {
        touched &&
        ((error &&
{error}
) ||
          (warning &&
{warning}
))
      }

);

class ContactForm extends React.Component { // eslint-disable-line react/prefer-stateless-function
  render() {
    const { handleSubmit, appStatus, contact } = this.props;
    const contactAppStatus = !!appStatus.get('contacts') &&
      appStatus.get('contacts').get(contact.get('id'));
    const pendingUpdate = !!contactAppStatus && (contactAppStatus.get('updatingStatus') === true);
    const updateError = !!contactAppStatus &&
      (typeof contactAppStatus.get('updatingStatus') === 'string') &&
      contactAppStatus.get('updatingStatus');

    return (

        <form onSubmit={handleSubmit} className="form-horizontal">
          <Field name="name" label="Name" type="text" component={renderField} />
          <Field name="email" label="Email" type="email" component={renderField} />
          <Field name="phone" label="Phone" type="text" component={renderField} />
          <Field name="lastContacted" label="Last Contacted" type="date" component={renderField} />
          <Field name="contactFrequency" label="Contact Frequency" type="text" component={renderField} />

                {updateError && 'Error: Couldn\'t update contact. Try again in a few seconds.'}

          <div className={`row ${styles.formControls}`}>

              <Button
                type="submit"
                bsStyle="primary"
                disabled={pendingUpdate}
                className={styles.editSubmitButton}
              >
                <Glyphicon glyph="ok" /> Submit
              </Button>
              <Button
                onClick={this.props.onCancelClick}
                bsStyle="danger"
                disabled={pendingUpdate}
                className={`pull-right ${styles.editCancelButton}`}
              >
                <Glyphicon glyph="remove" /> Cancel
              </Button>

        </form>

    );
  }
}

ContactForm.propTypes = {
  handleSubmit: React.PropTypes.func.isRequired,
  onCancelClick: React.PropTypes.func.isRequired,
  appStatus: React.PropTypes.object,
  contact: React.PropTypes.object,
};

export default reduxForm({
  validate,
  warn,
})(ContactForm);
<|endoftext|>