Add spec for issue #20.

Add spec for issue #20.

--- src/spec/stockimport.spec.coffee
+++ src/spec/stockimport.spec.coffee
@@ -138,7 +138,7 @@
         expect(s.quantityOnStock).toBe -3
         done()

-    it 'shoud not crash when quantity or index is missing', (done) ->
+    it 'shoud not crash when quantity is missing', (done) ->
       rawCSV =
         '''
         foo,id,amount
@@ -147,7 +147,6 @@
         '''
       Csv().from.string(rawCSV).to.array (data, count) =>
         stocks = @import._mapStockFromCSV _.rest(data), 1, 2
-        console.log "HAJO", stocks
         expect(_.size stocks).toBe 2
         s = stocks[0]
         expect(s.sku).toBe 'abc'
@@ -155,6 +154,17 @@
         s = stocks[1]
         expect(s.sku).toBe '123'
         expect(s.quantityOnStock).toBe 77
+        done()
+
+    xit 'shoud not crash when quantity is missing', (done) ->
+      rawCSV =
+        '''
+        foo,id,amount
+        bar
+        '''
+      Csv().from.string(rawCSV).to.array (data, count) =>
+        stocks = @import._mapStockFromCSV _.rest(data), 1, 2
+        expect(_.size stocks).toBe 0
         done()


Workspace: Fixed loading for collaborative workspace.

--- client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -140,8 +140,8 @@
       partial  : """<span class="text">Loading...<span>"""

     @loader.addSubView loaderView = new KDLoaderView size: width : 36
+    @loader.on "viewAppended", -> loaderView.show()
     @container.addSubView @loader
-    @loader.on "viewAppended", -> loaderView.show()

   joinSession: (sessionKey) ->
     {parent}           = @


Remove sinon/chai setup. Handled by karma plugin

--- test/all-tests.coffee
+++ test/all-tests.coffee
@@ -1,7 +1,3 @@
-chai = require 'chai'
-sinonChai = require("sinon-chai")
-chai.use(sinonChai)
-
 # For some reason, this navbar component test will have warnings about setting
 # state when component is unmounted if it's after some of the other specs.
 # The tests still run and progress just fine despite the warnings, but for now,
@@ -65,8 +61,3 @@
 require './helpers/period.spec'
 require './helpers/job.spec'
 require './helpers/time.spec'
-
-# The whole Boom Boom.
-# This should be done **last** because it starts up the whole app
-require './router.spec'
-###


Made showing/hiding of suggested topics not depend on bootstrap.

--- core/app/backbone/views/channels/filtered_suggested_topics_view.coffee
+++ core/app/backbone/views/channels/filtered_suggested_topics_view.coffee
@@ -53,4 +53,4 @@
     @updateTitle()

   updateTitle: ->
-    @$('.js-suggestions-title').toggleClass 'hide', @filtered_collection.length <= 0
+    @$el.toggle @filtered_collection.length > 0


cleaning up things a little, adding validation against wrong wild-cards-based requirements

--- src/toaster/grains/script.coffee
+++ src/toaster/grains/script.coffee
@@ -290,22 +290,28 @@
 			# looping through class dependencues
 			for dependency, index in klass.dependencies

-				# if dependency is a wild-card (namespace.*)
-				if dependency.substr( -1 ) == "*"
-
-					# then adds its index to the dead array
-					_dead_indexes.push index
-
-					# and find all classes under that namespace
-					reg = new RegExp dependency.replace /(\.|\/)/g, "\\$1"
-					props = ["classname", "classpath", "filename", "filefolder"]
-					found = ArrayUtil.find_all buffer, reg, props, true, true
-
-					# overwrites its indexes with only the classpath property
-					found[k] = found[k].item.classpath for v, k in found
-
-					# concat all dependencies together
-					_dependencies = _dependencies.concat found
+				# continue if dependency is not a wild-card (namespace.*)
+				continue if dependency.substr(-1) != "*"
+
+				# otherwise its index to the dead array
+				_dead_indexes.push index
+
+				# and find all classes under that namespace
+				reg = new RegExp dependency.replace /(\.|\/)/g, "\\$1"
+				props = ["classname", "classpath", "filename", "filefolder"]
+				found = ArrayUtil.find_all buffer, reg, props, true, true
+
+				# if nothing is found under the given namespace
+				if found.length <= 0
+					console.log "#{'WARNING'.bold.yellow} Nothing found".yellow,
+								"inside #{dependency.yellow}"
+					continue
+
+				# otherwise rewrites found array with classpath info only
+				found[k] = found[k].item.classpath for v, k in found
+
+				# concat all dependencies together
+				_dependencies = _dependencies.concat found

 			# desc sorting dead_indexes for proper removal
 			_dead_indexes = _dead_indexes.sort().reverse()
@@ -316,7 +322,6 @@
 			# concat the processed/found dependencies into classes dependencies
 			klass.dependencies = klass.dependencies.concat _dependencies

-
 		# return the buffer after processing everything
 		buffer


Remove semicolon

Remove semicolon

--- test/demo-mock.coffee
+++ test/demo-mock.coffee
@@ -24,7 +24,7 @@

   $.mockjax (settings) ->
     # url: '/content/<id>'
-    id = settings.url.match(/\/(content|module|folder|collection)\/(.*)$/);
+    id = settings.url.match(/\/(content|module|folder|collection)\/(.*)$/)
     id = id?[2] or null

     switch settings.type


fetch_recent, but with a shitload of noise

--- scripts/rc.coffee
+++ scripts/rc.coffee
@@ -55,7 +55,7 @@
       if options.match /recent/
         new RCScore(who).fetch_recent start, (err, self) =>
           return msg.reply "That fucking blew up: #{err}" if err
-          return msg.reply "It: #{util.inspect self.recent}"
+          return msg.reply "It: #{util.inspect self.recent.map (r) -> r.score}"
       else if options.match /graph/
         throw RCError("TODO(sshirokov): Spark graph time for #{who}")
       else
@@ -141,6 +141,7 @@
       @fetch_by_time stamp, cb

   fetch_by_time: (time, cb) =>
+    console.log "Fetching by time: #{time} for #{@who}"
     @storage.zscore @key("#{@who}:scores"), time, (err, score) =>
       return cb(err, this) if err
       [@score, @timestamp] = [score, time]
@@ -150,8 +151,47 @@
   # cb(error, [RCscore, ...]) with all RCScores found
   # after `start` for `@who`
   fetch_recent: (start, cb) =>
+    console.log "fetch_recent: #{start} for #{@who}"
     @storage.zrevrangebyscore @key("#{@who}:times"), '+inf', start, (err, res) =>
       return cb(err, this) if err
-      @recent = res
-      # TODO(sshirokov): Convert the times into RCScores and then invoke the callback
-      cb(false, this)
+      # If no error happeend, but no results came back
+      # we're still done
+      if not res.length
+        @recent = []
+        console.log "No results, no error"
+        return cb(false, this)
+      console.log "Results: %j", res
+      jobs = ({result: null, error: false, request: time} for time in res)
+      console.log "Jobs: %j", jobs
+      hasFailed = ->
+        console.log "Has failed: #{util.inspect jobs}"
+        res = jobs.filter((j) -> j.error).length
+        console.log "Failed?: #{res}"
+        res
+      hasFinished = ->
+        console.log "Has finished: #{util.inspect jobs}"
+        finished = jobs.filter((j) -> j.result).length
+        res = finished == jobs.length
+        console.log "Finish? #{res}: #{finished}/#{jobs.length}"
+        res
+      for job in jobs
+        do (job) => new RCScore(@who).fetch_by_time parseInt(job.request), (err, self) =>
+          if not err
+            console.log "Fetched: Score: #{self.score} @ #{self.timestamp}"
+          else
+            console.err "Error in fetch: #{err}"
+          # If we are the first error, call the callback, and mark the job failed
+          # so that future jobs can skip execution. We'll pass on whatever we already
+          # computed in the callback.
+          if err and not hasFailed()
+            cb(err, self)
+          job.error = err if err
+          # If we've already failed, stop
+          return if hasFailed()
+          # Store the result
+          job.result = self
+          # "Return" if we're done
+          if hasFinished()
+            @recent = (j.result for j in jobs).reverse()
+            console.log "Recent: #{@recent.map (r) -> [r.score, r.timestamp]}"
+            return cb(false, this)


Factor out repetetive code.

Factor out repetetive code.

--- spec/reproController.coffee
+++ spec/reproController.coffee
@@ -29,55 +29,31 @@
         fn()

     inject ($controller, Server, _$rootScope_, $q) ->
+
+      # Hook a call on our Server to return static information.
+      fakeReturn = (serverMethod, jsonReturned) ->
+        spyOn Server, serverMethod
+          .and.callFake () ->
+            deferred = $q.defer()
+            deferred.resolve
+              data: JSON.parse JSON.stringify jsonReturned
+            deferred.promise
+
       scope = _$rootScope_.$new()
+
       jitaMinerals = fixture.load('jitaMinerals.json')
-      # Hook calls on our Server to return static information.
-      spyOn Server, 'getReprocessPrices'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify jitaMinerals
-          deferred.promise
-
       autocomplete = fixture.load('autocomplete.json')
-      spyOn Server, 'getAutocomplete'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify autocomplete
-          deferred.promise
-
       pastebinTest = fixture.load('pastebin.json')
-      spyOn Server, 'parsePastebin'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify pastebinTest
-          deferred.promise
-
       marketOutput = fixture.load('marketOutput.json')
-      spyOn Server, 'searchStationMarket'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify marketOutput
-          deferred.promise
-
       reprocessOutput = fixture.load('reprocessOutput.json')
-      spyOn Server, 'reprocessItems'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify reprocessOutput
-          deferred.promise
-
       sessionInfo = fixture.load('session.json')
-      spyOn Server, 'getLoginStatus'
-        .and.callFake () ->
-          deferred = $q.defer()
-          deferred.resolve
-            data: JSON.parse JSON.stringify sessionInfo
-          deferred.promise
+
+      fakeReturn 'getReprocessPrices', jitaMinerals
+      fakeReturn 'getAutocomplete', autocomplete
+      fakeReturn 'parsePastebin', pastebinTest
+      fakeReturn 'searchStationMarket', marketOutput
+      fakeReturn 'reprocessItems', reprocessOutput
+      fakeReturn 'getLoginStatus', sessionInfo

       # getSkills and getEffectiveStandings will be called indirectly.
       spyOn Server, 'getSkills'


fixin spec tests

fixin spec tests

--- spec/status-bar-spec.coffee
+++ spec/status-bar-spec.coffee
@@ -30,7 +30,7 @@
     it "displays the editor's buffer path, cursor buffer position, and buffer modified indicator", ->
       expect(statusBar.currentPath.text()).toBe 'sample.js'
       expect(statusBar.bufferModified.text()).toBe ''
-      expect(statusBar.cursorPosition.text()).toBe '1,1'
+      expect(statusBar.cursorPosition.text()).toBe 'Line 1, Column 1'

     describe "when associated with an unsaved buffer", ->
       it "displays 'untitled' instead of the buffer's path, but still displays the buffer position", ->
@@ -41,7 +41,7 @@
         StatusBar.activate(rootView)
         statusBar = rootView.find('.status-bar').view()
         expect(statusBar.currentPath.text()).toBe 'untitled'
-        expect(statusBar.cursorPosition.text()).toBe '1,1'
+        expect(statusBar.cursorPosition.text()).toBe 'Line 1, Column 1'

   describe "when the associated editor's path changes", ->
     it "updates the path in the status bar", ->
@@ -105,7 +105,7 @@
   describe "when the associated editor's cursor position changes", ->
     it "updates the cursor position in the status bar", ->
       editor.setCursorScreenPosition([1, 2])
-      expect(statusBar.cursorPosition.text()).toBe '2,3'
+      expect(statusBar.cursorPosition.text()).toBe 'Line 2, Column 3'

   describe "git branch label", ->
     beforeEach ->


Environments newui: removed approval on connecting items

--- client/landing/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
@@ -52,15 +52,13 @@
         return new KDNotificationView
           title : "A domain name can only be bound to one VM."

