Separate belongsTo and hasOne tests and make the JSON testing more explicit

--- tests/batman/model/associations_test.coffee
+++ tests/batman/model/associations_test.coffee
@@ -1,16 +1,14 @@
 {TestStorageAdapter, AsyncTestStorageAdapter} = if typeof require isnt 'undefined' then require './model_helper' else window
 helpers = if typeof require is 'undefined' then window.viewHelpers else require '../view/view_helper'

-QUnit.module "Batman.Model One-To-One Associations"
+QUnit.module "belongsTo Associations"
   setup: ->
     class @App extends Batman.App
-    class @App.Store extends Batman.Model
-      @encode 'id', 'name'
-      @hasOne 'product'
-
     # Batman.currentApp exists under node and causes the adapters to explode
     Batman.currentApp = undefined

+    class @App.Store extends Batman.Model
+      @encode 'id', 'name'
     @storeAdapter = new AsyncTestStorageAdapter @App.Store
     @storeAdapter.storage =
       'stores1': {name: "Store One", id: 1}
@@ -20,66 +18,27 @@
     class @App.Product extends Batman.Model
       @encode 'id', 'name'
       @belongsTo 'store'
-
     @productAdapter = new AsyncTestStorageAdapter @App.Product
-    @productAdapter.storage =
-      'products1': {name: "Product One", id: 1, store_id: 1}
-      'products2': {name: "Product One", id: 1, store: {id:2, name:"JSON Store"}}
+    @productAdapter.storage = 'products1': {name: "Product One", id: 1, store_id: 1}
     @App.Product.persist @productAdapter

     @App.run()

-asyncTest "should work with model classes that haven't been loaded yet", ->
-  class @App.Blog extends Batman.Model
-    @encode 'id', 'name'
-    @hasOne 'customer'
-  blogAdapter = new AsyncTestStorageAdapter @App.Blog
-  blogAdapter.storage =
-    'blogs1': {name: "Blog One", id: 1}
-  @App.Blog.persist blogAdapter
-
-  setTimeout (=>
-    class @App.Customer extends Batman.Model
-      @encode 'id', 'name'
-    customerAdapter = new AsyncTestStorageAdapter @App.Customer
-    customerAdapter.storage =
-      'customer1': {name: "Customer One", id: 1, blog_id: 1}
-    @App.Customer.persist customerAdapter
-
-    @App.Blog.find 1, (err, blog) =>
-      customer = blog.get 'customer'
-      ok customer instanceof @App.Customer
-      equal customer.get('id'), 1
-      equal customer.get('name'), 'Customer One'
-      QUnit.start()
-  ), ASYNC_TEST_DELAY
+asyncTest "belongsTo yields the related model when toJSON is called", 1, ->
+  @App.Product.find 1, (err, product) =>
+    productJSON = product.toJSON()
+    storeJSON = product.get('store').toJSON()
+    # store will encode its product
+    delete storeJSON.product
+
+    deepEqual productJSON.store, storeJSON
+    QUnit.start()

 asyncTest "belongsTo associations are loaded via ID", 2, ->
   @App.Product.find 1, (err, product) =>
     store = product.get 'store'
     ok store instanceof @App.Store
     equal store.id, 1
-    QUnit.start()
-
-asyncTest "belongsTo yields the related model when toJSON is called", 1, ->
-  @App.Product.find 1, (err, product) =>
-    productJSON = product.toJSON()
-    storeJSON = product.get('store').toJSON()
-
-    # store will encode its product
-    delete storeJSON.product
-
-    deepEqual productJSON.store, storeJSON
-    QUnit.start()
-
-asyncTest "hasOne yields the related model when toJSON is called", 1, ->
-  @App.Store.find 1, (err, store) =>
-    storeJSON = store.toJSON()
-    productJSON = store.get('product').toJSON()
-    # store will encode its product
-    delete productJSON.store
-
-    deepEqual storeJSON.product, productJSON
     QUnit.start()

 asyncTest "belongsTo associations are saved", 5, ->
@@ -99,6 +58,66 @@
     deepEqual storedJSON.store, store.toJSON()
     QUnit.start()

+asyncTest "belongsTo associations render", 1, ->
+  @App.Product.find 1, (err, product) ->
+    source = ''
+    context = Batman(product: product)
+    helpers.render source, context, (node) =>
+      equal node[0].innerHTML, 'Store One'
+      QUnit.start()
+
+QUnit.module "hasOne Associations"
+  setup: ->
+    class @App extends Batman.App
+    # Batman.currentApp exists under node and causes the adapters to explode
+    Batman.currentApp = undefined
+
+    class @App.Store extends Batman.Model
+      @encode 'id', 'name'
+      @hasOne 'product'
+    @storeAdapter = new AsyncTestStorageAdapter @App.Store
+    @storeAdapter.storage =
+      'stores1': {name: "Store One", id: 1}
+      'stores2': {name: "Store Two", id: 2, product: {id:3, name:"JSON Product"}}
+    @App.Store.persist @storeAdapter
+
+    class @App.Product extends Batman.Model
+      @encode 'id', 'name'
+    @productAdapter = new AsyncTestStorageAdapter @App.Product
+    @productAdapter.storage = 'products1': {name: "Product One", id: 1, store_id: 1}
+    @App.Product.persist @productAdapter
+
+    @App.run()
+
+asyncTest "should work with model classes that haven't been loaded yet", ->
+  class @App.Blog extends Batman.Model
+    @encode 'id', 'name'
+    @hasOne 'customer'
+  blogAdapter = new AsyncTestStorageAdapter @App.Blog
+  blogAdapter.storage = 'blogs1': {name: "Blog One", id: 1}
+  @App.Blog.persist blogAdapter
+
+  setTimeout (=>
+    class @App.Customer extends Batman.Model
+      @encode 'id', 'name'
+    customerAdapter = new AsyncTestStorageAdapter @App.Customer
+    customerAdapter.storage =
+      'customer1': {name: "Customer One", id: 1, blog_id: 1}
+    @App.Customer.persist customerAdapter
+
+    @App.Blog.find 1, (err, blog) =>
+      customer = blog.get 'customer'
+      ok customer instanceof @App.Customer
+      equal customer.get('id'), 1
+      equal customer.get('name'), 'Customer One'
+      QUnit.start()
+  ), ASYNC_TEST_DELAY
+
+asyncTest "hasOne yields the related model when toJSON is called", 1, ->
+  @App.Store.find 1, (err, store) =>
+    deepEqual store.toJSON().product, @productAdapter.storage['products1']
+    QUnit.start()
+
 asyncTest "hasOne associations are loaded via ID", 2, ->
   @App.Store.find 1, (err, store) =>
     product = store.get 'product'
@@ -123,12 +142,12 @@
   store.save (err, record) =>
     equal storeSaveSpy.callCount, 1
     equal product.get('store_id'), record.id
+
     storedJSON = @storeAdapter.storage["stores#{record.id}"]
     deepEqual storedJSON, store.toJSON()
-
-    productJSON = store.get('product').toJSON()
-    delete productJSON.store
-    deepEqual storedJSON.product, productJSON
+    deepEqual storedJSON.product,
+      name: "Gizmo"
+      store_id: record.id
     QUnit.start()

 asyncTest "hasOne associations can be destroyed safely", 2, ->
@@ -139,20 +158,12 @@
       equal product._batman.attributes['store'], undefined
       QUnit.start()

-asyncTest "Models can save while related records are loading", 1, ->
+asyncTest "hasOne models can save while related records are loading", 1, ->
   @App.Store.find 1, (err, store) ->
     product = store.get 'product'
     product._batman.state = 'loading'
     store.save (err, savedStore) ->
       ok !err
-      QUnit.start()
-
-asyncTest "belongsTo associations render", 1, ->
-  @App.Product.find 1, (err, product) ->
-    source = ''
-    context = Batman(product: product)
-    helpers.render source, context, (node) =>
-      equal node[0].innerHTML, 'Store One'
       QUnit.start()

 asyncTest "hasOne associations render", 1, ->
@@ -163,17 +174,15 @@
       equal node[0].innerHTML, 'Product One'
       QUnit.start()

-QUnit.module "Batman.Model One-To-Many Associations"
+QUnit.module "hasMany Associations"
   setup: ->
     class @App extends Batman.App
-
-    class @App.Store extends Batman.Model
-      @encode 'id', 'name'
-      @hasMany 'products'
-
     # Batman.currentApp exists under node and causes the adapters to explode
     Batman.currentApp = undefined

+    class @App.Store extends Batman.Model
+      @encode 'id', 'name'
+      @hasMany 'products'
     @storeAdapter = new AsyncTestStorageAdapter @App.Store
     @storeAdapter.storage =
       'stores1': {name: "Store One", id: 1}
@@ -183,12 +192,18 @@
       @encode 'id', 'name', 'store_id'
       @belongsTo 'store'
       @hasMany 'productVariants'
-
     @productAdapter = new AsyncTestStorageAdapter @App.Product
     @productAdapter.storage =
       'products1': {name: "Product One", id: 1, store_id: 1}
       'products2': {name: "Product Two", id: 2, store_id: 1}
-      'products3': {name: "Product Three", id: 3, store_id: 1, productVariants: {productvariants5:{price:50,product_id:3},productvariants6:{price:60,product_id:3}}
+      'products3': {
+        name: "Product Three",
+        id: 3,
+        store_id: 1,
+        productVariants: {
+          productvariants5: {price:50,product_id:3},
+          productvariants6: {price:60,product_id:3}
+        }
       }
     @App.Product.persist @productAdapter

@@ -225,13 +240,9 @@
     equal product2.get('store_id'), record.id

     storedJSON = @storeAdapter.storage["stores#{record.id}"]
-    productsJSON = record.get('products').toJSON().map (product) ->
-      # Set.toJSON doesn't apply toJSON on its contents
-      productJSON = product.toJSON()
-      delete productJSON.store
-      productJSON
-    deepEqual storedJSON.products, productsJSON
-
+    deepEqual storedJSON.products,
+      [{name: "Gizmo", store_id: record.id},
+       {name: "Gadget", store_id: record.id}]
     QUnit.start()

 asyncTest "hasMany associations are saved via the child model", 2, ->


oauth: remove commented out code

oauth: remove commented out code

--- client/app/MainApp/login/loginview.coffee
+++ client/app/MainApp/login/loginview.coffee
@@ -117,9 +117,6 @@
                 title : "Thanks for linking your #{provider.capitalize()} account!"
             else
               @afterLoginCallback err, {account, replacementToken}
-
-    #mainController.on "ForeignAuthFailed", ->
-      #new KDNotificationView title : "Authorization failed."

   viewAppended:->


Fix syntax error in list selection

Fix syntax error in list selection

--- client/app/components/message-list.coffee
+++ client/app/components/message-list.coffee
@@ -378,7 +378,7 @@
             return false

         else if not applyToConversation
-            return {cout, messageIDs: selected, applyToConversation}
+            return {count, messageIDs: selected, applyToConversation}

         else
             conversationIDs = selected.map (id) =>


Add tests for _s3DocToMongo

Add tests for _s3DocToMongo

--- test/unit/coffee/DocArchiveManagerTests.coffee
+++ test/unit/coffee/DocArchiveManagerTests.coffee
@@ -212,3 +212,45 @@
 			@DocArchiveManager.unArchiveAllDocs @project_id, (err)=>
 				err.should.equal @error
 				done()
+
+	describe "_s3DocToMongoDoc", ->
+		describe "with the old schema", ->
+			it "should return the docs lines", (done) ->
+				@DocArchiveManager._s3DocToMongoDoc ["doc", "lines"], (error, doc) ->
+					expect(doc).to.deep.equal {
+						lines: ["doc", "lines"]
+					}
+					done()
+
+		describe "with the new schema", ->
+			it "should return the doc lines and ranges", (done) ->
+				@RangeManager.jsonRangesToMongo = sinon.stub().returns {"mongo": "ranges"}
+				@DocArchiveManager._s3DocToMongoDoc {
+					lines: ["doc", "lines"]
+					ranges: {"json": "ranges"}
+					schema_v: 1
+				}, (error, doc) ->
+					expect(doc).to.deep.equal {
+						lines: ["doc", "lines"]
+						ranges: {"mongo": "ranges"}
+					}
+					done()
+
+			it "should return just the doc lines when there are no ranges", (done) ->
+				@DocArchiveManager._s3DocToMongoDoc {
+					lines: ["doc", "lines"]
+					schema_v: 1
+				}, (error, doc) ->
+					expect(doc).to.deep.equal {
+						lines: ["doc", "lines"]
+					}
+					done()
+
+		describe "with an unrecognised schema", ->
+			it "should return an error", (done) ->
+				@DocArchiveManager._s3DocToMongoDoc {
+					schema_v: 2
+				}, (error, doc) ->
+					expect(error).to.exist
+					done()
+


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -72,8 +72,9 @@
       self.trigger "draw", canvas

       if I.showFPS
+        canvas.font("bold 9pt consolas, 'Courier New', 'andale mono', 'lucida console', monospace")
         canvas.fillColor("#FFF")
-        canvas.fillText("fps: " + framerate.fps, 5, 12)
+        canvas.fillText("fps: " + framerate.fps, 6, 18)

       framerate.rendered()


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -3,8 +3,7 @@
 velocity so that it will approach another object.

 This method relies on both objects having `position` methods.
-GameObject
-provides this method by default.
+GameObject provides this method by default.

 player = GameObject


update teaspoon paper test for author groups

--- test/javascripts/models/paper_test.js.coffee
+++ test/javascripts/models/paper_test.js.coffee
@@ -1,7 +1,7 @@
 #= require test_helper

 moduleForModel 'paper', 'Unit: Paper Model',
-  needs: ['model:author', 'model:user', 'model:figure', 'model:journal', 'model:supportingInformationFile', 'model:phase', 'model:task', 'model:comment', 'model:litePaper']
+  needs: ['model:author', 'model:user', 'model:figure', 'model:journal', 'model:supportingInformationFile', 'model:phase', 'model:task', 'model:comment', 'model:litePaper', 'model:authorGroup']

 test 'displayTitle displays short title if title is missing', ->
   shortTitle = 'test short title'


8ball

--- scripts/example.coffee
+++ scripts/example.coffee
@@ -46,6 +46,5 @@
    "Very doubtful",
    ]

