Fixed typos

Fixed typos

--- src/dropzone.coffee
+++ src/dropzone.coffee
@@ -83,7 +83,7 @@
     maxFilesize: 256 # in MB
     paramName: "file" # The name of the file param that gets transferred.
     createImageThumbnails: true
-    maxThumbnailFilesize: 2 # in MB. When the filename exeeds this limit, the thumbnail will not be generated.
+    maxThumbnailFilesize: 2 # in MB. When the filename exceeds this limit, the thumbnail will not be generated.
     thumbnailWidth: 100
     thumbnailHeight: 100

@@ -138,7 +138,7 @@
       file.previewTemplate.find(".details").append o """<img alt="#{file.name}" src="#{dataUrl}"/>"""

-    # Called whenever an error occures
+    # Called whenever an error occurs
     # Receives `file` and `message`
     error: (file, message) ->
       file.previewTemplate.addClass "error"
@@ -150,7 +150,7 @@
     processingfile: (file) ->
       file.previewTemplate.addClass "processing"

-    # Called whenever the upload progress gets upadted.
+    # Called whenever the upload progress gets updated.
     # You can be sure that this will be called with the percentage 100% when the file is finished uploading.
     # Receives `file` and `progress` (percentage)
     uploadprogress: (file, progress) ->
@@ -185,7 +185,7 @@
     @element.data "dropzone", @

-    # Get the `Dropzone.opions.elementId` for this element if it exists
+    # Get the `Dropzone.options.elementId` for this element if it exists
     elementId = @element.attr "id"
     elementOptions = (Dropzone.options[camelize elementId] if elementId) ? { }

@@ -380,7 +380,7 @@
     fileReader.readAsDataURL file

-  # Goes through the qeue and processes files if there aren't too many already.
+  # Goes through the queue and processes files if there aren't too many already.
   processQueue: ->
     parallelUploads = @options.parallelUploads
     processingLength = @files.processing.length


Changed file.previewTemplate to file.previewElement.

Kept file.previewTemplate for backwards compatibility.

--- src/dropzone.coffee
+++ src/dropzone.coffee
@@ -194,43 +194,45 @@
     # Called when a file is added to the queue
     # Receives `file`
     addedfile: (file) ->
-      file.previewTemplate = Dropzone.createElement @options.previewTemplate
-      @previewsContainer.appendChild file.previewTemplate
-      file.previewTemplate.querySelector(".filename span").textContent = file.name
-      file.previewTemplate.querySelector(".details").appendChild Dropzone.createElement """
#{@filesize file.size}
"""
+      file.previewElement = Dropzone.createElement @options.previewTemplate
+      file.previewTemplate = file.previewElement # Backwards compatibility
+
+      @previewsContainer.appendChild file.previewElement
+      file.previewElement.querySelector(".filename span").textContent = file.name
+      file.previewElement.querySelector(".details").appendChild Dropzone.createElement """
#{@filesize file.size}
"""

     # Called whenever a file is removed.
     removedfile: (file) ->
-      file.previewTemplate.parentNode.removeChild file.previewTemplate
+      file.previewElement.parentNode.removeChild file.previewElement

     # Called when a thumbnail has been generated
     # Receives `file` and `dataUrl`
     thumbnail: (file, dataUrl) ->
-      file.previewTemplate.classList.remove "file-preview"
-      file.previewTemplate.classList.add "image-preview"
-      file.previewTemplate.querySelector(".details").appendChild Dropzone.createElement """"""
+      file.previewElement.classList.remove "file-preview"
+      file.previewElement.classList.add "image-preview"
+      file.previewElement.querySelector(".details").appendChild Dropzone.createElement """"""

     # Called whenever an error occurs
     # Receives `file` and `message`
     error: (file, message) ->
-      file.previewTemplate.classList.add "error"
-      file.previewTemplate.querySelector(".error-message span").textContent = message
+      file.previewElement.classList.add "error"
+      file.previewElement.querySelector(".error-message span").textContent = message

     # Called when a file gets processed. Since there is a cue, not all added
     # files are processed immediately.
     # Receives `file`
     processingfile: (file) ->
-      file.previewTemplate.classList.add "processing"
+      file.previewElement.classList.add "processing"

     # Called whenever the upload progress gets updated.
     # You can be sure that this will be called with the percentage 100% when the file is finished uploading.
     # Receives `file`, `progress` (percentage 0-100) and `bytesSent`.
     # To get the total number of bytes of the file, use `file.size`
     uploadprogress: (file, progress, bytesSent) ->
-      file.previewTemplate.querySelector(".progress .upload").style.width = "#{progress}%"
+      file.previewElement.querySelector(".progress .upload").style.width = "#{progress}%"

     # Called just before the file is sent. Gets the `xhr` object as second
     # parameter, so you can modify it (for example to add a CSRF token) and a
@@ -240,7 +242,7 @@
     # When the complete upload is finished and successfull
     # Receives `file`
     success: (file) ->
-      file.previewTemplate.classList.add "success"
+      file.previewElement.classList.add "success"

     # When the upload is finished, either with success or an error.
     # Receives `file`


Enable passing multiple filters by string sperated by comma.

--- src/filters.coffee
+++ src/filters.coffee
@@ -14,7 +14,9 @@
       if filter instanceof RegExp
         filter.test str
       else
-        minimatch str, filter
+        for filter in filter.split ','
+          return true if minimatch str, filter
+        false

     # Private: Helper filter to test if {value} matches the {filter}
     fieldMatchFilter: (filter, value) ->


Fixes openWindow function def and call

Fixes openWindow function def and call

--- src/chaplin/views/layout.coffee
+++ src/chaplin/views/layout.coffee
@@ -132,7 +132,7 @@
       if @settings.openExternalToBlank
         # Open external links normally in a new tab.
         event.preventDefault()
-        @openWindow href el
+        @openWindow href, el
       return

     # Pass to the router, try to route the path internally.
@@ -142,9 +142,9 @@
     event.preventDefault()
     return

-    # Handle all browsing context resources
-    openWindow: (href, el) ->
-      window.open href
+  # Handle all browsing context resources
+  openWindow: (href, el) ->
+    window.open href

   # Region management
   # -----------------


make book title changes save

make book title changes save

--- scripts/gh-book/opf-file.coffee
+++ scripts/gh-book/opf-file.coffee
@@ -47,6 +47,11 @@
       @tocNodes.on 'tree:add',    (model, collection, options) => @tocNodes.add model, options
       @tocNodes.on 'tree:remove', (model, collection, options) => @tocNodes.remove model, options

+      @tocNodes.on 'change:title', (model, collection, options) =>
+        if model.get('title') != @$xml.find('title').text()
+          @$xml.find('title').text(model.get('title'))
+          @_save()
+
       @getChildren().on 'add remove tree:change tree:add tree:remove', (model, collection, options) =>
         setNavModel(options)
       @getChildren().on 'change reset', (collection, options) =>
@@ -92,6 +97,13 @@
       # This is useful when the OPF file was remotely updated
       @_localAddedItems[model.id] = model

+    _save: =>
+      clearTimeout(@_savingTimeout)
+      @_savingTimeout = setTimeout (() =>
+        allContent.save(@navModel, false, true) # include-resources, include-new-files
+        delete @_savingTimeout
+      ), SAVE_DELAY
+

     _loadComplex: (fetchPromise) ->
       fetchPromise
@@ -110,11 +122,7 @@
           if not _.isEmpty _.omit model.changedAttributes(), ['_isDirty', '_hasRemoteChanges', '_original', 'dateLastModifiedUTC']
             # Delay the save a little bit because a move is a remove + add
             # which would otherwise cause 2 saves
-            clearTimeout(@_savingTimeout)
-            @_savingTimeout = setTimeout (() =>
-              allContent.save(@navModel, false, true) # include-resources, include-new-files
-              delete @_savingTimeout
-            ), SAVE_DELAY
+            @_save()

     _parseNavModel: () ->


Update explorer.coffee

Update explorer.coffee


add 'anchor' field to grid_focus, refactor row and col into 'focus' field

--- src/coffee/client/puzzle.coffee
+++ src/coffee/client/puzzle.coffee
@@ -47,8 +47,8 @@
         grid_focus: @defaultGridFocus()

     defaultGridFocus: () ->
-        row: 0
-        col: 0
+        focus: {row: 0, col: 0}
+        anchor: {row: 0, col: 0}
         is_across: true
         field_open: "none" # "none" or "number" or "contents"

@@ -66,19 +66,19 @@
                     return "open_cell"
                 else
                     focus = @state.grid_focus
-                    if focus.row == row and focus.col == col
+                    if focus.focus.row == row and focus.focus.col == col
                         return "open_cell_highlighted"
                     else if (\
-                        (focus.is_across and focus.row == row and \
-                            ([col..focus.col].every (col1) -> grid[row][col1].open)) or \
-                        ((not focus.is_across) and focus.col == col and \
-                            ([row..focus.row].every (row1) -> grid[row1][col].open)))
+                        (focus.is_across and focus.focus.row == row and \
+                            ([col..focus.focus.col].every (col1) -> grid[row][col1].open)) or \
+                        ((not focus.is_across) and focus.focus.col == col and \
+                            ([row..focus.focus.row].every (row1) -> grid[row1][col].open)))
                         return "open_cell_highlighted_intermediate"
                     else
                         return "open_cell"
             else
-                if (@state.grid_focus != null and @state.grid_focus.row == row \
-                        and @state.grid_focus.col == col)
+                if (@state.grid_focus != null and @state.grid_focus.focus.row == row \
+                        and @state.grid_focus.focus.col == col)
                     return "closed_cell_highlighted"
                 else
                     return "closed_cell"
@@ -91,11 +91,14 @@
     fixFocus: (puzzle, grid_focus) ->
         if grid_focus == null
             return null
-        row = grid_focus.row
-        col = grid_focus.col
+        row = grid_focus.focus.row
+        col = grid_focus.focus.col
+        row1 = grid_focus.anchor.row
+        col1 = grid_focus.anchor.col
         height = puzzle.grid.length
         width = puzzle.grid[0].length
-        if not (row >= 0 and row < height and col >= 0 and col < width)
+        if not (row >= 0 and row < height and col >= 0 and col < width and \
+                row1 >= 0 and row1 < height and col1 >= 0 and col1 < width)
             null
         else
             grid_focus
@@ -121,13 +124,17 @@

     moveGridCursor: (drow, dcol) ->
         if @state.grid_focus
-            col1 = @state.grid_focus.col + dcol
-            row1 = @state.grid_focus.row + drow
+            col1 = @state.grid_focus.focus.col + dcol
+            row1 = @state.grid_focus.focus.row + drow
             if col1 >= 0 and col1 < @width() and row1 >= 0 and row1 < @height()
                 @setState
                     grid_focus:
-                        row: row1
-                        col: col1
+                        focus:
+                            row: row1
+                            col: col1
+                        anchor:
+                            row: row1
+                            col: col1
                         is_across: drow == 0
                         field_open: "none"
                 return true
@@ -138,27 +145,27 @@
         if grid_focus != null
             c = String.fromCharCode keyCode
             @props.requestOp Ot.opEditCellValue \
-                grid_focus.row, grid_focus.col, "contents", c
-            if grid_focus.is_across and grid_focus.col < @width() - 1
-                grid_focus.col += 1
-            else if (not grid_focus.is_across) and grid_focus.row < @height() - 1
-                grid_focus.row += 1
+                grid_focus.focus.row, grid_focus.focus.col, "contents", c
+            if grid_focus.is_across and grid_focus.focus.col < @width() - 1
+                grid_focus.focus.col += 1
+            else if (not grid_focus.is_across) and grid_focus.focus.row < @height() - 1
+                grid_focus.focus.row += 1
         @setState { grid_focus: grid_focus }

     deleteLetter: (keyCode) ->
         grid_focus = Utils.clone @state.grid_focus
         if grid_focus != null
             grid_focus.cell_field = "none"
-            row = grid_focus.row
-            col = grid_focus.col
+            row = grid_focus.focus.row
+            col = grid_focus.focus.col
             g = @state.puzzle.grid
             if g[row][col].open and g[row][col].contents != ""
                 @props.requestOp Ot.opEditCellValue \
-                    grid_focus.row, grid_focus.col, "contents", ""
-                if grid_focus.is_across and grid_focus.col > 0
-                    grid_focus.col -= 1
-                else if (not grid_focus.is_across) and grid_focus.row > 0
-                    grid_focus.row -= 1
+                    grid_focus.focus.row, grid_focus.focus.col, "contents", ""
+                if grid_focus.is_across and grid_focus.focus.col > 0
+                    grid_focus.focus.col -= 1
+                else if (not grid_focus.is_across) and grid_focus.focus.row > 0
+                    grid_focus.focus.row -= 1
             else
                 if grid_focus.is_across
                     row1 = row
