Added a line to re-start suspended plugins.

Suspended plugins = plugins that skipped their last start due to the
start method being commented out.

--- server.coffee
+++ server.coffee
@@ -36,6 +36,12 @@
 		Db.shared.set 'settings', 'totalperiod', totalperiod

 	if not Db.shared.get 'nextround'
+		exports.start()
+
+exports.onUpgrade = !->
+	# start plugins that are now hanging in limbo due to the start being commented.
+	nextround = Db.shared.get 'nextround'
+	if nextround < Plugin.time()
 		exports.start()

 exports.getTitle = ->
@@ -109,9 +115,9 @@
 	Db.shared.set 'votesopen', true
 	Db.shared.set 'roundcounter', (0|Db.shared.get 'roundcounter') + 1

-	newclosetime = newvoteclose - Plugin.time()
-	newremindtime = Math.round((newvoteclose - Plugin.time()) * 0.7)
-	newstarttime = nextround - Plugin.time()
+	newclosetime = (newvoteclose - Plugin.time()) * 1000
+	newremindtime = (Math.round((newvoteclose - Plugin.time()) * 0.7)) * 1000
+	newstarttime = (nextround - Plugin.time()) * 1000

 	if (newclosetime < 120 || newremindtime < 120 || newstarttime < 120)
 		log "INVALID PLUGIN STATE: Awards plugin attempted to set a timer for less than 2 minutes, possible even negative."
@@ -121,6 +127,8 @@
 		log "Newstarttime:", newstarttime
 		log "Will NOT start a new round."
 		return
+
+	log "Starting a new round. Plugintime, newremindtime, newstarttime:", Plugin.time(), newremindtime, newstarttime

 	Timer.set newclosetime, 'close'
 	Timer.set newremindtime, 'votereminder'


grammar

grammar

--- programs/about-me/src/html-ui.coffee
+++ programs/about-me/src/html-ui.coffee
@@ -15,7 +15,7 @@
       about education. Given this, it shouldn't be a
       surprise if I tell you that I strongly appreciate the
       Khan Academy, and I have the utmost respect and
-      admiration for what Salman and his team.
+      admiration for Salman and his team.
     </p>
     <p>
       I work as a software developer. I am very fond of


Final fix to practices, you need Math.floor

--- js/script.coffee
+++ js/script.coffee
@@ -40,7 +40,7 @@
 	for p in r
 		practice = new Date(p.date)
 		dayNumber = new Date(p.date).getDay()
-		diff = Math.round (today-practice)/ milliseconds
+		diff = Math.floor (today-practice)/ milliseconds
 		if diff < 16 then boxes[diff] = 1
 	for i in [14..0] by -1
 		document.querySelector('.practices').innerHTML += if boxes[i] then divOn else divOff
@@ -62,4 +62,4 @@
   req.send()
   return

-loadJSON 'https://ashtanga.github.io/practice.json?date=' + new Date().getTime()
+loadJSON '{{ site.practices_url }}?date=' + new Date().getTime()


Don't remove comment button on blur (that's weird!)

--- core/app/backbone/views/sub_comments/sub_comments_add_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comments_add_view.coffee
@@ -22,7 +22,6 @@
     @toggleForm false

   inputFocus: -> @toggleForm true
-  inputBlur: -> @toggleForm false if @text().length <= 0

   toggleForm: (active) ->
     @$el.toggleClass 'evidence-sub-comments-form-active', active
@@ -54,7 +53,6 @@
       placeholder: 'Comment..'

     @bindTo textAreaView, 'focus', @inputFocus, @
-    @bindTo textAreaView, 'blur', @inputBlur, @
     textAreaView

   enableSubmit: ->


fix redactor-imagemanager id vs file_id

fix redactor-imagemanager id vs file_id

--- app/assets/javascripts/folio/console/redactor-imagemanager.coffee
+++ app/assets/javascripts/folio/console/redactor-imagemanager.coffee
@@ -23,4 +23,5 @@
     folioConsoleInitReact($box[0])

     window.folioConsoleInsertImage = (image) =>
-      @image.insert(image)
+      json = Object.assign {}, image, id: image.file_id
+      @image.insert(json)


operations: 'unsubscribe-node' notifies unsubscribed user

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -1001,6 +1001,8 @@
         t.getNodeListeners @req.node, (err, listeners) =>
             if err
                 return cb err
+            if @req.extraListeners?
+                listeners = listeners.concat @req.extraListeners
             for listener in listeners
                 notification = Object.create(@req)
                 notification.listener = listener
@@ -1076,6 +1078,10 @@
             notifications = []
             if (notification = op.notification?())
                 for own node, notifications of groupByNode(notification)
+                    # Notify unsubscribed users too:
+                    if opName is 'unsubscribe-node'
+                        notification.extraListeners = getNodeUser(node)
+
                     notification.node = node
                     new Notify(router, notification).run (err) ->
                         if err


rename App to SimonAsks

rename App to SimonAsks

--- app/assets/javascripts/simon_asks/application/questions.js.coffee
+++ app/assets/javascripts/simon_asks/application/questions.js.coffee
@@ -1,6 +1,6 @@
-window.App ||= {}
+window.SimonAsks ||= {}

-App.prepareCommentsLinks = (context) ->
+SimonAsks.prepareCommentsLinks = (context) ->
   context = ((if context is `undefined` then '' else context+' '))

   $(context + 'a[data-comment-show=toggle]').click (e) ->
@@ -11,4 +11,4 @@

 $ ->
-  App.prepareCommentsLinks()+  SimonAsks.prepareCommentsLinks()


move down one post when opening with keyboard

--- src/main/coffee/main.coffee
+++ src/main/coffee/main.coffee
@@ -284,6 +284,22 @@
            window.open article.article.link, "_blank"
            if this.authedUser && article? && article.unread
                this.togglePostAsRead article
+
+           this.currentArticle = this.currentArticle + 1
+
+           if not this.currentArticle || this.currentArticle < 0
+               this.currentArticle = 0
+           else if this.currentArticle == this.articleList.length - 1
+               this.fetchMorePosts()
+           else if this.currentArticle > this.articleList.length - 1
+               this.currentArticle = this.articleList.length - 1
+
+           newArticle = this.articleList.at this.currentArticle
+           newArticleId = newArticle.article.id;
+           newArticleOffset = $("a[name='article" + newArticleId + "']").offset()
+           $('html, body').animate({
+                scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - $("#ad-block").height() - $(".jumbotron").height()
+           }, 500)

     _initializeKeyboardNavigation: ->
         # Set up keyboard navigation


adding a helper test for href

adding a helper test for href

--- src/tests/helpers/html_test.coffee
+++ src/tests/helpers/html_test.coffee
@@ -4,7 +4,7 @@
 require('../../lib/helpers/helpers-html').register Handlebars, {}

-# TODO:
+# TODO:
 # encodeURI
 # decodeURI
 # link
@@ -12,6 +12,12 @@
 # icon
 # nl2br

+describe 'href', ->
+    describe "{{href 'https://github.com/assemble/helper-lib' 'Awesome helpers'}}", ->
+        it 'should create an href tag.', ->
+            source = "{{href 'https://github.com/assemble/helper-lib' 'Awesome helpers'}}"
+            template = Handlebars.compile(source);
+            template({}).should.equal 'Awesome helpers'

 describe 'ul', ->
     describe '{{#ul list class="list"}} \n


untangle and hopefully clarify BoardAreaDom

--- app/coffee/game.coffee
+++ app/coffee/game.coffee
@@ -71,19 +71,6 @@

 exports.initialize = (sgfObject = null, newStoneColor = null) ->
   sgfObject or= smartgame.parse(getInputSgf() or '(;SZ[19])')
-  size = parseInt(sgfObject.gameTrees[0].nodes[0].SZ, 10) or 19
-
-  # can't put the response form under React control since it needs to be
-  # generated by WTForms, server-side
-  {boardState} = stateFromSgfObject sgfObject, scoring: true
-  scoredSgfObject = sgfObjectWithDeadStones sgfObject, boardState
-  setResponseSgf smartgame.generate(scoredSgfObject)
-  if tesuji_charm.onTurn and isPassedTwice(sgfObject)
-    # we're marking dead stones, so confirming the board as it stands
-    # is possible
-    $('.confirm_button').prop('disabled', false)
-  else
-    $('.confirm_button').prop('disabled', true)

   $('.pass_button').click ->
     newSgfObject = sgfObjectWithMoveAdded sgfObject