-   module.exports = (robot) ->
    robot.respond /(eightball|8ball)(.*)/i, (msg) ->
      msg.reply msg.random ball


Remove useless debug log

Remove useless debug log

--- scripts/goodbye.coffee
+++ scripts/goodbye.coffee
@@ -39,7 +39,6 @@
               "Hang on a sec..."

     git.update goodbye, greyhound, name, dod, gitOpts, (err) ->
-      console.log "result: #{err}"
       unless err?
         res.reply "#{name} moved to the Rainbow Bridge 😢"
       else


try another bootstrap strategry

try another bootstrap strategry

--- scripts/src/app.coffee
+++ scripts/src/app.coffee
@@ -1,4 +1,4 @@
-angular.element(document).ready(->
+angular.element(document).on('ready page:load', ->
         angular.module('kanban', ['kanban.controllers'])

         angular.module('unisson_kanban', ['kanban', 'ui.router', 'ngAnimate', 'restangular'])
@@ -40,4 +40,6 @@
         ])

         angular.bootstrap(document, ['unisson_kanban'])
+
+        return true
 )


modified

modified

--- src/jisilu.coffee
+++ src/jisilu.coffee
@@ -33,7 +33,7 @@
       json: true

     request options, (err,res, body)->
-      callback body unless err?
+      callback err, body # unless err?
       #jj = (each for each in body.rows when each.id is "150152")
       #console.log jj[0].cell


Sim: Better unique IPs.

Sim: Better unique IPs.

--- server/commands.coffee
+++ server/commands.coffee
@@ -230,10 +230,10 @@
   sockets = @users.get(username)
   ips = sockets.map (socket) ->
     socket = socket.socket
-    tempIPs = (socket.headers?['x-forwarded-for'] ? '').split(/\s*,\s*/)
+    tempIPs = (socket.headers?['x-forwarded-for'] ? '').split(',')
     tempIPs.unshift(socket.remoteAddress)
-    tempIPs.filter((ip) -> ip).map((ip) -> ip.trim()).join(',')
-  ips = _.chain(ips).compact().unique().value()
+    tempIPs.filter((ip) -> ip).map((ip) -> ip.trim())
+  ips = _.chain(ips).flatten().compact().unique().value()
   user.announce('success', "#{username}'s IP addresses: #{ips.join(', ')}")
   next()


Removed console.logging in leaving game

Removed console.logging in leaving game

--- server/src/Game.coffee
+++ server/src/Game.coffee
@@ -254,8 +254,6 @@
       return newPlayerIndex

   removeClient: (clientid) ->
-    console.log "Removing from client"
-    console.log @players.length
     if @players.length == 2
       @restartGame()
     else #now we need to update players and playersocketIds


Remove {KeyboardEvent} references

Remove {KeyboardEvent} references

--- src/keymap.coffee
+++ src/keymap.coffee
@@ -162,7 +162,7 @@
     @keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.source isnt source

   # Public: Dispatch a custom event associated with the matching key binding for
-  # the given {KeyboardEvent} if one can be found.
+  # the given `KeyboardEvent` if one can be found.
   #
   # If a matching binding is found on the event's target or one of its
   # ancestors, `.preventDefault()` is called on the keyboard event and the
@@ -380,7 +380,7 @@

   # Public: Translate a keydown event to a keystroke string.
   #
-  # event - A {KeyboardEvent} of type 'keydown'
+  # event - A `KeyboardEvent` of type 'keydown'
   #
   # Returns a {String} describing the keystroke.
   keystrokeForKeyboardEvent: (event) ->


Disable nagle on server side connections.

--- src/listener.iced
+++ src/listener.iced
@@ -54,6 +54,9 @@
   # Feel free to change this for your needs (if you want to wrap a connection
   # with something else)...
   make_new_transport : (c) ->
+    # Disable Nagle by default
+    c.setNoDelay true unless @do_tcp_delay
+
     x = new @TransportClass
       tcp_stream : c
       host : c.remoteAddress


remove schema support

remove schema support

--- src/loader.coffee
+++ src/loader.coffee
@@ -49,7 +49,6 @@
   module.code = module_code(module, answer)
   plv8_exports[currentModule] = module
   res = oldcompile.apply(this, arguments)
-  module.schema = @exports.plv8_schema
   for k,v of @exports when v.plv8_signature?
       sig = v.plv8_signature
       params = parse_params(v.toString())[1..-1]
@@ -79,7 +78,7 @@
   # todo validate params & signature
   params = (x.join(" ") for x in info.params).join(', ')
   pass_params = (x for [x,_] in info.params).join(', ')
