Fixed a problem introduced with the UI attribute in the confirmation dialog. See #63021

--- src/elements/ConfirmationChoice/ConfirmationChoice.coffee
+++ src/elements/ConfirmationChoice/ConfirmationChoice.coffee
@@ -74,9 +74,15 @@
 			if not choice
 				continue

+			uiKey = "confirmation.choice"
+			if choice.text
+				uiKey += ":#{choice.text.toLowerCase().replace(/s+/g, '_')}"
+			else if choice.loca_key
+				uiKey += ":#{choice.loca_key}"
+
 			btn_opts =
 				left: true
-				ui: "confirmation.choice:#{choice.text.toLowerCase().replace(/s+/g, '_')}"
+				ui: uiKey
 				value: choice
 				disabled: choice.disabled
 				tooltip: choice.tooltip


prep auto-export

prep auto-export

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -44,18 +44,33 @@
   register: -> Zotero.BetterBibTeX.pref.prefs.addObserver('', this, false)
   unregister: -> Zotero.BetterBibTeX.pref.prefs.removeObserver('', this)
   observe: (subject, topic, data) ->
-    if data == 'citeKeyFormat'
-      Zotero.BetterBibTeX.keymanager.reset()
-      Zotero.BetterBibTeX.DB.query('delete from keys where citeKeyFormat is not null and citeKeyFormat <> ?', [Zotero.BetterBibTeX.pref.get('citeKeyFormat')])
+    switch data
+      when 'citeKeyFormat'
+        Zotero.BetterBibTeX.keymanager.reset()
+        Zotero.BetterBibTeX.DB.query('delete from keys where citeKeyFormat is not null and citeKeyFormat <> ?', [Zotero.BetterBibTeX.pref.get('citeKeyFormat')])
+      when 'auto-export'
+        Zotero.BetterBibTeX.auto.process()
     return
 }

-Zotero.BetterBibTeX.pref.stash = ->
-  @stashed = Object.create(null)
-  keys = @prefs.getChildList('')
-  for key in keys
-    @stashed[key] = @get(key)
-  return @stashed
+Zotero.BetterBibTeX.pref.ZoteroObserver = {
+  register: -> Zotero.Prefs.prefBranch.addObserver('', this, false)
+  unregister: -> Zotero.Prefs.prefBranch.removeObserver('', this)
+  observe: (subject, topic, data) ->
+    if data == 'recursiveCollections'
+      recursive = Zotero.BetterBibTeX.auto.recursive()
+      Zotero.BetterBibTeX.DB.execute("update autoexport set recursive = ?, status = 'pending' where recursive <> ?", [recursive, recursive])
+      Zotero.BetterBibTeX.auto.process('recursiveCollections')
+    return
+}
+
+Zotero.BetterBibTeX.pref.snapshot = ->
+  stash = Object.create(null)
+  for key in @prefs.getChildList('')
+    stash[key] = @get(key)
+  return stash
+
+Zotero.BetterBibTeX.pref.stash = -> @stashed = @snapshot()

 Zotero.BetterBibTeX.pref.restore = ->
   for own key, value of @stashed ? {}
@@ -105,19 +120,19 @@
   if version == 0
     @DB.query('create table keys (itemID primary key, libraryID not null, citekey not null, pinned)')

-  if version <= 2
+  if version < 3
     @pref.set('scan-citekeys', true)