@@ -170,8 +177,8 @@
                     if g[row1][col1].open and g[row1][col1].contents != ""
                         @props.requestOp Ot.opEditCellValue \
                             row1, col1, "contents", ""
-                    grid_focus.col = col1
-                    grid_focus.row = row1
+                    grid_focus.focus.col = col1
+                    grid_focus.focus.row = row1
             @setState { grid_focus: grid_focus }

     # Perform an automatic renumbering.
@@ -184,8 +191,8 @@
             grid_focus = Utils.clone @state.grid_focus
             grid_focus.field_open = "none"
             @setState { grid_focus: grid_focus }
-            row = grid_focus.row
-            col = grid_focus.col
+            row = grid_focus.focus.row
+            col = grid_focus.focus.col
             newvalue = not @state.puzzle.grid[row][col].open
             op = Ot.opEditCellValue row, col, "open", newvalue
             if @state.maintainRotationalSymmetry
@@ -197,7 +204,7 @@
     openCellField: (type) ->
         grid_focus = Utils.clone @state.grid_focus
         if grid_focus != null and \
-                @state.puzzle.grid[grid_focus.row][grid_focus.col].open
+                @state.puzzle.grid[grid_focus.focus.row][grid_focus.focus.col].open
             grid_focus.field_open = type
             @setState { grid_focus: grid_focus }
     removeCellField: (grid_focus) ->
@@ -272,11 +279,11 @@

         grid_focus = @removeCellField(grid_focus)

-        if grid_focus != null and grid_focus.row == row and grid_focus.col == col
+        if grid_focus != null and grid_focus.focus.row == row and grid_focus.focus.col == col
             grid_focus.is_across = not grid_focus.is_across
         else
-            grid_focus.row = row
-            grid_focus.col = col
+            grid_focus.focus.row = row
+            grid_focus.focus.col = col
             grid_focus.is_across = true
         this.setState { grid_focus: grid_focus }

@@ -304,8 +311,8 @@
                 primaryNumber: null,
                 secondaryNumber: null
              }
-        row = @state.grid_focus.row
-        col = @state.grid_focus.col
+        row = @state.grid_focus.focus.row
+        col = @state.grid_focus.focus.col
         while true
             row1 = if isAcross then row else row - 1
             col1 = if isAcross then col - 1 else col
@@ -391,7 +398,7 @@
                     grid_row={@props.grid[row]}
                     cell_classes={@props.cell_classes[row]}
                     grid_focus={
-                        if @props.grid_focus? and @props.grid_focus.row == row then @props.grid_focus else null
+                        if @props.grid_focus? and @props.grid_focus.focus.row == row then @props.grid_focus else null
                     }
                     onCellFieldKeyPress={(event, col) => @props.onCellFieldKeyPress(event, row, col)}
                     onCellClick={(col) => @props.onCellClick(row, col)}
@@ -413,7 +420,7 @@
                     grid_cell={@props.grid_row[col]}
                     cell_class={@props.cell_classes[col]}
                     grid_focus={
-                        if @props.grid_focus? and @props.grid_focus.col == col then @props.grid_focus else null
+                        if @props.grid_focus? and @props.grid_focus.focus.col == col then @props.grid_focus else null
                     }
                     onCellFieldKeyPress={(event) => @props.onCellFieldKeyPress(event, col)}
                     onCellClick={(event) => @props.onCellClick(col)}


fix(Firefox): mysterious crash at Amazon Web Services Console

refs #9

--- src/coffee/firefox/index.coffee
+++ src/coffee/firefox/index.coffee
@@ -6,9 +6,7 @@
 # e.g. 'https://github.com/fczbkk/github-issues-extension/issues/2'
 include_pattern = ///
   # any protocol
-  .*://
-  # any subdomain at github.com
-  (.*\.)*github\.com/
+  .*://github\.com/
   # issues in any project under any user
   .*/(issues|pull)/
   # issue detail
@@ -51,7 +49,6 @@
   contentStyleFile: self.data.url 'content.css'
   onAttach: startListening

-
 pageMod.PageMod
   include: settings_pattern
   contentScriptFile: self.data.url 'settings.js'


Object: Fixed getOption method name.

Object: Fixed getOption method name.

--- src/coffee/object/Object.coffee
+++ src/coffee/object/Object.coffee
@@ -25,7 +25,7 @@

   getOptions: -> return @options

-  getOptions: (key) -> return @options[key] or null
+  getOption: (key) -> return @options[key] or null

   setData: (data) -> @data = data


Simplify queryString params for process function

--- src/coffee/services/base.coffee
+++ src/coffee/services/base.coffee
@@ -192,42 +192,37 @@
    * Process the resources for each page separatly using the function fn.
    * The function fn will then be called once for per page.
    * The function fn has to return a promise that should be resolved when all elements of the page are processed.
-   * E.g. page(3).perPage(5) will start processing at element 10, gives you a payload of 5 elements per call of fn again and again until all elements are processed.
    * @param {Function} the function to process a page that returns a promise
    * @return {Promise} A promise, fulfilled with an array of the resolved results of function fn or the rejected result of fn
+   * @example
+   *   page(3).perPage(5) will start processing at element 10, gives you a payload of 5 elements per call of fn again and again until all elements are processed.
   ###
   process: (fn) ->
-    unless _.isFunction fn
-      throw new Error 'Please provide a function to process the elements'
+    throw new Error 'Please provide a function to process the elements' unless _.isFunction fn

     deferred = Q.defer()
-
     endpoint = @_currentEndpoint
-    where = @_params.query.where
-    whereOperator = @_params.query.operator
-    sort = @_params.query.sort
-
-    processPage = (page, perPage, total, acc = []) =>
+    originalQuery = @_params.query
+
+    _processPage = (page, perPage, total, acc = []) =>
       if total? and (page - 1) * perPage > total
         deferred.resolve acc
       else
-        @_params.query.where = where
-        @_params.query.operator = whereOperator
-        @_params.query.sort = sort
-        @_params.query.page = page
-        @_params.query.perPage = perPage
+        @_params.query = _.extend {}, originalQuery,
+          page: page
+          perPage: perPage
         queryString = @_queryString()

         @_get("#{endpoint}?#{queryString}")
         .then (payload) ->
           fn(payload)
           .then (result) ->
-            processPage page + 1, perPage, payload.body.total, acc.concat(result)
+            _processPage page + 1, perPage, payload.body.total, acc.concat(result)
         .fail (error) ->
           deferred.reject error
         .done()

-    processPage(@_params.query.page or 1, @_params.query.perPage or 20)
+    _processPage(@_params.query.page or 1, @_params.query.perPage or 20)
     deferred.promise

   ###*


Add SAT, salesforce, cck12 design projects

--- scripts/pivotal-listener.coffee
+++ scripts/pivotal-listener.coffee
@@ -38,6 +38,9 @@
         "Fiber Toolkit": "FTK-",
         "SOTS Progress Tracking": "SOTS-",
         "TechDebt": "TD-",
+        "SAT": "SAT-",
+        "Salesforce": "SF-",
+        "CCK12 Design": "CCK12D-"

       project =
         projectId: reqBody["project"]["id"]


Write files when you have both name and content

--- src/components/WriteFile.coffee
+++ src/components/WriteFile.coffee
@@ -14,19 +14,22 @@
       error: new noflo.Port

     @inPorts.in.on 'data', (data) =>
+      if @filename
+        @writeFile @filename, data
+        @filename = null
+        return
       @data = data
-      @writeFile @filename, data if @filename

     @inPorts.filename.on 'data', (data) =>
+      if @data
+        @writeFile data, @data
+        @data = null
+        return
       @filename = data
-      @writeFile data, @data if @data
-
-    @inPorts.filename.on 'endgroup', =>
-      @filename = null

   writeFile: (filename, data) ->
     fs.writeFile filename, data, 'utf-8', (err) =>
-      return @outPorts.error send err if err
+      return @outPorts.error.send err if err
       @outPorts.out.send filename
       @outPorts.out.disconnect()


to string in base model

to string in base model

--- vendor/assets/javascripts/angular_objectify_resource/base_model.coffee
+++ vendor/assets/javascripts/angular_objectify_resource/base_model.coffee
@@ -30,6 +30,8 @@

     decorator: ->
       @_decorator ?= new (@constructor.DECORATOR)(@)
+
+    toString: -> @toParams()

     toParams: ->
       result = {}


Include node in 1 line too.

Include node in 1 line too.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -222,8 +222,7 @@
   conventions.assets  ?= /assets[\\/]/
   conventions.ignored ?= paths.ignored ? [
     /[\\/]_/
-    /vendor[\\/]node[\\/]/
-    /vendor[\\/](j?ruby-.*|bundle)[\\/]/
+    /vendor[\\/](node|j?ruby-.*|bundle)[\\/]/
   ]
   conventions.vendor  ?= /(^bower_components|vendor)[\\/]/


fixup! Call context menu item ::created hooks with the click event

--- src/context-menu-manager.coffee
+++ src/context-menu-manager.coffee
@@ -27,9 +27,9 @@
     @add '.workspace': [{
       label: 'Inspect Element'
       command: 'application:inspect'
-      created: (item, event) ->
+      created: (event) ->
         {pageX, pageY} = event
-        item.commandOptions = {x: pageX, y: pageY}
+        @commandOptions = {x: pageX, y: pageY}
     }]

     atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()


Don't use requestAnimationFrame for pointerdown/up events so iOS lets you change input field focus

--- src/core/LiterallyCanvas.coffee
+++ src/core/LiterallyCanvas.coffee
@@ -112,15 +112,14 @@
   setShapesInProgress: (newVal) -> @_shapesInProgress = newVal

   pointerDown: (x, y) ->
-    util.requestAnimationFrame () =>
-      p = @clientCoordsToDrawingCoords(x, y)
-      if @tool.usesSimpleAPI
-        @tool.begin p.x, p.y, this
-        @isDragging = true
-        @trigger("drawStart", {tool: @tool})
-      else
-        @isDragging = true
-        @trigger("pointerdown", {tool: @tool, x: p.x, y: p.y, rawX: x, rawY: y})
+    p = @clientCoordsToDrawingCoords(x, y)
+    if @tool.usesSimpleAPI
+      @tool.begin p.x, p.y, this
+      @isDragging = true
+      @trigger("drawStart", {tool: @tool})
+    else
+      @isDragging = true
+      @trigger("pointerdown", {tool: @tool, x: p.x, y: p.y, rawX: x, rawY: y})

   pointerMove: (x, y) ->
     util.requestAnimationFrame () =>
@@ -136,16 +135,15 @@
           @trigger("pointermove", {tool: @tool, x: p.x, y: p.y, rawX: x, rawY: y})

   pointerUp: (x, y) ->
-    util.requestAnimationFrame () =>
-      p = @clientCoordsToDrawingCoords(x, y)
-      if @tool.usesSimpleAPI
-        if @isDragging
-          @tool.end p.x, p.y, this
-          @isDragging = false
-          @trigger("drawEnd", {tool: @tool})
-      else
+    p = @clientCoordsToDrawingCoords(x, y)
+    if @tool.usesSimpleAPI
+      if @isDragging
+        @tool.end p.x, p.y, this
         @isDragging = false
-        @trigger("pointerup", {tool: @tool, x: p.x, y: p.y, rawX: x, rawY: y})
+        @trigger("drawEnd", {tool: @tool})
+    else
+      @isDragging = false
+      @trigger("pointerup", {tool: @tool, x: p.x, y: p.y, rawX: x, rawY: y})

   setColor: (name, color) ->
     @colors[name] = color


Fix dk spec that would fail, even if dk worked

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -435,7 +435,7 @@
         it "deletes the bottom two lines", ->
           keydown('d')
           keydown('k')
-          expect(editor.getText()).toBe("ABCDE")
+          expect(editor.getText()).toBe("12345")

       describe "on the beginning of the file", ->
         beforeEach ->
@@ -451,7 +451,7 @@
         it "deletes the first two lines", ->
           keydown('d')
           keydown('k')
-          expect(editor.getText()).toBe("12345")
+          expect(editor.getText()).toBe("ABCDE")

     describe "when followed by a G", ->
       beforeEach ->


Fix tests

Fix tests

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -164,12 +164,10 @@
           expect(treeView).toBeHidden()

       describe "when the tree view is not focused", ->