@@ -110,114 +97,141 @@
     proposedMove: null
     viewingMove: null

+  componentDidMount: -> @updateNonReactElements()
+  componentDidUpdate: -> @updateNonReactElements()
+
+  render: ->
+    {div} = React.DOM
+    div {},
+        [(React.createElement BoardDom,
+                              key: "BoardDom"
+                              boardState: @getBoardState()
+                              hoverColor: @getHoverColor()
+                              clickCallback: @getOnBoardClick()
+                              lastPlayed: @getLastPlayed()
+                              size: @getSize()),
+         (React.createElement NavigationDom,
+                              key: "NavigationDom"
+                              changeCallback: @onNavigate,
+                              sgfObject: @props.sgfObject,
+                              viewingMove: @state.viewingMove),
+         (React.createElement ScoreDom,
+                              key: "ScoreDom"
+                              prisoners: @getPrisoners()
+                              scores: @getScores())]
+
+  ## end of React-specific methods, beginning of custom ones
+
+  getHoverColor: ->
+    if @getMode().play
+      return @getNextColor()
+    else
+      return null
+
+  getMode: ->
+    # return a set of true/false flags indicating different modes we
+    # may operate in
+    isViewingLatestMove = @state.viewingMove == null or
+                          @state.viewingMove == actionCount(@props.sgfObject)
+    isOnTurn = tesuji_charm.onTurn and isViewingLatestMove
+    showScoring = isPassedTwice @getSgfWithNewMove(), moves: @getMoveToShow()
+    scoring = isOnTurn and showScoring
+    play = isOnTurn and not showScoring
+    return {
+      play: play  # ready to accept a new stone
+      scoring: scoring  # ready to accept dead stone proposals
+      showScoring: showScoring  # show territories and dead stones
+    }
+
+  # unlike @state.viewingMove, this takes into account the proposed new stone
+  getMoveToShow: ->
+    if @state.viewingMove == null \
+       then null \
+       else if @state.proposedMove \
+            then @state.viewingMove + 1 \
+            else @state.viewingMove
+
+  getNextColor: -> nextPlayerInSgfObject @props.sgfObject
+
+  getOnBoardClick: ->
+    if @getMode().scoring
+      return @onBoardClickScoring
+    else if @getMode().play
+      return @onBoardClickPlay
+    else  # not allowing user to make moves/mark dead right now
+      return ->
+
+  getSgfWithDeadStones: ->
+    sgfObjectWithDeadStones @props.sgfObject, @getBoardState()
+
+  getSgfWithNewMove: ->
+    if @state.proposedMove?
+      return sgfObjectWithMoveAdded(@props.sgfObject,
+                                    @state.proposedMove.color,
+                                    @state.proposedMove.x,
+                                    @state.proposedMove.y)
+    else
+      return @props.sgfObject
+
+  getSize: -> parseInt(@props.sgfObject.gameTrees[0].nodes[0].SZ, 10) or 19
+
+  getStateObject: ->
+    stateFromSgfObject @getSgfWithNewMove(),
+                       moves: @getMoveToShow(),
+                       scoring: @getMode().showScoring,
+                       deadStones: if @getMode().scoring \
+                                   then @state.deadStones \
+                                   else null
+  getBoardState: -> @getStateObject().boardState
+  getLastPlayed: -> @getStateObject().lastPlayed
+  getPrisoners: -> @getStateObject().prisoners
+  getScores: -> @getStateObject().scores
+
+  # on-board-click callback when ready to play a new stone
+  onBoardClickPlay: (xy) ->
+    return unless isLegalOnSgf @props.sgfObject, @getNextColor(), xy.x, xy.y
+    @setState proposedMove:
+      color: @getNextColor()
+      x: xy.x
+      y: xy.y
+    return
+
+  # on-board-click callback when ready to mark dead stones
+  onBoardClickScoring: (xy) ->
+    newDead = getNewDeadForToggledPoint @getBoardState(), xy.x, xy.y
+    @setState deadStones: newDead
+    return
+
   onNavigate: (newViewingMove) ->
-    setResponseSgf @props.initialSgf
     @setState
       deadStones: null
       proposedMove: null
       viewingMove: newViewingMove
-    # update non-React controlled button
-    isOnTurn = tesuji_charm.onTurn and
-               newViewingMove == actionCount(@props.sgfObject)
-    # show proposed scores in board view?
-    showScoring = isPassedTwice @props.sgfObject, moves: newViewingMove
-    # allow user to change stone markings?
-    scoringMode = isOnTurn and showScoring
-    if scoringMode
-      $('.confirm_button').prop('disabled', false)
+
+  updateNonReactElements: ->
+    @updateConfirmButton()
+    @updateResponseForm()
+    return
+
+  updateConfirmButton: ->
+    # enable/disable confirm button depending on current state
+    if @getMode().scoring
+      enabled = true
+    else if @getMode().play and @state.proposedMove != null
+      enabled = true
     else
-      # even if we're ready to play a stone, we just deleted the
-      # proposed stone so we definitely can't confirm the move right
-      # now
-      $('.confirm_button').prop('disabled', true)
-
-  render: ->
-    origSgfObject = @props.sgfObject
-    if @state.proposedMove?
-      sgfObject = sgfObjectWithMoveAdded(origSgfObject,
-                                         @state.proposedMove.color,
-                                         @state.proposedMove.x,
-                                         @state.proposedMove.y)
+      enabled = false
+    $('.confirm_button').prop 'disabled', not enabled
+    return
+
+  updateResponseForm: ->
+    if @getMode().play
+      setResponseSgf smartgame.generate(@getSgfWithNewMove())
+    else if @getMode().scoring
+      setResponseSgf smartgame.generate(@getSgfWithDeadStones())
     else
-      sgfObject = origSgfObject
-
-    moveToShow = if @state.viewingMove == null \
-                 then null \
-                 else if @state.proposedMove \
-                      then @state.viewingMove + 1 \
-                      else @state.viewingMove
-    isViewingLatestMove = @state.viewingMove == null or
-                          @state.viewingMove == actionCount(origSgfObject)
-    isOnTurn = tesuji_charm.onTurn and isViewingLatestMove
-    # show proposed scores in board view?
-    showScoring = isPassedTwice sgfObject, moves: moveToShow
-    # allow user to change stone markings?
-    scoringMode = isOnTurn and showScoring
-    # allow user to place a proposed new stone?
-    playMode = isOnTurn and not showScoring
-
-    objState = stateFromSgfObject sgfObject,
-                                  moves: moveToShow,
-                                  scoring: showScoring,
-                                  deadStones: if scoringMode \
-                                              then @state.deadStones \
-                                              else null
-    {boardState, lastPlayed, prisoners, scores} = objState
-    size = parseInt(sgfObject.gameTrees[0].nodes[0].SZ, 10) or 19
-    nextColor = null
-
-    if scoringMode
-      onBoardClick = (xy) =>
-        newDead = getNewDeadForToggledPoint boardState, xy.x, xy.y
-        @setState deadStones: newDead
-        # can't put the response form under React control since it needs to be
-        # generated by WTForms, server-side
-        {boardState: newState} = stateFromSgfObject origSgfObject,
-                                                    scoring: true
-                                                    deadStones: newDead
-        newSgfObject = sgfObjectWithDeadStones origSgfObject, newState
-        setResponseSgf smartgame.generate(newSgfObject)
-        return
-
-    else if playMode
-      # ready to accept a normal (non-dead stone marking) move
-      nextColor = nextPlayerInSgfObject origSgfObject
-      onBoardClick = (xy) =>
-        return unless isLegalOnSgf origSgfObject, nextColor, xy.x, xy.y
-        # can't put the response form under React control since it needs to be
-        # generated by WTForms, server-side
-        $('.confirm_button').prop('disabled', false)
-        newSgfObject = sgfObjectWithMoveAdded origSgfObject,
-                                              nextColor, xy.x, xy.y
-        setResponseSgf smartgame.generate(newSgfObject)
-        # back to React
-        @setState proposedMove:
-          color: nextColor
-          x: xy.x
-          y: xy.y
-        return
-
-    else  # not allowing user to make moves/mark dead right now
-      onBoardClick = ->
-
-    {div} = React.DOM
-    div {},
-        [(React.createElement BoardDom,
-                              key: "BoardDom"
-                              boardState: boardState
-                              hoverColor: nextColor
-                              clickCallback: onBoardClick
-                              lastPlayed: lastPlayed
-                              size: size),
-         (React.createElement NavigationDom,
-                              key: "NavigationDom"
-                              changeCallback: @onNavigate,
-                              sgfObject: origSgfObject,
-                              viewingMove: @state.viewingMove),
-         (React.createElement ScoreDom,
-                              key: "ScoreDom"
-                              prisoners: prisoners
-                              scores: scores)]
+      setResponseSgf @props.initialSgf
+    return

 BoardDom = React.createClass


