add tests for remaining new stuffs

add tests for remaining new stuffs

--- test/test-chain-builder.coffee
+++ test/test-chain-builder.coffee
@@ -89,10 +89,17 @@

 # 3. remove
-# - chain.remove(index, 'reason')
+# - chain.remove(index)
 # - chain.remove('someid', 'reason')
+# - chain.remove('someid')
+# - chain.remove('someid', 'reason')
+# - chain.remove(someFn)
 # - chain.remove(someFn, 'reason')
+#
+# - control.remove()
 # - control.remove('reason')
+#
+# - chain.select(fn).remove()
 # - chain.select(fn).remove('reason')

 describe 'test chain.remove()', ->
@@ -102,7 +109,9 @@
   fn2.options = id:'fn2'
   fn3 = ->
   fn3.options = id:'fn3'
+
   chain = null
+
   beforeEach ->
     chain = buildChain()
     chain.add fn1
@@ -124,67 +133,6 @@
     assert.equal result.result, false
     assert.equal result.reason, 'not found'

-  it 'should remove fn1 from array by itself', ->
-    result = chain.remove fn1
-    assert.equal chain.array.length, 0, 'array should be empty'
-    assert.strictEqual result.removed.length, 1, 'should remove one function'
-    assert.strictEqual result.removed?[0], fn1
-
-  it 'should remove fn1 from array by itself', ->
-    chain.add fn2, fn3
-    result = chain.remove fn1
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
-    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-    assert.strictEqual result.removed.length, 1, 'should remove one function'
-    assert.strictEqual result.removed?[0], fn1
-
-  it 'should remove fn2 from array by itself', ->
-    chain.add fn2, fn3
-    result = chain.remove fn2
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
-    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-    assert.strictEqual result.removed.length, 1, 'should remove one function'
-    assert.strictEqual result.removed?[0], fn2
-
-  it 'should remove fn3 from array by itself', ->
-    chain.add fn2, fn3
-    result = chain.remove fn3
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
-    assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
-    assert.strictEqual result.removed.length, 1, 'should remove one function'
-    assert.strictEqual result.removed?[0], fn3
-
-
-  it 'should remove fn1 from array by ID', ->
-    result = chain.remove 'fn1'
-    assert.equal chain.array.length, 0, 'array should be empty'
-    assert.strictEqual result.removed.length, 1, 'should remove one function'
-    assert.strictEqual result.removed?[0], fn1
-
-  it 'should remove fn1 from array by ID', ->
-    chain.add fn2, fn3
-    chain.remove 'fn1'
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
-    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-
-  it 'should remove fn2 from array by ID', ->
-    chain.add fn2, fn3
-    result = chain.remove 'fn2'
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
-    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-
-  it 'should remove fn3 from array by ID', ->
-    chain.add fn2, fn3
-    chain.remove 'fn3'
-    assert.equal chain.array.length, 2, 'array should be one less: 2'
-    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
-    assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
-

   it 'should remove fn1 from array by index', ->
     result = chain.remove 0
@@ -192,46 +140,272 @@
     assert.strictEqual result.removed.length, 1, 'should remove one function'
     assert.strictEqual result.removed?[0], fn1

+  it 'should remove fn1 from array by index with reason', ->
+    reason = 'the reason'
+    result = chain.remove 0, reason
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.equal result.removed.length, 1, 'should remove one function'
+    assert.equal result.reason, reason
+    assert.strictEqual result.removed?[0], fn1
+
   it 'should remove fn1 from array by index', ->
     chain.add fn2, fn3
-    chain.remove 0
+    result = chain.remove 0
     assert.equal chain.array.length, 2, 'array should be one less: 2'
     assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
     assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-
-  it 'should remove fn2 from array by index', ->
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn2 from array by index with reason', ->
+    reason = 'the reason'
+    result = chain.add fn2, fn3
+    result = chain.remove 'fn2', reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed?[0], fn2
+
+
+  it 'should remove fn1 from array by ID (single)', ->
+    result = chain.remove 'fn1'
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by ID (single) with reason', ->
+    reason = 'the reason'
+    result = chain.remove 'fn1', reason
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.equal result.reason, reason
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by ID (multiple)', ->
+    chain.add fn2, fn3
+    result = chain.remove 'fn1'
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by ID (multiple) with reason', ->
+    chain.add fn2, fn3
+    reason = 'the reason'
+    result = chain.remove 'fn1', reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn2 from array by ID (multiple)', ->
     chain.add fn2, fn3
     result = chain.remove 'fn2'
     assert.equal chain.array.length, 2, 'array should be one less: 2'
     assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
     assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
-
-  it 'should remove fn3 from array by index', ->
+    assert.strictEqual result.removed?[0], fn2
+
+  it 'should remove fn2 from array by ID (multiple) with reason', ->
+    reason = 'the reason'
     chain.add fn2, fn3
-    chain.remove 'fn3'
+    result = chain.remove 'fn2', reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed?[0], fn2
+
+  it 'should remove fn3 from array by ID (multiple)', ->
+    chain.add fn2, fn3
+    result = chain.remove 'fn3'
     assert.equal chain.array.length, 2, 'array should be one less: 2'
     assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
     assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
+    assert.strictEqual result.removed?[0], fn3
+
+  it 'should remove fn3 from array by ID (multiple) with reason', ->
+    reason = 'the reason'
+    chain.add fn2, fn3
+    result = chain.remove 'fn3', reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
+    assert.strictEqual result.removed?[0], fn3
+
+
+
+  it 'should remove fn1 from array by itself (single)', ->
+    result = chain.remove fn1
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by itself (single) with reason', ->
+    reason = 'the reason'
+    result = chain.remove fn1, reason
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.equal result.reason, reason
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by itself (multiple)', ->
+    chain.add fn2, fn3
+    result = chain.remove fn1
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn1 from array by itself (multiple) with reason', ->
+    reason = 'the reason'
+    chain.add fn2, fn3
+    result = chain.remove fn1, reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn2, 'first element should be fn2'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn1
+
+  it 'should remove fn2 from array by itself (multiple)', ->
+    chain.add fn2, fn3
+    result = chain.remove fn2
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn2
+
+  it 'should remove fn2 from array by itself (multiple) with reason', ->
+    reason = 'the reason'
+    chain.add fn2, fn3
+    result = chain.remove fn2, reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn3, 'second element should be fn3'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn2
+
+  it 'should remove fn3 from array by itself (multiple)', ->
+    chain.add fn2, fn3
+    result = chain.remove fn3
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn3
+
+  it 'should remove fn3 from array by itself (multiple) with reason', ->
+    reason = 'the reason'
+    chain.add fn2, fn3
+    result = chain.remove fn3, reason
+    assert.equal chain.array.length, 2, 'array should be one less: 2'
+    assert.equal result.reason, reason
+    assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
+    assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
+    assert.strictEqual result.removed.length, 1, 'should remove one function'
+    assert.strictEqual result.removed?[0], fn3

   it 'should remove via control', ->
     remover = (control) -> control.remove()
     chain.add remover, fn2
     assert.equal chain.array.length, 3, 'array should have 3'
-    chain.run()
+    result = chain.run()
     assert.equal chain.array.length, 2, 'array should be one less: 2'
     assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
     assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
-
-  it 'should remove via control', ->
-    remover = (control) -> control.remove()
+    assert.strictEqual result.removed?[0], remover
+
+  it 'should remove via control with reason', ->
+    reason = 'the reason'
+    remover = (control) -> control.remove reason
     chain.add remover, fn2
     assert.equal chain.array.length, 3, 'array should have 3'
-    chain.run()
+    result = chain.run()
     assert.equal chain.array.length, 2, 'array should be one less: 2'
     assert.strictEqual chain.array[0], fn1, 'first element should be fn1'
     assert.strictEqual chain.array[1], fn2, 'second element should be fn2'
-
+    assert.strictEqual result.removed?[0], remover
+
+
+
+  it 'should remove zero functions with falsey selector', ->
+    chain.array = [ fn1, fn2, fn3 ]
+    assert.equal chain.array.length, 3, 'array should start with 3'
+    result = chain.select(-> false).remove()
+    assert.equal chain.array.length, 3, 'array should be unchanged'
+    assert.strictEqual result.removed, undefined, 'shouldnt be any removed'
+
+  it 'should remove all functions with truthy selector', ->
+    chain.array = [ fn1, fn2, fn3 ]
+    result = chain.select(-> true).remove()
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.equal result.results.length, 3, 'there should be three internal action results'
+    assert.strictEqual result.results[0].removed?[0], fn1
+    assert.strictEqual result.results[1].removed?[0], fn2
+    assert.strictEqual result.results[2].removed?[0], fn3
+
+  it 'should remove one function with specific selector', ->
+    chain.array = [ fn1, fn2, fn3 ]
+    selector = (fn, index) -> (fn is fn2)
+    result = chain.select(selector).remove()
+    assert.equal chain.array.length, 2, 'array should be one less'
+    assert.equal result.results.length, 1, 'there should be one internal action result'
+    assert.strictEqual result.results[0].removed?[0], fn2
+
+  it 'should remove two function with specific selector', ->
+    chain.array = [ fn1, fn2, (->), fn3, (->) ]
+    selector = (fn, index) -> (fn is fn2 or fn is fn3)
+    result = chain.select(selector).remove()
+    assert.equal chain.array.length, 3, 'array should be two less'
+    assert.equal result.results.length, 2, 'there should be two internal action results'
+    assert.strictEqual result.results[0].removed?[0], fn2
+
+
+  it 'should remove zero functions with falsey selector', ->
+    reason = 'the reason'
+    chain.array = [ fn1, fn2, fn3 ]
+    assert.equal chain.array.length, 3, 'array should start with 3'
+    result = chain.select(-> false).remove reason
+    assert.equal chain.array.length, 3, 'array should be unchanged'
+    assert.strictEqual result.removed, undefined, 'shouldnt be any removed'
+
+  it 'should remove all functions with truthy selector', ->
+    reason = 'the reason'
+    chain.array = [ fn1, fn2, fn3 ]
+    result = chain.select(-> true).remove reason
+    assert.equal chain.array.length, 0, 'array should be empty'
+    assert.equal result.results.length, 3, 'there should be three internal action results'
+    assert.strictEqual result.results[0].removed?[0], fn1
+    assert.strictEqual result.results[1].removed?[0], fn2
+    assert.strictEqual result.results[2].removed?[0], fn3
+    assert.equal result.results[0].reason, reason
+
+  it 'should remove one function with specific selector', ->
+    reason = 'the reason'
+    chain.array = [ fn1, fn2, fn3 ]
+    selector = (fn, index) -> (fn is fn2)
+    result = chain.select(selector).remove reason
+    assert.equal chain.array.length, 2, 'array should be one less'
+    assert.equal result.results.length, 1, 'there should be one internal action result'
+    assert.strictEqual result.results[0].removed?[0], fn2
+    assert.equal result.results[0].reason, reason
+
+  it 'should remove two function with specific selector', ->
+    reason = 'the reason'
+    chain.array = [ fn1, fn2, (->), fn3, (->) ]
+    selector = (fn, index) -> (fn is fn2 or fn is fn3)
+    result = chain.select(selector).remove reason
+    assert.equal chain.array.length, 3, 'array should be two less'
+    assert.equal result.results.length, 2, 'there should be two internal action results'
+    assert.strictEqual result.results[0].removed?[0], fn2
+    assert.equal result.results[0].reason, reason

 # 4. chain.clear()