-        it "shifts focus to the tree view", ->
-          atom.workspaceView.openSync() # When we call focus below, we want an editor to become focused
-          atom.workspaceView.focus()
+        it "hides the tree view", ->
+          treeView.focus()
           atom.workspaceView.trigger 'tree-view:toggle'
-          expect(treeView).toBeVisible()
-          expect(treeView.list).toMatchSelector(':focus')
+          expect(treeView).toBeHidden()

     describe "when the tree view is hidden", ->
       it "shows and focuses the tree view", ->
@@ -242,7 +240,7 @@
       treeView.focus()
       expect(treeView.list).toMatchSelector(':focus')
       treeView.trigger 'tool-panel:unfocus'
-      expect(treeView).not.toBeVisible()
+      expect(treeView).toBeVisible()
       expect(treeView.list).not.toMatchSelector(':focus')
       expect(atom.workspaceView.getActiveView().isFocused).toBeTruthy()


insights: Fix calculation of time interval off by *24

--- src/insights.coffee
+++ src/insights.coffee
@@ -45,7 +45,7 @@
   queries = []
   end = new Date() # TODO: make configurable?
   start = new Date(end)
-  start.setTime(start.getTime()-options.period*60*60*1000)
+  start.setTime(start.getTime()-options.period*24*60*60*1000)
   queries = getTimeIntervals start, end, options.queryInterval

   # execute queries


friday(*): Minor bug patch for internal sidebar

--- internal_packages/sidebar-inbox-internal/lib/internal-admin-store.coffee
+++ internal_packages/sidebar-inbox-internal/lib/internal-admin-store.coffee
@@ -17,6 +17,7 @@
   dataForFocusedContact: ->
     return {loading: true} if @_accountCache is null or @_applicationCache is null
     contact = FocusedContactsStore.focusedContact()
+    return {} unless contact
     account = _.find @_accountCache, (account) -> account.email is contact.email
     apps = undefined
     if account


Demo fake upload progress

Demo fake upload progress

--- assets/demo.coffee
+++ assets/demo.coffee
@@ -12,9 +12,7 @@
     didAddAttachment: (attachment) ->
       console.log "Host received attachment:", attachment
       if file = attachment.file
-        uploadFile attachment.file, (response) ->
-          attributes = JSON.parse(response)
-          attachment.setAttributes(attributes)
+        uploadAttachment(attachment)
       else
         saveAttachment(attachment)

@@ -51,17 +49,26 @@
   if item = document.getElementById("attachment_#{attachment.id}")
     item.parentElement.removeChild(item)

-uploadFile = (file, callback) ->
+uploadAttachment = (attachment) ->
+  {file} = attachment
   e = (string) -> encodeURIComponent(string)
   url = "/attachments?contentType=#{e(file.type)}&filename=#{e(file.name)}"

   xhr = new XMLHttpRequest
   xhr.open("POST", url, true)
   xhr.setRequestHeader("Content-Type", "application/octet-stream")
-  xhr.onreadystatechange = (response) ->
+  xhr.onreadystatechange = (response) =>
     if xhr.readyState is 4
       if xhr.status is 200
-        callback?(xhr.responseText)
+        progress = 0
+        fakeProgress = =>
+          attachment.setUploadProgress(progress++)
+          if progress is 100
+            attributes = JSON.parse(xhr.responseText)
+            attachment.setAttributes(attributes)
+          else
+            setTimeout(fakeProgress, 20)
+        fakeProgress()
       else
         console.warn "Host failed to upload file:", file
   xhr.send(file)


Upload correct verison of .deb file

Upload correct verison of .deb file

--- build/tasks/publish-build-task.coffee
+++ build/tasks/publish-build-task.coffee
@@ -58,11 +58,12 @@
       ]
     when 'linux'
       buildDir = grunt.config.get('atom.buildDir')
-      sourcePath = fs.listSync(buildDir, ['.deb'])[0]
       if process.arch is 'ia32'
         arch = 'i386'
       else
         arch = 'amd64'
+      {version} = grunt.file.readJSON('package.json')
+      sourcePath = "#{buildDir}/atom-#{version}-#{arch}.deb"
       assetName = "atom-#{arch}.deb"

       {cp} = require('./task-helpers')(grunt)


Add getAssets method

Add getAssets method

--- build/tasks/publish-build-task.coffee
+++ build/tasks/publish-build-task.coffee
@@ -8,16 +8,6 @@
 request = require 'request'

 grunt = null