-  declaration = "#{mod.schema}.#{name}(#{params})"
+  declaration = "#{name}(#{params})"
   immutable = (info.immutable && 'IMMUTABLE') || ''

   """
@@ -110,9 +109,6 @@
   fns = []
   for fl, info of plv8_exports
     console.log("Compile module #{fl}...")
-    if info.schema
-      console.log("in schema", info.schema)
-      fns.push("CREATE SCHEMA IF NOT EXISTS #{info.schema};")
     deps.push(info.code)
     for k,v of info.exports
       console.log(" * fn #{k}...")


Overrides the handlebars watch task with the  correct handlebars task. Fixes #1

--- config/application.coffee
+++ config/application.coffee
@@ -40,3 +40,7 @@
     compile:
       files:
         "generated/template/handlebars.js": "<%= files.template.handlebars %>"
+
+  watch:
+    handlebars:
+      tasks: ["ember_handlebars:compile"]


Adding model needs for the test

Adding model needs for the test

--- tests/unit/controllers/filters/new-test.coffee
+++ tests/unit/controllers/filters/new-test.coffee
@@ -2,7 +2,38 @@

 moduleFor 'controller:filters/new', {
   # Specify the other units that are required for this test.
-  # needs: ['controller:foo']
+  needs: ['model:group','model:identifier',  'model:group-list', 'model:group-characteristic-component',
+        'model:human-name',
+        'model:contact-point',
+        'model:address',
+        'model:codeable-concept',
+        'model:attachment',
+        'model:patient-contact-component',
+        'model:patient-animal-component',
+        'model:patient-communication-component',
+        'model:patient-link-component',
+        'model:period',
+        'model:coding',
+        'model:encounter',
+        'model:encounter-participant-component',
+        'model:quantity',
+        'model:encounter-hospitalization-component',
+        'model:encounter-location-component',
+        'model:condition',
+        'model:condition-stage-component',
+        'model:condition-evidence-component',
+        'model:condition-location-component',
+        'model:condition-due-to-component',
+        'model:condition-occurred-following-component',
+        'model:medication-statement',
+        'model:medication-statement-dosage-component',
+        'model:notification-count',
+        'model:range',
+        'model:appointment',
+        'model:appointment-participant-component',
+        'model:reference',
+        'model:patient'
+      ]
 }

 # Replace this with your real tests.
@@ -12,4 +43,6 @@

 test 'it updates hasFilterPane appropriately', ->
   controller = @subject()
+  Ember.run ->
+    controller.set('model', controller.get('store').createRecord('group'))
   ok false == controller.get('hasFilterPane')


Indented code

Indented code

--- client/test/lib/helpers/idehelpers.coffee
+++ client/test/lib/helpers/idehelpers.coffee
@@ -168,21 +168,21 @@

   compressFileFolder: (browser, user, type, fileFolderName, compressType) ->

-    webPath         = '/home/' + user.username + '/Web'
-    name = fileFolderName
+    webPath     = '/home/' + user.username + '/Web'
+    name        = fileFolderName

     if type is 'folder'
-      webPath = '/home/' + user.username
-      name = fileFolderName.name
+      webPath   = '/home/' + user.username
+      name      = fileFolderName.name

-    fileFolderSelector    = "span[title='" + webPath + '/' + name + "']"
-    submenuSelector = "li.as-#{compressType}"
-    extension = '.zip'
+    fileFolderSelector  = "span[title='" + webPath + '/' + name + "']"
+    submenuSelector     = "li.as-#{compressType}"
+    extension           = '.zip'

     if compressType is 'targz'
       extension = '.tar.gz'

-    newFile         = "span[title='" + webPath + '/' + name + extension + "']"
+    newFile = "span[title='" + webPath + '/' + name + extension + "']"

     browser
       .waitForElementPresent     fileFolderSelector, 20000


don't overwrite selected

don't overwrite selected

--- js/app.coffee
+++ js/app.coffee
@@ -84,14 +84,9 @@
       # TODO: extract the billing portion from the project/users portion
       @org() && @org().loaded()

-
-    # Tracks what page we're on (for pages we care about)
-    @selected = ko.observable({})
-
     @navbar = ko.observable(new CI.inner.Navbar(@selected, @build))

     @billing = ko.observable(new CI.inner.Billing)
-

     @dashboard_ready = @komp =>
       @projects_have_been_loaded() and @builds_have_been_loaded()


Document Marker API

Document Marker API

--- src/marker.coffee
+++ src/marker.coffee
@@ -9,12 +9,60 @@

 OptionKeys = ['reversed', 'tailed', 'invalidate', 'persistent']

+# Public: Reprents a buffer annotation that remains logically stationary even
+# as the buffer changes. This is used to represent cursors, folds, snippet
+# targets, misspelled words, and anything else that needs to track a logical
+# location in the buffer over time.
+#
+# Head and Tail:
+# Markers always have a *head* and sometimes have a *tail*. If you think of a
+# marker as an editor selection, the head is the part that's stationary and the
+# tail is the part that moves when the mouse is moved. A marker without a tail
+# always reports an empty range at the head position. A marker with a head position
+# greater than the tail is in a "normal" orientation. If the head precedes the
+# tail the marker is in a "reversed" orientation.
+#
+# Validity:
+# Markers are considered *valid* when they are first created. Depending on the
+# invalidation strategy you choose, certain changes to the buffer can cause a
+# marker to become invalid, for example if the text surrounding the marker is
+# deleted.
+#
+# Change events:
+# When markers change in position for any reason, the emit a 'changed' event with
+# the following properties:
+#
+# * oldHeadPosition:
+#     A {Point} representing the former head position
+# * newHeadPosition:
+#     A {Point} representing the new head position
+# * oldTailPosition:
+#     A {Point} representing the former tail position
+# * newTailPosition:
+#     A {Point} representing the new tail position
+# * wasValid:
+#     A {Boolean} indicating whether the marker was valid before the change
+# * isValid:
+#     A {Boolean} indicating whether the marker is now valid
+# * hadTail:
+#     A {Boolean} indicating whether the marker had a tail before the change
+# * hasTail:
+#     A {Boolean} indicating whether the marker now has a tail
+# * oldProperties:
+#     An {Object} containing the marker's custom properties before the change.
+# * newProperties:
+#     An {Object} containing the marker's custom properties after the change.
+# * textChanged:
+#     A {Boolean} indicating whether this change was caused by a textual change
+#     to the buffer or whether the marker was manipulated directly via its public
+#     API.
 module.exports =
 class Marker
   Emitter.includeInto(this)
   Delegator.includeInto(this)
   Serializable.includeInto(this)

+  # Private:
   @extractParams: (inputParams) ->
     outputParams = {}
     if inputParams?
@@ -24,6 +72,7 @@
       outputParams.properties = properties if size(properties) > 0
     outputParams

+  # Private:
   @handleDeprecatedParams: (params) ->
     if params.isReversed?
       params.reversed = params.isReversed
@@ -44,6 +93,7 @@
   @delegatesMethods 'containsPoint', 'containsRange', 'intersectsRow', toProperty: 'range'
   @delegatesMethods 'clipPosition', 'clipRange', toProperty: 'manager'

+  # Private:
   constructor: (params) ->
     {@manager, @id, @range, @tailed, @reversed} = params
     {@valid, @invalidate, @persistent, @properties} = params
@@ -57,32 +107,52 @@
     Object.freeze(@properties)
     @updateIntervals()

+  # Private: Used by {Serializable} during serialization.
   serializeParams: ->
     range = @range.serialize()
     {@id, range, @tailed, @reversed, @valid, @invalidate, @persistent, @properties}

+  # Private: Used by {Serializable} during deserialization.
   deserializeParams: (state) ->
     state.range = Range.deserialize(state.range)
     state

+  # Public: Returns the current {Range} of the marker. The range is immutable.
   getRange: ->
     @range

-  setRange: (range, params) ->
-    params = @extractParams(params)
+  # Public: Sets the range of the marker.
+  #
+  # * range:
+  #     A {Range} or range-compatible {Array}. The range will be clipped before.
+  #     it is assigned.
+  # * properties:
+  #     An optional hash of properties to associate with the marker. The 'reversed'
+  #     property is reserved and if true, will cause the marker to be in a reversed
+  #     orientation.
+  setRange: (range, properties) ->
+    params = @extractParams(properties)
     params.tailed = true
     params.range = @clipRange(Range.fromObject(range, true))
     @update(params)

+  # Public: Returns a {Point} representing the marker's current head position.
   getHeadPosition: ->
     if @reversed
       @range.start
     else
       @range.end

-  setHeadPosition: (position, params) ->
+  # Public: Sets the head position of the marker.
+  #
+  # * position:
+  #     A {Point} or point-compatible {Array}. The position will be clipped
+  #     before it is assigned.
+  # * properties:
+  #     An optional hash of properties to associate with the marker.
+  setHeadPosition: (position, properties) ->
     position = @clipPosition(Point.fromObject(position, true))
-    params = @extractParams(params)
+    params = @extractParams(properties)

     if @hasTail()
       if @isReversed()
@@ -103,6 +173,8 @@

     @update(params)

+  # Public: Returns a {Point} representing the marker's current tail position.
+  # If the marker has no tail, the head position will be returned instead.
   getTailPosition: ->
     if @hasTail()
       if @reversed
@@ -112,9 +184,17 @@
     else
       @getHeadPosition()

-  setTailPosition: (position, params) ->
+  # Public: Sets the head position of the marker. If the marker doesn't have a
+  # tail, it will after calling this method.
+  #
+  # * position:
+  #     A {Point} or point-compatible {Array}. The position will be clipped
+  #     before it is assigned.
+  # * properties:
+  #     An optional hash of properties to associate with the marker.
+  setTailPosition: (position, properties) ->
     position = @clipPosition(Point.fromObject(position, true))
-    params = @extractParams(params)
+    params = @extractParams(properties)
     params.tailed = true

     if @reversed
@@ -132,47 +212,68 @@

     @update(params)

+  # Public: Returns a {Point} representing the start position of the marker,
+  # which could be the head or tail position, depending on its orientation.
   getStartPosition: ->
     if @reversed
       @getHeadPosition()
     else
       @getTailPosition()

+  # Public: Returns a {Point} representing the end position of the marker,
+  # which could be the head or tail position, depending on its orientation.
   getEndPosition: ->
     if @reversed
       @getTailPosition()
     else
       @getHeadPosition()

-  clearTail: (params) ->
-    params = @extractParams(params)
+  # Public: Removes the marker's tail. After calling the marker's head position
+  # will be reported as its current tail position until the tail is planted
+  # again.
+  #
+  # * properties: An optional hash of properties to associate with the marker.
+  clearTail: (properties) ->
+    params = @extractParams(properties)
     params.tailed = false
     headPosition = @getHeadPosition()
     params.range = new Range(headPosition, headPosition)
     @update(params)

-  plantTail: (params) ->
-    params = @extractParams(params)
+  # Public: Plants the marker's tail at the current head position. After calling
+  # the marker's tail position will be its head position at the time of the
+  # call, regardless of where the marker's head is moved.
+  #
+  # * properties: An optional hash of properties to associate with the marker.
+  plantTail: (properties) ->
+    params = @extractParams(properties)
     unless @hasTail()
       params.tailed = true
       params.range = new Range(@getHeadPosition(), @getHeadPosition())
     @update(params)

+  # Public: Returns a {Boolean} indicating whether the head precedes the tail.
   isReversed: ->
     @tailed and @reversed

+  # Public: Returns a {Boolean} indicating whether the marker has a tail.
   hasTail: ->
     @tailed

+  # Public:
   isValid: ->
     not @isDestroyed() and @valid

+  # Public:
   isDestroyed: ->
     @destroyed

+  # Public: Returns a {Boolean} indicating whether this marker is equivalent to
+  # another marker, meaning they have the same range and options.
   isEqual: (other) ->
     isEqual(@toParams(true), other.toParams(true))

+  # Public:
   getInvalidationStrategy: ->
     @invalidate

@@ -184,25 +285,35 @@
   setAttributes: (args...) ->
     @setProperties(args...)

+  # Public: Returns an {Object} containing any custom properties associated with
+  # the marker.
   getProperties: ->
     @properties

+  # Public: Merges an {Object} containing new properties into the marker's
+  # existing properties.
   setProperties: (properties) ->
     @update(properties: extend({}, @getProperties(), properties))

+  # Public: Creates and returns a new {Marker} with the same properties as this
+  # marker.
   copy: (params) ->
     @manager.createMarker(extend(@toParams(), @extractParams(params)))

+  # Public: Destroys the marker, causing it to emit the 'destroyed' event. Once
+  # destroyed, a marker cannot be restored by undo/redo operations.
   destroy: ->
     @destroyed = true
     @manager.removeMarker(@id)
     @emit 'destroyed'

+  # Private:
   extractParams: (params) ->
     params = @constructor.extractParams(params)
     params.properties = extend({}, @properties, params.properties) if params.properties?
     params

+  # Public: Compares this marker to another based on their ranges.
   compare: (other) ->
     @range.compare(other.range)

@@ -210,11 +321,15 @@
   matchesAttributes: (args...) ->
     @matchesParams(args...)

+  # Returns whether this marker matches the given parameters. The parameters
+  # are the same as {MarkerManager::findMarkers}.
   matchesParams: (params) ->
     for key, value of params
       return false unless @matchesParam(key, value)
     true

+  # Returns whether this marker matches the given parameter name and value.
+  # The parameters are the same as {MarkerManager::findMarkers}.
   matchesParam: (key, value) ->
     switch key
       when 'startPosition'
@@ -236,11 +351,13 @@
       else
         isEqual(@properties[key], value)

+  # Private:
   toParams: (omitId) ->
     params = {@range, @reversed, @tailed, @invalidate, @persistent, @properties}
     params.id = @id unless omitId
     params

+  # Private:
   update: (params) ->
     if patch = @buildPatch(params)
       @manager.recordMarkerPatch(patch)
@@ -249,6 +366,8 @@
     else
       false

+  # Private: Adjusts the marker's start and end positions and possibly its
+  # validity based on the given {BufferPatch}.
   handleBufferChange: (patch) ->
     {oldRange, newRange} = patch
     rowDelta = newRange.end.row - oldRange.end.row
@@ -290,6 +409,7 @@
     if markerPatch = @buildPatch({valid, range: newMarkerRange})
       patch.addMarkerPatch(markerPatch)

+  # Private:
   buildPatch: (newParams) ->
     oldParams = {}
     for name, value of newParams
@@ -301,6 +421,7 @@
     if size(newParams)
       new MarkerPatch(@id, oldParams, newParams)

+  # Private:
   applyPatch: (patch, textChanged=false) ->
     oldHeadPosition = @getHeadPosition()
     oldTailPosition = @getTailPosition()
@@ -346,5 +467,7 @@
     }
     true

+  # Private: Updates the interval index on the marker manager with the marker's
+  # current range.
   updateIntervals: ->
     @manager.intervals.update(@id, @range.start, @range.end)


:fire: dead code for handling deferred marker change events

--- src/marker.coffee
+++ src/marker.coffee
@@ -48,7 +48,6 @@
   oldTailBufferPosition: null
   oldTailScreenPosition: null
   wasValid: true
-  deferredChangeEvents: null

   ###
   Section: Construction and Destruction
@@ -332,11 +331,11 @@
     newTailScreenPosition = @getTailScreenPosition()
     isValid = @isValid()

-    return if _.isEqual(isValid, @wasValid) and
-      _.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and
-      _.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and
-      _.isEqual(newTailBufferPosition, @oldTailBufferPosition) and
-      _.isEqual(newTailScreenPosition, @oldTailScreenPosition)
+    return if isValid is @wasValid and
+      newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
+      newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
+      newTailBufferPosition.isEqual(@oldTailBufferPosition) and
+      newTailScreenPosition.isEqual(@oldTailScreenPosition)

     changeEvent = {
       @oldHeadScreenPosition, newHeadScreenPosition,
@@ -347,11 +346,8 @@
       isValid
     }

-    if @deferredChangeEvents?
-      @deferredChangeEvents.push(changeEvent)
-    else
-      @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
-      @emitter.emit 'did-change', changeEvent
+    @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
+    @emitter.emit 'did-change', changeEvent

     @oldHeadBufferPosition = newHeadBufferPosition
     @oldHeadScreenPosition = newHeadScreenPosition


Update redis env vars to match redis-brain's.

--- src/markov.coffee
+++ src/markov.coffee
@@ -31,10 +31,10 @@
 module.exports = (robot) ->

   # Configure redis the same way that redis-brain does.
-  info = Url.parse process.env.REDIS_URL or
-    process.env.REDISTOGO_URL or
+  info = Url.parse process.env.REDISTOGO_URL or
     process.env.REDISCLOUD_URL or
     process.env.BOXEN_REDIS_URL or
+    process.env.REDIS_URL or
     'redis://localhost:6379'
   client = Redis.createClient(info.port, info.hostname)


bits: Add bounds checking

bits: Add bounds checking

--- bits.coffee
+++ bits.coffee
@@ -278,7 +278,10 @@
   get_remaining_bytes: ->
     if @bit_index isnt 0
       console.warn "warning: bits.get_remaining_bytes: bit_index is not 0"
-    return @buf.length - @byte_index
+    remainingLen = @buf.length - @byte_index
+    if remainingLen < 0
+      remainingLen = 0
+    return remainingLen

   remaining_buffer: ->
     if @bit_index isnt 0
@@ -322,8 +325,11 @@
     else
       Bits.parse_bits_uint @buf, byteOffset * 8, 8

-  last_get_byte_at: (byteOffset) ->
-    return @buf[@buf.length - 1 - byteOffset]
+  last_get_byte_at: (offsetFromEnd) ->
+    offsetFromStart = @buf.length - 1 - offsetFromEnd
+    if offsetFromStart < 0
+      throw new Error "error: last_get_byte_at: index out of range"
+    return @buf[offsetFromStart]

   remove_trailing_bytes: (numBytes) ->
     if @buf.length < numBytes


Encapsulate fetching paginated responses.

--- src/models.coffee
+++ src/models.coffee
@@ -136,6 +136,29 @@

         isSet: ->
             return not @isEmpty()
+
+        fetchPaginated: (options) ->
+            deferred = $.Deferred()
+            xhr = null
+            cancelled = false
+            _.defaults options,
+                success: =>
+                    if cancelled == true
+                        return
+                    options.onPageComplete()
+                    if cancelled == true
+                        return
+                    hasNext = @fetchNext options
+                    if hasNext == false
+                        deferred.resolve @
+                    else
+                        xhr = hasNext
+            xhr = @fetch options
+            options.cancelToken.addHandler ->
+                xhr.abort()
+                cancelled = true
+                deferred.fail()
+            return deferred

         fetchNext: (options) ->
             if @fetchState? and not @fetchState.next


Handle collection filtering in url(), not fetch()

--- src/models.coffee
+++ src/models.coffee
@@ -53,12 +53,11 @@
             @
         clearFilters: ->
             @filters = {}
-        fetch: (options) ->
-            data = _.clone @filters
-            if options.data?
-                data = _.extend data, options.data
-            options.data = data
-            super options
+        url: ->
+            obj = new @model
+            uri = URI "#{BACKEND_BASE}/#{obj.resourceName}/"
+            uri.search @filters
+            return uri.toString()

     class RESTFrameworkCollection extends FilterableCollection
         parse: (resp, options) ->
@@ -137,10 +136,6 @@
                 if options.setComparator
                     @setDefaultComparator()
             super options
-
-        url: ->
-            obj = new @model
-            return "#{BACKEND_BASE}/#{obj.resourceName}/"

         isSet: ->
             return not @isEmpty()


Check modified time properly.

Check modified time properly.

--- src/module.coffee
+++ src/module.coffee
@@ -71,7 +71,7 @@
     fs.stat @absolutePath, (err, stat) =>
       return callback err if err?

-      if @mtime? and @mtime <= stat.mtime
+      if @mtime? and @mtime >= stat.mtime
         return callback()

       @mtime = stat.mtime


Player should expose way of checking distance fallen.

--- src/player.coffee
+++ src/player.coffee
@@ -48,6 +48,10 @@

     # Public
     # ------
+
+    distanceFallen: ->
+      return 0 unless @_fallingPoint?
+      @sprite.y - @_fallingPoint.y

     update: ->
       # First.
@@ -146,6 +150,8 @@
       @nextDirection = null
       @nextState = null

+      @_fallingPoint = null
+
     # Change
     # ------

@@ -167,6 +173,11 @@

       @debug 'state', @nextState
       @debug 'jump:start', @physics.position if @nextState is 'rising'
+
+      if @nextState is 'falling'
+        @_fallingPoint ?= @sprite.position.clone()
+      else if @nextState is 'still' and @_fallingPoint?
+        @_fallingPoint = null

       @state = @nextState


Whoops, a -> where we needed a => to bind 'this'

--- src/player.coffee
+++ src/player.coffee
@@ -243,7 +243,7 @@

 						skill.askParams @creature

-					.then (params) ->
+					.then (params) =>
 						skill.use @creature, params

 			when 'test-dir'


Rework Player x movement, refactoring included.

- Factor out methods for changing current state and animation, for
  easier debugging and control. Includes greatly reworking the animation
  updating, especially around guarding.

- Rework main update process by refactoring with new nextState property,
  so actual state and animation updates happen at the end.

- Use and inline strings for state and action constants, for easier
  debugging.

- Add debug logging.

- Fix start animations not happening during _turn with _justTurned.

- Fix how nextAction updated without user input; start should not be
  possible without user input.

--- src/player.coffee
+++ src/player.coffee
@@ -7,24 +7,11 @@
   'phaser'
 ], (Phaser) ->

-  Action =
-    None: 0
-    Stop: 1
-    Start: 2
-
   Direction =
     Left: -1
     Right: 1

-  State =
-    Still: 0
-    Running: 1
-
   class Player
-
-    @Action: Action
-    @Direction: Direction
-    @State: State

     constructor: (origin, game) ->
       @sprite = game.add.sprite origin.x, origin.y, 'player', 17
@@ -42,17 +29,28 @@

       @cursors = game.input.keyboard.createCursorKeys()

+      @debugging = on
+
+      # Readonly.
       @animation = null
-      @state = State.Still
-      @nextAction = Action.None
+      @state = 'still' # still, running
+      @nextAction = 'none' # none, start, stop
+      @nextState = null

-    inMidAir: -> no
+    debug: (label, value) -> console.log "player:#{label}", value if @debugging
+
+    isInMidAir: -> no
+    isRunning: (direction) -> direction is @direction and @velocity.x isnt 0

     update: ->
+      # Reset any state.
+      @nextAction = 'none'
+      @nextState = 'running'
+
       # Horizontal

-      @nextAction = Action.None
-      @acceleration.x = 0
+      # - Revert to still. (Our acceleration updates funny.)
+      @acceleration.x = 0 unless @isInMidAir()

       ###
       - Basically handle switching direction, and running or being still
@@ -68,11 +66,12 @@
         @_turn Direction.Right
         @_run Direction.Right

-      else if not @inMidAir()
-        @nextAction = if @velocity.x is 0 then Action.Start else Action.Stop
-        if @velocity.x is 0
-          @state = State.Still
-          @animations.frame = 17
+      else unless @isInMidAir()
+        if @velocity.x isnt 0 then @nextAction = 'stop'
+        else @nextState = 'still'
+
+      @_updateAnimations()
+      @_changeState @nextState

     _initAnimations: ->
       @animations.add 'run', [0..11], 24, on
@@ -90,29 +89,63 @@
       w = @sprite.width
       @_yOffset = h / 4
       @physics.setSize (w / 2), (h / 2), @_xOffset(), @_yOffset
+      @_justTurned = no # Because velocity won't be 0 when just turning.
+
+    _changeAnimation: (nameOrFrame, interrupt = yes) ->
+      return if interrupt is no and @animation?.isPlaying
+
+      if _.isNumber(nameOrFrame)
+        frame = nameOrFrame
+        return if @animations.frame is frame
+        @animations.frame = frame
+        @animation = null
+
+      else
+        name = nameOrFrame
+        return if @animation?.name is name
+        animation = @animations.play name
+        @animation = animation
+
+      @debug 'animation', nameOrFrame
+
+    _changeState: (state) ->
+      return if state is @state
+
+      @state = state
+      @debug 'state', state

     _run: (direction) ->
+      @nextAction = 'start' if @velocity.x is 0
+
       factor = 200
-      if @inMidAir()
+      if @isInMidAir()
         # TODO
-      else if @state isnt State.Running
-        @state = State.Running
-      @animations.play 'run' unless @animation?.isPlaying
+      else @nextState = 'running'
       @acceleration.x = factor * direction

-    _turn: (direction) -> # TODO: Replace with animation.
-      return if direction is @direction
-      shouldStop = @velocity.x isnt 0 and @nextAction isnt Action.Stop and @direction isnt direction
-      if shouldStop
-        @nextAction = Action.Stop
-        @animation = @animations.play 'stop' unless @inMidAir()
-      else if not @animation?.isPlaying
-        @nextAction = Action.Start
-        @animation = @animations.play 'start' unless @inMidAir()
+    _turn: (direction) ->
+      return if @isRunning(direction)
+
+      if @_justTurned
+        @nextAction = 'start'
+        @_justTurned = no
+      else if @velocity.x isnt 0 and @nextAction isnt 'stop'
+        @nextAction = 'stop'
+        @_justTurned = yes
+
       @direction = direction
       @sprite.scale.x = direction
       @physics.offset = new Phaser.Point @_xOffset(direction), @_yOffset

+    _updateAnimations: ->
+      unless @isInMidAir() or @nextAction is 'none'
+        @_changeAnimation @nextAction
+        return
+
+      switch @nextState
+        when 'running' then @_changeAnimation 'run', no
+        when 'still' then @_changeAnimation 17, no
+
     _xOffset: (direction = @direction) -> direction * 10

   Player


Increase jump enough for Player to reliably clear most Platforms.

--- src/player.coffee
+++ src/player.coffee
@@ -126,7 +126,7 @@
       @_yOffset = h / 4
       @physics.setSize (w / 2), (h / 2), @_xOffset(), @_yOffset

-      @jumpAcceleration = -4000 # A burst of energy on launch.
+      @jumpAcceleration = -4250 # A burst of energy on launch.
       @jumpMaxDuration = 500
       @jumpVelocityFactor = 1 / 4


allow output:source

allow output:source

--- src/plugin.coffee
+++ src/plugin.coffee
@@ -98,8 +98,11 @@
       }
     }

+    parser = PEG.buildParser grammar, options
+    return parser  if options.output is 'source'
+
     # FIXME pegjs should throw an exception if startRule is not defined
-    {parse} = PEG.buildParser grammar, options
+    {SyntaxError, parse} = parser

     fun = (input) ->
       parse input, {startRule}
@@ -107,6 +110,7 @@
       grammar
       options
     }
+    fun.SyntaxError = SyntaxError
     fun

   mod._ = {


Minor Player refactoring.

Minor Player refactoring.

--- src/player.coffee
+++ src/player.coffee
@@ -28,15 +28,12 @@
       @animations = @sprite.animations
       @_initAnimations()

+      @cursors = game.input.keyboard.createCursorKeys()
+      @direction = Direction.Right
+
       game.physics.arcade.enable @sprite
-      @physics = @sprite.body
-      @velocity = @physics.velocity
-      @acceleration = @physics.acceleration
-      @direction = Direction.Right
       @gravity = game.physics.arcade.gravity
       @_initPhysics()
-
-      @cursors = game.input.keyboard.createCursorKeys()

       @debugging = on
       @debugTextItems = {}
@@ -97,6 +94,10 @@
       @animations.add 'end', [34...53], 12

     _initPhysics: ->
+      @physics = @sprite.body
+      @velocity = @physics.velocity
+      @acceleration = @physics.acceleration
+
       @physics.collideWorldBounds = on

       @physics.drag.x = 1500
@@ -110,6 +111,7 @@
       @jumpMaxDuration = 500
       @jumpVelocityFactor = 1 / 4

+      @airFrictionRatio = 1 / 20
       @runAcceleration = 300

       @maxVelocity =
@@ -170,7 +172,7 @@

       if @isInMidAir()
         # No force, just air friction.
-        @acceleration.x = @runAcceleration / -20 * direction
+        @acceleration.x = @runAcceleration * @airFrictionRatio * -direction
         return

       else if isStill or landed()


Only take models from the parent scope if they are not already defined on the iterated scope (iterating a model on the same key as one in the parent view, for example).

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -403,7 +403,7 @@

         if not @iterated[index]?
           for key, model of @view.models
-            data[key] = model
+            data[key] ?= model

           previous = if @iterated.length
             @iterated[@iterated.length - 1].els[0]


Fix missed comma in unbindEvent call.

Fix missed comma in unbindEvent call.

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -100,7 +100,7 @@
       Rivets.config.adapter.unsubscribe @model, @keypath, @sync

       if @isBidirectional()
-        unbindEvent @el 'change', @publish
+        unbindEvent @el, 'change', @publish

     if @options.dependencies?.length
       for keypath in @options.dependencies


Added option to enable mutual TLS

Added option to enable mutual TLS

--- src/server.coffee
+++ src/server.coffee
@@ -7,6 +7,7 @@

 httpPort = 5001
 httpsPort = 5000
+mutualTLS = false

 koaMiddleware.setupApp (app) ->

@@ -17,7 +18,7 @@
 			console.log "HTTP listenting on port " + httpPort

 	if httpsPort
-		httpsServer = https.createServer tlsAuth.getServerOptions(), app.callback()
+		httpsServer = https.createServer tlsAuth.getServerOptions(mutualTLS), app.callback()
 		httpsServer.listen httpsPort
 		httpServer.on "listening", ->
 			console.log "HTTPS listenting on port " + httpPort


Use properties instead of globals

Use properties instead of globals

--- src/server.coffee
+++ src/server.coffee
@@ -2,10 +2,11 @@
 io     = require 'socket.io-client'
 logger = require './logger'
 FS     = require 'fs'
-buffer = ''
-ws     = undefined

 class Server
+  buffer: ''
+  ws: undefined
+
   constructor: (@host, @port, @options = {}) ->

   #
@@ -25,10 +26,9 @@
   # connect socket.io client to url, bind to socket.io events and our custom events
   #
   _setup_sio: ->
-    # logger.debug "Socket.io connecting to url #{@_url()}"
     socket = new io(@_url(), {})
     socket.on 'connect', =>
-      console.log "connected to socket at #{@_url()}"
+      logger.info "connected to socket at #{@_url()}"

       # make an initial call to our recursive _write_pipe() method
       @_write_pipe()
@@ -42,23 +42,20 @@
   # create writestream to kernel at /dev/ttyO1
   #
   _setup_writestream: ->
-    ws = FS.createWriteStream "/dev/ttyO1",
+    @ws = FS.createWriteStream "/dev/ttyO1",
       flags: "w+"

   #
-  # write buffer directly to the kernel via /dev/ttyO1 pipe
-  # if buffer has content, at 30ms resolution. recursively call this function on success
+  # if buffer has content, write buffer directly to the kernel via /dev/ttyO1 pipe
+  # at 15ms resolution. recursively call this function using setInterval()
   #
   _write_pipe: ->
     setInterval (=>
-      # console.log "setInterval called with buffer #{buffer}"
-      return if buffer.length == 0
-      ws.write(buffer, (err, written) =>
+      return if @buffer.length == 0
+      @ws.write(@buffer, (err, written) =>
         throw err if err
-        buffer = ''
-        # @_write_pipe()
+        @buffer = ''
       )
-      # return
     ), 15

   #
@@ -68,12 +65,11 @@
   #   # => '4,1,000,110,255,000;'
   #
   _data_to_instruction: (data) ->
-    # break colors string into array
+    # break colors string into array, pop empty last value
     colors = data.color.split '\n'
     colors.pop()

     # build our TTY instruction
-    # logger.debug "colors: #{colors} length: #{colors.length}"
     instruction = ''
     instruction += "4,#{i+1},#{color};" for color, i in colors

@@ -86,7 +82,6 @@
   # write socket.io color data to buffer as UART instruction
   #
   _to_buffer: (data) ->
-    # logger.info "writing to buffer #{data.color}"
-    buffer = @_data_to_instruction(data)
+    @buffer = @_data_to_instruction(data)

 module.exports = Server


removed livereload

removed livereload

--- src/server.coffee
+++ src/server.coffee
@@ -21,7 +21,7 @@
 server = new Application cfg.server

 server.use (app) ->
-  app.use(require('connect-livereload')())
+  #app.use(require('connect-livereload')())

   app.cookieParser = cookieParser()


Move Newrelic to the top

Move Newrelic to the top

--- src/server.coffee
+++ src/server.coffee
@@ -1,11 +1,12 @@
 #
 # Main API Server
 #
+# Newrelic should be on the top
+newrelic = require './helpers/newrelic'

 restify = require 'restify'
 chalk = require 'chalk'

-newrelic = require './helpers/newrelic'
 exceptionHandler = require './helpers/exceptionhandler'
 logger = require './helpers/logger'


Fix adxAdapterId bug

Fix adxAdapterId bug

--- src/server.coffee
+++ src/server.coffee
@@ -244,7 +244,7 @@
     key: nullIfEmpty config.getConf()['sync-type']['both-trigger-client-key']
     ca: nullIfEmpty config.getConf()['sync-type']['both-trigger-ca-cert']
   if out.adxAdapterID
-    options.url += '/' + adxAdapterID
+    options.url += '/' + out.adxAdapterID

   beforeTimestamp = new Date()
   request.post options, (err, res, body) ->


Update app.templates.coffee

Update app.templates.coffee

--- _src/coffee/app.templates.coffee
+++ _src/coffee/app.templates.coffee
@@ -134,7 +134,7 @@

Project

-
 Gitblog

+
 Gitblog

Author

 hyspace

@@ -163,4 +163,4 @@

         """
       )