@@ -736,7 +910,7 @@
     selector = (fn, index) -> (fn is target and index is 1)
     chain.add fn1, target, fn3
     assert.equal chain.array.length, 3, 'should start with 3 functions'
-    chain.select(selector).disable()
+    result = chain.select(selector).disable()
     assert.equal chain.array.length, 3, 'should still have 3 functions'
     assert.equal target.options.disabled, true
     assert.equal chain.array[1].options.disabled, true
@@ -903,7 +1077,7 @@

   it 'should select() a function and enable', ->
-
+
     target = ->
     target.options = disabled:true
     selector = (fn, index) -> (fn is target and index is 1)
@@ -932,37 +1106,150 @@
     assert.equal chain.array[1].options.disabled, undefined
     assert.equal chain.array[2].options.disabled, undefined

-
-
-
-  # TODO: Move these to testing for .select(...).remove()
-  # it 'should remove zero functions with falsey selector', ->
-  #   chain.array = [ fn1, fn2, fn3 ]
-  #   result = chain.clear -> false
-  #   assert.equal chain.array.length, 3, 'array should be unchanged'
-  #   assert.strictEqual result.removed.length, 0
-  #
-  # it 'should remove all functions from with truthy selector', ->
-  #   chain.array = [ fn1, fn2, fn3 ]
-  #   result = chain.clear -> true
-  #   assert.equal chain.array.length, 0, 'array should be empty'
-  #   assert.strictEqual result.removed.length, 3
-  #   assert.strictEqual result.removed?[0], fn1
-  #   assert.strictEqual result.removed?[1], fn2
-  #   assert.strictEqual result.removed?[2], fn3
-  #
-  #
-  # it 'should remove function from chain by id selector', ->
-  #   chain.array = [ fn1, fn2, fn3 ]
-  #   result = chain.clear (fn) -> fn?.options?.id is 'fn2'
-  #   assert.equal chain.array.length, 2, 'array should be one less'
-  #   assert.strictEqual result.removed.length, 1, 'should remove one function'
-  #   assert.strictEqual result.removed?[0], fn2
-  #
-  # it 'should remove functions from chain by two-id selector', ->
-  #   chain.array = [ fn1, fn2, fn3, (->) ]
-  #   result = chain.clear (fn) -> fn?.options?.id is 'fn2' or fn?.options?.id is 'fn3'
-  #   assert.equal chain.array.length, 2, 'array should be two less'
-  #   assert.strictEqual result.removed.length, 2, 'should remove two functions'
-  #   assert.strictEqual result.removed?[0], fn2
-  #   assert.strictEqual result.removed?[1], fn3
+# 8. select()
+
+describe 'test select', ->
+
+  chain = null
+
+  beforeEach 'create a chain', ->
+    chain = buildChain()
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select null
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select false
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select 'string'
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select 123
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select {}
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+  it 'should error if `selector` isnt a function', ->
+
+    result = chain.select []
+
+    assert result.error, 'result should contain an error'
+    assert result.disable, 'result should contain disable'
+    assert result.enable, 'result should contain enable'
+    assert result.remove, 'result should contain remove'
+    assert result.affect, 'result should contain affect'
+    assert.equal result.error, 'Selector must be an index, id, or filter function'
+    error = result.error
+
+    assert.strictEqual result.disable().error, error
+    assert.strictEqual result.enable().error, error
+    assert.strictEqual result.remove().error, error
+    assert.strictEqual result.affect().error, error
+
+
+# 9. select().affect()
+
+describe 'test select', ->
+
+  fn1 = ->
+  fn1.options = id:'fn1'
+  fn2 = ->
+  fn2.options = id:'fn2'
+  fn3 = ->
+  fn3.options = id:'fn3'
+
+  chain = null
+
+  beforeEach ->
+    chain = buildChain()
+
+  it 'should call affector on selected function', ->
+    target = ->
+    target.options = id:'target'
+    chain.add fn1, fn2, target, fn3
+
+    selector = (fn) -> fn is target
+    selected = chain.select selector
+
+    returnValue = 'return value'
+    affector = (fn) ->
+      fn.options.labels ?= []
+      fn.options.labels.push 'affected'
+      return returnValue
+
+    result = selected.affect affector
+
+    assert target.options.labels, 'should have created a labels array'
+    assert.equal target.options.labels?[0], 'affected'
+    assert.equal result?.results?[0], returnValue


Exclusive selections under ProblemClassifier and classifier list bug.

--- picoCTF-web/web/coffee/management-problems.coffee
+++ picoCTF-web/web/coffee/management-problems.coffee
@@ -89,28 +89,32 @@
     </PanelGroup>

 ClassifierItem = React.createClass
-  getInitialState: ->
-    active: false
-
   handleClick: (e) ->
-    activeState = !@state.active
-    @setState {active: activeState}
-    console.log @props, activeState
-    @props.setClassifier activeState, @props.classifier
+    @props.setClassifier !@props.active, @props.classifier
+    @props.onExclusiveClick @props.name

   render: ->
     glyph = <Glyphicon glyph="ok"/>

     <ListGroupItem onClick={@handleClick}>
-        {@props.name} {if @state.active then glyph}
<Badge>{@props.size}</Badge>

+        {@props.name} {if @props.active then glyph}
<Badge>{@props.size}</Badge>

     </ListGroupItem>

 ProblemClassifier = React.createClass
+  getInitialState: ->
+    _.object ([classifier.name, false] for classifier in @props.data)
+
+  handleClick: (name) ->
+    activeStates = @getInitialState()
+    activeStates[name] = !@state[name]
+    @setState activeStates
+
   render: ->
     <Panel header={@props.name} defaultExpanded collapsible>
       <ListGroup fill>
         {@props.data.map ((data, i) ->
-          <ClassifierItem key={i+data} setClassifier={@props.setClassifier} {...data}/>
+          <ClassifierItem onExclusiveClick={@handleClick} active={@state[data.name]}
+            key={i} setClassifier={@props.setClassifier} {...data}/>
         ).bind this}
       </ListGroup>
     </Panel>
@@ -181,7 +185,9 @@
     activeSort:
       name: "score"
       ascending: true
-    problemClassifier: (problem) -> true
+    problemClassifier: [
+      (problem) -> true
+    ]

   onFilterChange: (filter) ->
     try
@@ -196,15 +202,19 @@
       activeSort: $set: {name: name, ascending: ascending}

   setClassifier: (classifierState, classifier) ->
-    if not classifierState
-      classifier = (problem) -> true
-
-    @setState update @state,
-      problemClassifier: $set: classifier
+    #bug here
+    console.log classifierState, classifier
+    if classifierState
+      @setState update @state,
+        problemClassifier: $push: [classifier]
+    else
+      otherClassifiers = _.without @state.problemClassifier, classifier
+      @setState update @state,
+        problemClassifier: $set: otherClassifiers

   filterProblems: (problems) ->
     visibleProblems = _.filter problems, ((problem) ->
-      (@state.filterRegex.exec problem.name) != null and @state.problemClassifier problem
+      (@state.filterRegex.exec problem.name) != null and _.all (classifier problem for classifier in @state.problemClassifier)
     ).bind this

     sortedProblems = _.sortBy visibleProblems, @state.activeSort.name


Handle multiple connections while the process is still starting

Easiest way to test this is to add a process with a shell script that does sleep 10 before launching the actual job

--- src/proxy.coffee
+++ src/proxy.coffee
@@ -43,15 +43,28 @@
         @port = null
         @process = null
         @stopTimeout = null
+        @state = @states.stopped
+        @readyCallbacks = []
+
+    states:
+        stopped: 0
+        starting: 1
+        running: 2

     start: (readyCallback) =>
         if @stopTimeout?
             clearTimeout @stopTimeout
         @stopTimeout = setTimeout @stop, @serviceManager.config.timeout

-        if @process?
+        if @state == @states.running
             readyCallback()
             return
+        if @state == @states.starting
+            @readyCallbacks.push readyCallback
+            return
+
+        @state = @states.starting
+

         freeport (err, port) =>
             @port = port
@@ -76,19 +89,20 @@
                 console.log "#{@name}: stopped, code #{code}, signal #{signal}"
                 @port = null
                 @process = null
+                @state = @states.stopped

             # wait until it starts responding to TCP connections before connecting
             tester = =>
                 if @process # don't keep trying to connect if the process crashes
-                    console.log "testing", @name
                     testClient = net.createConnection @port, () ->
                         testClient.end()
-                        console.log 'connection successful'
-                    testClient.on 'end', () ->
-                        console.log 'ready'
+                    testClient.on 'end', () =>
+                        # ready to run
+                        @state = @states.running
                         readyCallback()
+                        for c in @readyCallbacks
+                            c()
                     testClient.on 'error', (e) ->
-                        console.log e
                         if e.code in ['ECONNREFUSED']
                             setTimeout tester, 1000
             tester()


boundary checks

boundary checks

--- src/scripts/hubot-demolition-man.coffee
+++ src/scripts/hubot-demolition-man.coffee
@@ -46,7 +46,7 @@
     'wank',
     'wichser'
   ]
-  regex = new RegExp("\b(#{words.join('|')})\b", 'ig');
+  regex = new RegExp("\\b(#{words.join('|')})\\b", 'ig');

   robot.hear regex, (msg) ->
     credit = msg.message.text.match(regex).length


Forgot a return.

Forgot a return.

--- src/quest.coffee
+++ src/quest.coffee
@@ -218,7 +218,7 @@
       try
         result = cb()
         @emit 'retryLoopFinish', opts, result
-        result
+        return result
       catch e
         error = e
         console.trace e


Implement about and by.

Implement about and by.

--- src/quote.coffee
+++ src/quote.coffee
@@ -10,6 +10,8 @@
 # Commands:
 #   hubot quote - Retrieve a random quote.
 #   hubot quote <query> - Retrieve a random quote that contains each word of <query>.
+#   hubot quoteby <username> [<query>] - Retrieve a random quote including a line spoken by <username>.
+#   hubot quoteabout <username> [<query>] - Retrieve a random quote including a line addressing <username>.
 #   hubot howmany <query> - Return the number of quotes that contain each word of <query>.
 #   hubot reload quotes - Reload the quote file.
 #   hubot quotestats - Show who's been quoted the most!
@@ -56,20 +58,35 @@
       msg.reply "Just a moment, the quotes aren't loaded yet."
       false

-  queryFrom = (msg) ->
-    if msg.match[1]?
-      words = msg.match[1].trim().split /\s+/
+  queryFrom = (msg, matchNumber = 1) ->
+    if msg.match[matchNumber]?
+      words = msg.match[matchNumber].trim().split /\s+/
     else
       words = ['']
     _.filter words, (part) -> part.length > 0

-  quotesMatching = (query) ->
+  quotesMatching = (query = [], speaker = null, mention = null) ->
+    results = quotes
+
+    if speaker? or mention?
+      results = _.filter results, (quote) ->
+        match = false
+        for line in quote.split /\n/
+          m = line.match /^\[[^\]]+\] @?([^:]+): (.*)$/
+          if m?
+            [x, sp, rest] = m
+            match = true if speaker? and sp is speaker
+            match = true if mention? and rest.includes mention
+        match
+
     if query.length > 0
       rxs = (new RegExp(q, 'i') for q in query)
-      _.filter quotes, (quote) ->
+      results = _.filter results, (quote) ->
         _.every rxs, (rx) -> rx.test(quote)
-    else
-      quotes
+
+      results
+
+    results

   # Perform the initial load.
   reloadThen ->
@@ -84,6 +101,40 @@
       msg.send potential[chosen]
     else
       msg.send "That wasn't notable enough to quote. Try harder."
