<commit_before>/**
 * hive.js
 * Copyright (C) 2013-2016 Marcel Klehr <mklehr@gmx.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Mozilla Public License version 2
 * as published by the Mozilla Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the Mozilla Public License
 * along with this program.  If not, see <https://www.mozilla.org/en-US/MPL/2.0/>.
 */
"use strict";
var koa = require('koa')
  , mount = require('koa-mount')
  , router = require('koa-router')
  , jsonBody = require('koa-json-body')
  , parseUpload = require('co-busboy')
  , WritableBuffer = require('stream-buffers').WritableStreamBuffer
  , JSONAPI = require('waterline-to-jsonapi')
  , qs = require('qs')

module.exports = setup
module.exports.consumes = ['http', 'auth', 'hooks', 'orm', 'sync', 'ot', 'importexport']

function setup(plugin, imports, register) {
  var httpApp = imports.http
    , auth = imports.auth
    , hooks = imports.hooks
    , orm = imports.orm
    , sync = imports.sync
    , ot = imports.ot
    , importexport = imports.importexport

  var api = koa()
  httpApp.use(mount('/api', api))

  var APIv1 = koa()
  api.use(mount('/v1', APIv1))

  APIv1.use(function *(next) {
    try {
      if(!this.accepts('application/vnd.api+json')) {
	return this.throw(406)
      }
      yield next;
    } catch (err) {
      this.app.emit('error', err, this)
      this.status = err.status || 500
      this.body = JSONAPI().errors(err)
    }
  })

  APIv1.use(function* (next) {
    var credentials = this.request.query.access_token
      , type = "token"
    if(!credentials) {
      credentials = this.get('Authorization')
      if(!credentials) return this.throw(401)
      var credParts = credentials.split(' ')
      type = credParts[0]
      credentials = credParts[1]
    }
    try {
      this.user = yield auth.authenticate(type, credentials)
    }catch(e) {console.log(e.stack ||e)}

    if(!this.user) {
      return this.throw(401)
    }
    yield next
  })

  APIv1.use(router(APIv1))

  function toWaterline(jsonAPIBody) {
    if(Array.isArray(jsonAPIBody.data)) return jsonAPIBody.data.map(toWaterline)
    var newItem = jsonAPIBody.data.attributes || {id: jsonAPIBody.data.id}
    for(var rel in jsonAPIBody.data.relationships) {
      var linkage = jsonAPIBody.data.relationships[rel].data
      if(Array.isArray(linkage)) {
	newItem[rel] = linkage.map(resource => resource.id)
      }else if(linkage) {
	newItem[rel] = linkage.id
      }
    }
    return newItem
  }

  hooks.on('orm:initialized', function*(models) {
    var VERBS = ['post', 'get', 'patch', 'delete', 'put']
      , jsonapi = JSONAPI(orm)

    Object.keys(orm.collections).forEach((model) => {

      // Routes for CRUD

      APIv1.post('/'+model+'s', jsonBody(), function*(next) {
	if(!(yield auth.authorize(this.user, model+':create', {body: this.request.body}))) {
	  this.throw(403)
	}
        if(!this.is('application/vnd.api+json')) {
          this.throw(415) // Unsupported media type
        }

	// Build a waterline-conformant object
	var newItem = toWaterline(this.request.body)
	  , data
	// create item
	if(orm.collections[model].override_create) {
	  data = yield orm.collections[model].override_create(newItem)
	}else {
	  data = yield orm.collections[model].create(newItem)
	}

        this.status = 201
	this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.get('/'+model+'s/:id', function*(next) {
        if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
          this.throw(403)
        }

	var data = yield orm.collections[model].findOne({id: this.params.id})
	if(!data) this.throw(404)
        this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.patch('/'+model+'s/:id', jsonBody(), function*(next) {
	if(!(yield auth.authorize(this.user, model+':write', {body: this.request.body, id: this.params.id}))) {
	  this.throw(403)
	}
        if(!this.is('application/vnd.api+json')) {
          this.throw(415) // Unsupported media type
        }
	if(this.request.body.data.id !== this.params.id) {
	  this.throw(409)
	}
        var oldModel
	if(!(oldModel = yield orm.collections[model].findOne({id: this.params.id}))) {
	  this.throw(404)
	}
	yield orm.collections[model].update({id: this.params.id}, toWaterline(this.request.body))
	var data = yield orm.collections[model].findOne({id: this.params.id})
        this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.delete('/'+model+'s/:id', function * (next) {
	if(!(yield auth.authorize(this.user, model+':destroy', {id: this.params.id}))) {
	  this.throw(403)
	}

	var item = yield orm.collections[model].findOne({id: this.params.id})
	if(!item) this.throw(404)
	yield item.destroy()

	this.status = 204 // No Content
        this.body = null
      })

      // Routes for relationships

      Object.keys(orm.collections[model]._attributes)
      .filter((attr) => {
        // only relations
        return models[model].attributes[attr].model
        || models[model].attributes[attr].collection
      })
      .forEach((relation) => {
        var isToMany = !!models[model].attributes[relation].collection
          , relatedModel = models[model].attributes[relation].model
                         || models[model].attributes[relation].collection

        APIv1.get('/'+model+'s/:id/relationships/'+relation, function*(next) {
          // XXX: Not sure how to infer a sensible authorize token
          if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
	    this.throw(403)
          }

          var item = yield orm.collections[model].findOne({id: this.params.id}).populate(relation)
          if(!item) this.throw(404)
          this.body = jsonapi.relation(item, model, relation, {fields: qs.parse(this.querystring).fields})
        })

        if(!isToMany) {
          APIv1.patch('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
	      this.throw(403)
	    }

            if(!this.is('application/vnd.api+json')) {
              this.throw(415) // Unsupported media type
            }

            var item = yield orm.collections[model].findOne({id: this.params.id})

            // Not found?
            if(!item) {
              this.throw(404)
            }

            var data = toWaterline(this.request.body)

            // This is a to-one relation... No arrays!
            if(Array.isArray(data)) {
              this.throw(400)
            }

            item[relation] = null !== data? data.id : null
            yield item.save()

            // all is well? Say nothing.
            this.status = 204
            this.body = null
          })
        }else{
          APIv1.patch('/'+model+'s/:id/relationships/'+relation, function*(next) {
            this.throw(403)
          })

          APIv1.post('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
	      this.throw(403)
            }
            if(!this.is('application/vnd.api+json')) {
              this.throw(415) // Unsupported media type
            }
            var item = yield orm.collections[model].findOne({id: this.params.id})

            // Not found?
            if(!item) {
              this.throw(404)
            }

            var data = toWaterline(this.request.body)

            // Must be an array
            if(!Array.isArray(data)) {
              this.throw(400)
            }

            // check all first
            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              if(!(yield orm.collections[relatedModel].findOne({id: newRel.id}))) {
                this.throw(400)
              }
            }
            // ... then add them all
            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              item[relation].add(newRel.id)
            }
            yield item.save()

            // All is well? Say nothing.
            this.status = 204
            this.body = null
          })

          APIv1.delete('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
	      this.throw(403)
	    }
            var item = yield orm.collections[model].findOne({id: this.params.id})
            var data = toWaterline(this.request.body)

            // Must be an array
            if(!Array.isArray(data)) {
              this.throw(400)
            }

            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              item[relation].remove(newRel.id)
            }
            yield item.save()

            // All is well? Say nothing.
            this.status = 204
            this.body = null
          })
        }
      })

      // Routes for class methods

      Object.keys(orm.collections[model]).forEach((method) => {
        // Automatically create routes for VERB_action class methods -> VERB /models/:id/action
        if(VERBS.some((verb) => method.indexOf(verb+'_') === 0)) {
          var verb = method.split('_')[0]
            , action = method.substr(verb.length+1)
          APIv1[verb]('/'+model+'s/:id/'+action, jsonBody(), models[model][method])
        }else
        // Automatically create routes for VERB class methods -> VERB /models/:id?
        if(VERBS.some((verb) => method.indexOf(verb) === 0)) {
          var verb = method.split('_')[0]
            , action = method.substr(verb.length+1)
          APIv1[verb]('/'+model+'s/:id?', jsonBody(), models[model][method])
        }
      })
    })

    var Document = models.document
      , Snapshot = models.snapshot
      , User = models.user

    APIv1
      .post('/documents/:document/snapshots', jsonBody(), function * (next) {
          if(!(yield auth.authorize(this.user, 'document:change', this.params))) {
            return this.throw(403)
          }
          if(!this.accepts('json')) {
           return this.throw(406)
          }
          var doc = yield Document.findOne({id: this.params.document})
          if(!doc) this.throw(404)

          doc = yield sync.getDocument(doc.id)
          var edit = yield function(cb) {
            doc.receiveEdit(JSON.stringify({
              cs: this.request.body.changes
            , parent: this.request.body.parent
            , user: this.user
            }), null, cb)
          }

          this.body = yield Snapshot.findOne({id: edit.id})
        })

      .post('/documents/:document/import', function * (next) {
          if(!(yield auth.authorize(this.user, 'document:change', this.params))) {
            return this.throw(403)
          }
          var doc = yield Document.findOne({id: this.params.document})
          if(!doc) this.throw(404)

          try {
            var parts = parseUpload(this, { autoFields: true})
              , part, bufferStream
            while(part = yield parts) {
              bufferStream = new WritableBuffer()
              var interval = setInterval(() => {
                if(bufferStream.size() > 8*1024*1024) {
                  part.unpipe()
                  this.throw('Attachment is bigger than 8MB', 400)
                  clearInterval(interval)
                }
              }, 100)
              yield function (cb) {
                part.on('end', cb)
                part.pipe(bufferStream)
              }
              clearInterval(interval)
              yield importexport.import(doc.id, this.user
              , part.mime, bufferStream.getContents())
            }
          }catch(e) {
            console.error(e.stack ||e)
            return this.throw(e.message, 500)
          }

          this.body = {message: 'ok'}
        })

      .get('/documents/:document/snapshots', function * (next) {
          if(!(yield auth.authorize(this.user, 'document:read', this.params))) {
            return this.throw(403)
          }

          // returns all snapshots since X
          if(this.query.since) {
            var sinceSnapshot = yield Snapshot.findOne({id: this.query.since})
            if(!sinceSnapshot) this.throw(404)
            if(sinceSnapshot.document != this.params.document) this.throw(400)

            var snapshots = []
             , s = sinceSnapshot
            while(s = yield Snapshot.findOne({parent: s.id}) ) {
              snapshots.push(s)
            }
            this.body = snapshots
            return
          }

          var doc = yield Document.findOne({id: this.params.document}).populate('snapshots')
          this.body = doc.snapshots // XXX: Allow streamin'
        })

      .get('/snapshots/:snapshot/export', function * () {
        if(!(yield auth.authorize(this.user, 'snapshot:show', this.params))) {
          return this.throw(403)
        }
        var snapshot = yield Snapshot.findOne({id:this.params.snapshot})
        if(!snapshot) this.throw(404)
        var document = yield Document.findOne({id:snapshot.document})
        var type = this.query.type || document.type
        this.type = type
        this.body = yield importexport.export(snapshot.id, type)
      })
  })

  register()
}
<commit_msg>Fix whitespace<commit_after>/**
 * hive.js
 * Copyright (C) 2013-2016 Marcel Klehr <mklehr@gmx.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Mozilla Public License version 2
 * as published by the Mozilla Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the Mozilla Public License
 * along with this program.  If not, see <https://www.mozilla.org/en-US/MPL/2.0/>.
 */