-  ]+  ]


Finish concept propagation script

Finish concept propagation script

--- scripts/node/2016-12-14-autotag-concepts.coffee
+++ scripts/node/2016-12-14-autotag-concepts.coffee
@@ -17,14 +17,18 @@
     try
       solution.source = _.template(solution.source)(plan.context)
     catch e
-      console.error "Problem with template and solution comments for", @get('slug'), e
+      console.error "Problem with template and solution comments for", level.get('slug'), e
   solutions

 tagSolution = (solution) ->
   code = solution.source
   engine = new esper.Engine()
-  engine.load(code)
-  ast = engine.evaluator.ast
+  try
+    engine.load(code)
+    ast = engine.evaluator.ast
+  catch err
+    console.error err
+    return null
   result = []
   for key of concepts
     tkn = concepts[key].tagger
@@ -38,27 +42,38 @@
 exports.run = ->
   mongoose = require 'mongoose'
   Level = require '../../server/models/Level'
-  query = Level.find(slug: {$exists: true}, concepts: {$exists: true}).limit(10)
+  query = Level.find(slug: {$exists: true}, concepts: {$exists: true})
   modified = 0
+  saved = 0
   query.exec (err, levels) ->
     console.log "Found #{levels.length} levels with concepts."
     for level in levels
       unless solution = _.find getSolutions(level), {language: 'javascript'}