+
+  robot.respond /quoteby\s+@?(\S+)(\s+.*)?$/i, (msg) ->
+    return unless isLoaded(msg)
+
+    speaker = msg.match[1].trim()
+    query = queryFrom msg, 2
+
+    potential = quotesMatching query, speaker, null
+
+    if potential.length > 0
+      chosen = _.random potential.length - 1
+      msg.send potential[chosen]
+    else
+      m = "No quotes spoken by #{speaker}"
+      if query.length > 0
+        m += " about that"
+      msg.send m + '.'
+
+  robot.respond /quoteabout\s+@?(\S+)(\s+.*)?$/i, (msg) ->
+    return unless isLoaded(msg)
+
+    mention = msg.match[1].trim()
+    query = queryFrom msg, 2
+
+    potential = quotesMatching query, null, mention
+
+    if potential.length > 0
+      chosen = _.random potential.length - 1
+      msg.send potential[chosen]
+    else
+      m = "No quotes about #{mention}"
+      if query.length > 0
+        m += " about that"
+      msg.send m + '.'

   robot.respond /howmany(\s.*)/i, (msg) ->
     return unless isLoaded(msg)


Fixed bugs for number fields

Fixed bugs for number fields

--- coffee/fields/number.coffee
+++ coffee/fields/number.coffee
@@ -19,17 +19,25 @@
   """

   setup: (el, model, index) ->
-    if model.get(Formbuilder.options.mappings.MIN)
-      el.attr("min", model.get(Formbuilder.options.mappings.MIN))
-    if model.get(Formbuilder.options.mappings.MAX)
-      el.attr("max", model.get(Formbuilder.options.mappings.MAX))
-    if !model.get(Formbuilder.options.mappings.INTEGER_ONLY)
-      if model.get(Formbuilder.options.mappings.STEP)
-        el.attr("step", model.get(Formbuilder.options.mappings.STEP))
-      else
-        el.attr("step",'any')
-    if model.get(Formbuilder.options.mappings.DEFAULT_NUM_VALUE)
-      el.val(model.get(Formbuilder.options.mappings.DEFAULT_NUM_VALUE))
+  if model.get(Formbuilder.options.mappings.MIN)
+    el.attr("min", model.get(Formbuilder.options.mappings.MIN))
+  if model.get(Formbuilder.options.mappings.MAX)
+    el.attr("max", model.get(Formbuilder.options.mappings.MAX))
+
+  if !model.get(Formbuilder.options.mappings.INTEGER_ONLY) and model.get(Formbuilder.options.mappings.STEP)
+    if model.get(Formbuilder.options.mappings.STEP)
+      el.attr("step", model.get(Formbuilder.options.mappings.STEP))
+    else
+      el.attr("step",'any')
+  else if !model.get(Formbuilder.options.mappings.INTEGER_ONLY)
+    el.attr("step",'any')
+  else
+    if model.get(Formbuilder.options.mappings.STEP)
+      rounded_value = Math.round(model.get(Formbuilder.options.mappings.STEP))
+      el.attr("step", rounded_value)
+
+  if model.get(Formbuilder.options.mappings.DEFAULT_NUM_VALUE)
+    el.val(model.get(Formbuilder.options.mappings.DEFAULT_NUM_VALUE))

   clearFields: ($el, model) ->
     $el.find("[name = " + model.getCid() + "_1]").val("")


Ensure state with transitions is deleted first

--- src/spec/integration/states.spec.coffee
+++ src/spec/integration/states.spec.coffee
@@ -63,28 +63,29 @@
       done(JSON.stringify(error))

   it 'should create some states and use them as transitions references', (done) ->
-    Q.all _.map [1..101], => @client.states.save(newState())
+    Q.all _.map [1..51], => @client.states.save(newState())
     .then (results) =>
-      mainState = _.head results
+      mainState = _.head(results).body
       otherStates = _.tail results
       transitions = _.map otherStates, (r) ->
         id: r.body.id
         typeId: 'state'
-      @client.states.byId(mainState.body.id).update
-        version: mainState.body.version
+      @client.states.byId(mainState.id).update
+        version: mainState.version
         actions: [
           {action: 'setTransitions', transitions: transitions}
         ]
       .then (result) =>
         expect(result.statusCode).toBe 200
         mainState = result.body
-        expect(mainState.transitions.length).toBe 100
+        expect(mainState.transitions.length).toBe 50

         # delete states
-        Q.all _.map results, (r) => @client.states.byId(r.body.id).delete(r.body.version)
+        @client.states.byId(mainState.id).delete(mainState.version)
+      .then (result) =>
+        Q.all _.map otherStates, (r) => @client.states.byId(r.body.id).delete(r.body.version)
       .then (results) ->
         done()
     .fail (error) =>
       @logger.error error
       done(JSON.stringify(error))
-


microflo: Attempt to fix communication by using newer API

Goes to connected now after selecting runtime, but
still not seeing graph: messages

--- src/runtimes/microflo.coffee
+++ src/runtimes/microflo.coffee
@@ -8,6 +8,14 @@
     @connecting = false
     @buffer = []
     @container = null
+
+    # MicroFlo things
+    @transport = null
+    @microfloGraph = null
+    @debugLevel = 'Error'
+
+    @on 'connected', @updatecontainer
+
     super definition

   getElement: -> @container
@@ -25,37 +33,42 @@
     graph.on 'changeProperties', @updatecontainer
     super graph

+  openComm: (serialPort, baudRate) ->
+      microflo.serial.openTransport serialPort, baudRate, (err, transport) =>
+        @connecting = false
+        if err
+          console.log 'MicroFlo error:', err
+          @emit 'error', err
+          return
+        @transport = transport
+
+        @emit 'status',
+          online: true
+          label: 'connected'
+        @emit 'connected'
+
+        # Perform capability discovery
+        @send 'runtime', 'getruntime', null
+        @flush()
+
   connect: ->
-    unless @container
-      throw new Error 'Unable to connect without a parent element'
+    return if @connecting
+    @connecting = true
+    @microfloGraph = {}
+
+    # TODO: remove hardcoding of baudrate and debugLevel
+    baudRate = 9600
+    serialPort = @getAddress().replace 'serial://', ''

     # Make sure serial transport is closed before reopening
-    if @getSerial and @getSerial()
-      @getSerial().close () ->
-        #
-
-    # Let the UI know we're connecting
-    @connecting = true
-    @emit 'status',
-      online: false
-      label: 'connecting'
-
-    # Set an ID for targeting purposes
-    @container.id = 'preview-container'
-
-    # Update container contents as needed
-    @on 'connected', @updatecontainer
-
-    # Setup runtime
-    # TODO: remove hardcoding of baudrate and debugLevel
-    baudRate = 9600
-    debugLevel = 'Error'
-    address = @getAddress()
-    serialPort = address.replace 'serial://', ''
-    @setupRuntime baudRate, serialPort, debugLevel
-
-    # HACK: sends initial message, which hooks up receiving as well
-    @onLoaded()
+    if @transport
+      @transport.close () =>
+        @transport = null
+        @openComm serialPort, baudRate
+    else
+        f = () =>
+          @openComm serialPort, baudRate
+        setTimeout f, 0

   disconnect: ->
     onClosed = (success) ->
@@ -64,38 +77,15 @@
         label: 'disconnected'
       @emit 'disconnected'

-      if @getSerial and @getSerial()
-        @getSerial().close onClosed
+      if @transport
+        @transport.close onClosed
       else
         onClosed false

   updatecontainer: =>
-    return if !@container or !@graph
-    # TEMP
-
-  setupRuntime: (baudRate, serialPort, debugLevel) ->
-    @microfloGraph = {}
-    # FIXME: nasty and racy, should pass callback and only then continue
-    @debugLevel = debugLevel
-    @getSerial = null
-    try
-      @getSerial = microflo.serial.openTransport serialPort, baudRate, (err, transport) ->
-        console.log err, transport
-    catch e
-      console.log 'MicroFlo setup:', e
-
-  # Called every time the container has loaded successfully
-  onLoaded: =>
-    @connecting = false
-    @emit 'status',
-      online: true
-      label: 'connected'
-    @emit 'connected'
-
-    # Perform capability discovery
-    @send 'runtime', 'getruntime', null
-
-    @flush()
+    return unless @container
+    # Set an ID for targeting purposes
+    @container.id = 'preview-container'

   send: (protocol, command, payload) ->
     msg =
@@ -106,8 +96,9 @@
       @buffer.push msg
       return

+    console.log 'MicroFlo send:', msg
     sendFunc = (response) =>
-      console.log 'sendFunc', response
+      console.log 'MicroFlo receive:', response
       @onMessage { data: response }
     conn = { send: sendFunc }
     try


do not fetch extras for drafts

do not fetch extras for drafts

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -20,7 +20,7 @@
       else if @isDraft()
         url = "#{AUTHORING}/contents/#{id}.json" # FIX: Remove .json from URL
       else
-        url = "#{ARCHIVE}/contents/#{id}"
+        url = "#{ARCHIVE}/contents/#{id}.json"

       return url

@@ -36,7 +36,7 @@
       if @id
         @set('downloads', 'loading')

-        if @get('version') is 'draft'
+        if @get('version') is 'draft' or not @get('version') # HACK for Untitled module
           @set('downloads', [])
           @set('isLatest', true)
         else


Fix typo

Fix typo

--- shared/accountButtons.coffee
+++ shared/accountButtons.coffee
@@ -1,5 +1,4 @@
-Handlebars.registerHelper
-  "accountButtons", ->
+Handlebars.registerHelper "accountButtons", ->
     return new Handlebars.SafeString(Template.entryAccountButtons())

 Template.entryAccountButtons.helpers


fix case when address might not be defined (should be an error) -- found in log

--- src/smc-hub/local_hub_connection.coffee
+++ src/smc-hub/local_hub_connection.coffee
@@ -388,6 +388,9 @@
     new_socket: (cb) =>     # cb(err, socket)
         @dbg("new_socket")
         f = (cb) =>
+            if not @address?
+                cb("no address")
+                return
             connect_to_a_local_hub
                 port         : @address.port
                 host         : @address.host


Somehow simplified code

Somehow simplified code

--- lib/flash-loader.coffee
+++ lib/flash-loader.coffee
@@ -92,25 +92,17 @@
 useFlashLoc = 'auto'
 CLIFlashPath = ''
 reCLIArg = /^--flash-player=(.+)$/i
-reUseSpecialPath = /^@(auto|builtin|chrome|system)$/i
 parseCLIArg = (arg) ->
   if (m = reCLIArg.exec arg)?
     value = m[1]
     if value.startsWith '@'
-      if (m = reUseSpecialPath.exec value)?
-        useFlashLoc = m[1]
-      else
-        error "Invalid flash player location: '#{value}'."
-        error "Use one of [@auto, @builtin, @chrome, @system], \
-          or the path to your '#{filename}'"
+      useFlashLoc = value.slice 1
     else
       useFlashLoc = 'cli'
       CLIFlashPath = value
-    true
-  else
-    false
+  m?

-getPath = (loc) ->
+getPath = (loc = 'auto') ->
   switch loc
     when 'auto'
       flashPath = getBuiltInFlashPath()
@@ -130,10 +122,12 @@
     when 'cli'
       flashPath = CLIFlashPath if validatePath CLIFlashPath
       errMsg = "Invalid path to '#{filename}': \n#{CLIFlashPath}"
+    else
+      errMsg = "Invalid Pepper Flash Player location: '@#{loc}'."
   if not flashPath?
     error errMsg
     dbg.ex.flashLoader?.log 'Falling back to auto-detection'
-    flashPath = getPath 'auto'
+    flashPath = getPath()
   flashPath

 load = ->


Allow explicit git-ignore path inclusion

--- src/path-filter.coffee
+++ src/path-filter.coffee
@@ -62,7 +62,8 @@
     return true if @inclusions['directory']?.length && @isPathIncluded('directory', filepath)

     @isPathIncluded('directory', filepath) &&
-    !@isPathGloballyExcluded('directory', filepath)
+    !@isPathGloballyExcluded('directory', filepath) &&
+    !@isPathExcludedByGit(filepath)

   isPathAccepted: (fileOrDirectory, filepath) ->
     !@isPathExcluded(fileOrDirectory, filepath) &&
@@ -83,7 +84,6 @@
     return false

   isPathExcluded: (fileOrDirectory, filepath) ->
-    return true if @repo?.isIgnored(@repo.relativize(filepath))
     exclusions = @exclusions[fileOrDirectory]
     return false unless exclusions?.length

@@ -93,13 +93,14 @@
     return false

   isPathGloballyExcluded: (fileOrDirectory, filepath) ->
-    return true if @repo?.isIgnored(@repo.relativize(filepath))
-
     exclusions = @globalExclusions[fileOrDirectory]
     index = exclusions.length
     while index--
       return true if (exclusions[index].match(filepath))
     return false
+
+  isPathExcludedByGit: (filepath) ->
+    @repo?.isIgnored(@repo.relativize(filepath))

   sanitizePaths: (options) ->
     return options unless options.inclusions?.length


fix bad wording :hurtrealbad:

fix bad wording :hurtrealbad:

--- app/assets/javascripts/spree/frontend/gestpay/stubs/standard-ok.js.coffee
+++ app/assets/javascripts/spree/frontend/gestpay/stubs/standard-ok.js.coffee
@@ -1,4 +1,4 @@
 GestPay.SendPayment = (options, callback) ->
-  console.log "stocazzo"
+  console.log "Sending Stubbed Payment"

 console.log("[gestpay][stub] loaded stub standard-ok")


removed some logs from check table row view

--- app/assets/javascripts/states/monitoring/views/_check_table_row.js.coffee
+++ app/assets/javascripts/states/monitoring/views/_check_table_row.js.coffee
@@ -8,14 +8,11 @@
       "click #delete_check": (e) ->
         v = @
         @check.destroy success: (model, response) ->
-          console.log this
           v.parent.redraw()
       "click a.edit": ->
-      	@log "hi", @
       	@check_editor.show() # TODO load options here

     post_render: ->
-    	@log "post_render"
     	super
     	@check_editor ||= @partial(exports.CheckEditor, { check: @check, host: @host, hosts: @hosts })
     	@


Reduce heroku ping interval down to 5 minutes (from 20)

--- src/robot.coffee
+++ src/robot.coffee
@@ -297,7 +297,7 @@
       @pingIntervalId = setInterval =>
         HttpClient.create("#{herokuUrl}hubot/ping").post() (err, res, body) =>
           @logger.info 'keep alive ping!'
-      , 1200000
+      , 5 * 60 * 1000

   # Setup an empty router object
   #


Replace express.bodyParser with express.urlencoded() and express.json()

This gets rid of the two warnings reported in github/hubot/828
express.bodyParser() is deprecated in favor of only using the parts you need.
I've replaced it with only the parsing bits that I believe Hubot is using: url encoded and json.
If Hubot does in fact need to parse multipart uploads, this patch will not work.

--- src/robot.coffee
+++ src/robot.coffee
@@ -288,7 +288,13 @@

     app.use express.basicAuth user, pass if user and pass
     app.use express.query()
-    app.use express.bodyParser()
+
+    # Removing bodyParser since it's deprecated
+    # Note that Hubot no longer supports multipart uploads due to no longer using express.bodyParser
+    # In order to put support back for that, we would need to use one of: formidable, multiparty, busboy, multer, etc.
+    app.use express.urlencoded()
+    app.use express.json()
+
     app.use express.static stat if stat

     try


Remove some comments

Remove some comments

--- src/robot.coffee
+++ src/robot.coffee
@@ -258,7 +258,6 @@
       for line in body.split "\n"
         break unless line[0] is '#' or line.substr(0, 2) is '//'

-        # remove leading comment
         cleanedLine = line.replace(/^(#|\/\/)\s?/, "").trim()

         continue if cleanedLine.length is 0
@@ -268,14 +267,12 @@
         if nextSection in HUBOT_DOCUMENTATION_SECTIONS
           currentSection = nextSection
           scriptDocumentation[currentSection] = []
-        # lines in a section _do_ have leading whitespace
         else
           if currentSection
             scriptDocumentation[currentSection].push cleanedLine.trim()
             if currentSection is 'commands'
               @commands.push cleanedLine.trim()

-      # no current section? probably using old style documentation
       if currentSection is null
         @logger.info "#{path} is using deprecated documentation syntax"
         scriptDocumentation.commands = []


fixing exercise preview

fixing exercise preview

--- src/components/exercise.cjsx
+++ src/components/exercise.cjsx
@@ -101,14 +101,15 @@
     </div>

   render: ->
-    console.log "RENDER EX"
-
     id = @getId()
     if not ExerciseStore.get(id) and not ExerciseStore.isFailed(id)
       if not ExerciseStore.isLoading(id) then ExerciseActions.load(id)
       return @renderLoading()
     else if ExerciseStore.isFailed(id)
       return @renderFailed()
+
+    ExerciseActions.sync(id)
+    exercise = ExerciseStore.get(id)

     exerciseUid = ExerciseStore.getId(id)
     preview = <Preview exercise={exercise} closePreview={@closePreview}/>


Fixing the name to not conflict with help

--- src/scripts/helpscout.coffee
+++ src/scripts/helpscout.coffee
@@ -5,9 +5,9 @@
 #   HUBOT_HELPSCOUT_API_KEY - Go to Your Profile -> API Keys
 #
 # Commands:
-#   hubot helpscout mailboxes - list Helpscout mailboxes
-#   hubot helpscout count MAILBOX_ID - Return the number of active tickets in a mailbox
-#   hubot helpscout users MAILBOX_ID - List the number of active tickets each user has in a mailbox
+#   hubot hs mailboxes - list Helpscout mailboxes
+#   hubot hs count MAILBOX_ID - Return the number of active tickets in a mailbox
+#   hubot hs users MAILBOX_ID - List the number of active tickets each user has in a mailbox
 #
 # Author:
 #   Brett Hardin (http://bretthard.in)
@@ -26,7 +26,7 @@

 module.exports = (robot) ->
   # hubot helpscout users FOLDER_ID
-  robot.hear /^helpscout users\s?(@\w+)?(.*)/i, (msg) ->
+  robot.respond /hs users\s?(@\w+)?(.*)/i, (msg) ->
     if api_key
       mailboxId = msg.match[2] # Second Term

@@ -55,7 +55,7 @@
       msg.send "Don't have the HUBOT_HELPSCOUT_API_KEY."

   # hubot helpscout count FOLDER_ID
-  robot.hear /^helpscout count\s?(@\w+)?(.*)/i, (msg) ->
+  robot.respond /hs count\s?(@\w+)?(.*)/i, (msg) ->
     if api_key
       mailboxId = msg.match[2] # Second Term

@@ -67,7 +67,7 @@
       msg.send "Don't have the HUBOT_HELPSCOUT_API_KEY."

   # hubot helpscout mailboxes
-  robot.hear /^helpscout mailboxes\s?(.*)?/i, (msg) ->
+  robot.respond /hs mailboxes\s?(.*)?/i, (msg) ->
     if api_key
       getRequest msg, "/mailboxes.json", (err, res, body) ->
         response = JSON.parse body


Various fix

Various fix

--- lib/linter-clang.coffee
+++ lib/linter-clang.coffee
@@ -13,9 +13,10 @@

   # A string, list, tuple or callable that returns a string, list or tuple,
   # containing the command line (with arguments) used to lint.
-  cmd: '-fsyntax-only -fno-caret-diagnostics -fexceptions'
+
   isCpp: false
   clang: null
+  @cmd: ''

   executablePath: null

@@ -28,24 +29,31 @@
   fileName: ''

   lintFile: (filePath, callback) ->
-    # save cmd to oldCmd
-    oldCmd = @cmd
+    @cmd = 'clang -fsyntax-only -fno-caret-diagnostics -fexceptions'

-    @clang = atom.config.get 'linter-clang.clangCommand'
+    {command, args} = @getCmdAndArgs(filePath)
+    @fileName = args[3]
+    args[3] = ''
+
+    command = @clang = atom.config.get 'linter-clang.clangCommand'
     if atom.inDevMode()
       console.log 'clang-command: ' + @clang

-    @cmd = "#{@clang} #{@cmd} -x #{@language}"
+    args.push '-x'
+    args.push @language
+    args.push @flag

     if @isCpp
-      @cmd += ' ' + atom.config.get 'linter-clang.clangDefaultCppFlags'
+      # TODO: loop flags separate from blank space
+      args.push atom.config.get 'linter-clang.clangDefaultCppFlags'
     else
-      @cmd += ' ' + atom.config.get 'linter-clang.clangDefaultCFlags'
+      args.push atom.config.get 'linter-clang.clangDefaultCFlags'

-    @cmd += ' -ferror-limit=' + atom.config.get 'linter-clang.clangErrorLimit'
+    args.push "-ferror-limit=#{atom.config.get 'linter-clang.clangErrorLimit'}"
+    args.push '-w' if atom.config.get 'linter-clang.clangSuppressWarnings'

     if atom.config.get 'linter-clang.clangCompleteFile'
-      @cmd += ' ' + ClangFlags.getClangFlags(@editor.getPath()).join ' '
+      args.push ClangFlags.getClangFlags(@editor.getPath()).join

     includepaths = atom.config.get 'linter-clang.clangIncludePaths'

@@ -56,23 +64,27 @@
       file = file.replace(/(\r\n|\n|\r)/gm, ' ')
       includepaths = "#{includepaths} #{file}"

-    split = includepaths.split " "
+    #split = includepaths.split " "
+    # split the include paths, taking care of quotes
+    regex = /[^\s"]+|"([^"]*)"/gi
+    includepathsSplit = []

-    # concat includepath
-    for custompath in split
+    loop
+      match = regex.exec includepaths
+      if match
+        includepathsSplit.push(if match[1] then match[1] else match[0])
+      else
+        break
+
+    # add includepaths
+    for custompath in includepathsSplit
       if custompath.length > 0
-        @cmd = "#{@cmd} -I #{custompath}"
+        # @cmd = "#{@cmd} -I #{custompath}"
         # if the path is relative, resolve it
         # TODO: if path contain blank space!!!
-        # custompathResolved = path.resolve(atom.project.getPaths()[0], custompath)
-        # @cmd = "#{@cmd} -I \"#{custompathResolved}\""
-        # custompathResolved = custompathResolved.replace(/\ /g, '\\ ')
-        # @cmd = "#{@cmd} -I #{custompathResolved}"
-
-    if atom.config.get 'linter-clang.clangSuppressWarnings'
-      @cmd = "#{@cmd} -w"
-    # build the command with arguments to lint the file
-    {command, args} = @getCmdAndArgs(filePath)
+        custompathResolved = path.resolve(atom.project.getPaths()[0], custompath)
+        args.push '-I'
+        args.push custompathResolved

     # add file to regex to filter output to this file,
     # need to change filename a bit to fit into regex
@@ -80,7 +92,7 @@
     ':(?<line>\\d+):.+: .*((?<error>error)|(?<warning>warning)): (?<message>.*)'

     if atom.inDevMode()
-      console.log 'is node executable: ' + @isNodeExecutable
+      console.log 'linter-clang: is node executable: ' + @isNodeExecutable

     # use BufferedNodeProcess if the linter is node executable
     if @isNodeExecutable
@@ -93,38 +105,44 @@

     stdout = (output) =>
       if atom.inDevMode()
-        console.log 'stdout', output
+        console.log 'clang: stdout', output
       if @errorStream == 'stdout'
         @processMessage(output, callback)

     stderr = (output) =>
       if atom.inDevMode()
-        console.warn 'stderr', output
+        console.warn 'clang: stderr', output
       if @errorStream == 'stderr'
         @processMessage(output, callback)

+    args.push @fileName
+
     if atom.inDevMode()
-      console.log "command = #{command}, args = #{args}, options = #{options}"
+      console.log "clang: command = #{command}, args = #{args}, options = #{options}"

     new Process({command, args, options, stdout, stderr})

     # restore cmd
-    @cmd = oldCmd
+    # @cmd = oldCmd

   constructor: (editor) ->
     @editor = editor

     if editor.getGrammar().name == 'C++'
-      @language = 'c++ -std=c++11'
+      @language = 'c++'
+      @flag = '-std=c++11'
       @isCpp = true
     if editor.getGrammar().name == 'Objective-C++'
       @language = 'objective-c++'
+      @flag = ''
       @isCpp = true
     if editor.getGrammar().name == 'C'
-      @language = 'c -std=c11'
+      @language = 'c'
+      @flag = ''
       @isCpp = false
     if editor.getGrammar().name == 'Objective-C'
       @language = 'objective-c'
+      @flag = ''
       @isCpp = false

     super(editor)


:bug: Show full paths in bubble trace

:bug: Show full paths in bubble trace

--- lib/linter-views.coffee
+++ lib/linter-views.coffee
@@ -104,7 +104,7 @@
     bubble.id = 'linter-inline'
     bubble.appendChild Message.fromMessage(message)
     if message.trace then message.trace.forEach (trace)->
-      bubble.appendChild Message.fromMessage(trace)
+      bubble.appendChild Message.fromMessage(trace, true)
     bubble

   _renderPanel: ->


Remove :large suffix to usefully expand on limechat

--- src/scripts/hubot-zoi.coffee
+++ src/scripts/hubot-zoi.coffee
@@ -17,7 +17,7 @@
       cb.onError(err)
     else
       zois = body.match(/image: 'https:\/\/pbs.twimg.com\/media\/.+.jpg:large'/mg).
-                  map((l) -> l.match("'(.+)'")[1])
+                  map((l) -> l.match("'(https.+\.jpg):large'")[1])
       (cb.onZoi || cb.onSuccess)(zois)
   )


Pass sync to format type component

Pass sync to format type component

--- src/components/question.cjsx
+++ src/components/question.cjsx
@@ -94,7 +94,7 @@
       {<BS.Alert bsStyle="warning"
         >To save your work, you must fill out the {invalid.part}</BS.Alert> unless invalid.valid}

-      <QuestionFormatType questionId={id} />
+      <QuestionFormatType sync={@props.sync} questionId={id} />

       <BS.Input type="checkbox" label="Order Matters"
         onChange={@preserveOrderClicked}


:bug: Fix missing observers of editor configs

--- lib/minimap-view.coffee
+++ lib/minimap-view.coffee
@@ -150,6 +150,15 @@
       newOptionValue = atom.config.get 'minimap.displayCodeHighlights'
       @setDisplayCodeHighlights(newOptionValue)

+    @subscriptions.add @asDisposable atom.config.observe 'editor.lineHeight', =>
+      @updateMinimapView()
+
+    @subscriptions.add @asDisposable atom.config.observe 'editor.fontSize', =>
+      @updateMinimapView()
+
+    @subscriptions.add @asDisposable atom.config.observe 'editor.softWrap', =>
+      @updateMinimapView()
+
   # Internal: Computes the scale of the minimap display relatively to the
   # corresponding editor view.
   # The scale factor are used to map scrolling and offset from the minimap


revert forum script

revert forum script

--- src/site/mtgsalvation.coffee
+++ src/site/mtgsalvation.coffee
@@ -1,7 +1,8 @@
 autoCardRegex = /AutoCard(?:Gatherer)?\('(.*?)'(?:,\s?'.*')?\)/

-$('a.autocardhref').scry {
+$('#posts').scry {
     query : ->
         match = autoCardRegex.exec $(this).attr('onclick')
         return unescape match[1]
+    selector : 'a.autocardhref'
 }


Code refactoring

Code refactoring

--- client/app/lib/stacks/stacktemplatelistcontroller.coffee
+++ client/app/lib/stacks/stacktemplatelistcontroller.coffee
@@ -72,9 +72,7 @@
       for stack in stacks
         [item] = listItems.filter (i) -> i.getData()._id is stack.baseStackId
         if item
-          stackTemplate = item.getData()
-          stackTemplate.inUse = yes
-          item.setData stackTemplate
+          item.getData().inUse = yes
           item.updateLabels()


 Merge branch 'master.activity.messagepane' into 'master.activity'

activity: pass message as item data parameter to `KDListViewController::removeItem` method

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -49,7 +49,7 @@

   prependMessage: (message) -> @listController.addItem message, 0

-  removeMessage: (message) -> @listController.removeItem message
+  removeMessage: (message) -> @listController.removeItem null, message

   viewAppended: ->


Build css

Build css

--- configs/webpack/base.coffee
+++ configs/webpack/base.coffee
@@ -3,6 +3,7 @@
 _ = require 'lodash'
 webpack = require 'webpack'
 ExtractTextPlugin = require 'extract-text-webpack-plugin'
+

 LOADERS =
   babel:  'babel-loader'
@@ -13,7 +14,6 @@
   style:  'style-loader'
   css:    'css-loader'
   less:   'less-loader'
-#  lessCompiled: ExtractTextPlugin.extract('css!less')

 RESOLVABLES =
   js:     { test: /\.js$/,     use: LOADERS.babel, exclude: /node_modules/}
@@ -34,7 +34,12 @@
   jsx:    RESOLVABLES.jsx
   coffee: RESOLVABLES.coffee
   cjsx:   RESOLVABLES.cjsx
-  less: { test: /\.less$/,  use: [ LOADERS.style, LOADERS.css, LOADERS.less ] }
+  less: {
+    test: /\.less$/, use: ExtractTextPlugin.extract({
+      fallback: 'style-loader',
+      use: [ LOADERS.css, LOADERS.less ]
+    })
+  }

 DEV_LOADERS = ['react-hot-loader/webpack']

@@ -68,7 +73,9 @@
       /\/sinon\.js/
     ]
     rules: _.values(STATICS)
-  plugins: [ ]
+  plugins: [
+
+  ]

 mergeWebpackConfigs = ->
@@ -97,9 +104,9 @@
   styleFilename ?= '[name].css'

   [
-    new ExtractTextPlugin({
-      filename: styleFilename
-    })
+    new ExtractTextPlugin(
+      styleFilename
+    )
   ]

@@ -192,7 +199,7 @@
       # to see success build.
       noInfo: false
       host: "#{host}",
-      filename: 'tutor.js'
+      filename: '[name].js'
       hot: true
       stats:
         # Config for minimal console.log mess.


remove extra debug

remove extra debug

--- src/deployinate-model.coffee
+++ src/deployinate-model.coffee
@@ -84,7 +84,6 @@

   _startService: (service, callback=->) =>
     debug '_startService', service
-    debug "fleetctl start #{service}"
     exec "fleetctl start #{service}", (error, stdout, stderr) =>
       debug 'startService error:', error.message if error?
       debug 'startService stdout:', stdout if stdout?


Ability to move edges, as well as pan/zoom

--- graphedit-coffee/coffee/graphedit.coffee
+++ graphedit-coffee/coffee/graphedit.coffee
@@ -33,16 +33,23 @@
 	.append "g"

 # init nodes
-node_data = ({id:a, reflexive:false} for a in [1..100])
+node_data = ({id:a, reflexive:false} for a in [1..10])
+link_data = [{"source":1, "target":3, "value":4}, {"source":1, "target":5, "value":4}]

 #animation
 tick = () ->
 	nodes.attr "cx", (d) -> d.x
 	nodes.attr "cy", (d) -> d.y

+	links.attr "x1", (d) -> d.source.x
+	links.attr "y1", (d) -> d.source.y
+	links.attr "x2", (d) -> d.target.x
+	links.attr "y2", (d) -> d.target.y
+
 #force layout
 force = d3.layout.force()
 		.nodes node_data
+		.links link_data
 		.size [width, height]
 		.linkDistance 150
 		.charge -500
@@ -51,6 +58,23 @@

 mousedown_node = false

+links = svg.selectAll ".link"
+		.data link_data
+		.enter()
+		.append "line"
+		.attr "class", "link"
+		.style "stroke", "#000"
+		.style "stroke-width", () -> 1
+
+		# override to turn off force layout
+		"""
+		.call force.drag().on "drag.force", () ->
+			d3.select(this).attr "transform", "translate(" + d3.event.x + "," + d3.event.y + ")"
+
+		.call force.drag().origin () ->
+			t = d3.transform(d3.select(this).attr("transform")).translate
+			{x:t[0], y:t[1]}
+		"""
 #draw data
 nodes = svg
 		.selectAll ".node"
@@ -61,23 +85,27 @@
 		.attr "r", 5
 		.style "fill", (d) -> colors(d.id)

-		# override to turn off force layout
-		.call force.drag().on "drag.force", () ->
-			d3.select(this).attr "transform", "translate(" + d3.event.x + "," + d3.event.y + ")"
-
-		.call force.drag().origin () ->
-			t = d3.transform(d3.select(this).attr("transform")).translate
-			{x:t[0], y:t[1]}
-
 		.on "mousedown", (d) ->
 			mousedown_node = true
-			scale = zoom.scale()
-			translate = zoom.translate()
+			#scale = zoom.scale()
+			#translate = zoom.translate()
 			select this
 		.on "mouseup", (d) ->
 			mousedown_node = false
-			zoom.scale scale
-			zoom.translate translate
+			#zoom.scale scale
+			#zoom.translate translate
+
+setPanMode = (turn_on) ->
+	if turn_on
+		console.log "drag on"
+		nodes.call force.drag
+	else
+		console.log "drag off"
+		nodes
+			.on "mousedown.drag", null
+			.on "touchstart.drag", null
+
+setPanMode(true)

 # mark a node as selected
 select = (node) ->
@@ -105,7 +133,7 @@

 stop = () ->
 	console.log("stopped")
-	force.stop()
+	#force.stop()
 setTimeout stop, 1000

 console.log nodes


STL/dom moving the node_list_test from vows to mocha

--- stl/dom/test/unit/node_list_test.coffee
+++ stl/dom/test/unit/node_list_test.coffee
@@ -1,11 +1,11 @@
 #
 # The `NodeList` module unit tests
 #
-# Copyright (C) 2011 Nikolay Nemshilov
+# Copyright (C) 2011-2012 Nikolay Nemshilov
 #
-{describe, assert, server, load} = require('../test_helper')
+{Browser} = require('../test_helper')

-server.respond "/node_list.html": """
+Browser.respond "/node_list.html": """
   <html>
     <head>
       <script src="/core.js"></script>
@@ -20,42 +20,50 @@
   """

