<commit_before>var GmailChannel = (function() {
  'use strict'
  /**
  *
  * GmailChannel - Pub/Sub & Middleware framework for easy dealing with Gmails by Channel
  *
  * GmailChannel provide a easy way to filter out the emails in gmail by search options to a named Channel,
  * then you could Sub to this Channel, and use Middleware to process them.
  *
  * Github - https://github.com/zixia/gas-gmail-channel
  *
  * Example:
  ```javascript
  if ((typeof GmailChannel)==='undefined') { // GmailChannel Initialization. (only if not initialized yet.)
    eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gas-gmail-channel/master/src/gas-gmail-channel-lib.js').getContentText())
  } // Class GmailChannel is ready for use now!

  var testChannel = new GmailChannel({
    keywords: ['the', '-abcdefghijilmn']
    , labels: ['inbox', '-trash']
    , limit: 1
    , doneLabel: 'OutOfGmailChannel'
  })

  testChannel.use(
    function (req, res, next) {
      Logger.log(req.thread.getFirstMessageSubject())
      req.data = 'set'
      next()
    }
    , function (req, res, next) {
      Logger.log('req.data got: ' + req.data)
      // NO next() here
    }
    , function (req, res,next) {
      throw Error('should not run to here')
    }
  )

  testChannel.done(function(req, res, next) {
    Logger.log('finalize after middlewares')
  })
  ```
  */

  var VERSION = '0.2.0'

  var DEFAULT = {
    name: 'GmailChannel v' + VERSION
    , labels: ['inbox', '-trash']
    , dayspan: '365'   // only check in last 365 days
    , limit: 500       // default max return 999 threads
    , doneLabel: null
    , keywords: []
    , res: JSON.parse('{}') // use {} directory will cause script editor indient error???
  }

  /************************************************************************************************
  *
  * Class GmailChannel
  * ------------------
  *
  * @param object optoins
  *    options.name string Channel Name
  *    options.labels array gmail channel labels. default: label:inbox label:unread -label:trash
  *    optoins.keywords array gmail search keywords
  *
  *    options.query string gmail search query string
  *    options.dayspan number newer_than:{{dayspan}}d
  *    options.limit number no more then {{limit}} results
  *
  *    options.doneLabel message labeled with {{doneLabel}} will be ignored
  */
  var GmailChannel = function (options) {

    if (!options) throw Error('options must be defined for GmailChannel!')

    var labels = options.labels || DEFAULT.labels
    var name = options.name || DEFAULT.name
    var res = options.res || DEFAULT.res
    var keywords = options.keywords || DEFAULT.keywords
    var limit = options.limit || DEFAULT.limit

    /**
    *
    * 1. if we don't set doneLabel/dayspan, then it should be the default label name.
    * 2. but if we set it to null, then we will not use doneLabel/dayspan anymore.
    *
    * so we use "typeof options.doneLabel === 'undefined'" to check if use defined it.
    *
    */
    if ((typeof options.doneLabel)==='undefined') {
      var doneLabel = DEFAULT.doneLabel;
    } else {
      doneLabel = options.doneLabel
    }

    if ((typeof options.dayspan)==='undefined') {
      var dayspan = DEFAULT.dayspan;
    } else {
      dayspan = options.dayspan
    }

    /**
    *
    * validation options input
    *
    */
    if (limit%1 !== 0 || limit>500) throw Error('limit must be integer(<500) for GmailChannle! error: limit=' + limit );

    if (!(labels instanceof Array) || !(keywords instanceof Array)) throw Error('options.keywords or options.labels must be array for GmailChannel!')

    /////////////////////////////////////////////////////
    //
    // queryString start building. to filter out email

    // 1. query
    var queryString  = options.query || ''
    // 2. timespan
    if (dayspan!==null) queryString += ' ' + 'newer_than:' + dayspan + 'd'
    // 3. -doneLabel
    if (doneLabel!==null) queryString += ' ' + '-label:' + doneLabel
    // 4. keywords
    keywords.forEach(function (k) {
      if (!k) return

      var minusKeyword = /^-(.+)$/.exec(k)
      if (/"/.test(k)) { // 1. keyword里面有引号(")
        queryString += ' ' + k + ' '
      } else if (minusKeyword) { // 2. keyword前面有减号(-)
        queryString += ' ' + '-' + '"' + minusKeyword[1] + '" '
      } else { // 3. 其他
        queryString += ' ' +       '"' + k + '" '
      }
    })
    // 5. labels
    labels.forEach(function (l) {
      if (!l) return // tolearnt empty label

      var minusLabel = /^-(.+)$/.exec(l)
      if (minusLabel) {
        queryString += ' ' + '-label:' + minusLabel[1]
      } else {
        queryString += ' ' + 'label:' + l
      }
    })

    // queryString has been built.
    //
    ///////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////
    //
    // UPPER_CASE varibles for quota in instance methods

    if (doneLabel) {
      var DONE_LABEL = GmailApp.getUserLabelByName(doneLabel)
      if (!DONE_LABEL) DONE_LABEL = GmailApp.createLabel(doneLabel)
    } else {
      DONE_LABEL = null
    }

    var NAME = name
    var LIMIT = limit
    var QUERY_STRING = queryString
    var RES = res
    var MIDDLEWARES = [] // for use() use

    // UPPER_CASE variables set
    //
    ///////////////////////////////////////////////////////

    /**
    * Instance of this
    */

    this.use = use
    this.done = done

    this.getName = getName
    this.getQueryString = function () { return QUERY_STRING }
    this.getMiddlewares = function () { return MIDDLEWARES }

    return this

    ////////////////////////////////////////////////////////
    //
    // Instance Methods

    function use(middleware) {

      if (middleware instanceof Array) return middleware.map(function (m) { return use(m) })

      if (!(middleware instanceof Function)) throw Error('must use function(s) for middleware! error[' + middleware + ']')

      MIDDLEWARES.push(middleware)

      /**
      *
      * in case of use(fn1, fn2....)
      *
      */
      if (arguments.length>1) {
        return Array.prototype.slice.call(arguments, 1).map(function (m) { return use(m) })
      }

      return true
    }

    /**
    *
    * Run all middlewares for each thread, then call finallCallback(if specified)
    *
    * @param <Function> finalCallback
    *   finalCallback will be called at the end of all middleware had ran.
    *   because middleware could be terminated at the middle, the lastest one could not know what happend.
    *
    */
    function done(finalCallback) {

      if (finalCallback && (!finalCallback instanceof Function)) throw Error('done need a function param to be finalCallback')

      var mailThreads = getNewThreads(LIMIT)

      var res, req

      for (var i=0; i<mailThreads.length; i++) {

        /**
        *
        * re-init res & req
        *
        */
        res = {}
        copyKeys(res, RES)

        req = {
          getChannelName: getName
          , getThread: (function (t) { return function () { return t } })(mailThreads[i]) // closure for the furture possible run in nodejs, because of async call back
          , errors: []
          // , thread: mailThreads[i] // Deprecated
        }

        for (var j=0; j<MIDDLEWARES.length; j++) {

          var middleware = MIDDLEWARES[j]

          var isNextCalled = false
          var error = undefined

          try {
            middleware(req, res, function (err) {
              isNextCalled = true
              error = err
            })
          } catch (e) {
            error = e
          }

          if (error) req.errors.push(error)

          if (!isNextCalled) {
            // loop end, because middleware did not call next
            break
          }

        } // END for loop of MIDDLEWARES

        if (finalCallback) finalCallback(req, res, function (err) { } )

        if (DONE_LABEL) mailThreads[i].addLabel(DONE_LABEL);

      } // END for loop of mailThreads

    }

    function getName() { return NAME }

    /**
    *
    * return arrary[]
    *
    */
    function getNewThreads(limit) {

      if (!limit) limit = 500 // 500 is the max number that gmail permit.
      if (!QUERY_STRING) return []

      // search for mails
      var threads = GmailApp.search(QUERY_STRING, 0, limit)

      return threads
    }
  }

  /**
  * Class GmailChannel
  */

  GmailChannel.getVersion = getVersion
  GmailChannel.copyKeys = copyKeys

  return GmailChannel

  ////////////////////////////////////////////////
  //                                           //
  // Class Static Methods                     //
  //                                         //
  ////////////////////////////////////////////

  function getVersion() {
    return VERSION
  }

  function copyKeys(destObj, srcObj) {
    for (var key in srcObj) {
      destObj[key] = srcObj[key]
    }
  }

}())
<commit_msg>use() report more detail error for middleware error<commit_after>var GmailChannel = (function() {
  'use strict'
  /**
  *
  * GmailChannel - Pub/Sub & Middleware framework for easy dealing with Gmails by Channel
  *
  * GmailChannel provide a easy way to filter out the emails in gmail by search options to a named Channel,
  * then you could Sub to this Channel, and use Middleware to process them.
  *
  * Github - https://github.com/zixia/gas-gmail-channel
  *
  * Example:
  ```javascript
  if ((typeof GmailChannel)==='undefined') { // GmailChannel Initialization. (only if not initialized yet.)
    eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gas-gmail-channel/master/src/gas-gmail-channel-lib.js').getContentText())
  } // Class GmailChannel is ready for use now!

  var testChannel = new GmailChannel({
    keywords: ['the', '-abcdefghijilmn']
    , labels: ['inbox', '-trash']
    , limit: 1
    , doneLabel: 'OutOfGmailChannel'
  })

  testChannel.use(
    function (req, res, next) {
      Logger.log(req.thread.getFirstMessageSubject())
      req.data = 'set'
      next()
    }
    , function (req, res, next) {
      Logger.log('req.data got: ' + req.data)
      // NO next() here
    }
    , function (req, res,next) {
      throw Error('should not run to here')
    }
  )

  testChannel.done(function(req, res, next) {
    Logger.log('finalize after middlewares')
  })
  ```
  */

  var VERSION = '0.2.0'

  var DEFAULT = {
    name: 'GmailChannel v' + VERSION
    , labels: ['inbox', '-trash']
    , dayspan: '365'   // only check in last 365 days
    , limit: 500       // default max return 999 threads
    , doneLabel: null
    , keywords: []
    , res: JSON.parse('{}') // use {} directory will cause script editor indient error???
  }

  /************************************************************************************************
  *
  * Class GmailChannel
  * ------------------
  *
  * @param object optoins
  *    options.name string Channel Name
  *    options.labels array gmail channel labels. default: label:inbox label:unread -label:trash
  *    optoins.keywords array gmail search keywords
  *
  *    options.query string gmail search query string
  *    options.dayspan number newer_than:{{dayspan}}d
  *    options.limit number no more then {{limit}} results
  *
  *    options.doneLabel message labeled with {{doneLabel}} will be ignored
  */
  var GmailChannel = function (options) {

    if (!options) throw Error('options must be defined for GmailChannel!')

    var labels = options.labels || DEFAULT.labels
    var name = options.name || DEFAULT.name
    var res = options.res || DEFAULT.res
    var keywords = options.keywords || DEFAULT.keywords
    var limit = options.limit || DEFAULT.limit

    /**
    *
    * 1. if we don't set doneLabel/dayspan, then it should be the default label name.
    * 2. but if we set it to null, then we will not use doneLabel/dayspan anymore.
    *
    * so we use "typeof options.doneLabel === 'undefined'" to check if use defined it.
    *
    */
    if ((typeof options.doneLabel)==='undefined') {
      var doneLabel = DEFAULT.doneLabel;
    } else {
      doneLabel = options.doneLabel
    }

    if ((typeof options.dayspan)==='undefined') {
      var dayspan = DEFAULT.dayspan;
    } else {
      dayspan = options.dayspan
    }

    /**
    *
    * validation options input
    *
    */
    if (limit%1 !== 0 || limit>500) throw Error('limit must be integer(<500) for GmailChannle! error: limit=' + limit );

    if (!(labels instanceof Array) || !(keywords instanceof Array)) throw Error('options.keywords or options.labels must be array for GmailChannel!')

    /////////////////////////////////////////////////////
    //
    // queryString start building. to filter out email

    // 1. query
    var queryString  = options.query || ''
    // 2. timespan
    if (dayspan!==null) queryString += ' ' + 'newer_than:' + dayspan + 'd'
    // 3. -doneLabel
    if (doneLabel!==null) queryString += ' ' + '-label:' + doneLabel
    // 4. keywords
    keywords.forEach(function (k) {
      if (!k) return

      var minusKeyword = /^-(.+)$/.exec(k)
      if (/"/.test(k)) { // 1. keyword里面有引号(")
        queryString += ' ' + k + ' '
      } else if (minusKeyword) { // 2. keyword前面有减号(-)
        queryString += ' ' + '-' + '"' + minusKeyword[1] + '" '
      } else { // 3. 其他
        queryString += ' ' +       '"' + k + '" '
      }
    })
    // 5. labels
    labels.forEach(function (l) {
      if (!l) return // tolearnt empty label

      var minusLabel = /^-(.+)$/.exec(l)
      if (minusLabel) {
        queryString += ' ' + '-label:' + minusLabel[1]
      } else {
        queryString += ' ' + 'label:' + l
      }
    })

    // queryString has been built.
    //
    ///////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////
    //
    // UPPER_CASE varibles for quota in instance methods

    if (doneLabel) {
      var DONE_LABEL = GmailApp.getUserLabelByName(doneLabel)
      if (!DONE_LABEL) DONE_LABEL = GmailApp.createLabel(doneLabel)
    } else {
      DONE_LABEL = null
    }

    var NAME = name
    var LIMIT = limit
    var QUERY_STRING = queryString
    var RES = res
    var MIDDLEWARES = [] // for use() use

    // UPPER_CASE variables set
    //
    ///////////////////////////////////////////////////////

    /**
    * Instance of this
    */

    this.use = use
    this.done = done

    this.getName = getName
    this.getQueryString = function () { return QUERY_STRING }
    this.getMiddlewares = function () { return MIDDLEWARES }

    return this

    ////////////////////////////////////////////////////////
    //
    // Instance Methods

    function use(middleware) {

      if (middleware instanceof Array) {
        for (var i in middleware) {
          if (!(middleware[i] instanceof Function)) throw Error('use(middleware[' + i + ']) is not function!');
        }
        return middleware.map(function (m) { return use(m) })
      }

      if (!(middleware instanceof Function)) throw Error('must use function for middleware! error[' + middleware + ']')

      MIDDLEWARES.push(middleware)

      /**
      *
      * in case of use(fn1, fn2....)
      *
      */
      if (arguments.length>1) {
        return Array.prototype.slice.call(arguments, 1).map(function (m) { return use(m) })
      }

      return true
    }

    /**
    *
    * Run all middlewares for each thread, then call finallCallback(if specified)
    *
    * @param <Function> finalCallback
    *   finalCallback will be called at the end of all middleware had ran.
    *   because middleware could be terminated at the middle, the lastest one could not know what happend.
    *
    */
    function done(finalCallback) {

      if (finalCallback && (!finalCallback instanceof Function)) throw Error('done need a function param to be finalCallback')

      var mailThreads = getNewThreads(LIMIT)

      var res, req

      for (var i=0; i<mailThreads.length; i++) {

        /**
        *
        * re-init res & req
        *
        */
        res = {}
        copyKeys(res, RES)

        req = {
          getChannelName: getName
          , getThread: (function (t) { return function () { return t } })(mailThreads[i]) // closure for the furture possible run in nodejs, because of async call back
          , errors: []
          // , thread: mailThreads[i] // Deprecated
        }

        for (var j=0; j<MIDDLEWARES.length; j++) {

          var middleware = MIDDLEWARES[j]

          var isNextCalled = false
          var error = undefined

          try {
            middleware(req, res, function (err) {
              isNextCalled = true
              error = err
            })
          } catch (e) {
            error = e
          }

          if (error) req.errors.push(error)

          if (!isNextCalled) {
            // loop end, because middleware did not call next
            break
          }

        } // END for loop of MIDDLEWARES

        if (finalCallback) finalCallback(req, res, function (err) { } )

        if (DONE_LABEL) mailThreads[i].addLabel(DONE_LABEL);

      } // END for loop of mailThreads

    }

    function getName() { return NAME }

    /**
    *
    * return arrary[]
    *
    */
    function getNewThreads(limit) {

      if (!limit) limit = 500 // 500 is the max number that gmail permit.
      if (!QUERY_STRING) return []

      // search for mails
      var threads = GmailApp.search(QUERY_STRING, 0, limit)

      return threads
    }
  }

  /**
  * Class GmailChannel
  */

  GmailChannel.getVersion = getVersion
  GmailChannel.copyKeys = copyKeys

  return GmailChannel

  ////////////////////////////////////////////////
  //                                           //
  // Class Static Methods                     //
  //                                         //
  ////////////////////////////////////////////

  function getVersion() {
    return VERSION
  }

  function copyKeys(destObj, srcObj) {
    for (var key in srcObj) {
      destObj[key] = srcObj[key]
    }
  }

}())
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');
var util = require('util');
var events = require('events');
var env = require('./env');