"use strict";
var koa = require('koa')
  , mount = require('koa-mount')
  , router = require('koa-router')
  , jsonBody = require('koa-json-body')
  , parseUpload = require('co-busboy')
  , WritableBuffer = require('stream-buffers').WritableStreamBuffer
  , JSONAPI = require('waterline-to-jsonapi')
  , qs = require('qs')

module.exports = setup
module.exports.consumes = ['http', 'auth', 'hooks', 'orm', 'sync', 'ot', 'importexport']

function setup(plugin, imports, register) {
  var httpApp = imports.http
    , auth = imports.auth
    , hooks = imports.hooks
    , orm = imports.orm
    , sync = imports.sync
    , ot = imports.ot
    , importexport = imports.importexport

  var api = koa()
  httpApp.use(mount('/api', api))

  var APIv1 = koa()
  api.use(mount('/v1', APIv1))

  APIv1.use(function *(next) {
    try {
      if(!this.accepts('application/vnd.api+json')) {
  return this.throw(406)
      }
      yield next;
    } catch (err) {
      this.app.emit('error', err, this)
      this.status = err.status || 500
      this.body = JSONAPI().errors(err)
    }
  })

  APIv1.use(function* (next) {
    var credentials = this.request.query.access_token
      , type = "token"
    if(!credentials) {
      credentials = this.get('Authorization')
      if(!credentials) return this.throw(401)
      var credParts = credentials.split(' ')
      type = credParts[0]
      credentials = credParts[1]
    }
    try {
      this.user = yield auth.authenticate(type, credentials)
    }catch(e) {console.log(e.stack ||e)}

    if(!this.user) {
      return this.throw(401)
    }
    yield next
  })

  APIv1.use(router(APIv1))

  function toWaterline(jsonAPIBody) {
    if(Array.isArray(jsonAPIBody.data)) return jsonAPIBody.data.map(toWaterline)
    var newItem = jsonAPIBody.data.attributes || {id: jsonAPIBody.data.id}
    for(var rel in jsonAPIBody.data.relationships) {
      var linkage = jsonAPIBody.data.relationships[rel].data
      if(Array.isArray(linkage)) {
        newItem[rel] = linkage.map(resource => resource.id)
      }else if(linkage) {
        newItem[rel] = linkage.id
      }
    }
    return newItem
  }

  hooks.on('orm:initialized', function*(models) {
    var VERBS = ['post', 'get', 'patch', 'delete', 'put']
      , jsonapi = JSONAPI(orm)

    Object.keys(orm.collections).forEach((model) => {

      // Routes for CRUD

      APIv1.post('/'+model+'s', jsonBody(), function*(next) {
        if(!(yield auth.authorize(this.user, model+':create', {body: this.request.body}))) {
          this.throw(403)
        }
        if(!this.is('application/vnd.api+json')) {
          this.throw(415) // Unsupported media type
        }

        // Build a waterline-conformant object
        var newItem = toWaterline(this.request.body)
          , data
        // create item
        if(orm.collections[model].override_create) {
          data = yield orm.collections[model].override_create(newItem)
        }else {
          data = yield orm.collections[model].create(newItem)
        }

        this.status = 201
        this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.get('/'+model+'s/:id', function*(next) {
        if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
          this.throw(403)
        }

        var data = yield orm.collections[model].findOne({id: this.params.id})
        if(!data) this.throw(404)
        this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.patch('/'+model+'s/:id', jsonBody(), function*(next) {
        if(!(yield auth.authorize(this.user, model+':write', {body: this.request.body, id: this.params.id}))) {
          this.throw(403)
        }
        if(!this.is('application/vnd.api+json')) {
          this.throw(415) // Unsupported media type
        }
        if(this.request.body.data.id !== this.params.id) {
          this.throw(409)
        }
        var oldModel
        if(!(oldModel = yield orm.collections[model].findOne({id: this.params.id}))) {
          this.throw(404)
        }
        yield orm.collections[model].update({id: this.params.id}, toWaterline(this.request.body))
        var data = yield orm.collections[model].findOne({id: this.params.id})
        this.body = jsonapi.single(data, model, {fields: qs.parse(this.querystring).fields})
      })

      APIv1.delete('/'+model+'s/:id', function * (next) {
        if(!(yield auth.authorize(this.user, model+':destroy', {id: this.params.id}))) {
          this.throw(403)
        }

        var item = yield orm.collections[model].findOne({id: this.params.id})
        if(!item) this.throw(404)
        yield item.destroy()

        this.status = 204 // No Content
        this.body = null
      })

      // Routes for relationships

      Object.keys(orm.collections[model]._attributes)
      .filter((attr) => {
        // only relations
        return models[model].attributes[attr].model
        || models[model].attributes[attr].collection
      })
      .forEach((relation) => {
        var isToMany = !!models[model].attributes[relation].collection
          , relatedModel = models[model].attributes[relation].model
                         || models[model].attributes[relation].collection

        APIv1.get('/'+model+'s/:id/relationships/'+relation, function*(next) {
          // XXX: Not sure how to infer a sensible authorize token
          if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
            this.throw(403)
          }

          var item = yield orm.collections[model].findOne({id: this.params.id}).populate(relation)
          if(!item) this.throw(404)
          this.body = jsonapi.relation(item, model, relation, {fields: qs.parse(this.querystring).fields})
        })

        if(!isToMany) {
          APIv1.patch('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
              this.throw(403)
            }

            if(!this.is('application/vnd.api+json')) {
              this.throw(415) // Unsupported media type
            }

            var item = yield orm.collections[model].findOne({id: this.params.id})

            // Not found?
            if(!item) {
              this.throw(404)
            }

            var data = toWaterline(this.request.body)

            // This is a to-one relation... No arrays!
            if(Array.isArray(data)) {
              this.throw(400)
            }

            item[relation] = null !== data? data.id : null
            yield item.save()

            // all is well? Say nothing.
            this.status = 204
            this.body = null
          })
        }else{
          APIv1.patch('/'+model+'s/:id/relationships/'+relation, function*(next) {
            this.throw(403)
          })

          APIv1.post('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
              this.throw(403)
            }
            if(!this.is('application/vnd.api+json')) {
              this.throw(415) // Unsupported media type
            }
            var item = yield orm.collections[model].findOne({id: this.params.id})

            // Not found?
            if(!item) {
              this.throw(404)
            }

            var data = toWaterline(this.request.body)

            // Must be an array
            if(!Array.isArray(data)) {
              this.throw(400)
            }

            // check all first
            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              if(!(yield orm.collections[relatedModel].findOne({id: newRel.id}))) {
                this.throw(400)
              }
            }
            // ... then add them all
            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              item[relation].add(newRel.id)
            }
            yield item.save()

            // All is well? Say nothing.
            this.status = 204
            this.body = null
          })

          APIv1.delete('/'+model+'s/:id/relationships/'+relation, jsonBody(), function*(next) {
            if(!(yield auth.authorize(this.user, model+':read', {id: this.params.id}))) {
              this.throw(403)
            }
            var item = yield orm.collections[model].findOne({id: this.params.id})
            var data = toWaterline(this.request.body)

            // Must be an array
            if(!Array.isArray(data)) {
              this.throw(400)
            }

            for(var i=0; i < data.length; i++) {
              var newRel = data[i]
              item[relation].remove(newRel.id)
            }
            yield item.save()

            // All is well? Say nothing.
            this.status = 204
            this.body = null
          })
        }
      })

      // Routes for class methods

      Object.keys(orm.collections[model]).forEach((method) => {
        // Automatically create routes for VERB_action class methods -> VERB /models/:id/action
        if(VERBS.some((verb) => method.indexOf(verb+'_') === 0)) {
          var verb = method.split('_')[0]
            , action = method.substr(verb.length+1)
          APIv1[verb]('/'+model+'s/:id/'+action, jsonBody(), models[model][method])
        }else
        // Automatically create routes for VERB class methods -> VERB /models/:id?
        if(VERBS.some((verb) => method.indexOf(verb) === 0)) {
          var verb = method.split('_')[0]
            , action = method.substr(verb.length+1)
          APIv1[verb]('/'+model+'s/:id?', jsonBody(), models[model][method])
        }
      })
    })

    var Document = models.document
      , Snapshot = models.snapshot
      , User = models.user

    APIv1
      .post('/documents/:document/snapshots', jsonBody(), function * (next) {
          if(!(yield auth.authorize(this.user, 'document:change', this.params))) {
            return this.throw(403)
          }
          if(!this.accepts('json')) {
           return this.throw(406)
          }
          var doc = yield Document.findOne({id: this.params.document})
          if(!doc) this.throw(404)

          doc = yield sync.getDocument(doc.id)
          var edit = yield function(cb) {
            doc.receiveEdit(JSON.stringify({
              cs: this.request.body.changes
            , parent: this.request.body.parent
            , user: this.user
            }), null, cb)
          }

          this.body = yield Snapshot.findOne({id: edit.id})
        })

      .post('/documents/:document/import', function * (next) {
          if(!(yield auth.authorize(this.user, 'document:change', this.params))) {
            return this.throw(403)
          }
          var doc = yield Document.findOne({id: this.params.document})
          if(!doc) this.throw(404)

          try {
            var parts = parseUpload(this, { autoFields: true})
              , part, bufferStream
            while(part = yield parts) {
              bufferStream = new WritableBuffer()
              var interval = setInterval(() => {
                if(bufferStream.size() > 8*1024*1024) {
                  part.unpipe()
                  this.throw('Attachment is bigger than 8MB', 400)
                  clearInterval(interval)
                }
              }, 100)
              yield function (cb) {
                part.on('end', cb)
                part.pipe(bufferStream)
              }
              clearInterval(interval)
              yield importexport.import(doc.id, this.user
              , part.mime, bufferStream.getContents())
            }
          }catch(e) {
            console.error(e.stack ||e)
            return this.throw(e.message, 500)
          }

          this.body = {message: 'ok'}
        })

      .get('/documents/:document/snapshots', function * (next) {
          if(!(yield auth.authorize(this.user, 'document:read', this.params))) {
            return this.throw(403)
          }

          // returns all snapshots since X
          if(this.query.since) {
            var sinceSnapshot = yield Snapshot.findOne({id: this.query.since})
            if(!sinceSnapshot) this.throw(404)
            if(sinceSnapshot.document != this.params.document) this.throw(400)

            var snapshots = []
             , s = sinceSnapshot
            while(s = yield Snapshot.findOne({parent: s.id}) ) {
              snapshots.push(s)
            }
            this.body = snapshots
            return
          }

          var doc = yield Document.findOne({id: this.params.document}).populate('snapshots')
          this.body = doc.snapshots // XXX: Allow streamin'
        })

      .get('/snapshots/:snapshot/export', function * () {
        if(!(yield auth.authorize(this.user, 'snapshot:show', this.params))) {
          return this.throw(403)
        }
        var snapshot = yield Snapshot.findOne({id:this.params.snapshot})
        if(!snapshot) this.throw(404)
        var document = yield Document.findOne({id:snapshot.document})
        var type = this.query.type || document.type
        this.type = type
        this.body = yield importexport.export(snapshot.id, type)
      })
  })

  register()
}
<|endoftext|>