-describe 'NodeList', module,
-  "constructor":
-    topic: ->
-      load "/node_list.html", this, (dom)-> dom.NodeList
+describe 'NodeList', ->
+  describe "\b#constructor", ->
+    get = (callback)->
+      (done)->
+        Browser.open "/node_list.html", ($, window)->
+          callback($.NodeList, $, window, window.document)
+          done()

-    "should accept a list of raw dom-elements as an argument": (NodeList)->
-      element1 = this.document.getElementById('one')
-      element2 = this.document.getElementById('two')
+    it "should accept a list of raw dom-elements as an argument", get (NodeList, $, window, document)->
+      element1 = document.getElementById('one')
+      element2 = document.getElementById('two')

       search   = new NodeList([element1, element2])

-      assert.instanceOf search,      NodeList
-      assert.lengthOf   search,      2
-      assert.instanceOf search[0],   this.Element
-      assert.instanceOf search[0],   this.Element
-      assert.same       search[0]._, element1
-      assert.same       search[1]._, element2
+      search.should.be.instanceOf NodeList
+      search.should.have.length   2
+      search[0].should.be.instanceOf $.Element
+      search[1].should.be.instanceOf $.Element
+      search[0]._.should.equal element1
+      search[1]._.should.equal element2

-  "DOM methods":
-    topic: ->
-      load "/node_list.html", this, (dom)->
-        new dom.NodeList([
-          this.document.getElementById('one'),
-          this.document.getElementById('two'),
-          this.document.getElementById('three')])