-        console.log "No solution for", level.get('name')
+        console.log "No solution for", level.get('name'), '\n'
         continue
       oldTags = level.get('concepts')
       autoTags = tagSolution solution
+      unless autoTags?
+        console.log "Couldn't parse solution for #{level.get('name')}\n#{solution.source}"
+        continue
       console.log 'Find concepts for', level.get('name')#, '\n' + solution.source
       console.log "Old     : #{oldTags.join(', ')}"
       console.log "Concepts: #{autoTags.join(', ')}"
       combinedTags = autoTags.concat (tag for tag in oldTags when _.find(concepts, (concept) -> concept.concept is tag and not concept.automatic and not concept.deprecated))
       console.log "Combined: #{combinedTags.join(', ')}\n"
-      #level.set('concepts', combinedTags)
-      #level.save()
-      #modified += 1
+      if _.isEqual _.clone(oldTags).sort(), _.clone(combinedTags).sort()
+        console.log 'No change--skipping'
+        continue
+      level.set('concepts', combinedTags)
+      level.save (err, result) ->
+        if err
+          console.log 'err saving', err
+        else
+          saved += 1
+          console.log 'Saved', saved, 'levels.'
+      modified += 1

     console.log("Modified #{modified} / #{levels.length} levels.")
-    setTimeout process.exit, 10 * 1000

 if _.last(process.argv) is 'run'
   database = require '../../server/commons/database'


Make package work with both versions of the display buffer

--- spec/find-view-spec.coffee
+++ spec/find-view-spec.coffee
@@ -2,6 +2,8 @@
 {$} = require 'atom-space-pen-views'

 path = require 'path'
+
+# TODO: Remove references to logical display buffer when it gets released.

 describe 'FindView', ->
   [workspaceElement, editorView, editor, findView, activationPromise] = []
@@ -912,23 +914,41 @@

       it "scrolls to the first match if the settings scrollToResultOnLiveSearch is true", ->
         atom.config.set('find-and-replace.scrollToResultOnLiveSearch', true)
-        editorView.setHeight(3)
+        if editorView.logicalDisplayBuffer
+          editorView.setHeight(3)
+        else
+          editor.setHeight(3)
         editor.moveToTop()
-        originalScrollPosition = editorView.getScrollTop()
+        if editorView.logicalDisplayBuffer
+          originalScrollPosition = editorView.getScrollTop()
+        else
+          originalScrollPosition = editor.getScrollTop()
         findView.findEditor.setText 'Array'
         advance()
-        expect(editorView.getScrollTop()).not.toEqual originalScrollPosition
+        if editorView.logicalDisplayBuffer
+          expect(editorView.getScrollTop()).not.toEqual originalScrollPosition
+        else
+          expect(editor.getScrollTop()).not.toEqual originalScrollPosition
         expect(editor.getSelectedBufferRange()).toEqual [[11, 14], [11, 19]]
         expect(findView.findEditor).toHaveFocus()

       it "doesn't scroll to the first match if the settings scrollToResultOnLiveSearch is false", ->
         atom.config.set('find-and-replace.scrollToResultOnLiveSearch', false)
-        editorView.setHeight(3)
+        if editorView.logicalDisplayBuffer
+          editorView.setHeight(3)
+        else
+          editor.setHeight()
         editor.moveToTop()
-        originalScrollPosition = editorView.getScrollTop()
+        if editorView.logicalDisplayBuffer
+          originalScrollPosition = editorView.getScrollTop()
+        else
+          originalScrollPosition = editor.getScrollTop()
         findView.findEditor.setText 'Array'
         advance()
-        expect(editorView.getScrollTop()).toEqual originalScrollPosition
+        if editorView.logicalDisplayBuffer
+          expect(editorView.getScrollTop()).toEqual originalScrollPosition
+        else
+          expect(editor.getScrollTop()).toEqual originalScrollPosition
         expect(editor.getSelectedBufferRange()).toEqual []
         expect(findView.findEditor).toHaveFocus()


Input

Input

- on "displayValue", set value only when it has changed, this
  prevents the INPUT from losing focus.

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -808,7 +808,7 @@

 		if @preventInvalidInput() and shadow_v.length > 0
 			ret = @checkInput(@correctValueForInput(shadow_v))
-			console.debug "checking shadow input", ret, shadow_v
+			# console.debug "checking shadow input", ret, shadow_v
 			if ret == false
 				return

@@ -911,7 +911,10 @@

 	displayValue: ->
 		super()
-		@__input0.value = @getValueForDisplay()
+		value = @getValueForDisplay()
+		if value != @__input0.value
+			# prevent focus loss if value is the same
+			@__input0.value = value
 		@checkInput()
 		@


Test that visual mode is retained when indenting

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -816,6 +816,17 @@
           it "outdents all three lines", ->
             expect(editor.getText()).toBe "12345\nabcde\nABCDE"

+    describe "in visual mode", ->
+      beforeEach ->
+        editor.setCursorScreenPosition([0, 0])
+        keydown('v', shift: true)
+        keydown('>')
+
+      it "indents the current line and remains in visual mode", ->
+        expect(editorView).toHaveClass 'visual-mode'
+        expect(editor.getText()).toBe "  12345\nabcde\nABCDE"
+        expect(editor.getSelectedText()).toBe "  12345\n"
+
   describe "the < keybinding", ->
     beforeEach ->
       editor.setText("  12345\n  abcde\nABCDE")
@@ -845,6 +856,16 @@

         it "indents both lines", ->
           expect(editor.getText()).toBe "  12345\n  abcde\nABCDE"
+
+    describe "in visual mode", ->
+      beforeEach ->
+        keydown('v', shift: true)
+        keydown('<')
+
+      it "indents the current line and remains in visual mode", ->
+        expect(editorView).toHaveClass 'visual-mode'
+        expect(editor.getText()).toBe "12345\n  abcde\nABCDE"
+        expect(editor.getSelectedText()).toBe "12345\n"

   describe "the = keybinding", ->
     oldGrammar = []


Remove unneeded process require

Remove unneeded process require

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -2,7 +2,6 @@
 {$, $$} = require 'atom-space-pen-views'
 fs = require 'fs-plus'
 path = require 'path'
-process = require 'process'
 temp = require('temp').track()
 os = require 'os'
 {buildDragEvents} = require "./event-helpers"
@@ -1333,6 +1332,7 @@
           it "should successfully move the file", ->
             # Files cannot contain asterisks on Windows
             return if process.platform is "win32"
+
             asteriskFilePath = path.join(dirPath, "test-file-**.txt")
             fs.writeFileSync(asteriskFilePath, "doesn't matter *")
             LocalStorage['tree-view:copyPath'] = JSON.stringify([asteriskFilePath])


:memo:

:memo:

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -3,6 +3,8 @@
 {getVimState} = require './spec-helper'
 settings = require '../lib/settings'

+# TODO
+# Thorough CSS class check should be done in this file.
 describe "VimState", ->
   [set, ensure, keystroke, editor, editorElement, vimState] = []


Use unshift instead of push for creating connections

--- src/flux/stores/database.coffee
+++ src/flux/stores/database.coffee
@@ -21,7 +21,7 @@
       if err or body is 'fail'
         Common.beep()
       else
-        cache.connections.push
+        cache.connections.unshift
           name: 'Example'
           status: true
           data: data


Fix order

Fix order

--- src/gui/tabs/local/index.coffee
+++ src/gui/tabs/local/index.coffee
@@ -11,7 +11,7 @@
 exports.a = (pane)->
   pane.innerHTML = t list = locals()
   showCount list
-  ask.x pane, click, list
+  ask.x pane, click, list.reverse()
   return

 exports.i = showCount = (locals)->


Groups: Credit card validation messages are  updated

--- client/Main/payment/paymentmethodentryform.coffee
+++ client/Main/payment/paymentmethodentryform.coffee
@@ -14,6 +14,7 @@
         placeholder       : 'First name'
         defaultValue      : KD.whoami().profile.firstName
         validate          :
+          notifications   : yes
           event           : "blur"
           rules           :
             required      : yes
@@ -26,6 +27,7 @@
             placeholder   : 'Last name'
             defaultValue  : KD.whoami().profile.lastName
             validate      :
+              notifications: yes
               event       : "blur"
               rules       :
                 required  : yes
@@ -40,6 +42,7 @@
         focus             : ->
           @setValue @oldValue  if @oldValue
         validate          :