<commit_before>import localforage from 'localforage';

import MapboxSource from './src/mapbox/source';
import Turnpoints from './src/turnpoints';
import {INITIAL_MAP_CENTER, INITIAL_MAP_ZOOM, MAPBOX_TOKEN} from './src/settings';

var viewInitialized = false;

var view = new ol.View({
    center: ol.proj.transform(INITIAL_MAP_CENTER, 'EPSG:4326', 'EPSG:3857'),
    zoom: INITIAL_MAP_ZOOM
});

var geolocation = new ol.Geolocation({
    projection: view.getProjection()
});

localforage.getItem('view', (err, value) => {
    viewInitialized = true;

    if (err || !value) {
        // enable tracking
        geolocation.setTracking(true);

    } else {
        view.setCenter(value.center);
        view.setZoom(value.zoom);
    }
});

geolocation.on('change', () => {
    map.beforeRender(ol.animation.pan({
        source: view.getCenter(),
        duration: 250
    }), ol.animation.zoom({
        resolution: view.getResolution(),
        duration: 250
    }));

    // set map center to geolocation
    view.setCenter(geolocation.getPosition());
    view.setZoom(7);

    // disable tracking again
    geolocation.setTracking(false);
});

var turnpoints = new Turnpoints();

turnpoints.push(new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.transform([7, 51], 'EPSG:4326', 'EPSG:3857'))
}));

turnpoints.push(new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.transform([8, 51.5], 'EPSG:4326', 'EPSG:3857'))
}));

var turnpointLayer = new ol.layer.Vector({
    source: new ol.source.Vector({
        features: turnpoints.getArray()
    })
});

var backgroundLayer = new ol.layer.Tile({
    source: new MapboxSource(MAPBOX_TOKEN)
});

var map = new ol.Map({
    interactions: ol.interaction.defaults({
        altShiftDragRotate: false,
        pinchRotate: false
    }),
    keyboardEventTarget: document,
    layers: [backgroundLayer, turnpointLayer],
    logo: false,
    target: 'map',
    view: view
});

map.on('moveend', () => {
    if (viewInitialized) {
        localforage.setItem('view', {
            center: view.getCenter(),
            zoom: view.getZoom()
        });
    }
});

var modify = new ol.interaction.Modify({
    features: turnpoints,
    pixelTolerance: 30
});

map.addInteraction(modify);

modify.setActive(true);

var fullScreenControl = new ol.control.FullScreen();

fullScreenControl.setMap(map);
<commit_msg>Add log messages<commit_after>import localforage from 'localforage';

import MapboxSource from './src/mapbox/source';
import Turnpoints from './src/turnpoints';
import {INITIAL_MAP_CENTER, INITIAL_MAP_ZOOM, MAPBOX_TOKEN} from './src/settings';

console.log('Starting application ...');

var view = new ol.View();

var geolocation = new ol.Geolocation({
    projection: view.getProjection()
});

localforage.getItem('view', (err, value) => {
    if (err || !value) {
        console.log('Enabling GeoLocation tracking ...');
        geolocation.setTracking(true);

        console.log('Setting view parameters to defaults ...');

        console.debug('center:', INITIAL_MAP_CENTER);
        view.setCenter(ol.proj.transform(INITIAL_MAP_CENTER, 'EPSG:4326', 'EPSG:3857'));

        console.debug('zoom:', INITIAL_MAP_ZOOM);
        view.setZoom(INITIAL_MAP_ZOOM);

    } else {
        console.log('Setting view parameters from previous session ...');

        console.debug('center:', ol.proj.transform(value.center, 'EPSG:3857', 'EPSG:4326'));
        view.setCenter(value.center);

        console.debug('zoom:', value.zoom);
        view.setZoom(value.zoom);
    }
});