var ServerCtrl = function () {
    events.EventEmitter.call(this);
};

util.inherits(ServerCtrl, events.EventEmitter);

ServerCtrl.prototype.servers = {};
ServerCtrl.prototype.env = env;

ServerCtrl.prototype.start = function(presetName, cb) {
    if (this.status(presetName) === 1) {
        throw new Error('Preset ' + presetName + ' is already active');
    }

    var server = require('./server')(presetName);

    spawnProcess(server);
    writePidFile(server);
    connectParsers(server);
    connectPlugins(server);

    this.servers[presetName] = server;
    console.log('Started server', server.name, 'PID:', server.proc.pid);
    this.emit('serverstart', server);

    if(typeof cb === 'function') {
        cb(presetName);
    }
};

ServerCtrl.prototype.stop = function(presetName, cb) {
    var server = this.servers[presetName];
    server.proc.kill();
    delete this.servers[presetName];
    console.log('Stopped server', server.name, 'PID:', server.proc.pid);
    this.emit('serverstop', server);

    if(typeof cb === 'function') {
        cb(presetName);
    }
};

ServerCtrl.prototype.status = function(presetName, cb) {
    var server = this.servers[presetName];
    var status;

    if(server === undefined) {
        status = 0;
    }
    else if (fs.existsSync(server.pidFile) === false) {
        status = 0;
    }
    else if(server.proc === undefined) {
        status = 0;
    }
    else {
        try {
            process.kill(server.proc.pid, 0);
            status = 1;
        }
        catch (e) {
            handleExit(server);
            status = -1;
        }
    }

    if(typeof cb === 'function') {
        cb(presetName, status);
    }
};

module.exports = exports = new ServerCtrl();

var handleExit = function(server) {
    fs.unlink(server.pidFile, function(err) {
        if(err) return console.error(err);
        console.log('Server process exit, removing pid file: ', server.pidFile);
    });
};

var connectParsers = function(server) {
    require('./server-parser').connect(server);
};

var connectPlugins = function(server) {
    require('./server-plugins').connect(server);
};

var writePidFile = function(server) {
    fs.writeFile(server.pidFile, server.proc.pid, function(err) {
        if(err) return console.error(err);
    });
};

var spawnProcess = function(server) {
    var exe = env.getServerExecutable();
    var args = [
        '-c', path.join(server.workPath, 'server_cfg.ini'),
        '-e', path.join(server.workPath, 'entry_list.ini')
    ];
    var opts = {
        cwd: env.getACPath()
    };

    var proc = require('child_process').spawn(exe, args, opts);
    proc.on('exit', handleExit.bind(null, server));
    proc.on('SIGINT', handleExit.bind(null, server));
    proc.on('uncaughtException', handleExit.bind(null, server));
    server.proc = proc;
};
<commit_msg>fixed access to servername<commit_after>var fs = require('fs');
var path = require('path');
var util = require('util');
var events = require('events');
var env = require('./env');

var ServerCtrl = function () {
    events.EventEmitter.call(this);
};

util.inherits(ServerCtrl, events.EventEmitter);

ServerCtrl.prototype.servers = {};
ServerCtrl.prototype.env = env;

ServerCtrl.prototype.start = function(presetName, cb) {
    if (this.status(presetName) === 1) {
        throw new Error('Preset ' + presetName + ' is already active');
    }

    var server = require('./server')(presetName);

    spawnProcess(server);
    writePidFile(server);
    connectParsers(server);
    connectPlugins(server);

    this.servers[presetName] = server;
    console.log('Started server', server.preset.serverName, 'PID:', server.proc.pid);
    this.emit('serverstart', server);

    if(typeof cb === 'function') {
        cb(presetName);
    }
};