Rename 'childAttr' to 'childName' in NodeWrapper.

--- src/NodeWrapper.coffee
+++ src/NodeWrapper.coffee
@@ -7,12 +7,12 @@
 # Properties:
 # * `node` - The original coffee-script node.
 # * `parent` - A `NodeWrapper` object for the parent of the coffee-script node.
-# * `childAttr` - A coffee-script node has multiple named children.  This is the name of the
-#   attribute which contains this node in `@parent.node`.  Note that `@parent.node[childAttr]`
+# * `childName` - A coffee-script node has multiple named children.  This is the name of the
+#   attribute which contains this node in `@parent.node`.  Note that `@parent.node[childName]`
 #   may be a single Node or it may be an array of nodes, depending on the implementation of the
 #   specific node type.
-# * `childIndex` - Where `@parent.node[childAttr]` is an array, this is the index of `@node`
-#   in `@parent.node[childAttr]`.  Note that inserting new nodes will obviously invalidate this
+# * `childIndex` - Where `@parent.node[childName]` is an array, this is the index of `@node`
+#   in `@parent.node[childName]`.  Note that inserting new nodes will obviously invalidate this
 #   value, so this is more of a "hint" than a hard and fast truism.
 # * `depth` - The depth in the AST from the root node.
 # * `type` - Copy of @node.constructor.name.
@@ -20,7 +20,7 @@
 # * `isStatement` - true if this node is a statement.
 #
 module.exports = class NodeWrapper
-    constructor: (@node, @parent, @childAttr, @childIndex, @depth=0) ->
+    constructor: (@node, @parent, @childName, @childIndex, @depth=0) ->
         assert @node
         @locationData = @node.locationData
         @type = @node.constructor?.name or null
@@ -28,31 +28,46 @@
         # TODO: Is this too naive?  coffee-script nodes have a `isStatement(o)` function, which
         # really only cares about `o.level`.  Should we be working out the level and calling
         # this function?
-        @isStatement = @parent? and @parent.type is 'Block' and @childAttr is 'expressions' and @type isnt 'Comment'
+        @isStatement = @parent? and @parent.type is 'Block' and @childName is 'expressions' and @type isnt 'Comment'

     # Run `fn(node)` for each child of this node.  Child nodes will be automatically wrapped in a
     # `NodeWrapper`.
     #
     forEachChild: (fn) ->
         if @node.children?
-            @node.children.forEach (attr) => @forEachChildOfType attr, fn
+            @node.children.forEach (childName) => @forEachChildOfType childName, fn

     # Like `forEachChild`, but only
-    forEachChildOfType: (attr, fn) ->
-        childAttr = @node[attr]
-        if childAttr?
-            attrs = _.flatten [childAttr], true
+    forEachChildOfType: (childName, fn) ->
+        children = @node[childName]
+        if children?
+            childNodes = _.flatten [children], true

             index = 0
-            while index < attrs.length
-                child = attrs[index]
+            while index < childNodes.length
+                child = childNodes[index]
                 if child.constructor.name?
-                    fn new NodeWrapper(child, this, attr, index, @depth + 1)
+                    fn new NodeWrapper(child, this, childName, index, @depth + 1)

                 # Bump index up in case we inserted nodes
                 # TODO: Guard against incrementing forever?
-                index++ while (child != attrs[index])
+                index++ while (child != childNodes[index])
                 index++
+
+    # Returns a NodeWrapper for the given child.  This only works if the child is not an array
+    # (e.g. `Block.expressions`)
+    child: (name, index=null) ->
+        child = @node[name]
+        if !child then return null
+
+        if !index?
+            assert !_.isArray child
+            return new NodeWrapper child, this, name, 0, @depth + 1
+        else
+            assert _.isArray child
+            if !child[index] then return null
+            return new NodeWrapper child[index], this, name, index, @depth + 1
+

     # Insert a new node before this node (only works if this node is in an array-based attribute,
     # like `Block.expressions`.)
@@ -61,17 +76,17 @@
     # and will be skipped when instrumenting code.
     #
     insertBefore: (csSource) ->
-        assert _.isArray @parent.node[@childAttr]
+        assert _.isArray @parent.node[@childName]

         compiled = compile csSource, @node

         # childIndex is more of a hint, since nodes can move around.
-        if @parent.node[@childAttr][@childIndex] isnt @node
-            childIndex = _.indexOf @parent.node[@childAttr], @node
+        if @parent.node[@childName][@childIndex] isnt @node
+            childIndex = _.indexOf @parent.node[@childName], @node
             if childIndex is -1 then throw new Error "Can't find node in parent"
             @childIndex = childIndex

-        @parent.node[@childAttr].splice(@childIndex, 0, compiled)
+        @parent.node[@childName].splice(@childIndex, 0, compiled)

     # Insert a chunk of code at the start of a child of this node.  E.g. if this is a Block,
     # then `insertAtStart('expressions', 'console.log "foo"'')` would add a `console.log`
@@ -80,28 +95,28 @@
     # Note that generated nodes will have the `node.coffeeCoverage.generated` flag set,
     # and will be skipped when instrumenting code.
     #
-    insertAtStart: (attr, csSource) ->
-        child = @node[attr]
+    insertAtStart: (childName, csSource) ->
+        child = @node[childName]

-        if @type is 'Block' and attr is 'expressions'
+        if @type is 'Block' and childName is 'expressions'
             if !child
-                @node[attr] = [compile(csSource, @node)]
+                @node[childName] = [compile(csSource, @node)]
             else
-                @node[attr].unshift compile(csSource, @node)
+                @node[childName].unshift compile(csSource, @node)

         else if child?.constructor?.name is 'Block'
             child.expressions.unshift compile(csSource, child)

         else if !child
             # This will generate a 'Block'
-            @node[attr] = compile(csSource, @node)
+            @node[childName] = compile(csSource, @node)

         else
-            throw new Error "Don't know how to insert statement into #{@type}.#{attr}: #{@type[attr]}"
+            throw new Error "Don't know how to insert statement into #{@type}.#{childName}: #{@type[childName]}"

     toString: ->
         answer = ''
-        if @childAttr then answer += "#{@childAttr}[#{@childIndex}]:"
+        if @childName then answer += "#{@childName}[#{@childIndex}]:"
         answer += @type
         if @node.locationData? then answer += " (#{@node.locationData?.first_line + 1}:#{@node.locationData.first_column + 1})"


Refactor question switching code

Refactor question switching code

--- frontend/coffeescript/app/webform/views.coffee
+++ frontend/coffeescript/app/webform/views.coffee
@@ -49,8 +49,14 @@
             @current_language = @set_current_language()

             @repopulateForm()
-            @_display_form_buttons(0)
+            @show_first_question()
             @
+
+        show_first_question: ->
+          first_question = document.flat_fields[0]
+          @toggle_question(first_question, false)
+          @_display_form_buttons(0)
+

         change_language: (language) ->
           @currentLanguage = language
@@ -412,39 +418,69 @@

         switch_question: ( next_index, advancing ) ->
           #TODO: if in group, test relevance/constraint for all children
-
-          next_index = 0 if next_index < 0
-
-          # Does the current active question pass our constraints?
+          current_question = document.flat_fields[@currentQuestionIndex]
+          next_question = document.flat_fields[next_index]
+
+          # Case 1: moving backwards through the form to the first question
+          # ---------------------------------------------------------------
+          #
+          # Show the first question, and hide the 'Previous' button.
+          if next_index is 0
+            @hide_question current_question
+            @show_question next_question
+            @currentQuestionIndex = 0
+            @update_progress_bar(0)
+            @_display_form_buttons(0)
+            return
+
+          # Case 2: finished the last question in the form
+          # ----------------------------------------------
+          #
+          # Show the submit button.
+          if next_index >= @numberOfQuestions
+            return unless @passes_question_constraints()
+            @currentQuestionIndex = @numberOfQuestions
+            @update_progress_bar(next_index)
+            @_display_form_buttons(next_index)
+            return
+
+          # Case 3: general case, navigating forwards or backwards
+          # ------------------------------------------------------
+
+          # If the current question is a group, we must advance the index to
+          # the end of the group to reach the next field.
           if advancing
