config/dev: typo

config/dev: typo

--- config/main.dev.coffee
+++ config/main.dev.coffee
@@ -498,7 +498,7 @@

         echo '#---> BUILDING CLIENT <---#'
         sh -c "scripts/install-npm.sh -d client -u -p -s"
-        make -C ${projectRoot}/client dist
+        make -C #{projectRoot}/client dist

         echo '#---> BUILDING GO WORKERS (@farslan) <---#'
         #{projectRoot}/go/build.sh
@@ -1055,7 +1055,7 @@
       elif [ "$1" == "buildclient" ]; then

         sh -c "scripts/install-npm.sh -d client -u -p -s"
-        make -C ${projectRoot}/client watch
+        make -C #{projectRoot}/client watch

       elif [ "$1" == "services" ]; then
         check_service_dependencies


fix wrong property name slug- should be id

--- desktop/models/sale.coffee
+++ desktop/models/sale.coffee
@@ -40,7 +40,7 @@
   bidUrl: (artwork) ->
     "#{@href()}/bid/#{artwork.id}"

-  liveAuctionUrl: -> "#{PREDICTION_URL}/#{@get('slug')}"
+  liveAuctionUrl: -> "#{PREDICTION_URL}/#{@get('id')}"

   redirectUrl: (artwork) ->
     if @isBidable() and artwork?


fixes #1398: comments in the REPL

fixes #1398: comments in the REPL

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -89,7 +89,7 @@
   sandbox.__dirname  = path.dirname sandbox.__filename
   o = {}; o[k] = v for k, v of options
   o.bare = on # ensure return value
-  js = compile "_=(#{code.trim()})", o
+  js = compile "_=(#{code.trim()}\n)", o
   vm.runInNewContext js, sandbox, sandbox.__filename

 # Instantiate a Lexer for our use here.


Refactor and comment Highlighter.handleBufferChange

Because it's kinda complex in a way I can't reduce any further to make
it self-evident.

--- src/atom/highlighter.coffee
+++ src/atom/highlighter.coffee
@@ -18,22 +18,31 @@
   handleBufferChange: (e) ->
     preRange = e.preRange.copy()
     postRange = e.postRange.copy()
+    previousState = @lines[preRange.end.row].state # used in spill detection below

-    previousState = @lines[preRange.end.row].state
     startState = @lines[postRange.start.row - 1]?.state or 'start'
     @lines[preRange.start.row..preRange.end.row] =
       @tokenizeRows(startState, postRange.start.row, postRange.end.row)

+    # spill detection
+    # compare scanner state of last re-highlighted line with its previous state.
+    # if it differs, re-tokenize the next line with the new state and repeat for
+    # each line until the line's new state matches the previous state. this covers
+    # cases like inserting a /* needing to comment out lines below until we see a */
     for row in [postRange.end.row...@buffer.lastRow()]
       break if @lines[row].state == previousState
       nextRow = row + 1
       previousState = @lines[nextRow].state
       @lines[nextRow] = @tokenizeRow(@lines[row].state, nextRow)

-      preRange.end.row++
-      preRange.end.column = @buffer.getLine(nextRow).length
-      postRange.end.row++
-      postRange.end.column = @buffer.getLine(nextRow).length
+    # if highlighting spilled beyond the bounds of the textual change, update
+    # the pre and post range to reflect area of highlight changes
+    if nextRow > postRange.end.row
+      preRange.end.row += (nextRow - postRange.end.row)
+      postRange.end.row = nextRow
+      endColumn = @buffer.getLine(nextRow).length
+      postRange.end.column = endColumn
+      preRange.end.column = endColumn

     @trigger("change", {preRange, postRange})


fixes foldUp followed by immediate unfold (#35)

--- oridomi.coffee
+++ oridomi.coffee
@@ -105,9 +105,6 @@
       switch fn.length
         when 1
           opt.callback = a0
-          # If the composition is already folded up, skip the queueing process
-          # and invoke the supplied callback immediately.
-          return opt.callback?() unless @isFoldedUp
         when 2
           if typeof a0 is 'function'
             opt.callback = a0
@@ -1234,7 +1231,8 @@

   # This is the queued version of `_unfold`.
-  unfold: prep (callback) -> @_unfold arguments...
+  unfold: prep (callback) ->
+    @_unfold arguments...

   # For custom folding behavior, you can pass a function to `map()` that will


Use correct levels for on/off on the digital inputs.

--- flexit.coffee
+++ flexit.coffee
@@ -51,9 +51,9 @@

               if(data.indexOf("RO 8") > -1)
                 if data[5] == "0"
+                  @H2 = on
+                else
                   @H2 = off
-                else
-                  @H2 = on

               if(data.indexOf("RO 9") > -1)
                 if data[5] == "0"
@@ -69,21 +69,21 @@

               if(data.indexOf("DI 2") > -1)
                 if data[5] == "0"
+                  @FI = on
+                else
                   @FI = off
-                else
-                  @FI = on

               if(data.indexOf("DI 3") > -1)
                 if data[5] == "0"
+                  @BT = on
+                else
                   @BT = off
-                else
-                  @BT = on

               if(data.indexOf("DI 4") > -1)
                 if data[5] == "0"
+                  @DR = on
+                else
                   @DR = off
-                else
-                  @DR = on
             )

             setTimeout(
@@ -122,22 +122,22 @@
       @writeVal("SET RO 10", @EV)

     fanspeed: ->
-      if @H2
+      if @H2 and not @H3
+        return 1
+      else if @H3 and @H2
+        return 3
+      else if not @H2 and not @H3
         return 2
-      else if @H3
-        return 3
-      else
-        return 1

     setfanspeed: (speed) ->
       if speed == 2
-        @H2 = on
+        @H2 = off
         @H3 = off
       else if speed == 3
         @H3 = on
-        @H2 = off
+        @H2 = on
       else
-        @H2 = off
+        @H2 = on
         @H3 = off

       @updateOutputs()


basic board draw routine

basic board draw routine

--- game.coffee
+++ game.coffee
@@ -36,7 +36,26 @@
     board_state = @goals.map((g) -> if g.tagged then 'y' else 'n').join('')
     board_state + '/' + @prog.state() + '/' + @bot.state()

-  draw: -> console.log @state()
+  draw: ->
+    console.log '----------'
+    for row, y in @board
+      chars = row.map (square, x) =>
+        attrs = []
+        char  =
+          if x is @bot.x and y is @bot.y
+            ['^', '>', 'v', '<'][@bot.dir]
+          else
+            square.elev
+        attrs.push 31 if @bot.color is 'red'
+        attrs.push 32 if @bot.color is 'green'
+        attrs.push 41 if square.color is 'red'
+        attrs.push 42 if square.color is 'green'
+        attrs.push 1 if square.goal
+        attrs.push 4 if square.tagged
+        char = "[#{attrs.join(';')}m#{char}" if attrs.length
+        char
+
+      console.log chars.join ''

   execute: (instr) ->
     unless instr.color and (instr.color isnt @bot.color)


WaitBlock: Move to CUI.WaitBlock.

WaitBlock: Move to CUI.WaitBlock.

--- src/elements/WaitBlock/WaitBlock.coffee
+++ src/elements/WaitBlock/WaitBlock.coffee
@@ -5,7 +5,7 @@
  * https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
 ###

-class WaitBlock extends Block
+class CUI.WaitBlock extends CUI.Block
 	constructor: (@opts={}) ->
 		super(@opts)
 		if @_inactive
@@ -90,3 +90,5 @@
 	destroy: ->
 		@hide()
 		super()
+
+WaitBlock = CUI.WaitBlock


Try out Atom's tree view

Try out Atom's tree view

--- atom/config.cson
+++ atom/config.cson
@@ -28,7 +28,6 @@
       "release-notes"
       "script"
       "timecop"
-      "tree-view"
       "update-package-dependencies"
       "welcome"
       "whitespace"
@@ -123,6 +122,9 @@
     ]
     markerType: "dot"
   "svg-preview": {}
+  "tree-view":
+    hideIgnoredNames: true
+    hideVcsIgnoredFiles: true
   welcome:
     showOnStartup: false
   whitespace:


add atom-vue and linter-docker

add atom-vue and linter-docker

--- atom/config.cson
+++ atom/config.cson
@@ -15,6 +15,7 @@
       "language-sass"
       "language-source"
       "language-xml"
+      "atom-vue"
     ]
     projectHome: "/Users/harouna/repositories"
     telemetryConsent: "limited"
@@ -35,10 +36,12 @@
   "git-diff":
     showIconsInEditorGutter: true
   linter: {}
+  "linter-docker": {}
   "linter-eslint": {}
   "linter-ui-default":
     gutterPosition: "Left"
     panelHeight: 69
+    showPanel: true
     showProviderName: true
   "markdown-preview":
     useGitHubStyle: true


Disable default JSDoc tags for flower box comments

--- atom/config.cson
+++ atom/config.cson
@@ -93,6 +93,7 @@
   docblockr:
     align_tags: "shallow"
     extend_double_slash: false
+    simple_mode: true
   editor:
     autoIndentOnPaste: false
     fontFamily: "Monaco"


atom: Update core packages config

atom: Update core packages config

--- atom/config.cson
+++ atom/config.cson
@@ -1,4 +1,6 @@
 "*":