ServerCtrl.prototype.stop = function(presetName, cb) {
    var server = this.servers[presetName];
    server.proc.kill();
    delete this.servers[presetName];
    console.log('Stopped server', server.name, 'PID:', server.proc.pid);
    this.emit('serverstop', server);

    if(typeof cb === 'function') {
        cb(presetName);
    }
};

ServerCtrl.prototype.status = function(presetName, cb) {
    var server = this.servers[presetName];
    var status;

    if(server === undefined) {
        status = 0;
    }
    else if (fs.existsSync(server.pidFile) === false) {
        status = 0;
    }
    else if(server.proc === undefined) {
        status = 0;
    }
    else {
        try {
            process.kill(server.proc.pid, 0);
            status = 1;
        }
        catch (e) {
            handleExit(server);
            status = -1;
        }
    }

    if(typeof cb === 'function') {
        cb(presetName, status);
    }
};

module.exports = exports = new ServerCtrl();

var handleExit = function(server) {
    fs.unlink(server.pidFile, function(err) {
        if(err) return console.error(err);
        console.log('Server process exit, removing pid file: ', server.pidFile);
    });
};

var connectParsers = function(server) {
    require('./server-parser').connect(server);
};

var connectPlugins = function(server) {
    require('./server-plugins').connect(server);
};

var writePidFile = function(server) {
    fs.writeFile(server.pidFile, server.proc.pid, function(err) {
        if(err) return console.error(err);
    });
};

var spawnProcess = function(server) {
    var exe = env.getServerExecutable();
    var args = [
        '-c', path.join(server.workPath, 'server_cfg.ini'),
        '-e', path.join(server.workPath, 'entry_list.ini')
    ];
    var opts = {
        cwd: env.getACPath()
    };

    var proc = require('child_process').spawn(exe, args, opts);
    proc.on('exit', handleExit.bind(null, server));
    proc.on('SIGINT', handleExit.bind(null, server));
    proc.on('uncaughtException', handleExit.bind(null, server));
    server.proc = proc;
};
<|endoftext|>


<commit_before>const { createBooruFetcher } = require('./booru');
const { mention } = require('../../util');
const {
  requirePrefix,
  requirePermission,
  splitCommands
} = require('../../handler');
const Promise = require('bluebird');
const { BOORU_MAX_SAVE_CODE } = require('./settings');
const {
  BOORU_MODIFY_BLOCKED_TAGS,
  BOORU_SAVE_PICTURE,
  BOORU_LOOKUP
} = require('./permissions');

const RATING_TAGS = [
  'rating:explicit',
  'rating:questionable',
  'rating:safe',
  '-rating:explicit',
  '-rating:questionable',
  '-rating:safe'
];

const tagComplement = tag => tag.startsWith('-') ? tag.substr(1) :`-${tag}`;

const pushSaveCode = function(bot, channelID, url) {
  const { redis } = bot;

  return Promise.all([
    bot.resolveSetting({channelID}, BOORU_MAX_SAVE_CODE),
    redis.hincrbyAsync(`shinobu_booru_next_save_codes`, channelID, 1)
  ]).then(([ maxSaveCode, nextSaveCode ]) => {
      const resetPromise = nextSaveCode === maxSaveCode
        ? redis.hsetAsync(`shinobu_booru_next_save_codes`, channelID, 0)
        : Promise.resolve();

      const setPromise = redis.hsetAsync(
        `shinobu_booru_save_codes:${channelID}`,
        nextSaveCode,
        url
      );

      return Promise.all([setPromise, resetPromise]).then(() => nextSaveCode);
  });
};

const createLookupHandler = function(sfw, options={}) {
  const defaultRatingTags = sfw ? 'rating:safe' : '-rating:safe';

  const fetcher = createBooruFetcher(options);

  const addRatingTags = tags => tags.some(tag => RATING_TAGS.indexOf(tag) > -1)
      ? tags
      : tags.concat([defaultRatingTags]);

  const resolveTags = (tags, blockedTags) =>
    addRatingTags(tags.filter(Boolean).map(tag => tag.toLowerCase()))
      .concat(blockedTags.map(tagComplement));

  return function(bot, messageInfo) {
    const userTags = messageInfo.tokens;
    const serverID = bot.serverFromChannelID(messageInfo.channelID);

    const tagPromise = bot.redis.smembersAsync(
      `shinobu_blocked_booru_tags:${serverID}`
    )
    .then(blockedTags => {
      if (userTags.some(tag => blockedTags.indexOf(tag) > -1)) {
        return Promise.reject('Blocked tag detected');
      }

      return resolveTags(userTags, blockedTags);
    });

    const messagePromise = tagPromise.then(tags =>
      bot.sendMessage({
        to: messageInfo.channelID,
        message: `**Search for tags:** ${tags.join(', ')}.`
      }));

    const urlPromise = tagPromise.then(fetcher).then(results => {
      if (results.length === 0) {
        return Promise.reject('No results found');
      }

      return 'https:' + results[Math.floor(Math.random() * results.length)];
    });

    const saveCodePromise = urlPromise.then(url =>
      pushSaveCode(bot, messageInfo.channelID, url));

    return Promise.all([
      urlPromise,
      messagePromise,
      tagPromise,
      saveCodePromise
    ])
    .then(([url, message, tags, saveCode]) => {
      const deletePromise = bot.deleteMessage({
        channelID: messageInfo.channelID,
        messageID: message.id
      });

      const sendPromise = bot.sendMessage({
        to: messageInfo.channelID,
        message: mention(messageInfo.userID),
        embed: {
          title: `**Result found for tags:** ${tags.join(', ')}.`,
          description: `Send \`!qt save ${saveCode}\` to save this picture.`,
          color: 0x6DEB60,
          url,
          image: {
            url
          },
          provider: {
            name: 'Gelbooru',
            url: 'http://gelbooru.com/'
          }
        }
      });

      return Promise.all([deletePromise, sendPromise]);
    })

    .catch(reason => {
      if (reason === 'Blocked tag detected') {
        return bot.sendMessage({
          to: messageInfo.channelID,
          message: `${mention(messageInfo.userID)} Blocked tag detected`
        });
      } else if (reason === 'No results found') {
        if (!messagePromise.isRejected() && !tagPromise.isRejected()) {
          return Promise.all([messagePromise, tagPromise])
          .then(([, tags]) =>
            bot.sendMessage({
              to: messageInfo.channelID,
              message: `**No results found for tags:** ${tags.join(', ')}.`
            })
          );
        }
      } else {
        return Promise.reject(reason);
      }
    });
  };
};

const handleSave = function(bot, messageInfo) {
  const { tokens, channelID } = messageInfo;

  if (tokens.length != 1) {
    return Promise.resolve(false);
  }

  const id = Number(tokens[0]);

  if (isNaN(id)) {
    return Promise.resolve(false);
  }

  const { redis } = bot;
  const serverID = bot.serverFromChannelID(channelID);

  const urlPromise = redis.hgetAsync(
    `shinobu_booru_save_codes:${channelID}`,
    id
  );

  urlPromise.then(() =>
    redis.hdelAsync(`shinobu_booru_save_codes:${channelID}`, id));

  const savePromise = urlPromise.then(url =>
    redis.saddAsync(`shinobu_booru_saved_images:${serverID}`, url));

  return Promise.all([
    urlPromise,
    savePromise
  ]).then(() => bot.sendMessage({
    to: channelID,
    message: 'Image url saved'
  }));
};

const handleSaved = function(bot, messageInfo) {
  const { redis } = bot;
  const { channelID } = messageInfo;
  const serverID = bot.serverFromChannelID(channelID);

  return redis.srandmemberAsync(`shinobu_booru_saved_images:${serverID}`)
    .then(url => bot.sendMessage({
      to: channelID,
      message: mention(messageInfo.userID),
      embed: {
          title: `Saved picture`,
          color: 0x6DEB60,
          url,
          image: {
            url
          },
          provider: {
            name: 'Gelbooru',
            url: 'http://gelbooru.com/'
          }
        }
    }));
};

const handleBlock = function(bot, messageInfo) {
  const tags = messageInfo.tokens.map(encodeURIComponent);

  if (!tags.length) {
    return Promise.resolve('noop');
  }

  const serverID = bot.serverFromChannelID(messageInfo.channelID);

  return bot.redis.saddAsync.apply(
    bot.redis,
    [`shinobu_blocked_booru_tags:${serverID}`].concat(tags)
  ).then(() => bot.sendMessage({
      to: messageInfo.channelID,
      message: `**Blocked tags:** ${tags.join(', ')}.`
    }));
};

const handleAllow = function(bot, messageInfo) {
  const tags = messageInfo.tokens.map(encodeURIComponent);

  if (!tags.length) {
    return Promise.resolve('noop');
  }

  const serverID = bot.serverFromChannelID(messageInfo.channelID);

  return bot.redis.sremAsync.apply(
    bot.redis,
    [`shinobu_blocked_booru_tags:${serverID}`].concat(tags)
  ).then(() => bot.sendMessage({
    to: messageInfo.channelID,
    message: `**Allowed tags:** ${tags.join(', ')}.`
  }));
};

const handleLookup =
  requirePermission(BOORU_LOOKUP)(createLookupHandler(true));

const handlers = {
  nsfw: requirePermission(BOORU_LOOKUP)(createLookupHandler(false)),
  saved: requirePermission(BOORU_LOOKUP)(handleSaved),
  block: requirePermission(BOORU_MODIFY_BLOCKED_TAGS)(handleBlock),
  allow: requirePermission(BOORU_MODIFY_BLOCKED_TAGS)(handleAllow),
  save: requirePermission(BOORU_SAVE_PICTURE)(handleSave)
};

const booruLookup =
  requirePrefix('!qt')(splitCommands(handlers, handleLookup));

module.exports = booruLookup;
<commit_msg>add help message to booru save command<commit_after>const { createBooruFetcher } = require('./booru');
const { mention } = require('../../util');
const {
  requirePrefix,
  requirePermission,
  splitCommands
} = require('../../handler');
const Promise = require('bluebird');
const { BOORU_MAX_SAVE_CODE } = require('./settings');
const {
  BOORU_MODIFY_BLOCKED_TAGS,
  BOORU_SAVE_PICTURE,
  BOORU_LOOKUP
} = require('./permissions');

const RATING_TAGS = [
  'rating:explicit',
  'rating:questionable',
  'rating:safe',
  '-rating:explicit',
  '-rating:questionable',
  '-rating:safe'
];