+            # Does the current question pass our constraints?
             return unless @passes_question_constraints()

-          current_question = document.flat_fields[@currentQuestionIndex]
-          return if not current_question
-
-          if current_question.type is 'group' and next_index isnt -1
-            #TODO: check if group is field-list or not first
-            if advancing
+            if current_question.type is 'group'
               next_index = next_index + current_question.children.length
-
-          next_question = document.flat_fields[next_index]
-          if not advancing
-            #is current question within group
+              next_question = document.flat_fields[next_index]
+
+              # If the last question is part of a group, then just show the submit button.
+              if next_index >= @numberOfQuestions
+                @update_progress_bar(next_index)
+                @_display_form_buttons(next_index)
+                return
+
+
+          # If going backwards, check if the question is in a group. If the
+          # next question is in a group, i.e. at the end of the group, then we
+          # need to move the index back to the beginning of the group.
+          else
             group = @get_group_for_question(next_question)
             if group
               next_index = next_index - group.children.length
               next_question = document.flat_fields[next_index]

-          if not next_question
-            @update_progress_bar(next_index)
-            @_display_form_buttons(next_index)
-            return
-
-          #Is this question relevant?  Or, is this question an equation?
-          if (next_question.bind and next_question.bind.calculate != null) or not
-            XFormConstraintChecker.isRelevant( next_question, @queryStringToJSON($( ".form" ).serialize()) )
+          # Now that we've found the next question, we need to determine if
+          # it's relevant; it could be irrelevant based on preceding values, or
+          # a calculation.
+          if (next_question.bind?.calculate?) or not
+            XFormConstraintChecker.isRelevant( next_question, @queryStringToJSON( $( ".form" ).serialize()) )
+
               # If its a calculation, calculate it!
-              $("#" + next_question.name).val _performCalcluate(next_question.bind.calculate)  if next_question.bind and next_question.bind.calculate
+              $("#" + next_question.name).val _performCalcluate(next_question.bind.calculate)  if next_question.bind?.calculate?

               # Switch to the next question!
               if advancing
@@ -455,15 +491,12 @@
               @switch_question( next_index, advancing )
               return

-          # Hide the current question, show the next question
+          # We've found the next question, so hide the current question, show
+          # the next one, and update the controls.
           @hide_question current_question
           @show_question next_question
-
-          # Update progress bar
+          @currentQuestionIndex = next_index
           @update_progress_bar(next_index)
-
-
-          @currentQuestionIndex = next_index
           @_display_form_buttons(@currentQuestionIndex)

           #Start the Geopoint display if geopoint
@@ -475,47 +508,12 @@
           new_width_percentage = ((next_index / @numberOfQuestions) * 100).toString()
           @$('.progress-bar').width("#{new_width_percentage}%")

-        next_question: () ->
-
-            #currentQuestion = document.repo.children[@currentQuestionIndex]
-
-            # Set up for field lists and grid lists
-            # if question.info.control and question.info.control.appearance
-            #     current_tree = question.info.tree
-            #     question_index += 1
-            #     question_index += 1  while @input_fields[question_index].tree is current_tree
-
-            # Attempt to switch to the next question
-            #if question_index < @input_fields.length
-            #    question_index += 1
-
-            @switch_question( @currentQuestionIndex + 1, true )
-
-            @
+        next_question: ->
+          @switch_question( @currentQuestionIndex + 1, true )
+          @

         prev_question: () ->
-
-            # question = @_active_question()
-
-            # if question_index <= 0
-            #     return @
-
-            # current_tree = @input_fields[question_index - 1].tree
-
-            # # If we are in a group, check if we are in a field/grid list group
-            # unless current_tree is "/"
-            #   temp_idx = question_index - 1
-            #   temp_idx -= 1  while temp_idx >= 0 and @input_fields[temp_idx].tree is current_tree
-            #   temp_idx += 1
-            #   if @input_fields[temp_idx].control and @input_fields[temp_idx].control.appearance
-            #     question_index = temp_idx
-            #   else
-            #     question_index -= 1
-            # else
-            #   question_index -= 1
-
             @switch_question( @currentQuestionIndex - 1, false )
-
             @

     return xFormView


Add error listener to avoid crash on listen when EACCESS occurs

--- src/ReplrServer.coffee
+++ src/ReplrServer.coffee
@@ -52,10 +52,16 @@
         return

       @socketServer = net.createServer @open.bind(@)
+
       @socketServer.on 'listening', ()=>
         @started = true
         @starting = false
         @emit 'listening'
+
+      onError = (err)=>
+        callback err if callback
+
+      @socketServer.once 'error', onError

       try
         @socketServer.listen @options.port
@@ -63,6 +69,8 @@
         @started = false
         @starting = false
         callback err if callback
+
+      @socketServer.removeListener 'error', onError

     if typeof @options.port == 'number'
       portscanner.checkPortStatus @options.port, '127.0.0.1', onVerified


Use splice instead of a weird CoffeeScript list syntax.

--- src/server.coffee
+++ src/server.coffee
@@ -35,7 +35,7 @@
       c.on 'close', =>
         # Remove the connection on close.
         i = @connections.indexOf(c)
-        @connections[i..i] = []
+        @connections.splice(i, 1)

   reloadBrowser: (path) ->
     if path


Issue #112: Refactored server.stop method. Added stopping of tcp servers

--- src/server.coffee
+++ src/server.coffee
@@ -159,36 +159,28 @@
 exports.stop = stop = (done) ->
 	promises = []

-	if httpServer
-		deferredHttp = Q.defer()
-		promises.push deferredHttp.promise
+	stopServer = (server, serverType) ->
+		deferred = Q.defer()

-		httpServer.close ->
-			logger.info "Stopped HTTP server"
-			deferredHttp.resolve()
+		server.close ->
+			logger.info "Stopped #{serverType} server"
+			deferred.resolve()

-	if httpsServer
-		deferredHttps = Q.defer()
-		promises.push deferredHttps.promise
+		return deferred.promise

-		httpsServer.close ->
-			logger.info "Stopped HTTPS server"
-			deferredHttps.resolve()
-
-	if apiHttpServer
-		deferredAPIHttp = Q.defer()
-		promises.push deferredAPIHttp.promise
-
-		apiHttpServer.close ->
-			logger.info "Stopped API server"
-			deferredAPIHttp.resolve()
-
+	promises.push stopServer(httpServer, 'HTTP') if httpServer
+	promises.push stopServer(httpsServer, 'HTTPS') if httpsServer
+	promises.push stopServer(apiHttpServer, 'API HTTP') if apiHttpServer
+	promises.push stopServer(tcpServer, 'TCP Socket') if tcpServer
+	promises.push stopServer(tcpHttpReceiver, 'TCP HTTP Receiver') if tcpHttpReceiver
 	promises.push stopAgenda().promise if agenda

 	(Q.all promises).then ->
 		httpServer = null
 		httpsServer = null
 		apiHttpServer = null
+		tcpServer = null
+		tcpHttpReceiver = null
 		agenda = null
 		done()


movin stuff around. oooooh yeaaaah

movin stuff around. oooooh yeaaaah

--- channel_to_JSON_schema.coffee
+++ channel_to_JSON_schema.coffee
@@ -1,20 +1,4 @@
 #!/usr/bin/env coffee
-
-#{
-#    "type": "object",
-#    "properties": {
-#      "on": {
-#        "type": "boolean",
-#        "required": true,
-#        "default": false
-#      },
-#      "color": {
-#        "type": "string",
-#        "required": true
-#      }
-#    }
-#  }
-

 fs = require 'fs'
 _  = require 'lodash'
@@ -22,14 +6,88 @@
 debug = require('debug')('channel-to-json-schema')

 class ParseChannelSchemaToJSONSchema
-  constructor: () ->
+  constructor: ->
     @channel_infile = commander.infile
-    @channel_outfile = commander.outfile
-    @channel_form_outfile = commander.form
+    @messageSchemaFile = commander.outfile
+    @formFile = commander.form
+    @channelConfigFile = commander.channelConfig

   channel: =>
     JSON.parse fs.readFileSync @channel_infile