-    "should call the setter methods on every item on the list": (list)->
+  describe "DOM methods", ->
+    get = (callback)->
+      (done)->
+        Browser.open "/node_list.html", ($, window)->
+          list = new $.NodeList([
+            window.document.getElementById('one'),
+            window.document.getElementById('two'),
+            window.document.getElementById('three')])
+
+          callback(list, $, window, window.document)
+          done()
+
+    it "should call the setter methods on every item on the list", get (list)->
       list.setClass('test')
-      assert.deepEqual list.map('getClass').toArray(), ['test', 'test', 'test']
+      list.map('getClass').toArray().should.eql ['test', 'test', 'test']

-    "should return search itself back to the code by default": (list)->
-      assert.same list.addClass('test'), list
+    it "should return search itself back to the code by default", get (list)->
+      list.addClass('test').should.equal list

-    "should return the result of the first element when calls a getter method": (list)->
-      assert.equal list.attr('id'), 'one'
+    it "should return the result of the first element when calls a getter method", get (list)->
+      list.attr('id').should.eql 'one'

-    "should return 'null' back when the NodeList is empty": (list)->
-      assert.isNull new this.NodeList([]).addClass('test')
+    it "should return 'null' back when the NodeList is empty", get (list, $)->
+      (new $.NodeList([]).addClass('test') is null).should.be.true


