Clean up specs

Clean up specs

--- spec/sqf-spec.coffee
+++ spec/sqf-spec.coffee
@@ -1,36 +1,28 @@
+util = require('util')
+
 describe "SQF grammar", ->
   grammar = null
+  workspaceElement = atom.views.getView(atom.workspace)

-  beforeEach ->
-    atom.project.setPaths([path.join(__dirname, 'sample')])
-    workspaceElement = atom.views.getView(atom.workspace)
+#  executeCommand = (callback) ->
+#    atom.commands.dispatch(workspaceElement, 'language-arma-atom:toggle')
+#    waitsForPromise ->
+#      activationPromise
+#    grammar = atom.grammars.grammarForScopeName("source.sqf")
+#    runs(callback)

-    ## Open our sample init.sqf file
-    waitsForPromise ->
-      atom.workspace.open('init.sqf')
+#  beforeEach ->
+    ## active our sqf package
+#    activationPromise  = atom.packages.activatePackage("language-arma-atom")

-    ## active our sqf package
-    #waitsForPromise ->
-    #  atom.packages.activatePackage("language-arma-atom")
+#  it "parses the grammar", ->
+#    executeCommand >
+#      expect(grammar).toBeDefined()
+#      expect(grammar.scopeName).toBe "source.sqf"

-    runs ->
-      activationPromise = atom.packages.activatePackage('language-arma-atom')
-      activationPromise.fail (reason) ->
-        throw reason
-
-      grammar = atom.grammars.grammarForScopeName("source.sqf")
-
-  it "parses the grammar", ->
-    expect(grammar).toBeTruthy()
-    expect(grammar.scopeName).toBe "source.sqf"
-
-  it "tokenizes multi-line strings", ->
-    tokens = grammar.tokenizeLines('"1\\\n2"')
-
-    expect(tokens[0][0].value).toBe '"'
-    expect(tokens[0][0].scopes).toEqual ['source.sqf', 'string.quoted.double.single-line.sqf', 'punctuation.definition.string.begin.sqf']
-    expect(tokens[0][1].value).toBe '1'
-    expect(tokens[0][1].scopes).toEqual ['source.sqf', 'string.quoted.double.single-line.sqf']
-    expect(tokens[0][2].value).toBe '\\'
-    expect(tokens[0][2].scopes).toEqual ['source.sqf', 'string.quoted.double.single-line.sqf', 'constant.character.escape.newline.sqf']
-    expect(tokens[0][3]).not.toBeDefined()
+#  it "tokenizes multi-line strings", ->
+#    executeCommand >
+#      tokens = grammar.tokenizeLines('"12"')
+#      expect(tokens[0][0].value).toBe '"'
+#      expect(tokens[0][1].value).toBe '12'
+#      expect(tokens[0][2].value).toBe '"'


fix(macAutocomplete): Fixed not able to select item when clicking on dropdown

Fixes #174

--- src/directives/autocomplete.coffee
+++ src/directives/autocomplete.coffee
@@ -148,7 +148,8 @@
       Create a click handler function to make sure directive is unbinding
       the correct handler
       ###
-      clickHandler = -> reset(true)
+      clickHandler = ->
+        $scope.$apply -> hide()

       ###
       @function
@@ -174,9 +175,13 @@
       @description
       Resetting autocomplete
       ###
-      reset = (invokeApply = false) ->
+      reset = ->
+        $menuScope.items.length = 0
+
+        hide()
+
+      hide = ->
         $animate.leave menuEl, ->
-          $menuScope.items = []
           $menuScope.index = 0

           # Clear menu element inline style
@@ -186,8 +191,6 @@
           isMenuAppended = false

           element.unbind "blur", clickHandler
-
-        $scope.$apply() if invokeApply

         return

@@ -327,7 +330,7 @@

           when keys.ESCAPE
             $scope.$apply ->
-              reset()
+              hide()

               event.preventDefault()


Hide page links from redirectable projects

Closes zooniverse/wildcam-gorongosa#90

--- app/pages/project/index.cjsx
+++ app/pages/project/index.cjsx
@@ -37,6 +37,14 @@
       education: 'Education'
       talk: 'Talk'

+ProjectAvatar = React.createClass
+  displayName: 'ProjectAvatar'
+
+  render: ->
+    <PromiseRenderer promise={@props.project.get 'avatar'} then={(avatar) =>
+
+    } catch={null} />
+
 ProjectPage = React.createClass
   displayName: 'ProjectPage'

@@ -51,6 +59,10 @@
     sugarClient.unsubscribeFrom "project-#{ @props.project.id }"
     document.documentElement.classList.remove 'on-project-page'