+          notifications   : yes
           event           : 'blur'
           rules           :
             creditCard    : yes
@@ -51,9 +54,9 @@
         placeholder       : "MM"
         maxLength         : 2
         validate          :
+          notifications   : yes
           event           : 'blur'
           rules           :
-            required      : yes
             maxLength     : 2
             regExp        : do ->
               remainingMonths = KD.utils
@@ -62,28 +65,31 @@
                 .map((item)-> item.title)
                 .join '|'
               return ///#{remainingMonths}///
+          messages        :
+            regExp        : "Expiration month should be 2 digits and between 01 to 12"
         nextElementFlat   :
           cardYear        :
             placeholder   : "YY"
             maxLength     : 2
             validate      :
+              notifications: yes
               event       : 'blur'
               rules       :
-                required  : yes
                 regExp    : do ->
                   twoDigitsYear = (new Date).getFullYear()%100
                   yearOptions   = [twoDigitsYear...twoDigitsYear+15].join '|'
                   return ///#{yearOptions}///
+              messages    :
+                regExp    : "Expiration year should be between #{twoDigitsYear = (new Date).getFullYear()%100} to #{twoDigitsYear+14}"
       cardCV              :
         placeholder       : 'CVC'
         validate          :
+          notifications   : yes
           event           : 'blur'
           rules           :
-            required      : yes
-            regExp        : /[0-9]{3,4}/
+            regExp        : /^[0-9]{3,4}$/
           messages        :
-            required      : 'Card verification code (CVC) is required!'
-            regExp        : 'Card verification code (CVC) should be a 3- or 4-digit number!'
+            regExp        : 'Card verification code (CVC) should be a 3 or 4-digit number!'

     super
       cssClass              : KD.utils.curry 'payment-method-entry-form', options.cssClass


trash button on top of conversation : Select Previous Message

--- client/app/components/toolbar_conversation.coffee
+++ client/app/components/toolbar_conversation.coffee
@@ -83,9 +83,11 @@
     onDelete: ->
         # Remove conversation
         conversationID = @props.conversationID
+
         MessageActionCreator.delete {conversationID}

-        @gotoNextConversation()
+        # Select previous conversation
+        @gotoPreviousConversation()

     onMark: (flag) ->
         conversationID = @props.conversationID


ComputeController::reject all kloud methods for managed vms called over cc.

--- client/app/lib/providers/computecontroller.coffee
+++ client/app/lib/providers/computecontroller.coffee
@@ -88,6 +88,12 @@
     @_trials[machine.uid] = {}

+  methodNotSupportedBy = (machine)->
+    if machine?.provider is 'managed'
+      return
+        name    : 'NotSupported'
+        message : 'Operation is not supported for this VM'
+
   errorHandler: (call, task, machine)->

     { timeout, Error } = ComputeController
@@ -367,6 +373,8 @@

   destroy: (machine, force)->

+    return if methodNotSupportedBy machine
+
     destroy = (machine)=>

       @eventListener.triggerState machine,
@@ -400,6 +408,8 @@

   reinit: (machine)->

+    return if methodNotSupportedBy machine
+
     ComputeController_UI.askFor 'reinit', {machine, force: @_force}, =>

       @eventListener.triggerState machine,
@@ -428,6 +438,8 @@

   resize: (machine, resizeTo = 10)->

+    return if methodNotSupportedBy machine
+
     ComputeController_UI.askFor 'resize', {
       machine, force: @_force, resizeTo
     }, =>
@@ -467,6 +479,8 @@

   build: (machine)->

+    return if methodNotSupportedBy machine
+
     @eventListener.triggerState machine,
       status      : Machine.State.Building
       percentage  : 0
@@ -490,6 +504,8 @@

   start: (machine)->

+    return if methodNotSupportedBy machine
+
     @eventListener.triggerState machine,
       status      : Machine.State.Starting
       percentage  : 0
@@ -512,6 +528,8 @@

   stop: (machine)->
+
+    return if methodNotSupportedBy machine

     @eventListener.triggerState machine,
       status      : Machine.State.Stopping
@@ -536,6 +554,9 @@

   setAlwaysOn: (machine, state, callback = kd.noop)->
+
+    if err = methodNotSupportedBy machine
+      return callback err

     options =
       machineId : machine._id


ComputeController::reinit: no need to follow destroy events on reinit

--- client/app/lib/providers/computecontroller.coffee
+++ client/app/lib/providers/computecontroller.coffee
@@ -715,7 +715,7 @@
       (@errorHandler call, 'buildStack', stack) err

-  destroyStack: (stack, callback) ->
+  destroyStack: (stack, callback, followEvents = yes) ->

     # TMS-1919: This only takes a stack instance so it's ok
     # for multiple stacks ~ GG
@@ -729,7 +729,7 @@
         name    : 'InProgress'
         message : "This stack is currently #{state.toLowerCase()}."

-    stack.machines.forEach (machineId) =>
+    if followEvents then stack.machines.forEach (machineId) =>
       return  unless machine = @findMachineFromMachineId machineId

       @eventListener.triggerState machine,
@@ -745,7 +745,7 @@

       stack.destroy callback
       actions.reinitStack stack._id
-      @eventListener.addListener 'apply', stackId
+      @eventListener.addListener 'apply', stackId  if followEvents

     .timeout globals.COMPUTECONTROLLER_TIMEOUT

@@ -1289,5 +1289,8 @@
           if template and not groupStack
           then @createDefaultStack no, template
           else @createDefaultStack()
+
+        , followEvents = no
+
     , ->
       callback new Error 'Stack is not reinitialized'


UserEnvironmentDataProvider: Added getMachineByUId method.

--- client/app/lib/userenvironmentdataprovider.coffee
+++ client/app/lib/userenvironmentdataprovider.coffee
@@ -100,6 +100,12 @@

     @machineGetter_ 'label', machineLabel, callback

+
+  getMachineByUId: (uid, callback) ->
+
+    @machineGetter_ 'uid', uid, callback
+
+
   getMachineAndWorkspaceByChannelId: (channelId, callback) ->

     machine = null


Nodejs Web Server Tests: test csrf token for recover handler

--- servers/lib/server/handlers/recover.test.coffee
+++ servers/lib/server/handlers/recover.test.coffee
@@ -1,19 +1,21 @@
-Bongo                                     = require 'bongo'
-koding                                    = require './../bongo'
-request                                   = require 'request'
-querystring                               = require 'querystring'
-
-{ daisy }                                 = Bongo
-{ expect }                                = require 'chai'
-{ generateRandomEmail
+{ daisy
+  expect
+  request
+  generateRandomEmail
   generateRandomString
   generateRandomUsername }                = require '../../../testhelper'
+{ testCsrfToken }                         = require '../../../testhelper/handler'
 { generateRecoverRequestParams }          = require '../../../testhelper/handler/recoverhelper'
 { generateRegisterRequestParams }         = require '../../../testhelper/handler/registerhelper'

 # here we have actual tests
 runTests = -> describe 'server.handlers.recover', ->
+
+  it 'should fail when csrf token is invalid', (done) ->
+
+    testCsrfToken generateRecoverRequestParams, 'post', done
+

   it 'should send HTTP 404 if request method is not POST', (done) ->


Remove pointless log from test

Remove pointless log from test

--- server/test/units/controllers/theme.coffee
+++ server/test/units/controllers/theme.coffee
@@ -285,8 +285,6 @@
         done(new Error("Expected res.send not to be called, but called with #{code}: #{body}"))
       render: (templateName, data) ->
         try
-          console.log data.dpsir
-
           assert.lengthOf data.themes, 1, "Expected one theme to be returned"
           assert.lengthOf data.themes[0].indicators, 1,
             "Expected the one driver indicator to be returned, regardless of the filter"


remove unused debugging

remove unused debugging

--- modules/plot/test/spec.coffee
+++ modules/plot/test/spec.coffee
@@ -255,7 +255,6 @@

     describe "render", ->
       it 'should display when the height is positive', (done) ->
-        console.log "Default timeout Interval", jasmine.DEFAULT_TIMEOUT_INTERVAL
         graphOpts =
           axes: [
             series: [['line']]


try being strict on limits when autoloading

--- vue/src/shared/loaders/thread_loader.coffee
+++ vue/src/shared/loaders/thread_loader.coffee
@@ -81,57 +81,57 @@

   autoLoadAfter: (obj) ->
     if (!@discussion.newestFirst && obj.event.depth == 1) || (obj.missingAfterCount && obj.missingAfterCount < @maxAutoLoadMore)
-      @loadAfter(obj.event)
-
-  loadAfter: (event) ->
-    @addLoadAfterRule(event)
+      @loadAfter(obj.event, obj.missingAfterCount)
+
+  loadAfter: (event, limit = null) ->
+    @addLoadAfterRule(event, limit)
     @fetch()

   autoLoadBefore: (obj) ->
     if (@discussion.newestFirst && obj.event.depth == 1) || (obj.missingEarlierCount && obj.missingEarlierCount < @maxAutoLoadMore)
-      @loadBefore(obj.event)
-
-  loadBefore: (event) ->
+      @loadBefore(obj.event, obj.missingEarlierCount)
+
+  loadBefore: (event, limit = null) ->
     @loading = 'before'+event.id
-    @addLoadBeforeRule(event)
-    @fetch()
-
-  autoLoadChildren: (obj) ->
-    if obj.missingChildCount && (obj.missingChildCount < @maxAutoLoadMore)
-      @loadChildren(obj.event)
-
-  loadChildren: (event) ->
-    @loading = 'children'+event.id
-    if event.kind == "new_discussion"
-      @addRuleAndFetch
-        name: "load discussion children"
-        local:
-          find:
-            discussionId: @discussion.id
-          simplesort: 'id'
-          limit: @padding
-        remote:
-          discussion_id: @discussion.id
-          order_by: 'position_key'
-          per: @padding
-    else
-      @addLoadAfterRule(event)
-      # @addRuleAndFetch
-      #   name: "load children (prefix #{event.positionKey})"
-      #   local:
-      #     find:
-      #       discussionId: @discussion.id
-      #       positionKey: {'$regex': "^#{event.positionKey}"}
-      #     simplesort: 'positionKey'
-      #     limit: @padding
-      #   remote:
-      #     discussion_id: @discussion.id
-      #     position_key_sw: event.positionKey
-      #     depth_gt: event.depth
-      #     order_by: 'position_key'
-      #     per: @padding
-
-  addLoadAfterRule: (event) ->
+    @addLoadBeforeRule(event, limit)
+    @fetch()
+
+  # autoLoadChildren: (obj) ->
+  #   if obj.missingChildCount && (obj.missingChildCount < @maxAutoLoadMore)
+  #     @loadChildren(obj.event)
+  #
+  # loadChildren: (event) ->
+  #   @loading = 'children'+event.id
+  #   if event.kind == "new_discussion"
+  #     @addRuleAndFetch
+  #       name: "load discussion children"
+  #       local:
+  #         find:
+  #           discussionId: @discussion.id
+  #         simplesort: 'id'
+  #         limit: @padding
+  #       remote:
+  #         discussion_id: @discussion.id
+  #         order_by: 'position_key'
+  #         per: @padding
+  #   else
+  #     @addLoadAfterRule(event)
+  #     # @addRuleAndFetch
+  #     #   name: "load children (prefix #{event.positionKey})"
+  #     #   local:
+  #     #     find:
+  #     #       discussionId: @discussion.id
+  #     #       positionKey: {'$regex': "^#{event.positionKey}"}
+  #     #     simplesort: 'positionKey'
+  #     #     limit: @padding
+  #     #   remote:
+  #     #     discussion_id: @discussion.id
+  #     #     position_key_sw: event.positionKey
+  #     #     depth_gt: event.depth
+  #     #     order_by: 'position_key'
+  #     #     per: @padding
+
+  addLoadAfterRule: (event, limit = null) ->
     # keys = event.positionKey.split('-')
     # num = parseInt(keys[keys.length - 1]) + 1
     # key = "0".repeat(5 - (""+num).length) + num
@@ -151,15 +151,15 @@
             $jgt: positionKey
             # $regex: (positionKeyPrefix && "^#{positionKeyPrefix}") || undefined
         simplesort: 'id'
-        limit: @padding
+        limit: limit || @padding
       remote:
         discussion_id: @discussion.id
         position_key_gt: positionKey
         # position_key_sw: positionKeyPrefix || null
         order_by: 'position_key'
-        per: @padding
-
-  addLoadBeforeRule: (event) ->
+        per: limit || @padding
+
+  addLoadBeforeRule: (event, limit) ->
     positionKeyPrefix = event.positionKey.split('-').slice(0,-1).join('-')
     @addRule
       name: "load before (prefix #{positionKeyPrefix})"
@@ -171,14 +171,14 @@
             $regex: (positionKeyPrefix && "^#{positionKeyPrefix}") || undefined
         simplesort: 'sequenceId'
         simplesortDesc: true
-        limit: @padding
+        limit: limit || @padding
       remote:
         discussion_id: @discussion.id
         position_key_lt: event.positionKey
         position_key_sw: positionKeyPrefix || null
         order_by: 'position_key'
         order_desc: 1
-        per: @padding
+        per: limit || @padding

   addLoadCommentRule: (commentId) ->
     @titleKey = 'strand_nav.from_comment'


Added google auth token timeout checking

--- src/code/providers/google-drive-provider.coffee
+++ src/code/providers/google-drive-provider.coffee
@@ -84,10 +84,17 @@
       gapi.auth.authorize args, (authToken) =>
         @authToken = if authToken and not authToken.error then authToken else null
         @user = null
+        @autoRenewToken @authToken
         if @authToken
           gapi.client.oauth2.userinfo.get().execute (user) =>
             @user = user
         @authCallback @authToken isnt null
+
+  autoRenewToken: (authToken) ->
+    if @_autoRenewTimeout
+      clearTimeout @_autoRenewTimeout
+    if authToken and not authToken.error
+      @_autoRenewTimeout = setTimeout (=> @authorize GoogleDriveProvider.IMMEDIATE), (parseInt(authToken.expires_in, 10) * 0.75) * 1000

   renderAuthorizationDialog: ->
     (GoogleDriveAuthorizationDialog {provider: @})
@@ -255,6 +262,12 @@
       content = model.createString ''
       model.getRoot().set 'content', content

+    error = (err) =>
+      if err.type is 'TOKEN_REFRESH_REQUIRED'
+        @authorize GoogleDriveProvider.IMMEDIATE
+      else
+        alert err.message
+
     if metadata.providerData?.id
       request = gapi.client.drive.files.get
         fileId: metadata.providerData.id
@@ -268,7 +281,7 @@
       if file?.id
         metadata.overwritable = file.editable
         metadata.providerData = id: file.id
-        gapi.drive.realtime.load file.id, fileLoaded, init
+        gapi.drive.realtime.load file.id, fileLoaded, init, error
       else
         callback 'Unable to load file'


Threadpage, increase pagesize to 50 to feel how that changes loading.

--- vue/src/shared/loaders/thread_loader.coffee
+++ vue/src/shared/loaders/thread_loader.coffee
@@ -20,7 +20,7 @@
     @visibleKeys = {}
     @collapsed = Vue.observable({})
     @loading = false
-    @padding = 20
+    @padding = 50
     @maxAutoLoadMore = 5

   firstUnreadSequenceId: ->


Updated comment.

Updated comment.

--- src/knockback_view_model.coffee
+++ src/knockback_view_model.coffee
@@ -46,6 +46,8 @@
       @model = null

 ####################################################
+# API
+#   * __destroy() - override for custom cleanup when all references are released. Note: this function is __destroy instead of _destroy due to an incompatibility with a Knockout convention (https://github.com/kmalakoff/knockback/pull/17)
 # options
 #   * read_only - default is read_write
 #   * internals - an array of atttributes that should be scoped with an underscore, eg. name -> _name


Added error message for redis connection error

--- lib/db.coffee
+++ lib/db.coffee
@@ -23,6 +23,10 @@
 exports.redis.auth(config.redis.password) if config.redis.password
 exports.redis.select(config.redis.database) if config.redis.database

+exports.redis.on 'error', (err) ->
+	exports.redis.last_error = 'Error while connecting to redis DB (' + err.message + ')'
+	console.error exports.redis.last_error
+
 exit.push 'Redis db', (callback) ->
 	try
 		exports.redis.quit() if exports.redis


use ref

use ref

--- resources/assets/lib/select-options.coffee
+++ resources/assets/lib/select-options.coffee
@@ -17,12 +17,13 @@
 ###

 import { a, i, div } from 'react-dom-factories'
-import { PureComponent } from 'react'
+import { createRef, PureComponent } from 'react'

 export class SelectOptions extends PureComponent
   constructor: (props) ->
     super props
     @bn = @props.bn ? 'select-options'
+    @ref = createRef()

     @state =
       showingSelector: false
@@ -41,11 +42,12 @@

   render: =>
-    classNames = "#{@bn} js-#{@bn}"
+    classNames = "#{@bn}"
     classNames += " #{@bn}--selecting" if @state.showingSelector

     div
       className: classNames
+      ref: @ref
       div
         className: "#{@bn}__select"
         @renderItem
@@ -99,7 +101,7 @@
   hideSelector: (e) =>
     return if e.button != 0
     return unless @state.showingSelector
-    return if $(e.target).closest(".js-#{@bn}").length
+    return if $(e.target).closest(@ref.current).length

     @setState showingSelector: false


ordered array of call list rows

ordered array of call list rows

--- app/assets/javascripts/calls.coffee
+++ app/assets/javascripts/calls.coffee
@@ -19,4 +19,22 @@
         ui.item.removeClass('active-item-shadow')
         # highlight the row on drop to indicate an update
         ui.item.children('td').effect('highlight', {}, 500)
-    );
+      update: (e, ui) ->
+        item_id = ui.item[0].id
+        parent = $('#'+item_id).parent()
+        children = parent.children()
+        result = []
+        for child, value of children
+          if value.id? then result.push(value.id)
+        console.log(result)
+
+#        $.ajax(
+#          type: 'POST'
+#          url: '/things/update_row_order'
+#          dataType: 'json'
+#          data: { thing: {thing_id: item_id, row_order_position: position } }
+#        )
+    )
+
+
+