-    @askForApprove items, 'create', (modal)=>
-      if machine? # Assign a domain to a machine
-        jDomain = domain.dia.data.domain # JDomain
-        vmName  = machine.dia.data.title # JVM.hostnameAlias
-        jDomain.bindVM hostnameAlias: vmName, (err)=>
-          modal.destroy()
-          return KD.showError err  if err
-          jDomain.hostnameAlias.push vmName
-          createConnection()
+    if machine? # Assign a domain to a machine
+      jDomain = domain.dia.data.domain # JDomain
+      vmName  = machine.dia.data.title # JVM.hostnameAlias
+      jDomain.bindVM hostnameAlias: vmName, (err)=>
+        return KD.showError err  if err
+        jDomain.hostnameAlias.push vmName
+        createConnection()

   updateConnections:->
     for _mkey, machine of @boxes.machines.dias


remove errant focus() call in AutoComplete

--- static/coffee/completions.coffee
+++ static/coffee/completions.coffee
@@ -208,10 +208,8 @@
     if variables
       @completions = variables.concat(@functions)

-
-
     $inputor = $(selector).atwho(@config)
-    $inputor.focus().atwho('run')
+    $inputor.atwho('run')

     # when an option is selected, insert the text and update the caret
     $inputor.on 'inserted.atwho', (atEvent, li, browserEvent) ->


Hacked plot clicking to work on FF

Hacked plot clicking to work on FF

DAMN YOU FF!!!

--- static/js/activity_scores.coffee
+++ static/js/activity_scores.coffee
@@ -71,8 +71,11 @@
         @graph.render()
         xaxis.render()

-        $(@graph.vis[0][0]).click (ev) =>
-            x = @graph.x.invert ev.offsetX
+        canvas = $ @graph.vis[0][0]
+        $(@graph.vis[0][0]).click (e) =>
+            # Come on world! This is sort of basic stuff!
+            px = e.offsetX ? (e.clientX - $(canvas).offset().left)
+            x = @graph.x.invert px
             if x > @highlight_range.from and x < @highlight_range.to
                 @el.trigger "plotdaterange", undefined
                 return


remove comment

remove comment