geolocation.on('change', () => {
    map.beforeRender(ol.animation.pan({
        source: view.getCenter(),
        duration: 250
    }), ol.animation.zoom({
        resolution: view.getResolution(),
        duration: 250
    }));

    console.log('Setting view parameters from GeoLocation ...');

    console.debug('center:', ol.proj.transform(geolocation.getPosition(), 'EPSG:3857', 'EPSG:4326'));
    view.setCenter(geolocation.getPosition());
    view.setZoom(7);

    console.log('Disabling GeoLocation tracking ...');
    geolocation.setTracking(false);
});

var turnpoints = new Turnpoints();

turnpoints.push(new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.transform([7, 51], 'EPSG:4326', 'EPSG:3857'))
}));

turnpoints.push(new ol.Feature({
    geometry: new ol.geom.Point(ol.proj.transform([8, 51.5], 'EPSG:4326', 'EPSG:3857'))
}));

var turnpointLayer = new ol.layer.Vector({
    source: new ol.source.Vector({
        features: turnpoints.getArray()
    })
});

var backgroundLayer = new ol.layer.Tile({
    source: new MapboxSource(MAPBOX_TOKEN)
});

var map = new ol.Map({
    interactions: ol.interaction.defaults({
        altShiftDragRotate: false,
        pinchRotate: false
    }),
    keyboardEventTarget: document,
    layers: [backgroundLayer, turnpointLayer],
    logo: false,
    target: 'map',
    view: view
});

map.on('moveend', () => {
    console.log('Saving view parameters to localforage ...');

    localforage.setItem('view', {
        center: view.getCenter(),
        zoom: view.getZoom()
    });
});

var modify = new ol.interaction.Modify({
    features: turnpoints,
    pixelTolerance: 30
});

map.addInteraction(modify);

modify.setActive(true);

var fullScreenControl = new ol.control.FullScreen();

fullScreenControl.setMap(map);
<|endoftext|>


<commit_before>import React from 'react';
import {connect} from 'react-redux';
import NewPaula from './new-paula';

class MainComponent extends React.Component {
	render() {
		return

React/Redux demo

This is where your content goes. Paula is {this.props.paula}.

			<NewPaula />

;
	}
}

export default connect((state, props) => ({
	//Select your state -> props mappings here
	paula: state.paula,
}))(MainComponent);
<commit_msg>Use a beautiful shorthand for mapping state to props<commit_after>import React from 'react';
import {connect} from 'react-redux';
import NewPaula from './new-paula';

class MainComponent extends React.Component {
	render() {
		return

React/Redux demo

This is where your content goes. Paula is {this.props.paula}.

			<NewPaula />

;
	}
}

export default connect(
	//Select your state -> props mappings here
	({paula}) => ({paula})
)(MainComponent);
<|endoftext|>


<commit_before>/// <reference path="~/phaser.js" />

// Global members
var weights = [0.01, 0.03, 0.07, 0.3, 0.59];

// bool to check if zombie has any balloons left
var hasBallon;

// bool to check if zombie has reached the Man's cloud
var atCloud;

// Initialize Phaser, and create a 400x490px game
var game = new Phaser.Game(400, 490, Phaser.AUTO, 'gameDiv');

// Create our 'main' state that will contain the game
var mainState = {

    preload: function () {
        // This function will be executed at the beginning
        // That's where we load the game's assets

        // Change the background color of the game
        //game.stage.backgroundColor = '#71c5cf';
        game.stage.backgroundColor = '#22AAFF';

        // Load the bird sprite
        game.load.image('man', 'assets/man.png');

        // Load the pipe sprite
        game.load.image('zombie', 'assets/zombie.png');

        // Load the Red Balloon sprite
        game.load.image('redBalloon', 'assets/redBalloon.png');
    },

    create: function () {
        // This function is called after the preload function
        // Here we set up the game, display sprites, etc.

        // Set the physics system
        game.physics.startSystem(Phaser.Physics.ARCADE);

        // Display the bird on the screen
        this.man = this.game.add.sprite(100, 245, 'man');

        // Add gravity to the bird to make it fall
        game.physics.arcade.enable(this.man);

        //this.man.body.gravity.y = 1000;

        // Call the 'jump' function when the spacekey is hit
        var spaceKey =
            this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

        spaceKey.onDown.add(this.shoot, this);

        this.zombies = game.add.group() // Create a group
        this.zombies.enableBody = true; // Add physics to the group
        this.zombies.createMultiple(100, 'zombie'); //Create 20 pipes TEMP 50

        // Set the position of the zombies group
        this.zombies.setAll('zombie.body.x', 400);
        this.zombies.setAll('zombie.body.y', 0);

        // Add timer :: call addRowOfPipes() every 1.5sec
        this.timer = game.time.events.loop(3500, this.addZombieHorde, this);
        this.addZombieHorde;

        this.balloons = game.add.group();
        this.balloons.enableBody = true;
        this.balloons.createMultiple(100, 'redBalloons');

        // Add timer :: call addRowOfPipes() every 1.5sec
        this.gameplayTimer = game.time.events.loop(10000, this.increaseStats,
this);

        // Create the Score object
        this.score = 0;

        this.labelScore = game.add.text(20, 20, "0", {
            font:
                "30px Arial", fill: "#ffffff" });
    },

    update: function () {
        // This function is called 60 times per second
        // It contains the game's logic

        //TODO: Fix this to move the zombie left and fall onto the cloud
        // If the bird is out of the world (too high or too low), call the
        //'restartGame' function
        //if (this.zombie.inWorld == false)
        //    this.restartGame();

        // call restartGame() each time the bird collides with a pipe
        game.physics.arcade.overlap(this.man, this.pipes, this.restartGame,
            null, this);
    },

    //Make the man jump
    shoot: function () {
        // Add a vertical velocity to the bird
        this.man.body.velocity.y = -350;
    },

    // Restart the game
    restartGame: function () {
        // Start the 'main'state, which restarts the game
        game.state.start('main');
    },

    addOneZombie: function (x, y) {
        // Get the first dead zombie of our group
        var zombie = this.zombies.getFirstDead();

        // Set the new position of the pipe
        zombie.reset(x, y);

        // TEMP make zombies float
        zombie.body.velocity.y = -20;

        // Set the gravity for the zombie
        //zombie.body.gravity.y = 1000;

        // Kill the zombie when it's no longer visible
        zombie.checkWorldBounds = true;
        zombie.outOfBoundsKill = true;
    },

    addZombieHorde: function () {
        var numberOfZombies = [5, 4, 3, 2, 1];

        // 1st: 2.5%, 2nd: 10%, 3rd: 15%, Lucky: 22.5%, Consolation: 50%
        weights = [0.01, 0.03, 0.07, 0.3, 0.59];

        var weighedList = [];
        var counter = 0;

        // loop over weights
        for (i = 0; i < 5; i++)
        {
            var amount = weights[i] * 100;

            // Push Current PLACE into array proper amount
            for (var j = 0; j < amount; j++) {
                // Current PLACE :: Use i counter
                // TODO: Test if this works when staticly adding as weights[i]
                //  or, try this.weightedList v.s. var weightedList
                counter++;
                weighedList[counter] = numberOfZombies[i];
            }
        }

        // Pick number of zombies to spawn
        var R_Index = Math.floor( Math.random() * (100 - 0 + 1) ) + 0;

        if (R_Index >= 59)
            R_NumberOfZombies = 1;
        else if (R_Index >= 30)
            R_NumberOfZombies = 2;
        else if (R_Index >= 7)
            R_NumberOfZombies = 3;
        else if (R_Index >= 3)
            R_NumberOfZombies = 4;
        else
            R_NumberOfZombies = 5;

        for (var i = 0; i < R_NumberOfZombies; i++)
        {
            var xCoord = Math.floor(Math.random() * 300) + 150;
            var yCoord = Math.floor(Math.random() * 50) + 420;

            this.addOneZombie(xCoord, yCoord + 10);
        }

        // Increment the score by 1 ea time new pipes are created
        this.score += 1;

        this.labelScore.text = this.score;
    },

    addBallons: function()
    {

    },

    increaseStats: function()
    {
        console.log("increaseStats called~");

        //weights = [0.025, 0.1, 0.15, 0.225, 0.5];
        for (var i = 0; i < weights.length; i++)
        {
            // 5th is still greater than 4th
            if (weights[4] >= 29)
            {
                // Take 5 away from 5th Index
                weights[4] -= 5;

                // Take from the last, give to second to last
                weights[3] += 5;
            }
            // 4th is still greater than 3rd
            /*
            else if (weights[2] < 30)
            {
                weights[3] -= 5;
                weights[2] += 5;
            }
            else if (weights[1] < 60)
            {
                weights[3] -= 5;
                weights[2] += 5;
            }*/
        }
    },
};