-
-if process.platform is 'darwin'
-  assets = [
-    {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'}
-    {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'}
-  ]
-else
-  assets = [
-    {assetName: 'atom-windows.zip', sourcePath: 'Atom'}
-  ]

 commitSha = process.env.JANKY_SHA1
 token = process.env.ATOM_ACCESS_TOKEN
@@ -33,6 +23,7 @@

     done = @async()
     buildDir = grunt.config.get('atom.buildDir')
+    assets = getAssets()

     zipAssets buildDir, assets, (error) ->
       return done(error) if error?
@@ -42,6 +33,18 @@
         deleteExistingAssets release, assetNames, (error) ->
           return done(error) if error?
           uploadAssets(release, buildDir, assets, done)
+
+getAssets = ->
+  if process.platform is 'darwin'
+    [
+      {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'}
+      {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'}
+      {assetName: 'atom-docs.zip', sourcePath: grunt.config.get('docsOutputDir')}
+    ]
+  else
+    [
+      {assetName: 'atom-windows.zip', sourcePath: 'Atom'}
+    ]

 logError = (message, error, details) ->
   grunt.log.error(message)


Validate IDs with Pixabay before adding

Validate IDs with Pixabay before adding

--- picksel.coffee
+++ picksel.coffee
@@ -1,6 +1,6 @@
 # Constants.
 USER_PATH = './.pickselacc'
-CONFIG_PATH = './picksel.json'
+PROJECT_PATH = './picksel.json'

 # Load core dependencies.
@@ -18,7 +18,7 @@
 i18n = require 'i18n'
 #pixabay = require 'pixabayjs'
 yn = require 'yn'
-mkdirp = require 'mkdir-p'
+mkdir = require 'mkdir-p'

 readline = require 'readline'

@@ -33,9 +33,9 @@
 i18n.configure i18nconfig
 i18n.setLocale 'eo'

-# Space for config files.
+# Space for workspace files.
 user = null
-config = null
+project = null

 # Initialize logging
@@ -69,7 +69,7 @@
 ]

-class ProjectConfiguration
+class Project
   constructor: () ->
     @directory = ''
     @images = []
@@ -131,14 +131,14 @@
   jsonfile.writeFile filename, obj, options, callback

-# Persists the currently loaded configuration settings
+# Persists a project.
 #
 persistProject = (obj, callback) ->
-  if obj then config = obj # Replace loaded config.
-  persist CONFIG_PATH, config, callback
-
-
-# Persists the currently loaded user settings.
+  if obj then project = obj # Replace loaded project.
+  persist PROJECT_PATH, project, callback
+
+
+# Persists a user.
 #
 persistUser = (obj, callback) ->
   if obj then user = obj # Replace loaded user.
@@ -208,15 +208,35 @@
 #
 # @param [Object] image the image to grab
 grab = (image) ->
-  path = "./#{config.directory}/#{image.destination}"
+  path = "./#{project.directory}/#{image.destination}"
   log.info "Installing image with ID #{image.id} at resolution" \
     + "#{humanResolutions[image.resolution]} to #{path}"
   download image.id, image.resolution, path

-# Installs all images present in the currently loaded configuration.
-#
-install = () -> grab image for image in config.images
+# Installs all images present in the currently loaded project.
+#
+install = () -> grab image for image in project.images
+
+
+# Checks whether or not a string contains only numeric characters.
+#
+# @param [String] str the string to test
+#
+isNumeric = (str) -> /^\d+$/.test str
+
+
+# Validates that an ID maps to an image on Pixabay.
+#
+# @param [String] id the ID to check
+#
+validateId = (id, callback) ->
+  url = buildUrl user.apiKey, id, (if isNumeric then 0 else 2) # Hash ID?
+  options =
+    url: url
+    json: true
+  request url, (error, response, body) -> # Request image JSON.
+    callback(!error && response.statusCode == 200)

 # Adds an image as an asset.
@@ -224,26 +244,32 @@
 # @param [Object] args the arguments to the program
 #
 add = (args) ->
-  id = args[3]
-
-  # Check resolution.
-  res = args[4]
-  if !isValidResolution res
-    log.error "That resolution '#{res}' isn't valid."
-    return false
-
-  # Add new image to configuration file.
-  image =
-    id: id
-    resolution: getResolutionCode res
-    destination: args[5]
-  config.images.push image
-
-  persistProject() # Update configuration file.
-
-  log.info "Added image with ID '#{image.id}' as asset."
-
-  install() # Freshly install all images.
+  id = args[3] # ID should be fourth argument.
+  resolution = args[4] # Resolution code should be fifth argument.
+  destination = args[5] # File destination should be sixth argument.
+  if typeof id == 'undefined' \
+    || typeof resolution == 'undefined' \
+    || typeof destination == 'undefined'
+      log.error 'You didn\'t provide enough arguments! Like this:' \
+        + ' \'picksel add <id> <resolution> <destination>\'' # ID not passed in.
+  else
+    if isValidResolution resolution
+      validateId id, (success) ->
+        if success
+          image =
+            id: id
+            resolution: resolution
+            destination: destination
+          project.images.push image # Add new image to project file.
+          persistProject null, (err) -> # Update project file.
+            if err
+              log.error 'Error writing your project file to disk!'
+            else
+              log.info "Added image with ID '#{image.id}' as asset." # Success!
+        else
+          log.error "That ID #{id} isn't valid." # Invalid ID
+    else
+      log.error "That resolution '#{resolution}' isn't valid." # Bad resolution.

 # Removes an image from installed assets.
@@ -251,34 +277,53 @@
 # @param [Object] args the arguments to the program
 #
 remove = (args) ->
-  id = args[3]
-  images = config.images
-  filteredImages = images.filter (obj) -> obj.id != args[3]
-  if images.length == filteredImages.length
-    log.warning "Couldn't uninstall image with ID '#{id}' because it's not" \
-      + ' installed in the first place.'
+  id = args[3] # ID should be fourth argument.
+  if typeof id == 'undefined'
+    log.error 'You need to pass in the ID of the image to remove! Like this:' \
+      + ' \'picksel remove <id>\'' # ID not passed in.
   else
-    config.images = filteredImages
-    persistProject()
-
-
-# Attempts to load the user and dependency configuration files.
-#
-loadConfig = () ->
-  if existsFile.sync USER_PATH
-    user = jsonfile.readFileSync USER_PATH
-  else
-    log.error "Couldn't find your '#{USER_PATH}' file to get your API key." \
-      + ' You should probably run \'picksel auth\' to set one up.'
-    return false
-  if existsFile.sync CONFIG_PATH
-    config = jsonfile.readFileSync CONFIG_PATH
-  else
-    log.error "Couldn't find your '#{CONFIG_PATH}' file with all your" \
-      + ' dependencies. You should probably run \'picksel init\' to set one' \
-      + ' up.'
-    return false
-  true
+    filtered = project.images.filter (obj) -> obj.id != id
+    if project.images.length == filtered.length
+      log.warning "Couldn't uninstall image with ID '#{id}' because it's not" \
+        + ' installed in the first place.' # Image to remove not installed.
+    else
+      project.images = filtered # Assign filtered image array to project.
+      persistProject null, (err) -> # Persist project file.
+        if err
+          log.error "Error writing your project file to disk!"
+        else
+          log.info "Image with ID #{id} uninstalled." # Success!
+
+
+# Attempts to load the user and project files.
+#
+loadWorkspace = (callback) ->
+  existsFile USER_PATH, (err, exists) -> # Check user file exists.
+    if exists
+      jsonfile.readFile USER_PATH, (err, userObj) -> # Read file.
+        if err
+          log.error "Couldn't read your '#{USER_PATH}' file!"
+          callback false # Error reading user file.
+        else
+          user = userObj # Store user object.
+          existsFile PROJECT_PATH, (err, exists) -> # Check project file exists.
+            if exists
+              jsonfile.readFile PROJECT_PATH, (err, projObj) -> # Read file.
+                if err
+                  log.error "Couldn't read your '#{PROJECT_PATH}' file!"
+                  callback false # Error reading project file.
+                else
+                  project = projObj # Store project object.
+                  callback true # Successfully read both files.
+            else
+              log.error "Couldn't find your '#{PROJECT_PATH}' file with all" \
+                + ' your dependencies. You should probably run \'picksel' \
+                + ' init\' to set one up.'
+              callback false # Error finding project file.
+    else
+      log.error "Couldn't find your '#{USER_PATH}' file to get your API key." \
+        + ' You should probably run \'picksel auth\' to set one up.'
+      callback false # Error finding user file.

 # Prompts the user to create a directory if it doesn't exist yet.
@@ -294,7 +339,7 @@
       ask "Doesn't look like the #{dir} directory exists, create it? (y/n) ", \
         (answer) ->
           if yn answer # Create directory? Yes or no.
-            mkdirp dir, (err) ->
+            mkdir dir, (err) ->
               if err
                 log.error 'Couldn\'t create directory!'
                 callback err, false # Folder doesn't exist and wasn't created.
@@ -309,25 +354,25 @@
 # Walks the user through initializing their project file.
 #
 init = () ->
-  existsFile CONFIG_PATH, (err, stats) -> # Check for an existing project file.
+  existsFile PROJECT_PATH, (err, stats) -> # Check for an existing project file.
     if stats
       log.warning 'Looks like this project has already been initialized for' \
         + ' Picksel.' # If file exists, inform user and abort.
     else
       log.info 'Let\'s initialize Picksel for this project...'
-      newProj = new ProjectConfiguration() # Initialize new project.
+      newProj = new Project() # Initialize new project.
       ask 'Relative to the current directory, where would you like to store' \
         + ' assets? ', (answer) ->
           promptDirectoryCreation answer, (err, exists) ->
             if !exists # Warn user about missing assets directory.
-              log.warning "The #{answer} directory isn't present on-disk. You" \
+              log.warning "The #{answer} directory isn't present on disk. You" \
                 + ' won\'t be able to install assets until it is.'
             newProj.directory = answer # Add asset path to project.
             persistProject newProj, (err) -> # Persist new project file.
               if err
                 log.error 'Error writing your project file to disk!'
               else
-                log.info "New file created at '#{CONFIG_PATH}' for holding" \
+                log.info "New file created at '#{PROJECT_PATH}' for holding" \
                   + ' your asset dependencies. Feel free to check this file' \
                   + ' in to source control.' # Project setup success!

@@ -399,8 +444,11 @@
   when 'init' then init()
   when 'auth' then auth()
   else
-    if loadConfig()
-      switch process.argv[2]
-        when 'install' then install()
-        when 'add' then add process.argv
-        when 'remove' then remove process.argv
+    loadWorkspace (success) ->
+      if success
+        switch process.argv[2]
+          when 'install' then install()
+          when 'add' then add process.argv
+          when 'remove' then remove process.argv
+      else
+        log.error "Couldn't load workspace for above reason. Terminating."


Also set it for install

Also set it for install

--- src/install.coffee
+++ src/install.coffee
@@ -83,7 +83,7 @@
     opts = {env, cwd: @atomDirectory}
     opts.streaming = true if @verbose

-    atomNodeGypPath = process.env.ATOM_NODE_GYP_PATH or require.resolve('node-gyp/bin/node-gyp')
+    atomNodeGypPath = process.env.ATOM_NODE_GYP_PATH or require.resolve('npm/node_modules/node-gyp/bin/node-gyp')
     @fork atomNodeGypPath, installNodeArgs, opts, (code, stderr='', stdout='') ->
       if code is 0
         callback()


small tweak, slightly cleaner

small tweak, slightly cleaner

--- src/keyring.coffee
+++ src/keyring.coffee
@@ -42,9 +42,9 @@
     delete data[@UNIQ_TAG]

     fillGuests = (p, kr) ->
-      pa = (kr.addGuest(name,data[name]) for name,key of data)
-      pa.push p
-      Utils.all(pa)
+      p.then ->
+        pa = (kr.addGuest(name,data[name]) for name,key of data)
+        Utils.all(pa)

     KeyRing.new(id).then (kr) ->
       p = kr.commFromSecKey strCommKey.fromBase64()


Add vim-surround to Atom

Add vim-surround to Atom

--- atom/packages.cson
+++ atom/packages.cson
@@ -19,4 +19,5 @@
   "terminal-plus"
   "toggle-quotes"
   "vim-mode"
+  "vim-surround"
 ]


Add quotes around nlobj* tab stops

Add quotes around nlobj* tab stops

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -438,11 +438,11 @@

   'search filter':
     'prefix': 'nlobjSearchFilter'
-    'body': 'nlobjSearchFilter(${1:fieldname}, ${2:null}, \'${3:is}\', ${4:value})$5'
+    'body': 'nlobjSearchFilter(\'${1:fieldname}\', ${2:null}, \'${3:is}\', ${4:value})$5'

   'search column':
     'prefix': 'nlobjSearchColumn'
-    'body': 'nlobjSearchColumn(${1:fieldname})$2'
+    'body': 'nlobjSearchColumn(\'${1:fieldname}\'$2)$3'

   'get value':
     'prefix': 'getValue'


started work on themes landing page

started work on themes landing page

--- server/routes/indicators.coffee
+++ server/routes/indicators.coffee
@@ -1,14 +1,15 @@
 Indicator = require('../models/indicator').model
+Theme = require('../models/theme').model
 _ = require('underscore')
 async = require('async')
 csv = require('express-csv')

 exports.index = (req, res) ->
-  Indicator.find( (err, indicators)->
+  Theme.getFatThemes( (err, themes) ->
     if err?
       console.error err
-      return res.render(500, "Error fetching the indicators")
-    res.render "indicators/index", indicators: indicators
+      return res.render(500, "Error fetching the themes")
+    res.render "indicators/index", themes: themes
   )

 exports.show = (req, res) ->


make command names local array

make command names local array

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee
@@ -1,8 +1,6 @@
 define [
 	"./top_hundred_snippets"
 ], (topHundred) ->
-
-	commandNames = (snippet.caption.match(/\w+/)[0] for snippet in topHundred)

 	class Parser
 		constructor: (@doc, @prefix) ->
@@ -96,6 +94,10 @@
 		constructor: (@metadataManager) ->

 		getCompletions: (editor, session, pos, prefix, callback) ->
+			console.log '>> this is running!'
+			commandNames = (
+				snippet.caption.match(/\w+/)[0] for snippet in topHundred
+			)
 			packages = @metadataManager.getAllPackages()
 			packageCommands = []
 			for pkg, snippets of packages


server: don't customize allow handler

server: don't customize allow handler

--- servers/lib/server/index.coffee
+++ servers/lib/server/index.coffee
@@ -61,9 +61,7 @@
 # routes ordered as before no particular structure

 app.post '/-/teams/validate-token'               , require './handlers/checktoken'
-app.post '/-/teams/allow'                        , setCrsfToken, (req, res) ->
-  res.header 'Access-Control-Allow-Origin', 'http://www.koding.com'
-  res.json { token: req.pendingCookies._csrf }
+app.post '/-/teams/allow'                        , setCrsfToken, (req, res) -> res.json { token: req.pendingCookies._csrf }
 app.post '/-/teams/create'                       , csrf,   require './handlers/createteam'
 app.post '/-/teams/join'                         , csrf,   require './handlers/jointeam'
 app.post '/-/teams/early-access'                 , require './handlers/earlyaccess'


removed optout class

removed optout class

--- servers/testhelper/index.coffee
+++ servers/testhelper/index.coffee
@@ -97,36 +97,6 @@
       target[prop] = source[prop]

   return target
-
-
-class OptoutHandlerHelper
-
-  @generateOptoutRequestBody = (opts = {}) ->
-
-    defaultBodyObject =
-      name : generateRandomString()
-
-    deepObjectExtend defaultBodyObject, opts
-
-    return defaultBodyObject
-
-
-  @generateOptoutRequestParams = (opts = {}) ->
-
-    name = opts?.body?.name or generateRandomString()
-
-    url  = generateUrl
-      route : "#{encodeURIComponent name}/Optout"
-
-    body = OptoutHandlerHelper.generateOptoutRequestBody()
-
-    params               = { url, body }
-    defaultRequestParams = generateDefaultRequestParams params
-    requestParams        = deepObjectExtend defaultRequestParams, opts
-    # after deep extending object, encodes body param to a query string
-    requestParams.body   = querystring.stringify requestParams.body
-
-    return requestParams

 class RecoverHandlerHelper
@@ -563,7 +533,6 @@
   TeamHandlerHelper
   LoginHandlerHelper
   ResetHandlerHelper
-  OptoutHandlerHelper
   RecoverHandlerHelper
   RegisterHandlerHelper
   ValidationHandlerHelper


library: Add concept of handlers for component factories

Allows to map a filepath extension like ".py" to a command
template which can start up new participants implemented in Python.
This will allow adding new components without having to
declare each of them in package.json (under msgflo.commands)

--- src/library.coffee
+++ src/library.coffee
@@ -1,6 +1,9 @@

 fs = require 'fs'
 path = require 'path'
+debug = require('debug')('msgflo:library')
+
+common = require './common'

 replaceMarker = (str, marker, value) ->
   marker = '#'+marker.toUpperCase()
@@ -11,44 +14,88 @@
     str = replaceMarker str, marker, value
   return str

+baseComponentCommand = (config, component, cmd) ->
+  variables = common.clone config.variables
+  componentName = component.split('/')[1]
+  componentName = component if not componentName
+  variables['COMPONENTNAME'] = componentName
+  variables['COMPONENT'] = component
+  return replaceVariables cmd, variables
+
+componentCommandForFile = (config, filename) ->
+  ext = path.extname filename
+  component = path.basename filename, ext
+  cmd = config.handlers[ext]
+  debug 'command for file', filename, ext, Object.keys(config.handlers)
+  return baseComponentCommand config, component, cmd
+
 componentsFromConfig = (config) ->
-  variables = config.variables or {}
-  config.components = {} if not config.components
-
   components = {}
   for component, cmd of config.components
-    componentName = component.split('/')[1]
-    componentName = component if not componentName
-    variables['COMPONENTNAME'] = componentName
-    variables['COMPONENT'] = component
+    components[component] = baseComponentCommand config, component, cmd
+  return components

-    components[component] = replaceVariables cmd, variables
-  return components
+componentsFromDirectory = (directory, config, callback) ->
+  components = {}
+  extensions = Object.keys config.handlers
+  fs.readDir directory, (err, filenames) ->
+    return callback err if err
+    supported = files.filter (f) -> path.extname(f) in extensions
+    unsupported = files.filter (f) -> not (path.extname(f) in extensions)
+    debug 'unsupported component files', unsupported if unsuppported.length
+    for filename in supported
+      components[component] = componentCommandForFile config, filename
+
+    return callback null, components
+
+# TODO: also add msgflo-python
+defaultHandlers =
+  ".yml":     "msgflo-register-foreign --role #ROLE participants/#COMPONENT.yml"
+  ".js":      "msgflo-nodejs --name #ROLE participants/#COMPONENT.js"
+  ".coffee":  "msgflo-nodejs --name #ROLE participants/#COMPONENT.coffee"
+  ".json":    "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"
+  ".fbp":     "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"
+
+languageExtensions =
+  'python': 'py'
+  'coffeescript': 'coffee'
+  'javascript': 'js'
+  'yaml': 'yml'
+
+normalizeConfig = (config) ->
+  config = {} if not config
+  config = config.msgflo if config.msgflo # Migth be under a .msgflo key, for instance in package.json
+
+  config.components = {} if not config.components
+  config.variables = {} if not config.variables
+  config.handlers = {} if not config.handlers
+
+  for k, v of defaultHandlers
+    config.handlers[k] = defaultHandlers[k] if not config.handlers[k]
+
+  return config

 class Library
   constructor: (options) ->
     options.config = JSON.parse(fs.readFileSync options.configfile, 'utf-8') if options.configfile
-    options.config = {} if not options.config
-    options.config = options.config.msgflo if options.config.msgflo
     options.componentdir = 'participants' if not options.componentdir
+    options.config = normalizeConfig options.config
     @options = options

+    # FIXME: also load components from directory
     @components = componentsFromConfig options.config

   addComponent: (name, language, code, callback) ->
-    extensions =
-      'python': 'py'
-      'coffeescript': 'coffee'
-      'javascript': 'js'
-      'yaml': 'yml'
-    ext = extensions[language]
+    debug 'adding component', name, language
+    ext = languageExtensions[language]
     ext = ext or language  # default to input lang for open-ended extensibility
     name = path.basename name # TODO: support multiple libraries?
     filename = path.join @options.componentdir, "#{name}.#{ext}"
-    # TODO: lookup handlers to build command, add to @components
-    fs.writeFile filename, code, (err) ->
+
+    fs.writeFile filename, code, (err) =>
       return callback err if err
-      return callback err
+      @components[name] = componentCommandForFile @options.config, filename
+      return callback null

   componentCommand: (component, role, iips={}) ->
     cmd = @components[component]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -126,7 +126,7 @@
   @name meter
   @methodOf Metered#
   @param {Object} options The meter configuration options
-    borderColor -
+    borderColor - The color of the bo
   ###
   meter: (options={}) ->
     Object.reverseMerge options,


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -18,7 +18,7 @@
 # => Sets up a health meter that will be drawn during the player overlay event

 enemy.meter 'health'
-# => Sets up a health meter that will be drawn during the enemy overlay event
+# => Sets up a health meter that will be drawn during the enemy overlay event. Since

 # now player has all the methods provided by this module
 player.position()


adding createFileExclusively native, for FileOps test

--- src/natives.coffee
+++ src/natives.coffee
@@ -713,7 +713,12 @@
             mode = stats.mode & 511
             true  # TODO: actually use the mode, checking if we're the owner or in group
         #o 'createDirectory(Ljava/lang/File;)Z', (rs, _this, file) ->
-        #o 'createFileExclusively(Ljava/lang/String;)Z', (rs, _this, path) ->
+        o 'createFileExclusively(Ljava/lang/String;Z)Z', (rs, _this, path, arg2) ->
+            #XXX: I have no idea what arg2 is
+            filepath = path.jvm2js_str()
+            return false if stat_file(filepath)?
+            fs.closeSync fs.openSync(filepath, 'w')  # creates an empty file
+            true
         #o 'delete0(Ljava/lang/File;)Z', (rs, _this, file) ->
         o 'getBooleanAttributes0(Ljava/io/File;)I', (rs, _this, file) ->
             filepath = file.get_field rs, 'path'


Double quotes in formatter parameters.

Double quotes in formatter parameters.

Example: text

--- src/parsers.coffee
+++ src/parsers.coffee
@@ -32,7 +32,7 @@
     tokens = []

     for arg in args
-      tokens.push if /^'.*'$/.test arg
+      tokens.push if /^'.*'$|^".*"$/.test arg
         type: @types.primitive
         value: arg.slice 1, -1
       else if arg is 'true'


Align lines of code to show better their relationship

--- src/publish.coffee
+++ src/publish.coffee
@@ -3,9 +3,9 @@
 	# Define a private publish/subscribe/unsubscribe mechanism based on jQuery
 	# trigger/on/off event methods.
 	ps = $({})
-	publish = -> ps.trigger.apply ps, arguments
-	subscribe = -> ps.on.apply ps, arguments
-	unsubscribe = -> ps.off.apply ps, arguments
+	publish     = -> ps.trigger.apply ps, arguments
+	subscribe   = -> ps.on     .apply ps, arguments
+	unsubscribe = -> ps.off    .apply ps, arguments

 	# ### Define a jQuery _publish_ method


changed report array to map

changed report array to map

--- src/reports.coffee
+++ src/reports.coffee
@@ -11,7 +11,7 @@
 # Function Sends the reports

 sendReports = (job, flag, done) ->
-  reportArray = []
+  reportMap = {}
   channelReportMap = {}

   #Select the right subscribers for the report
@@ -22,45 +22,44 @@

   fetchUsers (err, users) ->
     promises = []
-    userCount = 0
+    userCount = ''
     for user in users
       do (user) ->
         deferred = Q.defer()
-
+        userCount = user.email
         metrics.getAllowedChannels user
         .then (result) ->
           innerPromises = []
           for channel in result
             do (channel,channelReportMap) ->
               innerDeferred = Q.defer()
-              if (reportArray[userCount])
+              if (reportMap[userCount])
                 # Do nothing since object already exists
               else
                 # Create the object
-                reportArray[userCount] =
+                reportMap[userCount] =
                   email: user.email
                   data: []

               # If report has been fetched get it from the map
               if channelReportMap[channel._id]
-                reportArray[userCount].data.push channelReportMap[channel._id]
+                reportMap[userCount].data.push channelReportMap[channel._id]
               else
               # Fetch the report and add it to the map
                 fetchChannelReport channel,user,flag, (item) ->
-                  reportArray[userCount].data.push item
+                  reportMap[userCount].data.push item
                   channelReportMap[channel._id] = item
                   logger.info item
                   innerDeferred.resolve()
               innerPromises.push innerDeferred.promise

           (Q.all innerPromises).then ->
-            userCount++
             deferred.resolve()

         promises.push deferred.promise

     (Q.all promises).then ->
-      for report in reportArray
+      for report in reportMap
         if flag == 'dailyReport'
           report.type = 'Daily'
         else


Update victory.coffee

Update victory.coffee

added additional victory images.

--- src/victory.coffee
+++ src/victory.coffee
@@ -15,6 +15,17 @@

 victory = [
   "http://thejointblog.com/wp-content/uploads/2013/04/victory.jpg",
+  "http://www.quickmeme.com/img/ea/ea4671998341d9fbb6f7815394b49cb2890a50ac80b62802fb021c147c068d8e.jpg",
+  "http://cdn-media.hollywood.com/images/l/victory_620_080712.jpg",
+  "http://cf.chucklesnetwork.agj.co/items/5/5/9/6/0/one-does-not-simply-declare-victory-but-i-just-did.jpg",
+  "http://www.artschoolgeek.com/wp-content/uploads/2014/06/h7E4B96E6.jpeg",
+  "http://t.qkme.me/3qlspk.jpg",
+  "http://img.pandawhale.com/86036-victory-dance-gif-Despicable-M-EPnS.gif",
+  "http://1.bp.blogspot.com/-rmJLwpPevTg/UOEBgVNiVFI/AAAAAAAAFFY/-At3Z_DzBbw/s1600/dancing+charlie+murphy+animated+gif+victory+dance.gif",
+  "http://www.gifbin.com/bin/20048442yu.gif",
+  "http://www.quickmeme.com/img/30/300ace809c3c2dca48f2f55ca39cbab24693a9bd470867d2eb4e869c645acd42.jpg",
+  "http://jeffatom.files.wordpress.com/2013/09/winston-churchill-says-we-deserve-victory.jpg",
+  "http://i.imgur.com/lmmBt.gif",
   "http://danceswithfat.files.wordpress.com/2011/08/victory.jpg",
   "http://stuffpoint.com/family-guy/image/56246-family-guy-victory-is-his.gif",
   "http://thelavisshow.files.wordpress.com/2012/06/victorya.jpg",


Port model parameters and GBM output

Port model parameters and GBM output

--- src/ext/modules/routines.coffee
+++ src/ext/modules/routines.coffee
@@ -21,6 +21,10 @@
     description: 'Make a prediction'
     icon: 'bolt'

+parseNaNs = (array) ->
+  for element in array
+    if element is 'NaN' then undefined else element
+
 computeTruePositiveRate = (cm) ->
   [[tn, fp], [fn, tp]] = cm
   tp / (tp + fn)
@@ -261,50 +265,45 @@

   inspectModelParameters = (model) -> ->
     parameters = model.parameters
-    variables = [
-      Flow.Data.Variable 'label', TString
-      Flow.Data.Variable 'type', TString
-      Flow.Data.Variable 'level', TString
-      Flow.Data.Variable 'actual_value', TObject
-      Flow.Data.Variable 'default_value', TObject
+
+    attrs = [
+      [ 'label', TString ]
+      [ 'type', TString ]
+      [ 'level', TString ]
+      [ 'actual_value', TObject ]
+      [ 'default_value', TObject ]
     ]

-    Record = Flow.Data.Record variables
-    rows = new Array parameters.length
-    for parameter, i in parameters
-      rows[i] = row = new Record()
-      for variable in variables
-        row[variable.label] = parameter[variable.label]
-
-    Flow.Data.Table
-      label: 'parameters'
+    vectors = for [ name, type ] in attrs
+      data = new Array parameters.length
+
+      for parameter, i in parameters
+        data[i] = parameter[name]
+
+      switch type
+        when TString
+          window.plot.createFactor name, type, data
+        when TObject
+          window.plot.createList name, data, stringify
+
+    window.plot.createFrame 'parameters', vectors, (sequence parameters.length), null,
       description: "Parameters for model '#{model.key.name}'" #TODO frame key
-      variables: variables
-      rows: rows
-      meta:
-        origin: "getModel #{stringify model.key.name}"
+      origin: "getModel #{stringify model.key.name}"

   inspectGBMModelOutput = (model) -> ->
     output = model.output
-    variables = [
-      Flow.Data.Variable 'tree', TNumber
-      Flow.Data.Variable 'mse_train', TObject
-      Flow.Data.Variable 'mse_valid', TObject
+
+    size = output.mse_train.length
+
+    vectors = [
+      window.plot.createVector 'tree', TNumber, (sequence size)
+      window.plot.createVector 'mse_train', TNumber, parseNaNs output.mse_train
+      window.plot.createVector 'mse_valid', TNumber, parseNaNs output.mse_valid
     ]

-    Record = Flow.Data.Record variables
-    rows = new Array output.mse_train.length
-    for mse_train, i in output.mse_train
-      rows[i] = new Record i, mse_train, output.mse_valid[i]
-
-    Flow.Data.Table
-      label: 'output'
+    window.plot.createFrame 'output', vectors, (sequence size), null,
       description: "Output for GBM model '#{model.key.name}'"
-      variables: variables
-      rows: rows
-      meta:
-        origin: "getModel #{stringify model.key.name}"
-
+      origin: "getModel #{stringify model.key.name}"

   inspectKMeansModelOutput = (model) -> ->
     output = model.output
@@ -683,9 +682,8 @@
       [ 'cardinality', TNumber ]
     ]

-    dataset = {}
     for [ name, type ] in attrs
-      data = dataset[name] = new Array frameColumns.length
+      data = new Array frameColumns.length
       switch name
         when 'min'
           for column, i in frameColumns
@@ -700,12 +698,11 @@
           for column, i in frameColumns
             data[i] = column[name]

-    vectors = for [ name, type ] in attrs
       switch type
         when TString
-          window.plot.createFactor name, type, dataset[name]
+          window.plot.createFactor name, type, data
         when TNumber
-          window.plot.createVector name, type, dataset[name], identity
+          window.plot.createVector name, type, data

     window.plot.createFrame tableLabel, vectors, (sequence frameColumns.length), null,
       description: "A list of #{tableLabel} in the H2O Frame."
@@ -825,8 +822,8 @@

       vectors = [
         window.plot.createFactor 'interval', TString, intervalData
-        window.plot.createVector 'width', TNumber, widthData, identity
-        window.plot.createVector 'count', TNumber, countData, identity
+        window.plot.createVector 'width', TNumber, widthData
+        window.plot.createVector 'count', TNumber, countData
       ]

       window.plot.createFrame 'distribution', vectors, (sequence binCount), null,
@@ -853,8 +850,8 @@

       vectors = [
         window.plot.createFactor 'characteristic', TString, characteristicData
-        window.plot.createVector 'count', TNumber, countData, identity
-        window.plot.createVector 'percent', TNumber, percentData, identity
+        window.plot.createVector 'count', TNumber, countData
+        window.plot.createVector 'percent', TNumber, percentData
       ]

       window.plot.createFrame 'characteristics', vectors, (sequence characteristicData.length), null,
@@ -875,12 +872,12 @@

       vectors = [
         window.plot.createFactor 'column', TString, [ columnName ]
-        window.plot.createVector 'mean', TNumber, [ mean ], identity
-        window.plot.createVector 'q1', TNumber, [ q1 ], identity
-        window.plot.createVector 'q2', TNumber, [ q2 ], identity
-        window.plot.createVector 'q3', TNumber, [ q3 ], identity
-        window.plot.createVector 'min', TNumber, [ minimum ], identity
-        window.plot.createVector 'max', TNumber, [ maximum ], identity
+        window.plot.createVector 'mean', TNumber, [ mean ]
+        window.plot.createVector 'q1', TNumber, [ q1 ]
+        window.plot.createVector 'q2', TNumber, [ q2 ]
+        window.plot.createVector 'q3', TNumber, [ q3 ]
+        window.plot.createVector 'min', TNumber, [ minimum ]
+        window.plot.createVector 'max', TNumber, [ maximum ]
       ]

       window.plot.createFrame 'summary', vectors, (sequence 1), null,


First pass at Fusion Tables OAuth.

First pass at Fusion Tables OAuth.

--- src/js/collection-editor.coffee
+++ src/js/collection-editor.coffee
@@ -1,3 +1,13 @@
+google_oauth_parameters_for_fusion_tables =
+  response_type: 'token'
+  client_id: '891199912324.apps.googleusercontent.com'
+  redirect_uri: window.location.href
+  scope: 'https://www.googleapis.com/auth/fusiontables'
+  approval_prompt: 'auto'
+
+google_oauth_url =
+  "https://accounts.google.com/o/oauth2/auth?#{$.param(google_oauth_parameters_for_fusion_tables)}"
+
 build_input_for_valuelist = (valuelist) ->
   select = $('<select>')
   values = $(valuelist).find('value')
@@ -31,9 +41,62 @@
   form = $('<form>').attr('id','collection_form')
   properties = $(collection).find('citeProperty')
   add_property_to_form(property,form) for property in properties
+
+  form.append $('<br>')
+  form.append $('<a>').attr('href',google_oauth_url).append('OAuth Test')
+
+  # test table access
+  if get_cookie 'access_token'
+    $.ajax "https://www.googleapis.com/fusiontables/v1/tables/#{$(collection).attr('class')}?access_token=#{get_cookie 'access_token'}",
+      type: 'GET'
+      dataType: 'json'
+      crossDomain: true
+      error: (jqXHR, textStatus, errorThrown) ->
+        console.log "AJAX Error: #{textStatus}"
+      success: (data) ->
+        console.log data
+
   $('.container').append form

+parse_query_string = (query_string) ->
+  params = {}
+  if query_string.length > 0
+    regex = /([^&=]+)=([^&]*)/g
+    while m = regex.exec(query_string)
+      params[decodeURIComponent(m[1])] = decodeURIComponent(m[2])
+  return params
+
+get_cookie = (key) ->
+  key = key + "="
+  for cookie_fragment in document.cookie.split(';')
+    return cookie_fragment.replace(/^\s+/, '').substring(key.length, cookie_fragment.length) if cookie_fragment.indexOf(key) == 0
+  return null
+
+set_access_token_cookie = (params) ->
+  if params['access_token']?
+    # validate the token per https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken
+    $.ajax "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=#{params['access_token']}",
+      type: 'GET'
+      dataType: 'json'
+      crossDomain: true
+      error: (jqXHR, textStatus, errorThrown) ->
+        console.log "Access Token Validation Error: #{textStatus}"
+      success: (data) ->
+        console.log data
+        access_token_expires = new Date
+        access_token_expires.setTime(access_token_expires.getTime() + params['expires_in']*1000)
+        access_token_cookie = "access_token=#{params['access_token']}; "
+        access_token_cookie += "expires=#{access_token_expires.toGMTString()}; "
+        access_token_cookie += "path=/"
+        console.log 'Wrote access token cookie: ' + access_token_cookie
+        document.cookie = access_token_cookie
+
 $(document).ready ->
+  set_access_token_cookie parse_query_string(location.hash.substring(1))
+  # strip the hash from the URL, as Google will also reject any further queries if it's present
+  history.replaceState(null,'',window.location.href.replace("#{location.hash}",''))
+  if get_cookie 'access_token'
+    console.log 'Read access token cookie: ' + get_cookie 'access_token'
   $.ajax 'capabilities/testedit-capabilities.xml',
     type: 'GET'
     dataType: 'xml'


Trigger autosize update when populating from localStorage

--- src/js/collection-editor.coffee
+++ src/js/collection-editor.coffee
@@ -225,6 +225,7 @@
           history.replaceState(null,'',window.location.href.replace("#{location.hash}","#{location.hash}&#{$(child).attr('id')}=#{localStorage["#{collection}:#{$(child).attr('id')}"]}"))
         else
           $(child).val(localStorage["#{collection}:#{$(child).attr('id')}"])
+  autosize.update($('textarea'))

 # populate collection form values from a given URN
 load_collection_form_from_urn = (loaded_urn) ->


Nicer error when no composer.json #23

Nicer error when no composer.json #23

--- lib/services/php-proxy.coffee
+++ lib/services/php-proxy.coffee
@@ -74,7 +74,7 @@
     data.composer = JSON.parse(fs.readFileSync(path, options))
     return data.composer

-  throw new Error("Unable to find composer.json file or to open it.")
+  window.alert 'Unable to find composer.json file or to open it. The plugin will not work as expected. It only works on composer project'

 ###*
  * Throw a formatted error


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -14,6 +14,8 @@
       this.each (item) ->
         item.I[attr] = value

+
+
   ###*
   Get a selection of GameObjects that match the specified selector criteria. The selector language
   can select objects by id, class, or attributes. Note that this method always returns an Array,


tomdoc and some cleanup

tomdoc and some cleanup

--- src/google-calendar.coffee
+++ src/google-calendar.coffee
@@ -1,44 +1,29 @@
 # Description:
-#   Say Hi to Hubot.
+#   Get google calendar information from Hubot
 #
 # Dependencies:
-#   None
+#   "coffee-script": "~1.6",
+#   "moment": "~2.9.0",
+#   "moment-timezone": "~0.3.0"
+#   "hubot-googleapis": "~0.2.0"
 #
 # Configuration:
-#   None
+#   GOOGLE_API_CLIENT_ID      # hubot-googleapi
+#   GOOGLE_API_CLIENT_SECRET  # hubot-googleapi
+#   GOOGLE_API_SCOPES         # hubot-googleapi
 #
 # Commands:
-#   hubot hello - "hello!"
-#   hubot orly - "yarly"
+#   hubot gcal me - "Retrieves the events for the immediate future (defaults to a day)."
+#   hubot gcal calendar some.calendar@example.com - "Sets the calendar for the current user."
+#   hubot gcal look 10 days ahead - "Calls to `gcal me` will return 10 days worth of events."
+#   hubot gcal timezone America/Phoenix - "Sets the timezone for the current user."
 #
 # Author:
-#   tombell
-
+#   hubot-me

 module.exports = (robot) ->

   require('moment-timezone')
-  # CronJob = require('cron').CronJob
-  #
-  # checkCalendars = new CronJob("0,5,10,15,20,25,30,35,40,45,50,55  * * * *", ()->
-  #   # login to google calendars, refresh token if necessary
-  #   #
-  #   # for every user that is currently showing as available and has enabled gcal,
-  #   #
-  #   # robot.emit "googleapi:request",
-  #   #   service: "analytics"
-  #   #   version: "v3"
-  #   #   endpoint: "management.profiles.list"
-  #   #   params:                               # parameters to pass to API
-  #   #   accountId: '~all'
-  #   #   webPropertyId: '~all'
-  #   #   callback: (err, data)->               # node-style callback
-  #   #     return console.log(err) if err
-  #   #     console.log data.items.map((item)->
-  #   #     "#{item.name} - #{item.websiteUrl}"
-  #   #     ).join("\n")
-  #   #
-  #   #   update their status.

   robot.respond /gcal calendar (.*)/i, (msg)->
     console.log msg
@@ -62,11 +47,12 @@

     gcal[userId].id        = userId
     gcal[userId].daysAhead = daysAhead
-    robot.reply "OK, your calendar, `gcal me` will show the next #{daysAhead} days."
+    msg.reply "OK, your calendar, `gcal me` will show the next #{daysAhead} days."

   # TODO: robot.respond /gcal auto/ # enable/disable auto away behavior
   # TODO: robot.respond /gcal whereabouts/ # where is everyone
   # TODO: for recurring, make sure I'm showing the next occurrence, and all after that
+  # TODO: set the cron interval for auto-away status updates

   # make every effort to return the correct timezone - http://momentjs.com/timezone/
   robot.respond /gcal timezone (.*)/, (msg)->
@@ -77,7 +63,7 @@

     gcal[userId].id       = userId
     gcal[userId].timeZone = timeZone
-    robot.reply "OK, your calendar, `gcal me` will reflect your timzone, #{timeZone}."
+    msg.reply "OK, your calendar, `gcal me` will reflect your timzone, #{timeZone}."

   robot.respond /gcal me/i, (msg)->


Update TODO

Update TODO

--- admin/static/coffee/app.coffee
+++ admin/static/coffee/app.coffee
@@ -14,6 +14,7 @@
 progress_interval_value = 5000
 progress_short_interval = 1000

+#TODO Duplicate this function, and remove element not found in case of a call to /ajax
 apply_to_collection = (collection, collection_data) ->
     for id, data of collection_data
         if data isnt null
@@ -27,7 +28,6 @@
             else
                 data.id = id
                 collection.add(new collection.model(data))
-            #TODO remove not found object
         else
             if collection.get(id)
                 collection.remove(id)


[CLEANUP] Revert workaround in 6e6595f988ebe3421325ffad77a0d45884f03827

Doesn't seem to be necessary with latest version of bluebird

--- src/liquid/variable.coffee
+++ src/liquid/variable.coffee
@@ -41,12 +41,8 @@
   render: (context) ->
     return '' unless @name?

-    UNDEFINED = {}
-
-    mapper = (output, filter) =>
+    reducer = (output, filter) =>
       filterArgs = filter[1].map (a) -> context.get a
-
-      output = undefined if output is UNDEFINED

       Promise
       .join(output, filterArgs...)
@@ -60,10 +56,8 @@
     Promise
     .cast(context.get(@name))
     .then (value) =>
-      value = UNDEFINED unless value?
-
       Promise
-      .reduce(@filters, mapper, value)
+      .reduce(@filters, reducer, value)
       .then (value) =>
         if value instanceof Liquid.Drop
           if typeof value.toString == "function"


Allow passing container element to View.prototype.bindViews

--- src/marbles/view.js.coffee
+++ src/marbles/view.js.coffee
@@ -80,9 +80,9 @@

   initialize: =>

-  bindViews: =>
+  bindViews: (container = (@container?.el || @el)) =>
     @_child_views ?= {}
-    _.each Marbles.DOM.querySelectorAll('[data-view]', (@container?.el || @el)), (el) =>
+    _.each Marbles.DOM.querySelectorAll('[data-view]', container), (el) =>
       view_class_names = Marbles.DOM.attr(el, 'data-view').split(/\s+/)

       for view_class_name in view_class_names


Save game on every score change

Save game on every score change

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -62,8 +62,6 @@
       if $scope.countdown && $scope.timeout > 0
         timeout_promise = $timeout heartbeat, 1000
       else if $scope.timeout == 0
-        $scope.game.update () ->
-          console.log 'Saved game', $scope.game
         alert "Time is up!"

     $scope.startTimer = () ->
@@ -94,3 +92,6 @@
         $speechRecognition.speak(team.name + '! ' + team["player#{n}"] + '!')
     $scope.clearBuzzed = () ->
       $scope.buzzedFirst = undefined
+
+    $scope.$watchCollection 'game', () ->
+      $scope.game.update()


Convert expected_datetime to Date object

Required for input[date] directive in triage form

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -66,6 +66,8 @@
     $scope.filterStatus()
   .controller 'RequestTriageCtrl', ($scope, $http, $routeParams, $filter, Request, Waterpoint, flash) ->
     Request.get id: $routeParams.id, (request) ->
+      if request.expected_datetime
+        request.expected_datetime = new Date(request.expected_datetime)
       $scope.request = request
       Waterpoint.get where: {wpt_code: request.attribute.waterpoint_id}, (waterpoint) ->
         $scope.waterpoint = waterpoint._items[0]


Update local waterpoint data after successful triage

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -80,6 +80,8 @@
         console.log data, status, headers, config
         if status == 200 and data._status == 'OK'
           flash.success = 'Waterpoint successfully updated!'
+          for k, v of $scope.triage
+            $scope.waterpoint[k] = v
         if status == 200 and data._status == 'ERR'
           for field, message of data._issues
             flash.error = "#{field}: #{message}"


Fast radio port and preliminary receive

Fast radio port and preliminary receive

--- app/scripts/robot/radio.coffee
+++ app/scripts/robot/radio.coffee
@@ -98,6 +98,15 @@
       _init = false
       return true

+    radio.obj_receiver = (data) ->
+      _ndl3Radio.on 'object', (obj)=>
+        data[objects] = [] unless data[objects]?
+        data[objects].push obj
+
+    radio.config_receiver = ->
+      raw = typpo.read 'config_port_data'
+      device_list = raw.get_slot 'device_list'
+
     radio.onReceive = (channel, callback) ->
       return false unless _init # exit if we're not initialized

@@ -111,7 +120,11 @@

       if _ndl3Radio
         object._channel = channel
-        _ndl3Radio.send(object)
+        prefix = channel.substring 0, 2
+        if prefix == 'gp'
+          _ndl3Radio.send(object, 'fast')
+        else
+          _ndl3Radio.send(object)
       else
         console.log "_ndl3Radio not defined, not sending"


Fix servers parsing

Fix servers parsing

--- app/utils/parse-servers.coffee
+++ app/utils/parse-servers.coffee
@@ -7,11 +7,12 @@
       .replace('_', '.')
       .toLowerCase()

-    [ host, port, password ] = value.split ':'
-    port = parseInt port
+    for server in value.split ','
+      [ host, port, password ] = server.split ':'
+      port = parseInt port

-    return false unless tag or host or port or password
+      return false unless tag or host or port or password

-    servers.push { tag, host, port, password }
+      servers.push { tag, host, port, password }

   return servers


PackageFetcher: Factor out dependency handling

--- src/package-fetcher.coffee
+++ src/package-fetcher.coffee
@@ -57,20 +57,7 @@

 PackageFetcher.prototype._fetchFromRegistry = (name, spec, registry) ->
   handlePackage = (pkg) =>
-    # !!! TODO: Handle optionalDependencies, peerDependencies
-    deps = pkg.dependencies or {}
-    registry = makeNewRegistry registry, pkg.registry if 'registry' of pkg
-    for nm, dep of deps
-      # !!! Seeming conflict between CommonJS Registry spec and npm on the one
-      # hand and CommonJS Package spec on the other. Package spec allows deps
-      # to be an object of options (e.g. "ssl": { "gnutls": "1.2.3", "openssl": "2.3.4" })
-      # but npm only allows simple strings and Registry only allows version, registry
-      # objects in addition to simple strings. Ignoring package spec until/unless a
-      # registry entry in the wild shows up with that format
-      if dep instanceof Object
-        @fetch nm, dep.version, makeNewRegistry registry, dep.registry
-      else
-        @fetch nm, dep, registry
+    @_handleDeps pkg, registry
     unless pkg.dist.tarball?
       @emit 'error', "Could not find supported dist type for #{pkg.name}@#{pkg.version} in #{util.inspect pkg.dist}", name, spec
     else
@@ -108,6 +95,22 @@
 PackageFetcher.prototype._fetchFromGit = (name, spec, parsed) ->
   @emit 'error', "git dependencies not yet supported", name, spec

+PackageFetcher.prototype._handleDeps = (pkg, registry) ->
+  # !!! TODO: Handle optionalDependencies, peerDependencies
+  deps = pkg.dependencies or {}
+  registry = makeNewRegistry registry, pkg.registry if 'registry' of pkg
+  for nm, dep of deps
+    # !!! Seeming conflict between CommonJS Registry spec and npm on the one
+    # hand and CommonJS Package spec on the other. Package spec allows deps
+    # to be an object of options (e.g. "ssl": { "gnutls": "1.2.3", "openssl": "2.3.4" })
+    # but npm only allows simple strings and Registry only allows version, registry
+    # objects in addition to simple strings. Ignoring package spec until/unless a
+    # registry entry in the wild shows up with that format
+    if dep instanceof Object
+      @fetch nm, dep.version, makeNewRegistry registry, dep.registry
+    else
+      @fetch nm, dep, registry
+
 fetchUrl = do ->
   cache = {}
   (u, callback) ->


Remember mounted VMs.

Remember mounted VMs.

--- client/landing/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -77,16 +77,37 @@
       callback?()

     if vmNames then mountVms vmNames
-    else KD.remote.api.JVM.fetchVmsByContext {}, (err, vms)->
-      return callback? err  if err
-      if not vms or vms.length is 0
-        vms = [(KD.getSingleton 'vmController').getDefaultVmName()]
-      mountVms vms
+    else
+      groupSlug  = KD.singletons.groupsController.getGroupSlug()
+      groupSlug ?= 'koding'
+      appStorage = new AppStorage "vm-list", "1.0"
+      appStorage.fetchValue groupSlug, (vms)->
+        vms or= []
+        if vms.length > 0
+          mountVms vms
+        else
+          KD.remote.api.JVM.fetchVmsByContext {}, (err, vms)->
+            return callback? err  if err
+            if not vms or vms.length is 0
+              vms = [(KD.getSingleton 'vmController').getDefaultVmName()]
+            mountVms vms

   getVmNode:(vmName)->
     return null  unless vmName
     for path, vmItem of @treeController.nodes  when vmItem.data?.type is 'vm'
       return vmItem  if vmItem.data.vmName is vmName
+
+  saveMountState:(vmName, state)->
+    groupSlug  = KD.singletons.groupsController.getGroupSlug()
+    groupSlug ?= 'koding'
+    appStorage = new AppStorage "vm-list", "1.0"
+    appStorage.fetchValue groupSlug, (vms)->
+      vms or= []
+      if state and vmName not in vms
+        vms.push vmName
+      else if not state and vmName in vms
+        vms.splice vms.indexOf(vmName), 1
+      appStorage.setValue groupSlug, vms

   mountVm:(vm, fetchContent = yes)->
     return unless KD.isLoggedIn()
@@ -97,6 +118,8 @@

     if vmItem = @getVmNode vmName
       return warn "VM #{vmName} is already mounted!"
+
+    @saveMountState vmName, yes

     @vms.push FSHelper.createFile
       name   : "#{path}"
@@ -112,6 +135,8 @@
   unmountVm:(vmName)->
     return unless KD.isLoggedIn()
     return warn 'No such VM!'  unless vmItem = @getVmNode vmName
+
+    @saveMountState vmName, no

     if vmItem
       @stopWatching vmItem.data.path


sitewide: content-panel transitions improved

--- client/landing/app/MainApp/maincontroller/mainviews/contentpanel.coffee
+++ client/landing/app/MainApp/maincontroller/mainviews/contentpanel.coffee
@@ -67,10 +67,8 @@
     @state = 'develop'

   adjustForSocial:->
-    @once 'transitionend', =>
-      offset = parseInt(@$().css("left"),10)
-      @setWidth @windowController.winWidth - offset - @chatMargin
-
+    offset = if @windowController.winWidth < 768 then 50 else 160
+    @setWidth @windowController.winWidth - offset - @chatMargin
     @setClass 'social'
     @state = 'social'


Snail, IF fix

Snail, IF fix

--- assets/javascripts/game.coffee
+++ assets/javascripts/game.coffee
@@ -477,7 +477,7 @@

         setWeeding2:
           _onEnter: ->
-            @message = "\nPlease note the following:\n" + "   A. WEEDS COMPETE VIGOROUSLY WITH RICE AND LOWER\n" + "      THE POTENTIAL YIELD.\n" + "   B. WHEN A HAND WEEDING IS DONE IN COMBINATION WITH\n" + "      ANOTHER METHOD, THE HAND WEEDING IS USUALLY DONE\n" + "      LAST (I.E. '31', NOT '13'!)\n" + "   C. if YOU DESIRE NO WEEDING, ENTER 0 (ZERO).\n" + "\n" + "What weeding method(s) do you wish to use\n"
+            @message = "\nPlease note the following:\n" + "   A. WEEDS COMPETE VIGOROUSLY WITH RICE AND LOWER\n" + "      THE POTENTIAL YIELD.\n" + "   B. WHEN A HAND WEEDING IS DONE IN COMBINATION WITH\n" + "      ANOTHER METHOD, THE HAND WEEDING IS USUALLY DONE\n" + "      LAST (I.E. '31', NOT '13'!)\n" + "   C. IF YOU DESIRE NO WEEDING, ENTER 0 (ZERO).\n" + "\n" + "What weeding method(s) do you wish to use\n"
             return

           input: (line) ->
@@ -490,7 +490,7 @@

         setMolluscicide:
           _onEnter: ->
-            @snailAttacks++
+            @data.snailAttacks++
             @message = "\n" + "Oh my goodness! DURING THE WEED SURVEY MANY BRIGHT RED EGG\n" + "MASSES OF THE GOLDEN SNAIL WERE SEEN IN YOUR FIELDS. If you wish\n" + "you may spray MOLLUSCICIDE or remove the snails and eggs by\n" + "hand. You may also choose to do nothing.\n" + "\n" + "DO YOU WISH TO SPRAY A MOLLUSCICIDE?\n"
             return

@@ -508,7 +508,7 @@

         setHandPickSnails:
           _onEnter: ->
-            @snailAttacks++
+            @data.snailAttacks++
             @message = "\nDO YOU WISH TO HAND PICK YOUR FIELDS?\n"
             return

@@ -516,7 +516,7 @@
             response = @getYesNo(line)
             unless @errorFlag
               if response
-                @snailAttacks = 0
+                @data.snailAttacks = 0
                 @data.costs["molluscicide"] = 0
                 @data.hours["molluscicide"] = 60
               @transition "setWeedingStart"
@@ -599,7 +599,7 @@

             if @data.seedType is "IR64"
               r = Math.floor(Math.random() * 4)/100.0    # IR64 IS QUITE RESISTANT
-              @message += "LOOKS AS if BACTERIAL BLIGHT AND BLAST HAVE CAUSED ABOUT\n"
+              @message += "LOOKS AS IF BACTERIAL BLIGHT AND BLAST HAVE CAUSED ABOUT\n"
             else
               r = Math.floor(Math.random() * 13)/100.0    # local rice not resistant
               @message += "UNFORTUNATELY, GRASSY STUNT AND TUNGRO HAVE CAUSED ABOUT\n"
@@ -664,9 +664,9 @@
               @message += """

                 Brown plant hopper IS A VERY COMMON INSECT PEST. LOSS OF CROP
-                MAY BE ANYWHERE FROM 0% TO 15% if INSECTICIDE IS USED
+                MAY BE ANYWHERE FROM 0% TO 15% IF INSECTICIDE IS USED
                 AS SOON AS INFESTATION IS DISCOVERED (DEPENDING UPON DEGREE
-                OF INFESTATION). However, if CONTROLS ARE NOT USED, LOSSES
+                OF INFESTATION). However, IF CONTROLS ARE NOT USED, LOSSES
                 MAY RUN AS HIGH AS 30%. ON A SLIDING SCALE FROM 0 TO 10:

                 0 (NONE)...............5 (AVERAGE)..............10 (SEVERE)
@@ -736,7 +736,7 @@
           input: (line) ->
             response = @getYesNo(line)
             unless @errorFlag
-              r3 = (Math.random() * 6) + 1
+              r3 = Math.floor(Math.random() * 6) + 1
               @message = ""
               if response
                 if @data.runCount <= 4
@@ -1278,7 +1278,7 @@
             @message = """
               WHICH FACTOR DO YOU WISH TO CHANGE

-              ENTER '0' (ZERO) if YOU DESIRE NOT TO CHANGE ANYTHING.
+              ENTER '0' (ZERO) IF YOU DESIRE NOT TO CHANGE ANYTHING.
               OTHERWISE ENTER a number from the list below.
               1   'SIZE OF FARM'
               2   'COST OF OUTSIDE LABOR'


now you can 'hubot throw new InvalidArgumentException'

--- src/scripts/invalid.coffee
+++ src/scripts/invalid.coffee
@@ -27,5 +27,5 @@
 ]

 module.exports = (robot) ->
-  robot.hear /invalid arg/i, (msg) ->
+  robot.hear /invalid ?arg/i, (msg) ->
     msg.send msg.random invalids


add docs

add docs

--- src/scripts/invalid.coffee
+++ src/scripts/invalid.coffee
@@ -1,3 +1,7 @@
+# Display a random 'Your Argument is Invalid' image.
+#
+# invalid arg - supply an important counter-point to questionable arguments
+#
 invalids = [
   "http://s3.amazonaws.com/kym-assets/photos/images/newsfeed/000/007/517/my_hat_is_made_of_eggs.jpg",
   "http://s3.amazonaws.com/kym-assets/photos/images/newsfeed/000/019/719/NO5E65R2PRHVF7WAVBJNK7YHIYXLJEEJ.jpeg",


Bumping the rundeck plugin to make it spit a better message for the rundeck trigger job

--- src/scripts/rundeck.coffee
+++ src/scripts/rundeck.coffee
@@ -59,6 +59,7 @@
     parser = new Parser()

     @robot.http("#{@baseUrl}/#{url}").headers(@headers).get() (err, res, body) =>
+      console.log "#{@baseUrl}/#{url}"
       if err?
         @logger.error JSON.stringify(err)
       else
@@ -236,7 +237,7 @@
       rundeck = new Rundeck(robot, url, token)
       rundeck.jobs(project).run name, args, (job, results) ->
         if job
-          msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
+          msg.send "Successfully triggered a run for the job: #{name}"
         else
           msg.send "Could not execute rundeck job \"#{name}\"."


Makes 'me' optional

Makes 'me' optional

--- src/scripts/shorten.coffee
+++ src/scripts/shorten.coffee
@@ -3,13 +3,13 @@
 # (bitly|shorten) me <url> - Shorten the URL using bit.ly

 module.exports = (robot) ->
-  robot.respond /(bitly|shorten) me (.+)$/, (msg) ->
+  robot.respond /(bitly|shorten) (me)? (.+)$/, (msg) ->
     msg
       .http("http://api.bitly.com/v3/shorten")
       .query
         login: process.env.HUBOT_BITLY_USERNAME
         apiKey: process.env.HUBOT_BITLY_API_KEY
-        longUrl: msg.match[2]
+        longUrl: msg.match[3]
         format: "json"
       .get() (err, res, body) ->
         response = JSON.parse body


Update master

Update master

--- src/scripts/storage.coffee
+++ src/scripts/storage.coffee
@@ -3,7 +3,7 @@
 #
 # Commands:
 #  hubot show users - Display all users that hubot knows about
-#  hubot show storage - Display the contents that are persisted in redis
+#  hubot show storage - Display the contents that are persisted in the brain

 Util = require "util"


Revise for json change in api

Revise for json change in api

it now comes back like
```json
{"id": 384, "thought":"If they ever have a haunted house for dogs, I think a good display would be a bathtub full of soapy water."}
```

--- src/scripts/thought.coffee
+++ src/scripts/thought.coffee
@@ -26,4 +26,4 @@
             .get() (error, response, body) ->
                 results = JSON.parse body
                 # passes back the complete reponse
-                msg.send results.thought.thought+                msg.send results.thought


Added support for Bigrams/Phrases

Added support for Bigrams/Phrases

--- src/scripts/wordnik.coffee
+++ src/scripts/wordnik.coffee
@@ -11,6 +11,7 @@
 #   hubot define me <word> - Grabs a dictionary definition of a word.
 #   hubot pronounce me <word> - Links to a pronunciation of a word.
 #   hubot spell me <word> - Suggests correct spellings of a possible word.
+#   hubot bigram me <word> - Grabs the most frequently used bigram phrases containing this word
 #
 # Notes:
 #   You'll need an API key from http://developer.wordnik.com/
@@ -73,6 +74,24 @@
         list = wordinfo.suggestions.join(', ')
         msg.send "Suggestions for \"#{word}\": #{list}"

+  # Bigrams
+  robot.respond /bigram( me)? (.*)/i, (msg) ->
+    word = msg.match[2]
+
+    fetch_wordnik_resource(msg, word, 'phrases', {}) (err, res, body) ->
+      phrases = JSON.parse(body)
+
+      if phrases.length == 0
+        msg.send "No bigrams for \"#{word}\" found."
+      else
+        reply = "Bigrams for \"#{word}\":\n"
+
+        phrases = phrases.forEach (phrase) ->
+          if phrase.gram1 != undefined and phrase.gram2 != undefined
+            reply += "#{phrase.gram1} #{phrase.gram2}\n"
+
+        msg.send reply
+
 fetch_wordnik_resource = (msg, word, resource, query, callback) ->
   # FIXME prefix with HUBOT_ for
   if process.env.WORDNIK_API_KEY == undefined


add another repeat_until_done option (for removing blobs)

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -2775,9 +2775,13 @@

     # Free space in database used by blobs that have already been uploaded to gcloud. Once done,
     # these blobs get served from gcloud.
+    #
+    #   db._error_thresh = 1e6; db.free_uploaded_blobs(repeat_until_done:true, cb:done())
+    #
     free_uploaded_blobs: (opts) =>
         opts = defaults opts,
-            limit : required  # do this many of them
+            limit : 10000  # do this many of them
+            repeat_until_done : false
             cb    : required
         dbg = @dbg("free_uploaded_blobs()")
         dbg()
@@ -2794,7 +2798,15 @@
                 dbg("now deleting blob field on #{v.length} blobs")
                 v = (x.id for x in v)
                 @table('blobs').getAll(v...).replace(@r.row.without('blob')).run(cb)
-        ], opts.cb)
+        ], (err) =>
+            if err
+                opts.cb?(err)
+            else if opts.repeat_until_done and v?.length < opts.limit
+                @free_uploaded_blobs(opts)
+            else
+                opts.cb?()
+
+        )

     ###
     # User queries


webapp/terminal: hotfix for #1291 (creating new terminal) -- also fix the opts specification

--- src/smc-util/client.coffee
+++ src/smc-util/client.coffee
@@ -510,7 +510,7 @@
         opts = defaults opts,
             timeout    : DEFAULT_TIMEOUT # how long until give up on getting a new session
             type       : "console"   # only "console" supported
-            params     : undefined   # extra params relevant to the session
+            params     : required    # must include {path:?, filename:?}
             project_id : required
             cb         : required    # cb(error, session)  if error is defined it is a string


Revert "Stop propagation on the 'load more' link in case app is doing their own thing"

This reverts commit 93000886b8a69aa9de4852464fb01f7f04f09cba.

--- client/boot.coffee
+++ client/boot.coffee
@@ -58,7 +58,7 @@

   # Listen for any 'Load More' clicks
   $('body').on 'click', '.load-more', (e) ->
-    e.preventDefault() and e.stopPropagation()
+    e.preventDefault()
     if Session.get 'postLimit'
       Session.set 'postLimit', Session.get('postLimit') + Blog.settings.pageSize


remove comment

remove comment

--- BackofficeBundle/Resources/public/coffee/table/TableviewCollectionView.coffee
+++ BackofficeBundle/Resources/public/coffee/table/TableviewCollectionView.coffee
@@ -50,7 +50,6 @@

   clickAdd: (event) ->
     event.preventDefault()
-#    if $('#main .' + $(event.target).attr('class')).length
     displayLoader('div[role="container"]')
     Backbone.history.navigate('/add')
     title = @title


use the server file instead of americano directly

--- tests/helpers.coffee
+++ tests/helpers.coffee
@@ -29,9 +29,14 @@

 helpers.startApp = (host, port) -> (done) ->
     @timeout 20000
-    americano = require 'americano'
+    appPath = if process.env.USEJS then '../build/server.js'
+    else '../server.coffee'

-    americano.start name: 'template', host: host, port: port, (app, server) =>
+    app = require appPath
+
+    options = {host, port, name:'emails-tests'}
+    app options, (err, app, server) =>
+        return done err if err
         @app = app
         @app.server = server
         done()


Remove unneeded 'async' dependency

Remove unneeded 'async' dependency

Co-Authored-By: Jakob Ackermann <caa3e4f01d56b797520cbfed54bdab4c5e3b0ebb@outlook.com>

--- libraries/access-token-encryptor/lib/coffee/AccessTokenEncryptor.coffee
+++ libraries/access-token-encryptor/lib/coffee/AccessTokenEncryptor.coffee
@@ -1,5 +1,4 @@
 crypto = require('crypto')
-async = require('async')

 ALGORITHM = 'aes-256-ctr'


created name manager for autonaming

created name manager for autonaming

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -229,6 +229,7 @@

   addElement: (ef, x, y) ->
     e = new ef.constructor(@baseRect, x, y, 50)
+    e.props.name = @ve.namemanager.getName(e.constructor.name.toLowerCase())
     @ve.render()
     e

@@ -345,6 +346,17 @@
     @clearPropsGUI()
     @ve.render()

+class NameManager
+  constructor: () ->
+    @names = new Array()
+
+  getName: (s) ->
+    if !@names[s]?
+      @names[s] = 0
+    else
+      @names[s]++
+
+    s + @names[s]

 #VisualEnvironment holds the state associated with the Threejs objects used
 #to render Surfaces and the ElementBox. This class also contains methods
@@ -372,6 +384,7 @@
     @camera.position.z = 200
     @mouseh = new MouseHandler(this)
     @raycaster = new THREE.Raycaster()
+    @namemanager = new NameManager()

   render: ->
     @renderer.clear()


adds queuing functionality

adds queuing functionality

--- coffee/spot.coffee
+++ coffee/spot.coffee
@@ -6,6 +6,7 @@
 #
 # Configuration:
 #   HUBOT_SPOT_URL
+#   HUBOT_SPOT_MENTION_ROOM
 #
 # Commands:
 #   hubot play! - Plays current playlist or song.
@@ -29,11 +30,16 @@
 #   hubot last find - Pulls up the most recent find query
 #   hubot airplay <Apple TV> - Tell Spot to broadcast to the specified Apple TV.
 #   hubot spot - Start or restart the Spotify client.
+#   hubot queue #n - Queues a song to play up when the current song is done
+#   hubot queue list - Shows current song titles in the queue
+#
 # Authors:
-#   mcminton, <Shad Downey> github:andromedado
-VERSION = '1.3.1'
-
-URL = "#{process.env.HUBOT_SPOT_URL}"
+#   mcminton, <Shad Downey> github:andromedado, Eric Stiens - @mutualarising
+
+VERSION = '1.4'
+
+URL = process.env.HUBOT_SPOT_URL || "http://localhost:5051"
+MENTION_ROOM = process.env.HUBOT_SPOT_MENTION_ROOM || "#general"

 spotRequest = (message, path, action, options, callback) ->
   message.http("#{URL}#{path}")
@@ -131,7 +137,65 @@
     if (err)
       message.send(":flushed: " + err)

+queueTrack = (track, qL, message) ->
+  unless track && track.uri
+    message.send("Sorry, couldn't add that to the queue")
+    return
+  qL.push track
+  message.send(":small_blue_diamond: I'm adding " + track.name + " to the queue")
+  message.send "Current queue: #{qL.length} songs"
+
+playNextTrackInQueue = (robot) ->
+  track = robot.brain.data.ql.shift()
+  robot.messageRoom(MENTION_ROOM, "Switching to " + track.name)
+  robot.http(URL+'/play-uri')
+    .query({'uri' : track.uri})['post']() (err,res,body) ->
+      if (err)
+        console.log "Error playing Queued Track" + err
+      sleep(4000) # hacky but otherwise sometimes it plays two tracks in a row
+
+sleep = (ms) ->
+  start = new Date().getTime()
+  continue while new Date().getTime() - start < ms
+
 module.exports = (robot) ->
+
+  playQueue = (robot) ->
+    if robot.brain.data.ql && robot.brain.data.ql.length > 0
+      robot.http(URL+'/seconds-left')
+        .get() (err, res, body) ->
+          seconds_left = parseFloat(body)
+          if seconds_left < 3
+            playNextTrackInQueue(robot)
+    setTimeout (->
+      playQueue(robot)
+    ), 1000
+
+  playQueue(robot)
+
+  robot.respond /queue list/i, (message) ->
+    queue_list = []
+    if robot.brain.data.ql.length > 0
+      for track in robot.brain.data.ql
+        queue_list.push(track.name)
+      message.send "Current Queue: #{queue_list.join(', ')}"
+    else
+      message.send "Nothing queued up."
+
+  robot.respond /clear queue/i, (message) ->
+    robot.brain.data.ql = []
+    message.send "Queue cleared"
+
+  robot.respond /queue (.*)/i, (message) ->
+    robot.brain.data.ql ?= []
+    qL = robot.brain.data.ql
+    playNum = message.match[1].match(/#(\d+)\s*$/)
+    if (playNum)
+      r = getLastResultsRelevantToUser(robot, message.message.user)
+      i = parseInt(playNum[1], 10) - 1
+      if (r && r[i])
+        queueTrack(r[i], qL, message)
+        return

   robot.respond /play!/i, (message) ->
     message.finish()
@@ -144,8 +208,11 @@
       message.send("#{body} :cry:")

   robot.respond /next/i, (message) ->
-    spotRequest message, '/next', 'put', {}, (err, res, body) ->
-      message.send("#{body} :fast_forward:")
+    if robot.brain.data.ql and robot.brain.data.ql.length > 0
+      playNextTrackInQueue(robot)
+    else
+      spotRequest message, '/next', 'put', {}, (err, res, body) ->
+        message.send("#{body} :fast_forward:")

   robot.respond /back/i, (message) ->
     spotRequest message, '/back', 'put', {}, (err, res, body) ->
@@ -283,5 +350,3 @@
   robot.respond /respot/i, (message) ->
     spotRequest message, '/respot', 'put', {}, (err, res, body) ->
       message.send(body)
-
-