fix degradation, should return whenever search was not found #510

--- lib/search-model.coffee
+++ lib/search-model.coffee
@@ -29,7 +29,8 @@
         if settings.get('flashScreenOnSearchHasNoMatch')
           @vimState.flash(getVisibleBufferRange(@editor), type: 'screen')
           atom.beep()
-          return
+
+        return

       if settings.get('showHoverSearchCounter')
         hoverOptions =


bind location stats properly

bind location stats properly

--- src/layout/inspect_status.coffee
+++ src/layout/inspect_status.coffee
@@ -19,8 +19,8 @@
     }]
   }
 extract = (sprite, button) ->
-  key = button.get_plain 'value'
-  value = sprite.get_plain key
+  key = button.get 'value'
+  value = sprite.get key
   value = value.all() if sprite.is_array(value)
   "#{value}"


fixed a bug where scripts couldnt send commands

--- src/script/script_environment.coffee
+++ src/script/script_environment.coffee
@@ -15,8 +15,10 @@
     args = [name].concat(args)
     name = type
     type = opt_context
-    opt_context = {}
-  event = {opt_context, type, name, args}
+    context = {}
+  else
+    context = opt_context
+  event = {context, type, name, args}
   window.parent.postMessage(event, '*')

 @propagate = (event, propagation='all') ->


Use exact queries for searching ElasticSearch

--- src/scripts/control.search.js.coffee
+++ src/scripts/control.search.js.coffee
@@ -138,7 +138,8 @@
       method: 'get'
       url: 'http://scholar.arcticconnect.org/arctic/_search'
       data:
-        q: "GH:*#{query}*"
+        default_operator: "AND"
+        q: "GH:#{query}"
         size: 100
         sort: "SISN:desc"
     }).done((results) =>


regex tweaks

regex tweaks

--- lib/build.coffee
+++ lib/build.coffee
@@ -109,9 +109,11 @@
       if(atom.workspace.getActiveTextEditor().buffer?.file?)
         isWin = /^win/.test(process.platform)
         path = atom.workspace.getActiveTextEditor().buffer.file.path
-        projectPath = if isWin then @root+'\\\\src\\\\' else @root+'/src/'
+        projectPath = if isWin then @root+'\\src\\\\' else @root+'/src/'
         console.log('projectPath :'+projectPath)
         pathHeRegex = ///#{projectPath}///
+        console.log('path :'+path)
+        console.log('pathHeRegex : '+pathHeRegex)
         if(path.match(pathHeRegex))
           fileBaseName = atom.workspace.getActiveTextEditor().buffer.file.getBaseName()
           folderNamePath = path.replace ///#{fileBaseName}///, ''


:bug: Append HTML instead of Text

:bug: Append HTML instead of Text

--- lib/tooltip-view.coffee
+++ lib/tooltip-view.coffee
@@ -6,14 +6,15 @@
     @content: ->
       @div class: 'ide-hack-tooltip'

-    initialize: (@rect, text = null) ->
-      @text(text) if text?
+    initialize: (@rect, @LeMessage) ->
+      @append LeMessage
       $(document.body).append this
       @updatePosition()

     # update tooltip text
     updateText: (text) ->
-      @text(text)
+      @empty()
+      @append @LeMessage
       @updatePosition()

     # smart position update


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -35,6 +35,15 @@
     easing: "quadratic"

+

+  player = GameObject()
+
+  player.tween 30,
+    x: 50
+    y: 50
+    easing: "quadratic"
+

+
   @name tween
   @methodOf Tween#
   @param {Number} duration How long (in frames) until the object's properties reach their final values.


[fix] missing comma

[fix] missing comma

--- src/redis-model.coffee
+++ src/redis-model.coffee
@@ -65,7 +65,7 @@
   allJobs: (queue = "*", callback) ->
     ideally = errify callback

-    await @redis.keys "bull:#{queue}:[0-9]*" ideally defer jobsWithLocks
+    await @redis.keys "bull:#{queue}:[0-9]*", ideally defer jobsWithLocks
     result = (jobWithLock for jobWithLock in jobsWithLocks when jobWithLock[-5..] isnt ":lock")
     callback null, result

@@ -276,7 +276,7 @@
       active  = []
       stalled = []
       for job in allActive
-        await @redis.get "#{name}:#{job}:lock" ideally defer lock
+        await @redis.get "#{name}:#{job}:lock", ideally defer lock
         if lock? then active.push  job
         else          stalled.push job


fix query() when duration is specified

fix query() when duration is specified

--- lib/index.coffee
+++ lib/index.coffee
@@ -16,7 +16,7 @@
     execute = (params) ->
       db.prepare(text).then (stmt) ->
         stmt.execute(params).then (results) ->
-          cache.store text, results.rows if options.duration > 0
+          cache.store text, results.rows, options.duration if options.duration > 0
           queried.resolve results.rows
         .done()
       .done()


remove linebreaks so title display as one line

--- src/scripts/web.coffee
+++ src/scripts/web.coffee
@@ -21,11 +21,11 @@
             parser.parseComplete body
             results = (Select handler.dom, "head title")
             if results[0]
-              msg.send results[0].children[0].data
+              msg.send results[0].children[0].data.replace(/(\r\n|\n|\r)/gm,"")
             else
               results = (Select handler.dom, "title")
               if results[0]
-                msg.send results[0].children[0].data
+                msg.send results[0].children[0].data.replace(/(\r\n|\n|\r)/gm,"")
           else
             msg.send "Error " + res.statusCode

@@ -39,7 +39,7 @@
           format: "json"
         .get() (err, res, body) ->
           response = JSON.parse body
-          responseTitle = response.data.info[0].title
+          responseTitle = response.data.info[0].title.replace(/(\r\n|\n|\r)/gm,"")
           if responseTitle
             msg.send if response.status_code is 200 then responseTitle else response.status_txt
           else


make session key configureable -- for kiosk mode

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -8,7 +8,7 @@
   FACEBOOK_APP_NAMESPACE, MOBILE_MEDIA_QUERY, MOBILE_URL, APP_URL, REDIS_URL, DEFAULT_CACHE_TIME,
   CANONICAL_MOBILE_URL, IMAGES_URL_PREFIX, SECURE_IMAGES_URL, GOOGLE_ANALYTICS_ID, MIXPANEL_ID,
   COOKIE_DOMAIN, AUTO_GRAVITY_LOGIN, GOOGLE_MAPS_API_KEY, ADMIN_URL, CMS_URL,
-  DELTA_HOST, ENABLE_AB_TEST, KIOSK_MODE, KIOSK_PAGE } = config = require "../config"
+  DELTA_HOST, ENABLE_AB_TEST, KIOSK_MODE, KIOSK_PAGE, SESSION_COOKIE_KEY } = config = require "../config"
 { parse, format } = require 'url'

 _ = require 'underscore'
@@ -109,7 +109,7 @@
   app.use session
     secret: SESSION_SECRET
     domain: COOKIE_DOMAIN
-    key   : 'force.sess'
+    key   : SESSION_COOKIE_KEY
     maxage: SESSION_COOKIE_MAX_AGE
   app.use artsyPassport _.extend config,
     CurrentUser: CurrentUser


Do return something for the jsonp transport - makes rekonq happy

--- src/trans-jsonp.coffee
+++ src/trans-jsonp.coffee
@@ -60,5 +60,5 @@
             jsonp.didMessage(message)

         res.writeHead(200)
-        res.end()
+        res.end('ok')
         return true


Tweak messaging

Tweak messaging

--- lifeguard.coffee
+++ lifeguard.coffee
@@ -223,7 +223,7 @@
     # increment our start count
     @startCount += 1

-    @instance.on "start", => @_notifyRestart()
+    @instance.on "start",   => @_notify "Starting up!"
     @instance.on "restart", => @_notifyRestart()

   #----------
@@ -354,7 +354,7 @@
       @d = require("domain").create()

       @d.on "error", (err) =>
-        console.error "Campfire error: ", err
+        console.error "Slack error: ", err

       @d.run =>
         @slack = new (require "node-slack") @opts.team, @opts.token


add comments

add comments

--- src/utils/Timer.coffee
+++ src/utils/Timer.coffee
@@ -1,22 +1,43 @@
+# A classic millisecond timer
+#
+# @example How to create a new timer
+#   var timer = new Rotten.Timer({ timeout: 1000, interval: 100 })
+#
 class Rotten.Timer extends Rotten.EventManager

+
+    # Constructs a new timer
+    #
+    # @param {Object} options Timer options
+    # @option options {int} timeout Your timeout (in milliseconds)
+    # @option options {int} interval Your interval (in milliseconds)
+    # @option options {boolean} autostart Whether you want your timer to autostart or not
     constructor: (options) ->

+        # The timeout of your timer
         @timeout = options.timeout or 10000

+        # The interval between two ticks or your timer
         @interval = options.interval or 1000

+        # Whether your timer should start automatically when initialized
         @autostart = options.autostart or true

+        # Whether your timer is active or not
         @active = options.autostart or true