+  getPageTitles: (page) ->
+    page.filter((page) -> page.content isnt '' and page.content?)
+      .reduce(((accum, page) -> accum[page.url_key] = page.title; accum), {})
+
   render: ->
     <ChangeListener target={@props.project}>{=>
       <PromiseRenderer promise={@props.project.get 'owner'}>{(owner) =>
@@ -63,38 +75,46 @@
           } catch={null} />

-            <Link to="project-home" params={params} className="tabbed-content-tab">
-              <PromiseRenderer promise={@props.project.get 'avatar'} then={(avatar) =>
-
-              } catch={null} />
-              {@props.project.display_name}
-            </Link>
-            <Link to="project-research" params={params} className="tabbed-content-tab">
-              <Translate content="project.nav.research" />
-            </Link>
-            <PromiseRenderer promise={@props.project.get 'pages'}>{(pages) =>
-              pageTitles = pages.filter((page) -> page.content isnt '' and page.content?).reduce(((accum, page) -> accum[page.url_key] = page.title; accum), {})
-
-                {if pageTitles.result
-                  <Link to="project-results" params={params} className="tabbed-content-tab">
-                    {pageTitles.result}
-                  </Link>}
-                {if @props.project.redirect
-                  Visit project
-                else
-                  <Link to="project-classify" params={params} className="classify tabbed-content-tab">
-                    <Translate content="project.nav.classify" />
-                  </Link>}
-                {if pageTitles.faq
-                  <Link to="project-faq" params={params} className="tabbed-content-tab">
-                    {pageTitles.faq}
-                  </Link>}
-                {if pageTitles.education
-                  <Link to="project-education" params={params} className="tabbed-content-tab">
-                    {pageTitles.education}
-                  </Link>}
-
-            }</PromiseRenderer>
+            {if @props.project.redirect
+
+                <ProjectAvatar project={@props.project} />
+                Visit {@props.project.title}
+
+            else
+              <Link to="project-home" params={params} className="tabbed-content-tab">
+                <ProjectAvatar project={@props.project} />
+                {@props.project.display_name}
+              </Link>}
+            {unless @props.project.redirect
+              <Link to="project-research" params={params} className="tabbed-content-tab">
+                <Translate content="project.nav.research" />
+              </Link>}
+            {if @props.project.redirect
+
+                <Translate content="project.nav.classify" />
+
+            else
+              <Link to="project-classify" params={params} className="classify tabbed-content-tab">
+                <Translate content="project.nav.classify" />
+              </Link>}
+            {unless @props.project.redirect
+              <PromiseRenderer promise={@props.project.get 'pages'}>{(pages) =>
+                pageTitles = @getPageTitles(pages)
+
+                  {if pageTitles.result
+                    <Link to="project-results" params={params} className="tabbed-content-tab">
+                      {pageTitles.result}
+                    </Link>}
+                  {if pageTitles.faq
+                    <Link to="project-faq" params={params} className="tabbed-content-tab">
+                      {pageTitles.faq}
+                    </Link>}
+                  {if pageTitles.education
+                    <Link to="project-education" params={params} className="tabbed-content-tab">
+                      {pageTitles.education}
+                    </Link>}
+
+              }</PromiseRenderer>}
             <Link to="project-talk" params={params} className="tabbed-content-tab">
               <Translate content="project.nav.talk" />
             </Link>


test: Timeline: ensure no ticking happens when not running

--- test/Timeline.coffee
+++ test/Timeline.coffee
@@ -81,4 +81,29 @@
     receive.data("paused", true).
     receive.data("unpaused", true).

+  next().
+  discuss("Timeline doesn't tick while stopped").
+    send.data("start", true).
+    send.disconnect("start").
+    send.data("duration", 1000).
+    send.disconnect("duration").
+    send.data("tick", true).
+    send.disconnect("tick").
+    send.data("stop", true).
+    send.disconnect("stop").
+    send.data("tick", true).
+    send.disconnect("tick").
+    send.data("reverse", true).
+    send.disconnect("reverse").
+    send.data("start", true).
+    send.disconnect("start").
+    send.data("tick", true).
+    send.disconnect("tick").
+  discuss("get a started output & value = 1").
+    receive.data("started", true).
+    receive.data("value", 0.0).
+    receive.data("stopped", true).
+    receive.data("started", true).
+    receive.data("value", 1.0).
+
 export module


account: typo

account: typo

--- client/Account/AppController.coffee
+++ client/Account/AppController.coffee
@@ -202,7 +202,7 @@
                     callback      : (event)-> modal.destroy()
                 fields            :
                   vmToResize    :
-                    label         : "Select a WM to resize"
+                    label         : "Select a VM to resize"
                     cssClass      : "clearfix"
                     itemClass     : KDSelectBox
                     type          : "select"


small refactoring on kdview towards using native js on some methods where we use query

--- client/Framework/core/KDView.coffee
+++ client/Framework/core/KDView.coffee
@@ -221,10 +221,9 @@
   # shortcut method for @getDomElement()

   $:(selector)->
-    if selector?
-      @getDomElement().find(selector)
-    else
-      @getDomElement()
+    if selector
+    then @getDomElement().find(selector)
+    else @getDomElement()

 # #
 # MANIPULATE DOM ELEMENT
@@ -287,20 +286,25 @@
 # CSS METHODS
 # #

+  _helpSetClass = (el, addOrRemove, cssClass)->
+    el.classList[addOrRemove] cl for cl in cssClass.split(' ') when cl isnt ''
+
   setClass:(cssClass)->
-    @$().addClass cssClass
-    @
+    return unless cssClass
+    _helpSetClass @getElement(), "add", cssClass
+    return this

   unsetClass:(cssClass)->
-    @$().removeClass cssClass
-    @
+    return unless cssClass
+    _helpSetClass @getElement(), "remove", cssClass
+    return this

   toggleClass:(cssClass)->
     @$().toggleClass cssClass
-    @
+    return this

   hasClass:(cssClass)->
-    @$().hasClass cssClass
+    @getElement().classList.contains cssClass

   getBounds:->
     #return false unless @viewDidAppend


also navigate pictures on pgup/pgdown

also navigate pictures on pgup/pgdown

--- client/js/views/picture_view.coffee
+++ client/js/views/picture_view.coffee
@@ -41,12 +41,18 @@
   $(document).keydown (event) ->
     return true if event.altKey or event.ctrlKey or event.metaKey

-    LEFT_ARROW = 37
-    RIGHT_ARROW = 39
+    PREV_PICTURE_KEYCODES = [
+      37 # left arrow
+      33 # page up
+    ]
+    NEXT_PICTURE_KEYCODES = [
+      39 # right arrow
+      34  # page down
+    ]

     $link = $()
-    $link = $('#picture .next-link:visible') if event.keyCode == RIGHT_ARROW
-    $link = $('#picture .prev-link:visible') if event.keyCode == LEFT_ARROW
+    $link = $('#picture .next-link:visible') if event.keyCode in NEXT_PICTURE_KEYCODES
+    $link = $('#picture .prev-link:visible') if event.keyCode in PREV_PICTURE_KEYCODES
     $link.click()

     return !$link.length


forgot about step repeat in tests

forgot about step repeat in tests

--- test/gerber-parser_test.coffee
+++ test/gerber-parser_test.coffee
@@ -124,12 +124,13 @@

   describe 'step repeat', ->
     it 'should return a new step repeat block with good input', ->
+      p.format.places = [2, 2]
       p.parseCommand(param 'SRX1Y1').should.eql { new: { sr: { x: 1, y: 1 } } }
       p.parseCommand(param 'SRX1Y1I0J0').should.eql {
         new: { sr: { x: 1, y: 1, i: 0, j: 0 } }
       }
       p.parseCommand(param 'SRX2Y3I2.0J3.0').should.eql {
-        new: { sr: { x: 2, y: 3, i: 2, j: 3 } }
+        new: { sr: { x: 2, y: 3, i: 200, j: 300 } }
       }
       p.parseCommand(param 'SR').should.eql { new: { sr: { x: 1, y: 1 } } }
     it 'should throw if bad input', ->


fix(sql): fix single quote escaping in array queries

--- src/flux/attributes/matcher.coffee
+++ src/flux/attributes/matcher.coffee
@@ -85,13 +85,18 @@
         return false

   whereSQL: (klass) ->
+
+    # https://www.sqlite.org/faq.html#q14
+    # That's right. Two single quotes in a row…
+    singleQuoteEscapeSequence = "''"
+
     if @comparator is "like"
       val = "%#{@val}%"
     else
       val = @val

     if _.isString(val)
-      escaped = "'#{val.replace(/'/g, "''")}'"
+      escaped = "'#{val.replace(/'/g, singleQuoteEscapeSequence)}'"
     else if val is true
       escaped = 1
     else if val is false
@@ -100,7 +105,7 @@
       escapedVals = []
       for v in val
         throw new Error("#{@attr.jsonKey} value #{v} must be a string.") unless _.isString(v)
-        escapedVals.push("'#{v.replace(/'/g, '\\\'')}'")
+        escapedVals.push("'#{v.replace(/'/g, singleQuoteEscapeSequence)}'")
       escaped = "(#{escapedVals.join(',')})"
     else
       escaped = val


there are no tests … yet

there are no tests … yet

--- test/lunchroulette-test.coffee
+++ test/lunchroulette-test.coffee
@@ -5,23 +5,23 @@

 helper = new Helper('../src/lunchroulette.coffee')

-describe 'lunchroulette', ->
-  beforeEach ->
-    @room = helper.createRoom()
-
-  afterEach ->
-    @room.destroy()
-
-  it 'responds to hello', ->
-    @room.user.say('alice', '@hubot hello').then =>
-      expect(@room.messages).to.eql [
-        ['alice', '@hubot hello']
-        ['hubot', '@alice hello!']
-      ]
-
-  it 'hears orly', ->
-    @room.user.say('bob', 'just wanted to say orly').then =>
-      expect(@room.messages).to.eql [
-        ['bob', 'just wanted to say orly']
-        ['hubot', 'yarly']
-      ]
+# describe 'lunchroulette', ->
+#   beforeEach ->
+#     @room = helper.createRoom()
+#
+#   afterEach ->
+#     @room.destroy()
+#
+#   it 'responds to hello', ->
+#     @room.user.say('alice', '@hubot hello').then =>
+#       expect(@room.messages).to.eql [
+#         ['alice', '@hubot hello']
+#         ['hubot', '@alice hello!']
+#       ]
+#
+#   it 'hears orly', ->
+#     @room.user.say('bob', 'just wanted to say orly').then =>
+#       expect(@room.messages).to.eql [
+#         ['bob', 'just wanted to say orly']
+#         ['hubot', 'yarly']
+#       ]


moved getPanelType

moved getPanelType

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -415,13 +415,6 @@
         'bottom'

-  _getPanelType: (anchor) ->
-    if anchor is 'left' or anchor is 'right'
-      @vPanels
-    else
-      @hPanels
-
-
   _getRightAnchorCoord: ->
     @panelWidth * (@vPanels - 1) - @vPanels + 1


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -7,7 +7,7 @@
       bullet:
         target: 3
         approachBy: 1
-        value
+        value:

   obj.include(Cooldown)


Bugfix

Bugfix

--- models.coffee
+++ models.coffee
@@ -680,7 +680,7 @@
                         if fieldName is '_reactives'
                             # Loading denormalized reactive values isn't currently
                             # supported on the server
-                            delete mongoFieldsArg[fieldName]
+                            delete mongoFieldsArg[fieldSpec]
                         else
                             fieldNameSet[fieldName] = true


Failsafe way to init app.

Failsafe way to init app.


Simplify merit_order_hourly_supply.getLegendSeries

--- app/assets/javascripts/d3/merit_order_hourly_supply.coffee
+++ app/assets/javascripts/d3/merit_order_hourly_supply.coffee
@@ -26,12 +26,8 @@
           .attr('height', @height)

     getLegendSeries: ->
-      legendSeries = []
-      @series.forEach (serie) =>
-         if _.find(serie.future_value(), (v) -> (v > 0))
-           legendSeries.push(serie)
-
-      legendSeries
+      # Filter series whose values are all zero.
+      @series.filter (serie) -> _.some(serie.future_value())

     getSeries: ->
       weekNum = this.dateSelect && this.dateSelect.val() || 0


fix bug when there are two rules on a single question. make sure the operator and answer are context specific to the rule/condition being checked

--- app/assets/javascripts/hanuman/conditional-logic.js.coffee
+++ app/assets/javascripts/hanuman/conditional-logic.js.coffee
@@ -36,17 +36,17 @@
         # bind conditions based on element type
         # text, textarea, select
         if $triggerElement.length < 2
-          self.bindConditions(rule.question_id, $triggerElement, condition.operator, condition.answer)
+          self.bindConditions($triggerElement)
         # radio buttons
         else
           if ($triggerElement.is(":checkbox") || $triggerElement.is(":radio"))
             # limit binding of each checkbox if data-label-value and answer are the same-kdh
             $triggerElement = $triggerContainer.find(".form-control[data-label-value=" + condition.answer + "]")
-            self.bindConditions(rule.question_id, $triggerElement, condition.operator, condition.answer)
+            self.bindConditions($triggerElement)
           else
             for element in $triggerElement
               do (element) ->
-                self.bindConditions(rule.question_id, $(element), condition.operator, condition.answer)
+                self.bindConditions($(element))
         # determine if we are in a repeater
         inRepeater = false
         $repeater = $($triggerElement).closest(".form-container-repeater")
@@ -60,7 +60,7 @@
     return

   #bind conditions to question
-  bindConditions: (ancestor_id, $triggerElement, operator, answer) ->
+  bindConditions: ($triggerElement) ->
     $triggerElement.on "change", ->
       # pop out of condition into rules to handle all conditions defined in the rule
       # TODO it seems this is looping through ALL data-rule in the DOM instead of the data-rule associated with the element that triggered the onchange event-kdh
@@ -77,12 +77,14 @@
           conditions = rule.conditions
           ancestorId = rule.question_id
           matchingCondition = _.where(conditions, {question_id: Number(questionId)})
-          hideQuestions = self.evaluateCondition(operator, answer, self.getValue($triggerElement))
           if matchingCondition.length > 0
             if conditions.length > 1
               self.checkConditionsAndHideShow(conditions, ancestorId, $ruleElement, $container, inRepeater, matchType)
             else
-              self.hideShowQuestions(hideQuestions, ancestor_id, $ruleElement, $container, inRepeater)
+              operator = conditions[0].operator
+              answer = conditions[0].answer
+              hideQuestions = self.evaluateCondition(operator, answer, self.getValue($triggerElement))
+              self.hideShowQuestions(hideQuestions, ancestorId, $ruleElement, $container, inRepeater)
       # if not then lets assume its at the top most level outside of a repeater
       else
         $($triggerElement).closest(".form-container-survey").find("[data-rule]").each ->
@@ -95,12 +97,14 @@
           conditions = rule.conditions
           ancestorId = rule.question_id
           matchingCondition = _.where(conditions, {question_id: Number(questionId)})
-          hideQuestions = self.evaluateCondition(operator, answer, self.getValue($triggerElement))
           if matchingCondition.length > 0
             if conditions.length > 1
               self.checkConditionsAndHideShow(conditions, ancestorId, $ruleElement, $container, inRepeater, matchType)
             else
-              self.hideShowQuestions(hideQuestions, ancestor_id, $ruleElement, $container, inRepeater)
+              operator = conditions[0].operator
+              answer = conditions[0].answer
+              hideQuestions = self.evaluateCondition(operator, answer, self.getValue($triggerElement))
+              self.hideShowQuestions(hideQuestions, ancestorId, $ruleElement, $container, inRepeater)
     return

   checkConditionsAndHideShow: (conditions, ancestorId, $ruleElement, $container, inRepeater, matchType) ->


update config

update config

Signed-off-by: Adam Stokes <0a364f4bf549cc82d725fa7fd7ed34404be64079@ubuntu.com>

--- config.coffee
+++ config.coffee
@@ -1,13 +1,14 @@
 hb = require('handlebars')
 fs = require('fs-extra-promise')
 moment = require('moment')
+path = require('path')

 config =
   templates:
-    single: fs.readFileSync("templates/single.hbs").toString()
-    feed: fs.readFileSync("templates/feed.hbs").toString()
-    sitemap: fs.readFileSync("templates/sitemap.hbs").toString()
-    home: fs.readFileSync("templates/home.hbs").toString()
+    single: fs.readFileSync("./templates/single.hbs").toString()
+    feed: fs.readFileSync("./templates/feed.hbs").toString()
+    sitemap: fs.readFileSync("./templates/sitemap.hbs").toString()
+    home: fs.readFileSync("./templates/home.hbs").toString()

 nodeEnv = process.env.NODE_ENV or 'development'

@@ -26,7 +27,8 @@
 config.site = siteSettings[nodeEnv]

 for partial in ['footer', 'header', 'sidebar']
-  hb.registerPartial partial, fs.readFileSync("templates/partials/#{partial}.hbs").toString()
+  pFile = fs.readFileSync("./templates/partials/#{partial}.hbs").toString()
+  hb.registerPartial partial, pFile

 hb.registerHelper 'date', (date) ->
   moment(date).format 'Do MMMM YYYY'


container not required

container not required

--- resources/assets/coffee/main.coffee
+++ resources/assets/coffee/main.coffee
@@ -133,7 +133,6 @@
   user: JSON.parse(el.dataset.user)

 reactTurbolinks.register 'user-card-tooltip', _exported.UserCardTooltip, (el) ->
-  container: el
   lookup: el.dataset.lookup

 rootUrl = "#{document.location.protocol}//#{document.location.host}"


Stub clearInterval on fake window implementation

--- test/navigation/notifications-model.spec.coffee
+++ test/navigation/notifications-model.spec.coffee
@@ -12,6 +12,7 @@
         setItem: sinon.stub()
       document:
         hidden: false
+      clearInterval: sinon.spy()
     Notifications.init(@windowImpl)
     Notifications.loaded(data: TEST_NOTICES)


[docs] Fix broken links to cloud examples

--- salvus/page/r_help.cjsx
+++ salvus/page/r_help.cjsx
@@ -380,13 +380,13 @@

Technical Notes

-
The collection of examples is a Github repository.
+
The collection of examples is a Github repository.
                                 In a terminal in the sage-cloud-templates directory, you can type

git pull

                                 to get the latest changes and
                                 examples (you may have to type "git commit -a" first,
                                 if you have made changes). You can also
-                                 see
+                                 see
                                 what is new in the Github repository.


Fix websocket onopen alert

Fix websocket onopen alert

--- coffee/lib/websocket_handler.coffee
+++ coffee/lib/websocket_handler.coffee
@@ -30,7 +30,8 @@
       @ws.onopen = (e) =>
         msg = "[#{utils.now()}] Вебсокет #{@wsUrl} открыт"
         console.info msg
-        if @_wsTries
+        # Let it to fail one time without notice
+        if @_wsTries > 1
           utils.showAlert msg, "success", false
         @_wsTries = 0


Reset timer if seconds change

Reset timer if seconds change

--- coffee/react-countdown-clock.coffee
+++ coffee/react-countdown-clock.coffee
@@ -13,18 +13,18 @@

   displayName: 'ReactCountdownClock'

-  componentDidUpdate: (props) ->
-    console.log @props
-    if props.seconds != @props.seconds
+  componentDidUpdate: (prevProps) ->
+    if prevProps.seconds != @props.seconds
       @_seconds = @props.seconds
+      @_stopTimer()
       @_setupTimer()

-    if props.color != @props.color
+    if prevProps.color != @props.color
       @_clearBackground()
       @_drawBackground()
       @_updateCanvas()

-    if props.paused != @props.paused
+    if prevProps.paused != @props.paused
       @_startTimer() if !@props.paused
       @_pauseTimer() if @props.paused


Fix error in a test

Fix error in a test

--- test/function.coffee
+++ test/function.coffee
@@ -37,7 +37,7 @@
     result.should.equal '60 minutes'

   it 'makes a decimal of the smallest unit', ->
-    result = humanizing 2.minutes() + 15.seconds()
+    result = humanizing 2.minutes() + 15.seconds(),
       units: ['minute', 'second']
     result.should.equal '2 minutes, 15 seconds'
     result = humanizing 2.minutes() + 15.seconds(),


Inspect params; expect callback to be final arg.

Inspecting the type of arguments to see if a non-callback arg was
omitted is an established practice. Example: [Express's `app.render`](https://github.com/strongloop/express/blob/bbcc1e1f52cb3706660d88bc2884057d71ae1f92/lib/application.js#L519-L522).

--- src/sailthru.coffee
+++ src/sailthru.coffee
@@ -31,7 +31,11 @@
 class SailthruRequest
     valid_methods = ['GET', 'POST', 'DELETE']

-    _http_request: (uri, data, method, callback, binary_data_params) ->
+    _http_request: (uri, data, method, binary_data_params, callback) ->
+        # support callback function as fourth arg
+        if typeof binary_data_params is 'function'
+            callback = binary_data_params
+            binary_data_params = undefined
         if binary_data_params is undefined
             binary_data_params = []
         parse_uri = url.parse uri
@@ -96,7 +100,11 @@
         req.end()
         req.write url.format({query: options.query}).replace('?', ''), 'utf8' if method is 'POST'

-    _api_request: (uri, data, request_method, callback, binary_data_params) ->
+    _api_request: (uri, data, request_method, binary_data_params, callback) ->
+        # support callback function as fourth arg
+        if typeof binary_data_params is 'function'
+            callback = binary_data_params
+            binary_data_params = undefined
         if binary_data_params is undefined
             binary_data_params = []
         return @_http_request uri, data, request_method, callback, binary_data_params
@@ -152,7 +160,11 @@
     ###
     POST call
     ###
-    apiPost: (action, data, callback, binary_data_params) ->
+    apiPost: (action, data, binary_data_params, callback) ->
+        # support callback function as third arg
+        if typeof binary_data_params is 'function'
+            callback = binary_data_params
+            binary_data_params = undefined
         if binary_data_params is undefined
             binary_data_params = []
         if binary_data_params.length > 0 then @apiPostMultiPart action, data, callback, binary_data_params else @_apiRequest action, data, 'POST', callback
@@ -160,7 +172,11 @@
     ###
     POST call with Multipart
     ###
-    apiPostMultiPart: (action, data, callback, binary_data_params) ->
+    apiPostMultiPart: (action, data, binary_data_params, callback) ->
+        # support callback function as third arg
+        if typeof binary_data_params is 'function'
+            callback = binary_data_params
+            binary_data_params = undefined
         if binary_data_params is undefined
             binary_data_params = []
         binary_data = {}
@@ -203,7 +219,11 @@
     getEmail: (email, callback) ->
         @apiGet 'email', {email: email}, callback

-    setEmail: (email, callback, options) ->
+    setEmail: (email, options, callback) ->
+        # support callback function as second arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -211,7 +231,11 @@
         @apiPost 'email', data, callback

     # Send API Call
-    send: (template, email, callback, options) ->
+    send: (template, email, options, callback) ->
+        # support callback function as third arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -219,7 +243,11 @@
         data.email = email
         @apiPost 'send', data, callback

-    multiSend: (template, emails, callback, options) ->
+    multiSend: (template, emails, options, callback) ->
+        # support callback function as third arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -252,14 +280,22 @@
             schedule_time: ''
         @apiPost 'blast', data, callback

-    updateBlast: (blastId, callback, options) ->
+    updateBlast: (blastId, options, callback) ->
+        # support callback function as second arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
         data.blast_id = blastId
         @apiPost 'blast', data, callback

-    scheduleBlastFromBlast: (blastId, scheduleTime, callback, options) ->
+    scheduleBlastFromBlast: (blastId, scheduleTime, options, callback) ->
+        # support callback function as third arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -267,7 +303,11 @@
         data.schedule_time = scheduleTime
         @apiPost 'blast', data, callback

-    scheduleBlastFromTemplate: (blastId, template, list, scheduleTime, callback, options) ->
+    scheduleBlastFromTemplate: (blastId, template, list, scheduleTime, options, callback) ->
+        # support callback function as fifth arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -278,7 +318,11 @@

         @apiPost 'blast', data, callback

-    scheduleBlast: (name, list, scheduleTime, fromName, fromEmail, subject, contentHtml, contentText, callback, options) ->
+    scheduleBlast: (name, list, scheduleTime, fromName, fromEmail, subject, contentHtml, contentText, options, callback) ->
+        # support callback function as ninth arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -307,7 +351,11 @@
             revision: revisionId
         @apiGet 'template', data, callback

-    saveTemplate: (template, callback, options) ->
+    saveTemplate: (template, options, callback) ->
+        # support callback function as second arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -335,7 +383,11 @@
         @apiDelete 'list', data, callback

     # Contacts API Call
-    importContacts: (email, password, callback, includeNames) ->
+    importContacts: (email, password, includeNames, callback) ->
+        # support callback function as third arg
+        if typeof includeNames is 'function'
+            callback = includeNames
+            includeNames = undefined
         if includeNames is undefined
             includeNames = true
         data =
@@ -346,7 +398,11 @@
         @apiPost 'contacts', data, callback

     # Content API Call
-    pushContent: (title, url, callback, options) ->
+    pushContent: (title, url, options, callback) ->
+        # support callback function as third arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -361,7 +417,11 @@
             email: email
         @apiGet 'alert', data, callback

-    saveAlert: (email, type, template, callback, options) ->
+    saveAlert: (email, type, template, options, callback) ->
+        # support callback function as fourth arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -378,7 +438,11 @@
         @apiDelete 'alert', data, callback

     # purchase API Call
-    purchase: (email, items, callback, options) ->
+    purchase: (email, items, options, callback) ->
+        # support callback function as third arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         data = @_getOptions options
         data.email = email
         data.items = items
@@ -388,14 +452,22 @@
     stats: (data, callback) ->
         @apiGet 'stats', data, callback

-    statsList: (callback, options) ->
+    statsList: (options, callback) ->
+        # support callback function as first/only arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
         data.stat = 'blast'
         @stats data, callback

-    statsBlast: (callback, options) ->
+    statsBlast: (options, callback) ->
+        # support callback function as first/only arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
         if options is undefined
             options = null
         data = @_getOptions options
@@ -406,7 +478,22 @@
     getJobStatus: (jobId, callback) ->
         @apiGet 'job', {'job_id': job_id}, callback

-    processJob: (job, callback, options, report_email, postback_url, binary_data_params) ->
+    processJob: (job, options, report_email, postback_url, binary_data_params, callback) ->
+
+        # support callback function as second, third, fourth, or fifth arg
+        if typeof options is 'function'
+            callback = options
+            options = undefined
+        else if typeof report_email is 'function'
+            callback = report_email
+            report_email = undefined
+        else if typeof postback_url is 'function'
+            callback = postback_url
+            postback_url = undefined
+        else if typeof binary_data_params is 'function'
+            callback = binary_data_params
+            binary_data_params = undefined
+
         if options is undefined
             options = null
         if report_email is undefined
@@ -415,6 +502,7 @@
             postback_url = false
         if binary_data_params is undefined
             binary_data_params = Array
+
         data = @_getOptions options
         data['job'] = job
         data['report_email'] = report_email if report_email isnt false


Fleshed out tests for `ready`

Fleshed out tests for `ready`

--- test/service.coffee
+++ test/service.coffee
@@ -126,6 +126,22 @@
       Services.ready('TestService').spread (instance) ->
         expect(service.isPrototypeOf(instance)).to.be.true

-    it 'resolves when multiple services are ready'
-    it 'throws if ready is called before start'
-    it 'rejects if ready is called on a service that failed to start'
+    it 'resolves when multiple services are ready', ->
+      deferred = Q.defer()
+      service2 = Object.create(Services.Service)
+      service2.onStart = -> deferred.promise
+      Services.register('TestService2', service2)
+      Services.start('TestService2')
+      setTimeout(->
+        deferred.resolve()
+      , 1)
+      Services.ready('TestService', 'TestService2').spread (instance, instance2) ->
+        expect(service.isPrototypeOf(instance)).to.be.true
+        expect(service2.isPrototypeOf(instance2)).to.be.true
+
+    it 'throws if ready is called before start', ->
+      expect(-> Services.ready('Bogus')).to.throw(Error)
+
+    it 'throws if ready is called on a service that failed to start', ->
+      service.onStart = -> throw new Error("Could not start")
+      expect(-> Services.ready('Bogus')).to.throw(Error)


redirect to team/show after create

redirect to team/show after create

--- nodeko.coffee
+++ nodeko.coffee
@@ -26,7 +26,7 @@
         @error: error
         @render 'teams/new.html.haml'
       else
-        @redirect '/teams'
+        @redirect '/teams/' + @team.id()

 # list teams
 get '/teams', ->


don't die if somebody hits /teams/123

don't die if somebody hits /teams/123

--- nodeko.coffee
+++ nodeko.coffee
@@ -37,9 +37,12 @@
 # show team
 get '/teams/:id', ->
   Team.first @param('id'), (error, team) =>
-    @team: team
-    @members: team.members or []
-    @render 'teams/show.html.haml'
+    if team?
+      @team: team
+      @members: team.members or []
+      @render 'teams/show.html.haml'
+    else
+      @redirect '/'

 # delete team
 del '/teams/:id', -> # delete not working


Added javascript for codrops text input effects.

--- assets/js/gimme/gimme.coffee
+++ assets/js/gimme/gimme.coffee
@@ -49,3 +49,31 @@
 	Caman.Event.listen camanImage, "processStart", ->

 	Caman.Event.listen camanImage, "processStart", ->
+
+	# trim polyfill : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
+	onInputFocus = (ev) ->
+	  classie.add ev.target.parentNode, 'input--filled'
+	  return
+
+	onInputBlur = (ev) ->
+	  if ev.target.value.trim() == ''
+	    classie.remove ev.target.parentNode, 'input--filled'
+	  return
+
+	if !String::trim
+	  do ->
+	    # Make sure we trim BOM and NBSP
+	    rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g
+
+	    String::trim = ->
+	      @replace rtrim, ''
+
+	    return
+	[].slice.call(document.querySelectorAll('input.input__field')).forEach (inputEl) ->
+	  # in case the input is already filled..
+	  if inputEl.value.trim() != ''
+	    classie.add inputEl.parentNode, 'input--filled'
+	  # events:
+	  inputEl.addEventListener 'focus', onInputFocus
+	  inputEl.addEventListener 'blur', onInputBlur
+	  return


Fix error handling

Fix error handling

- Modify error handling of a wrong Room value
-- The process exits on error
-- Better error message is provided

--- src/typetalk.coffee
+++ src/typetalk.coffee
@@ -70,6 +70,11 @@
     @host = 'typetalk.in'
     @rate = parseInt options.apiRate, 10

+    for roomId in @rooms
+      unless roomId.length > 0 and parseInt(roomId) > 0
+        @robot.logger.error 'Room id must be greater than 0'
+        process.exit 1
+
     unless @rate > 0
       @robot.logger.error 'API rate must be greater then 0'
       process.exit 1
@@ -104,6 +109,10 @@
             count: 100

         @Topic(id).get opts, (err, data) =>
+          if !data.posts
+            @robot.logger.error 'Room must be accessible'
+            process.exit 1
+
           for post in data.posts
             continue unless lastPostId < post.id


Completed the cache testing

Completed the cache testing

--- test/wordnet_cache_test.coffee
+++ test/wordnet_cache_test.coffee
@@ -95,6 +95,13 @@
         results.should.have.property('pos', 'v')
         done()

+    it 'should return the exact same value for a second query', (done) ->
+      wordnet.findSense 'lie#v#1', (results) ->
+        should.exist(results)
+        wordnet.findSense 'lie#v#1', (results2) ->
+          (results == results2).should.be.true
+          done()
+
     it 'should return the exact same value for a second query and a two-argument callback', (done) ->
       wordnet.findSense 'lie#v#1', (err, results) ->
         should.exist(results)
@@ -113,11 +120,28 @@
         results.should.have.length(8)
         done()

+    it 'should succeed for node with a two-argument callback', (done) ->
+      wordnet.querySense 'node', (err, results) ->
+        should.exist(results)
+        should.not.exist(err)
+        results.should.be.an.instanceOf(Array)
+        results.should.have.length(8)
+        done()
+
     it 'should return the exact same value for a second query', (done) ->
       wordnet.querySense 'node', (results) ->
         should.exist(results)
         wordnet.querySense 'node', (results2) ->
           (results == results2).should.be.true
+          done()
+
+    it 'should return the exact same value for a second query and a two-argument callback', (done) ->
+      wordnet.querySense 'node', (err, results) ->
+        should.exist(results)
+        should.not.exist(err)
+        wordnet.querySense 'node', (err, results2) ->
+          (results == results2).should.be.true
+          should.not.exist(err)
           done()


More test cases

More test cases

--- test/strings.coffee
+++ test/strings.coffee
@@ -49,6 +49,14 @@
       follows #{2}  \
       too #{3}\
       !", 'interpolation 1 follows 2  too 3!'
+  eq "a #{
+    'string ' + "inside
+                 interpolation"
+    }", "a string inside interpolation"
+  eq '
+    indentation
+      doesn\'t
+  matter', 'indentation doesn\'t matter'

 #647
 eq "''Hello, World\\''", '''


Add token confirmation endpoint.

Add token confirmation endpoint.

--- js/src/http/UserDirectoryApi.coffee
+++ js/src/http/UserDirectoryApi.coffee
@@ -23,6 +23,7 @@
   usersInRealmUrl = -> Configuration.get('heraclesUrl') + '/directory'
   setFirstLoginUrl = -> Configuration.get('heraclesUrl') + '/directory/setlogin'
   getConfirmation = -> Configuration.get('heraclesUrl') + '/registration/confirmation/resend'
+  getVerification = -> Configuration.get('heraclesUrl') + '/registration/verification/'
   getUserIdFromEmail = (email) -> Configuration.get('heraclesUrl') + '/directory/validate/sharing/email/' + email
   getUserSettingUrl = (userSetting) -> getUserUrl() + '/setting/' + userSetting

@@ -204,5 +205,12 @@
         }, credentials)
       ))

+    sendConfirmationToken: (uuid, token) ->
+      Promise.resolve(axios(
+        {
+          url: getVerification() + uuid + '/' + token
+          method: 'GET'
+        })
+      )

   return UserDirectoryApi


winctrl: keyViewHistory is obsolete

winctrl: keyViewHistory is obsolete

--- lib/core/windowcontroller.coffee
+++ lib/core/windowcontroller.coffee
@@ -15,7 +15,6 @@

 module.exports = class KDWindowController extends KDController

-  @keyViewHistory = []
   superKey        = if navigator.userAgent.indexOf("Mac OS X") is -1 then "ctrl" else "command"
   addListener     = (eventName, listener, capturePhase = yes) ->
     window.addEventListener eventName, listener, capturePhase
@@ -222,8 +221,6 @@
     @keyView    = keyView
     @registerKeyCombos keyView

-    @constructor.keyViewHistory.push keyView
-
     keyView?.activateKeyView?()
     @emit 'WindowChangeKeyView', keyView


remove binary encoding when reading compiled snapshot into buffer

--- src/snapshot.coffee
+++ src/snapshot.coffee
@@ -281,7 +281,7 @@
 		.done () ->
 			# Snapshot test passed, clean up!
 			# Read the snapshot into a buffer
-			fileBuffer = fs.readFileSync @outputFilePath, 'binary'
+			fileBuffer = fs.readFileSync @outputFilePath
 			@cleanupTest().always () ->
 				# Resolve the deferred, we were succesful!
 				@runDeferred.resolveWith @, [fileBuffer, @tries]


Typo fix

Typo fix

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -946,7 +946,7 @@
     ###
     Main rendering engine, returning an xmldom object for the whole document.
     Also saves the table of tiles in `@tiles`, the corresponding
-    coordinates in `@coords`, and overall `@weight` and `@height`,
+    coordinates in `@coords`, and overall `@width` and `@height`,
     for use by `renderTeX`.
     ###
     doc = domImplementation.createDocument SVGNS, 'svg'


Convert unchanged SVG files

Convert unchanged SVG files

Even if SVG files didn't change, we may not have done these conversions before
or in the last run of SVG Tiler, so let svgink compare mod times and decide.

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -832,9 +832,7 @@
   writeSVG: (mappings, styles, filename) ->
     ## Generates SVG and writes to filename.
     ## Default filename is the input filename with extension replaced by .svg.
-    ## Returns generated .svg filename, but only if it actually changed
-    ## (and thus needs to be converted to other desired formats);
-    ## otherwise returns null.
+    ## Returns generated .svg filename (even if it didn't changed).
     unless filename?
       filename = path.parse @filename
       if filename.ext == '.svg'
@@ -846,10 +844,9 @@
       filename = path.format filename
     if maybeWrite filename, @renderSVG mappings, styles
       console.log '->', filename
-      filename
     else
-      console.log '->', filename, '(unchanged)'
-      null
+      console.log '->', filename, '(UNCHANGED)'
+    filename
   renderSVGDOM: (mappings, styles) ->
     ###
     Main rendering engine, returning an xmldom object for the whole document.
@@ -1125,10 +1122,9 @@
       filename = path.format filename
     if maybeWrite filename, @renderTeX filename, relativeDir
       console.log ' &', filename
-      filename
     else
-      console.log ' &', filename, '(unchanged)'
-      null
+      console.log ' &', filename, '(UNCHANGED)'
+    filename

 class ASCIIDrawing extends Drawing
   @title: "ASCII drawing (one character per symbol)"
@@ -1186,9 +1182,7 @@
     path.format filename2
   writeSVG: (mappings, styles, filename) ->
     for drawing in @drawings
-      filename = drawing.writeSVG mappings, styles, @subfilename '.svg', drawing
-      continue unless filename?  # don't list unchanged .svg files
-      filename
+      drawing.writeSVG mappings, styles, @subfilename '.svg', drawing
   writeTeX: (filename) ->
     for drawing in @drawings
       subfilename = @subfilename '.svg_tex', drawing
@@ -1483,9 +1477,10 @@
         else if input instanceof Drawing or input instanceof Drawings
           filenames = input.writeSVG mappings, styles
           input.writeTeX() if settings.texText
-          ## Convert to any additional formats
-          if filenames? and filenames.length
-            convert filenames, formats, settings
+          ## Convert to any additional formats.  Even if SVG files didn't
+          ## change, we may not have done these conversions before or in the
+          ## last run of SVG Tiler, so let svgink compare mod times and decide.
+          convert filenames, formats, settings
         else if input instanceof SVGFile
           convert input.filename, formats, settings
   unless files


Update format of standings output

Update format of standings output

--- src/worldcup.coffee
+++ src/worldcup.coffee
@@ -102,7 +102,7 @@
           msg.send "There is no game right now :("

   robot.respond /(worldcup|wc)( group)( .*)/i, (msg) ->
-    group_letter = msg.match[3]
+    group_letter = msg.match[3].toUpperCase()

     msg.http("http://worldcup2014bot.herokuapp.com/groups/#{group_letter}")
       .get() (err, res, body) ->
@@ -110,7 +110,9 @@

         if standings.length > 0
           scores_array = standings.map (gs) ->
-            "#{gs.team.name} - #{gs.games_played} #{gs.wins} #{gs.draws} #{gs.losses} #{gs.goals_for} #{gs.goals_against} #{gs.points}"
+            "#{gs.team.name} - #{gs.games_played}GP #{gs.wins}W #{gs.draws}D #{gs.losses}L #{gs.goals_for}GF #{gs.goals_against}GA #{gs.points}PTS"
+
+          scores_array.unshift("Group #{group_letter} standings")

           formatted_standings = scores_array.join("\n")


The k specifier is probably a single special case.

--- src/timezone.coffee
+++ src/timezone.coffee
@@ -140,9 +140,6 @@
     N: 9
     y: 2

-  spaced =
-    k: true
-
   paddings = { "-": (number) -> number }
   for flag, ch of { "_": " ", "0": "0" }
     paddings[flag] = do (ch) ->
@@ -162,7 +159,7 @@
         [ prefix, flags, specifier, rest ] = match.slice(1)
         value = specifiers[specifier](offset, locale)
         if padding[specifier]
-          flag = if spaced[specifier] then "_" else "0"
+          flag = if specifier is "k" then "_" else "0"
           for i in [0...flags.length]
             flag = flags[i] if paddings[flags[i]]
           value = paddings[flag](value, padding[specifier])


fix bug with line number being off by one

--- lib/omnisharp-atom/linter.coffee
+++ lib/omnisharp-atom/linter.coffee
@@ -53,7 +53,7 @@

             return {
                 message: error.Text,
-                line: line,
+                line: line + 1,
                 col: column,
                 level: level,
                 range: new Rng([line, start], [line, end]),


添加模拟器删除文件夹方法

添加模拟器删除文件夹方法

--- lib/emulator/cordova-emulate.coffee
+++ lib/emulator/cordova-emulate.coffee
@@ -62,3 +62,26 @@
         # 删除zip
         fs.unlink zipPath, (err) ->
           console.log if err then err else '删除成功'
+
+
+  # as same as rm -rf /folder
+  router.get '/fs/rmdir', (req, res, next) ->
+    deleteFolderRecursive = (path) ->
+      console.log path
+      files = []
+      if fs.existsSync path
+        files = fs.readdirSync path
+
+        files.forEach (file, index) ->
+          curPath = "#{path}/#{file}"
+          if fs.statSync(curPath).isDirectory() # recurse
+            deleteFolderRecursive curPath
+          else # delete file
+            fs.unlinkSync curPath
+
+        fs.rmdirSync path
+
+    deleteFolderRecursive req.rpath
+    res.status(200).json
+      err: null
+      result: true


Corrected comment syntax

Corrected comment syntax

--- tests/assets.coffee
+++ tests/assets.coffee
@@ -278,7 +278,7 @@
     c.get '/myapp/zappa/sammy.js', (err, res) ->
       t.equal 5, res.headers['content-type'], 'application/javascript'

-  ###
+###
   'socket.io_prefix': (t) ->
     t.expect 'content-type', 'length'
     t.wait 3000
@@ -289,4 +289,4 @@
     c.get '/myapp/socket.io/socket.io.js', (err, res) ->
       t.equal 'content-type', res.headers['content-type'], 'application/javascript'
       t.equal 'length', res.headers['content-length'], '74659'
-  ###
+###


Fix regex

Fix regex

--- src/worldcup.coffee
+++ src/worldcup.coffee
@@ -63,7 +63,7 @@
   robot.respond /(who).*(win).*(world).*(cup)/i, (msg) ->
     msg.send "BRASIL!!!"

-  robot.respond /(worldcup|wc) ( yellow|red) (.*)/i, (msg) ->
+  robot.respond /(worldcup|wc).*( yellow|red)( .*)/i, (msg) ->
     color = msg.match[2]
     person = msg.match[3]


Complete rewrite

Complete rewrite

--- src/wrangler.coffee
+++ src/wrangler.coffee
@@ -1,6 +1,11 @@
 ###

 Wrangler
+by Evan Hahn
+
+Get your got'dang data into your got'dang webpage and stop worrying about it.
+
+*******************************************************************************

 Copyright (c) 2012, Fetchnotes LLC

@@ -24,32 +29,64 @@

 ###

-window.wrangle = (model, template, options) ->
+class window.Wrangler

-	# Get the wrangler object we'll be returning
-	wrangler = {}
+	# Constructor.
+	constructor: (@model, template, options) ->

-	# Give this wrangler the model
-	wrangler.model = model
+		# Assigns the template and the DOM element and accepts jQuery/Zepto, a
+		# string ID, or just a straight DOM element.
+		if ($) and (template instanceof $)
+			template = template[0]
+		if (typeof template is 'string') or (template instanceof String)
+			template = document.getElementById(template)
+		@el = template
+		@template = @el.innerHTML

-	# Get template element
-	if ($?) and (template instanceof $)
-		template = template[0]
-	if (typeof template is 'string') or (template instanceof String)
-		template = document.getElementById(template)
-	wrangler.element = template
-	wrangler.template = template.innerHTML
+		# By default, bind the change event to an update.
+		Wrangler.addEvent(@el, 'change', @update)

-	# Set a property and then update
-	wrangler.set = (index, property, value) ->
-		if value?
-			@model[index][property] = value
-		else
-			@model[index] = property
+		# Initial DOM update.
 		@update()

-	# Update the DOM
-	wrangler.update = ->
+	# Get a property.
+	get: (property, escaped = true) ->
+
+		# Who owns the property?
+		if this[property]
+			result = this[property]
+			scope = this
+		else
+			result = @model[property]
+			scope = @model
+
+		# If it's a function, return its output
+		if typeof result is 'function'
+			result = result.call(scope)
+
+		# Escape HTML if I should
+		if escaped
+			result = Wrangler.escape result
+
+		# All done!
+		result
+
+	# Set a property of the model and then update.
+	# Can batch set array elements.
+	set: (property, value) ->
+		if (property.valueOf() is 'number') and (@model instanceof Array)
+			m[property] = value for m in @model
+		@model[property] = value
+		@update()
+
+	# Destroy me.
+	destroy: (alsoDestroyModel = false) ->
+		@el.parentNode.removeChild(@el)
+		delete @el
+		delete @model if alsoDestroyModel
+
+	# Update DOM.
+	update: ->
 		if @model instanceof Array
 			output = ''
 			for m in @model
@@ -58,8 +95,13 @@
 		else
 			@element.innerHTML = window.Milk.render(@template, @model)

-	# Do an inital update of the DOM
-	wrangler.update()
+# Add an event to the DOM.
+Wrangler.addEvent = (element, name, callback) ->
+	if element.addEventListener
+		element.addEventListener(name, callback, false)
+	else if element.attachEvent
+		element.attachEvent("on#{name}", callback)

-	# We're done!
-	return wrangler+# Escape some text to safe HTML.
+Wrangler.escape = (html) ->
+	html.replace(/&/g, '&').replace(/</g, "<").replace(/>/g, '>').replace(/\ \ /g, '  ')


Add some debugging output

Add some debugging output

--- library/visualize/monitor.coffee
+++ library/visualize/monitor.coffee
@@ -60,6 +60,7 @@
         noTagging = "The supplied tagging is not valid"
         throw new Error noTagging unless _.isSring tag
         throw new Error noKernel unless isKernel
+        logger.debug "Constructed monitor %s", @tag

     # Attach one of the ends of the monitor to the previously bound
     # transport instance. This method will attach the specific event
@@ -104,6 +105,7 @@
         throw new Error invalid unless correct
         @emit "transport", this, @$transport, transport
         (@$transport or undefined)?.removeAllListeners()
+        logger.debug "Attach monitor %s to transport", @tag
         attachToTransport @$transport = transport; this

     # Set the root tree element of this session. If the element is
@@ -117,4 +119,5 @@
         throw new Error invalid unless correct
         @emit "element", this, @$element, element
         (@$element or undefined)?.removeAllListeners()
+        logger.debug "Attach monitor %s to element", @tag
         attachToElement @$element = element; this


Fix no completion if comments before

Fix no completion if comments before

--- lib/services/php-file-parser.coffee
+++ lib/services/php-file-parser.coffee
@@ -427,7 +427,7 @@

           if null == matches
             return className
-
+
           return @findUseForClass(editor, matches[1])

       # /* @var $var Class (like intelliJ) */
@@ -435,10 +435,8 @@
         regexVarWithVarName = new RegExp("\\@var[\\s]+\\#{element}[\\s]+([a-zA-Z_\\\\]+)", "g")
         matches = regexVarWithVarName.exec(line)

-        if null == matches
-          return className
-
-        return @findUseForClass(editor, matches[1])
+        if null != matches
+          return @findUseForClass(editor, matches[1])

       if chain.indexOf("function") != -1
         regexFunction = new RegExp("function[\\s]+([a-zA-Z]+)[\\s]*[\\(](?:(?![a-zA-Z\\_\\\\]*[\\s]*\\#{element}).)*[,\\s]?([a-zA-Z\\_\\\\]*)[\\s]*\\#{element}[a-zA-Z0-9\\s\\$,=\\\"\\\'\(\)]*[\\s]*[\\)]", "g")


adding back parentheses wrapper around interpolated expressions -- we need it

--- lexer.coffee
+++ lexer.coffee
@@ -367,7 +367,7 @@
             tokens.push ['STRING', quote + before.substring(0, before.length - 1) + expression + quote] if before.length
           else
             tokens.push ['STRING', quote + before + quote] if before.length
-            nested: lexer.tokenize expression.substring(2, expression.length - 1), {rewrite: no}
+            nested: lexer.tokenize '(' + expression.substring(2, expression.length - 1) + ')', {rewrite: no}
             nested.pop()
             tokens.push ['TOKENS', nested]
           str: str.substring(group.length)


carefully categorizing JS keywords from JS reserved words from Coffee keywords, so that we can throw syntax errors at compile time if JS keywords are getting assigned to.

--- lexer.coffee
+++ lexer.coffee
@@ -11,19 +11,28 @@

 # Constants ============================================================

-# The list of keywords passed verbatim to the parser.
-KEYWORDS: [
-  "if", "else", "then", "unless",
-  "true", "false", "yes", "no", "on", "off",
-  "and", "or", "is", "isnt", "not",
+# Keywords that CoffeScript shares in common with JS.
+JS_KEYWORDS: [
+  "if", "else",
+  "true", "false",
   "new", "return",
   "try", "catch", "finally", "throw",
   "break", "continue",
-  "for", "in", "of", "by", "where", "while",
+  "for", "in", "while",
   "delete", "instanceof", "typeof",
-  "switch", "when",
-  "super", "extends"
+  "switch", "super", "extends"
 ]
+
+# CoffeeScript-only keywords -- which we're more relaxed about allowing.
+COFFEE_KEYWORDS: [
+  "then", "unless",
+  "yes", "no", "on", "off",
+  "and", "or", "is", "isnt", "not",
+  "of", "by", "where", "when"
+]
+
+# The list of keywords passed verbatim to the parser.
+KEYWORDS: JS_KEYWORDS.concat COFFEE_KEYWORDS

 # The list of keywords that are reserved by JavaScript, but not used, and aren't
 # used by CoffeeScript. Using these will throw an error at compile time.
@@ -31,6 +40,9 @@
   "case", "default", "do", "function", "var", "void", "with", "class"
   "const", "let", "debugger", "enum", "export", "import", "native"
 ]
+
+# JavaScript keywords and reserved words together, excluding CoffeeScript ones.
+JS_FORBIDDEN: JS_KEYWORDS.concat RESERVED

 # Token matching regexes. (keep the IDENTIFIER regex in sync with AssignNode.)
 IDENTIFIER : /^([a-zA-Z$_](\w|\$)*)/
@@ -233,7 +245,10 @@
   value: match and match[1]
   @tag_parameters() if value and value.match(CODE)
   value ||= @chunk.substr(0, 1)
-  tag: if value.match(ASSIGNMENT) then 'ASSIGN' else value
+  tag: value
+  if value.match(ASSIGNMENT)
+    tag: 'ASSIGN'
+    throw new Error('SyntaxError: Reserved word "' + @value() + '" on line ' + @line + ' can\'t be assigned') if JS_FORBIDDEN.indexOf(@value()) >= 0
   tag: 'TERMINATOR' if value == ';'

   prev: @tokens[@tokens.length - 1]


recfrac: showing lod score and rec frac

recfrac: showing lod score and rec frac

  - still trying to get the opposity cell to display

--- recfrac/rf.coffee
+++ recfrac/rf.coffee
@@ -7,6 +7,11 @@

 log2 = (x) -> Math.log(x)/Math.log(2.0)
 onedigit = d3.format ".1f"
+twodigits = d3.format ".2f"
+reverselabels = (label) ->
+  rc = label.split("c")
+  tmp = rc[0].split("r")
+  ["r", rc[1], "c", tmp[1]].join("")

 rf = []
 chr = []
@@ -15,6 +20,7 @@
 xscale = []
 yscale = []
 zscale = []
+matrix = []

 # create SVG element and shift it down and to right
@@ -39,6 +45,18 @@
   # scale for pixel color (orange indicates missing values)
   zscale = d3.scale.linear().domain([-1, 0, zmax]).range(["orange", "white", "blue"])

+  # create matrix of rec fracs and LODs
+  for i in [0...nmar]
+    matrix[i] = d3.range(nmar).map((j) -> {rf: null, lod:null})
+
+  rf.forEach((cell) ->
+    if cell.row > cell.col
+      matrix[cell.row][cell.col].rf = cell.value
+      matrix[cell.col][cell.row].rf = cell.value
+    else if cell.row < cell.col
+      matrix[cell.row][cell.col].lod = cell.value
+      matrix[cell.col][cell.row].lod = cell.value)
+
   # plug in -1 for nulls
   rf.forEach((cell) -> cell.value = -1 if cell.value is null)

@@ -55,8 +73,8 @@
       if cell.value != -1
         cell.value = 0.5 if cell.value > 0.5
         cell.value = -4*(log2(cell.value)+1)/12*zmax
-    cell.value = zmax if cell.value > zmax
-  )
+    cell.value = zmax if cell.value > zmax)
+

   # the pixels
   cells = svg.selectAll(".cell")
@@ -67,14 +85,20 @@
       .attr("y", (d) -> yscale(d.col))
       .attr("width", xscale.rangeBand())
       .attr("height", yscale.rangeBand())
-      .attr("id", (d) -> "#{d.row}_#{d.col}")
+      .attr("id", (d) -> "r#{d.row}c#{d.col}")
       .style("fill", (d) -> zscale(d.value))

   cells.on("mouseover", ->
-    d3.select(this).style("stroke","orange").style("stroke-width", 2))
+    d3.select(this).style("stroke","green")
+    label = reverselabels(this.id)
+    console.log("#{this.id} #{label}")
+    d3.select("rect ##{label}").style("stroke","red"))

   cells.on("mouseout", ->
-    d3.select(this).style("stroke","none"))
+    d3.select(this).style("stroke","none")
+    label = reverselabels(this.id)
+    console.log("#{this.id} #{label}")
+    d3.select("rect ##{label}").style("stroke","none"))

   cells.on("click", (d) ->
     d3.selectAll("#tooltip").transition().duration(500).attr("opacity",0).remove()
@@ -83,7 +107,7 @@
           if d.row is d.col
             markers[d.row].marker
           else
-            "#{markers[d.row].marker} : #{markers[d.col].marker}    value = #{onedigit(d.value)}"
+            "#{markers[d.row].marker} : #{markers[d.col].marker}    rf = #{twodigits(matrix[d.row][d.col].rf)}    lod = #{onedigit(matrix[d.row][d.col].lod)}"
         )
         .attr("id", "tooltip")
         .style("font-family", "sans-serif")


Game continue fix

Game continue fix

--- Content/MAZE/src/game/game.coffee
+++ Content/MAZE/src/game/game.coffee
@@ -140,7 +140,7 @@
         # Start brick move reporter
         setTimeout(() =>
             @requestPlayerPosition(player)
-        , 500)
+        , 1500)

     playerMovedInitialBrick: (player, position) ->

@@ -149,6 +149,9 @@
             if aPlayer.state != PlayerState.IDLE
                 aPlayer.state = PlayerState.DISABLED

+        # Get starting playing game
+        @gameState = GameState.PLAYING_GAME
+
         # Move player
         player.state = PlayerState.TURN
         @currentPlayer = player
@@ -173,7 +176,7 @@
         # Start brick move reporter
         setTimeout(() =>
             @requestPlayerPosition(@currentPlayer)
-        , 500)
+        , 1500)

     requestPlayerInitialPosition: (player) ->
         positions = ([position.x, position.y] for position in @mazeModel.positionsReachableByPlayer(player))
@@ -231,7 +234,7 @@
         if @isUpdatingMaze
             setTimeout(() =>
                 @updateMaze()
-            , 1200)
+            , 1500)

         @isUpdatingMaze = true


OX-103 fixed rightclick in 3d-view

OX-103 fixed rightclick in 3d-view

--- app/assets/javascripts/libs/mouse.coffee
+++ app/assets/javascripts/libs/mouse.coffee
@@ -156,7 +156,8 @@
       # on leftclick, return mouse position relative to the canvas
       @changedCallbackLeftclick [evt.pageX - $(@target).offset().left, evt.pageY - $(@target).offset().top], 0
     else
-      @changedCallbackRightclick [evt.pageX - $(@target).offset().left, evt.pageY - $(@target).offset().top], 0
+      if @changedCallbackRightclick
+        @changedCallbackRightclick [evt.pageX - $(@target).offset().left, evt.pageY - $(@target).offset().top], 0

     return false


Redraw report on view change.

Redraw report on view change.

--- app/assets/javascripts/reports.js.coffee
+++ app/assets/javascripts/reports.js.coffee
@@ -240,6 +240,9 @@
                                 @listenTo(@report, "updated", @reportView.updateView)
                         else
                                 application.report_region.close()
+                else
+                        if _.intersection(changed, ["view"]).length > 0
+                                @reportView.updateView()

         resize: () =>
                 @reportView.updateView() if @reportView?


Best in place after refresh

Best in place after refresh

--- app/assets/javascripts/tickets.js.coffee
+++ app/assets/javascripts/tickets.js.coffee
@@ -29,6 +29,7 @@

     $.get '/tickets/list', (data) ->
       $('#list').html(data)
+      $(".best_in_place").best_in_place()

   $('#list').on 'ajax:success', 'a.destroy', () ->
     $(this).closest('tr').remove()


On history, use de default avatar for not-existing-users and using the name from the history

--- app/coffee/modules/common/history.coffee
+++ app/coffee/modules/common/history.coffee
@@ -282,7 +282,10 @@
             return $scope.usersById[userId]?.full_name_display

         getUserAvatar = (userId) ->
-            return $scope.usersById[userId]?.photo
+            if $scope.usersById[userId]?
+                return $scope.usersById[userId].photo
+            else
+                return "/images/unnamed.png"

         countChanges = (comment) ->
             return _.keys(comment.values_diff).length
@@ -373,7 +376,7 @@

             return templateActivity({
                 avatar: getUserAvatar(comment.user.pk)
-                userFullName: getUserFullName(comment.user.pk)
+                userFullName: comment.user.name
                 creationDate: moment(comment.created_at).format("DD MMM YYYY HH:mm")
                 comment: comment.comment_html
                 changesText: renderChangesHelperText(comment)
@@ -388,7 +391,7 @@
         renderChange = (change) ->
             return templateActivity({
                 avatar: getUserAvatar(change.user.pk)
-                userFullName: getUserFullName(change.user.pk)
+                userFullName: change.user.name
                 creationDate: moment(change.created_at).format("DD MMM YYYY HH:mm")
                 comment: change.comment_html
                 changes: renderChangeEntries(change, false)


Run through the cells twice per update

Run through the cells twice per update

--- src/main.coffee
+++ src/main.coffee
@@ -16,7 +16,9 @@

   for y in [0...CELL_ROWS]
     for x in [0...CELL_COLUMNS]
-      cells.create x * 10, y * 10, 'cell', 0, game.rnd.between(0, 100) is 0
+      cells.create x * 10, y * 10, 'cell', 0, game.rnd.between(0, 20) is 0
+      cells.getTop().toBeKilled = false
+      cells.getTop().toBeReset = false

 update = ->
   cells.forEach (cell) ->
@@ -33,9 +35,17 @@
     ] when cells.getAt(cellPosition)?.alive

     if cell.alive
-      cell.kill() if aliveNeighbors not in [2..3]
+      cell.toBeKilled = true if aliveNeighbors not in [2..3]
     else
-      cell.revive() if aliveNeighbors is 3
+      cell.toBeReset = true if aliveNeighbors is 3
+
+  cells.forEach (cell) ->
+    if cell.toBeKilled
+      cell.kill()
+      cell.toBeKilled = false
+    else if cell.toBeReset
+      cell.reset(cell.x, cell.y)
+      cell.toBeReset = false

 render = ->


opt: run

opt: run

--- src/main.coffee
+++ src/main.coffee
@@ -10,11 +10,12 @@
 	 * The first argument fulfills the promise, the second argument rejects it.
 	 * We can call these functions, once our operation is completed.
 	###
-	constructor: (executor) ->
+	constructor: (executor, _isRunImmediately) ->
 		@_value = null
 		@_handlers = []

-		run @, executor
+		# "_isRunImmediately" is only used by then internally.
+		run @, executor, _isRunImmediately

 	###*
 	 * Appends fulfillment and rejection handlers to the promise,
@@ -29,6 +30,7 @@

 		new Promise (resolve, reject) ->
 			addHandler self, onFulfilled, onRejected, resolve, reject
+		, true

 # ********************** Private **********************

@@ -67,9 +69,16 @@
 		(fn) ->
 			process.nextTick fn

-	run = (self, executor) -> nextTick ->
-		executor genTrigger(self, $resolved),
-			genTrigger(self, $rejected)
+	run = (self, executor, _isRunImmediately) ->
+		if _isRunImmediately
+			# For the Promise created by "then", we don't have to run the
+			# executor after the next tick.
+			executor genTrigger(self, $resolved),
+				genTrigger(self, $rejected)
+		else
+			nextTick ->
+				executor genTrigger(self, $resolved),
+					genTrigger(self, $rejected)
 		return

 	# Push new handler to current promise.


:poop: remove print statements

:poop: remove print statements

--- src/app/theme.coffee
+++ src/app/theme.coffee
@@ -19,10 +19,8 @@

     theme =
       if TextMateTheme.testPath(path)
-        console.log "it's TM"
         new TextMateTheme(path)
       else
-        console.log "it's atom"
         new AtomTheme(path)

     theme.load()


Add snippet for the MIT license

Add snippet for the MIT license

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -20,6 +20,31 @@
   It:
     prefix: 'it'
     body: "it '$1', ->$0"
+'.source.gfm':
+  'MIT License':
+    prefix: 'mit'
+    body: """
+      The MIT License (MIT)
+
+      Copyright © 2014 [Lee Dohm](http://www.lee-dohm.com), [Lifted Studios](http://www.liftedstudios.com)
+
+      Permission is hereby granted, free of charge, to any person obtaining a copy of
+      this software and associated documentation files (the "Software"), to deal in
+      the Software without restriction, including without limitation the rights to
+      use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+      the Software, and to permit persons to whom the Software is furnished to do so,
+      subject to the following conditions:
+
+      The above copyright notice and this permission notice shall be included in all
+      copies or substantial portions of the Software.
+
+      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+      FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+      COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+      IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+      CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+    """
 '.source.ruby':
   Before:
     prefix: 'bef'


Add AppleScript snippets for try, on error

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -538,6 +538,14 @@
     'prefix': 'tellapp'
     'body': 'tell application "${1:App Name}"\n\t$2\nend tell'

+  'try':
+    'prefix': 'try'
+    'body': 'try\n\t$1\nend try'
+
+  'on error':
+    'prefix': 'onerror'
+    'body': 'on error number ${1:-128}\n\t$2'
+
   'display alert':
     'prefix': 'displayalert'
     'body': 'display alert ${1:theText} message ${2:theMessage}'


Make Apache apex rewrite snippet protocol-agnostic

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -875,6 +875,6 @@
     'prefix': 'https'
     'body': 'RewriteCond %{HTTPS} off\nRewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,NE]'

-  'www to HTTPS apex':
+  'www to apex':
     'prefix': 'apex'
-    'body': 'RewriteCond %{HTTP_HOST} ^www\\\\.(.+)$ [NC]\nRewriteRule ^ https://%1%{REQUEST_URI} [L,R=301,NE]'
+    'body': 'RewriteCond %{HTTP_HOST} ^www\\\\.(.+)$ [NC]\nRewriteRule ^ ${1:http}://%1%{REQUEST_URI} [L,R=301,NE]'


Use current path as default when selecting screenshot dir

--- views/components/settings/parts/poi-config.cjsx
+++ views/components/settings/parts/poi-config.cjsx
@@ -123,8 +123,10 @@
       @setState
         screenshotPath: droppedFiles[0].path
   folderPickerOnClick: ->
+    fs.ensureDirSync @state.screenshotPath
     filename = dialog.showOpenDialog
       title: __ 'Screenshot Folder'
+      defaultPath: @state.screenshotPath
       properties: ['openDirectory', 'createDirectory']
     if filename isnt undefined
       window.screenshotPath = filename


Replace hash of arrays of signatures with array of signatures.

This simplifies the code.  Searching for a method signature is
now a linear brute force search but the number of signatures in
each test is going to be small so the hash did not buy anything.

--- src/mock.coffee
+++ src/mock.coffee
@@ -11,15 +11,14 @@
 class Mock

   constructor: ->
-    @method_calls = {}
+    @method_calls = []
     @current_signature = undefined
     @state = undefined

   expects: (method_name) ->
     @_check_expects_usage(method_name)
     @current_signature = new Signature(method_name)
-    @method_calls[ method_name ] ?= []
-    @method_calls[ method_name ].push(@current_signature)
+    @method_calls.push(@current_signature)
     @[ method_name ] = @_define_expected_method(method_name)
     @_set_state("expects")
     @
@@ -45,8 +44,8 @@
   # private

   _find_signature: (method_name, args...) ->
-    for signature in @method_calls[ method_name ]
-      if ( signature.args.length == args.length ) and ( signature.args.every ( element, i ) -> element == args[ i ] )
+    for signature in @method_calls
+      if (signature.method_name == method_name) and ( signature.args.length == args.length ) and ( signature.args.every ( element, i ) -> element == args[ i ] )
         return signature
     undefined

@@ -74,9 +73,8 @@

   _check_for_uncalled_signatures: ->
     messages = ""
-    for method_name, signatures of @method_calls
-      for signature in signatures when signature.called == false
-        messages += "'#{method_name}(#{signature.args})' was never called\n"
+    for signature in @method_calls when signature.called == false
+      messages += "'#{signature.method_name}(#{signature.args})' was never called\n"
     throw new Error(messages) unless messages == ""

   _set_state: (state) ->


test autodeploy

test autodeploy

--- deploy.coffee
+++ deploy.coffee
@@ -1,8 +1,6 @@
 http = require('http')
 querystring = require('querystring')
 exec = require('child_process').exec
-
-
 last_payload = {}

 http.createServer((request, response) ->


Show and exit on processing errors

Show and exit on processing errors

--- src/bin/noflo.coffee
+++ src/bin/noflo.coffee
@@ -115,6 +115,10 @@
       baseDir: baseDir
       cache: options.cache
     , (network) ->
+      network.on 'process-error', (err) ->
+        console.error err.id, err.error
+        process.exit 1
+
       addDebug network, options.verbose, options.subgraph if options.debug

       return unless options.interactive


Hadn't removed tooltip

Hadn't removed tooltip

  - but something's not working right...the tooltip doesn't always
    show up

--- recfrac/rf.coffee
+++ recfrac/rf.coffee
@@ -115,6 +115,7 @@
   )

   cells.on("mouseout", ->
+    d3.selectAll("#tooltip").remove()
     hilit[0].remove()
     hilit[1].remove()
   )


Render the error as returned from the server

--- apps/authentication/client/forgot.coffee
+++ apps/authentication/client/forgot.coffee
@@ -12,6 +12,8 @@

   $form.on 'submit', (e) ->
     e.preventDefault()
+
+    $errors.empty()

     serializer = new Serializer $form

@@ -36,16 +38,12 @@

         track.submit 'User forgot password'

-      error: ->
-        $errors.show().text 'We were unable to locate that account'
+      error: ({ responseJSON: { description }}) ->
+        $errors.show()
+          .text description

         $submit
           .prop 'disabled', false
           .text 'Try again'

-        setTimeout ->
-          $submit.text label
-          $errors.empty()
-        , 5000
-
         track.error 'User forgot password error'


add some documentation about public raw files

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -670,6 +670,11 @@
         is_public = false
         async.series([
             (cb) ->
+                # Get a list of public paths in the project, or use the cached list
+                # The cached list is cached for a few seconds, since a typical access
+                # pattern is that the client downloads a bunch of files from the same
+                # project in parallel.  On the other hand, we don't want to cache for
+                # too long, since the project user may add/remove public paths at any time.
                 public_paths = public_raw_paths_cache[project_id]
                 if public_paths?
                     cb()
@@ -685,8 +690,14 @@
                                 cb()
             (cb) ->
                 if not misc.path_is_in_public_paths(path, public_paths)
+                    # The requested path is not public, so nothing to do.
                     cb()
                 else
+                    # The requested path *is* public, so we get the file
+                    # from one (of the potentially many) compute servers
+                    # that has the file -- (right now this is implemented
+                    # via sending/receiving JSON messages and using base64
+                    # encoding, but that could change).
                     project = bup_server.get_project(project_id)
                     project.read_file
                         path    : path


Require lowercase "leaflet"

Require lowercase "leaflet"

--- scripts/src/main.coffee
+++ scripts/src/main.coffee
@@ -6,7 +6,7 @@
     base64: 'lib/base64'
     jquery: 'lib/jquery'
     spine: 'lib/spine'
-    Leaflet: 'lib/leaflet'
+    leaflet: 'lib/leaflet'
     Raphael: 'lib/blank'
     zooniverse: 'lib/zooniverse'

@@ -18,7 +18,7 @@
       deps: ['jquery']
       exports: 'Spine'

-    Leaflet:
+    leaflet:
       deps: ['jquery']
       exports: ($) ->
         styleTags = '''


A little undefined there

A little undefined there

--- scripts/rc.coffee
+++ scripts/rc.coffee
@@ -74,7 +74,7 @@
             span = latest.timestamp - first.timestamp
             recent = (parseFloat(s.score) for s in self.recent[-140...]).filter (score) ->
               not isNaN(score)
-            return msg.reply "#{@who} #{sparkline recent} - Spans #{prettyMs span}, latest #{prettyMs age} old"
+            return msg.reply "#{who} #{sparkline recent} - Spans #{prettyMs span}, latest #{prettyMs age} old"
       else
         return msg.reply "What the fuck does #{options} even mean?"


Fix merge conflicts

Fix merge conflicts

--- tests/src/MeteorTest.coffee
+++ tests/src/MeteorTest.coffee
@@ -176,4 +176,4 @@
             done()
         , 500
       catch e1
-        done(e1)+        done(e1)


made li unrecognised command handling better!!

--- code/li.coffee
+++ code/li.coffee
@@ -13,6 +13,6 @@
   switch args[1]
     when 'help' then help(args)
     when undefined then help(args)
-    else process.stderr.write("Try li help for help#{args[1]}\n")
+    else process.stderr.write("Try li help for help\n")

 exports.main()


Some docs

Some docs

--- model.coffee
+++ model.coffee
@@ -18,10 +18,13 @@
   model._name = name

   _.extend model.prototype, Model.Events,
+    # Callback that can be used when a record is initialized
     initialize: -> # Do nothing

+    # Returns the model name of the record
     modelName: -> "#{_.classify this._name}"

+    # Returns the attributes associated with the record
     attributes: -> this._attributes

     _change: (record) ->
@@ -52,26 +55,14 @@
         , this
       , this

-      #  attrPieces = attr.split("_")
-      #  # is an association
-      #  if attrPieces.pop() == "attributes"
-      #    delete attributes[attr]
-      #    associationName = attrPieces.join("_")
-      #    if _.include model.listAssociations(), associationName
-      #      klass = _.classify associationName
-      #      _.each val, (attrs, tag_id) ->
-      #        # this line could probably just say new Model(klass). Need to test.
-      #        instance = eval("new #{klass}")
-      #        instance._refresh(attrs)
-      #        instance._tagId = parseInt(tag_id)
-      #        instance._parent = this
-      #        this[associationName].add instance
-      #      , this
-      #, this
-
+    # Get the attribute value.
+    # post.get("body")
     get: (key) ->
       this._attributes[key]

+    # Set the attribute value. Can set multiple keys at once.
+    # post.set("body", "Lorem Ipsum..")
+    # post.set({body: "Lorem Ipsum", title: "Fake Latin Lesson"})
     set: (key, value) ->
       if _.isString(key) or _.isNumber(key)
         # Run value through sanitation if set
@@ -93,6 +84,9 @@
         for k, v of key
           this.set(k, v)

+    # Set or get an attribute value/values
+    # post.attr("body", "Lorem Ipsum")
+    # post.attr("body") #=> "Lorem Ipsum"
     attr: (key, value) ->
       argLen = arguments.length
       return false if (_.isUndefined(key) or _.isEmpty(key)) && argLen > 0
@@ -105,31 +99,27 @@
       else
         this.set(key)

+    # Indicates if the record has changed or not.
+    # Returns true or false
     changed: -> this._changes != {}

+    # Changes to the attributes of the record
+    # Returns {key: "value", key2: "value2"}
     changes: -> this._changes

+    # Unbind record from a form. See bindTo()
     unbindFrom: (form) ->
       $(form).undelegate ":input", "change", this.onBoundChange
       $(form).undelegate ":input", "keyup", this.onBoundChange

+    # Bind record to a form. All changes to input/select/textarea fields will automatically update the
+    # attributes on a record.
+    # post.bindTo "#post_form"
     bindTo: (form) ->
       self = this
       $(form).delegate ":input", "change", {record: this}, this.onBoundChange
       $(form).delegate ":input", "keyup", {record: this}, this.onBoundChange
-        #element = $(this).getParams()[model._name]
-        #tag_id = $(this).attr("name").replace /[^0-9]+/gi, ''
-
-        #_.each element, (val, attr) ->
-        #  attrPieces = attr.split("_")
-
-        #  if attrPieces.pop() == "attributes"
-        #    associationName = attrPieces.join("_")
-        #    record = self[associationName].findByTagId tag_id
-        #    _.each val, (value) ->
-        #      record.attr value if record
-        #  else
-        #    self.attr attr, val
+
     onBoundChange: (e) ->
       el = $(e.target)
       record = e.data.record
@@ -160,20 +150,33 @@
     _parseNameField: (el) ->
       _.map el.attr("name").split("["), (p) -> p.replace("]", "")

+    # Gets the id of the record from the server
     id: -> this.get("id")

+    # The UID for the record object. This is generated randomly for each record and is never equal to the id()
     uid: -> this._uid

+    # Determines if the record is new or not. Looks at the id() and if it is set, then it's not new.
+    # Returns true or false
     isNew: -> !_.isNumber this.id()

+    # Displays the validations errors that came from the server response
     errors: -> this._errors

+    # Reset validation errors
     resetErrors: -> this._errors = {}

+    # When a record has been marked hasMany or hasOne, they become a child in that association.
+    # parent() is a convenient way to access the parent object.
+    # Example, Post.hasMany "comments"
+    # comment.parent() #=> post record
     parent: -> this._parent

+    # Deprecated
     tagId: -> this._tagId

+    # Generates JSON for the persistence request. You never need to call this but it's useful to look at
+    # for debugging purposes. You can also override this method to generate your own toJSON logic.
     toJSON: (options) ->
       baseObj = if options and options.child
         this._json = _.clone this.attributes()
@@ -200,14 +203,19 @@

       this._json

+    # The url used to make REST requests
     getUrl: (method) ->
       path = _.pluralize "/#{model._name}"
       path = "#{path}/#{this.id()}" unless this.isNew()
       path

-    # should pass success and error callbacks
-    # success: (record, resp, xhr)
-    # error: (record, resp, xhr)
+    # Saves the record
+    # Example:
+    # post.save
+    #   success: (validPost) ->
+    #     console.log "SUCCESS"
+    #   errors: (invalidPost) ->
+    #     console.log invalidPost.errors
     save: (options) ->
       method = if this.isNew() then "create" else "update"

@@ -232,6 +240,7 @@

       Model.Sync(record, method, options)

+    # Deprecated.
     toData: -> # Do nothing

   # Associations
@@ -243,6 +252,16 @@
     listAssociations: ->
       this._manyAssociations.concat this._oneAssociations

+    # Sets a hasMany association for the model
+    # Post.hasMany "comments"
+    #
+    # post.comments #=> [{comment1...}, {comment2...}]
+    #
+    # You can also extend the collection methods by using extend
+    # Post.hasMany "comments",
+    #   extend:
+    #     popularComments: ->
+    #       this.records = this.sortBy (comment) -> comment.position()
     hasMany: (name, options) ->
       this._manyAssociations.push name
       manyArray = {}
@@ -254,6 +273,8 @@
       manyArray[name] = collection
       _.extend model.prototype, manyArray

+    # Sets a hasOne association for the model
+    # Post.hasOne "user"
     hasOne: (name) ->
       this._oneAssociations.push name
       association = new Model.One(name)
@@ -263,6 +284,14 @@
       oneObj["clear_#{name}"] = -> association.clear()
       _.extend model.prototype, oneObj

+    # Sanitize incoming record attribute data
+    # Task.sanitize "hours", (hours) -> parseInt(hours)
+    #
+    # Then if you do this:
+    # task.set("hours", "5")
+    #
+    # You should get:
+    # task.get("hours") #=> 5
     sanitize: (key, callback) ->
       this._sanitizers[key] = callback


Adds more project urls

Adds more project urls

--- server/app.coffee
+++ server/app.coffee
@@ -58,6 +58,8 @@
 app.redirect '/sudosoldiers', 'http://students.washington.edu/uwsudo/'
 app.redirect '/hnplays2048', 'http://hnplays2048.herokuapp.com/'
 app.redirect '/milestone', 'http://www.milestoneapp.co/'
+app.redirect '/leappong', 'http://github.com/grant/leappong'
+app.redirect '/harmonic', 'http://github.com/grant/harmonic'

 #
 # Errors


Removed unnecessary console.log statements

--- src/code/routes.cjsx
+++ src/code/routes.cjsx
@@ -19,8 +19,6 @@
 		## Routes
 		path: '*'
 		getComponent: (location, cb) =>
-			console.log 'here 1'
 			require.ensure [], (require) =>
-				console.log 'here 2'
 				cb null, require './views/404'
 	]