+  run2: =>
+    channel = @channel()
+    messageSchema = @getMessageSchema channel.application.resources
+    form = @getForm channel.application.resources
+    channelConfig = @getChannelConfig channel
+
+    @writeMessageSchema messageSchema
+    @writeForm form
+    @writeChannelConfig channelConfig
+
+  writeMessageSchema: (messageSchema) =>
+    prettyMessageSchema = JSON.stringify(messageSchema, null, 2)
+    fs.writeFileSync @messageSchemaFile, prettyMessageSchema
+    debug 'message schema:', prettyMessageSchema
+
+  writeForm: (form) =>
+    prettyForm = JSON.stringify(form, null, 2)
+    fs.writeFileSync @formFile, prettyForm
+    debug 'form:', prettyForm
+
+  writeChannelConfig: (channelConfig) =>
+    prettyChannelConfig = JSON.stringify(channelConfig, null, 2)
+    fs.writeFileSync @channelConfigFile, prettyChannelConfig
+    debug 'form:', prettyChannelConfig
+
+  getMessageSchema : (resources)=>
+    actions = _.pluck resources, 'action'
+    messageSchema =
+      type: 'object'
+      properties:
+          action:
+            type: "string"
+            enum : actions
+
+    _.each actions, (action) =>
+      actionProperties = @getActionProperties resources, action
+      messageSchema.properties[action] =
+        type: "object"
+        properties: actionProperties
+
+    messageSchema
+
+  getForm: (resources) =>
+    form = _.map resources, @getFormFromResource
+    actionForm =
+      key: 'action'
+      title: 'Action'
+      titleMap: @getActionTitleMap resources
+
+    form.unshift actionForm
+
+    form
+
+  getFormFromResource: (resource) =>
+    key: "#{resource.action}"
+    notitle: true
+    condition: "model.action === '#{resource.action}'"
+
+
+  getActionTitleMap: (resources) =>
+    _.map resources, (resource) =>
+      value: resource.action, name: resource.displayName
+
+  getActionProperties: (resources, action) =>
+    resource = _.findWhere resources, action: action
+    properties = {}
+    _.each resource.params, (param) =>
+      properties["#{@sanitizeParam param.name}"] = @convertParam param
+
+    properties
+
+  getChannelConfig: =>
+    "I'm empty"
   run: =>
     newForm = []
     newChannel =
@@ -105,12 +163,12 @@

   writeOutput: (channel) =>
     prettyChannel = JSON.stringify channel, null, 2
-    fs.writeFileSync @channel_outfile, prettyChannel
+    fs.writeFileSync @messageSchemaFile, prettyChannel
     debug 'channel output:', prettyChannel

   writeForm: (form) =>
     prettyForm = JSON.stringify form, null, 2
-    fs.writeFileSync @channel_form_outfile, prettyForm
+    fs.writeFileSync @formFile, prettyForm
     debug 'form output:', prettyForm

 commander
@@ -118,10 +176,11 @@
   .option('-i, --infile [path]',  'Path to the channel file to input')
   .option('-o, --outfile [path]',  'Path to the channel file to output')
   .option('-f, --form [path]',  'Path to the schema form file to output')
+  .option('-c, --channel-config [path]',  'Path to the channel-config file to output')
   .parse(process.argv);

 commander.help() unless commander.infile?

 converter = new ParseChannelSchemaToJSONSchema channel_infile: commander.infile?,
-  channel_outfile: commander.outfile?
-converter.run()
+  messageSchemaFile: commander.outfile?
+converter.run2()


don't render page until we've parsed the customizations object

--- @client/app.coffee
+++ @client/app.coffee
@@ -209,6 +209,8 @@

     access_granted = @accessGranted()

+    return LOADING_INDICATOR if !fetch('customizations_signature').signature
+
     DIV
       className: 'full_height'
       style:


IE 11 fast key gen, and fix bugs

IE 11 fast key gen, and fix bugs

--- js/src/crypto/RsaKeyGenerator.coffee
+++ js/src/crypto/RsaKeyGenerator.coffee
@@ -20,9 +20,8 @@
     generate: (params) ->
       if window.crypto?.subtle?
         return @webCryptoGenerate(params)
-      # else if
-        # ie 11 web crypto
-      # Forge crypto
+      else if window.msCrypto?.subtle?
+        return @ieWebCryptoGenerate(params)
       else
         return @forgeGenerate(params)

@@ -44,7 +43,7 @@
         window.crypto.subtle.generateKey( {
           name: 'RSA-OAEP'
           modulusLength: params.bits
-          publicExponent: @numToUnsignedUint8Array(params.e)
+          publicExponent: new Uint8Array([1,0,1]) # constants
           hash: { name: 'SHA-256' }
         }, true, ['encrypt', 'decrypt'])
       .then (keys) ->
@@ -61,7 +60,46 @@
           return keyPair
         )

-    # Forge
+    # IE 11 Web Crypto
+    ieWebCryptoGenerate: (params) ->
+      Promise.resolve()
+      .then =>
+        deferred = Promise.defer()
+        keyOperation = window.msCrypto.subtle.generateKey( {
+          name: 'RSA-OAEP'
+          modulusLength: params.bits
+          publicExponent: new Uint8Array([1,0,1])
+          hash: { name: 'SHA-256' }
+        }, true, ['encrypt', 'decrypt'])
+        keyOperation.onerror = =>
+          console.log('sad failure')
+
+        keyOperation.oncomplete = =>
+          keyPair = keyOperation.result
+          console.log(keyPair)
+          return deferred.resolve(keyPair)
+
+        return deferred.promise
+
+      .then (keys) ->
+        deferred1 = Promise.defer()
+        keyOpPrivate = window.msCrypto.subtle.exportKey('pkcs8', keys.privateKey)
+        keyOpPrivate.oncomplete = =>
+          return deferred1.resolve(Forge.util.createBuffer(keyOpPrivate.result))
+        privateKeyPromise = deferred1.promise
+
+        deferred2 = Promise.defer()
+        keyOpPublic = window.msCrypto.subtle.exportKey('spki', keys.publicKey)
+        keyOpPublic.oncomplete = =>
+          return deferred2.resolve(Forge.util.createBuffer(keyOpPublic.result))
+        publicKeyPromise = deferred2.promise
+        Promise.join(privateKeyPromise, publicKeyPromise, (privateKey, publicKey) ->
+          keyPair            = {}
+          keyPair.privateKey = privateKey
+          keyPair.publicKey  = publicKey
+          return keyPair
+        )
+

     # Generate public and private RSA keys
@@ -83,8 +121,8 @@
       uLength     = Math.ceil(binary.length/8)
       unsignedInt = new Uint8Array(uLength)
       for i in [0..uLength]
-        uint8          = binary.slice(i*8, i*8 + 7)
-        unsignedInt[i] = Number.parseInt(uint8, 2)
+        uint8          = binary.subarray(i*8, i*8 + 7)
+        unsignedInt[i] = parseInt(uint8, 2) # Note: this shit is broken. Should probably hard-code constants.
       return unsignedInt

   return RsaKeyGenerator


Layout: Fixed a problem with absolute layouts.

Let content-resize events pass again even in maxed layouts.

--- src/base/Layout/Layout.coffee
+++ src/base/Layout/Layout.coffee
@@ -121,13 +121,14 @@

 		@registerTemplate(@__layout)

-		if @__maximize_horizontal and @__maximize_vertical
-			Events.listen
-				type: "content-resize"
-				instance: @
-				node: @DOM
-				call: (ev) =>
-					ev.stopPropagation()
+		# if @__maximize_horizontal and @__maximize_vertical
+		# 	Events.listen
+		# 		type: "content-resize"
+		# 		instance: @
+		# 		node: @DOM
+		# 		call: (ev) =>
+		# 			console.debug "stop", ev
+		# 			ev.stopPropagation()

 		@maximizeAddClasses()


Fix until chaining

Fix until chaining

--- src/stdlib.coffee
+++ src/stdlib.coffee
@@ -221,13 +221,20 @@
   until: (delay, callback) ->
     newScope = @createUntilScope(delay)
     callback.apply(newScope)
-    # Chain untils() to run sequentially
-    {
-      until: (_delay, _callback) => @after(delay, (=> @until(_delay, _callback)))
-    }
+
+    # You can chain until() calls and they'll run sequentially
+    class Chain
+      constructor: (@reference, @delay) ->
+      until: (_delay, _callback) =>
+        @reference.after(@delay, =>
+          @reference.until(_delay, _callback)
+        )
+        new Chain(@reference, @delay + _delay)
+    new Chain(@, delay)

   #
   # Internal API:
+
   withExpiration: (id) ->
     setTimeout((=> clearInterval(id)), @expiration - TinyRave.timer.getTime())


Payment: some error checking on breadcrumb