Add helpers for ahead and behind counts

Add helpers for ahead and behind counts

--- index.coffee
+++ index.coffee
@@ -65,6 +65,16 @@
   @register '_aheadBehind', (done) ->
     done null, repo?.getAheadBehindCount()

+  # Get the number of commits you're ahead of the remote
+  @register 'ahead', (done) ->
+    @get '_aheadBehind', (err, aheadBehind) ->
+      done err, aheadBehind.ahead
+
+  # Get the number of commits you're behind the remote
+  @register 'behind', (done) ->
+    @get '_aheadBehind', (err, aheadBehind) ->
+      done err, aheadBehind.behind
+
   # Returns an array of objects with 'path', 'code', 'staged', 'desc'
   #
   # This command *must* be passed through a formatter before its displayed


Load everything as 'plugins'

Load everything as 'plugins'

--- index.coffee
+++ index.coffee
@@ -9,6 +9,7 @@

 createPlugins = require 'voxel-plugins'

+# plugins
 createOculus = require 'voxel-oculus'
 createHighlight = require 'voxel-highlight'
 createPlayer = require 'voxel-player'
@@ -72,7 +73,7 @@

   # create the player from a minecraft skin file and tell the
   # game to use it as the main player
-  avatar = createPlayer(game, {image: 'player.png'})
+  avatar = game.plugins.load "voxel-player", {image: 'player.png'}
   avatar.pov('first');
   avatar.possess()
   home(avatar)
@@ -94,38 +95,38 @@
 defaultSetup = (game, avatar) ->
   console.log "entering setup"

-  debug = createDebug(game)
-  debug.axis([0, 0, 0], 10)
+  game.plugins.load("voxel-debug", {}) # TODO: allow disable
+  #debug.axis([0, 0, 0], 10)

   game.mode = GAME_MODE_SURVIVAL
   controlsTarget = game.controls.target()

-  game.flyer = createFly(game, {physical: controlsTarget, flySpeed: 0.8, enabled: false})
+  game.flyer = game.plugins.load "voxel-fly", {physical: controlsTarget, flySpeed: 0.8, enabled: false}

-  walk = createWalk(game, {
+  game.plugins.load "voxel-walk", {
     skin: controlsTarget.playerSkin
     bindGameEvents: true
     shouldStopWalking: () =>
       vx = Math.abs(controlsTarget.velocity.x)
       vz = Math.abs(controlsTarget.velocity.z)
       return vx > 0.001 || vz > 0.001
-    })
+    }

-  reach = createReach(game, { reachDistance: REACH_DISTANCE })
-  mine = createMine(game, {
+  reach = game.plugins.load "voxel-reach", { reachDistance: REACH_DISTANCE }
+  mine = game.plugins.load "voxel-mine", {
     instaMine: false
     reach: reach
     progressTexturesBase: "ProgrammerArt/textures/blocks/destroy_stage_"
     progressTexturesCount: 9
-  })
+  }

   console.log "configuring highlight "
   # highlight blocks when you look at them, hold <Ctrl> for block placement
-  highlight = createHighlight game, {
+  highlight = game.plugins.load "voxel-highlight", {
     color:  0xff0000
     distance: REACH_DISTANCE
     adjacentActive: () -> false
-    }
+  }

   # toggle between first and third person
   window.addEventListener 'keydown', (ev) ->
@@ -180,7 +181,7 @@
   # block interaction: left/right-click to break/place blocks, uses raytracing
   game.currentMaterial = 1

-  debris = createDebris(game, {power: 1.5})
+  debris = game.plugins.load("voxel-debris", {power: 1.5})
   debris.on 'collect', (item) ->
     console.log("collect", item)


even more destruction

even more destruction

--- index.coffee
+++ index.coffee
@@ -7,7 +7,7 @@
 runAndRun = =>
   debug 'running now'
   callback = =>
-    randomNumber = Math.round(Math.random() * 1000)
+    randomNumber = Math.round(Math.random() * 100)
     debug "running again in #{randomNumber}"
     _.delay runner.run, randomNumber, callback
   runner.run callback


storage api impl (copied from current bash agent)

--- index.coffee
+++ index.coffee
@@ -1,3 +1,5 @@
+fs            = require 'fs-extra'
+path          = require 'path'
 server        = require 'docker-dashboard-agent-api'
 Mqtt          = require './lib/mqtt'
 env           = require './lib/env'
@@ -6,12 +8,21 @@
 config =
   vlan: env.assert 'VLAN'
   domain: env.assert 'DOMAIN'
+  tld: env.assert 'TLD'
+  dataDir: env.assert 'DATA_DIR'
   mqtt:
     url: env.assert 'MQTT_URL'
   compose:
     scriptBaseDir: env.assert 'SCRIPT_BASE_DIR'

 console.log 'Config \n\n', config, '\n\n'
+
+try
+  fs.mkdirSync (projectDataPath = path.join config.dataDir, config.domain)
+catch err
+  unless err.code is 'EEXIST'
+    console.error 'Unable to create project data directory', projectDataPath
+    process.exit 1

 libcompose = (require './lib/compose') config

@@ -25,7 +36,8 @@
 agent = server.agent name: packageJson.name , version: packageJson.version
 agent.on 'start', (data) ->
   instanceName = data.instance.name
-  composition = libcompose.augmentCompose instanceName, (libcompose.appdef2compose instanceName, data.app.definition)
+  options = data.instance.options
+  composition = libcompose.augmentCompose instanceName, options, (libcompose.appdef2compose instanceName, data.app.definition)
   start = compose.start instanceName, composition
   start.on 'pulling', (event) ->
     event.instance = instanceName
@@ -34,3 +46,30 @@
 agent.on 'stop', (data) ->
   instanceName = data.instance.name
   compose.stop instanceName
+
+agent.on '/storage/list', (params, data, callback) ->
+  srcpath = path.join config.dataDir, config.domain
+  dirList = fs.readdirSync srcpath
+  files = dirList.map (file) ->
+    stat = fs.statSync path.join(srcpath, file)
+    if stat.isDirectory()
+      name: file
+      created: stat.birthtime
+      isLocked: ".#{file}.copy.lock" in dirList
+  .filter (file) -> file?
+  callback null, files
+
+agent.on '/storage/delete', ({name}, data, callback) ->
+  srcpath = path.join config.dataDir, config.domain, name
+  fs.remove srcpath, callback
+
+agent.on '/storage/create', (params, {name, source}, callback) ->
+  targetpath = path.join config.dataDir, config.domain, name
+  if source
+    srcpath = path.join config.dataDir, config.domain, source
+    lockFile = path.join config.dataDir, config.domain, ".#{name}.copy.lock"
+    fs.writeFile lockFile, "Copying #{srcpath} to #{targetpath}..."
+    child_process.exec "cp -rp #{srcpath} #{targetpath}", ->
+      fs.unlink lockFile, callback
+  else
+    fs.mkdirs targetpath, callback