--- lib/changes/file-summary-view.coffee
+++ lib/changes/file-summary-view.coffee
@@ -10,12 +10,6 @@
 </div>
 <button class="btn btn-xs"></button>
 """
-
-# model=
-#   file:
-#     path: -> String
-#     statusBit: -> Enum
-#   status: String

 class FileSummaryView extends HTMLElement
   initialize: (@model) ->


Display Current Volume

Display Current Volume

--- modules/player/commands.coffee
+++ modules/player/commands.coffee
@@ -220,7 +220,13 @@
         }

     # Change Volume
-    @registerCommand 'volume', { alisases: ['vol'], djOnly: true }, (m, args, d , player)=>
+    @registerCommand 'volume', { alisases: ['vol'] }, (m, args, d , player)=>
+      unless args
+        return m.reply """
+        Current Volume: **#{player.volume*100}**.
+        #{@hud.generateProgressOuter(player.queue.nowPlaying)}
+        """
+      return m.reply 'Only DJs can change the volume.' unless @permissions.isDJ(msg.member)
       return m.reply 'Invalid volume' if parseInt(args) > 100 or
              parseInt(args) < 0 or !isFinite(args)
       try


decorate group if requester is in group and request gets approved by admin

--- client/app/Applications/Home.kdapplication/Views/homeloginbar.coffee
+++ client/app/Applications/Home.kdapplication/Views/homeloginbar.coffee
@@ -75,6 +75,7 @@
             unless err
               @access.hide()
               @requested.show()
+              @listenToApproval()

     @join         = new CustomLinkView
       tagName     : "a"
@@ -232,6 +233,8 @@

               return  unless KD.isLoggedIn()

+              @group.on 'MemberAdded', @emit.bind this, 'MemberAdded'
+
               KD.whoami().getInvitationRequestByGroup @group, $in:['sent', 'pending'], (err, [request])=>
                 return console.warn err if err
                 return unless request
@@ -240,9 +243,18 @@
                 if request.status is 'sent'
                   @invited.show()
                 else
+                  @listenToApproval()
                   @requested.show()
     else
       @hide()
+
+  listenToApproval:->
+    @once 'MemberAdded', @listenToApprovalCallback ?= =>
+      @group.fetchMyRoles (err, roles)=>
+        return warn err  if err
+        if 'member' not in roles
+          return @once 'MemberAdded', @listenToApprovalCallback
+        KD.getSingleton('mainController').accountChanged KD.whoami()

   viewAppended:->
     super


showing redeem link fix for the groups which doesnt have invite code

--- client/app/Applications/Home.kdapplication/Views/homeloginbar.coffee
+++ client/app/Applications/Home.kdapplication/Views/homeloginbar.coffee
@@ -239,6 +239,7 @@
               @policy = policy
               if err then console.warn err
               else if policy.approvalEnabled
+                @redeem.hide()
                 @request.hide()
                 @join.hide()
                 @access.show()


send user update broadcast to all subscriber including sender

--- api/models/User.coffee
+++ api/models/User.coffee
@@ -126,6 +126,10 @@
 						cb err
 			else
 				cb "#{sails.config.authGrp} not defined"
+
+	broadcast: (roomName, eventName, data, socketToOmit) ->
+		# ignore socketToOmit to broadcast the event to sender also
+		sails.sockets.broadcast roomName, eventName, data

 	# return administrator
 	admin: (opts, cb) ->


Asterisssssssks

Asterisssssssks

--- js/lesson_plans.coffee
+++ js/lesson_plans.coffee
@@ -74,7 +74,7 @@
       startingText: "* Calculus, A professor, Has no hair, Often wears green\n* Castafiore, An opera singer, Has white hair, Is possibly mentally unwell"
       renderedAnswer: "
\n
Calculus
\n
A professor
\n
Has no hair
\n
Often wears green
\n
\n
\n
Castafiore
\n
An opera singer
\n
Has white hair
\n
Is possibly mentally unwell
\n
\n
\n
"
     4:
-      startingText: "1. Cut the cheese\n  * Make sure that the cheese is cut into little triangles.\n\n2. Slice the tomatoes\n  * Be careful when holding the knife.\n  * For more help on tomato slicing, see Thomas Jefferson's seminal essay **Tom Ate Those**."
+      startingText: "1. Cut the cheese\n  * Make sure that the cheese is cut into little triangles.\n\n2. Slice the tomatoes\n  * Be careful when holding the knife.\n  * For more help on tomato slicing, see Thomas Jefferson's seminal essay _Tom Ate Those_."
       renderedAnswer: "
\n

Cut the cheese
\n
Make sure that the cheese is cut into little triangles.
\n
\n

Slice the tomatoes
\n
Be careful when holding the knife.
\n
For more help on tomato slicing, see Thomas Jefferson's seminal essay Tom Ate Those.
\n
\n
"

   7:


Add real setup

Add real setup

--- app/src/controllers/onboarding/management/onboarding_management_provisioning_view_controller.coffee
+++ app/src/controllers/onboarding/management/onboarding_management_provisioning_view_controller.coffee
@@ -2,12 +2,6 @@

   navigation:
     continueUrl: '/onboarding/management/done'
-
-  initialize: ->
-    super
-    setTimeout =>
-      do @navigateContinue
-    , 3000

   navigationContinueParams: ->
     wallet_mode: @params.wallet_mode
@@ -16,4 +10,8 @@

   onAfterRender: ->
     super
-    @view.spinner = ledger.spinners.createLargeSpinner(@select('div.greyed-container')[0])+    @view.spinner = ledger.spinners.createLargeSpinner(@select('div.greyed-container')[0])
+    ledger.app.wallet.setup @params.pin, @params.seed, (success) =>
+      setTimeout =>
+        do @navigateContinue
+      , 3000


add cname record to kontrol proxy when a new domain is registered.

--- client/landing/app/Applications/Environments.kdapplication/views/DomainRegisterModalFormView.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/DomainRegisterModalFormView.coffee
@@ -60,7 +60,7 @@

     KD.remote.api.JVM.fetchVms (err, vms)=>
       selects = []
-      for i,vmName of vms
+      for i, vmName of vms
         selects.push {
           title :"#{vmName}.kd.io"
           value:"#{vmName}.kd.io"
@@ -186,13 +186,15 @@
         @notifyUser "This domain is already registered. Please try another domain."
         @searchForm.buttons.Register.hideLoader()

-      KD.remote.api.JDomain.registerDomain {domainName:domainInputVal, years:regYears}, (err, result)=>
+      KD.remote.api.JDomain.registerDomain {domainName:domainInputVal, years:regYears}, (err, domain)=>
         if not err
           new KDNotificationView
             type  : "top"
             title : "Your domain has been successfully registered."
           @searchForm.buttons.Register.hideLoader()
           @modalTabs.showPaneByIndex 2
+          domain.setDomainCNameToProxyDomain()
+
         warn err

     subView.destroy() for subView, i in @domainNameFieldView.getSubViews() when i > 1


My Account link open in new tab & not check unsaved-state

--- src/components/navbar/account-link.cjsx
+++ src/components/navbar/account-link.cjsx
@@ -11,8 +11,10 @@
     router: React.PropTypes.func

   redirectToAccount: ->
-    TransitionAssistant.checkTransitionStateTo("Account Profile Page").then ->
-      window.open(CurrentUserStore.getProfileUrl())
+    # workaround for 'No way to come back to dashboard after click on Account'
+    window.open(CurrentUserStore.getProfileUrl())
+    #TransitionAssistant.checkTransitionStateTo("Account Profile Page").then ->
+      #window.location.href = CurrentUserStore.getProfileUrl()

   render: ->
     return null unless CurrentUserStore.getProfileUrl()


Switch to transform-based motion in js

Switch to transform-based motion in js

--- pace.coffee
+++ pace.coffee
@@ -244,7 +244,9 @@

     el = @getElement()

-    el.children[0].style.width = "#{ @progress }%"
+    transform = "translate3d(#{ @progress }%, 0, 0)"
+    for key in ['webkitTransform', 'msTransform', 'transform']
+      el.children[0].style[key] = transform

     if not @lastRenderedProgress or @lastRenderedProgress|0 != @progress|0
       # The whole-part of the number has changed


lod_and_effect: Delete trailing whitespace

--- lod_and_effect/lod_and_effect.coffee
+++ lod_and_effect/lod_and_effect.coffee
@@ -181,7 +181,7 @@
          femaleloc2.push(xScale[2](i))
          femaleloc3.push(xScale[3](i))
      aves = [[average(femaleloc2), average(maleloc2)], [average(femaleloc3), average(maleloc3)]]
-
+
      # lower middle and lower right X axes
      for j in [2..3]
        XaxisGrp[j].selectAll("line").remove()
@@ -241,7 +241,7 @@
          .attr("stroke", "black")
          .attr("stroke-width", "2")

-
+
   # background rectangles for each chromosome, alternate color
   chrRect = topsvg.append("g").selectAll("empty")
      .data(data.chr)
@@ -556,7 +556,7 @@
                           .attr("y2", bottom[1] - tickHeight)
                           .attr("stroke", "black")
                           .attr("stroke-width", "1")
-
+

   # chr labels
   topsvg.append("g").selectAll("empty")


Throws an error if Readmill is missing required options

Also added documentation of Readmill class and constructor.

--- src/readmill.coffee
+++ src/readmill.coffee
@@ -3,15 +3,43 @@
 # Grab the Delegator class here as it's useful for other Classes.
 Annotator.Class = Annotator.__super__.constructor

+# Base class for the Readmill plugin. This will be called via the jQuery
+# annotator interface.
+#
+# The "book", "clientId" and "callbackUri" arguments are required. The
+# book should be an object literal for a book on Readmill ideally with an
+# "id" but if only a "title", "author" are provided the plugin will
+# create the book for you.
+#
+# Examples
+#
+#   jQuery("#content").annotator()
+#         .annotator("addPlugin", "Readmill", {
+#           book: {id: 52},
+#           clientId: "123456",
+#           callbackUri: "http://example.com/callback.html"
+#         });
+#
+# Returns a new instance of Readmill.
 Annotator.Readmill = class Readmill extends Annotator.Plugin
-  @API_ENDPOINT: "http://localhost:8000"
-
+  # DOM and custom event to callback map.
   events:
     "annotationCreated": "_onAnnotationCreated"
     "annotationUpdated": "_onAnnotationUpdated"
     "annotationDeleted": "_onAnnotationDeleted"

-  constructor: (options) ->
+  # Initialises the plugin instance and sets up properties.
+  #
+  # element - The root Annotator element.
+  # options - An object literal of options.
+  #           book        - An object of book metadata.
+  #           clientId    - The client id string for the service.
+  #           callbackUri - A full url pointing to the callback.html file.
+  #           accessToken - A pre activated accessToken (optional).
+  #
+  # Returns nothing.
+  # Raises an Error if any of the required arguments are missing.
+  constructor: (element, options) ->
     super

     # Rather than use CoffeeScript's scope binding for all the event handlers
@@ -19,6 +47,21 @@
     # utilty function to manually bind all functions beginning with "_on" to
     # the current scope.
     Readmill.utils.proxyHandlers this
+
+    # Ensure required options are provided.
+    errors = (key for own key in ["book", "clientId", "callbackUri"] when not options[key])
+    if errors.length
+      throw new Error """
+        options "#{errors.join('", ')}" are required by the Readmill plugin. eg:
+
+        jQuery("#content").annotator()
+        .annotator("addPlugin", "Readmill", {
+          book: {id: "52"}, /* Or use a title & author. */
+          book: {title: "Brighton Rock", author: "Graham Greene"}
+          clientId: "12345",
+          callbackUri: "http://example.com/callback.html"
+        });
+      """

     @user   = null
     @book   = @options.book
@@ -34,19 +77,35 @@
     @connected(token, silent: true) if token
     @unsaved = []

-  pluginInit: () ->
+  # Internal: Called by the Annotator after the intance has been created and
+  # @annotator property has been attached. Sets up initial plugin state.
+  #
+  # Returns nothing.
+  pluginInit: ->
     jQuery("body").append @view.render()
-    @lookupBook().done
-
+    @lookupBook()
+
+  # Public: Fetches the book resource from the Readmill API and updates the
+  # view when complete. This method is usually called as part of the plugin
+  # setup.
+  #
+  # The method also attaches a "promise" property to @book which can be used
+  # to determine if the book is loaded.
+  #
+  # Examples
+  #
+  #   readmill.lookupBook()
+  #
+  # Returns a jQuery.Deferred() promise.
   lookupBook: ->
-    return @book.deferred if @book.deferred
-
-    @book.deferred = if @book.id
+    return @book.promise if @book.promise
+
+    @book.promise = if @book.id
       @client.getBook @book.id
     else
       @client.matchBook @book

-    @book.deferred.then(@_onBookSuccess, @_onBookError).done =>
+    @book.promise.then(@_onBookSuccess, @_onBookError).done =>
       @view.updateBook @book

   lookupReading: ->
@@ -171,3 +230,5 @@
     if annotation.highlightUrl
       @client.deleteHighlight(annotation.highlightUrl).error =>
         @error "Unable to update annotation in Readmill"
+
+window.Annotator.Plugin.Readmill = Annotator.Readmill


DataTable: Support per-function generated data fields

This features was already present in SimpleForm, but not implemented in DataTable.

--- src/elements/DataTable/DataTable.coffee
+++ src/elements/DataTable/DataTable.coffee
@@ -104,7 +104,11 @@
 	init: ->
 		@__fieldList = []
 		for field in @getFieldOpts()
-			@__fieldList.push(CUI.DataField.new(field))
+			if CUI.util.isFunction(field)
+				_field = CUI.DataField.new(field(@))
+			else
+				_field = CUI.DataField.new(field)
+			@__fieldList.push(_field)
 		@

 	disable: ->


poke build

poke build

--- karma.conf.coffee
+++ karma.conf.coffee
@@ -1,4 +1,5 @@
 # Karma configuration
+
 module.exports = (config) ->
   config.set


[master] kind of working

[master] kind of working

--- merger/volume-and-read-to-csv.coffee
+++ merger/volume-and-read-to-csv.coffee
@@ -16,11 +16,12 @@
     cur.add 1, 'd'
   ret

-dateRange = weekRange '2012-05-01', '2015-04-01'
+dateRange = weekDayRange '2012-05-01', '2015-04-01'

 fs.readFile 'data/2015-04-07.json', encoding: 'ascii', (err, data) ->
   throw err if err
-  parser.bulletin data, (err, bulletinData) ->
+  data = JSON.parse data
+  parser.bulletin.f data, (err, bulletinData) ->
     throw err if err
     async.each index, (index, callback) ->
       fs.readFile "data/SH#{index}.txt", encoding: 'utf-8', (err, data) ->
@@ -28,7 +29,29 @@
         parser.stockFeed.f data, (err, stockFeedData) ->
           throw err if err
           symbolBulletin = bulletinData[index]
+          console.log index
           pairedData = []
           _.forEach dateRange, (date) ->
-            readCount = symbolBulletin[date].readCount
-            volume = stockFeedData
+            readCount =
+              if date of symbolBulletin
+                symbolBulletin[date].readCount
+              else if 0 < pairedData.length
+                pairedData[pairedData.length - 1].readCount
+              else 0
+            volume =
+              if date of stockFeedData
+                stockFeedData[date].volume
+              else if 0 < pairedData.length
+                pairedData[pairedData.length - 1].volume
+              else
+                0
+            pairedData.push
+              date: date
+              readCount: readCount
+              volume: volume
+          csv = _.map pairedData, (line) ->
+            "#{line.date},#{line.readCount},#{line.volume}"
+          .join '\n'
+          fs.writeFile "data/#{index}.csv", csv, encoding: 'ascii', (err) ->
+            throw err if err
+            callback()


minor changes

minor changes

--- lib/browse.coffee
+++ lib/browse.coffee
@@ -1,7 +1,7 @@
 {CompositeDisposable} = require 'atom'

 # Dependencies
-{execSync} = require('child_process')
+{exec} = require('child_process')
 fs = require 'fs'
 shell = require 'shell'

@@ -68,7 +68,7 @@
       return

     for item in items
-      continue if item.constructor.name isnt 'TextEditor'
+      continue unless item.constructor.name is 'TextEditor'

       if item?.buffer.file
         file = item?.buffer.file
@@ -124,7 +124,7 @@
     # Default file manager
     switch process.platform
       when "darwin"
-        exec "open -n -R #{path}"
+        exec "open -R #{path}"
         fileManager = "Finder"
       when "win32"
         exec "explorer /select,#{path}"


Minimize core

Minimize core

--- source/quo.coffee
+++ source/quo.coffee
@@ -1,31 +1,90 @@
 Quo = do ->
-    EMPTY_ARRAY = []

-    $$ = (selector, children) ->
-        unless selector
-            Q()
-        else if $$.toType(selector) is "function"
-            $$(document).ready selector
-        else
-            dom = $$.getDOMObject(selector, children)
-            Q(dom, selector)
+  EMPTY_ARRAY = []
+  OBJECT_PROTOTYPE  = Object::
+  IS_HTML_FRAGMENT  = /^\s*<(\w+|!)[^>]*>/

-    Q = (dom, selector) ->
-        dom = dom or EMPTY_ARRAY
-        dom.__proto__ = Q::
-        dom.selector = selector or ''
-        dom
+  CLASS_SELECTOR    = /^\.([\w-]+)$/
+  ID_SELECTOR       = /^#[\w\d-]+$/
+  TAG_SELECTOR      = /^[\w-]+$/

-    $$.extend = (target) ->
-        Array::slice.call(arguments, 1).forEach (source) ->
-          for key of source
-            target[key] = source[key]
-        target
-
-    Q:: = $$.fn = {}
-
-    $$
+  $$ = (selector, children) ->
+    unless selector
+      Q()
+    else if $$.toType(selector) is "function"
+      $$(document).ready selector
+    else
+      dom = $$.getDOMObject(selector, children)
+      Q(dom, selector)

-window.Quo = Quo
-"$$" of window or (window.$$ = Quo)
+  Q = (dom, selector) ->
+    dom = dom or EMPTY_ARRAY
+    dom.__proto__ = Q::
+    dom.selector = selector or ''
+    dom
+
+
+  $$.extend = (target) ->
+    Array::slice.call(arguments, 1).forEach (source) ->
+    target[key] = source[key] for key of source
+    target
+
+
+  $$.toType = (obj) ->
+    OBJECT_PROTOTYPE.toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase()
+
+
+  $$.getDOMObject = (selector, children) ->
+    domain = null
+    elementTypes = [ 1, 9, 11 ]
+    type = $$.toType(selector)
+
+    if type is "array"
+      domain = _compact(selector)
+
+    else if type is "string" and IS_HTML_FRAGMENT.test(selector)
+      domain = $$.fragment(selector.trim(), RegExp.$1)
+      selector = null
+
+    else if type is "string"
+      domain = $$.query(document, selector)
+      if children
+        if domain.length is 1
+          domain = $$.query(domain[0], children)
+        else
+          #@todo: BUG if selector count > 1
+          domain = $$.map(-> $$.query domain, children )
+
+    else if elementTypes.indexOf(selector.nodeType) >= 0 or selector is window
+      domain = [selector]
+      selector = null
+
+    domain
+
+
+  $$.query = (domain, selector) ->
+    selector = selector.trim()
+
+    if CLASS_SELECTOR.test(selector)
+      elements = domain.getElementsByClassName selector.replace(".", "")
+    else if TAG_SELECTOR.test(selector)
+      elements = domain.getElementsByTagName(selector)
+    else if ID_SELECTOR.test(selector) and domain is document
+      elements = domain.getElementById selector.replace("#", "")
+      unless elements then elements = []
+    else
+      elements = domain.querySelectorAll selector
+
+    if elements.nodeType then [elements] else Array::slice.call(elements)
+
+
+  # Private Methods
+  _compact = (array) ->
+    array.filter (item) ->
+      item isnt undefined and item isnt null
+
+  Q:: = $$.fn = {}
+  $$
+
+@Quo = @$$ = Quo


:art: Deactivate MessageRegistry on deactivate

--- lib/linter-plus.coffee
+++ lib/linter-plus.coffee
@@ -108,6 +108,7 @@
     @eachEditorLinter (linter) ->
       linter.deactivate()
     @views.destroy()
+    @linters.deactivate()
     @commands.destroy()
     @messages.destroy()


Add script that scrapes current game and returns score

--- src/worldcup.coffee
+++ src/worldcup.coffee
@@ -89,3 +89,12 @@
           msg.send formatted_scores
         else
           msg.send "There were no matches yesterday :("
+
+  robot.respond /(worldcup|wc)( score)/i, (msg) ->
+    msg.http("http://worldcup2014bot.herokuapp.com/scores/now")
+      .get() (err, res, body) ->
+        score = JSON.parse(body).score
+        if score
+          msg.send score.score_summary
+        else
+          msg.send "There is no game right now :("


kernel: Sign Jupyter messages

kernel: Sign Jupyter messages

--- lib/kernel.coffee
+++ lib/kernel.coffee
@@ -1,3 +1,4 @@
+crypto = require 'crypto'
 fs = require 'fs'
 path = require 'path'
 zmq = require 'zmq'
@@ -86,38 +87,62 @@
         console.log "sending SIGINT"
         @kernelProcess.kill('SIGINT')

+    # send a signed Jupyter message (adapted from github.com/n-riesco/jmp)
+    _send: (message, socket) ->
+        encodedMessage =
+            idents:        message.idents or []
+            signature:     ''
+            header:        JSON.stringify message.header
+            parent_header: JSON.stringify (message.parent_header or {})
+            metadata:      JSON.stringify (message.metadata or {})
+            content:       JSON.stringify (message.content or {})
+
+        if (@config.key)
+            hmac = crypto.createHmac @config.signature_scheme.slice(5),
+                @config.key
+            hmac.update encodedMessage.header
+            hmac.update encodedMessage.parent_header
+            hmac.update encodedMessage.metadata
+            hmac.update encodedMessage.content
+            encodedMessage.signature = hmac.digest "hex"
+
+        console.log encodedMessage
+
+        socket.send encodedMessage.idents.concat [
+            '<IDS|MSG>'
+            encodedMessage.signature
+            encodedMessage.header
+            encodedMessage.parent_header
+            encodedMessage.metadata
+            encodedMessage.content
+        ]
+
     # onResults is a callback that may be called multiple times
     # as results come in from the kernel
     _execute: (code, requestId, onResults) ->
         console.log "sending execute"
-        header = JSON.stringify({
+
+        header =
                 msg_id: requestId,
                 username: "",
                 session: "00000000-0000-0000-0000-000000000000",
                 msg_type: "execute_request",
                 version: "5.0"
-            })
-
-        contents = JSON.stringify({
+
+        content =
                 code: code
                 silent: false
                 store_history: true
                 user_expressions: {}
                 allow_stdin: false
-            })
-
-        message =  [
-                '<IDS|MSG>',
-                '',
-                header,
-                '{}',
-                '{}',
-                contents
-            ]
-        console.log message
+
+        message =
+                header: header
+                content: content

         @executionCallbacks[requestId] = onResults
-        @shellSocket.send message
+
+        @_send message, @shellSocket

     execute: (code, onResults) ->
         requestId = "execute_" + uuid.v4()


Temp fix for a bug in cnx-authoring

Temp fix for a bug in cnx-authoring

--- src/scripts/models/contents/page.coffee
+++ src/scripts/models/contents/page.coffee
@@ -3,11 +3,11 @@
   Node = require('cs!./node')

   return class Page extends Node
-    defaults:
-      authors: []
+    parse: (response, options) ->
+      response = super(arguments...)

-    parse: (response, options) ->
-      resonse = super(arguments...)
+      # FIX: cnx-authoring should not return a null value for content
+      response.content = response.content or ''

       # jQuery can not build a jQuery object with <head> or <body> tags,
       # and will instead move all elements in them up one level.


Add ranges into health check

Add ranges into health check

--- services/docstore/app/coffee/HealthChecker.coffee
+++ services/docstore/app/coffee/HealthChecker.coffee
@@ -19,7 +19,7 @@
 		jobs = [
 			(cb)->
 				opts = getOpts()
-				opts.json = {lines: lines, version: 42}
+				opts.json = {lines: lines, version: 42, ranges: {}}
 				request.post(opts, cb)
 			(cb)->
 				opts = getOpts()


#200 slim search further

#200 slim search further

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -754,6 +754,8 @@
       'sensitive'

   finish: ->
+    if @isIncrementalSearch?() and settings.get('showHoverSearchCounter')
+      @vimState.hoverSearchCounter.reset()
     @matches?.destroy()
     @matches = null

@@ -853,30 +855,22 @@
         when 'visit-next' then @matches.visit('next')
         when 'visit-prev' then @matches.visit('prev')

-  finish: ->
-    if @isIncrementalSearch() and settings.get('showHoverSearchCounter')
-      @vimState.hoverSearchCounter.reset()
-    super
-
   visitCursors: ->
+    visitCursor = (cursor) =>
+      @matches ?= @getMatchList(cursor, input)
+      if @matches.isEmpty()
+        @flashScreen()
+      else
+        @matches.visit()
+
     @matches?.destroy()
     @matches = null
     if settings.get('showHoverSearchCounter')
       @vimState.hoverSearchCounter.reset()
-    if @input
-      @visitCursor(cursor) for cursor in @editor.getCursors()
-
-  # Scroll to cursor position without changing position.
-  # used by incrementalSearch
-  visitCursor: (cursor) ->
-    # console.log "visiting!"
+
     input = @getInput()
-    return if input is ''
-    @matches ?= @getMatchList(cursor, input)
-    if @matches.isEmpty()
-      @flashScreen()
-    else
-      @matches.visit()
+    if input isnt ''
+      visitCursor(cursor) for cursor in @editor.getCursors()

   getPattern: (term) ->
     modifiers = if @isCaseSensitive(term) then 'g' else 'gi'


fix typo in comments

fix typo in comments

--- lib/parser.coffee
+++ lib/parser.coffee
@@ -279,7 +279,7 @@
     * @category Function
     * @package TagParser
     * @internal
-    * @param {object} commentData ({lineNumber, endLineNumber, file})
+    * @param {object} commentData (lineNumber, endLineNumber, file)
     * @return {object} The new comment object
   ###
   handleComment: (commentData) ->


Remove useless comment

Remove useless comment

--- lib/redeye.coffee
+++ lib/redeye.coffee
@@ -124,7 +124,6 @@
     @cache = {}
     @saved_keys = {}
     @sequence = []
-    @seen = {}
     @last_stage = 0
     num_workers++

@@ -159,9 +158,6 @@

   # Make sure the given key is being requested in a totally legal and consistent way.
   check_stage: (key) ->
-    # if !@seen[key]? && (@stage != @last_stage)
-    #   throw "#{@key} is requesting #{key} for the first time, but stage #{@stage} is less than last stage #{@last_stage}"
-    # @seen[key] = true
     if @key_index == @sequence.length
       @sequence.push key
     else


:art: further refactoring

:art: further refactoring

--- lib/text-object.coffee
+++ lib/text-object.coffee
@@ -1,6 +1,6 @@
 # Refactoring status: 95%
 {Range} = require 'atom'
-_    = require 'underscore-plus'
+_ = require 'underscore-plus'

 Base = require './base'
 swrap = require './selection-wrapper'
@@ -31,8 +31,7 @@
   eachSelection: (fn) ->
     fn(s) for s in @editor.getSelections()
     @emitDidSelect()
-    return if @isMode('operator-pending')
-    return if @isMode('visual', 'linewise')
+    return if @isMode('operator-pending') or @isMode('visual', 'linewise')
     if @isLinewise()
       @activateMode('visual', 'linewise')

@@ -139,19 +138,29 @@
         stop()
     found

+  findOpenPair: (pair, options) ->
+    options.which = 'open'
+    options.allowNextLine ?= @allowNextLine
+    @findPair(pair, options)
+
+  findClosePair: (pair, options) ->
+    options.which = 'close'
+    options.allowNextLine ?= @allowNextLine
+    @findPair(pair, options)
+
   getPairRange: (from, pair, what) ->
     range = null
     switch what
       when 'enclosed'
-        open  = @findPair pair, {from,       @allowNextLine, nest: 1, which: 'open'}
-        close = @findPair pair, {from: open, @allowNextLine, nest: 1, which: 'close'} if open?
+        open = @findOpenPair pair, {from: from, nest: 1}
+        close = @findClosePair pair, {from: open, nest: 1} if open?
       when 'next'
-        close = @findPair pair, {from,        @allowNextLine, nest: 0, which: 'close'}
-        open  = @findPair pair, {from: close, @allowNextLine, nest: 1, which: 'open'} if close?
+        close = @findClosePair pair, {from: from, nest: 0}
+        open = @findOpenPair pair, {from: close, nest: 1} if close?
       when 'previous' # FIXME but currently unused
-        open  = @findPair pair, {from,       @allowNextLine, nest: 0, which: 'open'}
-        close = @findPair pair, {from: open, @allowNextLine, nest: 1, which: 'close'} if open?
-    if open and close
+        open  = @findOpenPair pair, {from: from,  nest: 0}
+        close = @findClosePair pair, {from: open, nest: 1} if open?
+    if open? and close?
       range = new Range(open, close)
       range = range.translate([0, -1], [0, 1]) unless @isInner()
     range
@@ -444,7 +453,7 @@

   indentScopedLanguages: ['python', 'coffee']
   # FIXME: why go dont' fold closing '}' for function? this is dirty workaround.
-  omitingClosingCharLanguages: ['go']
+  omittingClosingCharLanguages: ['go']

   initialize: ->
     @language = @editor.getGrammar().scopeName.replace(/^source\./, '')
@@ -479,9 +488,8 @@
   adjustRowRange: (startRow, endRow) ->
     if @isInner()
       startRow += 1
-      unless @language in @indentScopedLanguages
-        endRow -= 1
-    endRow += 1 if (@language in @omitingClosingCharLanguages)
+      endRow -= 1 unless @language in @indentScopedLanguages
+    endRow += 1 if (@language in @omittingClosingCharLanguages)
     [startRow, endRow]

 class AFunction extends Function


Add FK requirements validations

Add FK requirements validations

--- app/models/note.coffee
+++ app/models/note.coffee
@@ -13,5 +13,7 @@
       maxLength: 10000
     user:
       model: 'user'
+      required: true
     milestone:
       model: 'milestone'
+      required: true


#1160 Refactor pullqueue code

#1160 Refactor pullqueue code

--- app/assets/javascripts/oxalis/model/binary/pullqueue.coffee
+++ app/assets/javascripts/oxalis/model/binary/pullqueue.coffee
@@ -85,26 +85,25 @@
             "Content-Type" : "multipart/mixed; boundary=#{requestData.boundary}"
           timeout : @MESSAGE_TIMEOUT
           compress : true
-        ).then(
-          (responseBuffer) =>
-            responseBuffer = new Uint8Array(responseBuffer)
-            @connctionInfo.log(@layer.name, roundTripBeginTime, batch.length, responseBuffer.length)
+        )
+      ).then((responseBuffer) =>
+        responseBuffer = new Uint8Array(responseBuffer)
+        @connctionInfo.log(@layer.name, roundTripBeginTime, batch.length, responseBuffer.length)

-            offset = 0
+        offset = 0

-            for bucket, i in batch
-              if bucket.fourBit
-                bucketData = @decode4bit(responseBuffer.subarray(offset, offset += (@cube.BUCKET_LENGTH >> 1)))
-              else
-                bucketData = responseBuffer.subarray(offset, offset += @cube.BUCKET_LENGTH)
+        for bucket, i in batch
+          if bucket.fourBit
+            bucketData = @decode4bit(responseBuffer.subarray(offset, offset += (@cube.BUCKET_LENGTH >> 1)))
+          else
+            bucketData = responseBuffer.subarray(offset, offset += @cube.BUCKET_LENGTH)

-              @boundingBox.removeOutsideArea(bucket, bucketData)
-              @cube.getBucketByZoomedAddress(bucket).receiveData(bucketData)
-        )
-        =>
-          for bucket in batch
-            if @cube.getBucketByZoomedAddress(item.bucket).dirty
-              @add({bucket : bucket, priority : @PRIORITY_HIGHEST})
+          @boundingBox.removeOutsideArea(bucket, bucketData)
+          @cube.getBucketByZoomedAddress(bucket).receiveData(bucketData)
+      ).catch(=>
+        for bucket in batch
+          if @cube.getBucketByZoomedAddress(item.bucket).dirty
+            @add({bucket : bucket, priority : @PRIORITY_HIGHEST})
       )
       =>
         @batchCount--


line offsets working

line offsets working

--- app/vis.coffee
+++ app/vis.coffee
@@ -3,7 +3,6 @@
 Shader = require "./shaders/sdf"
 xtend = require 'xtend'
 createOrbitViewer = require('three-orbit-viewer')(THREE)
-

 class Main
     SCALE_TEXT = 0.005
@@ -28,7 +27,7 @@
         document.body.appendChild( @renderer.domElement )

         @camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 )
-        @camera.position.z = -4
+        @camera.position.z = -10
         @camera.position.x = -1
         @camera.position.y = 0
         @camera.lookAt new THREE.Vector3(-1,0,0)
@@ -85,9 +84,12 @@
         chain.map (obj, i, array) ->
             obj.connector_index = obj.line.indexOf obj.connector
             if i > 0
-                prev = array[i-1].connector
-                prev_idx = obj.line.indexOf prev
-                obj.prev_connector = prev
+                prev = array[i-1]
+                prev_con = prev.connector
+                my_prev_idx = obj.line.indexOf prev_con
+                prev_idx = prev.connector_index
+                obj.prev_connector = prev_con
+                obj.my_prev_connector_index = my_prev_idx
                 obj.prev_connector_index = prev_idx
             obj

@@ -95,7 +97,6 @@
         mesh = @getTextMesh(geometry)
         textAnchor = new THREE.Object3D()
         textAnchor.add(mesh)
-        # textAnchor.scale.multiplyScalar(SCALE_TEXT)
         textAnchor.scale.multiplyScalar(-1)
         textAnchor

@@ -115,6 +116,7 @@
         lineObject = new THREE.Object3D()
         lineObject.add.apply(lineObject, letterObjects)
         lineObject.position.y = -index * line_layout.height
+        lineObject._line = _line
         lineObject

     addChain: (text) =>
@@ -124,8 +126,16 @@
         chainObject = new THREE.Object3D()
         chainObject.add.apply(chainObject, lineObjects)

-        # chainObject.children.forEach (line) ->
-            # debugger
+        chainObject.children.forEach (line, index, array) ->
+            return if index is 0
+
+            prev = array[index - 1]
+            prev_connector_idx = prev._line.connector_index
+            my_prev_connector_idx = line._line.my_prev_connector_index
+
+            line.position.x = prev.position.x
+            line.position.x += prev.children[prev_connector_idx].position.x
+            line.position.x -= line.children[my_prev_connector_idx].position.x

         chainObject.scale.multiplyScalar(SCALE_TEXT)


process lines as tree; rename setPositions to setNetworkPositions

--- app/vis.coffee
+++ app/vis.coffee
@@ -173,7 +173,7 @@

         network.reduceRight(_makeTree)

-    setPositions = (root) =>
+    setNetworkPositions = (root) =>
         root.position = new THREE.Vector3()

         radianScale = d3.scale.ordinal()
@@ -255,7 +255,7 @@
         @scene.add network_object

         root = makeTree network
-        root = setPositions root
+        root = setNetworkPositions root

         traverse = (node) =>
             return if ! node.position?
@@ -283,9 +283,24 @@

         traverse(root)

+    linesToTree = (lines) ->
+        lines = lines.map (line) ->
+            line: line.line
+            word: line.word
+            children: line.lines.map (_) -> line: _
+
+        reducer = (prev, current, index, array) ->
+            prev_index = current.children
+                .map (_) -> _.line
+                .indexOf(prev.line)
+            current.children[prev_index] = prev
+            current
+
+        lines.reduceRight reducer
+
     addLines: (lines) =>
-        console.log "from vis"
-        console.log lines
+        root = linesToTree lines
+        console.log root

     animate: =>
         requestAnimationFrame @animate


Add some ifs to check properties existence

--- lib/runner.coffee
+++ lib/runner.coffee
@@ -86,11 +86,14 @@
     env: @scriptOptions.mergedEnv(process.env)

   fillVarsInArg: (arg, codeContext, project_path) ->
-    arg = arg.replace(/{FILE_ACTIVE}/g, codeContext.filepath)
-    arg = arg.replace(/{FILE_ACTIVE_PATH}/g, path.join(codeContext.filepath, '..'))
-    arg = arg.replace(/{FILE_ACTIVE_NAME}/g, codeContext.filename)
-    arg = arg.replace(/{FILE_ACTIVE_NAME_BASE}/g, path.join(codeContext.filename, '..'))
-    arg = arg.replace(/{PROJECT_PATH}/g, project_path)
+    if codeContext.filepath?
+      arg = arg.replace(/{FILE_ACTIVE}/g, codeContext.filepath)
+      arg = arg.replace(/{FILE_ACTIVE_PATH}/g, path.join(codeContext.filepath, '..'))
+    if codeContext.filename?
+      arg = arg.replace(/{FILE_ACTIVE_NAME}/g, codeContext.filename)
+      arg = arg.replace(/{FILE_ACTIVE_NAME_BASE}/g, path.join(codeContext.filename, '..'))
+    if project_path?
+      arg = arg.replace(/{PROJECT_PATH}/g, project_path)

     arg


rename getWordObjects to getLetterObjectsForWord

--- app/vis.coffee
+++ app/vis.coffee
@@ -216,7 +216,7 @@

     alignToNode: alignToNode

-    getWordObjects: (text_object, word) ->
+    getLetterObjectsForWord: (text_object, word) ->
         word_object = new THREE.Object3D()
         begin = getWordIndex text_object._line, word
         end = begin + word.length
@@ -270,7 +270,7 @@
                     if next_child?
                         promises = node.children.map (child) =>
                             # Get the array of letters for the target word only
-                            children = @getWordObjects child._text_object, node.word
+                            children = @getLetterObjectsForWord child._text_object, node.word
                             @fadeToArray(1, 1000) children
                         return Promise.all promises
                 .then =>


Add preliminary support for filters.

Add preliminary support for filters.

--- lib/walker.coffee
+++ lib/walker.coffee
@@ -21,11 +21,17 @@
     @path.push(node)

     type = node.type
+
+    # check for a filter first
+    filters = @filters?[type]
+    if filters?
+      node = filter(node) for filter in filters
+
     fn = this[type]

     if fn
       out = fn.call(this, node, { path: @path })
-      out = @decorator(node, out)  if @decorator?
+      out = @decorator(node, out) if @decorator?
     else
       out = @onUnknownNode(node, { path: @path })


Let BLAKE2s accept key parameter with tip of the hat to GR.

--- library/BLAKE2s.coffee
+++ library/BLAKE2s.coffee
@@ -3,7 +3,7 @@
 # Exports a BLAKE2s subclass with a more convenient API for our purposes.
 module.exports = class BLAKE2s extends OriginalBLAKE2s
   constructor: (params) ->
-    OriginalBLAKE2s.call(this, params.length)
+    OriginalBLAKE2s.call(this, params.length, params.key)

   update: (input) ->
     OriginalBLAKE2s::update.call(this, input)


scroll to top on page change

scroll to top on page change

--- lineman/app/app.coffee
+++ lineman/app/app.coffee
@@ -25,8 +25,11 @@
     snakeName = _.snakeCase(name);
     'generated/components/' + snakeName + '/' + snakeName + '.html';

-angular.module('loomioApp').controller 'AppController', ($scope, $filter, $rootScope, $router, KeyEventService) ->
+angular.module('loomioApp').controller 'AppController', ($scope,$document, $filter, $rootScope, $router, KeyEventService) ->
   $scope.currentComponent = 'nothing yet'
+
+  $scope.$on 'currentComponent', ->
+    angular.element(document.querySelector('.main-container'))

   $scope.$on 'setTitle', (event, title) ->
     angular.element.find('title')[0].text = _.trunc(title, 300) + ' | Loomio'


temporary fix to code editor so failures don't prevent compiling result display

--- src/app/editors/codeEditor/fileCodeViewAce.coffee
+++ src/app/editors/codeEditor/fileCodeViewAce.coffee
@@ -180,8 +180,9 @@
       @editor.getSession().clearAnnotations()

     _clearErrorMarkers:=>
+      ###
       for marker in @_compileErrorsMarkers
-        @editor.setGutterMarker(marker.line,"lintAndErrorsGutter",  null)
+        @editor.setGutterMarker(marker.line,"lintAndErrorsGutter",  null)###
       @_compileErrorsMarkers = []

     _clearLintMarkers:=>


Fix issue with atom removing trailing whitespace.

Shame this isn't the default.

--- atom/.atom/config.cson
+++ atom/.atom/config.cson
@@ -25,3 +25,5 @@
   "travis-ci-status": {}
   welcome:
     showOnStartup: false
+  whitespace:
+    removeTrailingWhitespace: false


Update Atom Keymap

Update Atom Keymap

--- atom/.atom/keymap.cson
+++ atom/.atom/keymap.cson
@@ -3,3 +3,7 @@
     'ctrl-tab ^ctrl': 'unset!'
     'ctrl-shift-tab': 'pane:show-previous-item'
     'ctrl-shift-tab ^ctrl': 'unset!'
+
+'.platform-darwin atom-text-editor[data-grammar~="go"]:not([mini])':
+  'cmd-down': 'golang:godef'
+  'cmd-up': 'golang:godef-return'


Tweak a little code for clarity

Tweak a little code for clarity

--- tapestry-core/src/main/coffeescript/META-INF/modules/core/zone.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/core/zone.coffee
@@ -26,8 +26,6 @@

       this.trigger events.zone.willUpdate

-      # TODO: purge existing children?
-
       content = event.memo.content

       # The server may have passed down the empty string for the content; that removes the existing content.
@@ -44,9 +42,9 @@
     spi.onDocument events.zone.refresh, (event) ->

       # A Zone inside a form will render some additional parameters to coordinate updates with the Form on the server.
-      parameters = this.getAttribute "data-zone-parameters"
+      attr = this.getAttribute "data-zone-parameters"

-      parameters = if parameters is null then null else JSON.parse(parameters)
+      parameters = attr and JSON.parse attr

       ajax event.memo.url,
         parameters: _.extend { "t:zoneid": this.element.id }, parameters, event.memo.parameters


change to --actions flag

change to --actions flag

--- bin/cli.coffee
+++ bin/cli.coffee
@@ -23,8 +23,8 @@

   runner questions, (answers) ->
     # just actions
-    if argv._[1] is '-actions'
-      gentry.runActions questions, answers, ->
+    if argv.actions
+      return gentry.runActions questions, answers, ->
         process.exit()

     # full scaffold


Proper endpoint.

Proper endpoint.

--- src/Carrot.coffee
+++ src/Carrot.coffee
@@ -142,7 +142,7 @@
         'object_properties': objectProperties
       }
       params['object_instance_id'] = objectInstanceId if objectInstanceId
-      @getSignedRequest("/me/tweet.json", params, (jqXHR) =>
+      @getSignedRequest("/me/template_post.json", params, (jqXHR) =>
         FB.ui({
             method: 'feed',
             name: 'Facebook Dialogs',
@@ -171,7 +171,7 @@
       'object_properties': objectProperties
     }
     params['object_instance_id'] = objectInstanceId if objectInstanceId
-    @getSignedRequest("/me/tweet.json", params, (jqXHR) -> callback(jqXHR.responseJSON) if callback)
+    @getSignedRequest("/me/template_post.json", params, (jqXHR) -> callback(jqXHR.responseJSON) if callback)

   showTweet: (actionId, objectInstanceId, actionProperties, objectProperties, callback) ->
     @getTweet actionId, objectInstanceId, actionProperties, objectProperties, (reply) ->


Fix an error in how cancel buttons are submitted to the server

--- tapestry-core/src/main/coffeescript/META-INF/modules/core/forms.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/core/forms.coffee
@@ -48,7 +48,8 @@
         firstHidden.insertBefore hidden

       # TODO: Research why we need id and name and get rid of one if possible.
-      value = Object.toJSON [ submitter.element.id, isCancel ? "cancel" : submitter.element.name ]
+      name = if isCancel then "cancel" else submitted.element.name
+      value = Object.toJSON [ submitter.element.id, name ]

       hidden.value value


Return error when publishingName is not a string

Issue #79

--- rtmp.coffee
+++ rtmp.coffee
@@ -1435,6 +1435,26 @@
   respondPublish: (requestCommand, callback) ->
     @receiveTimestamp = null
     publishingName = requestCommand.objects[1]?.value
+
+    if typeof publishingName isnt 'string'
+      publishStart = createAMF0CommandMessage
+        chunkStreamID: 4
+        timestamp: 0
+        messageStreamID: 1
+        command: 'onStatus'
+        transactionID: requestCommand.transactionID
+        objects: [
+          createAMF0Data(null),
+          createAMF0Object({
+            level: 'error'
+            code: 'NetStream.Publish.Start'
+            description: 'Publishing Name parameter is invalid.'
+            details: @app
+            clientid: @clientid
+          })
+        ]
+      , @chunkSize
+      return callback null, publishStart

     # Strip query string part from a string like:
     # "livestream?videoKeyframeFrequency=5&totalDatarate=248"


Remove duplicate menu

Remove duplicate menu

Closes #13

--- menus/to-the-hubs.cson
+++ menus/to-the-hubs.cson
@@ -1,12 +0,0 @@
-'menu': [
-    'label': 'Packages'
-    'submenu': [
-      'label': 'GitHub'
-      'submenu': [
-        { 'label': 'Open on GitHub', 'command': 'github:open' }
-        { 'label': 'Blame on GitHub', 'command': 'github:blame' }
-        { 'label': 'History on GitHub', 'command': 'github:history' }
-        { 'label': 'Copy GitHub URL', 'command': 'github:copy-url' }
-      ]
-    ]
-]


should rollback transaction, not shipment

--- app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
+++ app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
@@ -246,7 +246,7 @@
       )

     cancelShipment: () ->
-      @get('currentShipment').rollback()
+      @get('transaction').rollback()
       @set('currentShipment', null)
       $('.shipment-form-modal').modal('hide')


atom-autocomplete-plus: keymaps

atom-autocomplete-plus: keymaps

(cherry picked from commit c9f3a95a8b1fc851be68355fc8a980b3cb222c19)

--- .atom/config.cson
+++ .atom/config.cson
@@ -1,7 +1,10 @@
 "*":
   "advanced-open-file":
     createDirectories: true
-  "autocomplete-plus": {}
+  "autocomplete-plus":
+    backspaceTriggersAutocomplete: true
+    confirmCompletion: "tab always, enter when suggestion explicitly selected"
+    defaultProvider: "Fuzzy"
   "autocomplete-python":
     addBracketAfterFunction: true
     addDotAfterModule: true


Client: repos that has no admin permission are disabled

--- client/admin/lib/views/integrations/adminintegrationdetailsview.coffee
+++ client/admin/lib/views/integrations/adminintegrationdetailsview.coffee
@@ -26,6 +26,17 @@
     @createEventCheckboxes()

     @createAuthView()
+
+    @disableAdminRepos()
+
+
+  disableAdminRepos: ->
+    data = @getData()
+    disabledRepos = data.repositories.filter (r) -> return r.disabled
+    selectOptions = @settingsForm.inputs.repository.getElement().options
+    disabledRepos.forEach (repo) ->
+      for option in selectOptions when option.value is repo.value
+        option.setAttribute 'disabled', 'disabled'

   createInstructionsView: ->
@@ -257,7 +268,16 @@

     if data.repositories
       for repository in data.repositories
-        repositories.push title: repository.full_name, value: repository.full_name
+        console.log 'repository repositories:', repository
+        console.log 'repository permissions:', repository.permissions
+        console.log 'repository permissions:', repository.permissions?.admin
+        if repository.permissions?.admin
+          repositories.push title: repository.full_name , value: repository.full_name
+        else
+          repositories.push
+            title: repository.full_name + ' (requires admin perms)'
+            value: repository.full_name
+            disabled: yes

     data.repositories = repositories or []


advanced settings to the bottom

advanced settings to the bottom

--- client/landing/app/Applications/Ace.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Ace.kdapplication/AppController.coffee
@@ -15,8 +15,6 @@
         router.openSection "Ace", name, query
     behavior      : "application"
     menu          : [
-      { title     : "Advanced Settings",   viewName  : "advancedSettings" }
-      { type      : "separator" }
       { title     : "Save",                eventName : "save" }
       { title     : "Save As",             eventName : "saveAs" }
       { type      : "separator" }
@@ -31,6 +29,8 @@
       { title     : "Reopen Latest Files", eventName : "reopen" }
       { type      : "separator" }
       { title     : "Exit",                eventName : "exit" }
+      { type      : "separator" }
+      { title     : "Advanced Settings",   viewName  : "advancedSettings" }
     ]
     fileTypes     : [
       "php", "pl", "py", "jsp", "asp", "aspx", "htm", "html", "phtml","shtml",


Redirect back to admin panel on successful /addSlide

--- routes/api.coffee
+++ routes/api.coffee
@@ -64,7 +64,10 @@
       return res.respond 'No such user!'
     user.addSlide data
     users.save()
-    res.respond user.data.slides
+    if user == req.body.user
+      res.redirect '/admin/cc'
+    else
+      res.redirect '/admin/admin'

 router.post '/deleteSlide', (req, res) ->
   if !req.body.id


Fix memberships page permissions error

Fix memberships page permissions error

--- lineman/app/components/memberships_page/memberships_page_controller.coffee
+++ lineman/app/components/memberships_page/memberships_page_controller.coffee
@@ -11,10 +11,6 @@

   @fetchMemberships = =>
     Records.memberships.fetchByNameFragment(@fragment, @group.key) if @fragment
-
-  @init Records.discussions.find $routeParams.key
-  Records.groups.findOrFetchById($routeParams.key).then @init, (error) ->
-    $rootScope.$broadcast('pageError', error)

   @canAdministerGroup = ->
     AbilityService.canAdministerGroup(@group)
@@ -53,4 +49,7 @@
   @name = (membership) ->
     membership.userName()

+  Records.groups.findOrFetchById($routeParams.key).then @init, (error) ->
+    $rootScope.$broadcast('pageError', error)
+
   return


Fix typo: size → parseSize.

Fix typo: size → parseSize.

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -267,7 +267,7 @@

   # Memory.
   if 'memory' of params
-    memory = size(params.memory)
+    memory = parseSize(params.memory)

     if memory < VM.memory.static[0]
       @throw(


KDFramework: input radio group selection fix

--- client/Framework/components/inputs/KDInputRadioGroup.coffee
+++ client/Framework/components/inputs/KDInputRadioGroup.coffee
@@ -33,11 +33,12 @@
       div.append label
       @domElement.append div

-      if not radioOptions.visible
-        div.hide()
+      div.hide()  unless radioOptions.visible
+
     @domElement

-  click:-> @setValue @getValue()
+  click:(event)->
+    @setValue $(event.target).closest(".kd-#{@getType()}-holder").find('input').val()

   setDefaultValue:(value) ->
     @inputDefaultValue = value
@@ -47,11 +48,9 @@
     @getDomElement().find("input:checked").val()

   setValue:(value)->
-    # @getDomElement().find("input[value='#{value}']").parent().siblings().removeClass('checked')
-    # @getDomElement().find("input[value='#{value}']").parent().addClass('checked')
-    @getDomElement().find("input[value='#{value}']").attr "checked","checked"
-    @getDomElement().find(".kd-radio-holder").removeClass 'active'
-    @getDomElement().find(".kd-radio-holder.#{value}").addClass 'active'  if value
+    @$("input[value='#{value}']").attr "checked", "checked"
+    @$(".kd-radio-holder").removeClass 'active'
+    @$(".kd-radio-holder.#{value}").addClass 'active'  if value

   getInputElements:->
     @getDomElement().find('input')


activity: ensure thread creation on popular message load

--- client/activity/lib/flux/stores/messagethreadssstore.coffee
+++ client/activity/lib/flux/stores/messagethreadssstore.coffee
@@ -26,6 +26,7 @@
   initialize: ->

     @on actions.LOAD_MESSAGE_SUCCESS, @ensureThread
+    @on actions.LOAD_POPULAR_MESSAGE_SUCCESS, @ensureThread
     @on actions.CREATE_MESSAGE_SUCCESS, @ensureThread

     @on actions.LOAD_COMMENT_SUCCESS, @handleLoadSuccess


Admin: Invitations: Copy change [ci skip]

--- client/admin/lib/views/invitations/invitesomeoneview.coffee
+++ client/admin/lib/views/invitations/invitesomeoneview.coffee
@@ -96,7 +96,7 @@

   notifyAdminInvites: (invites, admins) ->

-    title = if admins.length > 1 then "You're adding admin users" else "You're adding an admin"
+    title = if admins.length > 1 then "You're adding admins" else "You're adding an admin"
     @confirmModal = modal = new kd.ModalViewWithForms
       title                   : title
       overlay                 : yes


dashboard: indentation style is fixed

dashboard: indentation style is fixed

--- client/admin/lib/views/moderation/selectableitemview.coffee
+++ client/admin/lib/views/moderation/selectableitemview.coffee
@@ -1,10 +1,10 @@
-kd = require 'kd'
-JView = require 'app/jview'
-KDListItemView = kd.ListItemView
-KDCheckBox = kd.CheckBox
-KDLabelView = kd.LabelView
+kd              = require 'kd'
+JView           = require 'app/jview'
+KDListItemView  = kd.ListItemView
+KDCheckBox      = kd.CheckBox
+KDLabelView     = kd.LabelView

-module.exports = class SimilarItemView extends KDListItemView
+module.exports = class SelectableItemView extends KDListItemView

   JView.mixin @prototype


decrease column width, show review button as expected

--- src/components/scores/index.cjsx
+++ src/components/scores/index.cjsx
@@ -47,9 +47,9 @@
     sortIndex: 0
     tableWidth: 0
     tableHeight: 0
-    colDefaultWidth: 225
-    colSetWidth: 225
-    colResizeWidth: 225
+    colDefaultWidth: 180
+    colSetWidth: 180
+    colResizeWidth: 180
     colResizeKey: 0
     sort: INITIAL_SORT

@@ -79,10 +79,6 @@
         className='summary'
         id={"#{heading.plan_id}"}
         periodId={@state.period_id}/>
-    else if heading.average
-      summary =
-        {heading.average.toFixed(1)}
-

     else if heading.plan_id?
       linkParams =
@@ -97,6 +93,11 @@
           className='review-plan btn btn-default'>
           Review
         </Router.Link>
+
+    if heading.average
+      summary =
+        {heading.average.toFixed(1)}
+

     sortingHeader = <SortingHeader type={heading.type} sortKey={i}
       sortState={@state.sort} onSort={@changeSortingOrder}


fiddle command line args

fiddle command line args

--- code/li.coffee
+++ code/li.coffee
@@ -1,7 +1,10 @@
 #!/usr/bin/env coffee

 exports.main = (args) ->
-    if args?
-        args = process.argv
+    # If supplied *args* should be a list of arguments,
+    # including args[0], the command name; if not supplied,
+    # process.argv will be used.
+    if not args?
+        args = process.argv[1..]

 exports.main()


Include source.shell inside of sh/bash code blocks

--- src/packages/gfm/grammars/gfm.cson
+++ src/packages/gfm/grammars/gfm.cson
@@ -114,6 +114,16 @@
     'patterns': ['include': 'source.java']
   }
   {
+    'begin': '^```(sh|bash)$'
+    'beginCaptures':
+      '0': 'name': 'support.gfm'
+    'end': '^```$'
+    'endCaptures':
+      '0': 'name': 'support.gfm'
+    'name': 'markup.code.shell.gfm'
+    'patterns': ['include': 'source.shell']
+  }
+  {
     'begin': '^```.*$'
     'beginCaptures':
       '0': 'name': 'support.gfm'


WindowController: default listening object is changed to window rather than document

Hopefully this won't cause unexpected side effects, a separate implementation could be done here but since we're in the beginning and can afford such changes, I think now is the time.

--- src/core/windowcontroller.coffee
+++ src/core/windowcontroller.coffee
@@ -14,7 +14,7 @@
   @keyViewHistory = []
   superKey        = if navigator.userAgent.indexOf("Mac OS X") is -1 then "ctrl" else "command"
   addListener     = (eventName, listener, capturePhase=yes)->
-    document.body.addEventListener eventName, listener, capturePhase
+    window.addEventListener eventName, listener, capturePhase

   # Finding vendor prefixes for visibility
   getVisibilityProperty = ->


webpack: hide dev-server info logs, related to #51

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -102,6 +102,7 @@
   new WebpackDevServer compiler,
     publicPath: "//#{webpackDevHostname}:#{webpackDevPort}/"
     hot: true
+    noInfo: true
   .listen webpackDevPort, (err) ->
     if err
       console.trace err


hub: when mesg queue exceeds certain size, discard oldest messages!!

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -17,6 +17,17 @@
 SALVUS_HOME=process.cwd()

 REQUIRE_ACCOUNT_TO_EXECUTE_CODE = false
+
+# Anti DOS parameters:
+
+# If a client sends a burst of messages, we space handling them out by this many milliseconds:.
+MESG_QUEUE_INTERVAL_MS  = 50
+
+# If a client sends a burst of messages, we discard all but the most recent this many of them:
+MESG_QUEUE_MAX_COUNT    = 25
+
+# Any messages larger than this is not allowed (it could take a long time to handle, etc.).
+MESG_QUEUE_MAX_SIZE_MB  = 5

 # node.js -- builtin libraries
 net     = require 'net'
@@ -454,8 +465,8 @@
         # e.g., users storing a multi-gigabyte worksheet title,
         # etc..., which would (and will) otherwise require care with
         # every single thing we store.
-        if data.length >= 50000000 # 5 MB
-            @push_to_client(message.error(error:"Messages are limited to 5MB.", id:mesg.id))
+        if data.length >= MESG_QUEUE_MAX_SIZE_MB * 10000000
+            @error_to_client(id:mesg.id, error:"Messages are limited to #{MESG_QUEUE_MAX_SIZE_MB}MB.")

         if data.length == 0
             winston.error("EMPTY DATA MESSAGE -- ignoring!")
@@ -471,10 +482,10 @@
             return

         # The rest of the function is basically the same as "h(data.slice(1))", except that
-        # it ensure that we handle at most 1 message per client every 50ms (or whatever
-        # the param is below).   This is another anti-DOS measure, which of course also
-        # helps with scaling up to many users.
-
+        # it ensure that if there is a burst of messages, then (1) we handle at most 1 message
+        # per client every MESG_QUEUE_INTERVAL_MS, and we drop messages if there are too many.
+        # This is an anti-DOS measure.
+
         @_handle_data_queue.push([h, data.slice(1)])

         if @_handle_data_queue_empty_function?
@@ -488,12 +499,16 @@
                 delete @_handle_data_queue_empty_function
                 return

+            # drop oldest message to keep
+            while @_handle_data_queue.length > MESG_QUEUE_MAX_COUNT
+                @_handle_data_queue.shift()
+
             # get task
             task = @_handle_data_queue.shift()
             # do task
             task[0](task[1])
-            # do next one in >=50ms
-            setTimeout( @_handle_data_queue_empty_function, 50 )
+            # do next one in >= MESG_QUEUE_INTERVAL_MS
+            setTimeout( @_handle_data_queue_empty_function, MESG_QUEUE_INTERVAL_MS )

         @_handle_data_queue_empty_function()


update 'who' regex

update 'who' regex

need to account for period (.) or dash (-) in usernames

--- src/badges.coffee
+++ src/badges.coffee
@@ -60,7 +60,7 @@
   robot.brain.on 'loaded', ->
     robot.brain.data.ingressBadges ?= {}

-  robot.respond /(I|@?\w+) (?:have|has|got|earned)(?: the)? :?([\-\w,\s]+):? badges?/i, (msg) ->
+  robot.respond /(@?[.\w\-]+) (?:have|has|got|earned)(?: the)? :?([\-\w,\s]+):? badges?/i, (msg) ->
     who = msg.match[1].toLowerCase().replace '@', ''
     badgeNames = (msg.match[2].toLowerCase().replace /\s*/g, '').split ','

@@ -92,7 +92,7 @@
         msg.send "@#{who.name}: congrats on earning the :#{badgeNames.join ': :'}:
  badge#{if badgeNames.length > 1 then 's' else ''}!"

-  robot.respond /wh(?:at|ich) badges? do(?:es)? (I|@?\w+) have/i, (msg) ->
+  robot.respond /wh(?:at|ich) badges? do(?:es)? (@?[.\w\-]+) have/i, (msg) ->
     who = msg.match[1].replace '@', ''

     if who.toLowerCase() == 'i'
@@ -109,7 +109,7 @@
     else
       msg.reply "#{whowhat} no badges."

-  robot.respond /(I|@?\w+) (?:do(?:n't|esn't| not)) have the :?([\-\w]+):? badge/i, (msg) ->
+  robot.respond /(@?[.\w\-]+) (?:do(?:n't|esn't| not)) have the :?([\-\w]+):? badge/i, (msg) ->
     who = msg.match[1].toLowerCase().replace '@', ''
     badgeName = msg.match[2].toLowerCase()


properly remove clients when they are closed.

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -565,7 +565,7 @@

         @conn.on "data", @handle_data_from_client

-        @conn.on "close", () =>
+        @conn.on "end", () =>
             winston.debug("connection: hub <--> client(id=#{@id}, address=#{@ip_address})  CLOSED")
             @emit 'close'
             @compute_session_uuids = []


add script to run_all_account_creation_actions

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -3598,6 +3598,57 @@
     # We immediately move on -- it's ok to do the actions in parallel.
     opts.cb()

+run_all_account_creation_actions = (cb) ->
+    dbg = (m) -> winston.debug("all_account_creation: #{m}")
+
+    email_addresses = undefined
+    users           = undefined
+
+    async.series([
+        (cb) ->
+            dbg("get all email addresses in the account creation actions table")
+            database.select
+                table   : 'account_creation_actions'
+                columns : ['email_address']
+                cb      : (err, results) ->
+                    if err
+                        cb(err)
+                    else
+                        dbg("got #{results.length} creation actions from database")
+                        email_addresses = (x[0] for x in results)
+                        cb()
+        (cb) ->
+            dbg("for each action, determine if the account has been created (most probably won't be) and get account_id")
+            database.select
+                table     : 'email_address_to_account_id'
+                columns   : ['email_address', 'account_id']
+                where     : {email_address:{'in':email_addresses}}
+                objectify : true
+                cb        : (err, results) ->
+                    if err
+                        cb(err)
+                    else
+                        dbg("got #{results.length} of these accounts have already been created")
+                        users = results
+                        cb()
+        (cb) ->
+            dbg("for each of the #{users.length} for which the account has been created, do all the actions")
+            i = 0
+            f = (user, cb) ->
+                i += 1
+                dbg("considering user #{i} of #{users.length}")
+                account_creation_actions
+                    email_address : user.email_address
+                    account_id    : user.account_id
+                    cb            : cb
+            # We use mapSeries instead of map, so that the log output is clearer, and since this is fairly small.
+            # We could do it in parallel and be way faster, but not necessary.
+            async.mapSeries(users, f, (err) -> cb(err))
+    ], cb)
+
+
+
+
 change_password = (mesg, client_ip_address, push_to_client) ->
     account = null
     mesg.email_address = misc.lower_email_address(mesg.email_address)
@@ -4593,6 +4644,7 @@
     .option('--add_user_to_project [email_address,project_id]', 'Add user with given email address to project with given ID', String, '')
     .option('--base_url [string]', 'Base url, so https://sitenamebase_url/', String, '')  # '' or string that starts with /
     .option('--local', 'If option is specified, then *all* projects run locally as the same user as the server and store state in .sagemathcloud-local instead of .sagemathcloud; also do not kill all processes on project restart -- for development use (default: false, since not given)', Boolean, false)
+    .option('--account_creation_actions', 'Run all known account creation actions for accounts that have been created (used mainly to clean up after a particular bug)')
     .parse(process.argv)

     # NOTE: the --local option above may be what is used later for single user installs, i.e., the version included with Sage.
@@ -4611,6 +4663,11 @@
     if program.passwd
         console.log("Resetting password")
         reset_password(program.passwd, (err) -> process.exit())
+    if program.account_creation_actions
+        console.log("Account creation actions")
+        run_all_account_creation_actions (err) ->
+            console.log("DONE --", err)
+            (err) -> process.exit()
     else if program.add_user_to_project
         console.log("Adding user to project")
         v = program.add_user_to_project.split(',')


gcloud: more functionality related to changing/copying/moving disks

--- salvus/smc_gcloud.coffee
+++ salvus/smc_gcloud.coffee
@@ -49,7 +49,7 @@
 age_s = (time) -> (new Date() - time)/1000

 onCompleteOpts =
-    maxAttempts : 30
+    maxAttempts : 1200  # 3s * 1200 = 3600s = 1h

 handle_operation = (err, operation, done, cb) ->
     if err
@@ -72,6 +72,9 @@

     dbg: (f) -> @gcloud.dbg("vm.#{f}")

+    show: =>
+        @get_metadata(cb:console.log)
+
     _action: (cmd, cb) =>
         dbg = @dbg(cmd)
         dbg('calling api...')
@@ -100,6 +103,17 @@
             dbg("done")
             opts.cb(err, metadata)

+    disks: (opts) =>
+        opts = defaults opts,
+            cb : required
+        @get_metadata
+            cb : (err, data) =>
+                if err
+                    opts.cb(err)
+                else
+                    disks = (@gcloud.disk(zone:@zone, name:misc.path_split(x.source).tail) for x in data.disks)
+                    opts.cb(undefined, disks)
+
     status : (opts) =>
         opts = defaults opts,
             cb : required
@@ -107,11 +121,37 @@
             cb : (err, x) =>
                 opts.cb(err, x?.status)

+    # create disk and attach to this instance
+    create_disk: (opts) =>
+        opts = defaults opts,
+            name     : required
+            size_GB  : undefined
+            type     : 'pd-standard'   # 'pd-standard' or 'pd-ssd'
+            snapshot : undefined # if given, base on snapshot
+            cb       : undefined
+        dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        async.series([
+            (cb) =>
+                dbg("creating disk...")
+                @gcloud.create_disk
+                    name     : opts.name
+                    size_GB  : opts.size_GB
+                    type     : opts.type
+                    snapshot : opts.snapshot
+                    zone     : @zone
+                    cb       : cb
+            (cb) =>
+                dbg("attaching to this instance")
+                @gcloud.disk(name:opts.name, zone:@zone).attach_to
+                    vm : @
+                    cb : cb
+        ], (err) => opts.cb?(err))
+
     attach_disk: (opts) =>
         opts = defaults opts,
             disk      : required
             read_only : false
-            cb        : required
+            cb        : undefined
         dbg = @dbg("attach_disk")
         if not (opts.disk instanceof Disk)
             dbg("not Disk")
@@ -119,7 +159,7 @@
                 dbg("is string so make disk")
                 opts.disk = @gcloud.disk(name:opts.disk, zone:@zone)
             else
-                opts.cb("disk must be an instance of Disk")
+                opts.cb?("disk must be an instance of Disk")
                 return
         dbg("starting...")
         @_vm.attachDisk opts.disk._disk, {readOnly: opts.read_only}, (err, operation, apiResponse) =>
@@ -128,7 +168,7 @@
     detach_disk: (opts) =>
         opts = defaults opts,
             disk : required
-            cb   : required
+            cb   : undefined
         dbg = @dbg("detach_disk")
         if not (opts.disk instanceof Disk)
             dbg("not Disk")
@@ -136,7 +176,7 @@
                 dbg("is string so make disk")
                 opts.disk = @gcloud.disk(name:opts.disk, zone:@zone)
             else
-                opts.cb("disk must be an instance of Disk")
+                opts.cb?("disk must be an instance of Disk")
                 return
         dbg("starting...")
         vm_data = disk_data = undefined
@@ -170,7 +210,7 @@
                 disk = @gcloud._gce.zone(@zone).disk(deviceName)
                 @_vm.detachDisk disk, (err, operation, apiResponse) =>
                     handle_operation(err, operation, (->dbg("done")), cb)
-        ], (err) => opts.cb(err))
+        ], (err) => opts.cb?(err))

     get_serial_console: (opts) =>
         opts = defaults opts,
@@ -185,13 +225,187 @@
                 else
                     console.log(output)

+    # DIFFICULT change configuration of this VM
+    # WARNING: this may be a possibly dangerous multi-step process that
+    # could involve deleting and recreating the VM.
+    change: (opts) =>
+        opts = defaults opts,
+            preemptible  : undefined    # whether or not VM is preemptible
+            type         : undefined    # the instance type
+            zone         : undefined    # which zone it runs in
+            boot_size_GB : undefined    # size in GB of boot disk
+            cb           : undefined
+        dbg = @dbg("change(#{misc.to_json(misc.map_without_undefined(misc.copy_with(opts, ['cb'])))})")
+        dbg()
+        data = undefined
+        changes = {}
+        async.series([
+            (cb) =>
+                dbg('get vm metadata to see what needs to be changed')
+                @get_metadata
+                    cb : (err, x) =>
+                        data = x; cb(err)
+            (cb) =>
+                if opts.preemptible? and data.scheduling.preemptible != opts.preemptible
+                    changes.preemptible = opts.preemptible
+                if opts.type? and misc.path_split(data.machineType).tail != opts.type
+                    changes.type = opts.type
+                if opts.zone? and misc.path_split(data.zone).tail != opts.zone
+                    changes.zone = opts.zone
+                if not opts.boot_size_GB?
+                    cb(); return
+                boot_disk = undefined
+                for x in data.disks
+                    if x.boot
+                        boot_disk = @gcloud.disk(name: misc.path_split(x.source).tail)
+                        break
+                if not boot_disk?
+                    cb(); return  # is this possible
+                boot_disk.get_size_GB
+                    cb : (err, size_GB) =>
+                        if err
+                            cb(err)
+                        else
+                            if size_GB != opts.boot_size_GB
+                                changes.size_GB = opts.boot_size_GB
+                            cb()
+            (cb) =>
+                dbg("determined changes=#{misc.to_json(changes)}")
+                cb()
+        ], (err) => opts.cb?(err))
+
 class Disk
     constructor: (@gcloud, @name, @zone=DEFAULT_ZONE) ->
         @_disk = @gcloud._gce.zone(@zone).disk(@name)

     dbg: (f) -> @gcloud.dbg("disk.#{f}")

-    #createSnapshot, delete, getMetadata
+    show: =>
+        @get_metadata(cb:console.log)
+
+    copy: (opts) =>
+        opts = defaults opts,
+            name      : required
+            size_GB   : undefined  # if specified must be at least as large as existing disk
+            type      : undefined  # 'pd-standard' or 'pd-ssd'; if not given same as current
+            zone      : @zone      # zone of this disk
+            cb        : required
+        dbg = @dbg("copy(name=#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        if @name == opts.name
+            opts.cb()  # nothing to do
+            return
+        @_utility
+            name : opts.name
+            size_GB : opts.size_GB
+            type    : opts.type
+            zone    : opts.zone
+            move    : false
+            cb      : opts.cb
+
+    # Change size or type of a disk.
+    # Disk maybe attached to an instance.
+    change: (opts) =>
+        opts = defaults opts,
+            size_GB   : undefined  # if specified must be at least as large as existing disk
+            type      : undefined  # 'pd-standard' or 'pd-ssd'; if not given same as current
+            cb        : required
+        dbg = @dbg("reconfigure(name=#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        if not opts.size_GB? and not opts.type?
+            opts.cb()  # nothing to do
+            return
+        @_utility
+            size_GB : opts.size_GB
+            type    : opts.type
+            delete  : true
+            cb      : opts.cb
+
+    _utility: (opts) =>
+        opts = defaults opts,
+            name      : @name
+            size_GB   : undefined  # if specified must be at least as large as existing disk
+            type      : undefined  # 'pd-standard' or 'pd-ssd'; if not given same as current
+            zone      : @zone      # zone of this disk
+            delete    : false      # if true: deletes original disk after making snapshot successfully; also remounts if zone same
+            cb        : undefined
+        dbg = @dbg("_utility(name=#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        dbg()
+        vms = undefined  # vms that disk was attached to (if any)
+        snapshot_name = undefined
+        async.series([
+            (cb) =>
+                if not opts.size_GB?
+                    cb()
+                else
+                    dbg("size consistency check")
+                    if opts.size_GB < 10
+                        cb("size_GB must be at least 10")
+                        return
+                    @get_size_GB
+                        cb : (err, size_GB) =>
+                            if err
+                                cb(err)
+                            else
+                                if opts.size_GB < size_GB
+                                    cb("Requested disk size cannot be smaller than the current size")
+                                else
+                                    cb()
+            (cb) =>
+                dbg("determine new disk type")
+                if opts.type
+                    cb()
+                else
+                    @get_type
+                        cb : (err, type) =>
+                            opts.type = type
+                            cb(err)
+            (cb) =>
+                snapshot_name = "temp-#{@name}-#{misc.uuid()}"
+                dbg("create snapshot with name #{snapshot_name}")
+                @snapshot
+                    name : snapshot_name
+                    cb   : cb
+            (cb) =>
+                if not opts.delete
+                    cb(); return
+                dbg("detach disk from any vms")
+                @detach
+                    cb : (err, x) =>
+                        vms = x
+                        cb(err)
+            (cb) =>
+                if not opts.delete
+                    cb(); return
+                dbg("delete disk")
+                @delete(cb : cb)
+            (cb) =>
+                dbg("make new disk from snapshot")
+                @gcloud.snapshot(name:snapshot_name).create_disk
+                    name    : opts.name
+                    size_GB : opts.size_GB
+                    type    : opts.type
+                    zone    : opts.zone
+                    cb      : cb
+            (cb) =>
+                if not vms? or vms.length == 0
+                    cb(); return
+                if opts.zone? and @zone != opts.zone # moved zones
+                    cb(); return
+                dbg("remount new disk on same vms")
+                f = (vm, cb) =>
+                    vm.attach_disk
+                        disk      : opts.name
+                        read_only : vms.length > 1  # if more than 1 must be read only  (kind of lame)
+                        cb        : cb
+                async.map(vms, f, cb)
+            (cb) =>
+                if not snapshot_name?
+                    cb(); return
+                dbg("clean up snapshot #{snapshot_name}")
+                @gcloud.snapshot(name:snapshot_name).delete(cb : cb)
+        ], (err) =>
+            opts.cb?(err)
+        )
+

     snapshot: (opts) =>
         opts = defaults opts,
@@ -203,6 +417,20 @@
         done = -> dbg("done  -- took #{misc.walltime(start)}s")
         @_disk.createSnapshot opts.name, (err, snapshot, operation, apiResponse) =>
             handle_operation(err, operation, done, opts.cb)
+
+    get_size_GB: (opts) =>
+        opts = defaults opts,
+            cb : required
+        @get_metadata
+            cb : (err, data) =>
+                opts.cb(err, if data? then parseInt(data.sizeGb))
+
+    get_type: (opts) =>
+        opts = defaults opts,
+            cb : required
+        @get_metadata
+            cb : (err, data) =>
+                opts.cb(err, if data? then misc.path_split(data.type).tail)

     get_metadata: (opts) =>
         opts = defaults opts,
@@ -267,7 +495,7 @@
     detach: (opts) =>
         opts = defaults opts,
             vm : undefined   # if not given, detach from all users of this disk
-            cb : undefined
+            cb : undefined   # (err, list_of_vms_that_we_detached_disk_from)
         dbg = @dbg("detach")
         vms = undefined
         async.series([
@@ -283,7 +511,7 @@
                                 cb(err)
                             else
                                 # all the users must be in the same zone as this disk
-                                vms = (@gcloud.vm(name:misc.path_split(u).tail, zone:@zone) for u in data.users)
+                                vms = (@gcloud.vm(name:misc.path_split(u).tail, zone:@zone) for u in (data.users ? []))
                                 cb()
             (cb) =>
                 dbg('actually detach disk from that vm')
@@ -293,11 +521,14 @@
                         cb   : cb
                 async.map(vms, f, cb)

-            ], (err) => opts.cb?(err))
+            ], (err) => opts.cb?(err, vms))

 class Snapshot
     constructor: (@gcloud, @name) ->
         @_snapshot = @gcloud._gce.snapshot(@name)
+
+    show: =>
+        @get_metadata(cb:console.log)

     dbg: (f) -> @gcloud.dbg("snapshot.#{f}")

@@ -339,15 +570,12 @@
             type    : 'pd-standard'   # 'pd-standard' or 'pd-ssd'
             zone    : DEFAULT_ZONE
             cb      : undefined
-        dbg = @dbg("create_disk(#{misc.to_json(opts)})")
-        dbg("starting...")
-        config =
-            sourceSnapshot : "global/snapshots/#{@name}"
-        config.sizeGb = opts.size_GB if opts.size_GB?
-        config.type = "zones/#{opts.zone}/diskTypes/#{opts.type}"
-        @gcloud._gce.zone(opts.zone).createDisk opts.name, config, (err, disk, operation, apiResponse) =>
-            handle_operation(err, operation, (->dbg('done')), (err) => opts.cb?(err))
-
+        dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        if opts.size_GB? and opts.size_GB < 10
+            opts.cb?("size_GB must be at least 10")
+            return
+        opts.snapshot = @name
+        @gcloud.create_disk(opts)

 class GoogleCloud
     constructor: (opts={}) ->
@@ -366,25 +594,25 @@

     create_vm: (opts) =>
         opts = defaults opts,
-            name         : required
-            zone         : DEFAULT_ZONE
-            disks        : undefined      # see disks[] at https://cloud.google.com/compute/docs/reference/latest/instances
-                                          # can also pass in Disk objects in the array; or a single string which
-                                          # will refer to the disk with that name in same zone.
-            http         : undefined      # allow http
-            https        : undefined      # allow https
-            machine_type : undefined      # the instance type, e.g., 'n1-standard-1'
-            os           : undefined      # see https://github.com/stephenplusplus/gce-images#accepted-os-names
-            tags         : undefined      # array of strings
-            preemptible  : false
-            cb           : required
+            name        : required
+            zone        : DEFAULT_ZONE
+            disks       : undefined      # see disks[] at https://cloud.google.com/compute/docs/reference/latest/instances
+                                         # can also pass in Disk objects in the array; or a single string which
+                                         # will refer to the disk with that name in same zone.
+            http        : undefined      # allow http
+            https       : undefined      # allow https
+            type        : undefined      # the instance type, e.g., 'n1-standard-1'
+            os          : undefined      # see https://github.com/stephenplusplus/gce-images#accepted-os-names
+            tags        : undefined      # array of strings
+            preemptible : false
+            cb          : required
         dbg = @dbg("create_vm(name=#{opts.name})")
         config = {}
-        config.http        = opts.http if opts.http?
+        config.http        = opts.http  if opts.http?
         config.https       = opts.https if opts.https?
-        config.machineType = opts.machine_type if opts.machine_type?
-        config.os          = opts.os if opts.os?
-        config.tags        = opts.tags if opts.tags?
+        config.machineType = opts.type  if opts.type?
+        config.os          = opts.os    if opts.os?
+        config.tags        = opts.tags  if opts.tags?
         if opts.preemptible
             config.scheduling = {preemptible : true}
         if opts.disks?
@@ -424,6 +652,28 @@
         @_disk_cache ?= {}
         return (@_disk_cache[key] ?= new Disk(@, opts.name, opts.zone))

+    create_disk: (opts) =>
+        opts = defaults opts,
+            name     : required
+            size_GB  : undefined
+            type     : 'pd-standard'   # 'pd-standard' or 'pd-ssd'
+            zone     : DEFAULT_ZONE
+            snapshot : undefined
+            cb       : undefined
+        dbg = @dbg("create_disk(#{misc.to_json(misc.copy_without(opts, ['cb']))})")
+        if opts.size_GB? and opts.size_GB < 10
+            opts.cb?("size_GB must be at least 10")
+            return
+
+        dbg("starting...")
+        config = {}
+        if opts.snapshot?
+            config.sourceSnapshot = "global/snapshots/#{opts.snapshot}"
+        config.sizeGb = opts.size_GB if opts.size_GB?
+        config.type   = "zones/#{opts.zone}/diskTypes/#{opts.type}"
+        @_gce.zone(opts.zone).createDisk opts.name, config, (err, disk, operation, apiResponse) =>
+            handle_operation(err, operation, (->dbg('done')), (err) => opts.cb?(err))
+
     snapshot: (opts) =>
         opts = defaults opts,
             name : required
@@ -435,11 +685,14 @@
     get_snapshots: (opts) =>
         opts = defaults opts,
             filter : undefined
+            match  : undefined   # only return results whose name contains match
             cb     : required
         options = {maxResults:500}   # deal with pagers next year
         options.filter = opts.filter if opts.filter?
         dbg = @dbg("get_snapshots")
         dbg("options=#{misc.to_json(options)}")
+        if opts.match?
+            opts.match = opts.match.toLowerCase()
         @_gce.getSnapshots options, (err, snapshots) =>
             dbg("done")
             if err
@@ -448,6 +701,8 @@
                 s = []
                 for x in snapshots
                     i = x.metadata.sourceDisk.indexOf('/zones/')
+                    if opts.match? and x.name.toLowerCase().indexOf(opts.match) == -1
+                        continue
                     s.push
                         name      : x.name
                         timestamp : new Date(x.metadata.creationTimestamp)