--- client/Pricing/AppView.coffee
+++ client/Pricing/AppView.coffee
@@ -22,6 +22,8 @@

   selectItem : (name) ->

+    return  unless name
+
     @$('li').removeClass "active"
     @$("li.#{name}").addClass "active"

@@ -44,8 +46,7 @@
   pistachio : ->
     """

-
Select plan

-
Login/Register

+
Login/Register

Payment method

Overview

Group details

@@ -78,7 +79,8 @@

     workflow.off 'FormIsShown'

-    workflow.on 'FormIsShown', =>
+    workflow.on 'FormIsShown', (form)=>
+      return  unless workflow.active
       @breadcrumb.selectItem workflow.active.getOption 'name'


Terminal: vmItem: comment-out all vmOn/Off/Prepare stuff, update item to work with Machine object correctly

--- client/Terminal/vmitem.coffee
+++ client/Terminal/vmitem.coffee
@@ -1,10 +1,6 @@
 class TerminalStartTabVMItem extends KDCustomHTMLView

   JView.mixin @prototype
-
-  MESSAGE_MAP =
-    'started'                : 'Checking VM state'
-    'vm is already prepared' : 'READY'

   constructor:(options = {}, data)->

@@ -13,8 +9,7 @@

     super options, data

-    vm             = @getData()
-    {vmController} = KD.singletons
+    @machine = @getData()

     @loader = new KDLoaderView
       size          : width : 16
@@ -32,84 +27,91 @@

     @alwaysOn = new KDCustomHTMLView
       tagName  : 'cite'
-      partial  : if vm.alwaysOn then "always-on" else ""
+      partial  : if @machine.alwaysOn then "always-on" else ""
+
+    @once 'viewAppended', =>
+      @setClass 'ready'
+      @notice.updatePartial 'READY'
+      @loader.hide()

-  handleVMStart:(update)->
-    { message, currentStep, totalStep } = update
-
-    if message is 'FINISHED'
-      @setClass 'ready'
-      @emit 'vm.is.prepared'
-      @notice.updatePartial 'READY'
-      @loader.hide()
-      return
-
-    @unsetClass 'off ready'
-
-    if message is 'STARTED'
-      # @loader.canvas.setColor "#1aaf5d"
-      @loader.show()
-      @progress.setCss 'background-color', "#1aaf5d"
-      return
-    percentage = Math.round currentStep/totalStep*100
-    @progress.setWidth percentage, '%'
-    @notice.updatePartial "#{percentage}%"
+  click: -> @emit 'VMItemClicked', { @machine }

-  handleVMStop:(update)->
-    { message, currentStep, totalStep } = update
-    return @renderVMStop()  if message is 'FINISHED'
+  pistachio: ->

-    @unsetClass 'off ready'
-    # niceMessage = MESSAGE_MAP[message.toLowerCase()]
-    # @notice.updatePartial niceMessage or message
-    if message is 'STARTED'
-      # @loader.canvas.setColor "#FF7379"
-      @loader.show()
-      @progress.setCss 'background-color', "#FF7379"
-      @notice.updatePartial "100%"
-      @progress.setWidth 100, '%'
-      return
-    percentage = 100 - Math.round currentStep/totalStep*100
-    @progress.setWidth percentage, '%'
-    @notice.updatePartial "#{percentage}%"
+    name = @machine.getName()

-
-  handleVMInfo:(info)->
-    unless info
-      @unsetClass 'ready off'
-      @loader.show()
-      @notice.updatePartial 'LOADING'
-      @progress.setWidth 0, '%'
-      return
-
-    { state } = info
-    switch state.toLowerCase()
-      when "running"
-        @loader.hide()
-        @unsetClass 'off'
-        @notice.updatePartial 'READY'
-        @setClass 'ready'
-      when "stopped"
-        @unsetClass 'ready'
-        @renderVMStop()
-
-  handleVMError:(error)->
-    @renderVMStop()
-
-  renderVMStop: ->
-    @loader.hide()
-    @setClass 'off'
-    @notice.updatePartial 'OFF'
-
-  click : -> @emit 'VMItemClicked', @getData()
-
-  pistachio:->
-    vm    = @getData()
-    alias = vm.hostnameAlias
     """