healthcheck interface and ip prefix are configurable (default to eth0 and 10.25 respectively)

--- index.coffee
+++ index.coffee
@@ -9,6 +9,9 @@
 Mqtt          = require './src/coffee/mqtt'
 env           = require './src/coffee/env'
 packageJson   = require './package.json'
+
+NETWORK_HEALTHCHECK_TEST_INTERFACE = env.get 'NETWORK_HEALTHCHECK_TEST_INTERFACE', 'eth0'
+NETWORK_HEALTHCHECK_TEST_IP_PREFIX = env.get 'NETWORK_HEALTHCHECK_TEST_IP_PREFIX', '10.25'

 config =
   vlan: env.assertVlan 'VLAN'
@@ -25,7 +28,7 @@
     scriptBaseDir: env.assert 'SCRIPT_BASE_DIR'
   net_container:
     healthcheck:
-      test: env.get 'NETWORK_HEALTHCHECK_TEST', "ifconfig eth0 | grep 'inet addr:10.25'"
+      test: env.get 'NETWORK_HEALTHCHECK_TEST', "ifconfig #{NETWORK_HEALTHCHECK_TEST_INTERFACE} | grep 'inet addr:#{NETWORK_HEALTHCHECK_TEST_IP_PREFIX}'"
       interval:  env.get 'NETWORK_HEALTHCHECK_INTERVAL', '30s'
       timeout: env.get 'NETWORK_HEALTHCHECK_TIMEOUT', '5s'
       retries: parseInt(env.get 'NETWORK_HEALTHCHECK_RETRIES', 4)


No errors

No errors

--- index.coffee
+++ index.coffee
@@ -6,7 +6,8 @@
 indent       = require 'indent-string'

-module.exports = (opt) ->
+module.exports = (opt) =>
+	console.log this
 	modifyFile = (file) ->
 		# Handle Events
 		emitter = new events.EventEmitter
@@ -45,6 +46,8 @@
 				# Catching Errors
 				err.filename = file.path
 				console.log 'Error in :=> ' + err.filename
+				emitter.on 'error', (err) ->
+					console.log err.stack
 				return emitter.emit 'error', new gulp_util.PluginError 'gulp-require-classify', err

 		@emit 'data', file


Rm ndock panel

Rm ndock panel

--- views/components/main/parts/ndock-panel.cjsx
+++ views/components/main/parts/ndock-panel.cjsx
@@ -1,108 +0,0 @@
-{ROOT, layout, _, $, $$, React, ReactBootstrap} = window
-{Label, OverlayTrigger, Tooltip} = ReactBootstrap
-{join} = require 'path-extra'
-__ = i18n.main.__.bind(i18n.main)
-__n = i18n.main.__n.bind(i18n.main)
-
-CountdownTimer = require './countdown-timer'
-CountdownLabel = React.createClass
-  getLabelStyle: (timeRemaining) ->
-    switch
-      when timeRemaining > 600 then 'primary'
-      when timeRemaining > 60  then 'warning'
-      when timeRemaining >= 0  then 'success'
-      else 'default'
-  getInitialState: ->
-    @notify = _.once @props.notify
-    style: @getLabelStyle(CountdownTimer.getTimeRemaining @props.completeTime)
-  componentWillReceiveProps: (nextProps) ->
-    if nextProps.completeTime isnt @props.completeTime
-      @notify = _.once nextProps.notify
-      @setState
-        style: @getLabelStyle(CountdownTimer.getTimeRemaining nextProps.completeTime)
-  shouldComponentUpdate: (nextProps, nextState) ->
-    nextProps.completeTime isnt @props.completeTime or nextState.style isnt @state.style
-  tick: (timeRemaining) ->
-    @notify() if timeRemaining <= 60
-
-    style = @getLabelStyle timeRemaining
-    @setState {style: style} if style isnt @state.style
-  render: ->
-    <OverlayTrigger placement='left' overlay={
-      switch @state.style
-        when 'primary', 'warning'
-          <Tooltip id="ndock-finish-by-#{@props.dockIndex}">
-            {__ 'Finish by : '}{timeToString @props.completeTime}
-          </Tooltip>
-        else
-
-    }>
-
-      {
-        if @props.completeTime >= 0
-          <CountdownTimer countdownId={"ndock-#{@props.dockIndex+1}"}
-                          completeTime={@props.completeTime}
-                          tickCallback={@tick} />
-      }
-
-    </OverlayTrigger>
-
-
-class NDockInfo
-  constructor: (ndockApi) ->
-    if ndockApi? then @update(ndockApi) else @empty()
-  empty: ->
-    @name = __ 'Empty'
-    @completeTime = -1
-  setLocked: ->
-    @name = __ 'Locked'
-    @completeTime = -1
-  update: (ndockApi) ->
-    switch ndockApi.api_state
-      when -1 then @setLocked()
-      when 0  then @empty()
-      when 1
-        @name = window._ships[ndockApi.api_ship_id].api_name
-        @completeTime = ndockApi.api_complete_time
-
-NdockPanel = React.createClass
-  getInitialState: ->
-    docks: [1..4].map () -> new NDockInfo
-  handleResponse: (e) ->
-    {path, body, postBody} = e.detail
-    switch path
-      when '/kcsapi/api_port/port', '/kcsapi/api_get_member/ndock'
-        ndocks = if path is '/kcsapi/api_port/port' then body.api_ndock else body
-        docks = ndocks.map (ndock) -> new NDockInfo(ndock)
-        if !_.isEqual docks, @state.docks
-          @setState
-            docks: docks
-      when '/kcsapi/api_req_nyukyo/speedchange'
-        docks = @state.docks.slice()
-        docks[postBody.api_ndock_id - 1] = new NDockInfo
-        @setState
-          docks: docks
-  componentDidMount: ->
-    window.addEventListener 'game.response', @handleResponse
-  componentWillUnmount: ->
-    window.removeEventListener 'game.response', @handleResponse
-  repairIcon: join(ROOT, 'assets', 'img', 'operation', 'repair.png')
-  notify: (dockName) ->
-    notify "#{i18n.resources.__ dockName} #{__ 'repair completed'}",
-      type: 'repair'
-      title: __ 'Docking'
-      icon: @repairIcon
-  render: ->
-

-    {
-      for dock, i in @state.docks
-

-          {i18n.resources.__ dock.name}
-          <CountdownLabel dockIndex={i}
-                          completeTime={dock.completeTime}
-                          notify={@notify.bind @, dock.name}/>
-

-    }
-

-
-module.exports = NdockPanel


fix newly created quizzes

fix newly created quizzes

--- komodo/dashboard.coffee
+++ komodo/dashboard.coffee
@@ -13,5 +13,9 @@

     Template.dashboard.events
         "click #new": (evt) ->
-            id = Quizzes.insert({name: "New Quiz",lastmod: new Date(), owner: Meteor.userId()})
+            id = Quizzes.insert
+                name: "New Quiz"
+                lastmod: new Date()
+                owner: Meteor.userId()
+                questions: [{answers:[""]}]
             Router.go "/edit/#{id}"


authorization via tf role; add tf clone command

--- src/tf.coffee
+++ src/tf.coffee
@@ -22,6 +22,9 @@
 module.exports = (robot) ->

   robot.respond /tf create key$/i, (msg) ->
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+
     fn = msg.message.user.name
     fn.replace /\//, "_"
     fp = basepath + "/" + fn
@@ -35,6 +38,9 @@
       msg.send {room: msg.message.user.name}, "```\n#{pubkey}\n```"

   robot.respond /tf show key$/i, (msg) ->
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+
     fn = msg.message.user.name
     fn.replace /\//, "_"
     fp = basepath + "/" + fn
@@ -46,6 +52,9 @@
     return msg.send {room: msg.message.user.name}, "```\n#{pubkey}\n```"

   robot.respond /tf destroy key$/i, (msg) ->
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+
     fn = msg.message.user.name
     fn.replace /\//, "_"
     fp = basepath + "/" + fn
@@ -56,3 +65,18 @@
     fs.unlinkSync("#{fp}")
     fs.unlinkSync("#{fp}.pub")
     return msg.send {room: msg.message.user.name}, "Key destroyed!"
+
+  robot.respond /tf clone ([^\s]+) ([^\s]+)$/i, (msg) ->
+    unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,'tf')
+      return msg.send {room: msg.message.user.name}, "Not authorized.  Missing 'tf' role."
+
+    url = msg.match[1]
+    projname = msg.match[2].replace /\//, "_"
+    projpath = basepath + "/" + projname
+
+    fn = msg.message.user.name
+    fn.replace /\//, "_"
+    fp = basepath + "/" + fn
+
+    exec "GIT_SSH_COMMAND='ssh -i #{fp} -F /dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' git clone #{url} #{fp}", (error, stdout, stderr) ->
+      msg.send {room: msg.message.user.name}, "```\n#{stdout}\n```"


IE doesn't support Element#remove

IE doesn't support Element#remove

* Check for existence of Element#remove and polyfill
* Attaching to window to cater for scenarios where a script isn't loaded inside the same window e.g. iframe

--- app/scripts/lib/bounce/index.coffee
+++ app/scripts/lib/bounce/index.coffee
@@ -82,6 +82,11 @@
     deferred

   remove: ->
+    do ->
+      w = window
+      w.Element::remove ?= () ->
+        @?.parentNode?.removeChild(@);
+
     @styleElement?.remove()

   getPrefixes: (force) ->


Use destructuring for arguments.

Use destructuring for arguments.

--- sudoku.coffee
+++ sudoku.coffee
@@ -62,8 +62,7 @@
     # single square, then the digit can be eliminated from the other
     # Positions of that square.

-    @tricky_sets = rows.concat(cols).product(squares).flatMap (args) =>
-      [row, square] = args
+    @tricky_sets = rows.concat(cols).product(squares).flatMap ([row, square]) =>
       common = row.positions.intersect(square.positions)
       if !common.is_empty()
         # Each Array in @tricky_sets contains three
@@ -83,8 +82,7 @@
     # setup = File.read(filename).gsub(/#.*/, "").gsub(/\s/, "")
     # end

-    setup.split("").zip(@positions).forEach (args) =>
-      [c, position] = args
+    setup.split("").zip(@positions).forEach ([c, position]) =>
       if c != "-"
         console.log "placing initial #{c} in position #{position.number}"
         position.place(Number(c))
@@ -159,8 +157,7 @@
           puzzle.solve()

   eliminate_with_tricky_sets: ->
-    @tricky_sets.some (args) =>
-      [subset, rest_of_set, elimination_set] = args
+    @tricky_sets.some ([subset, rest_of_set, elimination_set]) =>
       subset.flatMap (position) =>
         if position.placed then [] else position.possible
       .uniq().filter (digit) =>


green spec for #31

green spec for #31

--- spec/imgflo-url.coffee
+++ spec/imgflo-url.coffee
@@ -114,6 +114,30 @@
           input: 'data:image/gif;base64,R0lGODlhDwAMAL...'

         expect(url).to.equal 'data:image/gif;base64,R0lGODlhDwAMAL...'
+
+    context 'with twitter image url issues', ->
+
+      it '.jpg:large should return noop.jpg', ->
+
+        config = getConfig()
+
+        params =
+          input: 'https://pbs.twimg.com/media/BlM0d2-CcAAT9ic.jpg:large'
+
+        url = imgflo config, 'noop', params, 'jpg'
+
+        expect(url).to.equal "https://imgflo.herokuapp.com/graph/key/0125cf1fec905afdcf07a2963c907b2d/noop.jpg?input=https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FBlM0d2-CcAAT9ic.jpg%3Alarge"
+
+      it '.jpg%3Alarge should return noop.jpg', ->
+
+        config = getConfig()
+
+        params =
+          input: 'https://imgflo.herokuapp.com/graph/2b2431f8e7ba7b0/dabcd2b47c33e25505fb1f5615586fb5/noop.jpg?input=https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FCocKWjJWEAAlPJI.jpg%3Alarge'
+
+        url = imgflo config, 'noop', params, 'jpg'
+
+        expect(url).to.equal "https://imgflo.herokuapp.com/graph/key/c2ee25e62a242c68c32702ad90aedf2e/noop.jpg?input=https%3A%2F%2Fimgflo.herokuapp.com%2Fgraph%2F2b2431f8e7ba7b0%2Fdabcd2b47c33e25505fb1f5615586fb5%2Fnoop.jpg%3Finput%3Dhttps%253A%252F%252Fpbs.twimg.com%252Fmedia%252FCocKWjJWEAAlPJI.jpg%253Alarge"

   describe 'without a graph name', ->