const tagComplement = tag => tag.startsWith('-') ? tag.substr(1) :`-${tag}`;

const pushSaveCode = function(bot, channelID, url) {
  const { redis } = bot;

  return Promise.all([
    bot.resolveSetting({channelID}, BOORU_MAX_SAVE_CODE),
    redis.hincrbyAsync(`shinobu_booru_next_save_codes`, channelID, 1)
  ]).then(([ maxSaveCode, nextSaveCode ]) => {
      const resetPromise = nextSaveCode === maxSaveCode
        ? redis.hsetAsync(`shinobu_booru_next_save_codes`, channelID, 0)
        : Promise.resolve();

      const setPromise = redis.hsetAsync(
        `shinobu_booru_save_codes:${channelID}`,
        nextSaveCode,
        url
      );

      return Promise.all([setPromise, resetPromise]).then(() => nextSaveCode);
  });
};

const createLookupHandler = function(sfw, options={}) {
  const defaultRatingTags = sfw ? 'rating:safe' : '-rating:safe';

  const fetcher = createBooruFetcher(options);

  const addRatingTags = tags => tags.some(tag => RATING_TAGS.indexOf(tag) > -1)
      ? tags
      : tags.concat([defaultRatingTags]);

  const resolveTags = (tags, blockedTags) =>
    addRatingTags(tags.filter(Boolean).map(tag => tag.toLowerCase()))
      .concat(blockedTags.map(tagComplement));

  return function(bot, messageInfo) {
    const userTags = messageInfo.tokens;
    const serverID = bot.serverFromChannelID(messageInfo.channelID);

    const tagPromise = bot.redis.smembersAsync(
      `shinobu_blocked_booru_tags:${serverID}`
    )
    .then(blockedTags => {
      if (userTags.some(tag => blockedTags.indexOf(tag) > -1)) {
        return Promise.reject('Blocked tag detected');
      }

      return resolveTags(userTags, blockedTags);
    });

    const messagePromise = tagPromise.then(tags =>
      bot.sendMessage({
        to: messageInfo.channelID,
        message: `**Search for tags:** ${tags.join(', ')}.`
      }));

    const urlPromise = tagPromise.then(fetcher).then(results => {
      if (results.length === 0) {
        return Promise.reject('No results found');
      }

      return 'https:' + results[Math.floor(Math.random() * results.length)];
    });

    const saveCodePromise = urlPromise.then(url =>
      pushSaveCode(bot, messageInfo.channelID, url));

    return Promise.all([
      urlPromise,
      messagePromise,
      tagPromise,
      saveCodePromise
    ])
    .then(([url, message, tags, saveCode]) => {
      const deletePromise = bot.deleteMessage({
        channelID: messageInfo.channelID,
        messageID: message.id
      });

      const sendPromise = bot.sendMessage({
        to: messageInfo.channelID,
        message: mention(messageInfo.userID),
        embed: {
          title: `**Result found for tags:** ${tags.join(', ')}.`,
          description: `Send \`!qt save ${saveCode}\` to save this picture.`,
          color: 0x6DEB60,
          url,
          image: {
            url
          },
          provider: {
            name: 'Gelbooru',
            url: 'http://gelbooru.com/'
          }
        }
      });

      return Promise.all([deletePromise, sendPromise]);
    })

    .catch(reason => {
      if (reason === 'Blocked tag detected') {
        return bot.sendMessage({
          to: messageInfo.channelID,
          message: `${mention(messageInfo.userID)} Blocked tag detected`
        });
      } else if (reason === 'No results found') {
        if (!messagePromise.isRejected() && !tagPromise.isRejected()) {
          return Promise.all([messagePromise, tagPromise])
          .then(([, tags]) =>
            bot.sendMessage({
              to: messageInfo.channelID,
              message: `**No results found for tags:** ${tags.join(', ')}.`
            })
          );
        }
      } else {
        return Promise.reject(reason);
      }
    });
  };
};

const handleSave = function(bot, messageInfo) {
  const { tokens, channelID } = messageInfo;

  if (tokens.length != 1) {
    return Promise.resolve(false);
  }

  const id = Number(tokens[0]);

  if (isNaN(id)) {
    return Promise.resolve(false);
  }

  const { redis } = bot;
  const serverID = bot.serverFromChannelID(channelID);

  const urlPromise = redis.hgetAsync(
    `shinobu_booru_save_codes:${channelID}`,
    id
  );

  urlPromise.then(() =>
    redis.hdelAsync(`shinobu_booru_save_codes:${channelID}`, id));

  const savePromise = urlPromise.then(url =>
    redis.saddAsync(`shinobu_booru_saved_images:${serverID}`, url));

  return Promise.all([
    urlPromise,
    savePromise
  ]).then(() => bot.sendMessage({
    to: channelID,
    message: 'Image url saved. Use `!qt saved` to pull up a random saved picture'
  }));
};

const handleSaved = function(bot, messageInfo) {
  const { redis } = bot;
  const { channelID } = messageInfo;
  const serverID = bot.serverFromChannelID(channelID);

  return redis.srandmemberAsync(`shinobu_booru_saved_images:${serverID}`)
    .then(url => bot.sendMessage({
      to: channelID,
      message: mention(messageInfo.userID),
      embed: {
          title: `Saved picture`,
          color: 0x6DEB60,
          url,
          image: {
            url
          },
          provider: {
            name: 'Gelbooru',
            url: 'http://gelbooru.com/'
          }
        }
    }));
};

const handleBlock = function(bot, messageInfo) {
  const tags = messageInfo.tokens.map(encodeURIComponent);

  if (!tags.length) {
    return Promise.resolve('noop');
  }

  const serverID = bot.serverFromChannelID(messageInfo.channelID);

  return bot.redis.saddAsync.apply(
    bot.redis,
    [`shinobu_blocked_booru_tags:${serverID}`].concat(tags)
  ).then(() => bot.sendMessage({
      to: messageInfo.channelID,
      message: `**Blocked tags:** ${tags.join(', ')}.`
    }));
};

const handleAllow = function(bot, messageInfo) {
  const tags = messageInfo.tokens.map(encodeURIComponent);

  if (!tags.length) {
    return Promise.resolve('noop');
  }

  const serverID = bot.serverFromChannelID(messageInfo.channelID);

  return bot.redis.sremAsync.apply(
    bot.redis,
    [`shinobu_blocked_booru_tags:${serverID}`].concat(tags)
  ).then(() => bot.sendMessage({
    to: messageInfo.channelID,
    message: `**Allowed tags:** ${tags.join(', ')}.`
  }));
};

const handleLookup =
  requirePermission(BOORU_LOOKUP)(createLookupHandler(true));

const handlers = {
  nsfw: requirePermission(BOORU_LOOKUP)(createLookupHandler(false)),
  saved: requirePermission(BOORU_LOOKUP)(handleSaved),
  block: requirePermission(BOORU_MODIFY_BLOCKED_TAGS)(handleBlock),
  allow: requirePermission(BOORU_MODIFY_BLOCKED_TAGS)(handleAllow),
  save: requirePermission(BOORU_SAVE_PICTURE)(handleSave)
};

const booruLookup =
  requirePrefix('!qt')(splitCommands(handlers, handleLookup));

module.exports = booruLookup;
<|endoftext|>


<commit_before>/**
 * Tiny library for interacting with Circonus' API v2
 *
 * Exported methods
 *  setup:  inital setup function to give the API your auth token an app name
 *  get, post, put, delete: docs for each method below, are proxies for the
 *                          various methods for REST calls
 *
 * Notes:
 *  callback functions take 3 args (code, error, body)
 *    code:   HTTP Response code, if null a non HTTP error occurred
 *    error:  Error message from API, null on 200 responses
 *    body:   Response body, i.e. the thing you probably want
 */

'use strict';

/* eslint-disable */

var qs = require('querystring');
var ProxyAgent = require('https-proxy-agent');

var singleton = null;
var api = null;