+        # Current time of your timer
         @time = 0

+        # Last tick of your timer
         @lastTick = new Date()

         @.fire "start", { timeout: @timeout } if @autostart is true

+    # Updates your timer
+    # If your timer was not added to a scene, update() should be called on every
+    # round of your game loop (in the update queue)
     update: ->
         if @active is true

@@ -29,6 +50,7 @@
                 @.fire "tick", { current: @time, remaining: @timeout - @time, completion: parseFloat @time / @timeout }

+    # Resets your timer : Put it back to 0 & fire a "timeout" event
     reset: ->
         @time = 0
         @lastTick = new Date()
@@ -37,12 +59,14 @@
         if @autostart is true then @.restart() else @active = false

+    # Stops your timer : nothing is counting time anymore. We are doomed.
     stop: ->
         @time = 0
         @lastTick = new Date()
         @.fire "stop", {}

+    # Restarts your timer & fires a "start" event
     restart: ->
         @time = 0
         @lastTick = new Date()
@@ -50,5 +74,9 @@
         @active = true

+    # Get the remaining time of your timer
+    # Useful when you're making a timed game
+    #
+    # @return {float} Remaining time
     getRemainingTime: ->
         @timeout - @time


move add code entries simultaneously

move add code entries simultaneously

--- tree2.coffee
+++ tree2.coffee
@@ -292,12 +292,23 @@
 class CodeNode extends Node
   constructor: (@symbol, @code) ->
     @error = null
+    @width = @radius

   render: (ctx) ->
     ctx.save()
     setStyle(ctx, node_text_style)
-    ctx.fillText("#{@symbol} = #{@code}", @x, @y)
+    ctx.textAlign = 'left'
+    ctx.textBaseline = 'top'
+    msg = "#{@symbol} = #{@code}"
+    ctx.fillText(msg, @x, @y)
+    @width = ctx.measureText(msg).width
     ctx.restore()