// Add and start the 'main' state to start the game
game.state.add('main', mainState);
game.state.start('main');
<commit_msg>This has some serious issues with the way that zombies are spawned; not grasping the concept of groups and sprites yet. Just a safety backup that is somewhat operational, but mostly for getting a checkpoint.<commit_after>/// <reference path="~/phaser.js" />

// Global objects
var zombie;

var player;

var torso;

var shotgun;

// Global helper members
var weights = [0.01, 0.03, 0.07, 0.3, 0.59];

// bool to check if zombie has any balloons left
var hasBallon;

// bool to check if zombie has reached the Man's cloud
var atCloud;

// Initialize Phaser, and create a 400x490px game
var game = new Phaser.Game(400, 490, Phaser.AUTO, 'gameDiv');

// Create our 'main' state that will contain the game
var mainState = {

    preload: function ()
    {
        // This function will be executed at the beginning
        // That's where we load the game's assets

        // Change the background color of the game
        game.stage.backgroundColor = '#71c5cf';

        // Load the player sprite
        game.load.image('man', 'assets/man.png');

        // Load the zombie sprite
        game.load.image('zombie', 'assets/zombie.png');

        // Load the Red Balloon sprite
        game.load.image('redBalloon', 'assets/redBalloon.png');
    },

    // This function is called after the preload function
    // Here we set up the game, display sprites, etc.
    create: function ()
    {
        // Set the physics system
        game.physics.startSystem(Phaser.Physics.ARCADE);

        // PLAYER
        // Display the player on the screen
        this.man = this.game.add.sprite(100, 245, 'man');

        // Add gravity to the man to make it fall
        game.physics.arcade.enable(this.man);

        //this.man.body.gravity.y = 1000;

        // Call the 'shoot' function when the spacekey is hit
        var spaceKey =
            this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

        spaceKey.onDown.add(this.shoot, this);

        // Main Holder Sprite :: ZOMBIE
        zombie = game.add.group();

        // Torso
        zombie.torso = game.add.sprite(0, 0, 'zombie');
        zombie.torso.anchor.setTo(0.5);
        zombie.addChild(zombie.torso);

        game.physics.arcade.enable(zombie.torso);

        //  Balloon
        zombie.balloon = game.add.sprite(0, -100, 'redBalloon');
        zombie.balloon.anchor.setTo(0.15, 0.5);
        zombie.addChild(zombie.balloon);

        // Zombie :: PROPERTIES
        this.zombies = game.add.group();
        this.zombies.enableBody = true; // Add physics to the group
        this.zombies.createMultiple(100, 'zombie');
        this.zombies.createMultiple(100, 'redBalloon');

        // Set the position of the zombies group
        //this.zombies.setAll('zombie.body.x', 400);
        //this.zombies.setAll('zombie.body.y', 0);

        // Add timer :: call addRowOfPipes() every 1.5sec
        this.timer = game.time.events.loop(3500, this.addZombieHorde, this);
        this.addZombieHorde;

        //this.balloons = game.add.group();
        //this.balloons.enableBody = true;
        //this.balloons.createMultiple(100, 'redBalloons');

        // Add timer :: Increase stats every 1.5sec
        //this.gameplayTimer = game.time.events.loop(10000, this.increaseStats, this);

        // Create the Score object
        this.score = 0;

        this.labelScore = game.add.text(20, 20, "0", {
            font:
                "30px Arial", fill: "#ffffff" });
    },

    update: function ()
    {
        // This function is called 60 times per second
        // It contains the game's logic

        //TODO: Fix this to move the zombie left and fall onto the cloud
        // If the bird is out of the world (too high or too low), call the
        //'restartGame' function
        //if (this.zombie.inWorld == false)
        //    this.restartGame();

        // call restartGame() each time the bird collides with a pipe
        game.physics.arcade.overlap(this.man, this.pipes, this.restartGame,
            null, this);
    },

    // TODO: Create shotgun
    // TODO: Create pivot on shotgun
    // TODO: Create projectile
    // TODO: Make the projectile shoot from Shotgun
    //Make the player shoot
    shoot: function () {
        // Add a vertical velocity to the bird
        this.man.body.velocity.y = -350;
    },

    // Restart the game
    restartGame: function ()
    {
        // Start the 'main'state, which restarts the game
        game.state.start('main');
    },

    addOneZombie: function (x, y)
    {
        // Get the first dead zombie of our group
        //var currentZombie = this.zombies.getFirstDead();

        // Set the new position of the pipe
        //currentZombie.reset(x, y);
        zombie.torso.reset(x,y);

        // TEMP make zombies float
        zombie.torso.body.velocity.y = -20;

        // Set the gravity for the zombie
        //zombie.body.gravity.y = 1000;

        // Kill the zombie when it's no longer visible
        zombie.torso.checkWorldBounds = true;
        zombie.torso.outOfBoundsKill = true;
    },

    addZombieHorde: function ()
    {
        var numberOfZombies = [5, 4, 3, 2, 1];

        var weighedList = [];
        var counter = 0;

        // loop over weights
        for (i = 0; i < 5; i++)
        {
            var amount = weights[i] * 100;

            // Push Current PLACE into array proper amount
            for (var j = 0; j < amount; j++)
            {
                // Current PLACE :: Use i counter
                counter++;
                weighedList[counter] = numberOfZombies[i];
            }
        }

        // Pick number of zombies to spawn
        var R_Index = Math.floor( Math.random() * (100 - 0 + 1) ) + 0;

        // Get RandomNumber of zombies
        R_NumberOfZombies = weighedList[R_Index];

        for (var i = 0; i < R_NumberOfZombies; i++)
        {
            var xCoord = Math.floor(Math.random() * 300) + 150;
            var yCoord = Math.floor(Math.random() * 50) + 420;

            this.addOneZombie(xCoord, yCoord + 10);
        }

        // Increment the score by 1 ea time new pipes are created
        this.score += 1;

        this.labelScore.text = this.score;
    },

    addBallons: function()
    {

    },

    increaseStats: function()
    {
        console.log("increaseStats called~");

        //weights = [0.025, 0.1, 0.15, 0.225, 0.5];
        for (var i = 0; i < weights.length; i++)
        {
            // 5th is still greater than 4th
            if (weights[4] >= 29)
            {
                // Take 5 away from 5th Index
                weights[4] -= 5;

                // Take from the last, give to second to last
                weights[3] += 5;
            }
            // 4th is still greater than 3rd
            /*
            else if (weights[2] < 30)
            {
                weights[3] -= 5;
                weights[2] += 5;
            }
            else if (weights[1] < 60)
            {
                weights[3] -= 5;
                weights[2] += 5;
            }*/
        }
    },
};

// Add and start the 'main' state to start the game
game.state.add('main', mainState);
game.state.start('main');
<|endoftext|>


<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');

var CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import Autocomplete from './Autocomplete';
import classNames from 'classnames';

import UserSettingsStore from '../../../UserSettingsStore';

export default class MessageComposer extends React.Component {
    constructor(props, context) {
        super(props, context);
        this.onCallClick = this.onCallClick.bind(this);
        this.onHangupClick = this.onHangupClick.bind(this);
        this.onUploadClick = this.onUploadClick.bind(this);
        this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
        this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
        this.onInputContentChanged = this.onInputContentChanged.bind(this);
        this.onUpArrow = this.onUpArrow.bind(this);
        this.onDownArrow = this.onDownArrow.bind(this);
        this._tryComplete = this._tryComplete.bind(this);
        this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
        this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
        this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
        this.onInputStateChanged = this.onInputStateChanged.bind(this);
        this.onEvent = this.onEvent.bind(this);

        this.state = {
            autocompleteQuery: '',
            selection: null,
            inputState: {
                style: [],
                blockType: null,
                isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
                wordCount: 0,
            },
            showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
        };

    }

    componentDidMount() {
        // N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
        // for 'event' fires *after* 'RoomEvent', and our room won't have yet been
        // marked as encrypted.
        // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
        MatrixClientPeg.get().on("event", this.onEvent);
    }

    componentWillUnmount() {
        MatrixClientPeg.get().removeListener("event", this.onEvent);
    }

    onEvent(event) {
        if (event.getType() !== 'm.room.encryption') return;
        if (event.getRoomId() !== this.props.room.roomId) return;
        this.forceUpdate();
    }

    onUploadClick(ev) {
        if (MatrixClientPeg.get().isGuest()) {
            let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
            Modal.createDialog(NeedToRegisterDialog, {
                title: "Please Register",
                description: "Guest users can't upload files. Please register to upload.",
            });
            return;
        }

        this.refs.uploadInput.click();
    }

    onUploadFileSelected(ev) {
        let files = ev.target.files;

        let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
        let TintableSvg = sdk.getComponent("elements.TintableSvg");

        let fileList = [];
        for (let i=0; i<files.length; i++) {
            fileList.push(

                <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}

);
        }