// extract proxy setting from environment for a specific protocol
function getProtocolProxyURL(urlProtocol) {
    var proxyServer = null;

    if (urlProtocol === 'http:') {
        if ({}.hasOwnProperty.call(process.env, 'http_proxy')) {
            proxyServer = process.env.http_proxy;
        } else if ({}.hasOwnProperty.call(process.env, 'HTTP_PROXY')) {
            proxyServer = process.env.HTTP_PROXY;
        }
    } else if (urlProtocol === 'https:') {
        if ({}.hasOwnProperty.call(process.env, 'https_proxy')) {
            proxyServer = process.env.https_proxy;
        } else if ({}.hasOwnProperty.call(process.env, 'HTTPS_PROXY')) {
            proxyServer = process.env.HTTPS_PROXY;
        }
    }
    if (proxyServer !== null && proxyServer !== '') {
        if (!(/^http[s]?:\/\//).test(proxyServer)) {
            proxyServer = 'http://' + proxyServer;
        }
    }

    return proxyServer;
}

api = function(token, app, options) {
    var protocol = null;

    options = options || {};
    this.authtoken = token;
    this.appname = app;
    protocol = options.protocol;
    if (typeof protocol === 'string') {
        protocol = (protocol === 'http') ? require('http') : undefined;
    }
    this.protocol = protocol || require('https');
    this.apihost = options.host || 'api.circonus.com';
    this.apiport = options.port || 443;
    this.apipath = options.path || '/v2/';
    this.verbose = options.verbose;

    this.proxyServer = getProtocolProxyURL(this.protocol.globalAgent.protocol);
};

/**
 * GET:
 *
 *  endpoint: (/check_bundle, /check/1, etc.)
 *  data:     object which will be converted to a query string
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.get = function(endpoint, data, callback) {
    var options = this.get_request_options('GET', endpoint, data);
    this.do_request(options, callback);
};

/**
 * POST:
 *
 *  endpoint: specify an object collection (/check_bundle, /graph, etc.)
 *  data:     object which will be stringified to JSON and written to the server
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.post = function(endpoint, data, callback) {
    var options = this.get_request_options('POST', endpoint, data);
    this.do_request(options, callback);
};

/**
 * PUT:
 *
 *  endpoint: specify an exact object (/check_bundle/1, /template/2, etc.)
 *  data:     object which will be stringified to JSON and written to the server
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.put = function(endpoint, data, callback) {
    var options = this.get_request_options('PUT', endpoint, data);
    this.do_request(options, callback);
};

/**
 * DELETE:
 *
 *  endpoint: specify an exact object (/check_bundle/1, /rule_set/1_foo, etc.)
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.delete = function(endpoint, callback) {
    var options = this.get_request_options('DELETE', endpoint);
    this.do_request(options, callback);
};

/**
 * This is called from the various exported functions to actually perform
 * the request.  Will retry up to 5 times in the event we get a connection
 * reset error.
 */
api.prototype.do_request = function(options, callback) {
    var self = this;

    if (self.verbose) {
        console.error(options.method + ' REQUEST:');
    }

    var req = this.protocol.request(options, function(res) {
        var body = '';

        res.on('data', function(chunk) {
            body += chunk;
        });

        res.on('end', function() {
            var err_msg = null;

            if (self.verbose) {
                console.error('RESPONSE ' + res.statusCode + ':');
                console.error(body);
            }

      // If this isn't a 200 level, extract the message from the body
            if (res.statusCode < 200 || res.statusCode > 299) {
                try {
                    err_msg = JSON.parse(body).message;
                } catch (err) {
                    err_msg = 'An error occurred, but the body could not be parsed: ' + err;
                }
            }
            var parsed = null;
            try { if (body) parsed = JSON.parse(body); } catch (unused) {}
            callback(res.statusCode, err_msg, parsed, body);
        });
    });

    req.on('error', function(e) {
        if (e.code === 'ECONNRESET' && options.circapi.retry < 5) {
            options.circapi.retry += 1;
            // sleep 1 second and try again, probably hit the rate limit
            setTimeout(function() {
                self.do_request(options, callback);
            }, 1000 * options.circapi.retry);
        } else {
            callback(null, e.message, null);
        }
    });

    if (options.method.toUpperCase() === 'POST' || options.method.toUpperCase() === 'PUT') {
        var stringified = JSON.stringify(options.circapi.data);
        req.write(stringified);
        if (self.verbose) {
            console.error(stringified);
        }

    }
    req.end();
};

/**
 * Hands back an options object suitable to use with the HTTPS class
 */
api.prototype.get_request_options = function(method, endpoint, data) {
    var options = {
        host: this.apihost,
        port: this.apiport,
        path: this.apipath,
        method: method.toUpperCase(),
        agent: false,
        headers: {
            'X-Circonus-Auth-Token': this.authtoken,
            'X-Circonus-App-Name': this.appname,
            'Accept': 'application/json'
        },
        circapi: {
            retry: 0,
            data: null
        }
    };

    if (this.proxyServer !== null) {
        options.agent = new ProxyAgent(this.proxyServer);
    }

    options.circapi.data = data;
    if (options.method === 'GET' && data !== null && Object.keys(data).length !== 0) {
        options.path += '?' + qs.stringify(data);
    }
    if (options.method === 'POST' || options.method === 'PUT' && data) {
        options.headers['Content-Length'] = JSON.stringify(data).length;
    }

    if (endpoint.match(/^\//)) {
        endpoint = endpoint.substring(1);
    }
    options.path += endpoint;

    return options;
};

exports.API = api;

/* support legacy API */
exports.setup = function(token, app, options) {
    singleton = new api(token, app, options);
};
exports.get = function(endpoint, data, callback) {
    singleton.get(endpoint, data, callback);
};
exports.post = function(endpoint, data, callback) {
    singleton.post(endpoint, data, callback);
};
exports.put = function(endpoint, data, callback) {
    singleton.put(endpoint, data, callback);
};
exports.delete = function(endpoint, callback) {
    singleton.delete(endpoint, callback);
};
<commit_msg>add: stepped backoff for 429/500 errors, gzip/deflate response support<commit_after>/**
 * Tiny library for interacting with Circonus' API v2
 *
 * Exported methods
 *  setup:  inital setup function to give the API your auth token an app name
 *  get, post, put, delete: docs for each method below, are proxies for the
 *                          various methods for REST calls
 *
 * Notes:
 *  callback functions take 3 args (code, error, body)
 *    code:   HTTP Response code, if null a non HTTP error occurred
 *    error:  Error message from API, null on 200 responses
 *    body:   Response body, i.e. the thing you probably want
 */

'use strict';

/* eslint-disable */

var qs = require('querystring');
var ProxyAgent = require('https-proxy-agent');
var zlib = require('zlib');

var singleton = null;
var api = null;

// extract proxy setting from environment for a specific protocol
function getProtocolProxyURL(urlProtocol) {
    var proxyServer = null;

    if (urlProtocol === 'http:') {
        if ({}.hasOwnProperty.call(process.env, 'http_proxy')) {
            proxyServer = process.env.http_proxy;
        } else if ({}.hasOwnProperty.call(process.env, 'HTTP_PROXY')) {
            proxyServer = process.env.HTTP_PROXY;
        }
    } else if (urlProtocol === 'https:') {
        if ({}.hasOwnProperty.call(process.env, 'https_proxy')) {
            proxyServer = process.env.https_proxy;
        } else if ({}.hasOwnProperty.call(process.env, 'HTTPS_PROXY')) {
            proxyServer = process.env.HTTPS_PROXY;
        }
    }
    if (proxyServer !== null && proxyServer !== '') {
        if (!(/^http[s]?:\/\//).test(proxyServer)) {
            proxyServer = 'http://' + proxyServer;
        }
    }

    return proxyServer;
}

api = function(token, app, options) {
    var protocol = null;

    options = options || {};
    this.authtoken = token;
    this.appname = app;
    protocol = options.protocol;
    if (typeof protocol === 'string') {
        protocol = (protocol === 'http') ? require('http') : undefined;
    }
    this.protocol = protocol || require('https');
    this.apihost = options.host || 'api.circonus.com';
    this.apiport = options.port || 443;
    this.apipath = options.path || '/v2/';
    this.verbose = options.verbose;

    this.proxyServer = getProtocolProxyURL(this.protocol.globalAgent.protocol);
};

/**
 * GET:
 *
 *  endpoint: (/check_bundle, /check/1, etc.)
 *  data:     object which will be converted to a query string
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.get = function(endpoint, data, callback) {
    var options = this.get_request_options('GET', endpoint, data);
    this.do_request(options, callback);
};

/**
 * POST:
 *
 *  endpoint: specify an object collection (/check_bundle, /graph, etc.)
 *  data:     object which will be stringified to JSON and written to the server
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.post = function(endpoint, data, callback) {
    var options = this.get_request_options('POST', endpoint, data);
    this.do_request(options, callback);
};

/**
 * PUT:
 *
 *  endpoint: specify an exact object (/check_bundle/1, /template/2, etc.)
 *  data:     object which will be stringified to JSON and written to the server
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.put = function(endpoint, data, callback) {
    var options = this.get_request_options('PUT', endpoint, data);
    this.do_request(options, callback);
};

/**
 * DELETE:
 *
 *  endpoint: specify an exact object (/check_bundle/1, /rule_set/1_foo, etc.)
 *  callback: what do we call when the response from the server is complete,
 *            arguments are callback(code, error, body)
 */
api.prototype.delete = function(endpoint, callback) {
    var options = this.get_request_options('DELETE', endpoint);
    this.do_request(options, callback);
};

/**
 * This is called from the various exported functions to actually perform
 * the request.  Will retry up to 5 times in the event we get a connection
 * reset error.
 */
api.prototype.do_request = function(options, callback) {
    var self = this;

    if (self.verbose) {
        console.error(options.method + ' REQUEST:');
    }

    var req = this.protocol.request(options, function(res) {
        var data = [];

        res.on('data', function(chunk) {
            data.push(chunk);
        });

        res.on('end', function() {
            // try again... on rate limit or internal (server-side, hopefully recoverable) error
            if (res.statusCode === 429 || res.statusCode === 500) {
                if (options.circapi.retry < options.circapi.retry_backoff.length) {
                    setTimeout(function() {
                        self.do_request(options, callback);
                    }, options.circapi.retry_backoff[options.circapi.retry]);
                    options.circapi.retry += 1;
                } else {
                    callback(res.statusCode, new Error('Giving up after '+options.circapi.retry+' attempts'), null, null);
                    return;
                }
            }

            // success, no content
            if (res.statusCode === 204) {
                callback(res.statusCode, null, null, null);
                return;
            }

            var buffer = Buffer.concat(data);
            var encoding = res.headers['content-encoding'];
            var err_msg = null;
            var body = null;

            if (data.length === 0) {
                err_msg = new Error('No data returned, 0 length body.');
            } else if (encoding === 'gzip') {
                try {
                    body = zlib.gunzipSync(buffer).toString();
                } catch (gzipErr) {
                    err_msg = gzipErr;
                }
            } else if (encoding === 'deflate') {
                try {
                    body = zlib.deflateSync(buffer).toString();
                } catch (deflateErr) {
                    err_msg = deflateErr;
                }
            } else {
                body = buffer.toString();
            }

            if (self.verbose) {
                console.error('RESPONSE ' + res.statusCode + ':');
                console.error(body);
            }

            if (err_msg !== null) {
                callback(res.statusCode, err_msg, null, body);
                return;
            }

            // If this isn't a 200 level, extract the message from the body
            if (res.statusCode < 200 || res.statusCode > 299) {
                try {
                    err_msg = JSON.parse(body).message;
                } catch (err) {
                    err_msg = 'An error occurred, but the body could not be parsed: ' + err;
                }
                callback(res.statusCode, err_msg, null, body);
                return;
            }

            var parsed = null;

            try {
                if (body) {
                    parsed = JSON.parse(body);
                }
            } catch (parseErr) {
                err_msg = new Error('Error parsing body');
                err_msg.detail = parseErr;
                err_msg.body = body;
            }

            callback(res.statusCode, err_msg, parsed, body);
        });
    });

    req.on('error', function(e) {
        if (e.code === 'ECONNRESET' && options.circapi.retry < options.circapi.retry_backoff.length) {
            // sleep and try again, hopefully a recoverable error
            setTimeout(function() {
                self.do_request(options, callback);
            }, options.circapi.retry_backoff[options.circapi.retry]);
            options.circapi.retry += 1;
        } else {
            callback(null, e.message, null);
        }
    });

    if (options.method.toUpperCase() === 'POST' || options.method.toUpperCase() === 'PUT') {
        var stringified = JSON.stringify(options.circapi.data);
        req.write(stringified);
        if (self.verbose) {
            console.error(stringified);
        }

    }
    req.end();
};

/**
 * Hands back an options object suitable to use with the HTTPS class
 */
api.prototype.get_request_options = function(method, endpoint, data) {
    var options = {
        host: this.apihost,
        port: this.apiport,
        path: this.apipath,
        method: method.toUpperCase(),
        agent: false,
        headers: {
            'X-Circonus-Auth-Token': this.authtoken,
            'X-Circonus-App-Name': this.appname,
            'Accept': 'application/json',
            'Accept-Encoding': 'gzip,deflate'
        },
        circapi: {
            retry: 0,
            retry_backoff: [
                null,       // 0 first attempt
                1 * 1000,   // 1, wait 1 second and try again
                2 * 1000,   // 2, wait 2 seconds and try again
                4 * 1000,   // 3, wait 4 seconds and try again
                8 * 1000,   // 4, wait 8 seconds and try again
                16 * 1000,  // 5, wait 16 seconds and try again
                32 * 1000   // 6, wait 32 seconds and retry again then give up if it fails
            ],
            data: null
        }
    };

    if (this.proxyServer !== null) {
        options.agent = new ProxyAgent(this.proxyServer);
    }

    if ((/^v[46]/).test(process.version)) {

        // currently 2016-10-27T16:01:42Z, these settings seem to be
        // necessary to prevent http/https requests from intermittently
        // emitting an end event prior to all content being received
        // when communicating with the API. at least until the actual
        // root cause can be determined.

        if (!{}.hasOwnProperty.call(options, 'agent') || options.agent === false) {
            options.agent = new this.protocol.Agent();
        }

        options.agent.keepAlive = false;
        options.agent.keepAliveMsecs = 0;
        options.agent.maxSockets = 1;
        options.agent.maxFreeSockets = 1;
        options.agent.maxCachedSessions = 0;
    }

    options.circapi.data = data;
    if (options.method === 'GET' && data !== null && Object.keys(data).length !== 0) {
        options.path += '?' + qs.stringify(data);
    }
    if (options.method === 'POST' || options.method === 'PUT' && data) {
        options.headers['Content-Length'] = JSON.stringify(data).length;
    }

    if (endpoint.match(/^\//)) {
        endpoint = endpoint.substring(1);
    }
    options.path += endpoint;

    return options;
};

exports.API = api;

/* support legacy API */
exports.setup = function(token, app, options) {
    singleton = new api(token, app, options);
};
exports.get = function(endpoint, data, callback) {
    singleton.get(endpoint, data, callback);
};
exports.post = function(endpoint, data, callback) {
    singleton.post(endpoint, data, callback);
};
exports.put = function(endpoint, data, callback) {
    singleton.put(endpoint, data, callback);
};
exports.delete = function(endpoint, callback) {
    singleton.delete(endpoint, callback);
};
<|endoftext|>


<commit_before>var reportBuilder = new function() {

    // public

    // this is sorting of simple, non-expanded data
    this.sortData = function(data, rowIndex, direction)
    {
        var sortedList = [];
        var sortedData = [];

        var grandTotal = {};

        _.each(data, function(e) {
            if (e.type == 'grandtotal') {
                grandTotal = e;
                return;
            }

            var val = e.values[rowIndex].sortValue;
            var sortIndex = _.sortedIndex(sortedList, val);

            sortedList.splice(sortIndex, 0, val);
            sortedData.splice(sortIndex, 0, e);
        });

        if (direction == "up")
            sortedData = sortedData.reverse();

        if (grandTotal)
            sortedData.push(grandTotal);

        return sortedData;
    };

    // this can sort expanded data, making sure to sort each independently
    this.sortExpandedData = function(data, dimensionsY, rowIndex, direction, expandedCells)
    {
        // transforms data into a tree, were the parent is the sub total of the leaves

        var treeLastData = [];

        var currentLevelRows = [];
        var currentLevelIndex = 0;

        var state = "";
        var tempState = []; // stack of unfinished children

        _.each(data, function(row) {

	        var e = cloneObj(row);

            var dim = dimensionsY[currentLevelIndex];

            function findValueInExpanded(expandedValue) {
                var values = _.map(expandedValue.split('|'), function (e) { return e.split(":")[1]; });
                return _.contains(values, e.values[currentLevelIndex].displayValue);
            };

            if (expandedCells[dim] && _.any(expandedCells[dim], findValueInExpanded)) // start of sub total
            {
                if (state != "start") {
                    _.each(currentLevelRows, function(e2) {
                        treeLastData.push(e2);
                    });
                } else {
                    tempState.push(currentLevelRows);
                }

                currentLevelRows = [];
                currentLevelRows.push(e);
                currentLevelIndex++;

                state = "start";
            }
            else if (e.type == 'subtotal') // this ends a sub total
            {
                e.child = currentLevelRows;

                if (tempState.length > 0)
                    currentLevelRows = tempState.pop();
                else
                    currentLevelRows = [];

                currentLevelRows.push(e);
                currentLevelIndex--;

                state = "end";
            }
            else if (e.type == 'grandtotal') // mother of all
            {
                _.each(currentLevelRows, function(e2) {
                    treeLastData.push(e2);
                });

                e.child = treeLastData;
                treeLastData = e;
            }
            else
                currentLevelRows.push(e);
        });

        function sortValues(el, level)
	    {
            var sortedList = [];
            var sortedData = [];

            _.each(el.child, function(e) {
                var val = e.values[rowIndex].sortValue;
                var sortIndex = _.sortedIndex(sortedList, val);

                sortedList.splice(sortIndex, 0, val);
                sortedData.splice(sortIndex, 0, e);
            });

            if (direction == "up")
                sortedData = sortedData.reverse();

            if (level >= 0) {
                // Fix expand to be on top
                _.each(sortedData, function(e, i) {
                    if (e.values[level].displayValue != '') {
                        if (i != 0) {
                            sortedData[0].values[level] = cloneObj(e.values[level]);
                            if (sortedData[0].child != null)
                                sortedData[0].child[0].values[level] = cloneObj(e.values[level]);
                            e.values[level].displayValue = '';
                        }
                        return false; // break
                    }
		        });
            }

            el.child = sortedData;
        }

        // sort each group of leaves
        // move the dim name up
        // flatten

        var level = 0;

        function traverseTreeSort(node)
	    {
            ++level;

            if (node.child != null) {
                _.each(node.child, function(child) {
                    traverseTreeSort(child);
                });
                sortValues(node, level - 2);
            }
            --level;
        }

        traverseTreeSort(treeLastData);

        var flattenedTree = [];

        function traverseTreeFlatten(node)
	    {
            if (node.type != "grandtotal" && node.type != "subtotal")
                flattenedTree.push(node);

            if (node.child != null)
                _.each(node.child, function(child) {
                    traverseTreeFlatten(child);
                });

            if (node.type == "grandtotal" || node.type == "subtotal")
                flattenedTree.push(node);
        }

        traverseTreeFlatten(treeLastData);

        return flattenedTree;
    };

    this.generateDataWithSubTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: true, grandTotals: false };
        return data;
    };

    this.generateDataWithGrandTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: false, grandTotals: true };
        return data;
    };

    this.generateDataWithSubAndGrandTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: true, grandTotals: true };
        return data;
    };

    this.generateData = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = { yAxis: [], xAxis: { keyfigures: [], restrictions: [] }, options: {} };

        _.each(yAxis, function(e, i) {
            data.yAxis.push({ id: e.id, left: e.left, right: e.right, restrictions: e.restrictions });
        });

        _.each(xAxis, function(e, i) {
            data.xAxis.keyfigures.push(e.id);
        });

        _.each(xAxisRestrictions, function(e, i) {
            data.xAxis.restrictions.push(e);
        });

        return data;
    };

    this.localizedHeaders = function(yAxis, fullXAxis, reportState)
    {
        _.each(yAxis, function(e) {
            e.name = reportState.translateDimKF[e.id];
        });

        _.each(fullXAxis, function(e) {
            if (e.name == 'LY')
                e.name = reportState.translateDimKF[e.id] + " " + reportState.translate["LastYear"];
            else if (e.name == 'INDEX')
                e.name = reportState.translateDimKF[e.id] + " " + reportState.translate["Index"];
            else
                e.name = reportState.translateDimKF[e.id];
        });
    };

    // this is a way to merge multiple data sets into one, a use case
    // would be to get data for this year and last year, and to
    // combine these, maybe adding an index (this year / last year).
    //
    // allYaxisValues: a list of dimension values
    // combinedXAxis: a list of keyfigures with the following syntax {id: '', name: '', [dataref: array, dataindex: indexinarray], [datacalculate: function(row) { }] }
    //
    // returns a list of combined rows
    this.mergeData = function(allYaxisValues, combinedXAxis)
    {
        var calculatedResultSet = [];

        _.each(allYaxisValues, function(row, i) {

            var yAxisColumns = row.length;

            _.each(combinedXAxis, function(e) {
                if (e.datacalculate != null) {
                    var val = e.datacalculate(row, i);
                    row.push({ sortValue: val, displayValue: val });
                } else
                    row.push(e.dataref[i].values[yAxisColumns + e.dataindex]);
            });

            calculatedResultSet.push({ type: 'row', values: row });
        });

        return calculatedResultSet;
    };

    // extracts dimension values
    this.getYAxisValues = function(data, yAxis)
    {
        return _.map(data, function(e) { return e.values.slice(0, yAxis.length); });
    };

    // extracts unique dimension values from multiple data results
    this.getAllYAxisValues = function(data, yAxis)
    {
        var allYaxisValues = _.map(data[0], function(e) { return e.values.slice(0, yAxis.length); });

        _.each(data.slice(1), function(table, i) {
            var tableYAxisValues = _.map(table, function(e) { return e.values.slice(0, yAxis.length); });

            _.each(tableYAxisValues, function(e) {
                if (!_.find(allYaxisValues, function(e2) { return _.isEqual(e, e2); }))
                    allYaxisValues.push(e);
            });
        });

        // FIXME: maybe sort this

        return allYaxisValues;
    };

    // insert column at index in data
    // data must be an array of values
    this.spliceColumn = function(data, index, column)
    {
        _.each(data.rows, function(row, i) {
            row.values.splice(index, 0, column[i]);
        });
    };

    // combines multiple server results into one
    //
    // data: a list of data from the server [[{values:[], type: ''}, ...], ...]
    // allXaxis: a list of keyfigure specifications [[{ id: '', name: '' }, ...], ...]
    //
    // FIXME: could work better if the number of yAxis values are not the same
    this.combineData = function(data, allXaxis, yAxis)
    {
	    var resultDimensions = {};
	    var result = [];

        _.each(data, function(jsonData, jsonDataI) {
            var noKeyfigures = allXaxis[jsonDataI].length;

            _.each(jsonData, function(e, i) {

		        var dimensions = JSON.stringify(e.values.slice(0, yAxis.length));

                if (dimensions in resultDimensions)
		        {
		            // append values to existing row
		            _.each(e.values, function(val, j) {
                        if (j >= yAxis.length)
			                result[resultDimensions[dimensions]].values.push(cloneObj(val));
		            });
		        }
		        else
		        {
		            result.push(cloneObj(jsonData[i]));
                    resultDimensions[dimensions] = result.length - 1;
		        }
	        });
        });

        // FIXME: maybe sort this by dimension

	    return result;
    };
};
<commit_msg>Fixes to sorting<commit_after>var reportBuilder = new function() {

    // public

    // this is sorting of simple, non-expanded data
    this.sortData = function(data, rowIndex, direction)
    {
        var sortedList = [];
        var sortedData = [];

        var grandTotal = {};

        _.each(data, function(e) {
            if (e.type == 'grandtotal') {
                grandTotal = e;
                return;
            }

            var val = e.values[rowIndex].sortValue;
            var sortIndex = _.sortedIndex(sortedList, val);

            sortedList.splice(sortIndex, 0, val);
            sortedData.splice(sortIndex, 0, e);
        });

        if (direction == "up")
            sortedData = sortedData.reverse();

        if (grandTotal)
            sortedData.push(grandTotal);

        return sortedData;
    };

    // this can sort expanded data, making sure to sort each independently
    this.sortExpandedData = function(data, dimensionsY, rowIndex, direction, expandedCells)
    {
        // transforms data into a tree, were the parent is the sub total of the leaves

        var treeLastData = [];

        var currentLevelRows = [];
        var currentLevelIndex = 0;

        var state = "";
        var tempState = []; // stack of unfinished children

        _.each(data, function(row) {

	        var e = cloneObj(row);

            var dim = dimensionsY[currentLevelIndex];

            function findValueInExpanded(expandedValue) {
                var values = _.map(expandedValue.split('|'), function (e) { return e.substring(e.indexOf(":") + 1); });
                return values[currentLevelIndex] == e.values[currentLevelIndex].displayValue;
            };

            while (true) {
                var startOfSubTotal = expandedCells[dim] && _.any(expandedCells[dim], findValueInExpanded);
                if (startOfSubTotal) {
                    tempState.push(currentLevelRows);
                    currentLevelRows = [];
                    currentLevelIndex++;
                } else
                    break;
            }

            if (e.type == 'subtotal') // this ends a sub total
            {
                e.child = currentLevelRows;
                currentLevelRows = tempState.pop();

                currentLevelRows.push(e);
                currentLevelIndex--;
            }
            else if (e.type == 'grandtotal') // mother of all
            {
                e.child = currentLevelRows;
                treeLastData = e;
            }
            else
                currentLevelRows.push(e);
        });

        function sortValues(el, level)
	    {
            var sortedList = [];
            var sortedData = [];

            _.each(el.child, function(e) {
                var val = e.values[rowIndex].sortValue;
                var sortIndex = _.sortedIndex(sortedList, val);

                sortedList.splice(sortIndex, 0, val);
                sortedData.splice(sortIndex, 0, e);
            });

            if (direction == "up")
                sortedData = sortedData.reverse();

            el.child = sortedData;
        }

        // sort each group of leaves
        // move the dim name up
        // flatten

        var level = 0;

        function traverseTreeSort(node)
	    {
            ++level;

            if (node.child != null) {
                _.each(node.child, function(child) {
                    traverseTreeSort(child);
                });
                sortValues(node, level - 2);
            }
            --level;
        }

        traverseTreeSort(treeLastData);

        var flattenedTree = [];

        function traverseTreeFlatten(node)
	    {
            if (node.type != "grandtotal" && node.type != "subtotal")
                flattenedTree.push(node);

            if (node.child != null)
                _.each(node.child, function(child) {
                    traverseTreeFlatten(child);
                });

            if (node.type == "grandtotal" || node.type == "subtotal")
                flattenedTree.push(node);
        }

        traverseTreeFlatten(treeLastData);

        return flattenedTree;
    };

    this.generateDataWithSubTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: true, grandTotals: false };
        return data;
    };

    this.generateDataWithGrandTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: false, grandTotals: true };
        return data;
    };

    this.generateDataWithSubAndGrandTotals = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = this.generateData(yAxis, xAxis, xAxisRestrictions);
        data.options = { subTotals: true, grandTotals: true };
        return data;
    };

    this.generateData = function(yAxis, xAxis, xAxisRestrictions)
    {
        var data = { yAxis: [], xAxis: { keyfigures: [], restrictions: [] }, options: {} };

        _.each(yAxis, function(e, i) {
            data.yAxis.push({ id: e.id, left: e.left, right: e.right, restrictions: e.restrictions });
        });

        _.each(xAxis, function(e, i) {
            data.xAxis.keyfigures.push(e.id);
        });

        _.each(xAxisRestrictions, function(e, i) {
            data.xAxis.restrictions.push(e);
        });

        return data;
    };

    this.localizedHeaders = function(yAxis, fullXAxis, reportState)
    {
        _.each(yAxis, function(e) {
            e.name = reportState.translateDimKF[e.id];
        });

        _.each(fullXAxis, function(e) {
            if (e.name == 'LY')
                e.name = reportState.translateDimKF[e.id] + " " + reportState.translate["LastYear"];
            else if (e.name == 'INDEX')
                e.name = reportState.translateDimKF[e.id] + " " + reportState.translate["Index"];
            else
                e.name = reportState.translateDimKF[e.id];
        });
    };

    // this is a way to merge multiple data sets into one, a use case
    // would be to get data for this year and last year, and to
    // combine these, maybe adding an index (this year / last year).
    //
    // allYaxisValues: a list of dimension values
    // combinedXAxis: a list of keyfigures with the following syntax {id: '', name: '', [dataref: array, dataindex: indexinarray], [datacalculate: function(row) { }] }
    //
    // returns a list of combined rows
    this.mergeData = function(allYaxisValues, combinedXAxis)
    {
        var calculatedResultSet = [];

        _.each(allYaxisValues, function(row, i) {

            var yAxisColumns = row.length;

            _.each(combinedXAxis, function(e) {
                if (e.datacalculate != null) {
                    var val = e.datacalculate(row, i);
                    row.push({ sortValue: val, displayValue: val });
                } else
                    row.push(e.dataref[i].values[yAxisColumns + e.dataindex]);
            });

            calculatedResultSet.push({ type: 'row', values: row });
        });

        return calculatedResultSet;
    };

    // extracts dimension values
    this.getYAxisValues = function(data, yAxis)
    {
        return _.map(data, function(e) { return e.values.slice(0, yAxis.length); });
    };

    // extracts unique dimension values from multiple data results
    this.getAllYAxisValues = function(data, yAxis)
    {
        var allYaxisValues = _.map(data[0], function(e) { return e.values.slice(0, yAxis.length); });

        _.each(data.slice(1), function(table, i) {
            var tableYAxisValues = _.map(table, function(e) { return e.values.slice(0, yAxis.length); });

            _.each(tableYAxisValues, function(e) {
                if (!_.find(allYaxisValues, function(e2) { return _.isEqual(e, e2); }))
                    allYaxisValues.push(e);
            });
        });

        // FIXME: maybe sort this

        return allYaxisValues;
    };

    // insert column at index in data
    // data must be an array of values
    this.spliceColumn = function(data, index, column)
    {
        _.each(data.rows, function(row, i) {
            row.values.splice(index, 0, column[i]);
        });
    };

    // combines multiple server results into one
    //
    // data: a list of data from the server [[{values:[], type: ''}, ...], ...]
    // allXaxis: a list of keyfigure specifications [[{ id: '', name: '' }, ...], ...]
    //
    // FIXME: could work better if the number of yAxis values are not the same
    this.combineData = function(data, allXaxis, yAxis)
    {
	    var resultDimensions = {};
	    var result = [];

        _.each(data, function(jsonData, jsonDataI) {
            var noKeyfigures = allXaxis[jsonDataI].length;

            _.each(jsonData, function(e, i) {

		        var dimensions = JSON.stringify(e.values.slice(0, yAxis.length));

                if (dimensions in resultDimensions)
		        {
		            // append values to existing row
		            _.each(e.values, function(val, j) {
                        if (j >= yAxis.length)
			                result[resultDimensions[dimensions]].values.push(cloneObj(val));
		            });
		        }
		        else
		        {
		            result.push(cloneObj(jsonData[i]));
                    resultDimensions[dimensions] = result.length - 1;
		        }
	        });
        });

        // FIXME: maybe sort this by dimension

	    return result;
    };
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import logo from './grappa.jpg';
import { Link } from 'react-router-dom';