unnecessary

unnecessary


switching to 'Compiled' messages after the code has finished compiling on --watch ...


Update to the latest list of Atom packages

--- atom/packages.cson
+++ atom/packages.cson
@@ -1,17 +1,46 @@
 packages: [
   "advanced-open-file"
-  "bug-report"
+  "atom-2048"
+  "atom-ide-ui"
+  "autocomplete-emojis"
+  "autocomplete-html-entities"
+  "badges"
+  "block-travel"
   "convert-date"
-  "file-type-icons"
+  "docblockr"
+  "double-tag"
+  "duotone-dark-syntax"
+  "duotone-light-syntax"
+  "file-icons"
+  "fortune-background-tips"
+  "github-atom-light-syntax"
   "highlight-selected"
+  "ide-css"
+  "ide-rust"
+  "ide-typescript"
   "indentation-indicator"
+  "language-diff"
+  "language-elixir"
   "language-generic-config"
+  "language-pegjs"
+  "language-rust"
+  "language-slime"
   "less-than-slash"
+  "markdown-helper"
   "minimap"
+  "npm-library-description"
   "package-sync"
+  "pretty-json"
   "red-wavy-underline"
   "right-case"
   "set-syntax"
+  "significant-other"
   "soft-wrap-indicator"
+  "sort-lines"
   "tabs-to-spaces"
+  "teletype"
+  "terminal-tab"
+  "toggle-quotes"
+  "town-crier"
+  "tree-view-git-status"
 ]