        Modal.createDialog(QuestionDialog, {
            title: "Upload Files",
            description: (

Are you sure you want upload the following files?

                        {fileList}

            ),
            onFinished: (shouldUpload) => {
                if(shouldUpload) {
                    // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
                    if (files) {
                        for(var i=0; i<files.length; i++) {
                            this.props.uploadFile(files[i]);
                        }
                    }
                }

                this.refs.uploadInput.value = null;
            },
        });
    }

    onHangupClick() {
        var call = CallHandler.getCallForRoom(this.props.room.roomId);
        //var call = CallHandler.getAnyActiveCall();
        if (!call) {
            return;
        }
        dis.dispatch({
            action: 'hangup',
            // hangup the call for this room, which may not be the room in props
            // (e.g. conferences which will hangup the 1:1 room instead)
            room_id: call.roomId,
        });
    }

    onCallClick(ev) {
        dis.dispatch({
            action: 'place_call',
            type: ev.shiftKey ? "screensharing" : "video",
            room_id: this.props.room.roomId,
        });
    }

    onVoiceCallClick(ev) {
        dis.dispatch({
            action: 'place_call',
            type: 'voice',
            room_id: this.props.room.roomId,
        });
    }

    onInputContentChanged(content: string, selection: {start: number, end: number}) {
        this.setState({
            autocompleteQuery: content,
            selection,
        });
    }

    onInputStateChanged(inputState) {
        this.setState({inputState});
    }

    onUpArrow() {
       return this.refs.autocomplete.onUpArrow();
    }

    onDownArrow() {
        return this.refs.autocomplete.onDownArrow();
    }

    _tryComplete(): boolean {
        if (this.refs.autocomplete) {
            return this.refs.autocomplete.onCompletionClicked();
        }
        return false;
    }

    _onAutocompleteConfirm(range, completion) {
        if (this.messageComposerInput) {
            this.messageComposerInput.setDisplayedCompletion(range, completion);
        }
    }

    onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
        event.preventDefault();
        this.messageComposerInput.onFormatButtonClicked(name, event);
    }

    onToggleFormattingClicked() {
        UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
        this.setState({showFormatting: !this.state.showFormatting});
    }

    onToggleMarkdownClicked(e) {
        e.preventDefault(); // don't steal focus from the editor!
        this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
    }

    render() {
        var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
        var uploadInputStyle = {display: 'none'};
        var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
        var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
            (UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));

        var controls = [];

        controls.push(

                <MemberAvatar member={me} width={24} height={24} />

        );

        if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
            // FIXME: show a /!\ if there are untrusted devices in the room...
            controls.push(

            );
        }

        var callButton, videoCallButton, hangupButton;
        if (this.props.callState && this.props.callState !== 'ended') {
            hangupButton =

;
        }
        else {
            callButton =

                    <TintableSvg src="img/icon-call.svg" width="35" height="35"/>

;
            videoCallButton =

                    <TintableSvg src="img/icons-video.svg" width="35" height="35"/>

;
        }

        var canSendMessages = this.props.room.currentState.maySendMessage(
            MatrixClientPeg.get().credentials.userId);

        if (canSendMessages) {
            // This also currently includes the call buttons. Really we should
            // check separately for whether we can call, but this is slightly
            // complex because of conference calls.
            var uploadButton = (

                    <TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
                    <input ref="uploadInput" type="file"
                        style={uploadInputStyle}
                        multiple
                        onChange={this.onUploadFileSelected} />

            );

            const formattingButton = (

            );

            controls.push(
                <MessageComposerInput
                    ref={c => this.messageComposerInput = c}
                    key="controls_input"
                    onResize={this.props.onResize}
                    room={this.props.room}
                    tryComplete={this._tryComplete}
                    onUpArrow={this.onUpArrow}
                    onDownArrow={this.onDownArrow}
                    tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
                    onContentChanged={this.onInputContentChanged}
                    onInputStateChanged={this.onInputStateChanged} />,
                formattingButton,
                uploadButton,
                hangupButton,
                callButton,
                videoCallButton
            );
        } else {
            controls.push(

                    You do not have permission to post to this room

            );
        }

        let autoComplete;
        if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) {
            autoComplete =

                <Autocomplete
                    ref="autocomplete"
                    onConfirm={this._onAutocompleteConfirm}
                    query={this.state.autocompleteQuery}
                    selection={this.state.selection} />

;
        }

        const {style, blockType} = this.state.inputState;
        const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
            name => {
                const active = style.includes(name) || blockType === name;
                const suffix = active ? '-o-n' : '';
                const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
                const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
                const className = classNames("mx_MessageComposer_format_button", {
                    mx_MessageComposer_format_button_disabled: disabled,
                });
                return <img className={className}
                            title={name}
                            onMouseDown={disabled ? null : onFormatButtonClicked}
                            key={name}
                            src={`img/button-text-${name}${suffix}.svg`}
                            height="17" />;
            },
        );

        return (

                        {controls}

                {UserSettingsStore.isFeatureEnabled('rich_text_editor') ?

                            {formatButtons}

                            <img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
                                 onMouseDown={this.onToggleMarkdownClicked}
                                className="mx_MessageComposer_formatbar_markdown"
                                src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />

: null
                }

        );
    }
};

MessageComposer.propTypes = {
    tabComplete: React.PropTypes.any,

    // a callback which is called when the height of the composer is
    // changed due to a change in content.
    onResize: React.PropTypes.func,

    // js-sdk Room object
    room: React.PropTypes.object.isRequired,

    // string representing the current voip call state
    callState: React.PropTypes.string,

    // callback when a file to upload is chosen
    uploadFile: React.PropTypes.func.isRequired,

    // opacity for dynamic UI fading effects
    opacity: React.PropTypes.number
};
<commit_msg>Prevent error when clicking 'log in'<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');

var CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import Autocomplete from './Autocomplete';
import classNames from 'classnames';

import UserSettingsStore from '../../../UserSettingsStore';

export default class MessageComposer extends React.Component {
    constructor(props, context) {
        super(props, context);
        this.onCallClick = this.onCallClick.bind(this);
        this.onHangupClick = this.onHangupClick.bind(this);
        this.onUploadClick = this.onUploadClick.bind(this);
        this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
        this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
        this.onInputContentChanged = this.onInputContentChanged.bind(this);
        this.onUpArrow = this.onUpArrow.bind(this);
        this.onDownArrow = this.onDownArrow.bind(this);
        this._tryComplete = this._tryComplete.bind(this);
        this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
        this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
        this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
        this.onInputStateChanged = this.onInputStateChanged.bind(this);
        this.onEvent = this.onEvent.bind(this);

        this.state = {
            autocompleteQuery: '',
            selection: null,
            inputState: {
                style: [],
                blockType: null,
                isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
                wordCount: 0,
            },
            showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
        };

    }

    componentDidMount() {
        // N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
        // for 'event' fires *after* 'RoomEvent', and our room won't have yet been
        // marked as encrypted.
        // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
        MatrixClientPeg.get().on("event", this.onEvent);
    }

    componentWillUnmount() {
        if (MatrixClientPeg.get()) {
            MatrixClientPeg.get().removeListener("event", this.onEvent);
        }
    }

    onEvent(event) {
        if (event.getType() !== 'm.room.encryption') return;
        if (event.getRoomId() !== this.props.room.roomId) return;
        this.forceUpdate();
    }

    onUploadClick(ev) {
        if (MatrixClientPeg.get().isGuest()) {
            let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
            Modal.createDialog(NeedToRegisterDialog, {
                title: "Please Register",
                description: "Guest users can't upload files. Please register to upload.",
            });
            return;
        }

        this.refs.uploadInput.click();
    }

    onUploadFileSelected(ev) {
        let files = ev.target.files;

        let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
        let TintableSvg = sdk.getComponent("elements.TintableSvg");

        let fileList = [];
        for (let i=0; i<files.length; i++) {
            fileList.push(

                <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}

);
        }