-  if version <= 3
+  if version < 4
     @DB.query('alter table keys rename to keys2')
     @DB.query('create table keys (itemID primary key, libraryID not null, citekey not null, citeKeyFormat)')
     @DB.query('insert into keys (itemID, libraryID, citekey, citeKeyFormat)
                select itemID, libraryID, citekey, case when pinned = 1 then null else ? end from keys2', [@pref.get('citeKeyFormat')])

-  if version <= 4
+  if version < 5
     @DB.query('drop table keys2')

-  if version <= 5
+  if version < 6
     @DB.query("
       create table cache (
         itemid not null,
@@ -126,7 +141,21 @@
         entry not null,
         primary key (itemid, context))
       ")
-  @DB.query("insert or replace into _version_ (tablename, version) values ('keys', 6)")
+
+  if version < 7
+    @DB.query("
+      create table autoexport (
+        id integer primary key not null,
+        collection_id not null,
+        collection_name not null,
+        path not null,
+        context not null,
+        recursive not null,
+        status not null,
+        unique (collection_id, path, context))
+      ")
+
+  @DB.query("insert or replace into _version_ (tablename, version) values ('keys', 7)")

   @keymanager.reset()
   @DB.query('delete from keys where citeKeyFormat is not null and citeKeyFormat <> ?', [@pref.get('citeKeyFormat')])


FinderController: Trigger the resizeHandler in reset method instead loadView which fixes the height issue for new registers

--- client/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -63,15 +63,15 @@

     @reset()  if @getOptions().loadFilesOnInit

-    # temp hack, if page opens in develop section.
-    @utils.wait 2500, =>
-      KD.getSingleton("mainView").sidebar._windowDidResize()
-
   reset:->
     if @getOptions().useStorage
       @appStorage.ready => @loadVms()
     else
       @utils.defer => @loadVms()
+
+    # temp hack, if page opens in develop section.
+    @utils.wait 2500, =>
+      KD.getSingleton("mainView").sidebar._windowDidResize()

   mountVms: (vms) ->
     return  unless Array.isArray vms


myteam/test: modify and test teammates with pending invitations

--- client/home/lib/myteam/components/hometeamteammates/test.coffee
+++ client/home/lib/myteam/components/hometeamteammates/test.coffee
@@ -11,52 +11,85 @@
 immutable          = require 'immutable'

 describe 'HomeTeamTeammates', ->
-
-  { createRenderer,
+
+  { createRenderer,
   renderIntoDocument,
   findRenderedDOMComponentWithClass,
   scryRenderedDOMComponentsWithClass } = TestUtils
-
+
   describe '::render', ->
-
+
     it 'should render search box', ->
-
+
       teamMates = renderIntoDocument <TeamTeammates />
       searchbox = findRenderedDOMComponentWithClass teamMates, 'search'
       expect(searchbox).toExist()
-
-
+
+
     it 'should render any row for members', ->
-
+
       teamMates = renderIntoDocument <TeamTeammates />
       membersListView = scryRenderedDOMComponentsWithClass teamMates, 'ListView-row'
       expect(membersListView.length).toEqual 0
-
-
+
+
     it 'should render with correct team members and their informations', ->
-
+
       members = mock.getTeamMembersWithRole()
       members = immutable.fromJS members
-
-      teamMates = renderIntoDocument <TeamTeammates members={members}/>
-
+
+      teamMates = renderIntoDocument <TeamTeammates
+        members={members}
+        handleRoleChange={kd.noop}
+        handleInvitation={kd.noop} />
+
       membersListView = scryRenderedDOMComponentsWithClass teamMates, 'ListView-row' #kdview kdlistview kdlistview-default
-
+
       expect(membersListView.length).toEqual members.size
-
-
+
+
     it 'should render with correct team member emails', ->
-
+
       members = mock.getTeamMembersWithRole()
       members = immutable.fromJS members
-
-      teamMates = renderIntoDocument <TeamTeammates members={members}/>
-
-      emails = scryRenderedDOMComponentsWithClass teamMates, 'email'
+
+      teamMates = renderIntoDocument <TeamTeammates
+        members={members}
+        handleRoleChange={kd.noop}
+        handleInvitation={kd.noop} />
+
+      emails = scryRenderedDOMComponentsWithClass teamMates, 'email-js'
       emails = emails.map (email) -> email.innerHTML
       emails = emails.sort()
-
+
       memberEmails = members.toArray().map (member) -> member.getIn ['profile', 'email']
-
+
       memberEmails = memberEmails.sort()
-      expect(emails).toEqual memberEmails+      expect(emails).toEqual memberEmails
+
+    it 'should render with correct team member emails and pending invitation emails', ->
+
+      members = mock.getTeamMembersWithPendings()
+      members = immutable.fromJS members
+
+      teamMates = renderIntoDocument <TeamTeammates
+        members={members}
+        handleRoleChange={kd.noop}
+        handleInvitation={kd.noop} />
+
+      emails = scryRenderedDOMComponentsWithClass teamMates, 'email-js'
+      emails = emails.map (email) -> email.innerHTML
+      emails = emails.sort()
+
+      memberEmails = members.toArray().map (member) ->
+        if member.get('status') is 'pending'
+          firstname = member.get 'firstName'
+          firstname = if firstname then firstname else ''
+          lastname = member.get 'lastName'
+          lastname = if lastname then lastname else ''
+          "#{firstname} #{lastname}"
+        else
+          member.getIn ['profile', 'email']
+
+      memberEmails = memberEmails.sort()
+      expect(emails).toEqual memberEmails


Add options to the AddModelToCollection addDefaultModel.

--- core/app/backbone/factlink/add_model_to_collection_mixin.coffee
+++ core/app/backbone/factlink/add_model_to_collection_mixin.coffee
@@ -1,8 +1,8 @@
 Backbone.Factlink ||= {}

 Backbone.Factlink.AddModelToCollectionMixin =
-  addModel: (model) ->
-    @options.addToCollection.add(model)
+  addModel: (model, options) ->
+    @options.addToCollection.add(model, options)
     model.save {},
       success: =>
         @addModelSuccess(model) if @addModelSuccess
@@ -10,6 +10,6 @@
         @options.addToCollection.remove(model)
         @addModelError(model) if @addModelError

-  addDefaultModel: -> @addModel @model
+  addDefaultModel: (options) -> @addModel @model, options

   addWrappedModel: -> @addModel @wrapNewModel(@model)


:racehorse: Save canvas to offscreen buffer, and only redraw what's necessary

--- lib/minimap-editor-view.coffee
+++ lib/minimap-editor-view.coffee
@@ -23,6 +23,9 @@
     @context = @lineCanvas[0].getContext('2d')
     @tokenColorCache = {}

+    @offscreenCanvas = document.createElement('canvas')
+    @offscreenCtxt = @offscreenCanvas.getContext('2d')
+
   initialize: ->
     @lineOverdraw = atom.config.get('minimap.lineOverdraw')

@@ -66,8 +69,8 @@

   forceUpdate: ->
     @tokenColorCache = {}
-    @firstRenderedScreenRow = null
-    @lastRenderedScreenRow = null
+    @offscreenFirstRow = null
+    @offscreenLastRow = null
     @requestUpdate()

   scrollTop: (scrollTop, options={}) ->
@@ -133,40 +136,64 @@
       @tokenColorCache[flatScopes] = color
     @tokenColorCache[flatScopes]

-  update: =>
-    return unless @editorView?
-    return unless @displayBuffer.tokenizedBuffer.fullyTokenized
-
-    #reset canvas virtual width/height
-    @lineCanvas[0].width = @lineCanvas[0].offsetWidth
-    @lineCanvas[0].height = @lineCanvas[0].offsetHeight
-
-    lines = @editor.linesForScreenRows(@getFirstVisibleScreenRow(), @getLastVisibleScreenRow())
-    @context.lineWidth = 1
+  drawLines: (firstRow, lastRow, offsetRow, context) ->
+    lines = @editor.linesForScreenRows(firstRow, lastRow)
+    context.lineWidth = 1
+    lineHeight = @getLineHeight()

     if @minimapView.displayCodeHighlights
-      for line, y in lines
+      for line, row in lines
         x = 0
+        y = offsetRow + row
         for token in line.tokens
           w = token.screenDelta
           if not token.isOnlyWhitespace()
             color = @getTokenColor(token)
-            @context.beginPath()
-            @context.strokeStyle = color
-            @context.moveTo(x, 2*y)
-            @context.lineTo(x+w, 2*y)
-            @context.stroke()
+            context.beginPath()
+            context.strokeStyle = color
+            context.moveTo(x, y*lineHeight)
+            context.lineTo(x+w, y*lineHeight)
+            context.stroke()
           x += w
     else
-      @context.strokeStyle = "#C0C0C0"
-      @context.beginPath()
-      for line, y in lines
+      context.strokeStyle = "#C0C0C0"
+      context.beginPath()
+      for line, row in lines
         w = line.text.length
+        y = offsetRow + row
         if w > 0
           w0 = line.indentLevel * 2
-          @context.moveTo(w0, 2*y+0.5)
-          @context.lineTo(w, 2*y+0.5)
-      @context.stroke()
+          context.moveTo(w0, y*lineHeight+0.5)
+          context.lineTo(w, y*lineHeight+0.5)
+      context.stroke()
+
+  update: =>
+    return unless @editorView?
+    return unless @displayBuffer.tokenizedBuffer.fullyTokenized
+
+    #reset canvas virtual width/height
+    @lineCanvas[0].width = @lineCanvas[0].offsetWidth
+    @lineCanvas[0].height = @lineCanvas[0].offsetHeight
+
+    firstRow = @getFirstVisibleScreenRow()
+    lastRow = @getLastVisibleScreenRow()
+
+    if @offscreenFirstRow?
+      @context.drawImage(@offscreenCanvas, 0, (@offscreenFirstRow-firstRow) * 2)
+      if firstRow < @offscreenFirstRow
+        @drawLines(firstRow, @offscreenFirstRow, 0, @context)
+      if lastRow > @offscreenLastRow
+        @drawLines(@offscreenLastRow, lastRow, @offscreenLastRow-firstRow, @context)
+    else
+      @drawLines(firstRow, lastRow, 0, @context)
+
+
+    # copy displayed canvas to offscreen canvas
+    @offscreenCanvas.width = @lineCanvas[0].width
+    @offscreenCanvas.height = @lineCanvas[0].height
+    @offscreenCtxt.drawImage(@lineCanvas[0], 0, 0)
+    @offscreenFirstRow = firstRow
+    @offscreenLastRow = lastRow

     @emit 'minimap:updated'


:bug: Fix broken minimap render view initialization

--- lib/minimap-render-view.coffee
+++ lib/minimap-render-view.coffee
@@ -37,6 +37,7 @@

   # Creates a new {MinimapRenderView}.
   constructor: ->
+    @subscriptions = new CompositeDisposable
     super
     @pendingChanges = []
     @context = @lineCanvas[0].getContext('2d')
@@ -44,7 +45,6 @@
     @decorationColorCache = {}
     @initializeDecorations()
     @tokenized = false
-    @subscriptions = new CompositeDisposable

     @offscreenCanvas = document.createElement('canvas')
     @offscreenCtxt = @offscreenCanvas.getContext('2d')


Change from recursive to while loop

Change from recursive to while loop

Adds better guarding of infinite loop too

--- lib/syntax-check-linter.coffee
+++ lib/syntax-check-linter.coffee
@@ -2,11 +2,18 @@
 helpers = require('atom-linter')
 path = require('path')

-getLibDir = (p) ->
-  dotdot = path.dirname(p)
-  return dotdot if dotdot in atom.project.getPaths()
-  return dotdot if path.basename(dotdot) is 'lib'
-  getLibDir(dotdot)
+getLibDir = (filepath) ->
+  projpaths = atom.project.getPaths()
+  home =
+    if process.platform.startsWith 'win'
+    then process.env.HOMEPATH else process.env.HOME
+  curpath = path.dirname filepath
+  while curpath != path.dirname(curpath)
+    return false if curpath in projpaths
+    return false if curpath is home
+    return "-I#{curpath}" if path.basename(curpath) is 'lib'
+    curpath = path.dirname curpath
+  return false

 # This provides linter support for Perl 6 using linter
 # Please see https://atom.io/packages/linter


instrument access module with events

instrument access module with events

--- library/exposure/access.coffee
+++ library/exposure/access.coffee
@@ -74,6 +74,7 @@
             p.get = s.get = -> entity or undefined
             Object.defineProperty container, key, p
             Object.defineProperty session, key, p
+            @emit "ressurect", container, entity
             assert container[key]?; callback()

     # Authenticate supplied entity as the authorized entity against
@@ -99,6 +100,7 @@
             assert session.random = _.random 0, 1, yes
             logger.debug message.blue; session.touch()
             session.save => @dereference container, ->
+                @emit "hibernate", container, entity
                 return callback undefined, content

     # A hook that will be called prior to firing up the processing
@@ -112,6 +114,7 @@
         logger.debug "Ingition dereferencing at #{id}"
         success = "Got valid ignition entity at #{id}"
         try @dereference request, (error, supply) ->
+            @emit "entity-ignition", arguments...
             succeeded = _.isObject request.entity
             logger.debug success.green if succeeded
             return next undefined if _.isEmpty error
@@ -130,6 +133,7 @@
         logger.debug "Handshake dereferencing at #{id}"
         success = "Got valid handshake entity at #{id}"
         try @dereference handshake, (error, supply) ->
+            @emit "entity-handshake", arguments...
             succeeded = _.isObject handshake.entity
             logger.debug success.green if succeeded
             return next undefined if _.isEmpty error


use broadcast metho

use broadcast metho

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -319,7 +319,7 @@
         p = "an exception happend at the server provider:"
         connected = c = "Established connection at %s".green
         disconnect = "lost socket connection at #{@location}"
-        r = (e, s) => this.emit(e, s...); $root.emit(e, s...)
+        r = (e, s) => this.emit(e, s...); @broadcast(e, s...)
         breaker = try this.STOP_ROOT_PROPAGATION or undefined
         forward = (evt) => @socket.on evt, => r evt, arguments
         forward "disconnect" # lost socket connection to server


mark the shadow with specific marker

mark the shadow with specific marker

--- library/nucleus/restful.coffee
+++ library/nucleus/restful.coffee
@@ -115,7 +115,8 @@
         shadow = request.shadow or Object.create this
         execute = -> implement.apply shadow, capture
         return execute() if _.isObject request.shadow
-        assert _.isObject request.shadow = shadow
+        assert _.isObject try request.shadow = shadow
+        _.extend shadow, __isolated: yes, __origin: @
         _.extend shadow, response: try weak response
         _.extend shadow, request: try weak request
         s = get: -> try request.session or undefined


remove unnecessary console.log

remove unnecessary console.log

--- app/assets/javascripts/annotation_iteration.coffee
+++ app/assets/javascripts/annotation_iteration.coffee
@@ -9,7 +9,6 @@
   # external API:

   register: (type, object) ->
-    console.log 'AnnotationIteration register'
     window.annotationLifecycle.registerInterfaceInstance(type, object)

   iterate: (template, data) ->


Remove turbolinks.

Remove turbolinks.

--- app/assets/javascripts/jobbr/application.js.coffee
+++ app/assets/javascripts/jobbr/application.js.coffee
@@ -1,6 +1,5 @@
 #= require jquery
 #= require jquery_ujs
-#= require turbolinks

 #= require_self


Fix a few cases that caused us to lose our lovely hacks

--- app/assets/javascripts/slack_ready_function.coffee
+++ app/assets/javascripts/slack_ready_function.coffee
@@ -1,8 +1,16 @@
+# 🙈
+slackRebuildMsg = TS.client.msg_pane.rebuildMsg
+TS.client.msg_pane.rebuild_sig = new signals.Signal
+TS.client.msg_pane.rebuildMsg = ->
+  slackRebuildMsg()
+  TS.client.msg_pane.rebuild_sig.dispatch()
+
 window.slackReadyFunction = (func) ->
-  TS.groups.message_received_sig.add func
-  TS.channels.message_received_sig.add func
-  TS.ims.message_received_sig.add func
-  TS.channels.switched_sig.add func
-  TS.groups.switched_sig.add func
-  TS.ims.switched_sig.add func
+  TS.client.msg_pane.rebuild_sig.add func
+
+  for channel_type in ["groups", "channels", "ims", "mpims"]
+    TS[channel_type].message_received_sig.add func
+    TS[channel_type].switched_sig.add func
+    TS[channel_type].history_fetched_sig.add func
+
   func()


delay resolving host_ready at least until all js has loaded

--- app/js/jail_iframe/classes/loading_promises.coffee
+++ app/js/jail_iframe/classes/loading_promises.coffee
@@ -1,10 +1,12 @@
-if /^(interactive|complete)$/.test(document.readyState)
-  FactlinkJailRoot.host_ready_promise.resolve()
-else
-  document.addEventListener('DOMContentLoaded', -> FactlinkJailRoot.host_ready_promise.resolve())
+setTimeout ->

-if 'complete' == document.readyState
-  FactlinkJailRoot.host_loaded_promise.resolve()
-else
-  window.addEventListener('load', -> FactlinkJailRoot.host_loaded_promise.resolve())
+  if /^(interactive|complete)$/.test(document.readyState)
+    FactlinkJailRoot.host_ready_promise.resolve()
+  else
+    document.addEventListener('DOMContentLoaded', -> FactlinkJailRoot.host_ready_promise.resolve())

+  if 'complete' == document.readyState
+    FactlinkJailRoot.host_loaded_promise.resolve()
+  else
+    window.addEventListener('load', -> FactlinkJailRoot.host_loaded_promise.resolve())
+, 0


Log incoming messages

Log incoming messages

--- site.coffee
+++ site.coffee
@@ -61,6 +61,7 @@

 onSocketMessage = (s) ->
   msg = JSON.parse s
+  console.log s
   state = lightState.create()
   if msg.data.on then state.on() else state.off()
   if msg.data.on then state.brightness(scaleByteToPercent(msg.data.bri))


Restore update_mappers - fix box regression

Fixes plots larger than defaults not behaving as we want

--- bokehjs/src/coffee/models/plots/plot_canvas.coffee
+++ bokehjs/src/coffee/models/plots/plot_canvas.coffee
@@ -401,6 +401,8 @@
     # AK: seems weird to me that this is here, but get solver errors if I remove it
     @update_constraints()

+    @model.get('frame')._update_mappers()
+
     ctx = @canvas_view.ctx
     ctx.pixel_ratio = ratio = @canvas_view.pixel_ratio  # Also store on cts for WebGL


test(e2e): coffee - use source maps

test(e2e): coffee - use source maps

--- test/e2e/coffee/karma.conf.coffee
+++ test/e2e/coffee/karma.conf.coffee
@@ -10,6 +10,10 @@

     browsers: [if process.env.TRAVIS then 'Firefox' else 'Chrome']

+    coffeePreprocessor:
+      options:
+        sourceMap: true
+
     preprocessors:
       '**/*.coffee': 'coffee'


Cleaner storage comments

Cleaner storage comments

--- lib/argumenta/storage.coffee
+++ lib/argumenta/storage.coffee
@@ -71,9 +71,14 @@

   # Add a user to the store.
   #
-  # @param [User] user A valid user with a unique username.
-  # @param [Function] cb(err) Called on completion or error.
-  # @param [StorageError|ValidationError|StorageConflictError] err Any error.
+  # Only valid users with unique usernames are accepted.
+  #
+  # Possible errors include:
+  #   StorageError, ValidationError, StorageConflictError
+  #
+  # @param [User]               user
+  # @param [Function]           cb(err)
+  # @param [Error]              err
   addUser: (user, cb) ->
     unless user instanceof User
       return cb new @Error "User instance required to add user."
@@ -91,32 +96,32 @@

   # Delete *all* entities from the store.
   #
-  # @param [Object]       opts An options hash.
-  # @param [Boolean]      opts.quick Enables quick deletion.
-  # @param [Function]     cb(err) Called on completion or error.
-  # @param [StorageError] err Any error.
-  clearAll: (opts={}, cb) ->
-    @store.clearAll opts, (err) ->
+  # @param [Object]             options
+  # @param [Boolean]            options.quick
+  # @param [Function]           cb(err)
+  # @param [StorageError]       err
+  clearAll: (options={}, cb) ->
+    @store.clearAll options, (err) ->
       return cb err

   # Get a user by username, omitting sensitive fields.
   #
   # @see getPasswordHash()
-  # @param [String]       username Of the user to retrieve.
-  # @param [Function]     cb(err, user) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Object]       user The retrieved user's public fields.
+  # @param [String]             username
+  # @param [Function]           cb(err, user)
+  # @param [StorageError]       err
+  # @param [PublicUser]         user
   getUser: (username, cb) ->
     @store.getUser username, (err, user) ->
-      return cb new @Error("Failed getting user: " + username, err) if err
+      return cb new @Error "Failed getting user '#{username}'.", err if err
       return cb null, user

   # Get users by usernames, omitting sensitive fields.
   #
-  # @param [Array<String>]     usernames
-  # @param [Function]          cb(err, users)
-  # @param [StorageError]      err
-  # @param [Array<PublicUser>] users
+  # @param [Array<String>]      usernames
+  # @param [Function]           cb(err, users)
+  # @param [StorageError]       err
+  # @param [Array<PublicUser>]  users
   getUsers: (usernames, cb) ->
     @store.getUsers usernames, (err, users) ->
       return cb new @Error "Failed getting users from store." if err
@@ -124,10 +129,10 @@

   # Get metadata for users by usernames.
   #
-  # @param [Array<String>] usernames
-  # @param [Function]      cb(err, metadata)
-  # @param [StorageError]  err
-  # @param [Array<Object>] metadata
+  # @param [Array<String>]      usernames
+  # @param [Function]           cb(err, metadata)
+  # @param [StorageError]       err
+  # @param [Array<Object>]      metadata
   getUsersMetadata: (usernames, cb) ->
     @store.getUsersMetadata usernames, (err, metadata) ->
       return cb new @Error "Failed getting users metadata from store." if err
@@ -135,10 +140,10 @@

   # Get users with metadata by usernames, omitting sensitive fields.
   #
-  # @param [Array<String>]     usernames
-  # @param [Function]          cb(err, users)
-  # @param [StorageError]      err
-  # @param [Array<PublicUser>] users
+  # @param [Array<String>]      usernames
+  # @param [Function]           cb(err, users)
+  # @param [StorageError]       err
+  # @param [Array<PublicUser>]  users
   getUsersWithMetadata: (usernames, cb) ->
     @store.getUsers usernames, (er1, users) =>
       @store.getUsersMetadata usernames, (er2, metadata) =>
@@ -153,22 +158,22 @@

   # Get repos for a given user.
   #
-  # @param [String] username     The username of the repo owner.
-  # @param [Object] opts         Options for retrieval.
-  # @param [Number] opts.limit   The maximum number of repos to return (Default: 50).
-  # @param [Number] opts.offset  The number of repos to skip. (Default: 0).
-  # @param [Boolean] opts.latest Whether to start with the latest repos. (Default: true).
-  getUserRepos: (username, opts, cb) ->
-    @store.getUserRepos username, opts, (err, repos) ->
+  # @param [String]             username
+  # @param [Object]             options
+  # @param [Number]             options.limit  (Default: 50)
+  # @param [Number]             options.offset (Default: 0)
+  # @param [Boolean]            options.latest (Default: true)
+  getUserRepos: (username, options, cb) ->
+    @store.getUserRepos username, options, (err, repos) ->
       return cb new @Error "Failed getting user repos for '#{username}'", err if err
       return cb null, repos

   # Get a user's password hash.
   #
-  # @param [String]       username Of the user whose hash to retrieve.
-  # @param [Function]     cb(err, hash) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [String]       hash The retrieved password hash.
+  # @param [String]             username
+  # @param [Function]           cb(err, hash)
+  # @param [StorageError]       err
+  # @param [String]             hash
   getPasswordHash: (username, cb) ->
     @store.getPasswordHash username, (err, hash) ->
       return cb new @Error("Failed getting user: "  + username, err) if err
@@ -176,9 +181,9 @@

   # Get an array of all users, omitting sensitive fields.
   #
-  # @param [Function]      cb(err, users) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Object>] users An object array of users' public fields.
+  # @param [Function]           cb(err, users)
+  # @param [StorageError]       err
+  # @param [Array<Object>]      users
   getAllUsers: (cb) ->
     @store.listUsers (er1, usernames) =>
       @getUsersWithMetadata usernames, (er2, users) =>
@@ -190,11 +195,11 @@

   # Add a user repo for a given commit hash.
   #
-  # @param [String] username The user's name.
-  # @param [String] reponame The repo's name.
-  # @param [Function] callback Called on success or error.
-  # @param [Error] err Any error.
-  # @param [String] commitHash The commit's hash.
+  # @param [String]             username
+  # @param [String]             reponame
+  # @param [Function]           callback
+  # @param [Error]              err
+  # @param [String]             commitHash
   addRepo: (username, reponame, commitHash, callback) ->
     @getUser username, (err, user) =>
       if err or not user
@@ -204,10 +209,10 @@

   # Delete a repo by owner and name.
   #
-  # @param [String] username The repo owner.
-  # @param [String] reponame The repo name.
-  # @param [Function] cb(err) Called on success or error.
-  # @param [Error] err Any error.
+  # @param [String]             username
+  # @param [String]             reponame
+  # @param [Function]           cb(err)
+  # @param [Error]              err
   deleteRepo: (username, reponame, cb) ->
     @store.deleteRepo username, reponame, (err) =>
       if err
@@ -221,11 +226,11 @@
   #     storage.getRepoHash username, repo, (err, hash) ->
   #       console.log "This repo points to commit: #{hash}!"
   #
-  # @param [String] username The user's name.
-  # @param [String] reponame The repo's name.
-  # @param [Function] callback Called on success or error.
-  # @param [Error] err Any error.
-  # @param [String] commitHash This repo's commit hash.
+  # @param [String]             username
+  # @param [String]             reponame
+  # @param [Function]           callback
+  # @param [Error]              err
+  # @param [String]             commitHash
   getRepoHash: (username, reponame, callback) ->
     @store.getRepoHash username, reponame, (err, hash) ->
       return callback null, hash
@@ -233,15 +238,15 @@
   # Get the commit and target for a given user repo.
   #
   #     storage.getRepoTarget name, repo, (err, commit, target) ->
-  #       console.log "This repo points to commit: #{commit.sha1()}\n"
+  #       console.log "Got repo for commit: #{commit.sha1()}\n"
   #                   "with target: #{target.sha1()}"
   #
-  # @param [String] username The user's name.
-  # @param [String] reponame The repo's name.
-  # @param [Function] cb(err, commit, target) Called on success or error.
-  # @param [Error] err Any error.
-  # @param [Commit] commit The target commit.
-  # @param [Argument] target The target object.
+  # @param [String]             username
+  # @param [String]             reponame
+  # @param [Function]           cb(err, commit, target)
+  # @param [Error]              err
+  # @param [Commit]             commit
+  # @param [Argument]           target
   getRepoTarget: (username, reponame, cb) ->
     @getRepoHash username, reponame, (er1, hash) =>
       @getCommit hash, (er2, commit) =>
@@ -261,10 +266,10 @@
   #     storage.getRepos keys, (err, repos) ->
   #       console.log "Got repos: ", repos
   #
-  # @param [Array<Array<String>>] keys An array of [username, reponame] arrays.
-  # @param [Function] cb(err, repos) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Array<Repo>] repos The retrieved repos.
+  # @param [Array<Array<String>>] keys
+  # @param [Function]             cb(err, repos)
+  # @param [StorageError]         err
+  # @param [Array<Repo>]          repos
   getRepos: (keys, cb) ->
     @store.getRepos keys, (err, repos) ->
       return cb err if err
@@ -274,9 +279,9 @@

   # Add an argument to the store.
   #
-  # @param [Argument] argument The argument to store.
-  # @param [Function] cb(err) Called on completion or error.
-  # @param [StorageError] err Any error.
+  # @param [Argument]           argument
+  # @param [Function]           cb(err)
+  # @param [StorageError]       err
   addArgument: (argument, cb) ->
     unless argument instanceof Argument
       return cb new @InputError "Argument instance required to store argument."
@@ -287,10 +292,10 @@

   # Get an argument from the store by hash.
   #
-  # @param [String] hash Hash id of the argument to retrieve.
-  # @param [Function] cb(err, args) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Argument] arg The retrieved argument.
+  # @param [String]             hash
+  # @param [Function]           cb(err, args)
+  # @param [StorageError]       err
+  # @param [Argument]           arg
   getArgument: (hash, cb) ->
     @store.getArguments [hash], (err, args) =>
       return cb new @RetrievalError "Failed getting arguments from the store." if err
@@ -299,12 +304,12 @@

   # Get arguments from the store by hashes.
   #
-  # @param [Array<String>]   hashes
-  # @param [Object]          options
-  # @param [Boolean]         options.metadata
-  # @param [Function]        cb(err, args)
-  # @param [StorageError]    err
-  # @param [Array<Argument>] args
+  # @param [Array<String>]      hashes
+  # @param [Object]             options
+  # @param [Boolean]            options.metadata
+  # @param [Function]           cb(err, args)
+  # @param [StorageError]       err
+  # @param [Array<Argument>]    args
   getArguments: (hashes, options..., cb) ->
     options = options[0] ? {}
     return @getArgumentsWithMetadata hashes, cb if options.metadata
@@ -315,10 +320,10 @@

   # Get arguments along with metadata from the store by hashes.
   #
-  # @param [Array<String>]   hashes
-  # @param [Function]        cb(err, args)
-  # @param [StorageError]    err
-  # @param [Array<Argument>] args
+  # @param [Array<String>]      hashes
+  # @param [Function]           cb(err, args)
+  # @param [StorageError]       err
+  # @param [Array<Argument>]    args
   getArgumentsWithMetadata: (hashes, cb) ->
     @store.getArguments hashes, (err, args) =>
       return cb new @RetrievalError "Failed getting arguments from the store." if err
@@ -342,9 +347,9 @@

   # Add an array of propositions to the store.
   #
-  # @param [Array<Proposition>] propositions The propositions array.
-  # @param [Function]     cb(err) Called on completion or error.
-  # @param [StorageError] err Any error.
+  # @param [Array<Proposition>] propositions
+  # @param [Function]           cb(err)
+  # @param [StorageError]       err
   addPropositions: (propositions, cb) ->
     for p in propositions
       unless p instanceof Proposition
@@ -358,10 +363,10 @@

   # Get a proposition from the store by hash.
   #
-  # @param [String] hash Hash id of the proposition to retrieve.
-  # @param [Function] cb(err, proposition) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Proposition] proposition The retrieved proposition.
+  # @param [String]             hash
+  # @param [Function]           cb(err, proposition)
+  # @param [StorageError]       err
+  # @param [Proposition]        proposition
   getProposition: (hash, cb) ->
     @store.getPropositions [hash], (err, propositions) =>
       return cb new @RetrievalError "Failed getting proposition from the store." if err
@@ -370,12 +375,12 @@

   # Get propositions from the store by hashes.
   #
-  # @param [Array<String>]       hashes
-  # @param [Object]              options
-  # @param [Boolean]             options.metadata
-  # @param [Function]            cb(err, propositions)
-  # @param [StorageError]        err
-  # @param [Array<Proposition>]  propositions
+  # @param [Array<String>]      hashes
+  # @param [Object]             options
+  # @param [Boolean]            options.metadata
+  # @param [Function]           cb(err, propositions)
+  # @param [StorageError]       err
+  # @param [Array<Proposition>] propositions
   getPropositions: (hashes, options..., cb) ->
     options = options[0] ? {}
     return @getPropositionsWithMetadata hashes, cb if options.metadata
@@ -386,10 +391,10 @@

   # Get propositions metadata from the store by hashes.
   #
-  # @param [Array<String>] hashes Hash ids of the propositions to retrieve.
-  # @param [Function]      cb(err, propositions) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Object>] metadata The retrieved metadata.
+  # @param [Array<String>]      hashes
+  # @param [Function]           cb(err, propositions)
+  # @param [StorageError]       err
+  # @param [Array<Object>]      metadata
   getPropositionsMetadata: (hashes, cb) ->
     @store.getPropositionsMetadata hashes, (err, metadata) ->
       return cb new @Error "Failed getting propositions metadata from the store." if err
@@ -397,10 +402,10 @@

   # Get propositions (along with metadata) from the store by hashes.
   #
-  # @param [Array<String>] hashes Hash ids of the propositions to retrieve.
-  # @param [Function]      cb(err, propositions) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Proposition>] propositions The retrieved propositions (with metadata).
+  # @param [Array<String>]       hashes
+  # @param [Function]            cb(err, propositions)
+  # @param [StorageError]        err
+  # @param [Array<Proposition>]  propositions
   getPropositionsWithMetadata: (hashes, cb) ->
     @store.getPropositions hashes, (er1, propositions) =>
       @store.getPropositionsMetadata hashes, (er2, metadata) =>
@@ -413,9 +418,9 @@

   # Add a commit to the store.
   #
-  # @param [Commit] commit The commit to store.
-  # @param [Function] cb(err) Called on completion or error.
-  # @param [StorageError] err Any error.
+  # @param [Commit]             commit
+  # @param [Function]           cb(err)
+  # @param [StorageError]       err
   addCommit: (commit, cb) ->
     unless commit instanceof Commit
       return cb new @InputError "Commit instance required to store commit."
@@ -428,10 +433,10 @@

   # Get a commit from the store by hash.
   #
-  # @param [String] hash Hash id of the commit to retrieve.
-  # @param [Function] cb(err, commit) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Commit] commit The retrieved commit.
+  # @param [String]             hash
+  # @param [Function]           cb(err, commit)
+  # @param [StorageError]       err
+  # @param [Commit]             commit
   getCommit: (hash, cb) ->
     @store.getCommits [hash], (err, commits) =>
       return cb new @RetrievalError "Failed getting commit from the store." if err
@@ -440,10 +445,10 @@

   # Get commits from the store by hashes.
   #
-  # @param [Array<String>] hashes Hash ids of the commits to retrieve.
-  # @param [Function]      cb(err, commits) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Commit>] commits The retrieved commits.
+  # @param [Array<String>]      hashes
+  # @param [Function]           cb(err, commits)
+  # @param [StorageError]       err
+  # @param [Array<Commit>]      commits
   getCommits: (hashes, cb) ->
     @store.getCommits hashes, (err, commits) ->
       return cb new @RetrievalError "Failed getting commits from the store." if err
@@ -451,10 +456,10 @@

   # Get commits from the store with given target hashes.
   #
-  # @param [Array<String>]  targetHashes Target hashes of tags to retrieve.
-  # @param [Function]       cb(err, commits) Called on completion or error.
-  # @param [StorageError]   err Any error.
-  # @param [Array<Commits>] commits The retrieved commits.
+  # @param [Array<String>]      targetHashes
+  # @param [Function]           cb(err, commits)
+  # @param [StorageError]       err
+  # @param [Array<Commits>]     commits
   getCommitsFor: (targetHashes, cb) ->
     @store.getCommitsFor targetHashes, (err, commits) ->
       return cb new @RetrievalError "Failed getting commits from the store." if err
@@ -462,9 +467,9 @@

   # Add a tag to the store.
   #
-  # @param [Tag] tag The tag to store.
-  # @param [Function] cb(err) Called on completion or error.
-  # @param [StorageError] err Any error.
+  # @param [Tag]                tag
+  # @param [Function]           cb(err)
+  # @param [StorageError]       err
   addTag: (tag, cb) ->
     unless tag instanceof Tag
       return cb new @InputError "Tag instance required to store tag."
@@ -475,10 +480,10 @@

   # Get a tag from the store by hash.
   #
-  # @param [String] hash Hash id of the tag to retrieve.
-  # @param [Function] cb(err, tag) Called on completion or error.
-  # @param [StorageError] err Any error.
-  # @param [Tag] tag The retrieved tag.
+  # @param [String]             hash
+  # @param [Function]           cb(err, tag)
+  # @param [StorageError]       err
+  # @param [Tag]                tag
   getTag: (hash, cb) ->
     @store.getTags [hash], (err, tags) =>
       return cb new @RetrievalError "Failed getting tag from the store." if err
@@ -487,10 +492,10 @@

   # Get tags from the store by hashes.
   #
-  # @param [Array<String>] hashes Hash ids of the tags to retrieve.
-  # @param [Function]      cb(err, tags) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Tag>] tags The retrieved tags.
+  # @param [Array<String>]      hashes
+  # @param [Function]           cb(err, tags)
+  # @param [StorageError]       err
+  # @param [Array<Tag>]         tags
   getTags: (hashes, cb) ->
     @store.getTags hashes, (err, tags) ->
       return cb new @RetrievalError "Failed getting tags from the store." if err
@@ -498,23 +503,25 @@

   # Get tags from the store with given target hashes.
   #
-  # @param [Array<String>] targetHashes Target hashes of tags to retrieve.
-  # @param [Function]      cb(err, tags) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Tag>]    tags The retrieved tags.
+  # @param [Array<String>]      targetHashes
+  # @param [Function]           cb(err, tags)
+  # @param [StorageError]       err
+  # @param [Array<Tag>]         tags
   getTagsFor: (targetHashes, cb) ->
     @store.getTagsFor targetHashes, (err, tags) ->
       return cb new @RetrievalError "Failed getting tags from the store." if err
       return cb null, tags

-  # Get tags (plus any source objects) from the store given target hashes.
-  #
-  # @param [Array<String>] targetHashes Target hashes of tags to retrieve.
-  # @param [Function]      cb(err, tags, sources, commits) Called on completion or error.
-  # @param [StorageError]  err Any error.
-  # @param [Array<Tag>]    tags The tags for the given targets.
-  # @param [Array<Object>] sources Any source objects for the tags.
-  # @param [Array<Commit>] commits The commits for tags and sources.
+  # Get tags (plus any source objects) from the store for given target hashes.
+  #
+  # Commits for all tags and sources are also retrieved.
+  #
+  # @param [Array<String>]      targetHashes
+  # @param [Function]           cb(err, tags, sources, commits)
+  # @param [StorageError]       err
+  # @param [Array<Tag>]         tags
+  # @param [Array<Object>]      sources
+  # @param [Array<Commit>]      commits
   getTagsPlusSources: (targetHashes, cb) ->
     @getTagsFor targetHashes, (er1, tags) =>
       objectHashes = []
@@ -537,10 +544,12 @@

   # Add a comment to the store.
   #
-  # @param [Comment]           comment
-  # @param [Function]          cb(err, id)
-  # @param [Error]             err
-  # @param [Number]            id
+  # The comment id is retrieved on success.
+  #
+  # @param [Comment]            comment
+  # @param [Function]           cb(err, id)
+  # @param [Error]              err
+  # @param [Number]             id
   addComment: (comment, cb) ->
     unless comment instanceof Comment
       return cb new @InputError "Comment instance required to store comment."
@@ -553,10 +562,10 @@

   # Get comments by ids.
   #
-  # @param [Array<String>]     ids
-  # @param [Function]          cb(err, comments)
-  # @param [Error]             err
-  # @param [Array<Comment>]    comments
+  # @param [Array<String>]      ids
+  # @param [Function]           cb(err, comments)
+  # @param [Error]              err
+  # @param [Array<Comment>]     comments
   getComments: (ids, cb) ->
     @store.getComments ids, (err, comments) =>
       if err
@@ -567,10 +576,12 @@

   # Add a discussion to the store.
   #
-  # @param [Discussion]        discussion
-  # @param [Function]          cb(err, id)
-  # @param [Error]             err
-  # @param [Number]            id
+  # The discussion id is retrieved on success.
+  #
+  # @param [Discussion]         discussion
+  # @param [Function]           cb(err, id)
+  # @param [Error]              err
+  # @param [Number]             id
   addDiscussion: (discussion, cb) ->
     unless discussion instanceof Discussion
       return cb new @InputError "Discussion instance required to store discussion."
@@ -597,10 +608,10 @@

   # Get discussions for the given target hashes.
   #
-  # @param [Array<String>]     targetHashes
-  # @param [Function]          cb(err, discussions)
-  # @param [Error]             err
-  # @param [Array<Discussion>] discussions
+  # @param [Array<String>]      targetHashes
+  # @param [Function]           cb(err, discussions)
+  # @param [Error]              err
+  # @param [Array<Discussion>]  discussions
   getDiscussionsFor: (targetHashes, cb) ->
     @store.getDiscussionsFor targetHashes, (err, discussions) =>
       if err


Style.

Style.

--- client/js/jquery.iscrubber.coffee
+++ client/js/jquery.iscrubber.coffee
@@ -50,7 +50,7 @@
       index = Math.floor(x / pageTriggerWidth)

       # Make sure the index is within bounds (0 <= index < count)
-      index = Math.min(Math.max(0, index), count-1)
+      index = Math.min(Math.max(0, index), count - 1)

       scrub(elements, index)


Await file creation before failing

Await file creation before failing

--- test/helpers/build_helpers.coffee
+++ test/helpers/build_helpers.coffee
@@ -8,6 +8,8 @@
 HOME_DIR = join __dirname, "..", ".."
 BASE_FIXTURE_DIR = join HOME_DIR, "test", "fixtures"
 BUILD_FILE = join __dirname, "builder.coffee"
+
+BUILD_TIMEOUT = 5000

 fixtureDir = (fixture) ->
   join BASE_FIXTURE_DIR, fixture
@@ -40,7 +42,15 @@
 # Asserts that the given test file exists under the fixture's build directory
 assertBuilt = (fixture, expectedFile) ->
   call ->
-    assert yield isBuilt(fixture, expectedFile), "File #{expectedFile} not built"
+    start = now = new Date()
+    built = false
+    while now - start <= BUILD_TIMEOUT
+      if built = yield isBuilt(fixture, expectedFile)
+        break
+      else
+        now = new Date()
+
+    assert built, "File #{expectedFile} not built"

 buildAndVerify = (fixture, expectedFile) ->
   call ->


current use use raven from utils

current use use raven from utils

--- src/client/current-user.coffee
+++ src/client/current-user.coffee
@@ -4,8 +4,7 @@
 Base64   = require '../utils/base64'
 clone    = require '../utils/clone'
 getKey   = require '../utils/get-key'
-
-Raven    = require 'raven-js'
+Raven    = require '../utils/raven'

 # Fix for http://www.hull.io/docs/users/backend on browsers where 3rd party cookies disabled


Exposing client's TaggedNodeCollection as window.__nodes when testing.

--- src/client/dnode_client.coffee
+++ src/client/dnode_client.coffee
@@ -1,14 +1,19 @@
 DNode = require('dnode')
 TaggedNodeCollection = require('./tagged_node_collection')

+test_env = false
+if process?.env?.TESTS_RUNNING
+    test_env = true
+
 module.exports = (window, document) ->
     nodes = new TaggedNodeCollection()
+    if test_env
+        window.__nodes = nodes

     dnodeConnection = DNode( (remote, conn) ->
         console.log "Connecting to server..."
         conn.on('ready', () ->
             console.log "Connection is ready"
-            console.log(remote)
             remote.auth(window.__envSessionID)
         )

@@ -20,7 +25,6 @@
         #TODO: Need to have a "batch proces function".  Need to add "TagDocument"
         # TODO: clear needs to be able to be called on a certain document.
         @DOMUpdate = (params) ->
-            console.log(params)
             processInstruction = (inst) ->
                 # SPECIAL CASE
                 # TODO: this is a quick hack.
@@ -59,7 +63,7 @@
                 if rvID? && /^node\d+$/.test(rvID)
                     if rv.__nodeID == undefined
                         nodes.add(rv, rvID)
-            #printMethodCall(target, method, args, rvID)
+                #printMethodCall(target, method, args, rvID)
             if params instanceof Array
                 for inst in params
                     processInstruction(inst)
@@ -80,6 +84,8 @@
             while document.hasChildNodes()
                 document.removeChild(document.firstChild)
             nodes = new TaggedNodeCollection()
+            if test_env
+                window.__nodes = nodes
             delete document.__nodeID

         # startEvents
@@ -144,7 +150,7 @@
                         return false
         )

-    if process?.env?.TESTS_RUNNING
+    if test_env == true
         console.log("Running DNode over TCP")
         dnodeConnection.connect(3002)
     else


gees -- special case of no quotas ever set

--- salvus/compute.coffee
+++ salvus/compute.coffee
@@ -1295,7 +1295,7 @@
                 else
                     quotas = {}
                     result = result[0]
-                    if result.disk and not result.disk_quota
+                    if result? and result.disk and not result.disk_quota
                         result.disk_quota = Math.round(misc.from_json(result.disk)*1.5)
                     for k, v of DEFAULT_SETTINGS
                         if not result?[k]


Don't display the first reaction as yours when logged out

--- client/angular/components/reactions/display/reactions_display.coffee
+++ client/angular/components/reactions/display/reactions_display.coffee
@@ -1,5 +1,6 @@
 Session        = require 'shared/services/session.coffee'
 Records        = require 'shared/services/records.coffee'
+AbilityService = require 'shared/services/ability_service.coffee'

 { emojiTitle } = require 'shared/helpers/helptext.coffee'

@@ -23,6 +24,7 @@
       mine.destroy() if mine

     $scope.myReaction = ->
+      return unless AbilityService.isLoggedIn()
       Records.reactions.find(_.merge(reactionParams(), userId: Session.user().id))[0]

     $scope.otherReaction = ->


fecha mes a mayusculas

fecha mes a mayusculas

--- app/assets/javascripts/fechas.js.coffee
+++ app/assets/javascripts/fechas.js.coffee
@@ -5,7 +5,7 @@
     $(".fecha").each ->
       $(this).text(moment().lang("es").format("LLL"))
     $(".mes").each ->
-      $(this).text(moment().lang("es").format("MMMM"))
+      $(this).text(moment().lang("es").format("MMMM").toUpperCase())
     $(".fecha_sin_hora").each ->
       $(this).text(moment().lang("es").format("LL"))
     $(".año").each ->


Set focus on Sheet design or Sheet Subject

--- app/assets/javascripts/global.js.coffee
+++ app/assets/javascripts/global.js.coffee
@@ -54,6 +54,12 @@
     .on('focus', "[rel~=popover]", () ->
       $(this).popover( offset: 10, trigger: 'focus' )
     )
+    .ready( () ->
+      if $("#sheet_design_id").val() == ''
+        $("#sheet_design_id").focus()
+      else
+        $("#sheet_subject_id").focus()
+    )

   # $("[rel~=popover]").popover( offset: 10, trigger: 'focus' )
   # $("span[rel~=tooltip]").tooltip( trigger: 'hover' )


fix 'remove panel' js

fix 'remove panel' js

--- app/assets/javascripts/panels.js.coffee
+++ app/assets/javascripts/panels.js.coffee
@@ -40,19 +40,19 @@
         old_name = element.attr('name')
         new_name = old_name.replace(new RegExp(/[0-9]+/), "#{count_nested_fieldsets}")
         element.attr('name', new_name)
-      #required for icon to work. This must go at end of each loop (so it does
-      #not get overwritten by above code)
-      if element.is('#icon1')
-        last_icon_id_number = parseInt(last_fieldset.children('.icon').attr('id').slice(4))
-        old_id = element.attr('id')
-        new_id = old_id.replace(new RegExp(/[0-9]+/), "#{last_icon_id_number + 1}")
-        element.attr('id', new_id)
+
+    #required for icon to work
+    last_icon_id = last_fieldset.children('.icon').attr('id')
+    last_icon_id_number = parseInt(last_icon_id.slice(4))
+    new_id = last_icon_id.replace(new RegExp(/[0-9]+/), "#{last_icon_id_number + 1}")
+    new_fieldset.children('.icon').attr('id', new_id)
+
     new_fieldset.insertBefore($('.add_fields'))
     $('remove panel
').insertBefore($(event.target))

   remove_fields = (event) ->
-    $(event.target).prev('.nested_fields').hide()
-    $(event.target).hide()
+    $(event.target).prev('.nested_fields').remove()
+    $(event.target).remove()

   $(document).on 'click', '.add_fields', (event) ->


Add coverage for 0 rows

Add coverage for 0 rows

--- app/assets/javascripts/purchases.coffee
+++ app/assets/javascripts/purchases.coffee
@@ -1,3 +1,12 @@
+addPurchaseDetailRow = (data) ->
+  if $(".purchase-detail-row:last").attr('class') != undefined
+    lastRowIsOdd = $(".purchase-detail-row:last").attr('class').split(" ").indexOf("odd-row") > 0
+
+  $(".purchase-detail-rows").append(data.content)
+  $(".purchase-detail-row:last").removeClass('odd-row').addClass('even-row') if lastRowIsOdd
+  $(".purchase-category .select2").select2({ theme: "bootstrap", width: "100%" })
+  $(".purchase-item .select2").select2({theme: "bootstrap", width: "100%"})
+
 calculateLineCostAndVariance = (activeElement) ->
   purchaseRow = activeElement.parents ".purchase-detail-row"
   quantityElement = purchaseRow.find ".quantity"
@@ -115,17 +124,12 @@
   purchaseId = $(@).data("purchaseId")
   purchaseDetailIndex = $(".purchase-detail-row").length
   data = purchase_id: purchaseId, purchase_detail_index: purchaseDetailIndex
-  lastRowIsOdd = $(".purchase-detail-row:last").attr('class').split(" ").indexOf("odd-row") > 0

   $.ajax "/purchase_details",
          type: 'POST',
          dataType: 'json',
          data: data
-         success: (data) ->
-           $(".purchase-detail-rows").append(data.content)
-           $(".purchase-detail-row:last").removeClass('odd-row').addClass('even-row') if lastRowIsOdd
-           $(".purchase-category .select2").select2({ theme: "bootstrap", width: "100%" })
-           $(".purchase-item .select2").select2({theme: "bootstrap", width: "100%"})
+         success: (data) -> addPurchaseDetailRow(data)

 $(document).on "click", ".remove-purchase-detail-fields", (event) ->
   event.preventDefault()


Mark notifications as read first and then set the unread count

--- components/logged_in_navigation/components/notifications/view.coffee
+++ components/logged_in_navigation/components/notifications/view.coffee
@@ -21,9 +21,9 @@
             @state.set('is_fetching', false)

       .on 'mouseleave click', (e) =>
+        @collection.markRead()
         @state.set('unread_count', 0)
-        @collection.markRead()
-
+
   render: ->
     @$el.html template
       feed: @collection


Sauce and Donate links in the same line

Sauce and Donate links in the same line

--- modules/player/hud.coffee
+++ modules/player/hud.coffee
@@ -74,9 +74,8 @@
     reply =
       url: item.sauce
       color: 0xAAFF00
-      title: '[click for sauce]'
       description: """
-      [[donate]](https://tblnk.me/focabot-donate/)
+      [[sauce]](#{item.sauce}) / [[donate]](https://tblnk.me/focabot-donate/)
       **Length**: #{@util.displayTime item.duration}
       **Position in queue**: ##{pos}
       """
@@ -101,9 +100,8 @@
     reply =
       url: item.sauce
       color: 0xF44277
-      title: '[click for sauce]'
       description: """
-      [[donate]](https://tblnk.me/focabot-donate/)
+      [[sauce]](#{item.sauce}) / [[donate]](https://tblnk.me/focabot-donate/)
       **Length**: #{@util.displayTime item.duration}
       """
       author:
@@ -156,9 +154,8 @@
     r ={
       url: item.sauce
       color: 0xCCAA00
-      title: '[click for sauce]'
       description: """
-      [[donate]](https://tblnk.me/focabot-donate/)
+      [[sauce]](#{item.sauce}) / [[donate]](https://tblnk.me/focabot-donate/)
       #{@generateProgressOuter item}
       **Length**: #{@util.displayTime item.duration}
       """
@@ -174,7 +171,7 @@
     if item.filters and item.filters.length
       r.description += "\n**Filters**: #{@util.displayFilters item.filters}"
     if item.radioStream
-      r.description += "\n\n#{await @radioInfo(item)}"
+      r.description += "\n#{await @radioInfo(item)}"
     r

   queue: (q, page=1)=>


use new method

use new method

--- src/modules/link-tooltip.coffee
+++ src/modules/link-tooltip.coffee
@@ -35,10 +35,7 @@
   initListeners: ->
     @quill.on(@quill.constructor.events.SELECTION_CHANGE, (range) =>
       return unless range? and range.isCollapsed()
-      index = Math.max(0, range.start - 1)
-      [line, offset] = @quill.editor.doc.findLineAt(index)
-      return unless line?
-      [leaf, offset] = line.findLeafAt(offset)
+      [leaf, offset] = @quill.editor.doc.findLeafAt(Math.max(0, range.start - 1))
       node = leaf.node
       while node?
         if node.tagName == 'A'


logger: ain2 syslog transport

logger: ain2 syslog transport

--- src/logger.coffee
+++ src/logger.coffee
@@ -12,8 +12,11 @@
         ain2.set
             tag: 'buddycloud'
             facility: 'daemon'
+            transport: 'file'
         if config_.syslog.hostname
-            ain2.set hostname: config_.syslog.hostname
+            ain2.set
+                transport: 'udp'
+                hostname: config_.syslog.hostname
         if config_.syslog.port
             ain2.set port: config_.syslog.port


No need to get value

No need to get value

--- src/plugins/oldiexdomain.coffee
+++ src/plugins/oldiexdomain.coffee
@@ -26,7 +26,7 @@
     # use XDomainRequest so that users know there's an issue right away, instead
     # of if/when they test in IE9.
     if req.headers
-      for own k, v of req.headers
+      for own k of req.headers
         throw new Error "You can't provide request headers when using the oldiexdomain plugin."

     # Don't do anything if we can't do anything (:


simple-search: Simplify, limit and extend query

- Maximum 100 entities per input pattern
- Maximum 100 links per entity
- Search for identifiers, identifier (old) and _id

--- src/routes/simple-search.coffee
+++ src/routes/simple-search.coffee
@@ -2,40 +2,69 @@
 Async  = require 'async'
 log = require('../log')(module)

+_denoise_id = (s) ->
+	sold = s
+	s = s.replace(/doi:/i, '')
+	s = s.replace(new RegExp("https?://(dx\\.)?doi.org/?"), '')
+	log.debug("_denoise_id: #{sold} -> #{s}")
+	return s
+
+_get_id = (s) ->
+	snew = s.substr(s.lastIndexOf('/') + 1)
+	log.debug("_get_id: #{s} -> #{snew}")
+	return snew
+
 module.exports = (app, done) ->

+	_find_links = (needleQuery, cb) ->
+		log.debug("Entity query: #{needleQuery}")
+		{Entity, EntityLink} = app.schemo.models
+		Entity.find(needleQuery).limit(100)
+			.exec (err, entities) ->
+				return cb err if err
+				return cb null, [] if entities.length == 0
+				linkQuery = $or: []
+				for tofrom in ['to', 'from']
+					for entity in entities
+						linkQuery.$or.push "#{tofrom}Entity": "#{entity.uri()}"
+				log.debug("Link query: ", linkQuery)
+				EntityLink.find(linkQuery).limit(100).exec (err, links) ->
+					return cb err if err
+					log.debug("Links:", x.uri() for x in links)
+					Async.map links, (link, linkDone) ->
+						link = link.toJSON()
+						return linkDone "Bad link" if not link.fromEntity or not link.toEntity
+						Async.each ['from', 'to'], (tofrom, tofromDone) ->
+							Entity.findOne(_id: _get_id(link["#{tofrom}Entity"])).exec (err, tofromEntity) ->
+								return tofromDone "Error" + err if err or not tofromEntity
+								link["#{tofrom}Entity"] = tofromEntity.toJSON()
+								return tofromDone()
+						, (err) ->
+							return linkDone err, link
+					, (err, newLinks) ->
+						return cb err if err
+						log.debug(newLinks)
+						cb null, newLinks
+
 	# Swagger interface
-	app.get '/simple-search', (req, res, next) ->
-		res.render 'simple-search'
-
-	app.get '/simple-search/link/:id(*)', (req, res, next) ->
-		log.debug("ID to look for: #{req.params.id}")
-		app.schemo.models.Entity.find(identifier:req.params.id)
-			.exec (err, entities) ->
-				return next err if err
-				linkQuery = $or: []
-				for entity in entities
-					linkQuery.$or.push fromEntity: "#{entity.uri()}"
-					linkQuery.$or.push toEntity: "#{entity.uri()}"
-				RET = []
-				app.schemo.models.EntityLink.find(linkQuery)
-					.exec (err, links) ->
-						return next err if err
-						Async.each links, (link, linkDone) ->
-							if not link.fromEntity or not link.toEntity
-								return linkDone()
-							app.schemo.models.Entity.findOne _id: link.fromEntity.substr(link.fromEntity.lastIndexOf('/') + 1)
-								.exec (err, fromEntity) ->
-									return linkDone err if err
-									return linkDone() if not fromEntity
-									app.schemo.models.Entity.findOne _id: link.toEntity.substr(link.toEntity.lastIndexOf('/') + 1)
-										.exec (err, toEntity) ->
-											return linkDone err if err
-											return linkDone() if not toEntity
-											RET.push [fromEntity.identifier, toEntity.identifier]
-											return linkDone null
-						, (err) ->
-							return next err if err
-							res.send RET
+	app.get '/search', (req, res, next) ->
+		locals = links: [], query: req.query
+		if not req.query.id
+			return res.render 'simple-search', locals
+		req.query.id = _denoise_id(req.query.id)
+		if not req.query.regex
+			needle = req.query.id
+		else
+			needle = '$regex': req.query.id
+		needleQuery = $or: []
+		needleQuery.$or.push _id: needle
+		needleQuery.$or.push identifier: needle
+		needleQuery.$or.push identifiers: needle
+		_find_links needleQuery, (err, links) ->
+			if err
+				next err
+			else
+				locals.links = links
+				return res.render 'simple-search', locals

 	done()


Does not always return on org-pulls

Does not always return on org-pulls

--- src/scripts/github-pulls.coffee
+++ src/scripts/github-pulls.coffee
@@ -63,7 +63,7 @@

     unless (org_name)
       msg.send "No organization specified, please provide one or set HUBOT_GITHUB_ORG accordingly."
-    return
+      return

     url = "#{url_api_base}/orgs/#{org_name}/issues?filter=all"
     github.get url, (issues) ->


Implemented stream and toBuffer

Implemented stream and toBuffer

--- src/io.coffee
+++ src/io.coffee
@@ -6,10 +6,10 @@
 {Buffer} = require 'buffer'
 {Stream} = require 'stream'

-tmpFilename = ->
+tmpFilename = (ext = 'tmp') ->
   tmpDir = process.env.TMPDIR || '/tmp'
   base = Math.random().toString(36).slice(2,12)
-  path.resolve tmpDir, "node-webp-#{base}.tmp"
+  path.resolve tmpDir, "node-webp-#{base}.#{ext}"

 proto =
   _createFileSource: ->
@@ -19,18 +19,18 @@
       nodefn.call fs.writeFile, filename, @source
     else if @source instanceof Stream
       deferred = When.defer()
-      stream = fs.createWriteStream filename
+      stream = fs.createWriteStream(filename)
+        .once('error', deferred.reject)
+        .once('finish', deferred.resolve)
       @source.pipe stream
-      stream.on 'error', deferred.reject
-      stream.on 'finish', deferred.resolve
       deferred.promise
     else
       When.reject new Error 'Mailformed source'
     @_tmpFilename = promise.then -> filename

-  _cleanup: ->
-    return unless promise = @_tmpFilename
-    delete @_tmpFilename
+  _cleanup: (varname) ->
+    return unless varname and promise = @[varname]
+    delete @[varname]
     When(promise).then (filename) ->
       nodefn.call fs.unlink filename if filename
     .otherwise ->
@@ -49,14 +49,33 @@
       else
         throw new Error 'outname in not specified'
     .tap =>
-      @_cleanup()
+      @_cleanup '_tmpFilename'
     nodefn.bindCallback promise, next

+  _writeTmp: ->
+    return @_tmpOutname if @_tmpOutname
+    outname = tmpFilename 'webp'
+    @_tmpOutname = @write(outname).then -> outname
+
   toBuffer: (next) ->
-    return @
+    promise = @_writeTmp().then (outname) ->
+      nodefn.call fs.readFile, outname
+    .tap =>
+      @_cleanup '_tmpOutname'
+    nodefn.bindCallback promise, next

   stream: (next) ->
-    return @
+    done = false
+    cleanup = =>
+      return if done
+      done = true
+      @_cleanup '_tmpOutname'
+    promise = @_writeTmp().then (outname) ->
+      fs.createReadStream(outname)
+        .once('error', cleanup)
+        .once('close', cleanup)
+        .once('end', cleanup)
+    nodefn.bindCallback promise, next

 module.exports = (Webp) ->
   for name, method of proto


we aren't checking the dots any more

we aren't checking the dots any more

--- client/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -205,7 +205,6 @@
           @showSuccess domain

     else # create a subdomain
-      # replace the dots at the end if there are any
       subDomainPattern = /^[a-zA-Z0-9][a-zA-Z0-9.-]+[a-zA-Z0-9]$/
       unless subDomainPattern.test domainName
         createButton.hideLoader()


Add coloring to own chat and switches sides that self and other chatters appear on.

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -129,9 +129,9 @@

     get_timeago: ->
         if @sender_is_viewer()
+            pull = "pull-right small"
+        else
             pull = "pull-left small"
-        else
-            pull = "pull-right small"
         <div className={pull} style={color:'#888', marginTop:'2px'}>
             <TimeAgo date={new Date(@props.message.get('date'))} />
         </div>
@@ -145,6 +145,10 @@

     content_column: ->
         value = @props.message.get('payload')?.get('content')
+        if @sender_is_viewer()
+            color = 'info'
+        else
+            color = ''
         # just for fun.
         value = value.replace(/:-\)/g, "☺")
                      .replace(/:-\(/g, "☹")
@@ -159,7 +163,7 @@
         <Col key={1} xs={8}>
             <Panel style={wordWrap:"break-word"}>
                 <ListGroup fill>
-                    <ListGroupItem>
+                    <ListGroupItem bsStyle={color}>
                         <Markdown value={value} project_id={@props.project_id} file_path={@props.file_path} />
                     </ListGroupItem>
                     {@get_timeago()}
@@ -173,14 +177,13 @@
     render: ->
         cols = []
         if @sender_is_viewer()
+            cols.push(@blank_column())
+            cols.push(@content_column())
+            cols.push(@avatar_column())
+        else
             cols.push(@avatar_column())
             cols.push(@content_column())
             cols.push(@blank_column())
-        else
-            cols.push(@blank_column())
-            cols.push(@content_column())
-            cols.push(@avatar_column())
-
         <Row>
             {cols}
         </Row>


webapp/analytics: robustness against not finding the actual GA object (init errors, etc.)

--- src/smc-webapp/misc_page.coffee
+++ src/smc-webapp/misc_page.coffee
@@ -1552,12 +1552,13 @@
     # GoogleAnalyticsObject contains the possibly customized function name of GA.
     # It's a good idea to call it differently from the default 'ga' to avoid name clashes...
     if window.GoogleAnalyticsObject?
-        switch type
-            when 'event', 'pageview'
-                ga = window[window.GoogleAnalyticsObject]
-                ga('send', type, args...)
-            else
-                console.warn("unknown analytics event '#{type}'")
+        ga = window[window.GoogleAnalyticsObject]
+        if ga?
+            switch type
+                when 'event', 'pageview'
+                    ga('send', type, args...)
+                else
+                    console.warn("unknown analytics event '#{type}'")

 exports.analytics_pageview = (args...) ->
     exports.analytics('pageview', args...)


Fix bug with ship canonical names.

Fix bug with ship canonical names.

--- coffeescripts/cards-common.coffee
+++ coffeescripts/cards-common.coffee
@@ -6521,7 +6521,7 @@
                 throw e

     for ship_name, ship_data of basic_cards.ships
-        ship_data.english_name = ship_name
+        ship_data.english_name ?= ship_name
         ship_data.canonical_name = ship_data.english_name.canonicalize()

     # Set sources from manifest
@@ -6740,4 +6740,5 @@
 exportObj.renameShip = (english_name, new_name) ->
     exportObj.ships[new_name] = exportObj.ships[english_name]
     exportObj.ships[new_name].name = new_name
+    exportObj.ships[new_name].english_name = english_name
     delete exportObj.ships[english_name]


S59447: Add Unscheduled to iteration picker- part 1 (backwards compat test tweaks

--- test/spec/iterationsummary/IterationSummaryAppSpec.coffee
+++ test/spec/iterationsummary/IterationSummaryAppSpec.coffee
@@ -15,6 +15,7 @@
           workspace:globalContext.getWorkspace()
           user:globalContext.getUser()
           subscription:globalContext.getSubscription()
+          timebox: @mom.getRecord('iteration')
         }, initialValues)
       })


Update css

Update css

--- docpad.coffee
+++ docpad.coffee
@@ -41,8 +41,8 @@
 			# Styles
 			styles: [
 				"/styles/bootstrap.css?v=2.0",
-				"/styles/style.css?v=2.0",
-				"/styles/site.css?v=2.0",
+				"/styles/style.css?v=2.01",
+				"/styles/site.css?v=2.01",
 				"/styles/monokai-sublime.css?v=2.0"
 			]


disable debugging for tf operation cmdline

--- src/tf.coffee
+++ src/tf.coffee
@@ -165,7 +165,7 @@
     environment = ekvs.join " "

     cmdline = "cd #{basepath}/#{projname}; #{environment} terraform #{action} -input=false -no-color"
-    msg.send {room: msg.message.user.name}, "```\n#{cmdline}\n```"
+    #msg.send {room: msg.message.user.name}, "```\n#{cmdline}\n```"
     exec cmdline, (error, stdout, stderr) ->
       if stderr
         msg.send {room: msg.message.user.name}, "stderr:\n```\n#{stderr}\n```"


on initialize, call renderSymbols only once

--- src/ui.coffee
+++ src/ui.coffee
@@ -109,11 +109,9 @@
     render()
     updateSymbols()

+  zoom.on('zoomend', renderSymbols)
   d3.select(window).on('resize', resize)
   resize()
-
-  zoom.on('zoomend', renderSymbols)
-  renderSymbols()

 d3.json 'build/ciucas.json', (error, map) ->


Add missing `

Add missing `

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -185,7 +185,7 @@
   # factions
   message += '\n'
   message += '*Factions*\n'
-  message += '`material exchange\n'
+  message += '`material exchange`\n'

   attachment =
     title: "Using the Weekly Bot"


sanitize help command from normal use

sanitize help command from normal use

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -8,6 +8,9 @@
 module.exports = (robot) ->
   # executes when any text is directed at the bot
   robot.respond /(.*)/i, (res) ->
+    if res.match[1] is 'help'
+      return
+
     array = res.match[1].split ' '

     # trims spaces and removes empty elements in array


add explicit dependency for filtered-collection

--- scripts/config.coffee
+++ scripts/config.coffee
@@ -136,6 +136,10 @@
       deps: ["css!#{BOWER}/jsdifflib/diffview"]
       exports:'diffview'

+    'filtered-collection':
+      deps: ['backbone']
+
+
   # Handlebars Requirejs Plugin Configuration
   # This configures `requirejs` plugins (used when loading templates `'hbs!...'`).
   hbs:


stringify content before send to slack.

stringify content before send to slack.

--- scripts/sledge.coffee
+++ scripts/sledge.coffee
@@ -30,6 +30,6 @@
       req.body

     msg     = data.message
-    content = data.content
+    content = JSON.stringify data.content, null, 2
     robot.messageRoom "#{room}", "#{msg}\n```#{content}```"
     res.send 'OK'


Fix groupId

Fix groupId

--- app/annotation/summary.cjsx
+++ app/annotation/summary.cjsx
@@ -36,6 +36,7 @@
     ProjectGroup.off 'fetch-fail', @onProjectGroupFail

   onProjectGroupFetch: ->
+    groupId = Subject.current?.group_id
     group = ProjectGroup.find groupId
     @setState({
       group: group


Fix emails and username mixup

Fix emails and username mixup

--- app/controllers/auth.coffee
+++ app/controllers/auth.coffee
@@ -175,7 +175,7 @@
                 data:
                   token: access.oauth_token
                   secret: access.oauth_token_secret
-                success: ([uid, token, name, email]) =>
+                success: ([uid, token, email, name]) =>
                   @saveToken(uid, token)
                   Setting.set("user_name", name)
                   Setting.set("user_email", email)


reset ipc here

reset ipc here

--- lib/connection/client.coffee
+++ lib/connection/client.coffee
@@ -101,10 +101,13 @@
       @clientCall 'interrupts', 'interrupt'

   kill: ->
-    if @isActive() and not @isWorking()
-      @import('exit')().catch ->
+    if @isActive()
+      if not @isWorking()
+        @import('exit')().catch ->
+      else
+        @clientCall 'kill', 'kill'
     else
-      @clientCall 'kill', 'kill'
+      @ipc.reset()

   clargs: ->
     {precompiled, optimisationLevel, deprecationWarnings} =


Create daemon.json on first start

Create daemon.json on first start

--- lib/daemon-management.coffee
+++ lib/daemon-management.coffee
@@ -14,6 +14,8 @@
   daemonItems: null
   rootPackageDir: null

+  daemonsFile: null
+
   increment : 0

   constructor: (@smartDaemonControl) ->
@@ -27,11 +29,18 @@
     @daemonStatusBarContainerView = new DaemonStatusBarContainerView(@smartDaemonControl.state.smartDaemonControlViewState,this)
     @daemonControl = new DaemonControl()

+    @createDamonsJsonIfNotExist()
     @loadDaemonItems()

   consumeStatusBar: (statusBar) ->
     @daemonStatusBarContainerView.initialize statusBar
     @daemonStatusBarContainerView.attach()
+
+
+  createDamonsJsonIfNotExist: ->
+    @daemonsFile = new File "#{@rootPackageDir}/daemons.json"
+    #if not @daemonsFile.existsSync()
+    @daemonsFile.writeSync "{}" if @daemonsFile.create()

   loadDaemonItems: () ->
     @daemonItems = require "../daemons.json"
@@ -47,8 +56,7 @@
     @daemonStatusBarContainerView.items[item.id].refresh()

   saveDaemonItems: () ->
-    file = new File "#{@rootPackageDir}/daemons.json"
-    file.write JSON.stringify(@daemonItems,null,4)
+    @daemonsFile.write JSON.stringify(@daemonItems,null,4)

   addDaemon : (item) ->
     if item instanceof DaemonItem


fix pagination loading indicators

fix pagination loading indicators

--- app/js/views/threads.coffee
+++ app/js/views/threads.coffee
@@ -19,6 +19,9 @@
       $('#toggle-lists').attr('checked', false) # Collapse the menu on mobile

       @$el.removeClass('loading')
+
+    @listenTo @collection, 'request', @startPaginating
+    @listenTo @collection, 'sync', @donePaginating

     @stateChange()

@@ -51,8 +54,7 @@

   paginate: =>
     return if @isPaginating || @collection.donePaginating || !@shouldPaginate()
-    @startPaginating
-    @collection.paginate().then(@donePaginating).done(@paginate)
+    @collection.paginate().done(@paginate)

   shouldPaginate: ->
     @$content.children().height() - @$content.scrollTop() < @$content.height() + 300


delay the client notification during start, so that the backbone collection's initialization completes before we send notifications

--- sync.coffee
+++ sync.coffee
@@ -300,6 +300,7 @@
 			@initFromLocalStorage = @loaded

 		if @loaded
+			notify_client_from_localStorage = [];
 			for own id, entity of @data.store
 				entity_copy = @jd.deepCopy(entity)
 				for change in @data.send_queue
@@ -308,7 +309,12 @@
 							entity_copy.object = @jd.apply_object_diff entity_copy.object, change.v
 						catch error
 							console.log id, entity_copy, change
-				@_notify_client id, entity_copy.object, entity_copy.version
+				notify_client_from_localStorage.push [ id, entity_copy.object, null, null, entity_copy.version ]
+			setTimeout( () =>
+				for notify in notify_client_from_localStorage
+					@_notify_client.apply @, notify
+
+			, 100 )
 			@loaded = 0

 		@started = true


improved logging

improved logging

--- lib/auth/Controllable.coffee
+++ lib/auth/Controllable.coffee
@@ -1,5 +1,5 @@

-module.exports =
+module.exports =

     ##
     ## @class floyd.auth.Controllable
@@ -7,113 +7,116 @@
     class Controllable

         ##
-        ##
+        ##
         ##
         constructor: (@ID, @parent)->
-
+
             @_hiddenKeys = ['constructor', 'identity', 'parent', 'logger', 'destroy']
-
+
             if @ID
                 @logger = @_createLogger @ID
-
+
                 @identity = @_createIdentity()
-
+

         ##
         ##
         ##
         destroy: (done)->
-            #@logger.info 'destroy', @identity.id, done
+            @logger.finer 'destroy', @identity.id, done
             @_getAuthManager().destroyIdentity @identity, (err)=>
                 return done(err) if err
-
+
                 if @__authManager
                     @_getAuthManager().destroy done
-
+
                 else done()
         ##
         ##
         ##
         _createLogger: (id)->
-
+
             new floyd.logger.Logger id
-
+

         ##
-        ##
+        ##
         ##
         _createIdentity: (id)->
             if @identity
                 id = @identity.id+'.'+id
-            else
+            else
                 id = @ID
-
-            #@logger.debug 'createIdentity', id
+
+            @logger.finer 'createIdentity', id
             @_getAuthManager().createIdentity id
-
-
-
+
+
+
         ##
         ##
         ##
         _getAuthManager: ()->
-            if ( manager = @__authManager || @parent?._getAuthManager?() )
+            if ( manager = @__authManager || @parent?._getAuthManager?() )
                 return manager

-            @__authManager = @_createAuthManager()
-
-
-
+            @__authManager = @_createAuthManager()
+
+
+
         ##
         ##
         ##
         _createAuthManager: ()->
             new floyd.auth.Manager @_createAuthHandler()

-
+
         ##
         ##
         ##
         _createAuthHandler: ()->
             new floyd.auth.Handler()
-
+
         ##
-        ## @param identity -
+        ## @param identity -
         ##
         forIdentity: (identity, fn)->
-
-            @logger.debug 'forIdentity', identity.id
-
+
+            @logger.finer 'forIdentity', identity.id
+
             @_allowAccess identity, (err)=>
-                return fn(err) if err
-
-                @logger.debug 'allowed access for', identity.id
-
+                if err
+                    @logger.warning 'access denied to', identity.id
+                    return fn(err)
+
+                @logger.fine 'access granted to', identity.id
+
                 wrapper = {}
-
-                ##
+
+                ##
                 floyd.tools.objects.process @,
-
+
                     ##
                     each: (key, value, next)=>
-
+
                         if key.charAt(0) isnt '_' && @_hiddenKeys.indexOf(key) is -1
-
-                            if typeof value is 'function'
-
+
+                            if typeof value is 'function'
+
                                 wrapper[key] = (_args...)=>
-
+
                                     @_permitAccess identity, key, _args, (err)=>
                                         if err
+                                            @logger.warning 'access to % denied to %s', key, identity.id
                                             if typeof (_fn = _args.pop()) is 'function'
                                                 return _fn err
                                             else
-                                                throw err
-
-                                        ## EXPERIMENTAL bind the identity to the method...
+                                                throw err
+
+                                        ## EXPERIMENTAL bind the identity to the method...
                                         value.identity = identity
-
-                                        try
+
+                                        try
                                             res = value.apply @, _args
                                             value.identity = @identity

@@ -121,39 +124,39 @@
                                             value.identity = @identity

                                             if typeof (_fn = _args.pop()) is 'function'
-                                                return _fn err
+                                                return _fn err
                                             else
-                                                throw err
-
-
+                                                throw err
+
+
                                         return res
-
+
                             else
-
+
                                 wrapper[key] = value
-
-
+
+
                         next()
-
-
+
+
                     ##
                     done: (err)->
                         return fn(err) if err
                         process.nextTick ()-> ## this is to prevent loooong stack traces
                             fn null, wrapper
-
-
-
+
+
+
         ##
         ##
         ##
         _allowAccess: (identity, fn)->
-            @logger.debug '_allowAccess', identity.id
+            @logger.finest '_allowAccess', identity.id
             @_getAuthManager().authenticate identity, fn
-
-
-
-
+
+
+
+
         ##
         ##
         ##


Preventdefault -> cancel()

Preventdefault -> cancel()

--- lib/autocomplete-view.coffee
+++ lib/autocomplete-view.coffee
@@ -37,11 +37,11 @@
     @subscribeToCommand @editorView, 'autocomplete:next', => @selectNextItemView()
     @subscribeToCommand @editorView, 'autocomplete:previous', => @selectPreviousItemView()

-    @filterEditorView.getModel().on 'will-insert-text', ({preventDefault, text}) =>
+    @filterEditorView.getModel().on 'will-insert-text', ({cancel, text}) =>
       unless text.match(@wordRegex)
         @confirmSelection()
         @editor.insertText(text)
-        preventDefault()
+        cancel()

   setCurrentBuffer: (@currentBuffer) ->


Make sure to do inline 'toMarkdown' on heading too

--- lib/birch-to-markdown.coffee
+++ lib/birch-to-markdown.coffee
@@ -51,7 +51,7 @@
     context.listIndex = 0
     context.listLevel = 0
     context.headingLevel++
-    repeat('#', context.headingLevel) + '  ' + item.bodyHTML
+    repeat('#', context.headingLevel) + '  ' + toMarkdown item.bodyHTML

   @didVisitHEADING: (item, context) ->
     context.headingLevel--


Stacks: Variables: Show warning tooltip for invalid state.

--- client/admin/lib/views/stacks/variablesview.coffee
+++ client/admin/lib/views/stacks/variablesview.coffee
@@ -91,6 +91,7 @@
     else if newState is 'INVALID'
       @indicator.setClass 'red'
       @indicator.updatePartial '!'
+      @indicator.setTooltip title: STATES.INVALID

   isPassed: -> @_state in ['PASSED', 'INITIAL']


Prettify json schemas

Prettify json schemas

--- src/execute-transaction.coffee
+++ src/execute-transaction.coffee
@@ -21,17 +21,27 @@
     return this.slice(0, str.length) is str

 prettify = (transaction) ->
+  prettyJson = (obj) ->
+    parsed = obj
+    try
+      parsed = JSON.parse obj
+    catch e
+      logger.debug "Error parsing as json: " + obj
+
+    obj = parsed
+    return parsed
+
+
   type = transaction?.headers['Content-Type'] || transaction?.headers['content-type']
   switch type
     when 'application/json'
-      try
-        parsed = JSON.parse transaction.body
-      catch e
-        logger.debug "Error parsing body as json: " + transaction.body
-        parsed = transaction.body
-      transaction.body = parsed
+      transaction.body = prettyJson transaction.body
     when 'text/html'
       transaction.body = html.prettyPrint(transaction.body, {indent_size: 2})
+
+  if transaction?.schema
+    transaction.schema = prettyJson transaction.schema
+
   return transaction

 executeTransaction = (transaction, callback) ->
@@ -111,8 +121,8 @@
         expected =
           headers: flattenHeaders response['headers']
           body: response['body']
-          bodySchema: response['schema']
-          statusCode: response['status']
+          schema: response['schema']
+          status: response['status']

         gavel.isValid real, expected, 'response', (error, isValid) ->
           configuration.emitter.emit 'test error', test if error


need Observer

need Observer

--- resources/assets/coffee/react/comments-show/main.coffee
+++ resources/assets/coffee/react/comments-show/main.coffee
@@ -17,6 +17,7 @@
 ###

 import { Comment } from 'comment'
+import { Observer } from 'mobx-react'
 import core from 'osu-core-singleton'
 import * as React from 'react'
 import { a, button, div, h1, li, ol, p, span } from 'react-dom-factories'
@@ -26,27 +27,25 @@

 export class Main extends React.PureComponent
   render: =>
-    commentsByParentId = store.getGroupedByParentId()
+    el Observer, null, () =>
+      mainComment = store.getCommentsByParentId(@props.comment.parent_id)[0]

-    mainComment = commentsByParentId[@props.comment.parent_id][0]
-    children = commentsByParentId[mainComment.id] ? []
+      div null,
+        div className: 'header-v3 header-v3--comments',
+          div className: 'header-v3__bg'
+          div className: 'header-v3__overlay'
+          div className: 'osu-page osu-page--header-v3',
+            @renderHeaderTitle()
+            @renderHeaderTabs()

-    div null,
-      div className: 'header-v3 header-v3--comments',
-        div className: 'header-v3__bg'
-        div className: 'header-v3__overlay'
-        div className: 'osu-page osu-page--header-v3',
-          @renderHeaderTitle()
-          @renderHeaderTabs()
-
-      div className: 'osu-page osu-page--comment',
-        el Comment,
-          comment: mainComment
-          parent: @props.comment.parent
-          showCommentableMeta: true
-          depth: 0
-          linkParent: true
-          modifiers: ['dark', 'single']
+        div className: 'osu-page osu-page--comment',
+          el Comment,
+            comment: mainComment
+            parent: @props.comment.parent
+            showCommentableMeta: true
+            depth: 0
+            linkParent: true
+            modifiers: ['dark', 'single']

   renderHeaderTabs: =>


fix coffee typos

fix coffee typos

--- materialize.coffee
+++ materialize.coffee
@@ -297,8 +297,8 @@
 Template.afPickadate.helpers
   atts: ->
     atts = _.clone @atts
-    delete atts.dateTimePickerOptions;
-    delete atts.pickadateOptions;
+    delete atts.dateTimePickerOptions
+    delete atts.pickadateOptions
     atts

 AutoForm.addInputType 'switch',


edison.coffee: Fix networking options

edison.coffee: Fix networking options

[issue #1]

Signed-off-by: Florin Sarbu <3a837a37a2ac9072a088641ec330e0e5d5b86f4e@resin.io>

--- edison.coffee
+++ edison.coffee
@@ -1,5 +1,5 @@
 deviceTypesCommon = require 'resin-device-types/common'
-networkOptions = deviceTypesCommon
+{ networkOptions } = deviceTypesCommon

 OSX_DFU_COREUTILS_USBUTILS = '''
 	You need MacPorts installed on your system.


updated server docs

updated server docs

--- lib/datatables.server.coffee
+++ lib/datatables.server.coffee
@@ -1,20 +1,27 @@
 # Server
 # ======
+# #### DataTable
+# A static class that provides a simple pub / sub interface for client dataTables
 class DataTable
-  @debug: false
-
+  # ##### @countCollection
+  # The name of the client only collection that tracks the collection counts for its base and filtered queries.
   @countCollection: "datatable_subscription_count"

-  @isDebug: -> return DataTable.debug or false
-
-  @log: ( message, object ) ->
-    if DataTable.isDebug()
-      if message.indexOf( DataTable.isDebug() ) isnt -1 or DataTable.isDebug() is "true"
-        console.log "dataTable:#{ message } ->", object
-
+  # ##### @publish()
+  # A static method for creating paginated DataTables publications. `DataTable.publish` takes a subscription name (string)
+  # and a Meteor Collection as parameters.
   @publish: ( subscription, collection ) ->
     Match.test subscription, String
     Match.test collection, Object
+
+    # ###### Meteor.publish
+    # The publication that DataTable provides is a paginated and filtered subset of the base query defined by the DataTable
+    # on the client.
+    # ###### Parameters
+    #   + baseQuery: ( object ) the initial query on the dataset. Eventually this may be defined only on the server, to prevent
+    # clients from gaining access to the entire dataset if they modify the baseQuery.
+    #   + filteredQuery: ( object ) the filter being applied by the client datatable's current state.
+    #   + options : ( object ) sort and pagination options supplied by the client datatables's current state.
     Meteor.publish subscription, ( baseQuery, filteredQuery, options ) ->
       self = @
       initialized = false
@@ -25,34 +32,56 @@
       DataTable.log "#{ subscription }:query:base", baseQuery
       DataTable.log "#{ subscription }:query:filtered", filteredQuery
       DataTable.log "#{ subscription }:options", options
-      updateCount = ( ready, first = false ) ->
+
+      # ###### updateCount
+      # Update the count values of the client DataTableCount Collection to reflect the current filter state.
+      updateCount = ( ready, added = false ) ->
+        # `ready` is the initialization state of the subscriptions observe handle. Counts are only published after the observes
+        # are initialized.
         if ready
           total = collection.find( baseQuery ).count()
           DataTable.log "#{ subscription }:count:total", total
           filtered = collection.find( filteredQuery ).count()
           DataTable.log "#{ subscription }:count:filtered", filtered
-          if first
+          # `added` is a flag that is set to true on the initial insert into the DaTableCount collection from this subscription.
+          if added
             self.added( DataTable.countCollection, subscription, { count: total } )
             self.added( DataTable.countCollection, "#{ subscription }_filtered", { count: filtered } )
           else
             self.changed( DataTable.countCollection, subscription, { count: total } )
             self.changed( DataTable.countCollection, "#{ subscription }_filtered", { count: filtered } )
+
+      # ###### observe
+      # DataTable observes just the filtered and paginated subset of the Collection. This is for performance reasons as
+      # observing large datasets entirely is unrealistic. The observe callbacks use `At` due to the sort and limit options
+      # passed the the observer.
       handle = collection.find( filteredQuery, options ).observe
+      # ###### addedAt()
+      # Updates the count and sends the new doc to the client.
         addedAt: ( doc, index, before ) ->
           updateCount initialized
           self.added collection._name, doc._id, doc
           DataTable.log "#{ subscription }:added", doc._id
+      # ###### changedAt()
+      # Updates the count and sends the changed properties to the client.
         changedAt: ( newDoc, oldDoc, index ) ->
           updateCount initialized
           self.changed collection._name, newDoc._id, newDoc
           DataTable.log "#{ subscription }:changed", newDoc._id
+      # ###### removedAt()
+      # Updates the count and removes the document from the client.
         removedAt: ( doc, index ) ->
           updateCount initialized
           self.removed collection._name, doc._id
           DataTable.log "#{ subscription }:removed", doc._id
+      # After the observer is initialized the `initialized` flag is set to true, the initial count is published,
+      # and the publication is marked as `ready()`
       initialized = true
       updateCount initialized, true
       self.ready()
+
+      # This is an attempt to monitor the last page in the dataset for changes, this is due to datatable on the client
+      # breaking when the last page no longer contains any data, or is no longer the last page.
       lastPage = collection.find( filteredQuery ).count() - options.limit
       if lastPage > 0
         countOptions = options
@@ -62,6 +91,29 @@
           changedAt: -> updateCount countInitialized
           removedAt: -> updateCount countInitialized
         countInitialized = true
+
+      # When the publication is terminated the observers are stopped to prevent memory leaks.
       self.onStop ->
         handle.stop()
-        countHandle.stop()+        countHandle.stop()
+
+  # ##### @debug
+  # Debug flag is initialized to false. Set `DataTable.debug = "true"` for all debug messages.
+  # Set Debug flag to a string to only log messages containing that string.
+  # ##### examples
+  #   + `"query"` : will log the base and filtered queries for every subscription.
+  #   + `"added"` : will log all documents added to subscriptions.
+  #   + `"changed"` : will log all documents changed for a subscription.
+  #   + `"removed"` : will log all documents removed from a collection.
+  @debug: false
+
+  # ##### @isDebug()
+  # A utility method for returning the current debug string or false if debug is not enabled.
+  @isDebug: -> return DataTable.debug or false
+
+  # ##### @log()
+  # Wraps console log and only logs messages if `DataTable.debug` is true or if the message contains the debug string.
+  @log: ( message, object ) ->
+    if DataTable.isDebug()
+      if message.indexOf( DataTable.isDebug() ) isnt -1 or DataTable.isDebug() is "true"
+        console.log "dataTable:#{ message } ->", object


docker compose v2 fixes

docker compose v2 fixes

--- server/cluster.coffee
+++ server/cluster.coffee
@@ -88,7 +88,9 @@
     # these labels are later communicated back to the dashboard
     # though Docker events and inspect information.
     # This way we can relate containers and events
-    for serviceName, service of dockerCompose
+    services = dockerCompose
+    services = dockerCompose.services if dockerCompose.services
+    for serviceName, service of services
       service.container_name = "#{project}-#{instance}-#{serviceName}"
       service.restart = 'unless-stopped'
       service.labels =
@@ -106,7 +108,7 @@
         'bigboat.container.enable_ssh':  if bigboatCompose[serviceName]?.enable_ssh then 'true' else undefined

       if bigboatCompose[serviceName]?.enable_ssh
-        dockerCompose["bb-ssh-#{serviceName}"] =
+        services["bb-ssh-#{serviceName}"] =
           image: 'jeroenpeeters/docker-ssh'
           container_name: "#{project}-#{instance}-#{serviceName}-ssh"
           environment:


more awesome help

more awesome help

--- editor.coffee
+++ editor.coffee
@@ -3,6 +3,8 @@
 _lead_finished = new Object

 default_options = {}
+
+default_target_command = 'img'

 graphite_function_docs = {}
 $.getJSON 'functions.fjson', (data) ->
@@ -15,6 +17,17 @@
     graphite_function_docs[tag.id[prefix_length..]] = tag.parentNode

 window.init_editor = ->
+  CodeMirror.commands.run = (cm) ->
+    setTimeout(-> run cm.getValue(), 1)
+
+  CodeMirror.commands.contextHelp = (cm) ->
+     cur = editor.getCursor()
+     token = cm.getTokenAt(cur)
+     if graphite_function_docs[token.string]
+       run "docs #{token.string}"
+     else if create_ns()[token.string]?
+       run "help #{token.string}"
+
   CodeMirror.keyMap.lead =
     Tab: (cm) ->
       if cm.somethingSelected()
@@ -22,8 +35,6 @@
       else
         spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
         cm.replaceSelection(spaces, "end", "+input")
-    'Shift-Enter': (cm) ->
-      setTimeout(-> run cm.getValue(), 1)
     fallthrough: ['default']

   $code = $ '#code'
@@ -36,6 +47,9 @@
     tabSize: 2
     autofocus: true
     viewportMargin: Infinity
+    extraKeys:
+      'Shift-Enter': 'run'
+      'F1': 'contextHelp'

   $output.css 'padding-bottom': ($code.height() + 60) + 'px'
   editor.on 'viewportChange', ->
@@ -90,7 +104,8 @@

     fn = (doc, wrapped) ->
       wrapped._lead_cli_fn = ->
-        cli.text 'Did you forget to call a function?'
+        cli.text "Did you forget to call a function? \"#{wrapped._lead_cli_name}\" must be called with arguments."
+        run "help #{wrapped._lead_cli_name}"
       wrapped._lead_doc = doc
       wrapped

@@ -105,9 +120,17 @@

     cli =
       help:
-        cmd 'Shows this help', ->
-          commands = ("  #{cmd}:\n    #{cli[cmd]._lead_doc}" for cmd in cli_commands).join('\n\n')
-          cli.pre "Check out these awesome built-in functions:\n\n#{commands}"
+        cmd 'Shows this help', (cmd) ->
+          if cmd?
+            cmd = cmd._lead_cli_name ? cmd
+            doc = cli[cmd]?._lead_doc
+            if doc
+              cli.pre "#{cmd}\n    #{doc}"
+            else
+              cli.pre "#{cmd} is not a command."
+          else
+            commands = ("  #{cmd}:\n    #{cli[cmd]._lead_doc}" for cmd in cli_commands).join('\n\n')
+            cli.pre "Check out these awesome built-in functions:\n\n#{commands}"

       object:
         fn 'Prints an object as JSON', output_object
@@ -173,7 +196,7 @@
                     examples.push line[8..]
               context.$result.append dl.cloneNode true
               for example in examples
-                cli.example "img #{JSON.stringify example}", run: false
+                cli.example "#{default_target_command} #{JSON.stringify example}", run: false
             else
               cli.text 'Documentation not found'
             context.success()
@@ -182,7 +205,7 @@
             names.sort()
             for name in names
               sig = $(graphite_function_docs[name].getElementsByTagName('dt')[0]).text().trim()
-              cli.example "docs #{JSON.stringify name}  # #{sig}"
+              cli.example "docs #{name}  # #{sig}"
             context.success()

       clear:
@@ -370,11 +393,13 @@

       q: do ->
         result = (targets...) ->
-          _lead: ['q', _lead_to_string: -> targets.join ',']
+          target_strings = targets.map String
+          _lead: ['q', {_lead_to_string: (-> target_strings.join ','), _lead_string_targets: target_strings}]
         result._lead_doc = 'Escapes a Graphite metric query'
         result

-    cli_commands = (k for k of cli)
+    cli_commands = for k, v of cli
+      v._lead_cli_name = k
     cli_commands.sort()

     cli
@@ -401,7 +426,7 @@
     $input.append $pre

     $entry.append $input
-    $entry.append $result
+    $output.append $entry
     context =
       $result: $result
       success: ->
@@ -450,13 +475,13 @@
             else
               ns.text "What do you want to do with #{lead_string}?"
               safe_string = JSON.stringify lead_string
-              for f in ['data', 'img', 'url']
+              for f in ['data', 'graph', 'img', 'url']
                 ns.example "#{f} #{safe_string}"
           else
             ns.object result
       catch e
         handle_exception e, compiled

-    $output.append $entry
+    $entry.append $result

   run 'intro'


Add reviewable status to submitted hits

Add reviewable status to submitted hits

--- saveCompletedTaskset.coffee
+++ saveCompletedTaskset.coffee
@@ -6,6 +6,7 @@

 saveCompletedTaskset = (tasksetData, callback) ->
   tasksetData.lock = false
+  tasksetData.status = 'reviewable'
   tasksetData.time.submit = new Date()
   tasksetData.time.workTime = tasksetData.time.submit - new Date(tasksetData.time.start)


Fix getting common attributes for location ranges spanning blocks and ending at offset 0

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -233,8 +233,9 @@
       blockAttributes = []

       @eachBlockAtLocationRange locationRange, (block, textRange) ->
-        textAttributes.push(block.text.getCommonAttributesAtRange(textRange))
-        blockAttributes.push(block.getAttributes())
+        unless textRange[0] is textRange[1]
+          textAttributes.push(block.text.getCommonAttributesAtRange(textRange))
+          blockAttributes.push(block.getAttributes())

       Trix.Hash.fromCommonAttributesOfObjects(textAttributes)
         .merge(Trix.Hash.fromCommonAttributesOfObjects(blockAttributes))