+  "bracket-matcher":
+    highlightMatchingLineNumber: true
   core:
     disabledPackages: [
       "background-tips"
@@ -24,10 +26,6 @@
     onChanges: true
   "highlight-selected":
     onlyHighlightWholeWords: false
-  "line-ending-selector":
-    defaultLineEnding: "LF"
-  "markdown-preview":
-    useGitHubStyle: true
   "omni-ruler":
     columns: [
       "80"
@@ -42,3 +40,7 @@
     locales: [
       "en-US"
     ]
+  tabs:
+    enableVcsColoring: true
+  whitespace:
+    ignoreWhitespaceOnCurrentLine: false


On retire le message d'accueil

On retire le message d'accueil

Etonnant qu'il soit encore là lui

--- atom/config.cson
+++ atom/config.cson
@@ -49,6 +49,7 @@
     AutoExpandLocals: true
     PathMaps: []
     currentPanelMode: "bottom"
+    showWelcome: false
   pigments:
     mergeColorDuplicates: true
     sortPaletteColors: "by color"


Re-enable expand-region (fixed in v0.5.0 of package)

--- atom/config.cson
+++ atom/config.cson
@@ -49,7 +49,6 @@
       "welcome"
       "whitespace"
       "teletype"
-      "expand-region"
     ]
     followSymlinks: false
     ignoredNames: [


Tests: Trigger build.

Tests: Trigger build.

--- tests/src/ide/workspace.coffee
+++ tests/src/ide/workspace.coffee
@@ -51,3 +51,4 @@
           .pause                  3000
           .assert.containsText    '.vm-info', name # Assertion
           .end()
+


chat/collabparticipants: return immutable.List

--- client/activity/lib/models/collaborationchannelparticipants.coffee
+++ client/activity/lib/models/collaborationchannelparticipants.coffee
@@ -28,8 +28,10 @@
   computePreviewParticipants: (participants) ->

     if @state.videoActive
-      return participants.filter (participant) =>
+      filtered = participants.filter (participant) =>
         participant.profile.nickname in @state.videoParticipants
+
+      return filtered.toList()

     super

@@ -46,9 +48,11 @@
   computeHiddenParticipants: (participants) ->

     if @state.videoActive
-      return participants.filter (participant) =>
+      filtered = participants.filter (participant) =>
         {nickname} = participant.profile
         return @state.videoParticipants.indexOf(nickname) is -1
+
+      return filtered.toList()

     super


[Email Confirmation] added default callback for displayEmailConfirmModal function

--- client/app/Applications/Account.kdapplication/AppController.coffee
+++ client/app/Applications/Account.kdapplication/AppController.coffee
@@ -164,7 +164,7 @@
   indexOfItem:(item)->
     @itemsOrdered.indexOf item

-  displayConfirmEmailModal:(name, username, callback)->
+  displayConfirmEmailModal:(name, username, callback=->)->
     name or= KD.whoami().profile.firstName
     message =
       """


webterm: allow for multiple terminals as per request

--- client/app/Applications/WebTerm.kdapplication/AppController.coffee
+++ client/app/Applications/WebTerm.kdapplication/AppController.coffee
@@ -24,7 +24,19 @@

   bringToFront: ->

-    data = @getOptions().view
+    data = new WebTermView
+    data.on "WebTerm.terminated", =>
+      @propagateEvent
+       KDEventType  : "ApplicationWantsToClose"
+       globalEvent  : yes
+      , data: data
+
+    data.on 'ViewClosed', =>
+      @propagateEvent
+        KDEventType : 'ApplicationWantsToClose'
+        globalEvent : yes
+      ,
+        data : data

     options =
       name         : "Terminal"


Adjust code whitespace.

Adjust code whitespace.

--- test/TinyMockJS.test.coffee
+++ test/TinyMockJS.test.coffee
@@ -1,5 +1,4 @@
 {mock, Mock} = require("../src/TinyMockJS")
-

 describe "Mock.expects(method_name)", ->
@@ -31,7 +30,6 @@
       (-> m.expects("#{reserved}") ).should.throw( "you cannot do my_mock.expects('#{reserved}'); '#{reserved}' is a reserved method name" )

-
 describe "Mock.args( value [, value ...] )", ->

   it "returns the mock instance", ->
@@ -53,7 +51,6 @@
   it "wraps strings with quotes in expection messages"

-
 describe "Mock.returns(value)", ->

   it "returns the mock instance", ->
@@ -81,7 +78,6 @@
     (-> m.returns(42) ).should.throw # anything

-
 describe "Mock.throws(error)", ->

   it "returns the mock instance", ->
@@ -113,7 +109,6 @@
   it "throws an error if it was not called immediately after either .expects() or .args()", ->
     m = new Mock()
     (-> m.throws("an error") ).should.throw( ".throws() must be called immediately after .expects() or .args()" )
-

 describe "Mock.my_method( [ value [, value ... ] ] )", ->
@@ -195,7 +190,6 @@
     m.expects("my_method").throws("an error")
     m.expects("my_method").throws("another error")
     (-> m.my_method() ).should.throw("my_method() is a duplicate expectation")
-

 describe "mock( function( mock1 [, mock2 ...] ) )", ->
@@ -217,4 +211,3 @@
         my_mock1.expects("my_method1").args(1,2,3)
         my_mock2.expects("my_method2")
     ).should.throw( "'my_method1(1,2,3)' was never called\n'my_method2()' was never called\n" )
-


failing test for linewise visual `c` issue

--- test/ace/visual_mode.coffee
+++ test/ace/visual_mode.coffee
@@ -27,6 +27,13 @@
   eq @adaptor.lastRow(), 10
   eq @adaptor.row(), 1

+test 'linewise changes', ->
+  eq @adaptor.lastRow(), 15
+  @press 'Vjcnew line!', @esc
+  eq @adaptor.lastRow(), 14
+  deepEqual @adaptor.position(), [0, 8]
+  eq @adaptor.lineText(0), 'new line!'
+
 test 'p, P', ->
   # p and P do the same thing in visual mode
   @press 'xlvep'


test(client): fix module reference

test(client): fix module reference

--- test/anvilClientSpec.coffee
+++ test/anvilClientSpec.coffee
@@ -18,7 +18,7 @@

-AnvilConnect = require path.join(cwd, 'promises')
+AnvilConnect = require path.join(cwd, 'index')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -25,7 +25,7 @@

   indexSum = 0

-  engine.each ('', (obj, index) ->
+  engine.each '', (obj, index) ->
     indexSum += index

     if index is 0


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -13,7 +13,7 @@
     x: 5
     y: 10

-  engine.add
+  obj engine.add
     class: "Awesome"
     x: 5
     y: 10


adjust args test

adjust args test

--- test/test-rivescript.coffee
+++ test/test-rivescript.coffee
@@ -896,17 +896,17 @@

 exports.test_arguments_in_setSubroutine = (test) ->
   bot = new TestCase(test, """
-    + hello *
+    + my name is *
     - hello <call>helper <star> 12</call>
   """)

-  bot.rs.setSubroutine("helper", (rs, text, int) ->
-    test.equal(text, "there")
+  bot.rs.setSubroutine("helper", (rs, name, int) ->
+    test.equal(name, "thomas edison")
     test.equal(int, "12")
-    "person"
-  )
-
-  bot.reply("hello there", "hello person")
+    name
+  )
+
+  bot.reply("my name is thomas edison", "hello thomas edison")
   test.done()

 exports.test_promises_in_objects = (test) ->


add event tests

add event tests

--- test/unit/core/quill.coffee
+++ test/unit/core/quill.coffee
@@ -178,6 +178,33 @@
     )
   )

+  describe('events', ->
+    it('format middle', (done) ->
+      @quill.setHTML('
abc
')
+      @quill.editor.checkUpdate()
+      @quill.setSelection(1, 2)
+      @quill.on('text-change', (delta) ->
+        expect(delta).toEqualDelta(new Quill.Delta().retain(1).retain(1, { strike: true }))
+        done()
+      )
+      $('i', @quill.root).wrap('<s></s>')
+      @quill.editor.checkUpdate()
+    )
+
+    it('ambiguous insert', (done) ->
+      @quill.setHTML('
aaa
')
+      @quill.editor.checkUpdate()
+      @quill.setSelection(1, 1)
+      @quill.on('text-change', (delta) ->
+        expect(delta).toEqualDelta(new Quill.Delta().retain(1).insert('a', { bold: true }))
+        done()
+      )
+      bold = @quill.root.querySelector('b')
+      bold.insertBefore(document.createTextNode('a'), bold.lastChild)
+      @quill.editor.checkUpdate()
+    )
+  )
+
   describe('_buildParams()', ->
     tests =
       'index range string formats'  : [1, 3, 'bold', true]


set collapsible elements

set collapsible elements

--- common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -19,6 +19,7 @@
     @next_problem_button = @$('.next-step-button')
     @next_problem_button.click @next_problem
     # valid states: 'initial', 'assessing', 'post_assessment', 'done'
+    Collapsible.setCollapsibles(@el)

     # Where to put the rubric once we load it
     @el = $(element).find('section.open-ended-child')


Render SubCommentsAddView in onRender, this fixes not getting focus. Also gives a clearer flow

--- core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comments_list_view.coffee
@@ -16,18 +16,19 @@
   initialize: ->
     @collection.fetch()

-    if Factlink.Global.signed_in
-      @_addViewContainer = new SubCommentContainerView
-        creator: currentUser
-        innerView: new SubCommentsAddView addToCollection: @collection
-      @_addViewContainer.render()
-
   onRender: ->
     return if @collection.loading()

     @closeEmptyView() if @collection.length <= 0

-    # The add view needs to be sibling in the DOM tree of the otherSubCommentContainerViews
-    @$el.append @_addViewContainer.el if @_addViewContainer?
+    if Factlink.Global.signed_in
+      @_addViewContainer = new SubCommentContainerView
+        creator: currentUser
+        innerView: new SubCommentsAddView addToCollection: @collection
+
+      # The add view needs to be sibling in the DOM tree of the other SubCommentContainerViews
+      @$el.append @_addViewContainer.el
+
+      @_addViewContainer.render()

   onClose: -> @_addViewContainer?.close()


Refactor CPS interpreter to use error continuation.

--- cps-interpreter.coffee
+++ cps-interpreter.coffee
@@ -11,11 +11,10 @@
     @prototype = @__ctor__.prototype

   apply: (_this, args) ->
-    await this.applyCps _this, args, defer(e, result)
-    throw e if e?
+    await this.applyCps _this, args, defer(result), errCont
     return result

-  applyCps: (appliedThis, args, cont) ->
+  applyCps: (appliedThis, args, cont, errCont) ->
     calleeNode = this.__call__
     calleeNode.env.increaseScope()
     argsObject = {}
@@ -25,64 +24,60 @@
       argsObject[i] = arg
     Util.defineNonEnumerable argsObject, 'length', args.length
     calleeNode.env.insert 'this', appliedThis
-    await interp calleeNode.body, calleeNode.env, defer(e, result)
+    await interp calleeNode.body, calleeNode.env, defer(result), (e) ->
+      if e instanceof ReturnException
+        calleeNode.env.decreaseScope()
+        cont(e.value)
+      else
+        errCont(e)
     calleeNode.env.decreaseScope()
-    return cont(null, e.value) if e instanceof ReturnException
-    return cont(e) if e?
-    return cont(null, result)
+    cont(result)

   call: (_this) -> @apply _this, Array::slice arguments, 1

-  callCps: (_this, cont) -> applyCps _this, (Array::slice arguments, 1), cont
-
-interp = (node, env=new Environment, cont) ->
+  callCps: (_this, cont, errCont) ->
+    applyCps _this, (Array::slice arguments, 1), cont, errCont
+
+interp = (node, env=new Environment, cont, errCont) ->
   try
     switch node.type
       when 'Program', 'BlockStatement'
         for stmt, i in node.body
           env.strict ||= i == 0 and stmt.expression?.value is 'use strict'
-          await interp(stmt, env, defer(e, v))
-          return cont(e) if e?
-          return cont(null, v) if node.type is 'Program' and i == node.body.length - 1 # for eval's return value
-        return cont()
+          await interp stmt, env, defer(v), errCont
+          return cont(v) if node.type is 'Program' and i == node.body.length - 1 # for eval's return value
+        cont()
       when 'FunctionDeclaration', 'FunctionExpression'
         node.env = env.copy()
         fn = (new Function "return function #{node.id?.name ? ''}() {}")()
         ifn = new InterpretedFunction fn, node
         if node.id?
           env.insert(node.id.name, ifn)
-        return cont(null, ifn)
+        cont(ifn)
       when 'VariableDeclaration'
         for dec in node.declarations
-          await interp dec, env, defer(e, result)
-          return cont(e) if e?
-        return cont()
+          await interp dec, env, defer(result), errCont
+        cont()
       when 'VariableDeclarator'
-        await interp node.init, env, defer(e, result)
-        return cont(e) if e?
+        await interp node.init, env, defer(result), errCont
         declaratorResult = env.insert node.id.name, result, env
-        return cont(null, declaratorResult)
+        cont(declaratorResult)
       when 'ExpressionStatement'
-        await interp node.expression, env, defer(e, result)
-        return cont(e, result)
+        interp node.expression, env, cont, errCont
       when 'CallExpression'
         callee = null
         if node.callee.type is 'MemberExpression'
-          await evalMemberExpr node.callee, env, defer(e, result)
-          return cont(e) if e?
-          [_this, calleeName] = result
+          await evalMemberExpr node.callee, env, defer(_this, calleeName), errCont
           callee = _this[calleeName]
         else
           _this = undefined
-          await interp(node.callee, env, defer(e, callee))
-          return cont(e) if e?
+          await interp node.callee, env, defer(callee), errCont
         args = []
         for arg in node.arguments
-          await interp(arg, env, defer(e, argResult))
-          return cont(e) if e?
+          await interp arg, env, defer(argResult), errCont
           args.push(argResult)
         if callee == eval
-          return cont(null, args[0]) unless Util.isString args[0]
+          return cont(args[0]) unless Util.isString args[0]
           ast = esprima.parse args[0]
           fnName =
             if node.callee.type is 'MemberExpression' and
@@ -93,197 +88,163 @@
             else
               null
           if fnName is 'eval'
-            await interp ast, env, defer(e, result)
-            return cont(e, result)
-          else
-            await interp ast, env.getGlobalEnv(), defer(e, result)
-            return cont(e, result)
+            interp ast, env, cont, errCont
+          else
+            interp ast, env.getGlobalEnv(), cont, errCont
         else
           if callee instanceof InterpretedFunction
-            await callee.applyCps _this, args, defer(e, applied)
-            return cont(e) if e?
-            return cont(null, applied)
-          else
-            applied = callee.apply _this, args
-            return cont(null, applied)
+            callee.applyCps _this, args, cont, errCont
+          else
+            cont callee.apply _this, args
       when 'NewExpression'
-        await interp node.callee, env, defer(e, callee)
-        return cont(e) if e?
-        args = []
-        for arg in node.arguments
-          await interp(arg, env, defer(e, result))
-          return cont(e) if e?
-          args.push(result)
+        await interp node.callee, env, defer(callee), errCont
+        args =
+          for arg in node.arguments
+            await interp arg, env, defer(result), errCont
+            result
         if callee.__ctor__?
           obj = new callee.__ctor__
           if callee instanceof InterpretedFunction
-            await callee.applyCps obj, args, defer(e, result)
-            return cont(e) if e?
+            await callee.applyCps obj, args, defer(result), errCont
           else
             callee.apply obj, args
         else
           if callee instanceof InterpretedFunction
-            await callee.bind.applyCps callee, ([null].concat args), defer(e, result)
-            return cont(e) if e?
+            await callee.bind.applyCps callee, ([null].concat args), defer(result), errCont
           else
             obj = new (callee.bind.apply callee, [null].concat args)
-        return cont(null, obj)
+        cont(obj)
       #*** Control Flow ***#
       when 'IfStatement'
-        await interp node.test, env, defer(e, test)
-        return cont(e) if e?
+        await interp node.test, env, defer(test), errCont
         if (test)
-          await interp node.consequent, env, defer(e, result)
-          return cont(e, result)
+          interp node.consequent, env, cont, errCont
         else if node.alternate?
-          await interp node.alternate, env, defer(e, result)
-          return cont(e, result)
-        else
-          return cont()
+          interp node.alternate, env, cont, errCont
+        else
+          cont()
       when 'WhileStatement'
         while (true)
           # Test
-          await interp node.test env, defer(e, test)
-          return cont(e) if e?
+          await interp node.test env, defer(test), errCont
           return cont() if not test
           # Body
-          await interp node.body, env, defer(e)
-          return cont() if e instanceof BreakException
-          return cont(e) if e? and not (e instanceof ContinueException)
-        return cont()
+          await
+            interp node.body, env, bodyCont = defer(),
+              makeLoopCont(node.body, env, bodyCont, cont, errCont)
       when 'DoWhileStatement'
         while (true)
           # Body
-          await interp node.body, env, defer(e)
-          return cont() if e instanceof BreakException
-          return cont(e) if e? and not (e instanceof ContinueException)
+          await
+            interp node.body, env, bodyCont = defer(),
+              makeLoopCont(node.body, env, bodyCont, cont, errCont)
           # Test
-          await interp node.test, env, defer(e, test)
-          return cont(e) if e?
+          await interp node.test, env, defer(test), errCont
           return cont() if not test
-        return cont()
       when 'ForStatement'
-        await interp node.init, env, defer(e)
-        return cont(e) if e?
+        await interp node.init, env, defer(), errCont
         while (true)
           # Test
-          await interp node.test, env, defer(e, test)
-          return cont(e) if e?
+          await interp node.test, env, defer(test), errCont
           return cont() if not test
           # Body
-          await interp node.body, env, defer(e)
-          return cont() if e instanceof BreakException
-          return cont(e) if e? and not (e instanceof ContinueException)
+          await
+            interp node.body, env, bodyCont = defer(),
+              makeLoopCont(node.body, env, bodyCont, cont, errCont)
           # Update
-          await interp node.update, env, defer(e)
-          return cont() if e instanceof BreakException
-          return cont(e) if e? and not (e instanceof ContinueException)
+          await interp node.update, env, defer(), errCont
       when 'ForInStatement'
-        await interp node.left, env, defer(e)
-        return cont(e) if e?
-        await interp node.right, env, defer(e, obj)
-        return cont(e) if e?
+        await interp node.left, env, defer(), errCont
+        await interp node.right, env, defer(obj), errCont
         for k of obj
           if node.left.type is 'VariableDeclaration'
-            await assign node.left.declarations[0].id, k, env, defer(e, result)
-            return cont(e) if e?
-          else
-            await assign node.left, k, env, defer(e, result)
-            return cont(e) if e?
-          await interp node.body, env, defer(e)
-          return cont(e) if e?
-        return cont()
+            await assign node.left.declarations[0].id, k, env, defer(), errCont
+          else
+            await assign node.left, k, env, defer(), errCont
+          await
+            interp node.body, env, bodyCont = defer(),
+              makeLoopCont(node.body, env, bodyCont, cont, errCont)
+        cont()
       when 'BreakStatement'
-        return cont(new BreakException)
+        errCont(new BreakException)
       when 'ContinueStatement'
-        return cont(new ContinueException)
+        errCont(new ContinueException)
       when 'ReturnStatement'
-        return cont(new ReturnException undefined) if node.argument is null
-        await interp node.argument, env, defer(e, result)
-        return cont(e || new ReturnException result)
+        if node.argument is null
+          errCont new ReturnException undefined
+        else
+          await interp node.argument, env, defer(result), errCont
+          errCont new ReturnException result
       when 'ThrowStatement'
-        await interp node.argument, env, defer(e, result)
-        return cont(e || new JSException result)
+        await interp node.argument, env, defer(result), errCont
+        errCont new JSException result
       when 'TryStatement'
-        await interp node.block, env, defer(errorInTry, result)
-        if errorInTry instanceof JSException and node.handlers.length > 0
-          catchEnv = env.increaseScope true
-          catchEnv.set node.handlers[0].param.name, errorInTry.exception
-          await interp node.handlers[0], env, defer(errorInCatch, result)
-          env.decreaseScope() # unless errorInCatch?
-          if node.finalizer # try->catch->finally
-            await interp node.finalizer, env, defer(errorInFinalizer, result)
-            return cont(errorInFinalizer || errorInCatch)
-          else # try->catch
-            return cont(eInCatch)
-        else if node.finalizer
-          await interp node.finalizer, env, defer(errorInFinalizer, result)
-          return cont(errorInFinalizer || errorInTry) # try->finally
-        else
-          return cont(errorInTry) # try
+        interp node.block, env, cont, (e) ->
+          if e instanceof JSException and node.handlers.length > 0
+            catchEnv = env.increaseScope true
+            catchEnv.set node.handlers[0].param.name, e.exception
+            await interp node.handlers[0], env, defer(), errCont
+            env.decreaseScope()
+          if node.finalizer
+            interp node.finalizer, env, cont, errCont
+          else
+            cont()
       when 'CatchClause'
-        await interp node.body, env, defer(e, result)
-        return cont(e, result)
+        await interp node.body, env, cont, errCont
       #*** Operator Expressions ***#
       when 'BinaryExpression'
-        await interp node.left, env, defer(e, lhs)
-        return cont(e) if e?
-        await interp node.right, env, defer(e, rhs)
-        return cont(e) if e?
+        await interp node.left, env, defer(lhs), errCont
+        await interp node.right, env, defer(rhs), errCont
         switch node.operator
           when '+'
-            return cont(null, lhs + rhs)
+            cont(lhs + rhs)
           when '-'
-            return cont(null, lhs - rhs)
+            cont(lhs - rhs)
           when '*'
-            return cont(null, lhs * rhs)
+            cont(lhs * rhs)
           when '/'
-            return cont(null, lhs / rhs)
+            cont(lhs / rhs) # TODO handle divzero
           when '&'
-            return cont(null, lhs & rhs)
+            cont(lhs & rhs)
           when '|'
-            return cont(null, lhs | rhs)
+            cont(lhs | rhs)
           when '^'
-            return cont(null, lhs ^ rhs)
+            cont(lhs ^ rhs)
           when '>>'
-            return cont(null, lhs >> rhs)
+            cont(lhs >> rhs)
           when '<<'
-            return cont(null, lhs << rhs)
+            cont(lhs << rhs)
           when '>>>'
-            return cont(null, lhs >>> rhs)
+            cont(lhs >>> rhs)
           when '<'
-            return cont(null, lhs < rhs)
+            cont(lhs < rhs)
           when '>'
-            return cont(null, lhs > rhs)
+            cont(lhs > rhs)
           when '<='
-            return cont(null, lhs <= rhs)
+            cont(lhs <= rhs)
           when '>='
-            return cont(null, lhs >= rhs)
+            cont(lhs >= rhs)
           when '=='
-            return cont(null, `lhs == rhs`)
+            cont(`lhs == rhs`)
           when '==='
-            return cont(null, `lhs === rhs`)
+            cont(`lhs === rhs`)
           when '!='
-            return cont(null, `lhs != rhs`)
+            cont(`lhs != rhs`)
           when '!=='
-            return cont(null, `lhs !== rhs`)
+            cont(`lhs !== rhs`)
           when 'instanceof'
-            return cont(null, lhs instanceof rhs.__ctor__)
-          else
-            return cont("Unrecognized operator #{node.operator}")
+            cont(lhs instanceof rhs.__ctor__)
+          else
+            errCont("Unrecognized operator #{node.operator}")
       when 'AssignmentExpression'
-        await interp node.right, env, defer(e, value)
-        return cont(e) if e?
+        await interp node.right, env, defer(value), errCont
         if node.operator is '='
-          await assign node.left, value, env, defer(e, result)
-          return cont(e, result)
+          assign node.left, value, env, cont, errCont
         else
           if node.left.type is 'Identifier'
             original = env.resolve node.left.name
           else if node.left.type is 'MemberExpression'
-            evalMemberExpr node.left, env, cont(e, result)
-            return cont(e) if e?
-            [object, property] = result
+            await evalMemberExpr node.left, env, defer(object, property), errCont
             original = object[property]
           else
             throw "Invalid LHS in assignment"
@@ -306,10 +267,9 @@
             env.insert node.left.name, original
           else if node.left.type is 'MemberExpression'
             object[property] = original
-          return cont(null, original)
+          cont(original)
       when 'UpdateExpression'
-        await interp node.argument, env, defer(e, original)
-        return cont(e) if e?
+        await interp node.argument, env, defer(original), errCont
         if node.operator is '++'
           newValue = original + 1
         else # '--'
@@ -317,105 +277,97 @@
         if node.argument.type is 'Identifier'
           env.insert node.argument.name, newValue
         else if node.argument.type is 'MemberExpression'
-          await evalMemberExpr node.argument, env, defer(e, result)
-          return cont(e) if e?
-          [object, property] = result
+          await evalMemberExpr node.argument, env, defer(object, property), errCont
           object[property] = newValue
-        if node.prefix
-          return cont(null, newValue)
-        else
-          return cont(null, original)
+        cont(if node.prefix then newValue else original)
       when 'UnaryExpression'
         if node.operator is 'delete'
           if node.argument.type is 'MemberExpression'
-            await evalMemberExpr node.argument, env, defer(e, result)
-            return cont(e) if e?
-            [object, property] = result
-            return cont(null, delete object[property])
-          else
-            throw "NYI"
-        else
-          await interp node.argument, env, defer(e, arg)
-          return cont(e) if e?
+            await evalMemberExpr node.argument, env, defer(object, property), errCont
+            cont(delete object[property])
+          else
+            errCont "NYI"
+        else
+          await interp node.argument, env, defer(arg), errCont
           switch node.operator
             when '-'
-              return cont(null, -arg)
+              cont(-arg)
             when '~'
-              return cont(null, ~arg)
+              cont(~arg)
             when '!'
-              return cont(null, !arg)
+              cont(!arg)
             when 'typeof'
-              return cont(null, typeof arg)
+              cont(typeof arg)
             else
-              return cont("NYI")
+              errCont("NYI")
       #*** Identifiers and Literals ***#
       when 'Identifier'
-        return cont(null, env.resolve node.name)
+        cont(env.resolve node.name)
       when 'MemberExpression'
-        await evalMemberExpr node, env, defer(e, result)
-        return cont(e) if e?
-        [object, property] = result
-        return cont(null, object[property])
+        await evalMemberExpr node, env, defer(object, property), errCont
+        cont(object[property])
       when 'ThisExpression'
-        return cont(null, env.resolve 'this')
+        cont(env.resolve 'this')
       when 'Literal'
-        return cont(null, node.value)
+        cont(node.value)
       when 'ObjectExpression'
         obj = {}
         for prop in node.properties
-          await interp prop.value, env, defer(e, propValue)
-          return cont(e) if e?
+          await interp prop.value, env, defer(propValue), errCont
           obj[prop.key.name ? prop.key.value] = propValue
-        return cont(null, obj)
+        cont(obj)
       when 'ArrayExpression'
-        arr = []
-        for el in node.elements
-          await interp el, env, defer(e, elValue)
-          return cont(e) if e?
-          arr.push elValue
-        return cont(null, arr)
+        cont(
+          for el in node.elements
+            await interp el, env, defer(elValue), errCont)
       else
         console.log "Unrecognized node!"
         console.log node
-        return cont('Unrecognized node!')
+        errCont('Unrecognized node!')
   catch e
     unless e instanceof InterpreterException
       console.log "Line #{node.loc.start.line}: Error in #{node.type}"
-    return cont(e)
-
-evalMemberExpr = (node, env, cont) ->
-  await interp node.object, env, defer(e, object)
-  return cont(e) if e?
+    errCont(e)
+  return
+
+evalMemberExpr = (node, env, cont, errCont) ->
+  await interp node.object, env, defer(object), errCont
   propNode = node.property
   if propNode.type is 'Identifier' and not node.computed
-    cont(null, [object, propNode.name])
+    cont(object, propNode.name)
   else
-    await interp propNode, env, defer(e, property)
-    return cont(e, [object, property])
-
-assign = (node, value, env, cont) ->
+    await interp propNode, env, defer(property), errCont
+    cont(object, property)
+
+assign = (node, value, env, cont, errCont) ->
   if node.type is 'Identifier'
     try
       env.update node.name, value
     catch e
       env.globalInsert node.name, value
-    return cont(null, value)
+    cont(value)
   else if node.type is 'MemberExpression'
-    await evalMemberExpr node, env, defer(e, result)
-    return cont(e) if e?
-    [object, property] = result
+    await evalMemberExpr node, env, defer(object, property), errCont
     object[property] = value
-    return cont(null, value)
+    cont(value)
   else
-    return cont("Invalid LHS in assignment")
+    errCont("Invalid LHS in assignment")
+
+makeLoopCont = (body, env, bodyCont, cont, errCont) ->
+  (e) ->
+    if e instanceof BreakException
+      cont()
+    else if e instanceof ContinueException
+      bodyCont()
+    else
+      errCont(e)

 toplevel = ->
   repl = require 'repl'
   env = new Environment
   repl.start
     eval: (cmd, ctx, filename, callback) ->
-      await interp (esprima.parse cmd[1..-2], loc: true), env, defer(e, result)
-      callback null, e ? result
+      await interp (esprima.parse cmd[1..-2], loc: true), env, callback, callback

 if require.main is module
   {argv} = require 'optimist'
@@ -424,6 +376,6 @@
   else
     parsed = esprima.parse (fs.readFileSync argv._[0]), loc: true
     # interp parsed, new Environment
-    await interp parsed, new Environment, defer(e, result)
-    throw e if e?
-    result
+    interp parsed, new Environment, (->), (e) ->
+      console.log "Error: ", e
+      process.exit 1


bugfix

bugfix

--- trust.coffee
+++ trust.coffee
@@ -33,7 +33,7 @@
       return callback e if e

       # INSERT OR IGNORE INTO tsettings
-      async.map trust_ca_cert_paths, fingerprint_for_cert_path, (e, fingerprints) ->
+      async.map settings.trust_ca_cert_paths, fingerprint_for_cert_path, (e, fingerprints) ->
         return callback e if e
         sql_statements = []
         if settings.trust_charles


Spelling

Spelling

--- src/core/environment.coffee
+++ src/core/environment.coffee
@@ -56,7 +56,7 @@
     ### Resolve locals and loads any required modules. ###
     @locals = {}

-    # Load locals json if neccessary
+    # Load locals json if necessary
     if typeof @config.locals == 'string'
       filename = @resolvePath @config.locals
       @logger.verbose "loading locals from: #{ filename }"
@@ -64,7 +64,7 @@
     else
       @locals = @config.locals

-    # Load and add modules specefied with the require option to the locals context.
+    # Load and add modules specified with the require option to the locals context.
     for alias, id of @config.require
       logger.verbose "loading module '#{ id }' available in locals as '#{ alias }'"
       if @locals[alias]?
@@ -85,7 +85,7 @@
     path.resolve @contentsPath, pathname or ''

   resolveModule: (module) ->
-    ### Resolve *module* to an absolute path, mimicing the node.js module loading system. ###
+    ### Resolve *module* to an absolute path, mimicking the node.js module loading system. ###
     switch module[0]
       when '.'
         require.resolve @resolvePath module
@@ -108,7 +108,7 @@

   registerContentPlugin: (group, pattern, plugin) ->
     ### Add a content *plugin* to the environment. Files in the contents directory
-        matching the glob *pattern* will be instanciated using the plugin's `fromFile`
+        matching the glob *pattern* will be instantiated using the plugin's `fromFile`
         factory method. The *group* argument is used to group the loaded instances under
         each directory. I.e. plugin instances with the group 'textFiles' can be found
         in `contents.somedir._.textFiles`. ###
@@ -160,7 +160,7 @@
     return rv

   loadPluginModule: (module, callback) ->
-    ### Load a plugin *module*. Calls *callback* when plugin is done loading, or an error ocurred. ###
+    ### Load a plugin *module*. Calls *callback* when plugin is done loading, or an error occurred. ###
     id = 'unknown'
     done = (error) ->
       error.message = "Error loading plugin '#{ id }': #{ error.message }" if error?


color var is now cached inside of the d3plus object to speed up visualizations

--- src/core/fetch/color.coffee
+++ src/core/fetch/color.coffee
@@ -6,7 +6,8 @@
 # Finds an object's color and returns random if it cannot be found
 module.exports = (vars, id, level) ->

-  return id.d3plus.color if id.d3plus and id.d3plus.color
+  if validObject(id) and id.d3plus and id.d3plus.color
+    return id.d3plus.color

   getRandom = (c) ->
     c = fetchValue(vars, c, level) if validObject(c)
@@ -17,14 +18,15 @@
   level = vars.id.nesting[level] if typeof level is "number"

   unless vars.color.value
-    getRandom id
+    returnColor = getRandom id
   else

     colors   = []
     i        = vars.id.nesting.indexOf(level)
     getColor = (color) ->
       unless color
-        return vars.color.valueScale(0) if vars.color.value and typeof vars.color.valueScale is "function"
+        if vars.color.value and typeof vars.color.valueScale is "function"
+          return vars.color.valueScale(0)
         getRandom id
       else unless vars.color.valueScale
         if validColor(color) then color else getRandom(color)
@@ -34,7 +36,7 @@
     while i >= 0
       colorLevel = vars.id.nesting[i]
       if validObject(id)
-        o = (unless (colorLevel of id) then fetchValue(vars, id, colorLevel) else id)
+        o = unless (colorLevel of id) then fetchValue(vars, id, colorLevel) else id
         value = fetchValue(vars, o, vars.color.value, colorLevel)
       else
         value = id
@@ -43,4 +45,8 @@
         colors.push color  if colors.indexOf(color) < 0
       i--

-    if colors.length is 1 then colors[0] else vars.color.missing
+    returnColor = if colors.length is 1 then colors[0] else vars.color.missing
+
+  if validObject(id) and id.d3plus
+    id.d3plus.color = returnColor
+  returnColor


decamelize the options text and compact the color pickers

--- demo.coffee
+++ demo.coffee
@@ -17,6 +17,11 @@
       init:
         bodyColor: '#ff0000'
 byId = (id) -> document.getElementById(id)
+decamelize = (str) ->
+  str = str.replace /([a-z])([A-Z])/g, "$1 $2"
+  str.toLowerCase()
+text = (t) ->
+  document.createTextNode(t)
 create = (tag, cz) ->
   e = document.createElement(tag)
   e.setAttribute( 'class', cz ) if cz
@@ -58,7 +63,7 @@
   for k,v of obj
     div = create 'div', 'indenter'
     h = create 'div', 'param-header'
-    h.innerHTML = k
+    h.innerHTML = decamelize k
     div.appendChild h
     parent.appendChild div
     if v instanceof Array
@@ -74,7 +79,9 @@
   s = create 'input'
   s.type = 'color'
   s.value = color
-  parent.appendChild s
+  h = parent.firstChild
+  h.appendChild text(' ')
+  h.appendChild s
   s.onchange = ->
     object[label] = s.value
 makeCheckbox = (label, obj, parent) ->
@@ -108,8 +115,8 @@
 window.toggleCharts = (element) ->
   try
     makeCharts = !makeCharts
-    text = if makeCharts then 'charts off' else 'charts on'
-    element.innerHTML = text
+    txt = if makeCharts then 'charts off' else 'charts on'
+    element.innerHTML = txt
     unless loaded
       google.load 'visualization', '1', packages: ['corechart']
       google.setOnLoadCallback drawChart


Add test for annotation finding when current time changes

--- tests/streamingvideo.coffee
+++ tests/streamingvideo.coffee
@@ -44,7 +44,6 @@
 			playerWrapper: '#myplayer'
 			url: 'http://www.youtube.com/watch?v=HYLacuAp76U&feature=fvsr'
 			easement: 5
-
 		app.run();

 		app.setCurrentMode 'Rectangle'
@@ -75,6 +74,8 @@
 		equal app.dataStore.canvas.items().length, 1, "Right number of items in data store"
 		equal app.dataView.currentAnnotations.items().length, 1, "Right number of items in the data view"

+		app.setCurrentMode 'Rectangle'
+
 		# add another annotation
 		app.insertShape
 			x:200


DEMO: fix label demo

DEMO: fix label demo

--- demo/src/demos/LabelDemo.coffee
+++ demo/src/demos/LabelDemo.coffee
@@ -89,8 +89,8 @@
 		label = new CUI.Label
 			text: "Label with Icon"
 			icon: "rotate_right"
-			appearance: "important"
-		@__addLabel("apperance: important",label)
+			appearance: "secondary"
+		@__addLabel("apperance: secondary",label)

 		label = new CUI.Label
 			text: "Label with Icon"
@@ -177,8 +177,8 @@
 			multiline: true
 			text: "First line\nfollowed by a second.\nPlus an Icon"
 			icon: "rotate_left"
-			appearance: "important"
-		@__addLabel("multiline: true, apperance: important",label)
+			appearance: "muted"
+		@__addLabel("multiline: true, apperance: muted",label)

 		@__addDivider("Markdown Label")
 		label = new CUI.Label
@@ -189,4 +189,4 @@

-Demo.register(new Demo.LabelDemo())+Demo.register(new Demo.LabelDemo())


add the keyword stuff back

add the keyword stuff back

Now it works, no complaints about `OUTDENT`

--- src/scripts/achewood.coffee
+++ src/scripts/achewood.coffee
@@ -48,6 +48,11 @@
     else if arg.match /\d{2}.?\d{2}.?\d{4}/
       date = arg.replace /\D/g, ''
       fetchAchewood(msg, withDate(date))
+    else
+      query = arg
+      msg.http("http://www.ohnorobot.com/index.pl?comic=636&lucky=1&s=#{query}")
+          .get() (err, res, body) ->
+            fetchAchewood(msg, res.headers['location'])

   robot.respond /.*saddest thing\?*/i, (msg) ->


watspace

watspace

--- src/scripts/achewood.coffee
+++ src/scripts/achewood.coffee
@@ -53,7 +53,6 @@
       msg.http("http://www.ohnorobot.com/index.pl?comic=636&lucky=1&s=#{query}")
           .get() (err, res, body) ->
             fetchAchewood(msg, res.headers['location'])
-

   robot.respond /.*saddest thing\?*/i, (msg) ->
     saddest = msg.random ["06022003", "11052001", "09052006", "07302007"]


¯\_(ツ)_/¯

--- atom/config.cson
+++ atom/config.cson
@@ -33,8 +33,7 @@
   "git-plus":
     general:
       _analyticsUserId: "b0b8fef0-e9f3-4ad9-b6b1-7af19d95e9f9"
-  linter:
-    showErrorPanel: false
+  linter: {}
   "linter-ui-default": {}
   "linter-write-good":
     severityLevel: "Warning"


Updated atom configuration

Updated atom configuration

--- atom/config.cson
+++ atom/config.cson
@@ -8,6 +8,9 @@
     ]
   "exception-reporting":
     userId: "c71e93a2-9756-41e0-b72b-8837ac20b26b"
+  "linter-ui-default":
+    panelHeight: 300
+    showPanel: true
   minimap:
     plugins:
       "highlight-selected": true


Turn on fixOnSave for linter-eslint

Turn on fixOnSave for linter-eslint

--- atom/config.cson
+++ atom/config.cson
@@ -23,7 +23,8 @@
   "git-plus": {}
   linter:
     errorPanelHeight: 26
-  "linter-eslint": {}
+  "linter-eslint":
+    fixOnSave: true
   "linter-rubocop": {}
   "linter-sass-lint":
     noConfigDisable: true


Add more detailed query information.

Add more detailed query information.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -70,13 +70,13 @@
       task(callback)
     , concurrency)

-  push: (fn, callback) =>
+  push: (fn, description, callback) =>
     start = new Date().getTime()
     @queue.push(fn, (error, results) =>
       if @timing
         elapsed = new Date().getTime() - start
         capacity = results.ConsumedCapacityUnits if results?
-        console.log('Dynamo['+elapsed+'ms,'+capacity+'] '+@name)
+        console.log('Dynamo['+elapsed+'ms,'+capacity+'] '+@name+': '+description)

       callback(error, results)
     )
@@ -87,12 +87,12 @@
       task(callback)
     , concurrency)

-  push: (fn, callback) =>
+  push: (fn, description, callback) =>
     start = new Date().getTime()
     @queue.push(fn, (error, results) =>
       if @timing
         elapsed = new Date().getTime() - start
-        console.log('S3['+elapsed+'ms] '+@name)
+        console.log('S3['+elapsed+'ms] '+@name+': '+description)

       callback(error, results)
     )
@@ -148,7 +148,7 @@

         snapshots_rw_queue.push((c) ->
           db.putItem(request, c)
-        , cb)
+        , 'write Snapshot('+docName+'-'+docData.v+')', cb)

       write_data: (cb) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
@@ -157,7 +157,7 @@

         s3_rw_queue.push((c) ->
           s3.put(path, headers, data, c)
-        , cb)
+        , 'write Snapshot('+docName+'-'+docData.v+')', cb)
     (error, results) ->
       if error?
         if error.message? and error.message.match 'The conditional request failed'
@@ -193,7 +193,7 @@
         # are more keys additional requests must be made.
         snapshots_ro_queue.push((c) ->
           db.query(request, c)
-        , cb)
+        , 'query Snapshots('+docName+')', cb)

       list_operations: (cb) ->
         request =
@@ -206,7 +206,7 @@
         # are more keys additional requests must be made.
         operations_ro_queue.push((c) ->
           db.query(request, c)
-        , cb)
+        , 'query Operations('+docName+')', cb)

       delete_snapshots: ['list_snapshots', (cb, results) ->
         return cb('Document does not exist', null) if results.list_snapshots.Count == 0
@@ -223,7 +223,7 @@
                   Value: { S: item.doc.S }
             snapshots_rw_queue.push((c) ->
               db.deleteItem(request, c)
-            , cb)
+            , 'delete Snapshot('+item.doc.S+'-'+item.v.N+')', cb)
           (error, result)->
             if error?
               cb(error, null)
@@ -239,7 +239,7 @@
           (item, cb) ->
             s3_rw_queue.push((c) ->
               s3.del('/'+snapshots_bucket+'/'+item.doc.S+'-'+item.v.N+'.snapshot', c)
-            , cb)
+            , 'delete Snapshot('+item.doc.S+'-'+item.v.N+')', cb)
           (error)->
             if error?
               cb(error, null)
@@ -263,7 +263,7 @@
                   Value: { S: item.doc.S }
             operations_rw_queue.push((c) ->
               db.deleteItem(request, c)
-            , cb)
+            , 'delete Operation('+item.doc.S+'-'+item.v.N+')', cb)
           (error)->
             if error?
               cb(error, null)
@@ -303,7 +303,7 @@

         snapshots_ro_queue.push((c) ->
           db.query(request, c)
-        , cb)
+        , 'query Snapshot('+docName+')', cb)

       get_data: ['get_snapshot', (cb, results) ->
         return cb('Document does not exist', null) unless results.get_snapshot.Count == 1
@@ -311,7 +311,7 @@
         item = results.get_snapshot.Items[0]
         s3_ro_queue.push((c) ->
           s3.get('/'+snapshots_bucket+'/'+item.doc.S+'-'+item.v.N+'.snapshot', 'buffer', c)
-        , cb)
+        , 'query Snapshot('+item.doc.S+'-'+item.v.N+')', cb)
       ]
     (error, results) ->
       if error?
@@ -376,7 +376,7 @@

         snapshots_rw_queue.push((c) ->
           db.putItem(request, c)
-        , cb)
+        , 'write Snapshot('+docName+'-'+docData.v+')', cb)

       write_data: (cb) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
@@ -385,7 +385,7 @@

         s3_rw_queue.push((c) ->
           s3.put(path, headers, data, c)
-        , cb)
+        , 'write Snapshot('+docName+'-'+docData.v+')', cb)
     (error, results) ->
       if error?
         if error.message? and error.message.match 'The conditional request failed'
@@ -429,7 +429,7 @@
         # handle getting more.
         operations_ro_queue.push((c) ->
           db.query(request, c)
-        , cb)
+        , 'query Operations('+docName+'-'+start+':'+end+')', cb)

     (error, results) ->
       if error?
@@ -485,7 +485,7 @@

         operations_rw_queue.push((c) ->
           db.putItem(request, c)
-        , cb)
+        , 'write Operation('+docName+'-'+opData.v+')', cb)

     (error, results) ->
       if error?


Increase Atom window width to 1080px

Increase Atom window width to 1080px

--- atom/init.coffee
+++ atom/init.coffee
@@ -30,7 +30,7 @@
 setPreferredWindowDimensions = ->
   originalWindowDimensions = atom.getWindowDimensions()
   atom.setWindowDimensions({
-    width: 1060,
+    width: 1080,
     height: 710,
     x: originalWindowDimensions.x,
     y: originalWindowDimensions.y


Use Python black formatter in Atom

Use Python black formatter in Atom

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -99,6 +99,10 @@
     # visually compare files in current split panes
     'ctrl-cmd-c': 'split-diff:toggle'

+'atom-text-editor[data-grammar*="python"].vim-mode-plus.normal-mode':
+    # python format
+    'cmd-shift-c': 'python-black:format'
+
 # insert mode
 # 'atom-text-editor.vim-mode-plus.insert-mode':


method was renamed to include Course

method was renamed to include Course

--- test/current-user-store.spec.coffee
+++ test/current-user-store.spec.coffee
@@ -81,8 +81,8 @@
     expect(CurrentUserStore.getDashboardRoute(3)).to.equal('taskplans')

   it 'should return expected menu routes for courses', ->
-    expect(CurrentUserStore.getMenuRoutes('1')).to.deep.equal(STUDENT_MENU)
-    expect(CurrentUserStore.getMenuRoutes('2')).to.deep.equal(TEACHER_MENU)
+    expect(CurrentUserStore.getCourseMenuRoutes('1')).to.deep.equal(STUDENT_MENU)
+    expect(CurrentUserStore.getCourseMenuRoutes('2')).to.deep.equal(TEACHER_MENU)

   it 'should return expected guest for non-attending course', ->
     expect(CurrentUserStore.getCourseRole(4)).to.equal('guest')


Mark Trader as a reaction card

Mark Trader as a reaction card

--- cards.coffee
+++ cards.coffee
@@ -2455,6 +2455,7 @@

 makeCard "Trader", action, {
   cost: 4
+  isReaction: true
   playEffect: (state) ->
     trashed = state.requireTrash(state.current, 1)[0]
     if trashed?


Animate rocket flare

Animate rocket flare

--- app/js/main.coffee
+++ app/js/main.coffee
@@ -22,9 +22,14 @@
     [ cos(theta) * length, sin(theta) * length ]

 class Ship
+
+  THRUST_STEP: 0.2
+
   constructor: (x, y, @theta= HALF_PI * 3) ->
     @position = [x, y]
     @velocity = [0, 0]
+    @thrust = false
+    @thrustLevel = 0

   update: (dt, controls) =>
     if controls.right
@@ -64,11 +69,17 @@
     ctx.translate @position...
     ctx.rotate @theta

-    if @thrust
+    if @thrust and @thrustLevel < 1
+      @thrustLevel += @THRUST_STEP
+    else if not @thrust and @thrustLevel > 0
+      @thrustLevel -= @THRUST_STEP * 2
+      @thrustLevel = 0 if @thrustLevel < 0
+
+    if @thrustLevel > 0
       ctx.beginPath()
       ctx.moveTo 0, s3/2
       ctx.lineTo 0, -s3/2
-      ctx.lineTo -SHIP_SIZE, 0
+      ctx.lineTo -SHIP_SIZE * @thrustLevel, 0

       ctx.fillStyle = "FB0"
       ctx.fill()


Consistency

Consistency

--- rest_controller.coffee
+++ rest_controller.coffee
@@ -1,6 +1,6 @@
 util = require 'util'
 _ = require 'underscore'
-Utils = require 'backbone-orm/lib/utils'
+ORMUtils = require 'backbone-orm/lib/utils'
 JSONUtils = require 'backbone-orm/lib/json_utils'

 module.exports = class RESTController
@@ -32,7 +32,7 @@

   index: (req, res) =>
     try
-      cursor = @model_type.cursor(Utils.parseRawQuery(req.query))
+      cursor = @model_type.cursor(ORMUtils.parseRawQuery(req.query))
       cursor = cursor.whiteList(@white_lists.index) if @white_lists.index
       cursor.toJSON (err, json) =>
         return res.send(404) if err


comment separators in views

comment separators in views


Update test with renamed attribute

Update test with renamed attribute

--- test/src/trix/text_view_test.coffee
+++ test/src/trix/text_view_test.coffee
@@ -66,7 +66,7 @@
   elements = getElementsForText(createText(".", underline: true))
   equal elements[0].style["text-decoration"], "underline", "text decoration is underline"

-  elements = getElementsForText(createText(".", selected: true))
+  elements = getElementsForText(createText(".", frozen: true))
   equal elements[0].style["background-color"], "highlight", "background color is highlight"

   elements = getElementsForText(fixture("linkWithFormatting"))


Add test for php, python

Add test for php, python

--- test/unit/dredd-command-test.coffee
+++ test/unit/dredd-command-test.coffee
@@ -258,6 +258,43 @@
       before (done) ->
         sinon.stub interactiveConfigStub, 'run', (argv, cb) ->
           cb({language: 'nodejs'})
+        sinon.stub configUtilsStub, 'save'
+        execCommand argv: ['init'], ->
+          done()
+
+      after () ->
+        interactiveConfigStub.run.restore()
+        configUtilsStub.save.restore()
+
+      it 'should run interactive config', ->
+        assert.isTrue interactiveConfigStub.run.called
+
+      it 'should save configuration', ->
+        assert.isTrue configUtilsStub.save.called
+
+    describe '"init"', ->
+      before (done) ->
+        sinon.stub interactiveConfigStub, 'run', (argv, cb) ->
+          cb({language: 'python'})
+        sinon.stub configUtilsStub, 'save'
+        execCommand argv: ['init'], ->
+          done()
+
+      after () ->
+        interactiveConfigStub.run.restore()
+        configUtilsStub.save.restore()
+
+      it 'should run interactive config', ->
+        assert.isTrue interactiveConfigStub.run.called
+
+      it 'should save configuration', ->
+        assert.isTrue configUtilsStub.save.called
+
+
+    describe '"init"', ->
+      before (done) ->
+        sinon.stub interactiveConfigStub, 'run', (argv, cb) ->
+          cb({language: 'php'})
         sinon.stub configUtilsStub, 'save'
         execCommand argv: ['init'], ->
           done()


Add ctrl-w for delete-to-beginning-of-word

--- keymaps/vim-mode.cson
+++ keymaps/vim-mode.cson
@@ -9,6 +9,9 @@

 '.platform-darwin .editor.vim-mode:not(.command-mode)':
   'ctrl-c': 'vim-mode:activate-command-mode'
+
+'.editor.vim-mode.insert-mode':
+  'ctrl-w': 'editor:delete-to-beginning-of-word'

 '.editor.vim-mode:not(.insert-mode)':
   'h': 'vim-mode:move-left'


:lipstick: GutterComponent

:lipstick: GutterComponent

--- src/gutter-component.coffee
+++ src/gutter-component.coffee
@@ -33,7 +33,7 @@
         lastBufferRow = bufferRow
         lineNumber = (bufferRow + 1).toString()

-      key = tokenizedLines[i]?.id
+      key = tokenizedLines[i].id
       screenRow = startRow + i
       lineNumbers.push(LineNumberComponent({key, lineNumber, maxDigits, bufferRow, screenRow, lineHeight}))
       lastBufferRow = bufferRow
@@ -45,10 +45,9 @@
   # non-zero-delta change to the screen lines has occurred within the current
   # visible row range.
   shouldComponentUpdate: (newProps) ->
-    {renderedRowRange, pendingChanges, scrollTop} = @props
-
     return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight')

+    {renderedRowRange, pendingChanges} = newProps
     for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0
       return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start


tweaked delete behavior to delete blank lines without checking them for commands

--- web/scripts/playerCodeEditor.coffee
+++ web/scripts/playerCodeEditor.coffee
@@ -111,10 +111,10 @@
             command = @getCommandFromLine line
             @commands[command]['used']--
             @UpdateCommandsStatus()
-            if text.getLength() == 1
-                text.insertLines currentRow + 1, ["\n"]
-                text.removeNewLine currentRow
-            text.removeLines currentRow, currentRow
+        if text.getLength() == 1
+            text.insertLines currentRow + 1, ["\n"]
+            text.removeNewLine currentRow
+        text.removeLines currentRow, currentRow
         return

     insertLine: ({text, currentRow}) ->


more debguggin

more debguggin

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -61,12 +61,14 @@

   robot.hear /^http(s?):\/\/(.*)/i, (msg) ->
     url = msg.match[0]
+    msg.send 'this is the url: ' + url
     unless process.env.TRELLO_API_KEY?
       msg.send "Please specify the Trello API key in TRELLO_API_KEY"
       return
     unless process.env.TRELLO_TOKEN?
       msg.send "Please specify the Trello token in TRELLO_TOKEN"
       return
+    msg.send 'going for it...'
     msg.http(url)
       .get() (err, res, body) ->
         msg.send 'got the url: ' + url


properly formatted test program.

properly formatted test program.

--- brainfuck.coffee
+++ brainfuck.coffee
@@ -90,6 +90,8 @@
 # TODO: handle input

 # ## Evaluate
+
+# (ast : tree String) -> unit
 evaluate = (ast) ->
   evaluateBlock ast

@@ -110,7 +112,9 @@
       environment[pointer]--
     when OUTPUT_BYTE
       console.log(String.fromCharCode(environment[pointer]))
-    when INPUT_BYTE then console.log "INPUT!"
+    when INPUT_BYTE
+      console.log "(Should prompt user)"
+      # prompt("Input:")

 isCommand = (child) -> typeof child is 'string'
 isBlock = (child) -> typeof child is 'object'
@@ -121,26 +125,26 @@

 # # Main
 program = "
-  +++++ +++++             initialize counter (cell #0) to 10
-  [                       use loop to set the next four cells to 70/100/30/10
-      > +++++ ++              add  7 to cell #1
-      > +++++ +++++           add 10 to cell #2
-      > +++                   add  3 to cell #3
-      > +                     add  1 to cell #4
-      <<<< -                  decrement counter (cell #0)
-  ]
-  > ++ .                  print 'H'
-  > + .                   print 'e'
-  +++++ ++ .              print 'l'
-  .                       print 'l'
-  +++ .                   print 'o'
-  > ++ .                  print ' '
-  << +++++ +++++ +++++ .  print 'W'
-  > .                     print 'o'
-  +++ .                   print 'r'
-  ----- - .               print 'l'
-  ----- --- .             print 'd'
-  > + .                   print '!'
+  +++++ +++++             initialize counter (cell #0) to 10\n
+  [                       use loop to set the next four cells to 70/100/30/10\n
+      > +++++ ++              add  7 to cell #1\n
+      > +++++ +++++           add 10 to cell #2\n
+      > +++                   add  3 to cell #3\n
+      > +                     add  1 to cell #4\n
+      <<<< -                  decrement counter (cell #0)\n
+  ]\n
+  > ++ .                  print 'H'\n
+  > + .                   print 'e'\n
+  +++++ ++ .              print 'l'\n
+  .                       print 'l'\n
+  +++ .                   print 'o'\n
+  > ++ .                  print ' '\n
+  << +++++ +++++ +++++ .  print 'W'\n
+  > .                     print 'o'\n
+  +++ .                   print 'r'\n
+  ----- - .               print 'l'\n
+  ----- --- .             print 'd'\n
+  > + .                   print '!'\n
   > .                     print '\n'
 "
 console.log "-*- Input Program -*-"


Remove S3 data along with project

Remove S3 data along with project

--- server/methods.coffee
+++ server/methods.coffee
@@ -24,38 +24,40 @@
   })
   [s3Bucket, s3Client]

+createZip = (files, user, id, s3Bucket, s3Client) ->
+  logger.debug("Generating zip...")
+  zip = new JSZip()
+  for file in files
+    logger.debug("Downloading file '#{file.url}'...")
+    result = Meteor.http.get(file.url)
+    if result.statusCode != 200
+      throw new Error("Couldn't download '#{file.url}', status code: #{result.statusCode}")
+    logger.debug("Adding file '#{file.name}' to zip")
+    zip.file(file.name, result.content)
+  output = zip.generate({
+    type: "nodebuffer",
+    compression: "DEFLATE",
+  })
+  logger.debug("Uploading zip file to S3 (bucket: '#{s3Bucket}')")
+  filePath = "u/#{user.username}/#{id}/#{id}.zip"
+  s3Client.putObjectSync({
+    Key: filePath,
+    ACL: "public-read",
+    Body: output,
+  })
+  region = getSetting('AWSRegion')
+  # TODO: Try to get URL from S3
+  ["https://s3.#{region}.amazonaws.com/#{s3Bucket}/#{filePath}", output.length]
+
 Meteor.methods({
   createProject: (id, title, description, instructions, tags, pictures, files) ->
-    createZip = () ->
-      logger.debug("Generating zip...")
-      zip = new JSZip()
-      for file in files
-        logger.debug("Downloading file '#{file.url}'...")
-        result = Meteor.http.get(file.url)
-        if result.statusCode != 200
-          throw new Error("Couldn't download '#{file.url}', status code: #{result.statusCode}")
-        logger.debug("Adding file '#{file.name}' to zip")
-        zip.file(file.name, result.content)
-      output = zip.generate({
-        type: "nodebuffer",
-        compression: "DEFLATE",
-      })
-      [s3Bucket, s3Client] = getS3Objs()
-      logger.debug("Uploading zip file to S3 (bucket: '#{s3Bucket}')")
-      filePath = "u/#{user.username}/#{id}/#{id}.zip"
-      s3Client.putObjectSync({
-        Key: filePath,
-        ACL: "public-read",
-        Body: output,
-      })
-      ["https://s3.amazonaws.com/#{s3Bucket}/#{filePath}", output.length]
-
     user = getUser(@)

     if Projects.findOne({owner: user.username, projectId: id})?
       throw new Error("Project '#{user.username}/#{id}' already exists")

-    [zipUrl, zipSize] = createZip()
+    [s3Bucket, s3Client] = getS3Objs()
+    [zipUrl, zipSize] = createZip(files, user, id, s3Bucket, s3Client)

     metadata = {
       owner: user.username,
@@ -108,6 +110,8 @@
     removeStaleFiles(project.pictures, pictures, 'picture')
     removeStaleFiles(project.files, files, 'file')

+    [zipUrl, zipSize] = createZip(files, user, id, s3Bucket, s3Client)
+
     Projects.update(selector, {$set: {
       title: title,
       description: description,
@@ -115,8 +119,22 @@
       tags: R.map(S.trim(null), tags.split(',')),
       pictures: pictures,
       files: files,
+      zipFile: {
+        url: zipUrl,
+        size: zipSize,
+      },
     }})
   removeProject: (id) ->
+    removeFolder = () ->
+      objects = s3Client.listObjectsSync({Prefix: dirPath})
+      toDelete = R.map(((o) -> {Key: o.Key}), objects.Contents)
+      result = s3Client.deleteObjectsSync({Delete: {Objects: toDelete}})
+      logger.debug("Deleted #{objects.Contents.length} object(s), result:", result)
+      # API will list max 1000 objects
+      if objects.Contents.length == 1000
+        logger.debug("We hit max number of listed objects, deleting recursively")
+        removeFolder()
+
     user = getUser(@)
     project = Projects.findOne({projectId: id})
     if !project?
@@ -124,5 +142,13 @@
     if project.owner != user.username
       throw new Meteor.Error("unauthorized", "Only the owner may remove a project")

-    Projects.remove({projectId: id})
+    logger.debug()
+
+    logger.debug("Removing project '#{user.username}/#{id}'")
+    Projects.remove({owner: user.username, projectId: id})
+
+    [s3Bucket, s3Client] = getS3Objs()
+    dirPath = "u/#{user.username}/#{id}"
+    logger.debug("Removing files from S3 bucket '#{s3Bucket}', directory '#{dirPath}'")
+    removeFolder()
 })


set inspector size

set inspector size

--- src/layout/inspector.coffee
+++ src/layout/inspector.coffee
@@ -13,10 +13,10 @@
 exports.inspector = {
   _LABEL: 'inspector'
   x: (world) -> world.get('width')
-  y: my.margin
-  height: (world) -> world.get('screen')[vector.size.height]
+  y: 2*my.margin
+  height:(world) -> world.get('screen').at vector.size.height
   fill: my.color.background
-  _AUTHORITY: {x:0, y:0, height: 'auto'}
+  _AUTHORITY: {x:0}
   _CHILDREN: [
     {
       _LABEL: 'status'


support for new connect

support for new connect

--- src/lib/expressError.coffee
+++ src/lib/expressError.coffee
@@ -205,6 +205,18 @@
   handleUncaughtExceptions(contextLinesCount) if handleUncaughtException

   return (err, req, res, next) ->
+    if typeof err is "number"
+      status = err
+      name = http.STATUS_CODES[status]
+      err = new Error(name)
+      err.name = name
+      err.status = status
+    else if typeof err is "string"
+      name = err
+      err = new Error(name)
+      err.name = name
+      err.status = 500
+
     res.statusCode = err.status  if err.status
     res.statusCode = 500  if res.statusCode < 400
     accept = req.headers.accept or ""


operations: fix generateSubscriptionsNotifications followee

This should fix Github issue #46

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -1382,7 +1382,7 @@
         followee = update.node.match(NODE_OWNER_TYPE_REGEXP)?[1]
         type: 'items'
         node: "/user/#{update.user}/subscriptions"
-        items: [{ id: update.followee }]
+        items: [{ id: followee }]
         # Actual item payload will be completed by Notify transaction
     ).filter((update) ->
         itemId = update.items[0].id


Cleaned up some code. Less looping and object creation.

--- start.coffee
+++ start.coffee
@@ -178,11 +178,9 @@

 			valid_docs = [ ]

-			# Iterate over all the documents we got back.
+			# Iterate over all the documents we got back. Ensure the attributes
+			# that we're looking for exist. Populate the valid_docs array.
 			for doc in (doc.value for doc in docs)
-				# Ensure the attributes
-				# that are requested exist in the document.
-
 				# Sanity check on each doc. Make sure it has the attributes requested..
 				skip = false
 				for attr in attrs
@@ -196,75 +194,47 @@

 				valid_docs.push doc

-			# For each feature that we're filtering by
-			# determine the mean and standard deviation.
-			# Note that we use arrays that correspond to
-			# the index of attrs.
-			means			= [ ]
-			standard_deviations	= [ ]
-
+			# Go through each attribute that was specified.
 			for attr in attrs

-				# Get the sum of the attribute for all the docs.
+				# Calculate the mean of the attribute for all docs in valid_docs.
 				sum = 0
 				for doc in valid_docs
 					sum += doc[attr]
-
-				# Calculate the mean using the number of documents
-				# and the sum.
 				mean = ( sum / valid_docs.length )
-				means.push mean

-				# To calculate the standard deviation we subtract the mean
-				# ( calculated above ) from the value of the attribute, and raise
-				# it to the power of 2. This gets summed for all documents.
-				# The square root is then taken of the summation divided by the number
-				# of documents.
-
+				# Calculate the standard deviation.
 				squared_diff_sum = 0
 				for doc in valid_docs
 					squared_diff_sum += Math.pow( ( doc[attr] - mean ), 2 )
-
-				standard_deviations.push Math.sqrt( squared_diff_sum / valid_docs.length )
-
-
-			# We've now got the means and standard deviations for
-			# each attribute. Normalize the values.
-
-			for doc in valid_docs
-				i = 0
-				for attr in attrs
-					doc[attr] = doc[attr] - means[i]
-					doc[attr] = doc[attr] / standard_deviations[i]
-					i++
-
-			# Generate a matrix of all the values we're going to svd..
+				standard_deviation = Math.sqrt( squared_diff_sum / valid_docs.length )
+
+				# Now that we have the mean and standard deviation for the attribute, run through
+				# each doc in valid_docs and compute the normalized attribute.
+				for doc in valid_docs
+					doc["normalized_" + attr] = doc[attr] - mean
+					doc["normalized_" + attr] = doc["normalized_" + attr] / standard_deviation
+
+			# We've normalized the data at this point, so each doc contains ["normalized_"+attr] for
+			# each attr in attrs. At this point, generate a quick matrix using arrays so that we
+			# can use the sylvester module to compute the PCA.
+
 			matrix = [ ]
 			for doc in valid_docs
 				_i = [ ]
 				for attr in attrs
-					_i.push doc[attr]
+					_i.push doc["normalized_"+attr]
 				matrix.push _i

+			# Project into 2 dimensions..
 			svd	= sylvester.Matrix.create matrix
 			k	= svd.pcaProject 2

-
-			_return = [ ]
-			for doc in valid_docs
-				if k.Z.elements[i]?
-					doc.x = k.Z.elements[i][0]
-					doc.y = k.Z.elements[i][1]
-				else
-					doc.x = 0
-					doc.y = 0
-
-				# Note that we don't shove the entire doc back..
-				_return.push { "artist": doc.artist, "title": doc.title, "x": doc.x, "y": doc.y, "_id": doc._id }
-
-				i++
-
-			res.json _return
+			for i in [0..valid_docs.length-1]
+				valid_docs[i].x = k.Z.elements[i][0]
+				valid_docs[i].y = k.Z.elements[i][1]
+
+			res.json valid_docs

 	app.get "/song/:id", ( req, res ) ->
 		res.sendfile req.doc.path


Fixing logging

Fixing logging

--- steam.coffee
+++ steam.coffee
@@ -2,7 +2,7 @@
 #   Steam Web API w/ Dota 2
 #
 # Dependencies:
-#   "moment": "^2.8.1"
+#   "moment": "^2.8.3"
 #   "msgpack": "^0.2.3"
 #   "ref": "^0.3.2"
 #
@@ -78,14 +78,13 @@
 GetSteamResult = (msg, endpoint, params = {}, handler, version = 1) ->
     params.key = STEAM_API_KEY

-    # Impl: Try-catch
     msg.http("#{STEAM_API_URL}/#{endpoint}/v#{version}/")
         .query(params)
         .get() (err, res, body) ->
             if err or res.statusCode isnt 200
                 err = "Bad request (invalid Steam web API key)" if res.statusCode is 400
-                msg.reply "An error occurred while attempting to process your request."
-                return robot.logger.error err
+                msg.reply "An error occurred while attempting to process your request. Please try again later."
+                return msg.robot.logger.error err
             handler JSON.parse(body)

 #
@@ -99,7 +98,6 @@
         callback object.response.steamid

 _GetCommunityID = (steamID) ->
-    # Impl: Try-catch
     buffer = new Buffer 8
     buffer.writeUInt64LE steamID, 0
     communityID = buffer.readUInt32LE 0
@@ -174,7 +172,6 @@
     if not STEAM_API_KEY?
         return robot.logger.debug "Missing STEAM_API_KEY in environment. Please set and try again."

-    # Impl: Try-catch
     fs.readFile DOTA_HEROES_DATA_PATH, (err, data) ->
         return robot.logger.error if err
         DOTA_HEROES = msgpack.unpack data
@@ -212,7 +209,7 @@
                 date = moment.unix(match.start_time).fromNow()
                 target = _GetPlayer communityID, match.players
                 hero = _GetHero(target.hero_id).LocalizedName
-                # W / L TBA
+                # TODO: W / L TBA
                 msg.send "Match ID: #{match.match_id} | Lobby: #{DOTA_LOBBIES[match.lobby_type]} | Hero: #{hero} | #{date}"

     # GET Player Dota 2 match details
@@ -238,7 +235,7 @@
                     return
                 faction = _GetFaction target.player_slot
                 msg.reply "#{faction} - #{_GetHero(target.hero_id).LocalizedName} (Lvl #{target.level}) | KDA: #{target.kills}/#{target.deaths}/#{target.assists} | LH: #{target.last_hits} | GPM: #{target.gold_per_min} | XPM: #{target.xp_per_min} | HD: #{target.hero_damage} | TD: #{target.tower_damage} | TGE: #{target.gold_per_min*duration}"
-                # Future: Team fight contribution? Overall contribution algo.
+                # Future (TODO): Team fight contribution? Overall contribution algo.

             if genericID?
                 if _IsSteamID genericID


switched login and logout to new api routes

--- mrfogg-front/app/coffee/app.coffee
+++ mrfogg-front/app/coffee/app.coffee
@@ -45,8 +45,8 @@

     apiUrls = {
         "root": "/"
-        "login": "/login"
-        "logout": "/logout"
+        "login": "/auth/login"
+        "logout": "/auth/logout"
         "users": "/users"
         "trips": "/trips"
         "cards": "/trips/%s/tasks/%s/cards"


Fix audio playback

Fix audio playback

--- widgets/support/support.coffee
+++ widgets/support/support.coffee
@@ -5,7 +5,7 @@
     @lastCount  = 0
     @muteButton = $('#mute-button')

-    @muteButton.click => this.mute()
+    @muteButton.click (e) => this.mute(e)

   onData: (data) ->
     currentCount = data.items.length
@@ -21,14 +21,18 @@

   alert: ->
     $(@node).addClass('active')
-    @audio.play() unless @muted
+    unless @muted
+      @audio.load()
+      @audio.play()
   clear: ->
     $(@node).removeClass('active')

-  mute: ->
+  mute: (e) ->
     @muted = !@muted

     if @muted
       @muteButton.addClass('muted')
     else
-      @muteButton.removeClass('muted')+      @muteButton.removeClass('muted')
+
+    false


update config syntax for contentful

update config syntax for contentful

--- root/contentful.coffee
+++ root/contentful.coffee
@@ -2,12 +2,9 @@
   access_token: '<%= delivery_token %>'
   management_token: '<%= management_token %>'
   space_id: '<%= space_id %>'
-  content_types: [
-    # {
+  content_types:
+    # posts:                          # data will be made available through this key on the `contentful` object in your templates
     #   id: 'xxxx'                    # ID of your content type
-    #   name: 'posts'                 # data will be made available through this key on the `contentful` object in your templates
     #   filters: {}                   # passes filters to the call to contentful's API, see contentful's docs for more info
     #   template: 'path/to/template'  # if present a single page view will be created for each entry in the content type
     #   path: (entry) ->              # override function for generating single page file path, passed in the entry object
-    # }
-  ]


Big performance improvements for scrubbing between frames; look out for bugs.

--- app/lib/surface/Surface.coffee
+++ app/lib/surface/Surface.coffee
@@ -100,7 +100,7 @@
     @stage.removeAllEventListeners()
     @stage.enableDOMEvents false
     @stage.enableMouseOver 0
-    @playScrubbedSounds = null
+    @onFramesScrubbed = null
     @onMouseMove = null
     @onMouseDown = null
     @tick = null
@@ -202,32 +202,33 @@
         .get(@)
         .to({currentFrame:@scrubbingTo}, scrubDuration, createjs.Ease.sineInOut)
         .call(onTweenEnd)
-      t.addEventListener('change', @playScrubbedSounds)
+      t.addEventListener('change', @onFramesScrubbed)
     else
       @currentFrame = @scrubbingTo
-      @playScrubbedSounds()
+      @onFramesScrubbed()  # For performance, don't play these for instant transitions.
       onTweenEnd()

     @updateState true
     @onFrameChanged()

-  playScrubbedSounds: (e) =>
-    # gotta play all the sounds, even when scrubbing!
-    rising = @currentFrame > @lastFrame
-    actualCurrentFrame = @currentFrame
-    tempFrame = if rising then Math.ceil(@lastFrame) else Math.floor(@lastFrame)
-    while true  # temporary fix to stop cacophony
-      break if rising and tempFrame > actualCurrentFrame
-      break if (not rising) and tempFrame < actualCurrentFrame
-      @currentFrame = tempFrame
-      frame = @world.getFrame(@getCurrentFrame())
-      frame.restoreState()
-      for thangID, sprite of @spriteBoss.sprites
-        sprite.playSounds false, Math.max(0.05, Math.min(1, 1 / @scrubbingPlaybackSpeed))
-      tempFrame += if rising then 1 else -1
-    @currentFrame = actualCurrentFrame
-
-    # TODO: are these needed, or perhaps do they duplicate things?
+  onFramesScrubbed: (e) =>
+    if e
+      # Gotta play all the sounds when scrubbing (but not when doing an immediate transition).
+      rising = @currentFrame > @lastFrame
+      actualCurrentFrame = @currentFrame
+      tempFrame = if rising then Math.ceil(@lastFrame) else Math.floor(@lastFrame)
+      while true  # temporary fix to stop cacophony
+        break if rising and tempFrame > actualCurrentFrame
+        break if (not rising) and tempFrame < actualCurrentFrame
+        @currentFrame = tempFrame
+        frame = @world.getFrame(@getCurrentFrame())
+        frame.restoreState()
+        for thangID, sprite of @spriteBoss.sprites
+          sprite.playSounds false, Math.max(0.05, Math.min(1, 1 / @scrubbingPlaybackSpeed))
+        tempFrame += if rising then 1 else -1
+      @currentFrame = actualCurrentFrame
+
+    @restoreWorldState()
     @spriteBoss.update true
     @onFrameChanged()

@@ -325,7 +326,7 @@
   onNewWorld: (event) ->
     return unless event.world.name is @world.name
     @casting = false
-
+
     # This has a tendency to break scripts that are waiting for playback to change when the level is loaded
     # so only run it after the first world is created.
     Backbone.Mediator.publish 'level-set-playing', { playing: @wasPlayingWhenCastingBegan } unless event.firstWorld
@@ -493,14 +494,23 @@
   tick: (e) =>
     # seems to be a bug where only one object can register with the Ticker...
     oldFrame = @currentFrame
+    oldWorldFrame = Math.floor oldFrame
     while true
       Dropper.tick()
       @trailmaster.tick() if @trailmaster
       # Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet)
       frameAdvanced = (@playing and @currentFrame < @world.totalFrames) or @totalFramesDrawn < 2
       @currentFrame += @world.frameRate / @frameRate if frameAdvanced
-      @updateSpriteSounds() if frameAdvanced
+      newWorldFrame = Math.floor @currentFrame
+      worldFrameAdvanced = newWorldFrame isnt oldWorldFrame
+      if worldFrameAdvanced
+        # Only restore world state when it will correspond to an integer WorldFrame, not interpolated frame.
+        @restoreWorldState()
+        oldWorldFrame = newWorldFrame
       break unless Dropper.drop()
+    if frameAdvanced and not worldFrameAdvanced
+      # We didn't end the above loop on an integer frame, so do the world state update.
+      @restoreWorldState()

     # these are skipped for dropped frames
     @updateState @currentFrame isnt oldFrame
@@ -513,7 +523,7 @@
       Backbone.Mediator.publish('surface:mouse-' + (if mib then "over" else "out"), {})
       @mouseInBounds = mib

-  updateSpriteSounds: ->
+  restoreWorldState: ->
     @world.getFrame(@getCurrentFrame()).restoreState()
     current = Math.max(0, Math.min(@currentFrame, @world.totalFrames - 1))
     if current - Math.floor(current) > 0.01
@@ -523,7 +533,7 @@
     @spriteBoss.updateSounds()

   updateState: (frameChanged) ->
-    # world state must have been restored in @updateSpriteSounds
+    # world state must have been restored in @restoreWorldState
     @camera.updateZoom()
     @spriteBoss.update frameChanged unless @casting
     @dimmer?.setSprites @spriteBoss.sprites


Update Alex's bio to remove a word

Update Alex's bio to remove a word

--- app/pages/about/team-page.cjsx
+++ app/pages/about/team-page.cjsx
@@ -21,7 +21,7 @@
         He's also working on a degree in astronomy with the OU.'''
       alexBowyer:
         title: 'Web Science Architect'
-        bio: '''Alex mostly works remotely for Zooniverse from his home in Northumberland.
+        bio: '''Alex works remotely for Zooniverse from his home in Northumberland.
         He's a software architect designing and running Zooniverse experiments. He has a computer science degree,
         and has developed software & websites at large enterprises and startups.'''
       alexWeiksnar:


Codecombat pricing plan schema change

Codecombat pricing plan schema change

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -2,6 +2,15 @@
 {FeatureAuthoritySchema, FeatureRecipientSchema} = require './feature.schema'

 emailSubscriptions = ['announcement', 'tester', 'level_creator', 'developer', 'article_editor', 'translator', 'support', 'notification']
+
+###
+SCHEMA WARNING
+
+Any changes made to this schema need to be shared on the Ozaria user schema.
+Both products share the same database collection and currently duplicating
+changes is how we can avoid validation errors as the user traverses between
+the two products.
+###

 UserSchema = c.object
   title: 'User'
@@ -272,7 +281,7 @@

   stripe: c.object {}, {
     customerID: { type: 'string' }
-    planID: { enum: ['basic'], description: 'Determines if a user has or wants to subscribe' }
+    planID: { enum: ['basic', 'price_1Hja49KaReE7xLUdlPuATOvQ'], description: 'Determines if a user has or wants to subscribe. Matches subscription plan on stripe.' }
     subscriptionID: { type: 'string', description: 'Determines if a user is subscribed' }
     token: { type: 'string' }
     couponID: { type: 'string' }


Add products and prepaids to User schema for consistency with codecombat

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -358,6 +358,7 @@
       endDate: c.stringDate()
       type: { type: ['string', 'null'] }
       includedCourseIDs: { type: ['array', 'null'], description: 'courseIDs that this prepaid includes access to', items: c.objectId() }
+      migrated: {type: 'boolean'}
     }
   }
   enrollmentRequestSent: { type: 'boolean', description: 'deprecated' }
@@ -418,6 +419,35 @@
         additionalProperties: FeatureRecipientSchema

   archived: c.date {description: 'Marks this record for automatic online archiving to cold storage by our cloud database.'}
+  products: c.array {title: 'Products purchased or used by this user'},
+    c.object { required: ['product', 'startDate', 'recipient', 'paymentService', 'paymentDetails'],  additionalProperties: true },
+      # ensure we can add additionalProperties
+      product: { type: 'string', enum: ['course', 'basic_subscription', 'pd', 'ai-league', 'online-classes'], decription: 'The "name" field for the product purchased' }  # And/or the ID of the Product in the database, if we make a Product for each thing we can buy?
+
+      prepaid: c.objectId(links: [ {rel: 'db', href: '/db/prepaid/{($)}'} ])  # required for type: “course” for legacy compatibility, optional for other types
+      productOptions:
+        oneOf: [
+          includedCourseIDs: {type: ['array', 'null']}
+        ]
+      startDate: c.date()
+      endDate: c.date()  # TODO: optional indication of no end date (lasts forever) - or do we just leave unset?
+      purchaser: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ]) # in case of gifts
+      recipient: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
+      purchaserDesc:
+        detailType: enum: ['email', 'phone']
+        detail: c.shortString(description: 'We may have a purchaser with no account, in which case only this email/phone/... will be set')
+      paymentService: { enum: ['stripe', 'testing', 'free', 'api', 'external', 'paypal']}  # Removed 'ios', could perhaps remove 'paypal', could differentiate 'external' further
+      paymentDetails:
+        c.object {additionalProperties: true},
+          purchaseDate: c.date()  # TODO: separate payment date and invoice date (esp. online classes)?
+          amount: { type: 'integer', description: 'Payment in cents on US server and in RMB cents on the China server' }
+          # Do we need something about autorenewal / frequency here?
+          oneOf: [
+            { stripeCustomerId: { type: 'string' }, subscriptionId: { type: 'string' }, paymentSession: c.objectId(links: [ {rel: 'extra', href: '/db/payment.session/{($)}'} ]) }  # TODO: other various Stripe-specific options
+            { paypalCustomerId: { type: 'string' } }  # TODO: various PayPal-specific options, if we keep PayPal
+            { staffCreator: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ]) }  # any other external payment source options?
+            # ... etc. for each possible payment service ...
+          ]

 c.extendBasicProperties UserSchema, 'user'


Don't try to give linked source to unlinked window

--- app/models/data_source.coffee
+++ app/models/data_source.coffee
@@ -39,11 +39,16 @@
   isExternal: =>
     (@get('type') is 'external')

+  isInternal: =>
+    (@get('type') is 'internal')
+
   sourceName: =>
     if @isExternal()
       name = Manager.get('sources').get(@get('source')).get('name')
+    else if @isInternal()
+      name = @tools.find((tool) => tool.get('channel') == @get('source')).get('name')
     else
-      name = @tools.find((tool) => tool.get('channel') == @get('source')).get('name')
+      name = ''
     return name

   dataExtents: (key, ids) =>


changes text on 'apply for beta' button

changes text on 'apply for beta' button

--- app/pages/lab/visibility.cjsx
+++ app/pages/lab/visibility.cjsx
@@ -61,7 +61,7 @@

-          <button type="button" className="standard-button" disabled={@props.project.private or not @props.project.live or @state.setting.beta_requested or @props.project.beta_requested} onClick={@set.bind this, 'beta_requested', true}>Apply for beta</button>{' '}
+          <button type="button" className="standard-button" disabled={@props.project.private or not @props.project.live or @state.setting.beta_requested or @props.project.beta_requested} onClick={@set.bind this, 'beta_requested', true}>Apply for review</button>{' '}
           {if @props.project.private
             Only public projects can apply for a beta.
           else if not @props.project.live


Remember that localStorage only stores strings

--- app/partials/main-footer.cjsx
+++ app/partials/main-footer.cjsx
@@ -47,7 +47,11 @@
     checked = apiClient.params.admin ? false

     unless @state.checked is checked
-      localStorage.setItem 'adminFlag', checked
+      if checked
+        localStorage.setItem 'adminFlag', checked
+      else
+        localStorage.removeItem 'adminFlag'
+
       @setState {checked}

   toggleAdminMode: (e) ->


Update messaging

Update messaging

Now that we have a random dataset active rather than interesting images, and given we want people to classify, this changes the wording to encourage classifying and discourage browsing.

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -20,7 +20,7 @@
         We’re done for now, but we still need you

-        With your help, we've classified all the data we have so far. Great work! We are leaving some images active for you to browse. Please keep an eye on the blog for updates about research and when new Snapshot Serengeti data will be available.

+        With your help, we've classified all the data we have so far. Great work! We are leaving some images active for you to classify. Please keep an eye on the blog for updates about research and when new Snapshot Serengeti data will be available.

         If you’d like to work on a similar project, we need your animal-watching skills at
         Chicago Wildlife Watch


Fix typo

Fix typo

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -89,9 +89,8 @@
       headUp: 'Head Up'
       headDown: 'Head Down'
       neither: 'Neither'
-      notVisible: 'Not Visble'
+      notVisible: 'Not Visible'
       notApplicable: 'N/A'
-

     babies: 'Young present'
     rain: 'Raining'


Show humans vs. ogres for Wakka Maul ladder

--- app/views/ladder/utils.coffee
+++ app/views/ladder/utils.coffee
@@ -11,10 +11,14 @@
     color = teamConfigs[team].color
     bgColor = hslToHex([color.hue, color.saturation, color.lightness + (1 - color.lightness) * 0.5])
     primaryColor = hslToHex([color.hue, 0.5, 0.5])
+    if level.get('slug') in ['wakka-maul']
+      displayName = _.string.titleize(team)
+    else
+      displayName = $.i18n.t("ladder.#{team}")  # Use Red/Blue instead of Humans/Ogres
     teams.push({
       id: team
       name: _.string.titleize(team)
-      displayName: $.i18n.t("ladder.#{team}")  # Use Red/Blue instead of Humans/Ogres
+      displayName: displayName
       otherTeam: otherTeam
       otherTeamDisplayName: $.i18n.t("ladder.#{otherTeam}")
       bgColor: bgColor


Fix time sensitive spec

Fix time sensitive spec

--- apps/shows/test/routes.coffee
+++ apps/shows/test/routes.coffee
@@ -56,7 +56,7 @@
           @next.called.should.be.true()

     it 'fetches the cities & shows and renders the city template', ->
-      @upcomingShow = new PartnerShow fabricate('show', start_at: moment().add(5, 'days').format(), end_at: moment().add(15, 'days').format())
+      @upcomingShow = new PartnerShow fabricate('show', start_at: moment().add(8, 'days').format(), end_at: moment().add(15, 'days').format())
       @openingShow = new PartnerShow fabricate('show', start_at: moment().add(1, 'days').format(), end_at: moment().add(10, 'days').format())
       @currentShow = new PartnerShow fabricate('show', start_at: moment().subtract(5, 'days').format(), end_at: moment().add(5, 'days').format())
       @pastShow = new PartnerShow fabricate('show', start_at: moment().subtract(15, 'days').format(), end_at: moment().subtract(5, 'days').format())


Remove the mixpanel shim now that it's fully deployed

--- js/app.coffee
+++ js/app.coffee
@@ -1,13 +1,5 @@
 noop = () ->
   null
-
-window.mixpanel ||=
-  name_tag: noop
-  identify: noop
-  track: noop
-  track_link: noop
-  track_pageview: noop
-  register_once: noop

 CI.ajax.init()


Remove unused variable

Remove unused variable

--- js/app.coffee
+++ js/app.coffee
@@ -66,7 +66,6 @@
     @security = new CI.outer.Page("security", "Security", "View Security", {addLinkTargets: true})
     @securityHOF = new CI.outer.Page("security_hall_of_fame", "Security Hall of Fame", "View Security Hall of Fame")
     @builds_per_page = 30
-    @builds_list_path = null

     @sticky_help_is_open = ko.observable(false)

@@ -181,7 +180,6 @@
     position = {offset: page * @builds_per_page, limit: @builds_per_page}

     $.getJSON path, position, (data) =>
-      @builds_list_path = path
       @builds(new CI.inner.Build d for d in data)
       @builds_have_been_loaded(true)
     .fail (request) => VM.error.display()