        Modal.createDialog(QuestionDialog, {
            title: "Upload Files",
            description: (

Are you sure you want upload the following files?

                        {fileList}

            ),
            onFinished: (shouldUpload) => {
                if(shouldUpload) {
                    // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
                    if (files) {
                        for(var i=0; i<files.length; i++) {
                            this.props.uploadFile(files[i]);
                        }
                    }
                }

                this.refs.uploadInput.value = null;
            },
        });
    }

    onHangupClick() {
        var call = CallHandler.getCallForRoom(this.props.room.roomId);
        //var call = CallHandler.getAnyActiveCall();
        if (!call) {
            return;
        }
        dis.dispatch({
            action: 'hangup',
            // hangup the call for this room, which may not be the room in props
            // (e.g. conferences which will hangup the 1:1 room instead)
            room_id: call.roomId,
        });
    }

    onCallClick(ev) {
        dis.dispatch({
            action: 'place_call',
            type: ev.shiftKey ? "screensharing" : "video",
            room_id: this.props.room.roomId,
        });
    }

    onVoiceCallClick(ev) {
        dis.dispatch({
            action: 'place_call',
            type: 'voice',
            room_id: this.props.room.roomId,
        });
    }

    onInputContentChanged(content: string, selection: {start: number, end: number}) {
        this.setState({
            autocompleteQuery: content,
            selection,
        });
    }

    onInputStateChanged(inputState) {
        this.setState({inputState});
    }

    onUpArrow() {
       return this.refs.autocomplete.onUpArrow();
    }

    onDownArrow() {
        return this.refs.autocomplete.onDownArrow();
    }

    _tryComplete(): boolean {
        if (this.refs.autocomplete) {
            return this.refs.autocomplete.onCompletionClicked();
        }
        return false;
    }

    _onAutocompleteConfirm(range, completion) {
        if (this.messageComposerInput) {
            this.messageComposerInput.setDisplayedCompletion(range, completion);
        }
    }

    onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
        event.preventDefault();
        this.messageComposerInput.onFormatButtonClicked(name, event);
    }

    onToggleFormattingClicked() {
        UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
        this.setState({showFormatting: !this.state.showFormatting});
    }

    onToggleMarkdownClicked(e) {
        e.preventDefault(); // don't steal focus from the editor!
        this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
    }

    render() {
        var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
        var uploadInputStyle = {display: 'none'};
        var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
        var TintableSvg = sdk.getComponent("elements.TintableSvg");
        var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
            (UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));

        var controls = [];

        controls.push(

                <MemberAvatar member={me} width={24} height={24} />

        );

        if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
            // FIXME: show a /!\ if there are untrusted devices in the room...
            controls.push(

            );
        }

        var callButton, videoCallButton, hangupButton;
        if (this.props.callState && this.props.callState !== 'ended') {
            hangupButton =

;
        }
        else {
            callButton =

                    <TintableSvg src="img/icon-call.svg" width="35" height="35"/>

;
            videoCallButton =

                    <TintableSvg src="img/icons-video.svg" width="35" height="35"/>

;
        }

        var canSendMessages = this.props.room.currentState.maySendMessage(
            MatrixClientPeg.get().credentials.userId);

        if (canSendMessages) {
            // This also currently includes the call buttons. Really we should
            // check separately for whether we can call, but this is slightly
            // complex because of conference calls.
            var uploadButton = (

                    <TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
                    <input ref="uploadInput" type="file"
                        style={uploadInputStyle}
                        multiple
                        onChange={this.onUploadFileSelected} />

            );

            const formattingButton = (

            );

            controls.push(
                <MessageComposerInput
                    ref={c => this.messageComposerInput = c}
                    key="controls_input"
                    onResize={this.props.onResize}
                    room={this.props.room}
                    tryComplete={this._tryComplete}
                    onUpArrow={this.onUpArrow}
                    onDownArrow={this.onDownArrow}
                    tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
                    onContentChanged={this.onInputContentChanged}
                    onInputStateChanged={this.onInputStateChanged} />,
                formattingButton,
                uploadButton,
                hangupButton,
                callButton,
                videoCallButton
            );
        } else {
            controls.push(

                    You do not have permission to post to this room

            );
        }

        let autoComplete;
        if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) {
            autoComplete =

                <Autocomplete
                    ref="autocomplete"
                    onConfirm={this._onAutocompleteConfirm}
                    query={this.state.autocompleteQuery}
                    selection={this.state.selection} />

;
        }

        const {style, blockType} = this.state.inputState;
        const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
            name => {
                const active = style.includes(name) || blockType === name;
                const suffix = active ? '-o-n' : '';
                const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
                const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
                const className = classNames("mx_MessageComposer_format_button", {
                    mx_MessageComposer_format_button_disabled: disabled,
                });
                return <img className={className}
                            title={name}
                            onMouseDown={disabled ? null : onFormatButtonClicked}
                            key={name}
                            src={`img/button-text-${name}${suffix}.svg`}
                            height="17" />;
            },
        );

        return (

                        {controls}

                {UserSettingsStore.isFeatureEnabled('rich_text_editor') ?

                            {formatButtons}

                            <img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
                                 onMouseDown={this.onToggleMarkdownClicked}
                                className="mx_MessageComposer_formatbar_markdown"
                                src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />

: null
                }

        );
    }
};

MessageComposer.propTypes = {
    tabComplete: React.PropTypes.any,

    // a callback which is called when the height of the composer is
    // changed due to a change in content.
    onResize: React.PropTypes.func,

    // js-sdk Room object
    room: React.PropTypes.object.isRequired,

    // string representing the current voip call state
    callState: React.PropTypes.string,

    // callback when a file to upload is chosen
    uploadFile: React.PropTypes.func.isRequired,

    // opacity for dynamic UI fading effects
    opacity: React.PropTypes.number
};
<|endoftext|>


<commit_before>/*
 * gDoc Sub System
 *
 */

/*** Global Params ***/
var numHeaderRows = 1;
var numHeaderCols = 2;
var numSubColumnPairs = 5;
var toEmail = 'group@listserv.com';
var emailSubject = 'Group Name';

/*** Optional Params ***/
var toastTimeout = 3;
var headerBgColor = '#465C8E'; //darkBlue
var borderColor = '#A2A3A4'; //lightBlue
var altColor = '#EAEBED'; //veryLightBlue

/*** Main Functions ***/

// adds a 'Subs' menu
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [];
  menuEntries.push({
    name: "Send Email",
    functionName: "sendEmail"
  });
  menuEntries.push({
    name: "Update Colors",
    functionName: "updateColors"
  });
  menuEntries.push(null);
  menuEntries.push({
    name: "About",
    functionName: "about"
  });
  ss.addMenu("Subs", menuEntries);
}

// updates the colors of edited rows
function onEdit(e) {
  for (var row = e.range.getRow(); row <= e.range.getLastRow(); row++) {
    colorRow(row);
  }
}

// colors all rows
function updateColors() {
  var startRow = 1 + numHeaderRows;
  var endRow = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getMaxRows();
  for (var row = startRow; row <= endRow; row++) {
    colorRow(row);
  }
}

// colors a row based on the date in first column
function colorRow(row) {
  if (row <= numHeaderRows) {
    return;
  }

  var firstSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var date = firstSheet.getRange(row, 1).getValue();
  var rowRange = firstSheet.getRange(row, 1 + numHeaderCols, 1, numSubColumnPairs * 2);

  if (!(date instanceof Date)) {
    rowRange.setBackground('white');
    return;
  }

  if (dateTodayDiff(date) < 0) {
    rowRange.setBackground('grey');
    return;
  }

  var rowData = rowRange.getValues()[0];
  var rowColors = [];

  for (var col = 0; col < rowData.length; col += 2) {
    var orig = rowData[col];
    var sub = rowData[col + 1];

    if (orig != '' && sub == '') {
      rowColors.push('pink');
    } else if (orig != '' && sub != '') {
      rowColors.push('lightgreen');
    } else {
      rowColors.push('white');
    }
    rowColors.push('#F3F3F3');
  }
  rowRange.setBackgrounds([rowColors]);
}

// sends an email digest with sub requests
function sendEmail() {
  var firstSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var names = [],
    dates = [],
    reasons = [];

  for (var row = 1 + numHeaderRows; row <= firstSheet.getMaxRows(); row++) {
    var date = firstSheet.getRange(row, 1).getValue();

    if ((date instanceof Date) && dateTodayDiff(date) >= 0) {
      var rowRange = firstSheet.getRange(row, 1 + numHeaderCols, 1, numSubColumnPairs * 2);
      var rowData = rowRange.getValues()[0];
      var rowNotes = rowRange.getNotes()[0];
      for (var col = 0; col < rowData.length; col += 2) {
        var orig = rowData[col];
        var sub = rowData[col + 1];
        if (orig != '' && sub == '') {
          names.push(orig);
          dates.push(date);
          var r = rowNotes[col];
          if (r == '') {
            r = '{reason not specified. please add reason as a NOTE in gdoc}';
          }
          reasons.push(r);
        }
      }
    }
  }

  if (names.length > 0) {
    var htmlMsg = createHTMLTable(names, dates, reasons);
    htmlMsg += '

 *This message was automatically generated at ' + formatToday('h:mm a (M/d/yy)') + ' by ' + appName + ' [' + appUrl + '].';
    var subject = emailSubject + ' ' + formatToday('M/d') + ' Digest [' + names.length + ' needed]';
    MailApp.sendEmail(toEmail, subject, "Your email client doesn't support HTML :(", {
      htmlBody: htmlMsg
    });
    SpreadsheetApp.getActiveSpreadsheet().toast(names.length + ' people need subs', 'Emailed ' + toEmail, toastTimeout);
  } else {
    SpreadsheetApp.getActiveSpreadsheet().toast('No one needs a sub', 'Email not sent', toastTimeout);
  }
}