class Contract extends Component {
    constructor(props) {
        super(props);
        this.handleContractChange = this.handleContractChange.bind(this);
        this.state = {
           completionEta: "",
           supervision: "",
           misc: ""
        }
    }

    componentDidMount() {
        document.title = "Grappa: Contract page";
    }

    handleContractChange(event) {
      console.log("handler called "+event.target.name + " " + event.target.value);
      this.setState({
        [event.target.name]: event.target.value
      });
    }

    sendForm(event) {
      console.log(event);
      console.log("Nappia painettiin.");
    }

    render() {
      return (

Thesis Contract

Gradusopimus tehdään gradunohjauksen alkaessa

          Sopimusta voidaan muuttaa osapuolten yhteisestä päätöksestä.

          <form onSubmit={this.handlePost}>
          <textarea ref="input" placeholder="arvioitu gradun valmistumisen aikataulu" type="text" name="completionEta" value={this.state.completionEta} onChange={this.handleContractChange} />

          <textarea ref="input" placeholder="ohjauksen määrän ja laadun yksityiskohdat" type="text" name="supervision" value={this.state.supervision} onChange={this.handleContractChange}/>

          <textarea ref="input" placeholder="muut sovittavat asiat" type="text" name="misc" value={this.state.misc} onChange={this.handleContractChange} />

          <input type="submit" value="send" onClick={(event) => this.sendForm(event)}/>
        </form>

          <Link to="/"> Go back to HomePage :P </Link>

      );
    }
  }

  export default Contract;