+
+  isHit: (pos) ->
+    dx = pos.x - @x
+    dy = pos.y - @y
+
+    dy > 0 && dy < node_text_spacing && dx > 0 && dx < @width

 collection_dropdown_menu = [
   {
@@ -919,6 +930,18 @@
       codeobj.y = y
       y += node_text_spacing

+  mousemove: (pos) ->
+    if @selected? and @selected.node in @nodes
+      s = @selected
+      dx = pos.x - s.mx + s.ox - s.node.x
+      dy = pos.y - s.my + s.oy - s.node.y
+
+      for n in @nodes
+        n.x += dx
+        n.y += dy
+    else
+      super pos
+
 lerp2d = (t, p0, p1) ->
   if t < 0
     t = 0


Don't set the size in the plugin, the site owner can set this using css

--- src/plugins/image/current.coffee
+++ src/plugins/image/current.coffee
@@ -95,31 +95,13 @@

       tmpImg = new Image()

-      # Calculate the size for the image
-      maxWidth = @options.maxWidth
-      maxHeight = @options.maxHeight
       jQuery(tmpImg).bind 'load', ->
-        width = tmpImg.width
-        height = tmpImg.height
-
-        if width > maxWidth or height > maxHeight
-          if width > height
-            ratio = (tmpImg.width / maxWidth).toFixed()
-          else
-            ratio = (tmpImg.height / maxHeight).toFixed()
-          width = (tmpImg.width / ratio).toFixed()
-          height = (tmpImg.height / ratio).toFixed()
-
-        imageInsert.attr
-          width: width
-          height: height
-
       tmpImg.src = image.src

       imageInsert.attr
         src: tmpImg.src
         alt: jQuery(image).attr('alt') unless tmp
-        class: if tmp then 'halloTmp' else ''
+        class: if tmp then 'halloTmp' else 'imageInText'

       imageInsert.show()
       imageInsert
@@ -128,6 +110,9 @@
       jQuery('<div/>').addClass 'halloTmpLine'

     _removeFeedbackElements: ->
+      @overlay.big.remove()
+      @overlay.left.remove()
+      @overlay.right.remove()
       jQuery('.halloTmp, .halloTmpLine', @options.editable.element).remove()

     _removeCustomHelper: ->


let the user know when membership policy is updated

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -695,8 +695,10 @@
     membershipPolicyView.enableInvitations.setValue policy.invitationsEnabled

   updateMembershipPolicy:(group, policy, formData, membershipPolicyView, callback)->
-    group.modifyMembershipPolicy formData, ->
-      policy.emit 'MembershipPolicyChangeSaved'
+    group.modifyMembershipPolicy formData, (err)->
+      policy.emit 'MembershipPolicyChangeSaved'  unless err
+      KD.showError err
+      new KDNotificationView {title:"Membership policy has been updated."}

   editPermissions:(group)->
     group.getData().fetchPermissions (err, permissionSet)->


Add fetchTopics support to show topics a user followed

--- client/landing/app/Applications/Topics.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Topics.kdapplication/AppController.coffee
@@ -45,14 +45,17 @@
               KD.remote.api.JTag.byRelevance @_searchValue, options, callback
             else
               KD.remote.api.JTag.someWithRelationship selector, options, callback
-        followed            :
-          title             : "Followed"
+        following           :
+          title             : "Following"
           dataSource        : (selector, options, callback)=>
-            callback 'Coming soon!'
-        recommended         :
-          title             : "Recommended"
-          dataSource        : (selector, options, callback)=>
-            callback 'Coming soon!'
+            KD.whoami().fetchTopics selector, options, (err, items)=>
+              for item in items
+                item.followee = true
+              callback err, items
+        # recommended         :
+        #   title             : "Recommended"
+        #   dataSource        : (selector, options, callback)=>
+        #     callback 'Coming soon!'
       sort                  :
         'counts.followers'  :
           title             : "Most popular"


landing/payment: submit after recaptcha success

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/landing/site.landing/coffee/team/forms/stripepaymenttabform.coffee
+++ client/landing/site.landing/coffee/team/forms/stripepaymenttabform.coffee
@@ -144,9 +144,21 @@
   submit: (event) ->
     kd.utils.stopDOMEvent event

+    @button.hideLoader()
+
+    grecaptcha?.execute()
+
+
+  onRecaptchaSuccess: (token) ->
+
+    @button.showLoader()
     utils.authorizeCreditCard(@stripeClient, @cardNumber)
-      .then (token) => @options.onSubmitSuccess token
-      .catch (err) => @options.onSubmitError err
+      .then (token) =>
+        @button.hideLoader()
+        @options.onSubmitSuccess token
+      .catch (err) =>
+        @button.hideLoader()
+        @options.onSubmitError err


eliminate dead code involving public sharing of project...

--- src/smc-webapp/project_settings.cjsx
+++ src/smc-webapp/project_settings.cjsx
@@ -382,108 +382,6 @@
                 include the following URL:
                 <URLBox />
             </span>
-        </ProjectSettingsPanel>
-
-SharePanel = rclass ({name}) ->
-    displayName : 'ProjectSettings-SharePanel'
-
-    propTypes :
-        project      : rtypes.object.isRequired
-        public_paths : rtypes.object.isRequired
-        desc         : rtypes.string.isRequired
-        name         : rtypes.string
-
-    reduxProps :
-        "#{name}" :
-            get_public_path_id : rtypes.func
-
-    getInitialState: ->
-        state : 'view'    # view --> edit --> view
-        desc  : @props.desc
-
-    componentWillReceiveProps: (nextProps) ->
-        if @state.desc isnt nextProps.desc
-            @setState
-                desc  : nextProps.desc
-                state : 'view'
-
-    cancel: ->
-        @setState(state : 'view')
-
-    save: ->
-        actions = @actions(name)
-        actions.set_public_path('', ReactDOM.findDOMNode(@refs.share_project).value)
-        @setState(state : 'view')
-
-    render_share_cancel_buttons: ->
-        <ButtonToolbar style={paddingBottom:'5px'}>
-            <Button bsStyle='primary' onClick={@save}>
-                <Icon name='share-square-o' /> Share
-            </Button>
-            <Button onClick={@cancel}>Cancel</Button>
-        </ButtonToolbar>
-
-    render_update_desc_button: ->
-        <ButtonToolbar style={paddingBottom:'5px'}>
-            <Button bsStyle='primary' onClick={@save} disabled={@state.desc == @props.desc} >
-                <Icon name='share-square-o' /> Change description
-            </Button>
-        </ButtonToolbar>
-
-    render_share: (shared) ->
-        if @state.state == 'edit' or shared
-            <form onSubmit={(e)=>e.preventDefault(); @save()}>
-                <FormGroup>
-                    <FormControl
-                        ref         = 'share_project'
-                        type        = 'text'
-                        value       = {@state.desc}
-                        onChange    = {=>@setState(desc : ReactDOM.findDOMNode(@refs.share_project).value)}
-                        placeholder = 'Give a description...' />
-                </FormGroup>
-                {@render_share_cancel_buttons() if @state.state == 'edit'}
-                {@render_update_desc_button() if shared}
-            </form>
-
-    toggle_share: (shared) ->
-        actions = @actions(name)
-        if shared
-            actions.disable_public_path('')
-        else
-            @setState(state : 'edit')
-
-    render_share_unshare_button: (shared) ->
-        <Button
-            bsStyle = {if shared then 'warning' else 'primary'}
-            onClick = {=>@toggle_share(shared)}
-            style   = {float: 'right', marginBottom:'10px'} >
-            <Icon name={if shared then 'shield' else 'share-square-o'} /> {if shared then 'Unshare' else 'Share'} Project...
-        </Button>
-
-    render: ->
-        if not @props.public_paths?
-            return <Loading />
-        project_id = @props.project.get('project_id')
-        id = @props.get_public_path_id('')
-        shared = @props.public_paths.get(id)? and not @props.public_paths.getIn([id, 'disabled'])
-        if shared
-            share_message = "This project is publicly shared, so anyone can see it."
-        else
-            share_message = "Share this project publicly. You can also share individual files or folders from the file listing."
-        <ProjectSettingsPanel title='Project sharing' icon='share'>
-            <Row>
-                <Col sm=8>
-                    {share_message}
-                </Col>
-                <Col sm=4>
-                    {@render_share_unshare_button(shared) if @state.state == 'view'}
-                </Col>
-            </Row>
-            <Row>
-                <Col sm=12>
-                    {@render_share(shared)}
-                </Col>
-            </Row>
         </ProjectSettingsPanel>

 HideDeletePanel = rclass
@@ -1080,15 +978,12 @@
         project_id    : rtypes.string.isRequired
         project       : rtypes.immutable.Map.isRequired
         user_map      : rtypes.immutable.Map.isRequired
-        public_paths  : rtypes.immutable.List.isRequired
         customer      : rtypes.object
         email_address : rtypes.string
         project_map   : rtypes.object  # if this changes, then available upgrades change, so we may have to re-render, if editing upgrades.
         name          : rtypes.string

     reduxProps :
-        "#{name}" :
-            get_public_path_id : rtypes.func
         account :
             get_total_upgrades : rtypes.func
             groups : rtypes.array
@@ -1106,7 +1001,6 @@

     render: ->
         # get the description of the share, in case the project is being shared
-        share_desc = @props.public_paths.get(@props.get_public_path_id(''))?.get('description') ? ''
         id = @props.project_id

         upgrades_you_can_use                 = @props.get_total_upgrades()
@@ -1150,9 +1044,6 @@
                     <ProjectControlPanel   key='control'       project={@props.project} />
                     <SageWorksheetPanel    key='worksheet'     project={@props.project} />
                     <JupyterServerPanel    key='jupyter'        project_id={@props.project_id} />
-                    {# TEMPORARILY DISABLED -- this very badly broken, due to hackish design involving componentWillReceiveProps above.}
-                    {#<SharePanel            key='share'         project={@props.project} }
-                        {#public_paths={@props.public_paths} desc={share_desc} name={@props.name} /> }
                 </Col>
             </Row>
         </div>
@@ -1172,8 +1063,6 @@
             user_type       : rtypes.string    # needed for projects get_my_group call in render
         billing :
             customer : rtypes.immutable  # similar to stripe_customer
-        "#{name}" :
-            stripped_public_paths : rtypes.immutable

     propTypes :
         project_id : rtypes.string.isRequired
@@ -1206,7 +1095,7 @@
         </Alert>

     render: ->
-        if not @props.project_map? or not @props.user_map? or not @props.stripped_public_paths?
+        if not @props.project_map? or not @props.user_map?
             return <Loading />
         user_map = @props.user_map
         project = @props.project_map?.get(@props.project_id) ? @state.admin_project
@@ -1226,10 +1115,9 @@
                     project_id    = {@props.project_id}
                     project       = {project}
                     user_map      = {@props.user_map}
-                    public_paths  = {@props.stripped_public_paths}
                     customer      = {@props.customer}
                     email_address = {@props.email_address}
                     project_map   = {@props.project_map}
                     name          = {name}
                 />
-            </div>+            </div>


social: add deleteCachedItem method

social: add deleteCachedItem method

--- client/app/lib/socialapicontroller.coffee
+++ client/app/lib/socialapicontroller.coffee
@@ -86,7 +86,7 @@
     delete socialapi.openedChannels[channelName] if socialapi.openedChannels[channelName]?

     {typeConstant, id} = channel
-    delete socialapi._cache[typeConstant][id]
+    socialapi.deleteCachedItem typeConstant, id

     {realtime} = kd.singletons

@@ -440,6 +440,11 @@

     return null

+
+  deleteCachedItem: (type, id) ->
+    delete @_cache[type]?[id]
+
+
   cacheable: (type, id, force, callback) ->
     [callback, force] = [force, no]  unless callback


code cleanup

code cleanup

--- components/inputs/KDInputViewWithPreview.coffee
+++ components/inputs/KDInputViewWithPreview.coffee
@@ -20,8 +20,6 @@
     @setClass "kdinputwithpreview"

     @showPreview = options.preview.showInitially or no
-
-    log @getId()

   setDomElement:(CssClass="")->
     super CssClass


Fix insensitive sorting

Fix insensitive sorting

--- ribs.coffee
+++ ribs.coffee
@@ -535,8 +535,8 @@
                     b = walk_context field, mb.toJSON()

                     # make string sorting case insensitive
-                    a = a.toLowerCase() if (a instanceof String)
-                    b = b.toLowerCase() if (b instanceof String)
+                    a = a.toLowerCase() if a.toLowerCase?
+                    b = b.toLowerCase() if b.toLowerCase?

                     return  0     if a is b
                     return +1*dir if a > b or not b?


Add hidden form field support to form API

--- client/modules/packages/form/index.coffee
+++ client/modules/packages/form/index.coffee
@@ -31,6 +31,16 @@
 				$wrapper = angular.element '<div class="form-group">'
 				$wrapper.append $field = switch field.type

+					when 'hidden'
+
+						scope[name] = field.value
+
+						angular.element(
+							'<input type="hidden">'
+						).attr(
+							name: name
+						)
+
 					when 'email', 'password', 'text'
 						$wrapper.append(
 							angular.element('<label>').text field.label


Classify playlist view calculation.

Classify playlist view calculation.

--- frontend/script.coffee
+++ frontend/script.coffee
@@ -1,3 +1,4 @@
+RESIZE_UPDATE_DELAY = 500
 SEARCH_UPDATE_PRELOAD_DELAY = 2000
 CONVERSION_WAIT_TIMEOUT = 180 * 60 * 1000

@@ -480,7 +481,9 @@
 songToQueueRow = (song) ->
         return [song.title, song.album, song.artist]

-QueueView = (queueButton, queueElement, queue, player, playlistElement) ->
+initialPlayerComponentsHeight = (document) ->
+
+QueueView = (queueButton, queueElement, queue, player, playlistElement, viewport) ->
         columns = []

         for column in PLAYLIST_BASIC_COLUMNS
@@ -490,11 +493,8 @@
         for song in queue.fullQueue()
                 queueData.push songToQueueRow song

-        # Magical numbers based on Stetson-Harrison method.
-        playlistHeight = queueElement.data 'targetHeight'
-
         table = queueElement.dataTable
-                sScrollY: "#{playlistHeight}px"
+                sScrollY: "#{viewport.playlistHeight}px"
                 sScrollX: "100%"
                 sScrollXInner: "100%"
                 bScrollCollapse: false
@@ -531,7 +531,7 @@
                 song = queue.remove iPos
                 player.play song

-PlaylistView = (playlistElement, songData, player, queue, router, search) ->
+PlaylistView = (playlistElement, songData, player, queue, router, search, viewport) ->
         columns = []

         columns.push
@@ -550,11 +550,8 @@
                 title = song.title or 'UNKNOWN'
                 tableData.push([index, title, album, artist])

-        # Magical numbers based on Stetson-Harrison method.
-        playlistHeight = playlistElement.data 'targetHeight'
-
         table = playlistElement.dataTable
-                sScrollY: "#{playlistHeight}px"
+                sScrollY: "#{viewport.playlistHeight}px"
                 sScrollX: "100%"
                 sScrollXInner: "100%"
                 bScrollCollapse: false
@@ -648,6 +645,10 @@
         $("#search").keyup doSearch
         $("#search").change doSearch

+        # viewport.on "resize", (width, height) ->
+        # oSettings = table.fnSettings()
+        # oSettings.oScroll.sY = viewport.playlistHeight
+        # table.fnDraw();

 class PlayerRouter extends Backbone.Router
         routes:
@@ -722,6 +723,28 @@
                 toggleQueueElement.click()

+class Viewport
+        constructor: (@document) ->
+                _.extend @, Backbone.Events
+                @decoratorHeight = $("#content", @document).height()
+                @playlistHeight = $(@document).height() - @decoratorHeight
+                $(".remove-after-height-calculation", @document).remove()
+                @lastSizeUpdate = 0
+
+        _updateHeight: =>
+                @playlistHeight = $(@document).height() - @decoratorHeight
+                @trigger "resize"
+
+        _bind: =>
+                $(@document).resize =>
+                        @lastSizeUpdate++
+                        currentUpdate = @lastSizeUpdate
+                        updateCallback = =>
+                                if @lastSizeUpdate != currentUpdate
+                                        return
+                                @_updateHeight()
+                        setTimeout updateCallback, RESIZE_UPDATE_DELAY
+
 $(document).ready ->
         audio = new Audio();
         playbackType = null
@@ -752,15 +775,11 @@

         router = new PlayerRouter()

-        playlistHeight = $(document).height() - $("#content").height()
-        $(".remove-after-height-calculation").remove()
+        viewport = new Viewport document

         queueTable = $("#queue")
-        queueTable.data "targetHeight", playlistHeight
         playlist = $("#playlist")
-        playlist.data "targetHeight", playlistHeight
-
-        QueueView $("#toggle-queue"), queueTable, songQueue, playerInstance, playlist
+        QueueView $("#toggle-queue"), queueTable, songQueue, playerInstance, playlist, viewport

         search = new Search localStorage

@@ -818,7 +837,7 @@

                 $("#initial-status").append $("
Creating playlist...
")

-                PlaylistView playlist, files, playerInstance, songQueue, router, search
+                PlaylistView playlist, files, playerInstance, songQueue, router, search, viewport

                 $("#initial-status").remove()


Use absolute path to db.

Use absolute path to db.

--- lib/db.coffee
+++ lib/db.coffee
@@ -1,3 +1,4 @@
+path = require 'path'
 PouchDB = require 'pouchdb'
 PouchDB.plugin require 'pouchdb-upsert'
 Promise = require 'bluebird'
@@ -15,7 +16,7 @@
 diff = ->
   ddoc

-db = new PouchDB('../db/thirdhandinfo')
+db = new PouchDB(path.resolve __dirname, '../db/thirdhandinfo')

 getQueryProps = (query) ->
   props = [ 'key', 'startkey', 'endkey', 'descending' ]


fix some issues I found when testing refactor

--- src/smc-hub/hub.coffee
+++ src/smc-hub/hub.coffee
@@ -6197,10 +6197,9 @@
             # However it can still serve many things without database.  TODO: Eventually it could inform user
             # that database isn't working.
             {http_server, express_app} = init_express_http_server()
-            winston.debug("initializaing primus server")
-            init_primus_server(http_server)
             winston.debug("starting express webserver listening on #{program.host}:#{program.port}")
             http_server.listen(program.port, program.host, cb)
+
         (cb) ->
             winston.debug("Connecting to the database.")
             misc.retry_until_success
@@ -6212,12 +6211,21 @@
                     cb()
         (cb) ->
             # init authentication via passport (requires database)
-            init_passport(app, cb)
+            init_passport(express_app, cb)
         (cb) ->
             init_stripe(cb)
         (cb) ->
             init_compute_server(cb)
-        (cb) ->
+    ], (err) =>
+        if err
+            winston.error("Error starting hub services! err=#{err}")
+        else
+            # Synchronous initialize of other functionality, now that the database, etc., are working.
+
+            winston.debug("initializing primus websocket server")
+            init_primus_server(http_server)
+
+            winston.debug("initializing the http proxy server")
             init_http_proxy_server()

             # Start updating stats cache every so often -- note: this is cached in the database, so it isn't
@@ -6226,12 +6234,9 @@
             # database when somebody happens to visit /stats
             database.get_stats(); setInterval(database.get_stats, 120*1000)

+            # Register periodically with the hub.
             register_hub(); setInterval(register_hub, REGISTER_INTERVAL_S*1000)
-            cb()
-    ], (err) =>
-        if err
-            winston.error("Error starting hub services! err=#{err}")
-        else
+
             winston.info("Started hub. HTTP port #{program.port}; keyspace #{program.keyspace}")
         cb?(err)
     )


Changed new command default to stream to panel.

--- lib/views/edit/command-choose-view.coffee
+++ lib/views/edit/command-choose-view.coffee
@@ -35,6 +35,8 @@

   addNewCommand: ->
     command = new ProcessConfig({});
+    command.stream = true;
+    command.outputTarget = 'panel';
     @commands.push(command);
     itemView = @addCommandItemView(command);
     @commandItemViewSelected(itemView);


Added necessary link check, fixed publishing/editing error

--- src/scripts/helpers/links.coffee
+++ src/scripts/helpers/links.coffee
@@ -36,7 +36,7 @@
       title = trim(model.get?('title') or model.title)

       if model.isBook?()
-        title = trim(model.get?('currentPage').get('title'))
+        title = trim(model.get('currentPage').get('title')) if model.get('currentPage')?
         page = ":#{model.getPageNumber()}"

       return "#{settings.root}contents/#{id}#{page}/#{title}"


Reduce noise from interval when poll_res is unset.

--- src/scripts/pingdom-tools.coffee
+++ src/scripts/pingdom-tools.coffee
@@ -30,7 +30,7 @@
                   .get() (err, res, poll_body) ->
                     if poll_body
                       poll_res = JSON.parse poll_body
-                      if poll_res.state == 'completed' and poll_res.results
+                      if poll_res and poll_res.results and poll_res.state == 'completed'
                         result = poll_res.results
                         replies = []
                         if result.report_url