// returns a formatted html table
function createHTMLTable(nameList, dateList, reasonList) {
  var HTMLtable = wrapHTML('Sub Requests', 'h2', 'style="color:' + headerBgColor + '"');
  HTMLtable += '
';
  var hDate = wrapHTML('Date', 'th', 'width="15%" align="center"');
  var hName = wrapHTML('Name', 'th', 'width="15%" align="center"');
  var hMessage = wrapHTML('Message', 'th', 'width="70%" align="center"');
  HTMLtable += wrapHTML(hDate + hName + hMessage, 'tr', 'style="background-color:' + headerBgColor + ';color:white"');

  for (var i = 0; i < nameList.length; i++) {
    var bgColor = '';
    if (i % 2 != 0) bgColor = 'bgcolor="' + altColor + '" ';

    var cellStyle = 'style="border:1px solid ' + borderColor + '"';
    hDate = wrapHTML(formatDate(dateList[i], 'M/d (EEE)'), 'td', cellStyle);
    hName = wrapHTML(nameList[i], 'td', cellStyle);
    hMessage = wrapHTML(reasonList[i], 'td', cellStyle);
    HTMLtable += wrapHTML(hDate + hName + hMessage, 'tr', bgColor + cellStyle);
  }

  HTMLtable += "
";
  HTMLtable += '
Sign up: ';
  HTMLtable += wrapHTML('LINK', 'a', 'href="' + SpreadsheetApp.getActive().getUrl() + '"');
  return HTMLtable;
}

// displays app info
function about() {
  SpreadsheetApp.getActiveSpreadsheet().toast(appUrl, appName);
}

/*** Helper Functions ***/

function dateTodayDiff(d) {
  var today = new Date();
  var dateUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
  var todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
  return Math.floor((dateUtc - todayUtc) / (1000 * 60 * 60 * 24));
}

function wrapHTML(inside, html, options) {
  return '<' + html + ' ' + options + '>' + inside + '</' + html + '>';
}

function formatDate(d, f) {
  return Utilities.formatDate(d, Session.getTimeZone(), f);
}

function formatToday(f) {
  return formatDate(new Date(), f);
}

/*** App Params ***/
var appName = 'gDoc Sub System';
var appUrl = 'http://ultimateanu.github.io/gdoc-sub-system';<commit_msg>comment<commit_after>/*
 * gDoc Sub System
 *
 */

/*** Global Params ***/
var numHeaderRows = 1;
var numHeaderCols = 2;
var numSubColumnPairs = 5;
var toEmail = 'group@listserv.com';
var emailSubject = 'Group Name';

/*** Optional Params ***/
var toastTimeout = 3;
var headerBgColor = '#465C8E'; //darkBlue
var borderColor = '#A2A3A4'; //lightBlue
var altColor = '#EAEBED'; //veryLightBlue

/*** Main Functions ***/

// adds a 'Subs' menu
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [];
  menuEntries.push({
    name: "Send Email",
    functionName: "sendEmail"
  });
  menuEntries.push({
    name: "Update Colors",
    functionName: "updateColors"
  });
  menuEntries.push(null);
  menuEntries.push({
    name: "About",
    functionName: "about"
  });
  ss.addMenu("Subs", menuEntries);
}

// updates the colors of edited rows
function onEdit(e) {
  for (var row = e.range.getRow(); row <= e.range.getLastRow(); row++) {
    colorRow(row);
  }
}

// colors all rows
function updateColors() {
  var startRow = 1 + numHeaderRows;
  var endRow = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getMaxRows();
  for (var row = startRow; row <= endRow; row++) {
    colorRow(row);
  }
}

// colors a row based on the date in first column
function colorRow(row) {
  if (row <= numHeaderRows) {
    return;
  }

  var firstSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var date = firstSheet.getRange(row, 1).getValue();
  var rowRange = firstSheet.getRange(row, 1 + numHeaderCols, 1, numSubColumnPairs * 2);

  if (!(date instanceof Date)) {
    rowRange.setBackground('white');
    return;
  }

  if (dateTodayDiff(date) < 0) {
    rowRange.setBackground('grey');
    return;
  }

  var rowData = rowRange.getValues()[0];
  var rowColors = [];

  for (var col = 0; col < rowData.length; col += 2) {
    var orig = rowData[col];
    var sub = rowData[col + 1];

    if (orig != '' && sub == '') {
      rowColors.push('pink');
    } else if (orig != '' && sub != '') {
      rowColors.push('lightgreen');
    } else {
      rowColors.push('white');
    }
    rowColors.push('#F3F3F3');
  }
  rowRange.setBackgrounds([rowColors]);
}

// sends an email digest with sub requests
function sendEmail() {
  var firstSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
  var names = [],
    dates = [],
    reasons = [];

  for (var row = 1 + numHeaderRows; row <= firstSheet.getMaxRows(); row++) {
    var date = firstSheet.getRange(row, 1).getValue();

    if ((date instanceof Date) && dateTodayDiff(date) >= 0) {
      var rowRange = firstSheet.getRange(row, 1 + numHeaderCols, 1, numSubColumnPairs * 2);
      var rowData = rowRange.getValues()[0];
      var rowNotes = rowRange.getNotes()[0];
      for (var col = 0; col < rowData.length; col += 2) {
        var orig = rowData[col];
        var sub = rowData[col + 1];
        if (orig != '' && sub == '') {
          names.push(orig);
          dates.push(date);
          var r = rowNotes[col];
          if (r == '') {
            r = '{reason not specified. please add reason as a NOTE in gdoc}';
          }
          reasons.push(r);
        }
      }
    }
  }

  if (names.length > 0) {
    var htmlMsg = createHTMLTable(names, dates, reasons);
    htmlMsg += '

 *This message was automatically generated at ' + formatToday('h:mm a (M/d/yy)') + ' by ' + appName + ' [' + appUrl + '].';
    var subject = emailSubject + ' ' + formatToday('M/d') + ' Digest [' + names.length + ' needed]';
    MailApp.sendEmail(toEmail, subject, "Your email client doesn't support HTML :(", {
      htmlBody: htmlMsg
    });
    SpreadsheetApp.getActiveSpreadsheet().toast(names.length + ' people need subs', 'Emailed ' + toEmail, toastTimeout);
  } else {
    SpreadsheetApp.getActiveSpreadsheet().toast('No one needs a sub', 'Email not sent', toastTimeout);
  }
}

// returns a formatted html table
function createHTMLTable(nameList, dateList, reasonList) {
  var HTMLtable = wrapHTML('Sub Requests', 'h2', 'style="color:' + headerBgColor + '"');
  HTMLtable += '
';
  var hDate = wrapHTML('Date', 'th', 'width="15%" align="center"');
  var hName = wrapHTML('Name', 'th', 'width="15%" align="center"');
  var hMessage = wrapHTML('Message', 'th', 'width="70%" align="center"');
  HTMLtable += wrapHTML(hDate + hName + hMessage, 'tr', 'style="background-color:' + headerBgColor + ';color:white"');

  for (var i = 0; i < nameList.length; i++) {
    var bgColor = '';
    if (i % 2 != 0) bgColor = 'bgcolor="' + altColor + '" ';

    var cellStyle = 'style="border:1px solid ' + borderColor + '"';
    hDate = wrapHTML(formatDate(dateList[i], 'M/d (EEE)'), 'td', cellStyle);
    hName = wrapHTML(nameList[i], 'td', cellStyle);
    hMessage = wrapHTML(reasonList[i], 'td', cellStyle);
    HTMLtable += wrapHTML(hDate + hName + hMessage, 'tr', bgColor + cellStyle);
  }

  HTMLtable += "
";
  HTMLtable += '
Sign up: ';
  HTMLtable += wrapHTML('LINK', 'a', 'href="' + SpreadsheetApp.getActive().getUrl() + '"');
  return HTMLtable;
}

// displays app info
function about() {
  SpreadsheetApp.getActiveSpreadsheet().toast(appUrl, appName);
}

/*** Helper Functions ***/

function dateTodayDiff(d) {
  var today = new Date();
  var dateUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
  var todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
  return Math.floor((dateUtc - todayUtc) / (1000 * 60 * 60 * 24));
}

function wrapHTML(inside, html, options) {
  return '<' + html + ' ' + options + '>' + inside + '</' + html + '>';
}

function formatDate(d, f) {
  return Utilities.formatDate(d, Session.getTimeZone(), f);
}

function formatToday(f) {
  return formatDate(new Date(), f);
}

/*** App Info ***/
var appName = 'gDoc Sub System';
var appUrl = 'http://ultimateanu.github.io/gdoc-sub-system';<|endoftext|>