<commit_msg>Contract class update to bind sendForm function to props<commit_after>import React, { Component } from 'react';
import logo from './grappa.jpg';
import { Link } from 'react-router-dom';

class Contract extends Component {
    constructor(props) {
        super(props);
        this.handleContractChange = this.handleContractChange.bind(this);
        this.sendForm = this.sendForm.bind(this);
        this.state = {
           completionEta: "",
           supervision: "",
           misc: ""
        }
    }

    componentDidMount() {
        document.title = "Grappa: Contract page";
    }

    handleContractChange(event) {
      console.log("handler called "+event.target.name + " " + event.target.value);
      this.setState({
        [event.target.name]: event.target.value
      });
    }

    sendForm(event) {
      console.log(event);
      console.log("Nappia painettiin.");
    }

    render() {
      return (

Thesis Contract

Gradusopimus tehdään gradunohjauksen alkaessa

          Sopimusta voidaan muuttaa osapuolten yhteisestä päätöksestä.

          <form onSubmit={this.handlePost}>
          <textarea ref="input" placeholder="arvioitu gradun valmistumisen aikataulu" type="text" name="completionEta" value={this.state.completionEta} onChange={this.handleContractChange} />

          <textarea ref="input" placeholder="ohjauksen määrän ja laadun yksityiskohdat" type="text" name="supervision" value={this.state.supervision} onChange={this.handleContractChange}/>

          <textarea ref="input" placeholder="muut sovittavat asiat" type="text" name="misc" value={this.state.misc} onChange={this.handleContractChange} />

          <input type="submit" value="send" onClick={this.sendForm}/>
        </form>

          <Link to="/"> Go back to HomePage :P </Link>

      );
    }
  }

  export default Contract;