-
{{> @loader}}
#{alias.replace 'koding.kd.io', 'kd.io'} {{> @alwaysOn}} {{> @notice}}
+
{{> @loader}}
#{name.replace 'koding.kd.io', 'kd.io'} {{> @alwaysOn}} {{> @notice}}
     {{> @progress}}
     """

+
+  # handleVMStart:(update)->
+  #   { message, currentStep, totalStep } = update
+
+  #   if message is 'FINISHED'
+  #     @setClass 'ready'
+  #     @emit 'vm.is.prepared'
+  #     @notice.updatePartial 'READY'
+  #     @loader.hide()
+  #     return
+
+  #   @unsetClass 'off ready'
+
+  #   if message is 'STARTED'
+  #     # @loader.canvas.setColor "#1aaf5d"
+  #     @loader.show()
+  #     @progress.setCss 'background-color', "#1aaf5d"
+  #     return
+  #   percentage = Math.round currentStep/totalStep*100
+  #   @progress.setWidth percentage, '%'
+  #   @notice.updatePartial "#{percentage}%"
+
+
+  # handleVMStop:(update)->
+  #   { message, currentStep, totalStep } = update
+  #   return @renderVMStop()  if message is 'FINISHED'
+
+  #   @unsetClass 'off ready'
+  #   # niceMessage = MESSAGE_MAP[message.toLowerCase()]
+  #   # @notice.updatePartial niceMessage or message
+  #   if message is 'STARTED'
+  #     # @loader.canvas.setColor "#FF7379"
+  #     @loader.show()
+  #     @progress.setCss 'background-color', "#FF7379"
+  #     @notice.updatePartial "100%"
+  #     @progress.setWidth 100, '%'
+  #     return
+  #   percentage = 100 - Math.round currentStep/totalStep*100
+  #   @progress.setWidth percentage, '%'
+  #   @notice.updatePartial "#{percentage}%"
+
+
+  # handleVMInfo:(info)->
+  #   unless info
+  #     @unsetClass 'ready off'
+  #     @loader.show()
+  #     @notice.updatePartial 'LOADING'
+  #     @progress.setWidth 0, '%'
+  #     return
+
+  #   { state } = info
+  #   switch state.toLowerCase()
+  #     when "running"
+  #       @loader.hide()
+  #       @unsetClass 'off'
+  #       @notice.updatePartial 'READY'
+  #       @setClass 'ready'
+  #     when "stopped"
+  #       @unsetClass 'ready'
+  #       @renderVMStop()
+
+  # handleVMError:(error)->
+  #   @renderVMStop()
+
+  # renderVMStop: ->
+  #   @loader.hide()
+  #   @setClass 'off'
+  #   @notice.updatePartial 'OFF'


fixed name spacing

fixed name spacing

--- src/middleware/stats.coffee
+++ src/middleware/stats.coffee
@@ -67,8 +67,8 @@
             orchestrationStatus = orchestration.response.status
             sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.orchestrations.' + orchestration.name, orchestratrionDuration
             sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.orchestrations.' + orchestration.name + '.StatusCodes.' + orchestrationStatus , orchestratrionDuration
-            sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.StatusCodes' + route.response.status + '.orchestrations.' + orchestration.name, orchestratrionDuration
-            sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.StatusCodes' + route.response.status + '.orchestrations.' + orchestration.name + '.StatusCodes.' + orchestrationStatus , orchestratrionDuration
+            sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.StatusCodes.' + route.response.status + '.orchestrations.' + orchestration.name, orchestratrionDuration
+            sdc.timing domain + '.Channels.' + ctx.authorisedChannel._id + '.nonPrimaryRoutes.' + route.name + '.StatusCodes.' + route.response.status + '.orchestrations.' + orchestration.name + '.StatusCodes.' + orchestrationStatus , orchestratrionDuration


Fix bug with event exceptions in Firefox

--- app/core/Tracker.coffee
+++ app/core/Tracker.coffee
@@ -130,8 +130,7 @@
     @trackEventInternal action, _.cloneDeep properties
     @trackSnowplow action, _.cloneDeep properties

-
-    unless event in ['View Load', 'Script Started', 'Script Ended', 'Heard Sprite']
+    unless action in ['View Load', 'Script Started', 'Script Ended', 'Heard Sprite']
       # Google Analytics
       # https://developers.google.com/analytics/devguides/collection/analyticsjs/events
       gaFieldObject =
@@ -142,7 +141,7 @@
       gaFieldObject.eventValue = properties.value if properties.value?
       ga? 'send', gaFieldObject
       ga? 'codeplay.send', gaFieldObject if features.codePlay
-
+
     # Inspectlet
     # http://www.inspectlet.com/docs#tagging
     __insp?.push ['tagSession', action: action, properies: properties]
@@ -167,13 +166,13 @@
     # TODO: delete properites.level for 'Saw Victory' after 2/8/15.  Should be using levelID instead.
     if event in ['Clicked Start Level', 'Inventory Play', 'Heard Sprite', 'Started Level', 'Saw Victory', 'Click Play', 'Choose Inventory', 'Homepage Loaded', 'Change Hero']
       delete properties.label
-
+
     # SnowPlow
     snowplowAction = event.toLowerCase().replace(/[^a-z0-9]+/ig, '_')
     properties.user = me.id
     delete properties.category
     #console.log "SnowPlow", snowplowAction, properties
-
+
     try
       schema = require("schemas/events/#{snowplowAction}")
     catch
@@ -186,7 +185,7 @@
         text = 'Snowplow event schema validation failed! See console'
         console.log 'Snowplow event failure info:', {snowplowAction, properties, error: result.error}
         noty {text, layout: 'center', type: 'error', killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3}
-
+
     window.snowplow 'trackUnstructEvent',
       schema: "iglu:com.codecombat/#{snowplowAction}/jsonschema/#{schema.self.version}"
       data: properties


Updated gulpfile

Updated gulpfile

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -2,7 +2,7 @@
  * Development tasks
  *
  * @author  Christopher Pappas & Matthew Fordham
- * @date    5.30.14
+ * @date    6.4.14
  *
  * Primary Tasks:
  *    gulp dev   : Development mode, file-watcher
@@ -12,8 +12,9 @@

 gulp         = require 'gulp'
 autoprefixer = require 'gulp-autoprefixer'
-bower        = require 'gulp-bower'
+bowerFiles   = require 'gulp-bower-files'
 browserify   = require 'gulp-browserify'
+coffeeify    = require 'coffeeify'
 concat       = require 'gulp-concat'
 connect      = require 'gulp-connect'
 clean        = require 'gulp-clean'
@@ -21,7 +22,6 @@
 livereload   = require 'gulp-livereload'
 mocha        = require 'gulp-mocha'
 notify       = require 'gulp-notify'
-stylus       = require 'gulp-stylus'
 rename       = require 'gulp-rename'
 stylus       = require 'gulp-stylus'
 uglify       = require 'gulp-uglify'
@@ -36,24 +36,40 @@

 basePath = '.'
 src      = "#{basePath}/src"
-output   = "#{basePath}/public"
+output   = "#{basePath}/public/static"
 dist     = "#{basePath}/dist"
 test     = "#{basePath}/test"
-vendor   = "#{sources}/vendor"
+vendor   = "#{src}/vendor"
 port     = 3000

 #--------------------------------------------------------
-# Bower / Vendor Concatenation/Minification
+# Copy Bower libraries to vendor directory
 #--------------------------------------------------------

 gulp.task "bower", ->
    bowerFiles()
-      #.pipe concat("vendor.js")
-      .pipe gulp.dest("#{output}/assets/scripts")
+      .pipe gulp.dest "#{vendor}"
+
+
+
+
+#--------------------------------------------------------
+# Compile scripts using Browserify
+#--------------------------------------------------------
+
+
+gulp.task "browserify", ->
+   gulp.src "#{src}/scripts/app.coffee", read: false
+      .pipe browserify
+         transform:  ["coffeeify"]
+         extensions: [".coffee", ".js"]
+
+      .pipe rename "app.js"
+      .pipe gulp.dest "#{output}/assets/scripts"

@@ -63,84 +79,79 @@
 # --------------------------------------------------------

-
-
-#--------------------------------------------------------
-# Compile coffeescript files
-#--------------------------------------------------------
-
-
-gulp.task "coffee", ->
-   gulp.src "#{src}/js/*.coffee", read: false
-      .pipe browserify {transform: ["coffeeify"], extensions: [".coffee"]}
-      .pipe rename("app.js")
-      .pipe gulp.dest("#{output}/assets/scripts")
-
+gulp.task "clean", ->
+   gulp.src "#{output}/assets", read: false
+      .pipe clean()

 # --------------------------------------------------------
-# Concatinate Bower Vendor files copied from `grunt bower`
+# Concatinate vendor files
 # --------------------------------------------------------

-gulp.task 'concat'
+gulp.task "concat", ->
+
+   vendorFiles = [
+      "#{vendor}/lodash/dist/lodash.compat.js"
+      "#{vendor}/jquery/jquery.js"
+      "#{vendor}/backbone/backbone.js"
+      "#{vendor}/greensock/src/uncompressed/TweenMax.js"
+   ]
+
+   gulp.src vendorFiles
+      .pipe concat "vendor.js"
+      .pipe gulp.dest "#{output}/assets/scripts/"
+
+
+
+#--------------------------------------------------------
+# Compile Stylus stylesheet files
+#--------------------------------------------------------
+
+
+gulp.task "stylus", ->
+   gulp.src "#{src}/styles/app.styl"
+      .pipe stylus()
+      .pipe autoprefixer()
+      .pipe gulp.dest "#{output}/assets/styles"

 #--------------------------------------------------------
-# Stylesheets
-#--------------------------------------------------------
-
-
-gulp.task "stylus", ->
-   gulp.src "#{src}/css/*.styl"
-      .pipe stylus()
-      .pipe autoprefixer()
-      .pipe gulp.dest("#{output}/assets/styles")
-
-
-
-
-
-#--------------------------------------------------------
-# Watch
+# Watch for changes and reload page
 #--------------------------------------------------------

 gulp.task "watch", ->

-  # CSS Compilation
-  gulp.watch "#{src}/css/*.styl", ["stylus"]
+  # CSS
+  gulp.watch "#{src}/styles/**/*.styl", ["stylus"]

-  # JavaScript Complilation
-  gulp.watch "#{src}/js/*.coffee", ["coffee"]
+  # JavaScript
+  gulp.watch "#{src}/scripts/**/*.coffee", ["browserify"]

-  # Vendor Concatenation/Minification with Bower
-  gulp.watch "#{src}/vendor/**", ["bower"]
+  # Vendor
+  #gulp.watch "#{src}/vendor/**", ["concat"]

   # LiveReload
   server = livereload()
-  gulp.watch(["#{dist}/**"]).on "change", (file) ->
+  gulp.watch(["#{output}/**"]).on "change", (file) ->
     server.changed file.path

+# + ----------------------------------------------------------

-#--------------------------------------------------------
-# Tasks
-#--------------------------------------------------------
+

 gulp.task "default", [
-   "stylus",
-   "coffee",
-   "bower",
+   "clean"
+   "bower"
+   "browserify"
+   "stylus"
    "watch"
 ]

-
-
-
-


better name for gulp template precompiling

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -3,7 +3,7 @@
 concat = require 'gulp-concat'
 coffee = require 'gulp-coffee'
 compass = require 'gulp-compass'
-precompile = require './gulp/gulp-ember-handlebars'
+handlebars = require './gulp/gulp-ember-handlebars'

 coffeeFiles = [
   'data/pivotal'
@@ -27,9 +27,9 @@
     .pipe(concat('all.js'))
     .pipe(gulp.dest('./build/'))

-gulp.task 'precompile', ->
+gulp.task 'handlebars', ->
   gulp.src('./src/scripts/templates/*.hbs')
-    .pipe(precompile())
+    .pipe(handlebars())
     .pipe(concat('templates.js'))
     .pipe(gulp.dest('./build/'))

@@ -43,7 +43,7 @@
     gulp.run 'coffee'

   gulp.watch './src/scripts/templates/*.hbs', ->
-    gulp.run 'precompile'
+    gulp.run 'handlebars'

   gulp.watch './src/stylesheets/*.scss', ->
     gulp.run 'compass'


further changes for css cleaner

further changes for css cleaner

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -330,7 +330,7 @@
                 new RegExp('^.loading.*')
                 new RegExp('^.parsley-.*')
                 new RegExp('^.datalist.*')
-                new RegExp('^.active.*')
+                new RegExp('^li.active.*')
                 ]
                 ))
     .pipe(gulp.dest('build/css'))


refactor(gulpfile): don't watch for changes in `src-bin/`

That directory doesn't exist anymore.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -43,6 +43,6 @@
 				.pipe istanbul.writeReports()

 gulp.task "watch", ->
-	gulp.watch "./src{,-bin}/**/*.coffee", [ "coffee" ]
+	gulp.watch "./src/**/*.coffee", [ "coffee" ]

 gulp.task "default", [ "coffee" ]


fixed bug where /names wasn't working

fixed bug where /names wasn't working

--- src/chat/chat_commands.coffee
+++ src/chat/chat_commands.coffee
@@ -65,8 +65,9 @@
       win = @chat.currentWindow
       return unless (conn = win.conn) and (target = win.target) and
           (names = conn.irc.channels[target]?.names)
-        names = (v for k,v of names).sort()
-        @chat.currentWindow.message '*', "Users in #{target}: JSON.stringify names", 'circ'
+      names = (v for k,v of names).sort()
+      msg = "Users in #{target}: #{JSON.stringify names}"
+      @chat.currentWindow.message '*', msg, 'notice names'

     help: ->
       commands = @chat.chatCommands.getCommands()


Refactor ajax error handling logic reported in #422

The real issue was handling aborted requests. A status of '0' denotes
the request was initialized but not yet sent. Aborted requests are
intentional and should not cause error messages to appear.

--- src/coffee/cilantro/ui.coffee
+++ src/coffee/cilantro/ui.coffee
@@ -19,19 +19,22 @@
     './ui/notify'
 ], ($, _, Backbone, c, mods...) ->

-    externalLink = false
+    $(document).ajaxError (event, xhr, settings, exception) ->
+        # Status of 0 is an aborted request which is usually intentional
+        # by the app or from a page reload.
+        # An empty exception value is an unknown error which usually
+        # means the server is unavailable
+        if xhr.status is 0 or exception
+            return

-    $(document).ajaxError (event, xhr, settings, exception) ->
-        # Unknown error which usually means the server is unavailable
-        if not exception and not externalLink
-            c.notify
-                timeout: null
-                dismissable: false
-                level: 'error'
-                header: 'Uh oh.'
-                message: 'There is a communication problem with the server. ' +
-                    'Refreshing ' +
-                    'the page may help.'
+        c.notify
+            timeout: null
+            dismissable: false
+            level: 'error'
+            header: 'Uh oh.'
+            message: 'There is a communication problem with the server. ' +
+                'Refreshing ' +
+                'the page may help.'

     # Route based on the URL
     $(document).on 'click', 'a', (event) ->
@@ -53,11 +56,8 @@
         # otherwise let the event process normally to load the new
         # location.
         if c.router.hasRoute(pathname)
-            externalLink = false
             event.preventDefault()
             c.router.navigate(pathname, trigger: true)
-        else
-            externalLink = true

         return


[track] added login topic link click tracking for analytics

--- client/app/Applications/Topics.kdapplication/AppController.coffee
+++ client/app/Applications/Topics.kdapplication/AppController.coffee
@@ -107,6 +107,7 @@

   openTopic:(topic)->
     {entryPoint} = KD.config
+    KD.track "Topic", "Open", topic
     KD.getSingleton('router').handleRoute "/Topics/#{topic.slug}", {state:topic, entryPoint}

   updateTopic:(topicItem)->


refactor: minor change in the code

refactor: minor change in the code

--- app/models/file.coffee
+++ app/models/file.coffee
@@ -36,10 +36,7 @@

   isApiScanning: (->
     apiScanProgress = @get "apiScanProgress"
-    if apiScanProgress in [0, undefined, 100]
-      false
-    else
-      true
+    apiScanProgress not in [0, undefined, 100]
   ).property "apiScanProgress"

   fileDetailsClass: (->


Save images without sending the data-uri and without making a 404

--- scripts/gh-book/xhtml-file.coffee
+++ scripts/gh-book/xhtml-file.coffee
@@ -236,26 +236,56 @@
         src = $img.attr('src')
         attrs = imgDataSrcToAttributes(src)
         imgAbsolutePath = attrs.id
-        path = Utils.relativePath(@id, imgAbsolutePath)
-        $img.attr('src', path)
-
-      # FIXME: The following is DEAD CODE because Aloha always strips off the data-src attribute.
-      #
+        # Create a relative path to src
+        src = Utils.relativePath(@id, imgAbsolutePath)
+
+        # To prevent a 404 and trying to load a bunch of images, do not
+        # change the image source.
+        # Instead, use a dummy element and then replace it from the resulting HTML
+        $imgHolder = jQuery('<prefix-img></prefix-img>')
+        $imgHolder.attr(Utils.elementAttributes $img)
+        $imgHolder.attr 'src', src
+
+        $img.replaceWith $imgHolder
+
       # Replace all the `img[data-src]` attributes with `img[src]`
-      # $body.find('img[data-src]').each (i, img) ->
-      #   $img = jQuery(img)
-      #   src = $img.attr('data-src')
-      #   $img.removeAttr('data-src')
-      #   $img.attr 'src', src
+      $body.find('img[data-src]').each (i, img) ->
+        $img = jQuery(img)
+        src = $img.attr('data-src')
+        $img.removeAttr('data-src')
+        $img.removeAttr('src')
+
+        # To prevent a 404 and trying to load a bunch of images, do not
+        # change the image source.
+        # Instead, use a dummy element and then replace it from the resulting HTML
+        $imgHolder = jQuery('<prefix-img></prefix-img>')
+        $imgHolder.attr(Utils.elementAttributes $img)
+        $imgHolder.attr 'src', src
+
+        $img.replaceWith $imgHolder
+
+      headHtml = $head[0].innerHTML
+      bodyHtml = $body[0].innerHTML
+
+      # The text currently contains `<prefix-img ... ></prefix-img>`
+      # To make the `<img>` valid XHTML (self-closing like `<img/>`)
+      # replace "></prefix-img" with "/". Note the missing '>' ; )
+      bodyHtml = bodyHtml.replace(/><\/prefix-img/g, '/')
+      bodyHtml = bodyHtml.replace(/<prefix-img/g, '<img')
+
+      # HACK: For Collaborative edits of the ToC encourage elements to be on multiple lines
+      # by inserting newlines between tags
+      headHtml = headHtml.replace(/></g, '>\n<')
+      bodyHtml = bodyHtml.replace(/></g, '>\n<')

       return """
         <?xml version="1.0" encoding="UTF-8"?>
         <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
           <head>