Add emmet for Atom

Add emmet for Atom

--- atom/packages.cson
+++ atom/packages.cson
@@ -1,5 +1,6 @@
 packages: [
   "auto-indent"
+  "emmet"
   "expand-region"
   "git-diff-popup"
   "git-plus"


log: test number of logs

log: test number of logs

--- test/api/log.coffee
+++ test/api/log.coffee
@@ -10,32 +10,34 @@
   describe 'local via log option', ->

     it 'convert string to objects', (next) ->
-      log = null
+      logs = []
       mecano
       .call
-        log: (l) -> log = l
+        log: (l) -> logs.push l
         handler: (options) -> options.log 'handler'
       .then (err) ->
-        log.level.should.eql 'INFO'
-        log.message.should.eql 'handler'
-        (log.module is undefined).should.be.true()
-        log.time.should.match /\d+/
-        log.total_depth.should.eql 1
+        logs.length.should.eql 1
+        logs[0].level.should.eql 'INFO'
+        logs[0].message.should.eql 'handler'
+        (logs[0].module is undefined).should.be.true()
+        logs[0].time.should.match /\d+/
+        logs[0].total_depth.should.eql 1
         next err

   describe 'global via on', ->

     it 'convert string to objects', (next) ->
-      log = null
+      logs = []
       mecano
-      .on 'text', (l) -> log = l
+      .on 'text', (l) -> logs.push l
       .call (options) -> options.log 'handler'
       .then (err) ->
-        log.level.should.eql 'INFO'
-        log.message.should.eql 'handler'
-        (log.module is undefined).should.be.true()
-        log.time.should.match /\d+/
-        log.total_depth.should.eql 1
+        logs.length.should.eql 1
+        logs[0].level.should.eql 'INFO'
+        logs[0].message.should.eql 'handler'
+        (logs[0].module is undefined).should.be.true()
+        logs[0].time.should.match /\d+/
+        logs[0].total_depth.should.eql 1
         next err

   it.skip 'serialize into string with default serializer', (next) ->


Make JSON kv snippet more flexible

Make JSON kv snippet more flexible

Allowing for object literal to newline-expand very easily.


Replace semicolons with tab stops for SuiteScript snippets

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -423,27 +423,27 @@

   'get value':
     'prefix': 'getValue'
-    'body': 'getValue(${1:fieldname});'
+    'body': 'getValue(${1:fieldname})$2'

   'get text':
     'prefix': 'getText'
-    'body': 'getText(${1:fieldname});'
+    'body': 'getText(${1:fieldname})$2'

   'get field value':
     'prefix': 'getFieldValue'
-    'body': 'getFieldValue(${1:fieldname});'
+    'body': 'getFieldValue(${1:fieldname})$2'

   'get field text':
     'prefix': 'getFieldText'
-    'body': 'getFieldText(${1:fieldname});'
+    'body': 'getFieldText(${1:fieldname})$2'

   'set field value':
     'prefix': 'setFieldValue'
-    'body': 'setFieldValue(${1:fieldname}, ${2:value});'
+    'body': 'setFieldValue(${1:fieldname}, ${2:value})$3'

   'set field text':
     'prefix': 'setFieldText'
-    'body': 'setFieldText(${1:fieldname}, ${2:text});'
+    'body': 'setFieldText(${1:fieldname}, ${2:text})$3'

   'submit record':
     'prefix': 'nlapiSubmitRecord'


Change CHAR snippet's `maxlen` arg to `len`

`maxlen` implies variable-length, when the CHAR type is fixed-length.

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -660,7 +660,7 @@

   'char':
     'prefix': 'char'
-    'body': 'CHAR(${1:maxlen})$2'
+    'body': 'CHAR(${1:len})$2'

   'varchar':
     'prefix': 'varchar'


identical

identical

--- tools/objects.coffee
+++ tools/objects.coffee
@@ -25,8 +25,8 @@

         return done() if !obj

-        waiting = 0
-        next = (err)->
+        waiting = 0
+        next = (err)->
             if --waiting <= 0 || err
                 return done(err)

@@ -42,7 +42,7 @@
             return done() if !( waiting = floyd.tools.objects.keys(obj).length )

             for key, item of obj
-                each key, item, next
+                each key, item, next

@@ -60,7 +60,7 @@
             if objects.isArray obj
                 obj.splice key, 1

-            else
+            else
                 delete obj[key]

             return data
@@ -181,9 +181,9 @@
                 value = handler.handle type, key, value

             if type is 'object'
-                _all.push value
-
-            return value
+                _all.push value
+
+            return value

         ##
         JSON.stringify obj, _handle, indent
@@ -198,7 +198,7 @@
         list = []

         ##
-        model = floyd.tools.objects.traverse obj,
+        model = floyd.tools.objects.traverse obj,

             function: (key, value)->
                 code = value.toString()
@@ -211,7 +211,7 @@
         ##
         while list.length
             [id, code] = list.pop()
-            model = model.replace '"'+id+'"', code
+            model = model.replace '"'+id+'"', code

         ##
         return model
@@ -220,7 +220,7 @@
     ##
     ##
     ##
-    immutable: (target, key, value)->
+    immutable: (target, key, value)->
         objects.property target, key, value,
             get: (-> value)
             set: (-> value)
@@ -273,7 +273,7 @@
     ##
     ##
     ##
-    copy: (obj, addon)->
+    copy: (obj, addon)->
         _obj = {}
         for key, value of obj
             _obj[key] = value
@@ -292,7 +292,7 @@

     ##
     ##
-    ##
+    ##
     extend: (target, args...)->
         if !target
             target = {}
@@ -319,7 +319,7 @@
     resolve: (item, base, fallback)->

         if typeof item is 'string' && item.indexOf('/') != -1
-            return require item
+            return require item

         if fallback is false
             list = [base]
@@ -328,7 +328,7 @@

         for base in list
             if (_item = _resolve item, base) || _item is false
-                return _item
+                return _item

     ##
     ##
@@ -378,6 +378,39 @@

         return obj

+
+    ##
+    ## this is an coffeescript adaption of Object.identical by Chris O'Brien
+    ###
+        Original script title: "Object.identical.js"; version 1.12
+        Copyright (c) 2011, Chris O'Brien, prettycode.org
+        http://github.com/prettycode/Object.identical.js
+
+        Permission is hereby granted for unrestricted use, modification, and redistribution of this
+        script, only under the condition that this code comment is kept wholly complete, appearing
+        directly above the script's code body, in all original or modified non-minified representations
+    ###
+    ##
+    identical: (a, b, sortArrays)->
+
+        sort = (obj)->
+
+            if sortArrays && floyd.tools.objects.isArray obj
+                return obj.sort()
+
+            else if typeof obj isnt 'object' || obj is null
+                return obj
+
+            result = []
+
+            for key in floyd.tools.objects.keys(obj).sort()
+                result.push
+                    key: key
+                    value: sort obj[key]
+
+            return result
+
+        return JSON.stringify(sort a) is JSON.stringify(sort b)

 ##
 ##
@@ -403,7 +436,7 @@

 ##
 ## private static helper to recursively merge objects
-##
+##
 _extend = (target, source)->

     if !objects.isArray(target) && objects.isArray(source)
@@ -423,9 +456,9 @@
                             value = _item
                             break;
                 ## removed this to prevent element merging if id attribute is not present
-                ##else
+                ##else
                 ##    t_index ?= 0
-                ##    if value = target[t_index++]
+                ##    if value = target[t_index++]
                 ##
                 ##        while value.id
                 ##            value = target[t_index++]


account: put all the routes behinde main controller ready check

--- client/Account/routes.coffee
+++ client/Account/routes.coffee
@@ -8,7 +8,7 @@
     new KDNotificationView title: "Verification failed!"
     @clear()

-  handler = (callback)->
+  handler = (callback) -> KD.singletons.mainController.ready ->
     if KD.isLoggedIn()
       appManager = KD.singleton('appManager')
       if appManager.getFrontApp()?.getOption('name') is 'Account'
@@ -19,7 +19,7 @@

   KD.registerRoutes 'Account',
     "/:name?/Account"          : -> KD.singletons.router.handleRoute '/Account/Profile'
-    "/:name?/Account/:section" : ({params:{section}})-> handler (app)-> app.openSection section
+    "/:name?/Account/:section" : ({params:{section}}) -> handler (app) -> app.openSection section
     "/:name?/Account/Referrer" : -> KD.singletons.router.handleRoute '/'
     "/Verified": handleVerified
     "/VerificationFailed": handleVerificationFailed


Longer timespans in cache validity tests

--- test/caching.coffee
+++ test/caching.coffee
@@ -14,15 +14,15 @@
       expect(cache.get("nonexistant")).to.be.rejectedWith(Error).notify(done)

     it "should allow setting", (done) =>
-      expect(cache.set("anelement", new Date + 1000, {a: 'value'})).to.become(true).notify(done)
+      expect(cache.set("anelement", new Date + 10000, {a: 'value'})).to.become(true).notify(done)

     it "should allow deleting", (done) =>
-      cache.set("anelement", new Date + 1000, {a: 'value'})
+      cache.set("anelement", new Date + 10000, {a: 'value'})
         .then ->
           expect(cache.del("anelement")).to.become(true).notify(done)

     it "should retrieve existing keys", (done) =>
-      cache.set("anelement", new Date + 1000, {a: 'value'})
+      cache.set("anelement", new Date + 10000, {a: 'value'})
         .then ->
           expect(cache.get("anelement")).to.become({a: 'value'}).notify(done)

@@ -33,7 +33,7 @@

     it "should not retrieve keys after deletion", (done) =>

-      cache.set("anelement", new Date + 1000, {a: 'value'})
+      cache.set("anelement", new Date + 10000, {a: 'value'})
         .then ->
           cache.del("anelement")
             .then ->


comment out anonymous homepage test

comment out anonymous homepage test

--- test/integration/home_nologin.coffee
+++ test/integration/home_nologin.coffee
@@ -15,7 +15,7 @@
       @bodyText = text
       done()

-  it 'gives me a link to sign up for an account', (done) ->
+  xit 'gives me a link to sign up for an account', (done) ->
     browser.elementByPartialLinkText 'Sign up', (err, link) ->
       should.exist link
       link.getAttribute 'href', (err, href) ->


check and enforce react-native-cli version

--- natal.coffee
+++ natal.coffee
@@ -27,6 +27,7 @@
 rnVersion       = '0.17.0'
 rnPackagerPort  = 8081
 podMinVersion   = '0.38.2'
+rnCliMinVersion = '0.1.10'
 process.title   = 'natal'
 reactInterfaces =
   om:        'org.omcljs/om "0.9.0"'
@@ -177,6 +178,16 @@
     logErr message

+installRnCli = ->
+  try
+    exec "npm i -g react-native-cli@#{rnCliMinVersion}"
+  catch
+    logErr """
+           react-native-cli@#{rnCliMinVersion} is required
+           Run `[sudo] npm i -g react-native-cli@#{rnCliMinVersion}` then try again
+           """
+
+
 init = (projName, interfaceName) ->
   log "Creating #{projName}", 'bgMagenta'
   log ''
@@ -185,15 +196,11 @@
     logErr 'Invalid project name. Use an alphanumeric CamelCase name.'

   try
-    exec 'type react-native'
+    cliVersion = exec('react-native --version', true).toString().trim()
+    unless semver.satisfies rnCliMinVersion, ">=#{rnCliMinVersion}"
+      installRnCli()
   catch
-    try
-      exec 'npm i -g react-native-cli'
-    catch
-      logErr '''
-             react-native-cli is required
-             Run `[sudo] npm i -g react-native-cli` then try again
-             '''
+    installRnCli()

   projNameHyph = projName.replace(camelRx, '$1-$2').toLowerCase()
   projNameUs   = toUnderscored projName


Redo out path

Redo out path

--- plugins/fa/fa.coffee
+++ plugins/fa/fa.coffee
@@ -6,6 +6,6 @@
     name: "fa"

     writeAfter: ->
-      src = path.join require.resolve('font-awesome/package'), '../fonts'
-      dst = path.join __dirname, '../../out/fonts'
+      src = path.join require.resolve('font-awesome/package'), '..', 'fonts'
+      dst = path.join  @docpad.config.outPath, 'fonts'
       ncp src, dst


Fix 1067; Fix problem with paste text after file upload

--- client/views/app/room.coffee
+++ client/views/app/room.coffee
@@ -304,7 +304,8 @@
 					file: item.getAsFile()
 					name: 'Clipboard'

-		fileUpload files
+		if files.length > 0
+			fileUpload files

 	'keydown .input-message': (event) ->
 		Template.instance().chatMessages.keydown(@_id, event, Template.instance())