<|endoftext|>


<commit_before>var React = require('react'),
	moment = require('moment')
;

var DOM = React.DOM;
var DateTimePickerDays = React.createClass({

	render: function() {
		var footer = this.renderFooter(),
			date = this.props.viewDate,
			locale = date.localeData(),
			tableChildren
		;

		tableChildren = [
			DOM.thead({ key: 'th'}, [
				DOM.tr({ key: 'h'},[
					DOM.th({ key: 'p', className: 'rdtPrev' }, DOM.button({onClick: this.props.subtractTime(1, 'months'), type: 'button' }, '‹')),
					DOM.th({ key: 's', className: 'rdtSwitch', onClick: this.props.showView('months'), colSpan: 5, 'data-value': this.props.viewDate.month() }, locale.months( date ) + ' ' + date.year() ),
					DOM.th({ key: 'n', className: 'rdtNext' }, DOM.button({onClick: this.props.addTime(1, 'months'), type: 'button' }, '›'))
				]),
				DOM.tr({ key: 'd'}, this.getDaysOfWeek( locale ).map( function( day ){ return DOM.th({ key: day, className: 'dow'}, day ); }) )
			]),
			DOM.tbody({key: 'tb'}, this.renderDays())
		];

		if( footer )
			tableChildren.push( footer );

		return DOM.div({ className: 'rdtDays' },
			DOM.table({}, tableChildren )
		);
	},

	/**
	 * Get a list of the days of the week
	 * depending on the current locale
	 * @return {array} A list with the shortname of the days
	 */
	getDaysOfWeek: function( locale ){
		var days = locale._weekdaysMin,
			first = locale.firstDayOfWeek(),
			dow = [],
			i = 0
		;

		days.forEach( function( day ){
			dow[ (7 + (i++) - first) % 7 ] = day;
		});

		return dow;
	},

	renderDays: function() {
		var date = this.props.viewDate,
			selected = this.props.selectedDate && this.props.selectedDate.clone(),
			prevMonth = date.clone().subtract( 1, 'months' ),
			currentYear = date.year(),
			currentMonth = date.month(),
			weeks = [],
			days = [],
			renderer = this.props.renderDay || this.renderDay,
			isValid = this.props.isValidDate || this.isValidDate,
			classes, disabled, dayProps, currentDate
		;

		// Go to the last week of the previous month
		prevMonth.date( prevMonth.daysInMonth() ).startOf('week');
		var lastDay = prevMonth.clone().add(42, 'd');

		while( prevMonth.isBefore( lastDay ) ){
			classes = 'rdtDay';
			currentDate = prevMonth.clone();

			if( prevMonth.year() < currentYear || prevMonth.month() < currentMonth )
				classes += ' rdtOld';
			else if( prevMonth.year() > currentYear || prevMonth.month() > currentMonth )
				classes += ' rdtNew';

			if( selected && prevMonth.isSame( {y: selected.year(), M: selected.month(), d: selected.date()} ) )
				classes += ' rdtActive';

			if (prevMonth.isSame(moment(), 'day') )
				classes += ' rdtToday';

			disabled = !isValid( currentDate, selected );
			if( disabled )
				classes += ' rdtDisabled';

			dayProps = {
				key: prevMonth.format('M_D'),
				'data-value': prevMonth.date(),
				className: classes
			};
			if( !disabled )
				dayProps.onClick = this.props.updateSelectedDate;

			days.push( renderer( dayProps, currentDate, selected ) );

			if( days.length == 7 ){
				weeks.push( DOM.tr( {key: prevMonth.format('M_D')}, days ) );
				days = [];
			}

			prevMonth.add( 1, 'd' );
		}

		return weeks;
	},

	renderDay: function( props, currentDate, selectedDate ){
		return DOM.td( props, currentDate.date() );
	},

	renderFooter: function(){
		if( !this.props.timeFormat )
			return '';

		var date = this.props.selectedDate || this.props.viewDate;
		return DOM.tfoot({ key: 'tf'},
			DOM.tr({},
				DOM.td({ onClick: this.props.showView('time'), colSpan: 7, className: 'rdtTimeToggle'}, date.format( this.props.timeFormat ))
			)
		);
	},
	isValidDate: function(){ return 1; }
});

module.exports = DateTimePickerDays;
<commit_msg>Updated Days View to allow duplicate Weekdays<commit_after>var React = require('react'),
	moment = require('moment')
;

var DOM = React.DOM;
var DateTimePickerDays = React.createClass({

	render: function() {
		var footer = this.renderFooter(),
			date = this.props.viewDate,
			locale = date.localeData(),
			tableChildren
		;

		tableChildren = [
			DOM.thead({ key: 'th'}, [
				DOM.tr({ key: 'h'},[
					DOM.th({ key: 'p', className: 'rdtPrev' }, DOM.button({onClick: this.props.subtractTime(1, 'months'), type: 'button' }, '‹')),
					DOM.th({ key: 's', className: 'rdtSwitch', onClick: this.props.showView('months'), colSpan: 5, 'data-value': this.props.viewDate.month() }, locale.months( date ) + ' ' + date.year() ),
					DOM.th({ key: 'n', className: 'rdtNext' }, DOM.button({onClick: this.props.addTime(1, 'months'), type: 'button' }, '›'))
				]),
				DOM.tr({ key: 'd'}, this.getDaysOfWeek( locale ).map( function( day, index ){ return DOM.th({ key: day + index, className: 'dow'}, day ); }) )
			]),
			DOM.tbody({key: 'tb'}, this.renderDays())
		];

		if( footer )
			tableChildren.push( footer );

		return DOM.div({ className: 'rdtDays' },
			DOM.table({}, tableChildren )
		);
	},

	/**
	 * Get a list of the days of the week
	 * depending on the current locale
	 * @return {array} A list with the shortname of the days
	 */
	getDaysOfWeek: function( locale ){
		var days = locale._weekdaysMin,
			first = locale.firstDayOfWeek(),
			dow = [],
			i = 0
		;

		days.forEach( function( day ){
			dow[ (7 + (i++) - first) % 7 ] = day;
		});

		return dow;
	},

	renderDays: function() {
		var date = this.props.viewDate,
			selected = this.props.selectedDate && this.props.selectedDate.clone(),
			prevMonth = date.clone().subtract( 1, 'months' ),
			currentYear = date.year(),
			currentMonth = date.month(),
			weeks = [],
			days = [],
			renderer = this.props.renderDay || this.renderDay,
			isValid = this.props.isValidDate || this.isValidDate,
			classes, disabled, dayProps, currentDate
		;

		// Go to the last week of the previous month
		prevMonth.date( prevMonth.daysInMonth() ).startOf('week');
		var lastDay = prevMonth.clone().add(42, 'd');

		while( prevMonth.isBefore( lastDay ) ){
			classes = 'rdtDay';
			currentDate = prevMonth.clone();

			if( prevMonth.year() < currentYear || prevMonth.month() < currentMonth )
				classes += ' rdtOld';
			else if( prevMonth.year() > currentYear || prevMonth.month() > currentMonth )
				classes += ' rdtNew';

			if( selected && prevMonth.isSame( {y: selected.year(), M: selected.month(), d: selected.date()} ) )
				classes += ' rdtActive';

			if (prevMonth.isSame(moment(), 'day') )
				classes += ' rdtToday';

			disabled = !isValid( currentDate, selected );
			if( disabled )
				classes += ' rdtDisabled';

			dayProps = {
				key: prevMonth.format('M_D'),
				'data-value': prevMonth.date(),
				className: classes
			};
			if( !disabled )
				dayProps.onClick = this.props.updateSelectedDate;

			days.push( renderer( dayProps, currentDate, selected ) );

			if( days.length == 7 ){
				weeks.push( DOM.tr( {key: prevMonth.format('M_D')}, days ) );
				days = [];
			}

			prevMonth.add( 1, 'd' );
		}

		return weeks;
	},

	renderDay: function( props, currentDate, selectedDate ){
		return DOM.td( props, currentDate.date() );
	},

	renderFooter: function(){
		if( !this.props.timeFormat )
			return '';

		var date = this.props.selectedDate || this.props.viewDate;
		return DOM.tfoot({ key: 'tf'},
			DOM.tr({},
				DOM.td({ onClick: this.props.showView('time'), colSpan: 7, className: 'rdtTimeToggle'}, date.format( this.props.timeFormat ))
			)
		);
	},
	isValidDate: function(){ return 1; }
});

module.exports = DateTimePickerDays;
<|endoftext|>