-            #{$head[0].innerHTML}
+            #{headHtml}
           </head>
           <body>
-            #{$body[0].innerHTML}
+            #{bodyHtml}
           </body>
         </html>
         """


Support sending in windowState to AtomWindow ctor

--- src/atom-window.coffee
+++ src/atom-window.coffee
@@ -9,7 +9,7 @@
 class AtomWindow
   browserWindow: null

-  constructor: ({bootstrapScript, resourcePath, pathToOpen, exitWhenDone, @isSpec}) ->
+  constructor: ({bootstrapScript, resourcePath, pathToOpen, exitWhenDone, @isSpec, windowState}) ->
     global.atomApplication.addWindow(this)

     @setupNodePath(resourcePath)
@@ -20,7 +20,8 @@
     try
       initialPath = path.dirname(pathToOpen) if fs.statSync(pathToOpen).isFile()

-    @browserWindow.loadSettings = {initialPath, bootstrapScript, resourcePath, exitWhenDone, windowState: ''}
+    windowState ?= ''
+    @browserWindow.loadSettings = {initialPath, bootstrapScript, resourcePath, exitWhenDone, windowState}
     @browserWindow.once 'window:loaded', => @loaded = true
     @browserWindow.loadUrl "file://#{resourcePath}/static/index.html"


Make sessions a little more likely to save if we're not in production.

--- app/lib/LevelBus.coffee
+++ app/lib/LevelBus.coffee
@@ -29,8 +29,10 @@
   constructor: ->
     super(arguments...)
     @changedSessionProperties = {}
-    #@saveSession = _.debounce(@reallySaveSession, 1000, {maxWait: 5000})  # not during HoC apparently
-    @saveSession = _.debounce(@reallySaveSession, 10000, {maxWait: 30000})
+    if document.location.href.search('codecombat.com') isnt -1
+      @saveSession = _.debounce(@reallySaveSession, 10000, {maxWait: 30000})  # Save us during HoC
+    else
+      @saveSession = _.debounce(@reallySaveSession, 1000, {maxWait: 5000})  # Not this fast during HoC
     @playerIsIdle = false

   init: ->