title is removed if image title is set.

title is removed if image title is set.

--- src/models/NavigationBar.coffee
+++ src/models/NavigationBar.coffee
@@ -102,6 +102,7 @@

       if options.titleImagePath?
         params.titleImagePath = relativeTo + options.titleImagePath
+        params.title = ""
       else
         params.titleImagePath = ""


Stop bubbling and cancel click event

Stop bubbling and cancel click event

--- lib/tool-bar-button-view.coffee
+++ lib/tool-bar-button-view.coffee
@@ -27,7 +27,7 @@
     @on 'click', (e) =>
       _restoreFocus()
       _executeCallback(options, e) unless @hasClass 'disabled'
-      e.stopPropagation()
+      return false

     @on 'mouseover', -> _storeFocusedElement()


Added 'params' option to sync to allow additional request parameters with Backbone server interaction

--- vendor/assets/javascripts/brainstem/brainstem-sync.coffee
+++ vendor/assets/javascripts/brainstem/brainstem-sync.coffee
@@ -6,7 +6,7 @@
     delete: 'DELETE'
     read:   'GET'

-  type = methodMap[method];
+  type = methodMap[method]

   # Default options, unless specified.
   _.defaults(options || (options = {}), {
@@ -18,7 +18,7 @@
   params = { type: type, dataType: 'json' }

   # Ensure that we have a URL.
-  if (!options.url)
+  unless options.url
     params.url = _.result(model, 'url') || urlError()

   # Ensure that we have the appropriate request data.
@@ -38,6 +38,9 @@

     data.include = Brainstem.Utils.extractArray("include", options).join(",")
     data.filters = Brainstem.Utils.extractArray("filters", options).join(",")
+
+    _.extend data, options.params or {}
+
     params.data = JSON.stringify(data)

   # For older servers, emulate JSON by encoding the request into an HTML-form.


drawing task values Whenever we add/remove a mark, set drawing task value to an array of marks for the current task.

--- src/classify-page.coffee
+++ src/classify-page.coffee
@@ -78,6 +78,12 @@
       @finishSubject()

     @decisionTreeContainer.append @decisionTree.el
+
+    @listenTo @subjectViewer.markingSurface, 'marking-surface:add-tool', =>
+      @updateDrawingTask()
+
+    @listenTo @subjectViewer.markingSurface, 'marking-surface:remove-tool', =>
+      @updateDrawingTask()

     @summary = new ClassificationSummary

@@ -207,12 +213,6 @@
     for keyAndValue in decisionTreeValues then for key, value of keyAndValue
       annotations.push {key, value}

-    for {mark} in @subjectViewer.markingSurface.tools
-      # A drawing task's value is the last-selected tool, which is not terribly
-      # useful. Replace it with the task's marks.
-      task = annotations[mark._taskIndex]
-      task?.value.push mark
-
     annotations

   promptToLogIn: ->
@@ -244,6 +244,13 @@
         setTimeout (=> @destroy()), 600

     prompt.show()
+
+  updateDrawingTask: ->
+    marks = []
+    for {mark} in @subjectViewer.markingSurface.tools
+      marks.push mark if mark._taskIndex is @subjectViewer.taskIndex
+
+    @decisionTree.currentTask?.reset marks

   events:
     'change input[name="favorite"]': (e) ->


Update tests

Update tests

--- spec/csound-spec.coffee
+++ spec/csound-spec.coffee
@@ -419,7 +419,7 @@
         ]

     it 'tokenizes quoted strings', ->
-      tokens = (grammar.tokenizeLines '"characters$MACRO."')[0]
+      {tokens} = grammar.tokenizeLine '"characters$MACRO."'
       expect(tokens.length).toBe 4
       expect(tokens[0]).toEqual value: '"', scopes: [
         'source.csound'
@@ -479,14 +479,14 @@
         '\\345'
         '\\67'
       ]
-      tokens = (grammar.tokenizeLines "\"#{escapeSequences.join ''}\"")[0]
+      {tokens} = grammar.tokenizeLine "\"#{escapeSequences.join ''}\""
       for i in [1...tokens.length - 1]
         expect(tokens[i]).toEqual value: escapeSequences[i - 1], scopes: [
           'source.csound'
           'string.quoted.csound'
           'constant.character.escape.csound'
         ]
-      tokens = (grammar.tokenizeLines "{{#{escapeSequences.join ''}}}")[0]
+      {tokens} = grammar.tokenizeLine "{{#{escapeSequences.join ''}}}"
       for i in [1...tokens.length - 1]
         expect(tokens[i]).toEqual value: escapeSequences[i - 1], scopes: [
           'source.csound'
@@ -600,7 +600,7 @@
         '\\T'
       ]
       for opcode in opcodes
-        tokens = (grammar.tokenizeLines "#{opcode} \"#{escapeSequences.join ''}\"")[0]
+        {tokens} = grammar.tokenizeLine "#{opcode} \"#{escapeSequences.join ''}\""
         expect(tokens[0]).toEqual value: opcode, scopes: [
           'source.csound'
           'support.function.csound'
@@ -674,7 +674,7 @@

     it 'tokenizes include directives', ->
       for character in ['"', '|']
-        tokens = (grammar.tokenizeLines "#include/**/#{character}file.udo#{character}")[0]
+        {tokens} = grammar.tokenizeLine "#include/**/#{character}file.udo#{character}"
         expect(tokens.length).toBe 7
         expect(tokens[0]).toEqual value: '#include', scopes: [
           'source.csound'
@@ -978,7 +978,7 @@
         ]

     it 'tokenizes function-like macro use', ->
-      tokens = (grammar.tokenizeLines '$MACRO.(i1#i2)//')[0]
+      {tokens} = grammar.tokenizeLine '$MACRO.(i1#i2)//'
       expect(tokens.length).toBe 9
       expect(tokens[0]).toEqual value: '$MACRO.', scopes: [
         'source.csound'


add another user reported case, confirmed to pass.

--- spec/filter-spec.coffee
+++ spec/filter-spec.coffee
@@ -657,6 +657,14 @@

       expect(bestMatch(candidates, 'apps_controller')).toBe candidates[1]

+      candidates = [
+        path.join('config', 'application.rb'),
+        path.join('app', 'controllers', 'application_controller.rb')
+      ]
+
+      expect(bestMatch(candidates, 'appcon')).toBe candidates[1]
+
+
     it "prefer path together to shorter path", ->

       candidates = [


Format on save test passing

Format on save test passing

--- spec/format-spec.coffee
+++ spec/format-spec.coffee
@@ -27,3 +27,18 @@
       runs ->
         # just check that some whitespace and other goodies got added
         expect(atom.workspace.getActiveTextEditor().getText()).not.toMatch(@fileText)
+
+    it "can format the whole buffer if Format on save is turned on", ->
+      # format_on_save test
+
+      waitsForPromise ->
+        atom.workspace.open('specfiles/index.js')
+
+      runs ->
+        fileText = atom.workspace.getActiveTextEditor().getText()
+        atom.config.set('jsformat.format_on_save', true)
+        atom.workspace.getActiveTextEditor().save()
+
+        # just check that some whitespace and other goodies got added
+        expect(atom.workspace.getActiveTextEditor().getText()).not.toMatch(fileText)
+        atom.config.set('jsformat.format_on_save', false)


two tests converted, that's pretty easy. :-)

--- spec/legacy.spec.coffee
+++ spec/legacy.spec.coffee
@@ -7,47 +7,71 @@
 # Distributed under MIT license
 # http://github.com/derickbailey/jasmine.async
 runAsync = (block) ->
-	->
-		done = false
-		complete = ->
-			done = true
+    ->
+        done = false
+        complete = ->
+            done = true

-		runs ->
-			block complete
+        runs ->
+            block complete

-		waitsFor ->
-			done
+        waitsFor ->
+            done

 class AsyncSpec
-	constructor: (@spec) ->
+    constructor: (@spec) ->

-	beforeEach: (block) ->
-		@spec.beforeEach runAsync(block)
+    beforeEach: (block) ->
+        @spec.beforeEach runAsync(block)

-	afterEach: (block) ->
-		@spec.afterEach runAsync(block)
+    afterEach: (block) ->
+        @spec.afterEach runAsync(block)

-	it: (description, block) ->
-		global.it description, runAsync(block)
+    it: (description, block) ->
+        global.it description, runAsync(block)

 describe 'Snockets Legacy API', ->

-	snockets = null
-	async = new AsyncSpec(@)
+    snockets = null
+    async = new AsyncSpec(@)

-	beforeEach ->
-		snockets = new Snockets { src }
+    beforeEach ->
+        snockets = new Snockets { src }

-	afterEach ->
-		snockets = null
+    afterEach ->
+        snockets = null

-	async.it 'doesn\'t find dependencies for independant js files', (done) ->
+    describe 'doesn\'t find dependencies for independant js files', ->
+        async.it 'async', (done) ->
+            snockets.scan 'b.js', (err) ->
+                throw err if err
+                expect(snockets.depGraph.map['b.js']).toBeDefined()
+                expect(snockets.depGraph.getChain 'b.js').toEqual []
+                done()

-		snockets.scan 'b.js', (err) ->
-			throw err if err
-			expect(snockets.depGraph.map['b.js']).toBeDefined()
-			expect(snockets.depGraph.getChain 'b.js').toEqual []
-			done()
+        it 'sync', ->
+            snockets.options.async = false
+            snockets.scan 'b.js', ->
+            expect(snockets.depGraph.map['b.js']).toBeDefined()
+            expect(snockets.depGraph.getChain 'b.js').toEqual []
+
+    describe 'Single-step dependencies are correctly recorded', ->
+        async.it 'async', (done) ->
+            snockets.scan 'a.coffee', (err) ->
+                throw err if err
+                expect(snockets.depGraph.getChain('a.coffee')).toEqual ['b.js']
+                done()
+
+        it 'sync', ->
+            snockets.options.async = false
+            snockets.scan 'a.coffee', ->
+            expect(snockets.depGraph.getChain('a.coffee')).toEqual ['b.js']
+
+    xdescribe '', ->
+        async.it 'async', (done) ->
+
+        it 'sync', ->
+            snockets.options.async = false


fix #34

fix #34

--- src/coffee/github.coffee
+++ src/coffee/github.coffee
@@ -125,30 +125,8 @@

       else
         if @page in ['commit', 'compare', 'pull']
-          if res.base?
-            if report.totals.c?  # v4
-              total = report.totals.c
-              if res.base?.report?.totals?.c
-                compare = self.format(parseFloat(total) - parseFloat(res.base.report.totals.c))
-              else
-                total = if report.totals.c? then report.totals.c else report.coverage  # v4 || v3
-                $('.toc-diff-stats, .diffbar-item.diffstat, #diffstat')
-                  .append(""" #{self.format total}%""")
-            else  # v3
-              total = report.coverage
-              compare = self.format(parseFloat(total) - parseFloat(res.base))
-            plus = if compare > 0 then '+' else ''
-            $('.toc-diff-stats, .diffbar-item.diffstat, #diffstat')
-              .append(
-                if compare is '0.00'
-                  """Coverage did not change."""
-                else
-                  """ #{plus}#{compare}%"""
-              )
-          else
-            total = if report.totals.c? then report.totals.c else report.coverage  # v4 || v3
-            $('.toc-diff-stats, .diffbar-item.diffstat, #diffstat')
-              .append(""" #{self.format total}%""")
+          $('.toc-diff-stats, .diffbar-item.diffstat, #diffstat')
+            .append(""" #{self.format report.totals.c}%""")

         self = @
         total_hits = 0


an approach to URL handling which is entirely wrong

--- client/game.coffee
+++ client/game.coffee
@@ -28,22 +28,42 @@
 noughts.state.history = []

 # Changes the current game state to 'newState' and records the change in
-# noughts.state.history
-noughts.state.changeState = (newState) ->
+# noughts.state.history. The "urlBehavior" parameter shoud be a
+# noughts.UrlBehavior, and the browser URL will be modified accordingly.
+noughts.state.changeState = (newState, urlBehavior) ->
   noughts.state.history.unshift(newState)
-  me.state.change(newState)
+  me.state.change(newState, urlBehavior)
+
+# What a new state should do to the browser URL when entered.
+noughts.UrlBehavior =
+  # Change the current URL and add it to the browser history stack:
+  PUSH_URL: 1
+  # Keep the existing URL without modifying browser history:
+  PRESERVE_URL: 2
+  # Change the current URL without modifying browser history:
+  REPLACE_URL: 3

 # Returns true if there's a previous state in the state history to go back to
 noughts.state.hasPreviousState = ->
   noughts.state.history.length > 1

+# Possibly modifies the current browser URL to the to provided path, based on
+# the behavior requested in the 'urlBehavior' parameter (a
+# noughts.state.UrlBehavior).
+noughts.state.updateUrl = (path, urlBehavior) ->
+  if urlBehavior == noughts.UrlBehavior.PUSH_URL
+    window.history.pushState({}, "", path)
+  else if urlBehavior == noughts.UrlBehavior.REPLACE_URL
+    window.history.replaceState({}, "", path)
+  # UrlBehavior.PRESERVE_URL is a no-op.
+
 # Changes the current game state to the previous one in history. It's an error
-# to call popState() if no state history exists.
+# to call popState() if no state history exists. Always updates the browser URL.
 noughts.state.popState = ->
   unless noughts.state.hasPreviousState()
     displayError("Tried to popState with no more states available")
   noughts.state.history.shift()
-  me.state.change(noughts.state.history[0])
+  me.state.change(noughts.state.history[0], noughts.UrlBehavior.REPLACE_URL)

 # Pops up an alert to the user saying that an error has occurred. Should be used
 # for un-recoverable errors.
@@ -85,7 +105,8 @@
       if noughts.state.hasPreviousState()
         noughts.state.popState()
       else
-        noughts.state.changeState(noughts.state.INITIAL_PROMO)
+        noughts.state.changeState(noughts.state.INITIAL_PROMO,
+            noughts.UrlBehavior.PUSH_URL)

     $(".nUrlInviteButton").on "click", ->
       Meteor.call "newGame", (err, gameId) ->
@@ -96,10 +117,10 @@
         $(".nOkUrlCalloutButton").on "click", ->
           $(".nBubble").hide()
           $(".nDarkenScreen").hide()
-        noughts.state.changeState(noughts.state.PLAY)
-
-  onResetEvent: ->
-    window.history.pushState({}, "", "/new")
+        noughts.state.changeState(noughts.state.PLAY, noughts.UrlBehavior.PUSH_URL)
+
+  onResetEvent: (urlBehavior) ->
+    noughts.state.updateUrl("/new", urlBehavior)
     $(".nGame").children().hide()
     $(".nNewGameMenu").show()
     $(".nGame").css({border: ""})
@@ -107,10 +128,11 @@
 noughts.InitialPromo = me.ScreenObject.extend
   init: ->
     $(".nNewGameButton").on "click", ->
-      noughts.state.changeState(noughts.state.NEW_GAME_MENU)
-
-  onResetEvent: ->
-    window.history.pushState({}, "", "/")
+      noughts.state.changeState(noughts.state.NEW_GAME_MENU,
+          noughts.UrlBehavior.PUSH_URL)
+
+  onResetEvent: (urlBehavior) ->
+    noughts.state.updateUrl("/", urlBehavior)
     $(".nGame").children().hide()
     $(".nGame").css({border: ""})
     $(".nNewGamePromo").show()
@@ -123,9 +145,9 @@

   # Called whenever the game state changes to noughts.state.PLAY, initializes the
   # game and hooks up the appropriate game click event handlers.
-  onResetEvent: ->
+  onResetEvent: (urlBehavior) ->
     gameId = Session.get("gameId")
-    window.history.pushState({}, "", "/#{gameId}")
+    noughts.state.updateUrl("/#{gameId}", urlBehavior)
     $(".nGame").children().hide()
     $(".nGame").css({border: "none"})
     $(".nMain canvas").show()
@@ -177,12 +199,13 @@
         Meteor.call("facebookSetRequestId", gameId, requestId, (err) ->
           if err then throw err
           Session.set("gameId", gameId)
-          noughts.state.changeState(noughts.state.PLAY))
+          noughts.state.changeState(noughts.state.PLAY,
+              noughts.UrlBehavior.PUSH_URL))
     else
       # TODO(dthurn): Display regular invite dialog
       Session.set("gameId", gameId)
       $(".nNewGameModal").modal()
-      #noughts.state.changeState(noughts.state.PLAY)
+      #noughts.state.changeState(noughts.state.PLAY, noughts.UrlBehavior.PUSH_URL)

 # Initializer to be called after the DOM ready even to set up MelonJS.
 initialize = ->
@@ -206,11 +229,6 @@
 # Callback for when the user's games are retrieved from the server. Sets up
 # some reactive functions and handles facebook ?request_ids params
 onSubscribe = ->
-  # Update the URL when the game ID changes
-  Meteor.autorun ->
-    gameId = Session.get("gameId")
-    if gameId then window.history.pushState({}, "", "/#{gameId}")
-
   # Update game scale when scaleFactor changes
   Meteor.autorun ->
     scaleFactor = Session.get("scaleFactor")
@@ -237,6 +255,9 @@
 noughts.maybeInitialize = _.after 2, ->
   cacheFacebookData() if Session.get("useFacebook")
   Meteor.subscribe("myGames", onSubscribe)
+
+  $(window).on "popstate", ->
+    setStateFromUrl()

 # Inspects the URL and sets the initial game state accordingly.
 setStateFromUrl = ->
@@ -252,13 +273,15 @@
       # the game for the last one.
     displayError("Game not found for requestIds: " + requestIds) unless game
     Session.set("gameId", game._id)
-    noughts.state.changeState(noughts.state.PLAY)
+    noughts.state.changeState(noughts.state.PLAY, noughts.UrlBehavior.PUSH_URL)
   else if path == "new"
-    noughts.state.changeState(noughts.state.NEW_GAME_MENU)
+    noughts.state.changeState(noughts.state.NEW_GAME_MENU,
+        noughts.UrlBehavior.PRESERVE_URL)
   else if path == ""
     # TODO(dthurn): If the user is logged in, display their game list instead
     # of the new game promo
-    noughts.state.changeState(noughts.state.INITIAL_PROMO)
+    noughts.state.changeState(noughts.state.INITIAL_PROMO,
+        noughts.UrlBehavior.PRESERVE_URL)
   else # For simplicity, assume any unrecognized path is a game id
     Meteor.call "validateGameId", path, (err, gameExists) ->
       if err then throw err
@@ -267,5 +290,7 @@
       Meteor.call "addPlayerIfNotPresent", path, (err) ->
         if err then throw err
         # TODO(dthurn): Show some kind of message if the game is full and the
-        # viewer is only a spectator
-        noughts.state.changeState(noughts.state.PLAY)+        # viewer is only a spectator, allowing the viewer to watch or perhaps
+        # "clone" the game.
+        noughts.state.changeState(noughts.state.PLAY,
+            noughts.UrlBehavior.PRESERVE_URL)


changed my mind about URLs again

changed my mind about URLs again

--- client/game.coffee
+++ client/game.coffee
@@ -43,8 +43,8 @@

 displayNotice = (msg) -> $(".nNotification").text(msg)

-setUrl = (gameId, player) ->
-    window.history.pushState({}, "", "?game_id=#{gameId}&player=#{player}")
+setUrl = (gameId) ->
+    window.history.pushState({}, "", "?game_id=#{gameId}")

 PlayScreen = me.ScreenObject.extend
   handleClick_: (tile) ->
@@ -129,10 +129,7 @@
   gameId = $.url().param("game_id")

   Meteor.autorun ->
-    game = noughts.Games.findOne Session.get("gameId")
-    if game
-      isXPlayer = game.currentPlayer == game.xPlayer
-      setUrl(game._id, if isXPlayer then "x" else "o")
+    setUrl(Session.get("gameId"))

   if gameId
     game = noughts.Games.findOne(gameId)


One more jQuery shorthand!

One more jQuery shorthand!

--- coffee/toggle-video.coffee
+++ coffee/toggle-video.coffee
@@ -1,6 +1,4 @@
-$ = jQuery
-
-$ ->
+jQuery ($) ->
   $(".js-toggle-video-link").click ->
     $(".js-toggle-video-link").hide()
     $(".scroll-indicator-link").remove()


update coding style with @paulmillr's notes

--- src/commands/test.coffee
+++ src/commands/test.coffee
@@ -5,6 +5,7 @@
 sysPath = require 'path'
 async = require 'async'
 Mocha = require 'mocha'
+fs_utils = require '../fs_utils'
 helpers = require '../helpers'
 watch = require './watch'

@@ -12,7 +13,7 @@
   showJsdomNote = ->
     console.log '\n\nIn order to run tests in a CLI/jsdom environment, you have to install jsdom.'

-    if os.platform() == 'win32'
+    if os.platform() is 'win32'
       console.log '\nBefore installing jsdom, you have to install the following dependencies:'
       console.log '* Python 2.7'
       console.log '* Microsoft Visual Studio or Visual C++ Express\n'
@@ -23,7 +24,7 @@
     console.log '\nnpm install jsdom'

   try
-    return require sysPath.resolve './node_modules/jsdom'
+    require sysPath.resolve './node_modules/jsdom'
   catch error
     showJsdomNote()
     process.exit 1
@@ -76,7 +77,7 @@
   startTestRunner: (window) =>
     testHelpersFile = sysPath.resolve sysPath.join @config.paths.root, @config.paths.test, 'test-helpers.coffee'

-    sysPath.exists testHelpersFile, (exists) =>
+    fs_utils.exists testHelpersFile, (exists) =>
       globals = exists and require(testHelpersFile) or {}
       globals.window = window


Refactor, extract GitHubClient class

Refactor, extract GitHubClient class

--- collector/collector.coffee
+++ collector/collector.coffee
@@ -10,61 +10,74 @@
 if !process.env.GITHUB_TOKEN or process.env.GITHUB_TOKEN.length < 1
   console.warn "Missing recommended environment variable GITHUB_TOKEN!"

-sendRequest = (callback, extra_headers) ->
-  headers = {
-    'Host': 'api.github.com',
-    'User-Agent': 'moddb14-proj Collector (https://github.com/Koronen/moddb14-proj/collector)',
-    'Accept': 'application/vnd.github.v3+json'
-  }
-  for key in extra_headers
-    headers[key] = extra_headers[key]
-  options = {
-    method: 'GET', hostname: 'api.github.com', port: 443, path: '/events', headers: headers
-  }
-  req = https.request options, (res) ->
-    res.setEncoding 'utf8'
+class GitHubClient
+  constructor: (token, callback) ->
+    @token = token
+    @callback = callback
+    @etag = undefined
+    @lastModified = undefined

-    body = ''
-    res.on 'data', (chunk) ->
-      body += chunk
-    res.on 'end', ->
-      callback null, res.headers, body
-  req.on 'error', (e) ->
-    callback(e, null, null)
-  console.log new Date() + ": Sending request to GitHub"
-  req.end()
+  start: ->
+    @sendRequest()

-extra_headers = {}
-if process.env.GITHUB_TOKEN
-  extra_headers.Authorization = "token #{process.env.GITHUB_TOKEN}"
+  sendRequest: ->
+    headers = {
+      'Host': 'api.github.com',
+      'User-Agent': 'moddb14-proj Collector (https://github.com/Koronen/moddb14-proj/collector)',
+      'Accept': 'application/vnd.github.v3+json'
+    }
+    if @token
+      headers['Authorization'] = "token #{@token}"
+    if @etag
+      headers['If-None-Match'] = @etag
+    if @lastModified
+      headers['If-Modified-Since'] = @lastModified

-processResponse = (err, headers, body) ->
-  delay = 1000
+    options = {
+      method: 'GET', hostname: 'api.github.com', port: 443, path: '/events', headers: headers
+    }
+    req = https.request options, (res) =>
+      res.setEncoding 'utf8'

-  if err
-    console.error new Date() + ": " + err
-  else
-    if headers['status'] is '200 OK'
-      extra_headers['If-None-Match'] = headers['etag']
-      extra_headers['If-Modified-Since'] = headers['last-modified']
-      delay = headers['x-poll-interval'] * 1000
+      body = ''
+      res.on 'data', (chunk) ->
+        body += chunk
+      res.on 'end', =>
+        @processResponse null, { headers: res.headers, body: body }
+    req.on 'error', (e) =>
+      @processResponse e, null
+    console.log new Date() + ": Sending request to GitHub"
+    req.end()

-      processEvents(JSON.parse(body))
-    else if headers['status'] is '304 Not Modified'
-      # No-op
-    else if headers['status'] is '403 Forbidden' and headers['x-ratelimit-remaining'] is '0'
-      console.warn new Date() + ": Hit rate limit!"
-      delay = headers['x-ratelimit-reset']*1000-Date.now()
+  processResponse: (err, response) ->
+    delay = 1000
+
+    if err
+      console.error new Date() + ": " + err
     else
-      console.warn new Date() + ": Unexpected response status \"" + headers['status'] + "\""
+      headers = response.headers
+      body = response.body

-  console.log new Date() + ": Sending next request in " + delay/1000 + "s"
-  setTimeout ->
-    sendRequest processResponse, extra_headers
-  , delay
-sendRequest processResponse, extra_headers
+      if headers['status'] is '200 OK'
+        @etag = headers['etag']
+        @lastModified = headers['last-modified']
+        delay = headers['x-poll-interval'] * 1000

-processEvents = (events) ->
+        @callback(JSON.parse(body))
+      else if headers['status'] is '304 Not Modified'
+        # No-op
+      else if headers['status'] is '403 Forbidden' and headers['x-ratelimit-remaining'] is '0'
+        console.warn new Date() + ": Hit rate limit!"
+        delay = headers['x-ratelimit-reset']*1000-Date.now()
+      else
+        console.warn new Date() + ": Unexpected response status \"" + headers['status'] + "\""
+
+    console.log new Date() + ": Sending next request in " + delay/1000 + "s"
+    setTimeout =>
+      @sendRequest
+    , delay
+
+client = new GitHubClient process.env.GITHUB_TOKEN, (events) ->
   connection = amqp.createConnection url: process.env.AMQP_URL
   connection.on 'ready', ->
     connection.publish('raw-events', event) for event in events
@@ -72,3 +85,4 @@
   connection.on 'error', (err) ->
     console.error err
     process.exit 1
+client.start()


Fixed default sort order again; this time it actually works. Also made it easier to sort by things other than name

--- SingularityUI/app/collections/TaskFiles.coffee
+++ SingularityUI/app/collections/TaskFiles.coffee
@@ -54,11 +54,19 @@
         taskFiles

     comparator: (a, b) ->
+        return @nameComparator(a, b)
+
+    nameComparator: (a, b) ->
         if a.get('isDirectory') and not b.get('isDirectory')
             return -1
         else if not a.get('isDirectory') and b.get('isDirectory')
             return 1
-        return a.get('name') > b.get('name')
+        else if a.get('name') > b.get('name')
+            return 1
+        else if a.get('name') < b.get('name')
+            return -1
+        else
+            return 0

     testSandbox: ->
         $.ajax


photobanner uses generic back to homepage button

--- @client/banner.coffee
+++ @client/banner.coffee
@@ -892,23 +892,15 @@
     return  DIV
               style:
                 backgroundColor: 'white'
+
               DIV
                 style:
                   margin: 'auto'
                   fontSize: 43
                   padding: '10px 20px'

-                A
-                  href: '/'
-
-                  '< '
-
-                  SPAN
-                    style:
-                      fontSize: 32
-                      position: 'relative'
-                      left: 5
-                    'Homepage'
+                back_to_homepage_button {fontSize: 32}, translator 'engage.back_to_homepage', 'Homepage'
+

   has_image_background = edit_banner.masthead_preview != '*delete*' && (edit_banner.masthead_preview || customization('banner')?.background_image_url || opts.backgroundImage)
   if has_image_background


"earliest" sort order added

"earliest" sort order added

--- @client/filter.coffee
+++ @client/filter.coffee
@@ -200,6 +200,10 @@
     comp: (a,b) -> new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
     name: 'Newest'
     description: "The responses submitted most recently are shown first."
+  }, {
+    comp: (a,b) -> new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
+    name: 'Earliest'
+    description: "The responses submitted first are shown first."
   }, {
     name: 'Unity'
     description: "Responses where the community is most united for or against are shown highest."
@@ -422,7 +426,7 @@
             DIV
               style:
                 fontWeight: 600
-                fontSize: 20
+                fontSize: 18

               translator "engage.sort_order.#{option.name}", option.name


Add windows1252

Add windows1252

--- src/config-schema.coffee
+++ src/config-schema.coffee
@@ -67,6 +67,7 @@
           'utf8',
           'windows1250',
           'windows1251',
+          'windows1252',
           'windows1253',
           'windows1254',
           'windows1255',


Can draw grid without mutation

Can draw grid without mutation

--- src/config/layout.coffee
+++ src/config/layout.coffee
@@ -10,6 +10,6 @@
   _LABEL: "layout"
   _CHILDREN: [
     #controls
-    #grid
+    grid
   ]
 }


Changing to Adler.

Changing to Adler.

--- app/controllers/team-page.coffee
+++ app/controllers/team-page.coffee
@@ -17,7 +17,7 @@

Audrey Aronowsky (Outreach Coordinator, BioSynC)

-
Zooniverse

+
The Adler Planetarium

Arfon Smith, Co-Principal Investigator (Director of Citizen Science)

Michael Parrish


renamed __processSyncUseTag to __processUseTag since there is virtually no difference between the sync and async version

renamed __processSyncUseTag to __processUseTag since there is virtually
no difference between the sync and async version

--- meta-engine.coffee
+++ meta-engine.coffee
@@ -253,7 +253,7 @@
     return [ regionMap, content ]

-  __processSyncUseTag: (resourcePath, content, regionMap)->
+  __processUseTag: (resourcePath, content, regionMap)->

     linebreakCharacter = @optionMap.linebreakCharacter

@@ -354,7 +354,7 @@

       [ regionMap, content ] = @__processRegionTag resourcePath, content

-      content = @__processSyncUseTag resourcePath, content, regionMap
+      content = @__processUseTag resourcePath, content, regionMap

       if trim


Timeline: prevent reentrancy in stop/start

--- components/Timeline.coffee
+++ components/Timeline.coffee
@@ -54,6 +54,7 @@

   start: () ->
+    return if @running
     @lastTime = @currentTime()
     @elapsedTime = 0
     @repeat = @repeatCount
@@ -64,15 +65,18 @@
     @outPorts.started.disconnect()

   stop: () ->
+    return unless @running
     @running = false
     return unless @outPorts.stopped.isAttached()
     @outPorts.stopped.send(true)
     @outPorts.stopped.disconnect()

   pause: () ->
+    return unless @running
     @running = false

   unpause: () ->
+    return if @running
     @lastTime = @currentTime()
     @running = true


Use stub instead of spy in buttonview

Use stub instead of spy in buttonview

--- test/components/buttons/buttonview.coffee
+++ test/components/buttons/buttonview.coffee
@@ -7,7 +7,7 @@
 describe 'KDButtonView', ->
   beforeEach ->
     @sinon = sinon.sandbox.create()
-    @callback = @sinon.spy (->)
+    @callback = @sinon.stub()
     @view = new KDButtonView
       title: 'button'
       iconClass: 'cupid-green'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -6,4 +6,5 @@
   # Add events and methods here
   self.bind "afterUpdate", ->

-  clamp:
+  clamp: ->
+


Fix variable name bug

Fix variable name bug

--- src/picksel.coffee
+++ src/picksel.coffee
@@ -284,7 +284,7 @@
                       percs = []
                       perc = (file, callback) ->
                         jimp.read file.filename, (err, img) ->
-                          percs.push {hash: file.hash,distance:jimp.distance(baseImg, img)}
+                          percs.push {hash: file.hash,distance:jimp.distance(base, img)}
                           callback()
                       percDone = () ->
                         filedel './picksel_temp'


Add attribution

Add attribution

--- coffee/home.coffee
+++ coffee/home.coffee
@@ -1,3 +1,8 @@
+###
+@preserve
+Effect based on https://github.com/dmotz/dmotz.github.com/blob/master/src/oxism.coffee
+###
+
 touch = 'ontouchstart' of window
 w = h = 0
 positions = []


the slow, slow trickle

the slow, slow trickle

--- models/team.coffee
+++ models/team.coffee
@@ -41,7 +41,7 @@
 TeamSchema.static 'canRegister', (next) ->
   Team.count {}, (err, count) ->
     return next err if err
-    max = 307 + 1 # +1 because team fortnight labs doesn't count
+    max = 310 + 1 # +1 because team fortnight labs doesn't count
     next null, count < max, max - count

 TeamSchema.static 'uniqueName', (name, next) ->


Update default options to speed up bar on fast pages

--- pace.coffee
+++ pace.coffee
@@ -1,24 +1,25 @@
 defaultOptions =
   # How long should it take for the bar to animate to a new
   # point after receiving it
-  catchupTime: 500
+  catchupTime: 100

   # How quickly should the bar be moving before it has any progress
   # info from a new source in %/ms
   initialRate: .03

   # What is the minimum amount of time the bar should be on the
-  # screen
-  minTime: 500
+  # screen.  Irrespective of this number, the bar will always be on screen for
+  # 33 * (100 / maxProgressPerFrame) + ghostTime ms.
+  minTime: 250

   # What is the minimum amount of time the bar should sit after the last
   # update before disappearing
-  ghostTime: 500
+  ghostTime: 100

   # Its easy for a bunch of the bar to be eaten in the first few frames
   # before we know how much there is to load.  This limits how much of
   # the bar can be used per frame
-  maxProgressPerFrame: 10
+  maxProgressPerFrame: 20

   # This tweaks the animation easing
   easeFactor: 1.25


Whitespace fix got rid of that documentation warning, good to go.

--- src/scripts/geolocate-ip.coffee
+++ src/scripts/geolocate-ip.coffee
@@ -1,4 +1,3 @@
-
 # Description:
 #   Geolocate an IP Address
 #


Fixed credits

Fixed credits

--- src/scripts/romanemperor.coffee
+++ src/scripts/romanemperor.coffee
@@ -3,8 +3,7 @@
 # <user> disapproves - Disapprove of something
 # <user> approves - Approve of something
 #
-# Developed # Developed by One Mighty Roar (http://github.com/onemightyroar)
- One Mighty Roar - http://github.com/onemightyroar
+# Developed by One Mighty Roar (http://github.com/onemightyroar)

 module.exports = (robot) ->
   robot.respond /@?([\w .-_]+) disapproves/i, (msg) ->


app: Store Oghma.App instance in O global for ease of debugging.

--- oghma/app.coffee
+++ oghma/app.coffee
@@ -14,6 +14,8 @@

 Oghma = @Oghma ?= {}
 Oghma.Thingy ?= {}
+
+root = this

 # Main application class.
 #
@@ -43,6 +45,9 @@
   # - tableverse [{Heron.Thingyverse}]
   #
   constructor: (config = {}) ->
+    # Store in global for easy introspection from console.
+    root.O = this
+
     @_ = {}
     @_.debug = config.debug ? false


Removes unnecessary parentheses and semicolon

--- pace.coffee
+++ pace.coffee
@@ -117,7 +117,7 @@

       '''
-      targetElement = document.querySelector(options.target);
+      targetElement = document.querySelector options.target
       if targetElement.firstChild?
         targetElement.insertBefore @el, targetElement.firstChild
       else


Correctly removes old status classes on the number widget when updating the status

--- templates/project/widgets/number/number.coffee
+++ templates/project/widgets/number/number.coffee
@@ -17,4 +17,4 @@

   onData: (data) ->
     if data.status
-      $(@get('node')).addClass("status-#{data.status}")
+      $(@get('node')).removeClass("status-danger status-warning").addClass("status-#{data.status}")


fix time editor (fallout from sync timestamp ticket)

--- src/smc-webapp/editor_time.cjsx
+++ src/smc-webapp/editor_time.cjsx
@@ -203,19 +203,19 @@
             label  : ''
             total  : 0
             state  : 'stopped'  # 'paused', 'running', 'stopped'
-            time   : salvus_client.server_time()
+            time   : salvus_client.server_time() - 0

     stop_stopwatch: (id) =>
         @_set
             id    : id
             total : 0
             state : 'stopped'
-            time  : salvus_client.server_time()
+            time  : salvus_client.server_time() - 0

     start_stopwatch: (id) =>
         @_set
             id    : id
-            time  : salvus_client.server_time()
+            time  : salvus_client.server_time() - 0
             state : 'running'

     pause_stopwatch: (id) =>
@@ -225,7 +225,7 @@
             return
         @_set
             id    : id
-            time  : salvus_client.server_time()
+            time  : salvus_client.server_time() - 0
             total : x.get('total') + (salvus_client.server_time() - x.get('time'))
             state : 'paused'


CoffeeScript CLI should be using the `-s` option for string compare.

--- src/similar.coffee
+++ src/similar.coffee
@@ -9,10 +9,9 @@
   tanimoto: require "./similar/tanimoto"
   experimental: require "./similar/experimental"

-determine = (file_or_string) ->
-  if fs.existsSync file_or_string
-  then fs.readFileSync file_or_string, "utf-8"
-  else file_or_string
+get_data = (is_string, value) ->
+  if is_string then value
+  else fs.readFileSync value, "utf-8"

 generate_ngrams = (arr, start = 1, end = arr.length) ->
   if end > arr.length
@@ -51,9 +50,8 @@
   if !opts.compare then error "no compare propery provided"
   if !opts.to then error "no to propery provided"

-  src = opts.compare || ""
-  cmp = opts.to || ""
-  ngram_type = "list" # or "tree"
+  src = get_data opts.stringCompare, (opts.compare || "")
+  cmp = get_data opts.stringCompare, (opts.to || "")
   algorithm = opts.algorithm || "jaccard"
   language = opts.language || "js"
   compare = algorithms[algorithm].compare
@@ -62,11 +60,11 @@

   switch language
     when "js"
-      src_t = js.normalize js.tokenize determine src
-      cmp_t = js.normalize js.tokenize determine cmp
+      src_t = js.normalize js.tokenize src
+      cmp_t = js.normalize js.tokenize cmp
     when "coffee"
-      src_t = coffee.normalize coffee.tokenize determine src
-      cmp_t = coffee.normalize coffee.tokenize determine cmp
+      src_t = coffee.normalize coffee.tokenize src
+      cmp_t = coffee.normalize coffee.tokenize cmp

   a = generate_ngrams src_t, n_start, n_end
   b = generate_ngrams cmp_t, n_start, n_end


Yet another fix

Yet another fix

--- modules/player/index.coffee
+++ modules/player/index.coffee
@@ -109,6 +109,7 @@
       # Bot Switches Voice Channels
       if newMember.id is Core.bot.user.id and newMember.voiceChannel?
         player = @_guilds[newMember.guild.id]
+        return unless player.queue
         return player.skip() if player.queue.nowPlaying and not newMember.voiceChannel?
         return unless player.queue.nowPlaying and
                       player.queue.nowPlaying.voiceChannel.id isnt newMember.voiceChannel.id


login: decorate GitHub button title respectively

--- client/Login/AppView.coffee
+++ client/Login/AppView.coffee
@@ -121,7 +121,6 @@
         registerHandler()

     @github       = new KDButtonView
-      title       : "Sign up with GitHub"
       style       : 'solid github'
       icon        : yes
       callback    : ->
@@ -529,6 +528,7 @@

       switch name
         when "register"
+          @github.setTitle "Sign up with GitHub"
           @registerForm.email.input.setFocus()
         when "finishRegistration"
           @finishRegistrationForm.username.input.setFocus()
@@ -536,6 +536,7 @@
           @$('.flex-wrapper').addClass 'one'
           @redeemForm.inviteCode.input.setFocus()
         when "login"
+          @github.setTitle "Sign in with GitHub"
           @loginForm.username.input.setFocus()
         when "recover"
           @$('.flex-wrapper').addClass 'one'


Social: add socialapicontroller into includes.coffee of main app

--- client/Main/includes.coffee
+++ client/Main/includes.coffee
@@ -14,6 +14,7 @@

   # mainapp controllers
   "activitycontroller.coffee",
+  "socialapicontroller.coffee",
   "notificationcontroller.coffee",
   "linkcontroller.coffee",
   "oauthcontroller.coffee",


code style: use local variables

code style: use local variables

--- client/ide/fs/fsitem.coffee
+++ client/ide/fs/fsitem.coffee
@@ -285,7 +285,7 @@
     @osKite.vmStart()

     .then =>
-      @osKite.fsEnsureNonexistentPath(path: @getPath())
+      @osKite.fsEnsureNonexistentPath(path: newpath)

     .then (response) =>
       @osKite.fsRename(


Shared VM: Remove shared machine item from sidebar when the participant is kicked

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -15,6 +15,7 @@
 FSHelper                      = require 'app/util/fs/fshelper'
 isKoding                      = require 'app/util/isKoding'
 showError                     = require 'app/util/showError'
+actionTypes                   = require 'app/flux/environment/actiontypes'
 KDModalView                   = kd.ModalView
 KDSplitView                   = kd.SplitView
 IDEWorkspace                  = require './workspace/ideworkspace'
@@ -1662,6 +1663,8 @@

   showUserRemovedModal: ->

+    { reactor } = kd.singletons
+
     options        =
       title        : 'Machine access revoked'
       content      : 'Your access to this machine has been removed by its owner.'
@@ -1671,7 +1674,7 @@
           style    : 'solid light-gray medium'
           title    : 'OK'
           callback : =>
-
+            reactor.dispatch actionTypes.SHARED_VM_INVITATION_REJECTED, @mountedMachine._id
             @modal.destroy()
             @quit()


Allow better method chaining.

Allow better method chaining.

--- client/lib/annotator.coffee
+++ client/lib/annotator.coffee
@@ -23,8 +23,12 @@
   _setupEditor: =>
     # Overridden and disabled

+    @ # For chaining
+
   _setupDynamicStyle: =>
     # Overridden and disabled
+
+    @ # For chaining

   startViewerHideTimer: =>
     # Overridden and disabled
@@ -35,12 +39,14 @@
   _setupWrapper: =>
     @wrapper = $(document)

+    @ # For chaining
+
   _setupDocumentEvents: =>
     $(document).on 'mousedown': (e) =>
       @checkForStartSelection e if e.which is 1 # Left mouse button
       return # Make sure CoffeeScript does not return anything

-    @
+    @ # For chaining

   checkForStartSelection: (event) =>
     super


Closes #50 giCrud service now only uses promises

--- client/services/crud.coffee
+++ client/services/crud.coffee
@@ -2,7 +2,7 @@
 , ['$resource', '$q', 'giSocket'
 , ($resource, $q, Socket) ->

-  factory = (resourceName, usePromises, prefix, idField) ->
+  factory = (resourceName, prefix, idField) ->

     if not prefix?
       prefix = '/api'
@@ -48,69 +48,53 @@
       itemsById[newItem[idField]] = newItem
       return

-    all = (params, callback) ->
+    all = (params) ->
+      deferred = $q.defer()
       options = {}
       cacheable = true
       r = resource

-      if _.isFunction(params)
-        callback = params
-        if items.length > 0
-          callback items if callback
-          return
+      if not params? and items.length > 0
+        deferred.resolve items
       else
-        cacheable = false
+        if params?
+          cacheable = false
+
         options = params

-        if params.query?
+        if params?.query?
           r = queryResource

-      r.query options, (results) ->
-        if cacheable
-          items = results
-          angular.forEach results, (item, index) ->
-            itemsById[item[idField]] = item
-            return
+        r.query options, (results) ->
+          if cacheable
+            items = results
+            angular.forEach results, (item, index) ->
+              itemsById[item[idField]] = item

-        callback results if callback
-
-    allPromise = (params) ->
-      deferred = $q.defer()
-
-      if params
-        all params, (results) ->
-          deferred.resolve results
-      else
-        all (results) ->
           deferred.resolve results

       deferred.promise

-    save = (item, success, fail) ->
+    save = (item) ->
+      deferred = $q.defer()
+
       if item[idField]
         #we are updating
         resource.save {id: item[idField]}, item, (result) ->
           updateMasterList result
-          success(result) if success
+          deferred.resolve result
         , (failure) ->
-          fail(failure) if fail
-
+          deferred.reject failure
       else
         #we are createing a new object on the server
         resource.create {}, item, (result) ->
           updateMasterList result
-          success(result) if success
+          deferred.resolve result
         , (failure) ->
-          fail(failure) if fail
-
-    savePromise = (item) ->
-      deferred = $q.defer()
-      save item, (res) ->
-        deferred.resolve res
-      , (err) ->
-        deferred.reject err
+          deferred.reject failure

       deferred.promise
+

     getCached = (id) ->
       return itemsById[id]
@@ -118,20 +102,17 @@
     allCached = () ->
       return items

-    get = (id, callback) ->
+    get = (id) ->
+      deferred = $q.defer()
       resource.get {id: id}, (item) ->
         if items.length > 0
           updateMasterList item
-        callback item if callback
-
-    getPromise = (id) ->
-      deferred = $q.defer()
-      get id, (item) ->
         deferred.resolve item

       deferred.promise

-    destroy = (id, callback) ->
+    destroy = (id) ->
+      deferred = $q.defer()
       resource.delete {id: id}, () ->
         removed = false
         delete itemsById[id]
@@ -141,12 +122,8 @@
               removed = true
               items.splice index, 1

-        callback() if callback
+        deferred.resolve()

-    destroyPromise = (id) ->
-      deferred = $q.defer()
-      destroy id, () ->
-        deferred.resolve()
       deferred.promise

     count = () ->
@@ -182,13 +159,6 @@
       version: version
       clearCache: clearCache

-    if usePromises
-      exports.all = allPromise
-      exports.query = allPromise
-      exports.get = getPromise
-      exports.save = savePromise
-      exports.destroy = destroyPromise
-
     exports

   #export the crud factory method


Add proxy config

Add proxy config

--- src/tabelog.coffee
+++ src/tabelog.coffee
@@ -14,6 +14,10 @@
 _         = require 'underscore'
 cheerio   = require 'cheerio'
 request   = require 'request'
+
+if proxy = process.env.HUBOT_TABELOG_PROXY
+  console.log 'proxy: ' + proxy
+  request = request.defaults {'proxy': proxy}

 module.exports = (robot) ->
   robot.respond /tabelog(:? (lunch|dinner))?( for [^ ]+)?( (:?in|at) [^ ]+)?$/i, (msg) ->


Fix app.DB#version returning boolean instead version

--- assets/javascripts/app/db.coffee
+++ assets/javascripts/app/db.coffee
@@ -98,7 +98,7 @@
       store = txn.objectStore('docs')

       req = store.get(doc.slug)
-      req.onsuccess = -> fn(!!req.result)
+      req.onsuccess = -> fn(req.result)
       req.onerror = -> fn(false)
       return
     return


Remove redundant stub

Remove redundant stub

--- tests/unit_tests/test_chrome_stubs.coffee
+++ tests/unit_tests/test_chrome_stubs.coffee
@@ -95,5 +95,3 @@
         callback() if callback
         # Now, generate (supposedly asynchronous) notification for listeners.
         global.chrome.storage.onChanged.callEmpty(key)
-
-exports.setTimeout = (callback,timeout) -> callback()


Revert "trigger commands from the browser process on the active.element"

This reverts commit f023ce89096b1c7a6faf337cf48d29cfbb296091.

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -9,7 +9,7 @@
 class WindowEventHandler
   constructor: ->
     @subscribe ipc, 'command', (command, args...) ->
-      $(document.activeElement).trigger(command, args...)
+      $(window).trigger(command, args...)

     @subscribe $(window), 'focus', -> $("body").removeClass('is-blurred')
     @subscribe $(window), 'blur',  -> $("body").addClass('is-blurred')


check for `on` method

check for `on` method

during `window-event-handler-spec`, `applicationDelegate.getCurrentWindow`
seems to return a truncated version of a browserWindow object, which does not include the `on` method

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -24,8 +24,8 @@
     @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit)

     browserWindow = @applicationDelegate.getCurrentWindow()
-    browserWindow.on 'enter-full-screen', @handleEnterFullScreen
-    browserWindow.on 'leave-full-screen', @handleLeaveFullScreen
+    browserWindow.on? 'enter-full-screen', @handleEnterFullScreen
+    browserWindow.on? 'leave-full-screen', @handleLeaveFullScreen

     @subscriptions.add @atomEnvironment.commands.add @window,
       'window:toggle-full-screen': @handleWindowToggleFullScreen


fix(snooze): Only allow snooze from inbox

--- internal_packages/thread-list/lib/thread-list.cjsx
+++ internal_packages/thread-list/lib/thread-list.cjsx
@@ -125,30 +125,31 @@
         Actions.queueTasks(tasks)
         callback(true)

-    props.onSwipeLeftClass = 'swipe-snooze'
-    props.onSwipeLeft = (callback) =>
-      # TODO this should be grabbed from elsewhere
-      {PopoverStore} = require 'nylas-exports'
-      SnoozePopoverBody = require '../../thread-snooze/lib/snooze-popover-body'
-
-      # TODO
-      # The question I want to ask is if I am already swiping, i.e. mid swipe,
-      # but I don't know how to ask it.
-      # This is good enough for now
-      if PopoverStore.isPopoverOpen()
-        callback(false)
-        return
-
-      element = document.querySelector("[data-item-id=\"#{item.id}\"]")
-      rect = element.getBoundingClientRect()
-      Actions.openPopover(
-        <SnoozePopoverBody
-          threads={[item]}
-          swipeCallback={callback}
-          closePopover={Actions.closePopover}/>,
-        rect,
-        "right"
-      )
+    if perspective.isInbox()
+      props.onSwipeLeftClass = 'swipe-snooze'
+      props.onSwipeLeft = (callback) =>
+        # TODO this should be grabbed from elsewhere
+        {PopoverStore} = require 'nylas-exports'
+        SnoozePopoverBody = require '../../thread-snooze/lib/snooze-popover-body'
+
+        # TODO
+        # The question I want to ask is if I am already swiping, i.e. mid swipe,
+        # but I don't know how to ask it.
+        # This is good enough for now
+        if PopoverStore.isPopoverOpen()
+          callback(false)
+          return
+
+        element = document.querySelector("[data-item-id=\"#{item.id}\"]")
+        rect = element.getBoundingClientRect()
+        Actions.openPopover(
+          <SnoozePopoverBody
+            threads={[item]}
+            swipeCallback={callback}
+            closePopover={Actions.closePopover}/>,
+          rect,
+          "right"
+        )

     props


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -5,7 +5,7 @@
     width: 32
     height: 32

-  Ap
+  ap

   self.bind "update", ->
     I.x = I.x.clamp(I.width / 2, App.width - I.width / 2)


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -1,6 +1,6 @@
 ###*
 The ClampBounds module adds a check to make sure
-that the including GameObject doe
+that the including GameObject doesn't

 # Health regeneration


now the newly created tab is activated

now the newly created tab is activated

--- src/coffee/views.coffee
+++ src/coffee/views.coffee
@@ -134,6 +134,7 @@
         model: s_pc, render_loop:true,
         el: $CDX.main_tab_set.add_tab_el(
           tab_name:"plot#{plot_num}",  view: {}, route:"plot#{plot_num}"))
+      $CDX.main_tab_set.activate("plot#{plot_num}")

   build_children : () ->
     @mainlist = $("

")


Resolve ????? error with purchase_invoice totals headers

--- app/assets/javascripts/backend/purchase_invoices.js.coffee
+++ app/assets/javascripts/backend/purchase_invoices.js.coffee
@@ -16,6 +16,9 @@
         amountExcludingTaxes = $(item).find('.pretax-amount-column label.amount-excluding-taxes').text()
         vatRate = $(item).find('.total-column label.vat-rate').text().split("%")[0]
         amountIncludingTaxes = $(item).find('.total-column label.amount-including-taxes').text()
+
+        if amountExcludingTaxes == "??????" || amountIncludingTaxes == "??????" || vatRate == "??????"
+          return

         totalAmountExcludingTaxes += parseFloat(amountExcludingTaxes)
         totalAmountIncludingTaxes += parseFloat(amountIncludingTaxes)


[Cartilage.Views.ListView] Restore support for deselecting previously selected items with the meta key.

--- app/assets/javascripts/cartilage/views/list_view.js.coffee
+++ app/assets/javascripts/cartilage/views/list_view.js.coffee
@@ -169,12 +169,18 @@
   #
   onMouseDown: (event) =>

+    # Get the list item element
+    element = ($ event.target).parents("li") || event.target
+
     # Clear the selection if the user clicks in the list container.
     @clearSelection() if @allowsDeselection and event.target.tagName == "UL"

     if event.metaKey
-      element = event.target
-      @selectAnother ($ element).parents("li") || element
+      model = ($ element).data("model")
+      if model in @selected.models
+        @deselect element
+      else
+        @selectAnother element
       event.preventDefault()

     else if event.shiftKey


fix wrong path for icons in dashboard when using show-user-view

--- app/assets/javascripts/views/spotlight_dataset_view.coffee
+++ app/assets/javascripts/views/spotlight_dataset_view.coffee
@@ -15,13 +15,13 @@

           <a href="/datasets/<%= name %>/view" title="View tracing">
-
+

-
+

-
+


add inquiry request route

add inquiry request route

--- node/server.coffee
+++ node/server.coffee
@@ -138,4 +138,7 @@
 gravity.post '/api/v1/me/unsubscribe*', (req, res) ->
   res.send [fabricate 'user']

+gravity.post '/api/v1/me/artwork_inquiry_request', (req, res) ->
+  res.send fabricate 'user'
+
 gravity.all '*', (req, res) -> res.send 404, "Not Found."


Add supervisord memmon environment configuration

--- config/main.sandbox.coffee
+++ config/main.sandbox.coffee
@@ -762,6 +762,10 @@
   KONFIG.supervisord.unix_http_server =
     file : "#{KONFIG.supervisord.rundir}/supervisor.sock"

+  KONFIG.supervisord.memmon =
+    limit : '3072MB'
+    email : 'sysops+supervisord-sandbox@koding.com'
+

   #-------------------------------------------------------------------------#
   #---- SECTION: AUTO GENERATED CONFIGURATION FILES ------------------------#


sandbox: premium broker configuration

sandbox: premium broker configuration

--- config/main.sandbox.coffee
+++ config/main.sandbox.coffee
@@ -171,13 +171,13 @@
       broker    :
         servicesEndpoint: "/-/services/broker"
       premiumBroker    :
-        servicesEndpoint: "/-/services/premiumBroker"
+        servicesEndpoint: "/-/services/broker"
       brokerKite:
         servicesEndpoint: "/-/services/brokerKite"
         brokerExchange: 'brokerKite'
       premiumBrokerKite:
-        servicesEndpoint: "/-/services/premiumBrokerKite"
-        brokerExchange: 'premiumBrokerKite'
+        servicesEndpoint: "/-/services/brokerKite"
+        brokerExchange: 'brokerKite'
       apiUri    : 'https://koding.com'
       appsUri   : 'https://rest.kd.io'
       uploadsUri: 'https://koding-uploads.s3.amazonaws.com'
@@ -240,7 +240,7 @@
     webProtocol       : 'https:'
     authExchange      : authExchange
     authAllExchange   : authAllExchange
-    failoverUri       : 'stage-premiumbroker.koding.com'
+    failoverUri       : 'sandbox.koding.com'
   brokerKite          :
     name              : "brokerKite"
     serviceGenericName: "brokerKite"
@@ -256,13 +256,13 @@
     name              : "premiumBrokerKite"
     serviceGenericName: "brokerKite"
     ip                : ""
-    port              : 443
+    port              : 453
     certFile          : "/opt/ssl_certs/wildcard.koding.com.cert"
     keyFile           : "/opt/ssl_certs/wildcard.koding.com.key"
     webProtocol       : 'https:'
     authExchange      : authExchange
     authAllExchange   : authAllExchange
-    failoverUri       : 'stage-premiumbrokerkite.koding.com'
+    failoverUri       : 'sandbox.koding.com'
   kites:
     disconnectTimeout: 3e3
     vhost       : 'kite'


Support jQuery < 1.6 by not using $.fn.is().

Adapted from rooby's pull request for issue #1555.

--- coffee/chosen.jquery.coffee
+++ coffee/chosen.jquery.coffee
@@ -166,7 +166,8 @@

   test_active_click: (evt) ->
-    if @container.is($(evt.target).closest('.chosen-container'))
+    active_container = $(evt.target).closest('.chosen-container')
+    if active_container.length and @container[0] == active_container[0]
       @active_field = true
     else
       this.close_field()


Convert to jquery storyturtle plugin

Convert to jquery storyturtle plugin

--- storyturtle.coffee
+++ storyturtle.coffee
@@ -3,9 +3,9 @@
 License MIT
 http://simplectic.com/story_turtle
 ###
-$ ->
-  game = $("#game")
-    .hide()
+$ = @jQuery
+$.fn.storyturtle = ->
+  game = @hide()

   editor = $("<textarea>",
     rows: 15


renaming button to be clearer

renaming button to be clearer

--- src/components/app.cjsx
+++ src/components/app.cjsx
@@ -88,7 +88,7 @@

             <BS.ButtonToolbar className="navbar-btn">
-              New Exercise
+              New Blank Exercise
               <AsyncButton
                 bsStyle='info'
                 onClick={@saveExercise}


Retry getting url on error or when server unavailable page returned while crawling

--- tasks/crawl.coffee
+++ tasks/crawl.coffee
@@ -20,10 +20,16 @@
       if res.statusCode == 200
         data = ''
         res.on 'data', (chunk) -> data += chunk
-        res.on 'end', -> callback data
+        res.on 'end', ->
+          if data.length == 2740
+            console.log "Server unavailable while getting #{urlStr}."
+            get(urlStr, callback)
+          else
+            callback data
         res.pipe fs.createWriteStream(cachePath(urlStr))
       else
-        grunt.warn "#{res.statusCode} while getting #{urlStr}."
+        console.log "#{res.statusCode} while getting #{urlStr}."
+        get(urlStr, callback)
     .on 'error', (err) -> grunt.warn "#{err} while getting #{urlStr}."

   queue = grunt.utils.async.queue (filename, callback) ->


Added 'danger' alert type for Bootstrap 3 compatibility (error is now danger)

--- src/flashular.coffee
+++ src/flashular.coffee
@@ -50,7 +50,7 @@
     """
   link: (scope, iElement, iAttrs) ->
     scope.flash = flash.now
-    scope.alertTypes = ["info", "success", "error", "warning"]
+    scope.alertTypes = ["info", "success", "error", "warning", "danger"]
     if not iAttrs.preProcess?
       # Define a default preProcess function that does no processing of the alert.
       scope.preProcess = (alert) -> $interpolate("{{alert}}")(alert)


added button test

added button test

[#95231438]

--- spec/javascripts/acceptance/projects_index_page.js.coffee
+++ spec/javascripts/acceptance/projects_index_page.js.coffee
@@ -124,3 +124,8 @@
     equal(findFirstProjectAccepted(), '1')
     equal(findFirstProjectTotal(), '6')
     equal(findFirstProjectStatus(), 'good')
+
+test "find API key buttons", ->
+  visit '/projects'
+  andThen ->
+    equal(find(".copy-to-clipboard").length, 2)


JRaphical: remove data field from query param

--- node_modules_koding/jraphical/lib/module.coffee
+++ node_modules_koding/jraphical/lib/module.coffee
@@ -371,6 +371,7 @@
             callback err
           else if options?.respondWithCount
             delete rdef.targetId
+            delete rdef.data
             Relationship.count rdef, (err, count)->
               if err
                 callback err


fix blank messages with ajax queries

fix blank messages with ajax queries

--- app/assets/javascripts/flash_message.js.coffee
+++ app/assets/javascripts/flash_message.js.coffee
@@ -18,4 +18,5 @@
 $(document).ajaxComplete (event, request) ->
   msg = request.getResponseHeader("X-Message")
   type = request.getResponseHeader("X-Message-Type")
-  show_ajax_message msg, type+  if msg
+    show_ajax_message msg, type


optimize js graphs-uri determination.

optimize js graphs-uri determination.

--- app/assets/javascripts/plugins/graph.js.coffee
+++ app/assets/javascripts/plugins/graph.js.coffee
@@ -13,11 +13,7 @@
 sentence_klasses = ["non_theorem", "proven", "unproven"]

 graphs_uri = window.location.href
-re = /\?[^/]+$/
-if graphs_uri.search(re) != -1
-  graphs_uri = graphs_uri.replace(re, "/graphs$&")
-else
-  graphs_uri += "/graphs" if graphs_uri.search(/\/graphs/) == -1
+  .replace(/\/graphs(\?[^/]+|\/)?$/, '/graphs')

 d3.select('div#graph_depth_setting ul li a').on 'click', (e) ->
   d3.event.preventDefault()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/controllable.coffee
+++ src/controllable.coffee
@@ -4,7 +4,7 @@

   self.bind "update", ->
     if keydown.left
-      I.x
+      I.x -= I.spe


Sort filenames when creating a content tree

--- src/core/content.coffee
+++ src/core/content.coffee
@@ -163,6 +163,7 @@
     fs.readdir directory, callback

   resolveFilenames = (filenames, callback) ->
+    filenames.sort()
     async.map filenames, (filename, callback) ->
       relname = path.join reldir, filename
       callback null,


change updatecache to split out attachments

--- tools/updatecache.coffee
+++ tools/updatecache.coffee
@@ -16,6 +16,14 @@
 if !fs.existsSync(outdir)
   console.log "Error: output directory does not exist: #{outdir}"
   process.exit -1
+
+attachdir = "#{outdir}/attachments"
+if !fs.existsSync(attachdir)
+  try
+    fs.mkdirSync attachdir
+  catch err
+    console.log "Error: could not create attachment  directory #{attachdir}: #{err.message}"
+    process.exit -1

 # try to read config
 configpath = "#{outdir}/_updatecache.json"
@@ -194,19 +202,66 @@
     if revs.length==0
       return fn()
     rev = (revs.splice 0,1)[0]
-    getpath="/#{encodeURIComponent rev.id}?rev=#{encodeURIComponent rev.rev}&revs=true&attachments=true"
+    getpath="/#{encodeURIComponent rev.id}?rev=#{encodeURIComponent rev.rev}&revs=true" #&attachments=true"
     # TODO atts_since based on last checkpoint!
     atts_since = []
     if oldResults?
       for res in oldResults when res.id==rev.id
         for r in res.changes
           atts_since.push r.rev
-    if atts_since.length>0
-      getpath = getpath+"&atts_since=#{encodeURIComponent JSON.stringify atts_since}"
+    #if atts_since.length>0
+    #  getpath = getpath+"&atts_since=#{encodeURIComponent JSON.stringify atts_since}"
     downloadFile couchurl+getpath,"#{checkpointDir}/#{rev.id}/#{rev.rev}", (err,path) ->
       if err
         console.log "Error downloading #{getpath}: #{err}"
         process.exit -1
+      # TODO attachments...
+      try
+        doc = JSON.parse fs.readFileSync path,encoding:'utf8'
+      catch err
+        console.log "Error reading downloaded doc #{path}: #{err}"
+        process.exit -1
+      if doc._attachments?
+        atts = []
+        for name,att of doc._attachments
+          atts.push {name:name, info:att}
+        getAtts = (fn) ->
+          if atts.length==0
+            return fn()
+          att = (atts.splice 0,1)[0]
+          console.log "- #{rev.id} attachment #{att.name} hash #{att.info.digest}"
+          docattachdir = "#{attachdir}/#{rev.id}"
+          if !fs.existsSync(docattachdir)
+            try
+              fs.mkdirSync docattachdir
+            catch err
+              console.log "Error: could not create attachment  directory #{docattachdir}: #{err.message}"
+              process.exit -1
+          # hopefully rev will ensure uniqueness/no race
+          attachpath = "/#{encodeURIComponent rev.id}/#{encodeURIComponent att.name}?rev=#{encodeURIComponent rev.rev}"
+          # / in base64 = bad
+          attachfile = "#{attachdir}/#{rev.id}/#{att.info.digest.replace '/', '_'}"
+          if fs.existsSync attachfile
+            try
+              state = fs.statSync attachfile
+              if state.isFile
+                # check size
+                if state.size == att.info.length
+                  console.log "skip download existing attachment #{attachfile}"
+                  return getAtts fn
+                console.log "Error: existing attachment wrong size #{state.size} vs #{att.info.length}"
+            catch err
+              console.log "Error: stat'ing #{attachfile}: #{err.message}"
+              process.exit -1
+
+          downloadFile couchurl+attachpath,attachfile, (err,path) ->
+            if err
+              console.log "Error downloading #{attachpath}: #{err}"
+              process.exit -1
+            getAtts fn
+
+        return getAtts () -> getRevs fn
+
       getRevs fn

   getRevs ()->


removed last refresh-artefact.

removed last refresh-artefact.

--- public/views/cart_detail_view.coffee
+++ public/views/cart_detail_view.coffee
@@ -128,7 +128,6 @@

     if win
       App.closeModal()
-      App.cart.needsRefresh()
     else
       # In case something blocked the new tab/window,
       # just open the checkout in the current page.


Review pipeline: Added websocket

Review pipeline: Added websocket

--- app/scripts/controllers/review-pipeline.coffee
+++ app/scripts/controllers/review-pipeline.coffee
@@ -1,23 +1,27 @@
 'use strict'

 class ReviewPipelineCtrl
-  constructor: (@scope, @NextJobsFcty) ->
+  constructor: (@scope, @NextJobsFcty, @WebSocketFcty) ->
     @reviews = []
     @reviewCount = 0

     @getReviews()

+    @WebSocketFcty.on((message) =>
+      @getReviews()
+    )
+
+  _fillReviews: (data) =>
+    @reviews = data.pages
+    @reviewCount = data.reviewCount

   getReviews: (currentPage, pageSize) ->
-    @NextJobsFcty.one('').get({current_page: currentPage, page_size: pageSize}).then( (data) =>
-      @reviews = data.pages
-      @reviewCount = data.reviewCount
-    )
+    @NextJobsFcty.one('').get({current_page: currentPage, page_size: pageSize}).then(@_fillReviews)

   updateReviews: (currentPage, pageSize) =>
     @getReviews(currentPage, pageSize)

 angular.module('holmesApp')
-  .controller 'ReviewPipelineCtrl', ($scope, NextJobsFcty) ->
-    $scope.model = new ReviewPipelineCtrl($scope, NextJobsFcty)
+  .controller 'ReviewPipelineCtrl', ($scope, NextJobsFcty, WebSocketFcty) ->
+    $scope.model = new ReviewPipelineCtrl($scope, NextJobsFcty, WebSocketFcty)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color2.coffee
+++ test/color2.coffee
@@ -15,4 +15,6 @@
   equal color.g, 1
   equal color.b, 2

+test "accepts
+
 module()


Minor changes to config test

Minor changes to config test

--- test/config.coffee
+++ test/config.coffee
@@ -29,7 +29,7 @@
         assert.equal topic.cols, 30
       'rows is 15': (topic)->
         assert.equal topic.rows, 15
-     'when retrieving controls':
+    'when retrieving controls':
       topic: config.controls
       'controls is defined': (topic)->
         assert.isObject topic
@@ -40,8 +40,7 @@
       'height is 25': (topic)->
         assert.equal 25, topic.height
     'when retrieving feature':
-      topic: ->
-        return config.feature
+      topic: -> config.feature
       'it is a function': (topic)->
         assert.isFunction topic
       'it is same as feature': (topic)->


handle mixed case URLs

handle mixed case URLs

 On branch master

	modified:   controllers/gallery.coffee

--- controllers/gallery.coffee
+++ controllers/gallery.coffee
@@ -102,7 +102,14 @@

     # We have to rewrite the request path before the static handler gets it.
     parts = req.path.split('/').slice(5)
-    req.url = "#{widgetId}/#{parts.join('/')}"
+    reqURL = "/#{widgetId}/#{parts.join('/')}"
+
+    parts = GALLERY.split('/').concat(reqURL.split('/'))
+    abspath = pathFromArray(parts)
+    if not abspath.exists()
+        reqURL = reqURL.toLowerCase()
+
+    req.url = reqURL
     return next()

@@ -126,6 +133,13 @@
         static_domain: "http://#{static_domain}"
     res.updateContext(ext)
     return next()
+
+
+pathFromArray = (arr) ->
+    path = PATH.newPath.apply(PATH, arr)
+    if arr[0] is '' then path = PATH.newPath('/'+ path)
+    return path
+

 # Extract a widget zip archive and write it to disk.
 saveWidget = (opts) ->


Tweak tests

Tweak tests

--- test/event_emitter_tests.coffee
+++ test/event_emitter_tests.coffee
@@ -10,80 +10,79 @@

     beforeEach ->
         emitter = new EventEmitter()
+
+    it "will add a listener with an event name", ->
+        event_handler = ->
+        emitter.on event_name, event_handler
+        assert.equal emitter._events[event_name][0]["listener"], event_handler, "Handler did not match."

-    describe "on method", ->
-        it "will add a listener with an event name", ->
-            event_handler = ->
-            emitter.on event_name, event_handler
-            assert.equal emitter._events[event_name][0]["listener"], event_handler, "Handler did not match."
+    it "will add a listener without an event name", ->
+        event_handler = (event) ->
+        emitter.on null, event_handler
+        assert.equal emitter._events["__all_events"][0]["listener"], event_handler, "Handler did not match."

-        it "will add a listener without an event name", ->
-            event_handler = (event) ->
-            emitter.on null, event_handler
-            assert.equal emitter._events["__all_events"][0]["listener"], event_handler, "Handler did not match."
+    it "will trigger a listener for an event", ->
+        debugger
+        did_trigger = false
+        emitter.on event_name, ->
+            did_trigger = true
+        emitter.trigger new Event event_name
+        assert.ok did_trigger

-        it "will trigger a listener for an event", ->
-            debugger
-            did_trigger = false
-            emitter.on event_name, ->
-                did_trigger = true
-            emitter.trigger new Event event_name
-            assert.ok did_trigger
+    it "will trigger all listeners for an event in order added", ->
+        count = 0
+        emitter.on event_name, ->
+            count += 1
+            assert.equal count, 1
+        emitter.on event_name, ->
+            count += 1
+            assert.equal count, 2
+        emitter.trigger new Event event_name

-        it "will trigger all listeners for an event in order added", ->
-            count = 0
-            emitter.on event_name, ->
-                count += 1
-                assert.equal count, 1
-            emitter.on event_name, ->
-                count += 1
-                assert.equal count, 2
-            emitter.trigger new Event event_name
+    it "will not trigger an event after it has been removed", ->
+        count = 0
+        handler_one = ->
+            count += 1
+            assert.fail "Should not have called this handler"
+        emitter.on event_name, handler_one
+        emitter.on event_name, ->
+        emitter.off event_name, handler_one
+        emitter.trigger new Event event_name

-        it "will not trigger an event after it has been removed", ->
-            count = 0
-            handler_one = ->
-                count += 1
-                assert.fail "Should not have called this handler"
-            emitter.on event_name, handler_one
-            emitter.on event_name, ->
-            emitter.off event_name, handler_one
-            emitter.trigger new Event event_name
+    it "will wait for all events to complete before calling completion", (done) ->
+        count = 0
+        emitter.on event_name, (event, completion) ->
+            count += 1
+            assert.equal count, 1
+            setTimeout ->
+                completion()
+            , 10
+            return false
+        emitter.on event_name, (event, completion) ->
+            count += 1
+            assert.equal count, 2
+            setTimeout ->
+                completion()
+            , 10
+        event = new Event event_name
+        emitter.trigger event, ->
+            done()

-        it "will wait for all events to complete before calling completion", (done) ->
-            count = 0
-            emitter.on event_name, (event, completion) ->
-                count += 1
-                assert.equal count, 1
-                setTimeout ->
-                    completion()
-                , 10
-                return false
-            emitter.on event_name, (event, completion) ->
-                count += 1
-                assert.equal count, 2
-                setTimeout ->
-                    completion()
-                , 10
-            event = new Event event_name
-            emitter.trigger event, ->
-                done()
+    it "will stop handling events if true is returned", ->
+        count = 0
+        emitter.on event_name, ->
+            count += 1
+            assert.equal count, 1
+            return true
+        emitter.on event_name, ->
+            count += 1
+        event = new Event event_name
+        emitter.trigger event
+        assert.equal count, 1

-        it "will stop handling events if true is returned", ->
-            count = 0
-            emitter.on event_name, ->
-                count += 1
-                assert.equal count, 1
-                return true
-            emitter.on event_name, ->
-                count += 1
-            event = new Event event_name
-            emitter.trigger event
-            assert.equal count, 1
-
-        it "can listen with a specified scope", ->
-            scope = {}
-            emitter.on event_name, ->
-                assert.equal @, scope
-            , scope
-            emitter.trigger new Event event_name
+    it "can listen with a specified scope", ->
+        scope = {}
+        emitter.on event_name, ->
+            assert.equal @, scope
+        , scope
+        emitter.trigger new Event event_name


Pass along session_id all the time

Pass along session_id all the time

--- models/collector_profile.coffee
+++ models/collector_profile.coffee
@@ -1,7 +1,7 @@
 _ = require 'underscore'
 Q = require 'q'
 Backbone = require 'backbone'
-{ API_URL } = require('sharify').data
+{ SESSION_ID, API_URL } = require('sharify').data
 Relations = require './mixins/relations/collector_profile.coffee'

 module.exports = class CollectorProfile extends Backbone.Model
@@ -9,8 +9,11 @@

   url: "#{API_URL}/api/v1/me/collector_profile"

+  defaults:
+    session_id: SESSION_ID
+
   fetch: (options = {}) ->
-    options.data = _.extend options.data or {}, @pick('anonymous_session_id')
+    options.data = _.extend options.data or {}, @pick('anonymous_session_id'), session_id: SESSION_ID
     super options

   instantiate: (options = {}) ->


improve test

improve test

--- test/hinoki.coffee
+++ test/hinoki.coffee
@@ -55,6 +55,8 @@
         'missing factory': (test) ->
             container = {}

-            test.throws ->
+            block = ->
                 hinoki.inject container, (a) ->
+
+            test.throws block, Error, "missing factory for service 'a'"
             test.done()


Tiny bit of cleanup

Tiny bit of cleanup

--- src/globber/utils.coffee
+++ src/globber/utils.coffee
@@ -26,7 +26,7 @@

 exports.isFile = (path, cb) ->
   fs.stat path, (err, stats) ->
-    if err? then return cb(err) else cb(null, stats.isFile())
+    if err? then cb(err) else cb(null, stats.isFile())

 exports.isFileSync = (path) ->
   (fs.statSync path).isFile()


Environments: removed environments link from account dropdown

--- client/Main/avatararea/avatarareagroupswitcherpopup.coffee
+++ client/Main/avatararea/avatarareagroupswitcherpopup.coffee
@@ -129,16 +129,6 @@
         router.handleRoute '/Account'
         @hide()

-
-    @avatarPopupContent.addSubView new KDCustomHTMLView
-      tagName    : 'a'
-      attributes : href : '/Environments'
-      partial    : 'Environments'
-      click      : (event)=>
-        KD.utils.stopDOMEvent event
-        router.handleRoute '/Environments'
-        @hide()
-
     @avatarPopupContent.addSubView new KDCustomHTMLView
       tagName    : 'a'
       partial    : 'System health check'
@@ -167,7 +157,6 @@
     if (Cookies.get cookieName) is "1"
       @avatarPopupContent.addSubView new KDCustomHTMLView
         tagName    : 'a'
-
         partial    : 'Switch back to secure (https) mode'
         click      : (event)=>
           KD.utils.stopDOMEvent event


payment: remove paymentformmodal

payment: remove paymentformmodal

--- client/landing/app/MainApp/payment/paymentformmodal.coffee
+++ client/landing/app/MainApp/payment/paymentformmodal.coffee
@@ -1,90 +0,0 @@
-class PaymentFormModal extends KDModalView
-
-  constructor: (options = {}, data = {}) ->
-
-    options.title    or= 'Billing information'
-    options.width    or= 520
-    options.height   or= 'auto'
-    options.cssClass or= 'payments-modal'
-    options.overlay   ?= yes
-
-    super options, data
-
-  viewAppended:->
-
-    @mainLoader = new KDLoaderView
-      showLoader  : yes
-      size        : { width: 14 }
-
-    @addSubView @mainLoader
-
-    @useExistingView = new PaymentMethodChoiceView
-    @useExistingView.hide()
-
-    @useExistingView.on 'PaymentMethodSelected', (paymentMethodId) =>
-      if paymentMethodId
-        @emit 'PaymentMethodSelected', paymentMethodId
-      else
-        @useExistingView.hide()
-        @paymentForm.show()
-
-    @addSubView @useExistingView
-
-    @paymentForm = new PaymentForm
-
-    @forwardEvent @paymentForm, 'PaymentInfoSubmitted'
-    @addSubView @paymentForm
-
-    @paymentForm.hide()
-
-    super()
-
-  # showLoader:->
-  #   @mainLoader.show()
-
-  # hideLoader:->
-  #   @mainLoader.hide()
-
-  createSelectPersonalView: (paymentMethods) ->
-    @useExistingView.show()
-    paymentMethods.forEach (paymentMethod) =>
-      @useExistingView.addPaymentMethod paymentMethod
-
-  setState: (state, data) ->
-    @mainLoader.hide()
-    switch state
-      when 'editExisting'
-        @paymentForm.setPaymentInfo data
-        @paymentForm.show()
-      when 'selectPersonal'
-        @createSelectPersonalView data
-      else
-        @paymentForm.show()
-
-  handleFormData:->
-
-  setPaymentInfo: (paymentMethod) ->
-    @paymentForm.setPaymentInfo paymentMethod
-
-  handleRecurlyResponse:(callback, err) ->
-    @paymentForm.buttons.Save.hideLoader()
-
-    recurlyFieldMap =
-      first_name         : 'cardFirstName'
-      last_name          : 'cardLastName'
-      number             : 'cardNumber'
-      verification_value : 'cardCV'
-
-    for e in err
-      if recurlyFieldMap[e.field]
-        input = @paymentForm.inputs[recurlyFieldMap[e.field]]
-        input.giveValidationFeedback yes
-        input.showValidationError "#{input.inputLabel?.getTitle()} #{e.message}"
-      else
-        input = @paymentForm.inputs.cardNumber
-        input.showValidationError e.message
-        input.giveValidationFeedback yes  if e.message.indexOf('card') > -1
-
-  updateCardTypeDisplay: (cardType) ->
-    console.trace()
-    @paymentForm.updateCardTypeDisplay cardType


The progress bar works correctly now

The progress bar works correctly now

Some changes may be required for existent specs:

- code inside 'describe' blocks used to work like 'beforeAll'

Now they should be placed inside a beforeAll if that was the intention
as the dsl is now eager evaluated so that the progress bar works correctly

--- lib/assets/javascripts/oojspec.js.coffee
+++ lib/assets/javascripts/oojspec.js.coffee
@@ -33,13 +33,13 @@
   runSpecs: ->
     @reporter = buster.reporters.html.create detectCssPath: false
     @reporter.listen @runner
+    d.processDsl @runner for d in @descriptions
     @runner.emit 'suite:start', name: "Specs"
     @runNextDescription()

   runNextDescription: =>
     (@runner.emit 'suite:end', @stats; return) unless @descriptions.length
-    # TODO: think about non null contexts usage
-    @descriptions.shift().run @runner, @assertions, null, false, @runNextDescription
+    @descriptions.shift().run @assertions, @runNextDescription

   describe: (description, block)=>
     @stats.contexts++ # only root descriptions will be count
@@ -56,37 +56,10 @@
       @block = @description
       @description = @block.description or @block.name

-  run: (@runner, @assertions, @binding, @bare, @onFinish, @beforeBlocks = [], @afterBlocks = [])->
-    @runner.emit 'context:start', name: @description
+  processDsl: (@runner, @binding, @bare)->
     @dsl = new DescribeDsl
-    @doRun() unless (@block.runSpecs or @block.prototype?.runSpecs) and @throwOnInvalidBinding()
-
-  doRun: -> @runAround @beforeBlocks, @afterBlocks, @onDescriptionFinished, @processDescriptionBlock
-
-  throwOnInvalidBinding: ->
-    try
-      @binding = if @block.prototype then new @block else @block
-      if @binding and not (@bare = @block.bare)
-        for reserved in RESERVED when @binding[reserved]
-          throw new Error("'#{reserved}' method is reserved for oojspec usage only")
-    catch e
-      e.name = "syntax error"
-      @runner.emit 'test:error', name: @description, error: e
-      @onDescriptionFinished(e)
-      return true
-    false
-
-  onDescriptionFinished: (error)=>
-    if error and not error.handled
-      error.handled = true
-      @runner.emit 'test:error', { name: 'Error running describe statements', error }
-    @runner.emit 'context:end'
-    @onFinish error
-
-  runAround: (befores, afters, onFinish, block)->
-    new AroundBlock(befores, afters, block).run @runner, @assertions, @binding, @bare, onFinish
-
-  processDescriptionBlock: (onFinish)=>
+    (@block.runSpecs or @block.prototype?.runSpecs) and @detectBindingError()
+
     @binding or= {}
     @injectDsl() unless @bare
     if @block.runSpecs or @block.prototype?.runSpecs
@@ -96,12 +69,46 @@
     @runner.emit 'oojspec:examples:add', @dsl._examplesCount_
     @removeDsl() unless @bare
     @bare or= @binding.bare
+
+    d.processDsl @runner, @binding, @bare for d in @dsl._examples_ when d instanceof Description
+
+  detectBindingError: ->
+    try
+      @binding = if @block.prototype then new @block else @block
+      if @binding and not (@bare = @block.bare)
+        for reserved in RESERVED when @binding[reserved]
+          throw new Error("'#{reserved}' method is reserved for oojspec usage only")
+    catch e
+      e.name = "syntax error"
+      @bindingError = e
+
+  injectDsl: -> @binding[p] = v for p, v of @dsl; return
+
+  removeDsl: -> delete @binding[p] for p in RESERVED_FOR_DESCRIPTION_DSL; return
+
+  run: (@assertions, @onFinish, @beforeBlocks = [], @afterBlocks = [])->
+    @runner.emit 'context:start', name: @description
+    if @bindingError
+      @runner.emit 'test:error', name: @description, error: @bindingError
+      @onDescriptionFinished @bindingError
+    else
+      @doRun()
+
+  doRun: -> @runAround @beforeBlocks, @afterBlocks, @onDescriptionFinished, @processDescriptionBlock
+
+  onDescriptionFinished: (error)=>
+    if error and not error.handled
+      error.handled = true
+      @runner.emit 'test:error', { name: 'Error running describe statements', error }
+    @runner.emit 'context:end'
+    @onFinish error
+
+  runAround: (befores, afters, onFinish, block)->
+    new AroundBlock(befores, afters, block).run @runner, @assertions, @binding, @bare, onFinish
+
+  processDescriptionBlock: (onFinish)=>
     @runAround @dsl._beforeAllBlocks_, @dsl._afterAllBlocks_, onFinish, (@onExamplesFinished)=>
       @runNextStep()
-
-  injectDsl: -> @binding[p] = v for p, v of @dsl; return
-
-  removeDsl: -> delete @binding[p] for p in RESERVED_FOR_DESCRIPTION_DSL; return

   runNextStep: =>
     (@onExamplesFinished(); return) unless @dsl._examples_.length
@@ -109,8 +116,7 @@
     (@reportDeferred(nextStep.description); @runNextStep(); return) if nextStep.pending
     nextTick =
       if nextStep instanceof Description then =>
-        nextStep.run @runner, @assertions, @binding, @bare, @runNextStep, \
-          @dsl._beforeBlocks_, @dsl._afterBlocks_
+        nextStep.run @assertions, @runNextStep, @dsl._beforeBlocks_, @dsl._afterBlocks_
       else => # ExampleWithHooks
         nextStep.run @runner, @assertions, @binding, @bare, @onExampleFinished
     setTimeout nextTick, 0


change default flags from 111 to 101

change default flags from 111 to 101

--- src/healthExamine.coffee
+++ src/healthExamine.coffee
@@ -4,7 +4,7 @@
 # Commands:
 #  * ():省略可能, <>:引数, []:どちらか一方
 #  (l2-t2) she ex|examine - サイト生死検査イベントを発火
-#  (l2-t2) she chflag \d\d\d - サイト生死検査イベントの通知設定を変更. 1st: ERROR時, 2nd: Match時, 3rd: Mismatch時 ... ex)101: エラー発生もしくはステータスコード異常の時に通知
+#  (l2-t2) she chflag \d\d\d - サイト生死検査イベントの通知設定を変更. 1st: ERROR時, 2nd: Match時, 3rd: Mismatch時 ... デフォルト値)101: エラー発生もしくはステータスコード異常の時に通知.
 #  (l2-t2) she cron start - ボットのいるチャンネル内でcronを実行
 #
 # Author:
@@ -19,7 +19,7 @@
 Nurse = require('hubot-site-health-manager').NurseWithRedis

 module.exports = (robot) ->
-  flags = [1, 1, 1]
+  flags = [1, 0, 1]

   ### flags manager ###
   robot.hear /she chflag (\d\d\d)$/i, (msg) ->


Change key binding ID to be different for each command

--- lib/controllers/config-controller.coffee
+++ lib/controllers/config-controller.coffee
@@ -27,7 +27,7 @@
       bindings = {};
       binding[@config.keystroke] = @config.getCommandName();
       bindings[cssSelector] = binding;
-      atom.keymaps.add('process-palette', bindings);
+      atom.keymaps.add(@config.getCommandName(), bindings);

     @addMenus();


fix membership model

fix membership model

--- src/node_modules/membership-model/membership-model.coffee
+++ src/node_modules/membership-model/membership-model.coffee
@@ -1,15 +1,15 @@
-module.exports = ->
+module.exports = (UserModel) ->
   class MembershipModel
     constructor: (data = {}) ->
       @id = data.id
       @groupId = data.group_id
-      @userId = data.user_id
+      @user = new UserModel(data.user)

     serialize: ->
       {
         id: @id
         group_id: @groupId
-        user_id: @userId
+        user: @user.serialize()
       }

     save: (MembershipsStore) ->


Minor typo.

Minor typo.

--- test/i_love_u.coffee
+++ test/i_love_u.coffee
@@ -1,5 +1,6 @@
 luv = require "i_love_u"
 assert = require 'assert'
+limit = 10123

 new_luv = (args...) ->
   new luv.i_love_u(args...)
@@ -177,8 +178,7 @@
       u.run()
       assert.deepEqual stack(u), [3]

-    it "does not run past 5123 total loops", () ->
-      limit = 10123
+    it "does not run past #{limit} total loops", () ->
       u = new_luv """
         Number is: 1.
         While "Number not equal to 3":


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/matrix.coffee
+++ test/matrix.coffee
@@ -85,7 +85,9 @@

     matrix.skew(0.125.turns, 0)

-    equals(tran)
+
+
+    equals(matrix.transformPoint(p))

   test ".rotation", ->
     matrix = Matrix.rotation(Math.PI / 2)


Improved error handling if  no network devices found to capturing ARP requests

--- amazing-dash-button.coffee
+++ amazing-dash-button.coffee
@@ -30,7 +30,13 @@

       # auto-discovery
       @framework.deviceManager.on('discover', (eventData) =>
-        @framework.deviceManager.discoverMessage 'pimatic-amazing-dash-button', 'Searching for dash-buttons. Please press dash-button now!'
+        if @noInterfacesFound?
+          @framework.deviceManager.discoverMessage 'pimatic-amazing-dash-button', @noInterfacesFound
+          @base.error @noInterfacesFound
+          return
+        else
+          @framework.deviceManager.discoverMessage 'pimatic-amazing-dash-button', 'Searching for dash-buttons. Please press dash-button now!'
+
         @candidatesSeen = []
         @lastId = null

@@ -69,7 +75,13 @@
         device = cap.findDevice @interfaceAddress
       else
         device = cap.findDevice()
-      @base.debug "Sniffing for ARP requests on device", device
+
+      if device?
+        @base.debug "Sniffing for ARP requests on device", device
+      else
+        @noInterfacesFound = "Error: No suitable network interface found for sniffing ARP requests"
+        @base.error @noInterfacesFound
+        return

       # List of registered Mac addresses with IEEE as of 18 July 2016 for Amazon Technologies Inc.
       # source: https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries


stripping protocol from instagram feed images

--- app/assets/javascripts/mobile/products.js.coffee
+++ app/assets/javascripts/mobile/products.js.coffee
@@ -76,7 +76,7 @@
                         else
                             response.product_images

-                        product_images = _.map(product_images, (product_image) -> { product_image })
+                        product_images = _.map(product_images, (product_image) -> { product_image: product_image.split('http:')[1] })

                         $new_product_image_grid.append(
                             Mustache.render(existing_photo_grid_photos_template, {


Add collectionUUID to context & props

Add collectionUUID to context & props

--- test/progress/index.spec.coffee
+++ test/progress/index.spec.coffee
@@ -8,6 +8,7 @@
 Wrapper = React.createClass
   childContextTypes:
     moduleUUID:     React.PropTypes.string
+    collectionUUID: React.PropTypes.string

   getChildContext: ->
     _.pick @props, 'moduleUUID', 'collectionUUID'
@@ -20,6 +21,7 @@
   beforeEach ->
     @props =
       moduleUUID: 'm_uuid'
+      collectionUUID: 'C_UUID'

     Collection.load("#{@props.collectionUUID}/#{@props.moduleUUID}", _.extend({}, TASK, @props))


Trivial test cases.

Trivial test cases.

--- test/mvstar.coffee
+++ test/mvstar.coffee
@@ -1,3 +1,7 @@
+mvstar = require '../lib'
+should = require('chai').should()
+
 describe 'mvstar', ->
-  it 'should mvstar', (done) ->
-    done()
+  it 'should expose Model and View constructors', ->
+    mvstar.View.should.exist
+    mvstar.Model.should.exist


Started meatballing spaghetti code

Started meatballing spaghetti code

--- app/assets/javascripts/sugar/rich_text.js.coffee
+++ app/assets/javascripts/sugar/rich_text.js.coffee
@@ -1,49 +1,98 @@
+markdownDecorator =
+  bold: (str)           -> ["**", str, "**"]
+  emphasis: (str)       -> ["_", str, "_"]
+  link: (url, name)     -> ["[", name, "](#{url})"]
+  image: (url)          -> ["![](", url, ")"]
+  blockquote: (str)     -> ["", ("> " + line for line in str.split("\n")).join("\n"), ""]
+  spoiler: (str)        -> ["
", str, "
"]
+  code: (str, language) -> ["```#{language}\n", str, "\n```"]
+  quote: (text, html, username, permalink) ->
+    wrapInBlockquote = (str) ->
+      ("> " + line for line in str.split("\n")).join("\n")
+    cite = if permalink
+      "Posted by [#{username}](#{permalink}):"
+    else
+      "Posted by #{username}:"
+    quotedPost = wrapInBlockquote("#{cite}\n\n#{html}")
+    ["", quotedPost + "\n\n", ""]
+
+htmlDecorator =
+  bold: (str)           -> ["", str, ""]
+  emphasis: (str)       -> ["", str, ""]
+  link: (url, name)     -> ["", name, ""]
+  image: (url)          -> [""]
+  blockquote: (str)     -> ["
", str, "
"]
+  spoiler: (str)        -> ["
", str, "
"]
+  code: (str, language) -> ["```#{language}\n", str, "\n```"]
+  quote: (text, html, username, permalink) ->
+    cite = if permalink
+      "Posted by #{username}:"
+    else
+      "Posted by #{username}:"
+    content = html.replace(/\n/g, "").replace(/
/g, "\n")
+    quotedPost = "
#{cite}#{content}
"
+    ["", quotedPost + "\n\n", ""]
+
+# Gets the selected text from an element
+getSelection = (elem) -> $(elem).getSelection().text
+
+# Gets the selected range from an element
+getSelectionRange = (elem) ->
+  if typeof elem.selectionStart != "undefined"
+    [elem.selectionStart, elem.selectionEnd]
+  else
+    [0, 0]
+
+# Sets a new selection range for object
+setSelectionRange = (elem, start, end) ->
+  if typeof elem.setSelectionRange != "undefined"
+    elem.setSelectionRange(start, end)
+
+adjustSelection = (elem, callback) ->
+  selectionLength = getSelection(elem).length
+  [start, end] = getSelectionRange(elem)
+  [replacementLength, prefixLength] = callback()
+  newEnd = (end + (replacementLength - selectionLength) + prefixLength)
+  newStart = if start == end then newEnd else (start + prefixLength)
+  setSelectionRange(elem, newStart, newEnd)
+
+replaceSelection = (elem, prefix, replacement, postfix) ->
+  adjustSelection elem, ->
+    $(elem).replaceSelection(prefix + replacement + postfix)
+    $(elem).focus()
+    [replacement.length, prefix.length]
+
+# Drag and drop upload handler
+bindUploads = (elem, decorator) ->
+  uploadBanner = (file) -> "[Uploading \"#{file.name}\"...]"
+  startUpload = (file) -> replaceSelection(elem, "", uploadBanner(file) + "\n", "")
+  finishUpload = (file, response) ->
+    replacedText = $(elem).val().replace(uploadBanner(file), decorator().image(response.url).join(""))
+    $(elem).val(replacedText)
+  $(elem).filedrop
+    allowedfiletypes: ['image/jpeg', 'image/png', 'image/gif']
+    maxfiles: 25
+    maxfilesize: 2
+    paramname: "upload[file]"
+    url: "/uploads.json"
+    headers:
+      "X-CSRF-Token": Sugar.authToken($(elem).closest("form"))
+    uploadStarted: (i, file, len) ->
+      startUpload file
+    uploadFinished: (i, file, response, time) ->
+      finishUpload file, response
+
 Sugar.RichTextArea = (textarea, options) ->
-
+  # Only apply it once
   return this if textarea.richtext
-
-  markdownDecorator =
-    bold: (str)       -> ["**", str, "**"]
-    emphasis: (str)   -> ["_", str, "_"]
-    link: (url, name) -> ["[", name, "](#{url})"]
-    image: (url)      -> ["![](", url, ")"]
-    blockquote: (str) -> ["", ("> " + line for line in str.split("\n")).join("\n"), ""]
-    spoiler: (str)    -> ["
", str, "
"]
-
-    code: (str, language) ->
-      ["```#{language}\n", str, "\n```"]
-
-    quote: (text, html, username, permalink) ->
-      wrapInBlockquote = (str) ->
-        ("> " + line for line in str.split("\n")).join("\n")
-      cite = if permalink
-        "Posted by [#{username}](#{permalink}):"
-      else
-        "Posted by #{username}:"
-      quotedPost = wrapInBlockquote("#{cite}\n\n#{html}")
-      ["", quotedPost + "\n\n", ""]
-
-  htmlDecorator =
-    bold: (str)       -> ["", str, ""]
-    emphasis: (str)   -> ["", str, ""]
-    link: (url, name) -> ["", name, ""]
-    image: (url)      -> [""]
-    blockquote: (str) -> ["
", str, "
"]
-    spoiler: (str)    -> ["
", str, "
"]
-
-    code: (str, language) ->
-      ["```#{language}\n", str, "\n```"]
-
-    quote: (text, html, username, permalink) ->
-      cite = if permalink
-        "Posted by #{username}:"
-      else
-        "Posted by #{username}:"
-      content = html.replace(/\n/g, "").replace(/
/g, "\n")
-      quotedPost = "
#{cite}#{content}
"
-      ["", quotedPost + "\n\n", ""]
-
-  decorator = markdownDecorator
+  textarea.richtext = true
+
+  formatButton = $("
Markdown
")
+  toolbar = $("

").append(formatButton).insertBefore(textarea)
+  emojiBar = $("

").insertBefore(textarea)
+  emojiBar.hide()
+
+  decorator = -> markdownDecorator

   if $(textarea).data('formats')
     formats = $(textarea).data('formats').split(" ")
@@ -63,10 +112,10 @@
     format = newFormat
     if format == "markdown"
       label = "Markdown"
-      decorator = markdownDecorator
+      decorator = -> markdownDecorator
     else if format == "html"
       label = "HTML"
-      decorator = htmlDecorator
+      decorator = -> htmlDecorator

     formatButton.find("a").html(label)

@@ -81,56 +130,25 @@

   nextFormat = ->
     setFormat formats[(formats.indexOf(format) + 1) % formats.length]
-    false
-
-  formatButton = $("
Markdown
")
-  toolbar = $("

").append(formatButton).insertBefore(textarea)
-  emojiBar = $("

").insertBefore(textarea)
-  emojiBar.hide()

   setFormat(format, true)

-  formatButton.find("a").click nextFormat
-
-  getSelection = ->
-    $(textarea).getSelection().text
-
-  replaceSelection = (prefix, replacement, postfix) ->
-    selection = getSelection()
-
-    if typeof textarea.selectionStart != "undefined"
-      selectionStart = textarea.selectionStart
-      selectionEnd = textarea.selectionEnd
-
-    $(textarea).replaceSelection(prefix + replacement + postfix)
-    $(textarea).focus()
-
-    # Modify selection
-    if typeof textarea.setSelectionRange != "undefined"
-      newSelectionStart = (selectionStart + prefix.length)
-      newSelectionEnd  = (selectionEnd + (replacement.length - selection.length) + prefix.length)
-
-      if selectionStart == selectionEnd
-        # No text was selected, move the cursor
-        textarea.setSelectionRange(newSelectionEnd, newSelectionEnd)
-      else
-        # Set the new selection range
-        textarea.setSelectionRange(newSelectionStart, newSelectionEnd)
+  formatButton.find("a").click -> nextFormat()

   addButton = (name, className, callback) ->
     link = $("")

     link.click ->
-      [prefix, replacement, postfix] = callback(getSelection())
-      replaceSelection(prefix, replacement, postfix)
+      [prefix, replacement, postfix] = callback(getSelection(textarea))
+      replaceSelection(textarea, prefix, replacement, postfix)

     $("

").append(link).insertBefore(formatButton)

   # Bold button
-  addButton "Bold", "bold", (selection) -> decorator.bold(selection)
+  addButton "Bold", "bold", (selection) -> decorator().bold(selection)

   # Italic button
-  addButton "Italics", "italic", (selection) -> decorator.emphasis(selection)
+  addButton "Italics", "italic", (selection) -> decorator().emphasis(selection)

   # Link button
   addButton "Link", "link", (selection) ->
@@ -138,23 +156,23 @@
     name = if selection.length > 0 then selection else "Link text"
     url = if url.length > 0 then url else "http://example.com/"
     url = url.replace(/^(?!(f|ht)tps?:\/\/)/, 'http://')
-    decorator.link(url, name)
+    decorator().link(url, name)

   # Image tag
   addButton "Image", "picture", (selection) ->
     url = if selection.length > 0 then selection else prompt("Enter image URL", "")
-    decorator.image(url)
+    decorator().image(url)

   # Block Quote
-  addButton "Block Quote", "quote-left", (selection) -> decorator.blockquote(selection)
+  addButton "Block Quote", "quote-left", (selection) -> decorator().blockquote(selection)

   # Code button
   addButton "Code", "code", (selection) ->
     lang = prompt("Enter language (leave blank for no syntax highlighting)", "")
-    decorator.code(selection, lang)
+    decorator().code(selection, lang)

   # Spoiler button
-  addButton "Spoiler", "warning-sign", (selection) -> decorator.spoiler(selection)
+  addButton "Spoiler", "warning-sign", (selection) -> decorator().spoiler(selection)

   # Emoticons
   addButton "Emoticons", "smile", (selection) ->
@@ -163,13 +181,13 @@

   # Quoting
   $(Sugar).on "quote", (event, data) ->
-    [prefix, replacement, postfix] = decorator.quote(
+    [prefix, replacement, postfix] = decorator().quote(
       data.text,
       data.html,
       data.username,
       data.permalink
     )
-    replaceSelection(prefix, replacement, postfix)
+    replaceSelection(textarea, prefix, replacement, postfix)

     # Scroll to the bottom of the textarea
     textarea.scrollTop = textarea.scrollHeight
@@ -177,7 +195,7 @@
   # Emoticons
   addEmojiButton = (name, image) ->
     link = $("")
-    link.click -> replaceSelection("", ":" + name + ": ", "")
+    link.click -> replaceSelection(textarea, "", ":" + name + ": ", "")
     $("

").append(link).appendTo(emojiBar)

   # TODO: Get these from the backend
@@ -205,32 +223,7 @@
   addEmojiButton "licking-lips", "/images/emoji/ziggy/licking-lips.gif"
   addEmojiButton "deal-with-it", "/images/emoji/ziggy/deal-with-it.gif"

-  # Uploads
-  uploadBanner = (file) -> "[Uploading \"#{file.name}\"...]"
-
-  startUpload = (file) ->
-    replaceSelection("", uploadBanner(file) + "\n", "")
-
-  finishUpload = (file, response) ->
-    replacedText = $(textarea).val().replace(uploadBanner(file), decorator.image(response.url).join(""))
-    $(textarea).val(replacedText)
-
-  if Sugar.Configuration.uploads
-    $(textarea).filedrop
-      allowedfiletypes: ['image/jpeg', 'image/png', 'image/gif']
-      maxfiles: 25
-      maxfilesize: 2
-      paramname: "upload[file]"
-      url: "/uploads.json"
-      headers:
-        "X-CSRF-Token": Sugar.authToken($(textarea).closest("form"))
-      uploadStarted: (i, file, len) ->
-        startUpload file
-      uploadFinished: (i, file, response, time) ->
-        finishUpload file, response
-
-  textarea.richtext = true
-
+  bindUploads(textarea, decorator) if Sugar.Configuration.uploads

 $(Sugar).bind 'ready modified', ->
   $('textarea.rich').each -> new Sugar.RichTextArea(this)