Remove some unused code

Remove some unused code

--- app/models/LevelSession.coco.coffee
+++ app/models/LevelSession.coco.coffee
@@ -20,7 +20,7 @@
     @set 'permissions', permissions

   getSourceFor: (spellKey) ->
-    # spellKey ex: 'tharin/plan'
+    # spellKey ex: 'hero-placeholder/plan'
     code = @get('code')
     parts = spellKey.split '/'
     code?[parts[0]]?[parts[1]]
@@ -42,17 +42,6 @@

   completed: ->
     @get('state')?.complete || false
-
-  shouldAvoidCorruptData: (attrs) ->
-    return false unless me.team is 'humans'
-    if _.string.startsWith (attrs?.code ? @get('code'))?.anya?.makeBid ? '', 'var __interceptThis'
-      noty text: "Not saving session--it's trying to overwrite Anya's code with transpiled output. Please let us know and help us reproduce this bug!", layout: 'topCenter', type: 'error', killer: false, timeout: 120000
-      return true
-    false
-
-  save: (attrs, options) ->
-    return if @shouldAvoidCorruptData attrs
-    super attrs, options

   increaseDifficulty: (callback) ->
     state = @get('state') ? {}


Fixed typo

Fixed typo

--- spec/grammar-spec.coffee
+++ spec/grammar-spec.coffee
@@ -465,7 +465,7 @@
       expect(lines[25][0]).toEqual value: "~                                    ^<<<<<<<<<<<<<<<<<<<<<<<...", scopes: ["source.perl", "meta.format.perl"]
       expect(lines[27][0]).toEqual value: ".", scopes: ["source.perl", "meta.format.perl"]

-  describe "when a function call tokenizes", ->
+  describe "when a heredoc tokenizes", ->
     it "does not highlight the whole line", ->
       lines = grammar.tokenizeLines("""$asd->foo(<<TEST, $bar, s/foo/bar/g);
       asd


Simplify pusher handling in store

Simplify pusher handling in store

--- app/services/store.coffee
+++ app/services/store.coffee
@@ -74,14 +74,12 @@
       job = @recordForId('job', data.id)
       job.appendLog(number: parseInt(data.number), content: data._log, final: data.final)
     else if data[name]
-      @_loadOne(name, data)
+      @loadOne(name, data)
     else
       throw "can't load data for #{name}" unless type

-  _loadOne: (type, json) ->
-    payload = {}
-    payload[type.pluralize()] = [json[type]]
-    @push(this.normalize(type, json))
+  loadOne: (type, json) ->
+    record = @push(this.normalize(type, json))

     # we get other types of records only in a few situations and
     # it's not always needed to update data, so I'm specyfing which


testParticipant: Fix wrong expected format of discovery message

--- spec/heterogenous.coffee
+++ spec/heterogenous.coffee
@@ -66,11 +66,14 @@
       definitions = []

       onDiscovery = (msg) ->
-        def = msg.data
-        definitions.push def
-        state.broker.ackMessage msg
-        if typeof onParticipantDiscovered == 'function'
-          onParticipantDiscovered def, definitions
+        if msg.data.protocol == 'discovery' and msg.data.command == 'participant'
+          def = msg.data.payload
+          definitions.push def
+          state.broker.ackMessage msg
+          if typeof onParticipantDiscovered == 'function'
+            onParticipantDiscovered def, definitions
+        else
+          console.log "WARNING", 'unknown discovery message:', msg.data.protocol, msg.data.command
       state.broker.subscribeParticipantChange onDiscovery

       participant = startForeign state.commands, name, options, done


test with comparing objects

test with comparing objects

--- spec/openSongSpec.coffee
+++ spec/openSongSpec.coffee
@@ -45,18 +45,40 @@

   describe ".parseLyrics", ->

-    it "parses comments", ->
-      src = "; Comment"
+    it "parses header", ->
+      src = "[C]"
       model = opensong.helper.parseLyrics src
-      expect(model.length).toEqual 1
+      expect(model).toEqual [
+        header: "C"
+        lines: []
+      ]

     it "parses chords", ->
-      src = ". C D"
+      src = ".D"
       model = opensong.helper.parseLyrics src
-      expect(model.length).toEqual 1
+      expect(model).toEqual [
+        header: undefined
+        lines: [
+          chords: ["D"]
+        ]
+      ]

     it "parses lyrics", ->
       src = " Lyrics"
       model = opensong.helper.parseLyrics src
-      expect(model.length).toEqual 1
+      expect(model).toEqual [
+        header: undefined
+        lines: [
+          lyrics: ["Lyrics"]
+        ]
+      ]

+    it "parses comments", ->
+      src = ";Comment"
+      model = opensong.helper.parseLyrics src
+      expect(model).toEqual [
+        header: undefined
+        lines: [
+          comments: "Comment"
+        ]
+      ]


fixed broken test

fixed broken test

--- spec/readdir-spec.coffee
+++ spec/readdir-spec.coffee
@@ -11,6 +11,6 @@
     dotEnsimesFilter = (path, stats) ->
       stats.isFile() && ! path.endsWith('.ensime')

-    recread(root, [ignoreFilter], (err, files) ->
+    recread(root, [], (err, files) ->
       console.log(files)
       )


Update buffered-process's documentation

Update buffered-process's documentation

--- src/buffered-process.coffee
+++ src/buffered-process.coffee
@@ -2,19 +2,27 @@
 path = require 'path'
 _ = require 'underscore'

-# Private: A wrapper which provides buffering for ChildProcess
+# Private: A wrapper which provides buffering for ChildProcess.
 module.exports =
 class BufferedProcess
   process: null
   killed: false

-  # options -
-  #  command: The command to execute.
-  #  args: The arguments for the given command.
-  #  options: The options to pass to ChildProcess.
-  #  stdout: The callback to receive stdout data.
-  #  stderr: The callback to receive stderr data.
-  #  exit: The callback to receive exit status.
+  # Executes the given command.
+  #
+  # * options
+  #    + command:
+  #      The command to execute.
+  #    + args:
+  #      The arguments for the given command.
+  #    + options:
+  #      The options to pass to ChildProcess.
+  #    + stdout:
+  #      The callback to receive stdout data.
+  #    + stderr:
+  #      The callback to receive stderr data.
+  #    + exit:
+  #      The callback to receive exit status.
   constructor: ({command, args, options, stdout, stderr, exit}={}) ->
     options ?= {}
     @process = ChildProcess.spawn(command, args, options)
@@ -49,11 +57,12 @@

   # Private: Helper method to pass data line by line.
   #
-  # stream  - The Stream to read from.
-  # onLines - The callback to call with each line of data.
-  # onDone  - The callback to call when the stream has closed.
-  #
-  # Returns nothing.
+  # * stream:
+  #   The Stream to read from.
+  # * onLines:
+  #   The callback to call with each line of data.
+  # * onDone:
+  #   The callback to call when the stream has closed.
   bufferStream: (stream, onLines, onDone) ->
     stream.setEncoding('utf8')
     buffered = ''
@@ -72,8 +81,6 @@
       onDone()

   # Public: Terminates the process.
-  #
-  # Returns nothing.
   kill: ->
     @killed = true
     @process.kill()


Catch wmic spawn errors

Catch wmic spawn errors

--- src/buffered-process.coffee
+++ src/buffered-process.coffee
@@ -139,7 +139,12 @@
       'processid'
     ]

-    wmicProcess = ChildProcess.spawn(cmd, args)
+    try
+      wmicProcess = ChildProcess.spawn(cmd, args)
+    catch spawnError
+      @killProcess()
+      return
+
     wmicProcess.on 'error', -> # ignore errors
     output = ''
     wmicProcess.stdout.on 'data', (data) -> output += data


Better autocomplete algorithm

Better autocomplete algorithm

--- lib/services/php-file-parser.coffee
+++ lib/services/php-file-parser.coffee
@@ -56,15 +56,32 @@
    * @return string className
   ###
   getStackClasses: (editor, position) ->
-    # Get the line
-    text = editor.getTextInBufferRange([[position.row, 0], position])
-
-    idx = 1
-    while (not text.match(/([\$][a-zA-Z0-9]*)/g)) or (position.row - idx <= 0)
+    lineIdx = 0
+    parenthesisOpened = 0
+    parenthesisClosed = 0
+    idx = 0
+    end = false
+
+    # Algorithm to get something inside parenthesis
+    # Count parenthesis, when opened == closed and found a variable, it's done
+    while (position.row - lineIdx > 0) and end == false
       text = editor.getTextInBufferRange([[position.row - idx, 0], position])
-      idx++
-
-    text = text.substr(text.lastIndexOf("$"), text.length)
+      lineIdx++
+      len = text.length
+
+      while idx < len and end == false
+        if text[len - idx] == "("
+          parenthesisOpened += 1
+        else if text[len - idx] == ")"
+          parenthesisClosed += 1
+        else if text[len - idx] == "{" # If curly brace, we failed.
+          end = true
+        if text[len - idx] == "$" and parenthesisClosed == parenthesisOpened
+          end = true
+
+        idx += 1
+
+    text = text.substr(text.length - idx + 1, text.length)

     # Remove parenthesis content
     regx = /\((?:[^\])\(\)]+|(?:[^\(\)\])]*\([^\(\)\])]*\)[^\)]*))*\)*/g
@@ -74,7 +91,7 @@
     return [] if not text

     elements = text.split("->")
-
+
     # Remove parenthesis and whitespaces
     for key, element of elements
       element = element.replace /^\s+|\s+$/g, ""


Regular nodes in closed loops with collectors should not have sliders [#145981913]

--- src/code/models/node.coffee
+++ src/code/models/node.coffee
@@ -54,6 +54,7 @@
     @color ?= Colors.choices[0].value

     @isInCycle = false      # we always initalize with no links, so we can't be in cycle
+    @hasAccumulatorAncestor = false

     @_collectorImageProps = null

@@ -131,20 +132,23 @@
   checkIsInIndependentCycle: ->
     visitedNodes = []
     original = this
-    isOwnGrandpa = false
+    hasAccumulatorAncestor = false
+    isOwnAncestor = false

     visit = (node) ->
       visitedNodes.push node
       for link in node.inLinks() when link.relation?.isDefined
         upstreamNode = link.sourceNode
         return true unless upstreamNode.isDependent(true)      # fast exit if we have an independent ancestor
-        if upstreamNode is original then isOwnGrandpa = true
+        if upstreamNode is original then isOwnAncestor = true
+        if upstreamNode.isAccumulator then hasAccumulatorAncestor = true
         unless _.contains visitedNodes, upstreamNode
           return true if visit upstreamNode

     hasIndependentAncestor = visit @

-    @isInCycle = not hasIndependentAncestor and isOwnGrandpa
+    @isInCycle = not hasIndependentAncestor and isOwnAncestor
+    @hasAccumulatorAncestor = hasAccumulatorAncestor

   infoString: ->
     linkNamer = (link) ->
@@ -220,7 +224,7 @@
       frames: _.clone @frames

   canEditInitialValue: ->
-    not @isDependent(true) or @isAccumulator or @isInCycle
+    not @isDependent(true) or @isAccumulator or (@isInCycle and not @hasAccumulatorAncestor)

   canEditValueWhileRunning: ->
     not @isDependent(true)


A bit less spam.

A bit less spam.

--- src/coffee/pricesync.coffee
+++ src/coffee/pricesync.coffee
@@ -76,6 +76,8 @@
         version: variantDataInMaster.productVersion
         variantId: variantDataInMaster.productId
         actions: actions
+
+      console.error "ACTIONS %j", data

       @masterClient.products.byId(variantDataInMaster.productId).save(data)
       .then ->
@@ -140,7 +142,7 @@
             variants = [product.masterVariant].concat(product.variants)
             match = _.find variants, (v) ->
               v.sku is masterSku
-            if match
+            if match?
               data =
                 productId: product.id
                 productVersion: product.version
@@ -152,12 +154,6 @@
     deferred.promise

   _filterPrices: (retailerVariant, variantInMaster, retailerCustomerGroup, masterCustomerGroup, retailerChannel) ->
-    console.error "f retailerVariant %j", retailerVariant
-    console.error "f variantInMaster %j", variantInMaster
-    console.error "f retailerCustomerGroup %j", retailerCustomerGroup
-    console.error "f masterCustomerGroup %j", masterCustomerGroup
-    console.error "f retailerChannel %j", retailerChannel
-
     retailerPrices = _.select retailerVariant.prices, (price) ->
       not _.has(price, 'customerGroup') or price.customerGroup.id is retailerCustomerGroup.id

@@ -171,7 +167,6 @@
       retailerPrices: retailerPrices
       masterPrices: masterPrices

-    console.error "f data %j", data
     data

@@ -198,7 +193,7 @@
         price.channel =
           typeId: 'channel'
           id: channelId
-        # If the price has a customerGroup set, we have to update the id with the one from master
+        # when the price has a customerGroup set, we have to update the id with the one from master
         if _.has price, 'customerGroup'
           price.customerGroup.id = masterCustomerGroupId

@@ -215,20 +210,19 @@
         console.error "SKU #{variantInMaster.sku}: There are NO #{priceType} prices at all."

     action = syncAmountOrCreate(@_normalPrice(retailerPrices), @_normalPrice(masterPrices))
-    if action
+    if action?
       #actions.push action
       liveAction = _.clone action
       liveAction.staged = false
       actions.push liveAction

     action = syncAmountOrCreate(@_salesPrice(retailerPrices, retailerCustomerGroupId), @_salesPrice(masterPrices, masterCustomerGroupId), CUSTOMER_GROUP_SALE)
-    if action
+    if action?
       #actions.push action
       liveAction = _.clone action
       liveAction.staged = false
       actions.push liveAction

-    console.error "ACTIONS %j", actions
     actions

   _normalPrice: (prices) ->


Move comment above module.exports

Move comment above module.exports

Make it parseable by donna

Refs atom/donna#2

--- src/command-registry.coffee
+++ src/command-registry.coffee
@@ -5,8 +5,6 @@

 SequenceCount = 0
 SpecificityCache = {}
-
-module.exports =

 # Public: Associates listener functions with commands in a
 # context-sensitive way using CSS selectors. You can access a global instance of
@@ -42,6 +40,7 @@
 #     # editor = @getModel()
 #     editor.insertText(new Date().toLocaleString())
 # ```
+module.exports =
 class CommandRegistry
   constructor: (@rootNode) ->
     @registeredCommands = {}


Increasing limit

Increasing limit

--- twitch.coffee
+++ twitch.coffee
@@ -33,7 +33,7 @@
     robot.respond /ttv follows/i, (msg) ->
         user = msg.message.user.name.toLowerCase()
         if twitchUser = GetTTVData()[user]
-            GetTwitchResult msg, "/users/#{twitchUser}/follows/channels", null, (followsObj) ->
+            GetTwitchResult msg, "/users/#{twitchUser}/follows/channels", { limit: 100 }, (followsObj) ->
                 if followsObj._total is 0 or followsObj.status is 404
                     msg.reply "Your Twitch account is not following anyone or it does not exist."
                     return


cross browser say to set cursor to end of textbox

--- src/modules/link-tooltip.coffee
+++ src/modules/link-tooltip.coffee
@@ -3,8 +3,9 @@

 enterEditMode = (url) ->
   url = 'http://' + url unless /^http[s]?:\/\//.test(url)
+  Scribe.DOM.addClass(@tooltip, 'editing')
+  @tooltipInput.focus()
   @tooltipInput.value = url
-  Scribe.DOM.addClass(@tooltip, 'editing')

 exitEditMode = ->
   if @tooltipLink.innerText != @tooltipInput.value
@@ -48,7 +49,6 @@
         enterEditMode.call(this, url)
         Scribe.DOM.addClass(@tooltip, 'editing')
         showTooptip.call(this, @editor.selection.getDimensions())
-        @tooltipInput.focus()
     if value?
       @editor.selection.format('link', value, { source: 'user' })
       @toolbar.emit(Scribe.Toolbar.events.FORMAT, 'link', value)


unit test for sync conflict

unit test for sync conflict

--- services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
+++ services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
@@ -103,6 +103,25 @@
 			it "should call the callback with a failure statue", ->
 				@callback.calledWith(null, @status).should.equal true

+		describe "with a sync conflict", ->
+			beforeEach ->
+				@ClsiManager.sendRequestOnce = sinon.stub()
+				@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {syncType:"full"}).callsArgWith(3, null,	@status = "success")
+				@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {}).callsArgWith(3, null, "conflict")
+				@ClsiManager.sendRequest @project_id, @user_id, {}, @callback
+
+			it "should call the sendRequestOnce method twice", ->
+				@ClsiManager.sendRequestOnce.calledTwice.should.equal true
+
+			it "should call the sendRequestOnce method once with syncType:full", ->
+				@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {syncType:"full"}).calledOnce.should.equal true
+
+			it "should call the sendRequestOnce method once without syncType:full", ->
+				@ClsiManager.sendRequestOnce.withArgs(@project_id, @user_id, {}).calledOnce.should.equal true
+
+			it "should call the callback with a success status", ->
+				@callback.calledWith(null, @status, ).should.equal true
+
 	describe "deleteAuxFiles", ->
 		beforeEach ->
 			@ClsiManager._makeRequest = sinon.stub().callsArg(2)


ComputeProvider: helpers: use default plan if not provided

--- workers/social/lib/social/models/computeproviders/helpers.coffee
+++ workers/social/lib/social/models/computeproviders/helpers.coffee
@@ -61,7 +61,7 @@
 getPlanConfig = (group) ->

   return {
-    plan      : group._activePlan
+    plan      : group._activePlan ? 'default'
     overrides : group.getAt 'config.planOverrides'
   }


ACLization of PBD object

ACLization of PBD object

--- src/api/pbd.coffee
+++ src/api/pbd.coffee
@@ -3,54 +3,52 @@
 {$coroutine, $wait} = require '../fibers-utils'

 #=====================================================================
+# Delete

-exports.delete = $coroutine ({id}) ->
-  try
-    PBD = @getObject id, 'PBD'
-  catch
-    @throw 'NO_SUCH_OBJECT'
-
+exports.delete = $coroutine ({PBD}) ->
   xapi = @getXAPI PBD

   # TODO: check if PBD is attached before
   $wait xapi.call 'PBD.destroy', PBD.ref

   return true
-exports.delete.permission = 'admin'
 exports.delete.params = {
   id: { type: 'string' }
 }
+exports.delete.resolve = {
+  PBD: ['id', 'PBD']
+}

-exports.disconnect = $coroutine ({id}) ->
-  try
-    PBD = @getObject id, 'PBD'
-  catch
-    @throw 'NO_SUCH_OBJECT'
+#=====================================================================
+# Disconnect

+exports.disconnect = $coroutine ({PBD}) ->
   xapi = @getXAPI PBD

   # TODO: check if PBD is attached before
   $wait xapi.call 'PBD.unplug', PBD.ref

   return true
-exports.disconnect.permission = 'admin'
 exports.disconnect.params = {
   id: { type: 'string' }
 }
+exports.disconnect.resolve = {
+  PBD: ['id', 'PBD']
+}

-exports.connect = $coroutine ({id}) ->
-  try
-    PBD = @getObject id, 'PBD'
-  catch
-    @throw 'NO_SUCH_OBJECT'
+#=====================================================================
+# Connect

+exports.connect = $coroutine ({PBD}) ->
   xapi = @getXAPI PBD

   # TODO: check if PBD is attached before
   $wait xapi.call 'PBD.plug', PBD.ref

   return true
-exports.disconnect.permission = 'admin'
-exports.disconnect.params = {
+exports.connect.params = {
   id: { type: 'string' }
 }
+exports.connect.resolve = {
+  PBD: ['id', 'PBD']
+}


Fix vif.connect().

Fix vif.connect().

--- src/api/vif.coffee
+++ src/api/vif.coffee
@@ -50,12 +50,12 @@

   return true

-disconnect.params = {
+connect.params = {
   id: { type: 'string' }
 }

-disconnect.resolve = {
+connect.resolve = {
   vif: ['id', 'VIF']
 }

-exports.disconnect = disconnect
+exports.connect = connect


:lipstick: Use hard newline

:lipstick: Use hard newline

--- src/apm-cli.coffee
+++ src/apm-cli.coffee
@@ -39,7 +39,8 @@

     Usage: apm <command>

-    where <command> is one of:\n
+    where <command> is one of:
+
   """
   usage += wordwrap(4, 80)(Object.keys(commands).sort().join(', '))
   usage += ".\n\nRun `apm help <command>` to see the more details about a specific command."


BO don't add candidate w/o ids

BO don't add candidate w/o ids

--- app/assets/javascripts/backbone/models/election.js.coffee
+++ app/assets/javascripts/backbone/models/election.js.coffee
@@ -70,6 +70,8 @@
   addCandidate: (candidate) ->
     election = @
     data = {candidateIds: candidate.id}
+    unless candidate.id
+      return 0
     $.ajax
       type: 'POST'
       url: "#{@url()}/addcandidacy"


Audio sample rate dynamic adjustment.

Audio sample rate dynamic adjustment.

--- emulator/core/units/apu.coffee
+++ emulator/core/units/apu.coffee
@@ -237,10 +237,11 @@
         @recordingBuffer = new Float32Array bufferSize # Audio samples which are curreltly being recorded
         @recordingPosition = 0
         @recordingCycle = 0
-        @outputBuffer = new Float32Array bufferSize    # Cached audio samples, ready for output to sound card
-        @outputBufferAvailable = false
-        @sampleRate = sampleRate
-        @sampleRateAdjustment = 0
+        @outputBuffer = new Float32Array bufferSize # Cached audio samples, ready for output to sound card
+        @outputBufferAvailable = false              # True when output buffer is ready
+        @sampleRate = sampleRate                    # How often are samples taken (samples per second)
+        @bufferUnderflowLimit = bufferSize  / 3     # When we need to increase sample rate (buffer underflow protection)
+        @bufferOverflowLimit = bufferSize * 2 / 3   # When we need to decrease sample rate (buffer overflow protection)

     startRecording: ->
         throw "Cannot start audio recording without initialization" unless @recordingBuffer
@@ -251,18 +252,18 @@

     recordOutputValue: ->
         if @recordingActive
-            newRecordingPosition = ~~(@recordingCycle++ * (@sampleRate + @sampleRateAdjustment) / @cpuFrequency)
+            newRecordingPosition = ~~(@recordingCycle++ * @sampleRate / @cpuFrequency)
             if newRecordingPosition > @recordingPosition
                 @recordingPosition = newRecordingPosition
                 @recordingBuffer[@recordingPosition] = @getOutputValue()
-                if @outputBufferAvailable
-                    limit = @recordingBuffer.length * 3 / 2
-                    @sampleRateAdjustment = @sampleRate * (@recordingBuffer.length - @recordingPosition) / (@recordingBuffer.length - limit) if @recordingPosition > limit
-                if @recordingPosition >= @recordingBuffer.length - 1
+                if @outputBufferAvailable and @recordingPosition > @bufferOverflowLimit
+                    @sampleRate -= 0.1 if @sampleRate > 1             # buffer overflow protection
+                else if not @outputBufferAvailable or @recordingPosition < @bufferUnderflowLimit
+                    @sampleRate += 0.1 if @sampleRate < @cpuFrequency # buffer underflow protection
+                if @recordingPosition >= @recordingBuffer.length - 1 and not @outputBufferAvailable
                     @swapOutputBuffer()
                     @recordingPosition = 0
                     @recordingCycle = 0
-        # TODO handle buffer overflow by adjusting sampleRate

     readOutputBuffer: ->
         @completeOutputBuffer() unless @outputBufferAvailable # Buffer overflow
@@ -270,15 +271,12 @@
         @outputBuffer

     completeOutputBuffer: ->
-        logger.warn "Audio buffer underflow"
         while @recordingPosition < @recordingBuffer.length
             @recordingBuffer[@recordingPosition++] = 0
         @swapOutputBuffer()

     swapOutputBuffer: ->
-        logger.warn "Audio buffer overflow" if @outputBufferAvailable
         [ @recordingBuffer, @outputBuffer ] = [ @outputBuffer, @recordingBuffer ]
         @outputBufferAvailable = true
-        @sampleRateAdjustment = 0

 module.exports = APU


Cleaned up muffin install and removed muffin update. Muffin update doesn't do anything extra than muffin install.

--- src/command.coffee
+++ src/command.coffee
@@ -41,8 +41,6 @@
     Package management:
       * muffin install <package-name> (install a Muffin package and save in config.json)
       * muffin install (install all packages listed in config.json)
-      * muffin update <package-name> (update a Muffin package and save in config.json)
-      * muffin update  (update all packages listed in config.json)

     Watch mode:
       * muffin watch (watch the client files and recompile as needed)
@@ -247,29 +245,27 @@

   pkgs = opts.arguments[1..]
   if pkgs.length > 0
-    # install the packages
+    # Install the packages
     for pkg in pkgs
       [repo, version] = pkg.split('@')
       pkgmgr.install repo, version

-    # save to config.json
-    config = project.clientConfig
+    # Save to `client/config.json`
+    path = sysPath.join(project.clientDir, 'config.json')
+    config = JSON.parse(fs.readFileSync(path))
     config.dependencies ?= {}
     for pkg in pkgs
       [repo, version] = pkg.split('@')
       config.dependencies[repo] = version ? '*'
-    fs.writeFileSync(sysPath.join(project.clientDir, 'config.json'), JSON.stringify(config, null, 2))
+    fs.writeFileSync(path, JSON.stringify(config, null, 2))
   else
-    # install all dependencies listed in config.json
+    # Install all dependencies listed in `client/config.json`
     for repo, version of project.clientConfig.dependencies
       pkgmgr.install repo, version

     # Call `npm install` in the serverDir if using the Node.js stack
     if project.config.serverType is 'nodejs'
       spawn 'npm', ['install'], {cwd: project.serverDir, stdio: 'inherit'}
-
-# Task - update packages
-task 'update', 'update packages', ->

 # Common subtask: build
 build = (done) ->


Refactor, remove format functions that are not used

--- bootstrap-magic.coffee
+++ bootstrap-magic.coffee
@@ -31,10 +31,6 @@
   reactive.overrides.get(@key) || reactive.defaults.get(@key)

 #Something for Everyone
-format = (str, del) -> str.replace(/\s+/g, del || '-').toLowerCase()
-formatCamel = (str) ->
-  str = str.replace(/([^A-Za-z0-9\.\$])|([A-Z])(?=[A-Z][a-z])|([^\-\$\.0-9])(?=\$?[0-9]+(?:\.[0-9]+)?)|([0-9])(?=[^\.0-9])|([a-z])(?=[A-Z])/g, '$2$3$4$5 ')
-  str.charAt(0).toUpperCase() + str.slice(1)
 camelToSnake = (str) -> str.replace(/\W+/g, '_').replace(/([a-z\d])([A-Z])/g, '$1-$2')

 @currentSubCat = new ReactiveVar()
@@ -97,7 +93,6 @@
   "subCategories" : ->  _.map objByCat(), (obj) -> obj
   "previewTmpl" : -> Template["bootstrap_magic_preview_#{camelToSnake @keyName}"] || null
   "inputTmpl" : -> Template["bootstrap_magic_input_#{@type}"] || null
-  "format" : (str) -> formatCamel(str)
   "typeIs" : (type) -> @type is type
   "currentSubCat" : getCurrentSubCat
   "isSelectedCat" : -> @category is currentCat.get()


Update app.Routes to new este.Routes. Simply!

--- app/client/js/routes.coffee
+++ app/client/js/routes.coffee
@@ -5,34 +5,14 @@
 class app.Routes extends este.Routes

   ###*
+    @param {app.Storage} storage
     @constructor
     @extends {este.Routes}
   ###
-  constructor: ->
+  constructor: (@storage) ->
     super()
-    @home = new este.Route '/', Routes.MSG_HOME
-    @newSong = new este.Route '/@me/songs/new', Routes.MSG_NEW_SONG
-    @song = new este.Route '/@me/songs/:urlArtist/:urlName', 'Routes.MSG_NEW_SONG'
-    @notFound = new este.Route '*'

-    @list = [
-      @home
-      @newSong
-      @song
-      @notFound
-    ]
-
-  ###*
-    @desc app.Routes
-  ###
-  @MSG_HOME: goog.getMsg 'Songary | Your personal songbook'
-
-  ###*
-    @desc app.Routes
-  ###
-  @MSG_NEW_SONG: goog.getMsg 'New Song | Songary'
-
-  # ###*
-  #   @desc app.Routes
-  # ###
-  # @MSG_SONG: goog.getMsg '{$songName} - {$artist} | Songary'+    @home = @route '/'
+    @newSong = @route '/@me/songs/new'
+    @song = @route '/@me/songs/:urlArtist/:urlName'
+    @notFound = @route '*'


FIX: Enhance error message with details for task dates

--- src/app/tasks/task-definition-editor/task-definition-editor.coffee
+++ src/app/tasks/task-definition-editor/task-definition-editor.coffee
@@ -170,7 +170,7 @@
         task.group_set_id = -1

       if (Date.parse(task.start_date) > Date.parse(task.target_date)) || (Date.parse(task.target_date) > Date.parse(task.due_date))
-        alertService.add("danger", "Invalid task dates, unit not saved", 5000)
+        alertService.add("danger", "Invalid task dates, unit not saved. Ensure start date is before due date, and due date is before deadline.", 5000)
       else
         if task.target_date && task.target_date.getMonth
           tgt = task.target_date


remove unused var

remove unused var

--- src/context.coffee
+++ src/context.coffee
@@ -164,7 +164,7 @@
           $item.attr 'data-async-status', "failed in #{duration()}"
         promise

-    run_context.fns = fns = bind_context_fns run_context, run_context.context_fns
+    run_context.fns = bind_context_fns run_context, run_context.context_fns
     run_context.current_context = run_context
     run_context.root_context = run_context


reset cleanup

reset cleanup

--- src/control.coffee
+++ src/control.coffee
@@ -5,8 +5,7 @@
   draw_button = (label, dir, delta) ->
     T.button {
       class: ["control", "dir", label]
-      click: ->
-        me.reset(dir, delta)
+      click: -> me.reset(dir, delta)
     }, label
   T.div {
     class: "controls"


Bug fix: https://github.com/kmalakoff/knockback/issues/159

--- src/core/kb.coffee
+++ src/core/kb.coffee
@@ -151,7 +151,8 @@
     if el.childNodes.length is 1 # do not return the template wrapper if possible
       el = el.childNodes[0]
     else if el.childNodes.length
-      ko.storedBindingContextForNode(el, ko.contextFor(el.childNodes[0])) # ensure the context is passed up to wrapper
+      for i in [0..el.childNodes.length] # ensure the context is passed up to wrapper from a child
+        try ko.storedBindingContextForNode(el, ko.contextFor(el.childNodes[i])); break catch;
     kb.releaseOnNodeRemove(view_model, el)
     observable.dispose() # we will handle memory management with ko.removeNode (otherwise creates memory leak on default bound dispose function)


use Fs.mkdirSync to prevent ENOENT on hubot --create

--- src/creator.coffee
+++ src/creator.coffee
@@ -17,8 +17,7 @@
   mkdirDashP: (path) ->
     Fs.exists path, (exists) ->
       unless exists
-        Fs.mkdir path, 0o0755, (err) ->
-          throw err if err
+        Fs.mkdirSync path, 0o0755

   # Copy the contents of a file from one place to another.
   #


re-implement run based off decking.json metadata with support for container dependencies

--- src/decking.coffee
+++ src/decking.coffee
@@ -54,22 +54,64 @@
       done()

   run: (done) ->
-    args = @commands.run.split " "
+    # @TODO use the remote API when it supports -name and -link
+    # @TODO check the target image exists locally, otherwise
+    # `docker run` will try to download it. we want to take care
+    # of dependency resolution ourselves
+    [container] = @args
+    @_run container, done

-    dr = child_process.spawn "docker", args.slice 1
+  _run: (container, done) ->

-    dr.stdout.on "data", (d) -> process.stdout.write d
-    dr.stderr.on "data", (d) -> process.stderr.write d
+    throw new Error("Please supply a container to run") if not container

-    dr.on "exit", (code) -> done()
+    target = @config.containers[container]

+    throw new Error("Container #{container} does not exist in decking.json") if not target
+
+    cmdArgs = ["run", "-d", "-name", "#{container}"]
+    cmdArgs = [].concat cmdArgs, @getRunArg key, val for key,val of target
+
+    doRun = =>
+      cmdArgs.push target.image
+
+      stream = child_process.spawn "docker", cmdArgs
+      stream.stdout.pipe process.stdout
+      stream.stderr.pipe process.stderr
+      stream.on "exit", done
+
+    depLength = target.dependencies?.length || 0
+
+    if depLength
+      for dependency in target.dependencies
+        [container, alias] = dependency.split ":"
+        @log "Resolving dependency on #{container}"
+        cmdArgs.push "-link"
+        cmdArgs.push dependency
+        do =>
+          @_run container, =>
+            depLength -= 1
+            if depLength is 0
+              doRun()
+    else
+      doRun()
+
+  getRunArg: (key, val) ->
+    arg = []
+    switch key
+      when "port"
+        arg = ["-p", "#{val[0]}"]
+      when "env"
+        arg = [].concat arg, ["-e", "#{k}=#{v}"] for k,v of val
+
+    return arg

   execute: (done) ->
     fn = this[@command]

     throw new Error("Invalid arg") if typeof fn isnt "function"

-    return fn.call this, (err) =>
+    return fn.call this, (err) => console.log "DONE", "ERR?", err

     ###
     @prepare (err) =>


ludicrously ham-fisted attempt at dependency resolution and cluster start behaviour

--- src/decking.coffee
+++ src/decking.coffee
@@ -16,11 +16,26 @@

     @config = @loadConfig "./decking.json"

+  processConfig: (config) ->
+    for name, details of config.containers
+      if typeof details is "string"
+        details = config.containers[name] =
+          image: details
+
+      details.dependencies = [] if not details.dependencies
+      details.aliases = []
+      for dependency,i in details.dependencies
+        [name, alias] = dependency.split ":"
+        details.dependencies[i] = name
+        details.aliases[i] = alias
+
+    return config
+
   parseConfig: (data) -> JSON.parse data

   loadConfig: (file) ->
     log "Loading package file..."
-    @parseConfig fs.readFileSync file
+    @processConfig @parseConfig fs.readFileSync file

   commands:
     build: (done) ->
@@ -44,8 +59,8 @@
       @create container, done

     start: (done) ->
-      [container] = @args
-      @start container, done
+      [cluster] = @args
+      @start cluster, done

     stop: (done) ->
       [container] = @args
@@ -62,15 +77,29 @@
         process.stdout.write stdout
         done()

-  start: (container, done) ->
-    throw new Error("Please supply a container to start") if not container
-
-    target = @config.containers[container]
-
-    throw new Error("Container #{container} does not exist in decking.json") if not target
-
-    container = docker.getContainer container
-    container.start done
+  start: (cluster, done) ->
+    throw new Error "Please supply a cluster to start" if not cluster
+
+    target = @config.clusters[cluster]
+
+    throw new Error "Cluster #{cluster} does not exist in decking.json"  if not target
+
+    log "Resolving dependencies for cluster #{cluster}"
+
+    resolveOrder @config, target, (list) ->
+
+      async.eachSeries list, (details, callback) ->
+        name = details.name
+        container = docker.getContainer name
+        container.inspect (err, data) ->
+          if not data.State.Running
+            log "Starting container #{name}"
+            container.start callback
+          else
+            log "Container #{name} already running..."
+            callback null
+      , done
+

   stop: (container, done) ->
     throw new Error("Please supply a container to stop") if not container
@@ -206,3 +235,42 @@
     return fn.call this, (err) -> throw err if err

 module.exports = Decking
+
+resolveOrder = (config, cluster, callback) ->
+  containerDetails = {}
+
+  for containerName in cluster
+    container = config.containers[containerName]
+    containerDetails[containerName] = container
+
+    for dependency in container.dependencies
+      [dep] = dependency.split ":"
+      if not containerDetails[dep]
+        containerDetails[dep] = config.containers[dep]
+
+
+  # at this point we have a full map of the containers involved in this
+  # cluster, albeit not necessarily in dependency order
+
+  deps = []
+  for name, details of containerDetails
+    for dep in details.dependencies
+      if deps.indexOf(dep) is -1
+        deps.push dep
+
+  orderedContainers = {}
+  for dep in deps
+    details = containerDetails[dep]
+    orderedContainers[dep] = details
+
+  for name, details of containerDetails
+    if not orderedContainers[name]
+      orderedContainers[name] = details
+
+
+  list = []
+  for name, details of orderedContainers
+    details.name = name
+    list.push details
+
+  callback list


make 2 letter account names valid

make 2 letter account names valid

--- dl/src/common/validation.coffee
+++ dl/src/common/validation.coffee
@@ -11,7 +11,7 @@
     is_account_name: (value)->
         return no if is_empty value
         length = value.length
-        return no if length < 3 or length > 63
+        return no if length < 2 or length > 63
         for label in value.split('.')
             if !(
                 # Starts with a letter, has only letters, digits, and dashes


Record allEnabled state on players

Record allEnabled state on players

--- src/server/room_controls.coffee
+++ src/server/room_controls.coffee
@@ -89,6 +89,8 @@

 # Player documents
 #
+#   allEnabled {Boolean} True if everyone in the room is enabled.
+#
 #   isEnabled {Boolean} If true, the player can use their controller.
 #
 #   isMaster {Boolean} If true, the player can always use their
@@ -122,21 +124,28 @@
 #   roomId {String} The name of the room.

-
 joinRoom = (roomId, playerId, isMaster) ->
   # Enable the player if they are a master or that is the default
   # state.
+  room = Rooms.findOne
+    roomId: roomId
+  ,
+    fields:
+      allEnabled: true
+  allEnabled = room?.allEnabled ? false
+
   isEnabled = if isMaster or ServerSettings.players.enableOnJoin
     true
   else
     # The joining player should be enabled if all players in the room
     # are enabled.
-    Rooms.findOne(roomId: roomId, allEnabled: true)?
+    allEnabled

   Players.upsert
     playerId: playerId
   ,
     $set:
+      allEnabled: allEnabled
       isEnabled: isEnabled
       isMaster: isMaster
       name: generateName()
@@ -219,7 +228,7 @@
         isEnabled: true
         singledAt: Date.now()

-    # Update the room message.
+    # Update the room.
     Rooms.upsert
       roomId: roomId
     ,
@@ -227,6 +236,7 @@
         allEnabled: false
         enabledPlayerId: player.playerId
         message: player.name
+
   else
     # Show a message indicating the were no players to enable.
     Rooms.upsert
@@ -238,14 +248,22 @@
       $unset:
         enabledPlayerId: ''

+  Players.update
+    allEnabled: true
+    roomId: roomId
+  ,
+    $set:
+      allEnabled: false
+  ,
+    multi: true
+

 enableAllPlayers = (roomId) ->
   Players.update
-    isEnabled: false
-    isMaster: false
-    roomId: roomId
-  ,
-    $set:
+    roomId: roomId
+  ,
+    $set:
+      allEnabled: true
       isEnabled: true
   ,
     multi: true
@@ -271,6 +289,15 @@
   ,
     multi: true

+  Players.update
+    allEnabled: true
+    roomId: roomId
+  ,
+    $set:
+      allEnabled: false
+  ,
+    multi: true
+
   Rooms.upsert
     roomId: roomId
   ,


fix more stopwatch icons

fix more stopwatch icons

--- src/smc-webapp/project_new.cjsx
+++ src/smc-webapp/project_new.cjsx
@@ -191,9 +191,9 @@
                         tip='Create a todo list to keep track of everything you are doing on a project.  Put #hashtags in the item descriptions and set due dates.'>
                         <NewFileButton icon='tasks' name='Task list' on_click={@props.create_file} ext='tasks' />
                     </Tip>
-                    <Tip title='Stopwatch'   icon='clock-o'
+                    <Tip title='Stopwatch'   icon='stopwatch'
                         tip='Create a collaborative stopwatch to keep track how long it takes to do something.'>
-                        <NewFileButton icon='clock-o' name='Stopwatch' on_click={@props.create_file} ext='time' />
+                        <NewFileButton icon='stopwatch' name='Stopwatch' on_click={@props.create_file} ext='time' />
                     </Tip>
                 </Col>
                 <Col sm={6}>


redux: better store cleanup code

redux: better store cleanup code

--- src/smc-webapp/smc-react.coffee
+++ src/smc-webapp/smc-react.coffee
@@ -63,7 +63,6 @@
     destroy: =>
         @removeAllListeners()
         @redux.removeStore(@name)
-        @setState(undefined)

     getState: =>
         return @redux._redux_store.getState().get(@name)
@@ -101,12 +100,19 @@
         type   : 'SET_STATE'
         change : change

+action_remove_store = (name) ->
+    action =
+        type : 'REMOVE_STORE'
+        name : name
+
 redux_app = (state, action) ->
     if not state?
         return immutable.Map()
     switch action.type
         when 'SET_STATE'
             return state.mergeDeep(action.change)
+        when 'REMOVE_STORE'
+            return state.delete(action.name)
         else
             return state

@@ -173,6 +179,7 @@
             S = @_stores[name]
             delete @_stores[name]
             S.destroy()
+            @_redux_store.dispatch(action_remove_store(name))

     removeActions: (name) =>
         if @_actions[name]?


Not needed

Not needed

--- src/emitter.coffee
+++ src/emitter.coffee
@@ -7,13 +7,6 @@
     @_callbacks[event] or= []
     @_callbacks[event].push fn
     this
-
-  once: (event, fn) ->
-    once = =>
-      @off event, once
-      fn.apply this, arguments
-    fn._off = once
-    @on event, once

   off: (event, fn) ->
     callbacks = @_callbacks[event]


Request scrollView measurement on resize events

--- src/editor-component.coffee
+++ src/editor-component.coffee
@@ -257,10 +257,10 @@
     @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged
     @subscribe editor, 'selection-added', @onSelectionAdded
     @subscribe editor, 'decoration-changed', @onDecorationChanged
+    @subscribe editor.$scrollTop.changes, @onScrollTopChanged
+    @subscribe editor.$scrollLeft.changes, @requestUpdate
     @subscribe editor.$height.changes, @requestUpdate
     @subscribe editor.$width.changes, @requestUpdate
-    @subscribe editor.$scrollTop.changes, @onScrollTopChanged
-    @subscribe editor.$scrollLeft.changes, @requestUpdate
     @subscribe editor.$defaultCharWidth.changes, @requestUpdate
     @subscribe editor.$lineHeightInPixels.changes, @requestUpdate

@@ -271,6 +271,7 @@

     scrollViewNode = @refs.scrollView.getDOMNode()
     scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
+    window.addEventListener 'resize', @requestScrollViewMeasurement

   listenForCommands: ->
     {parentView, editor, mini} = @props


log calls to site pages

log calls to site pages

--- routes/siteRoutes.coffee
+++ routes/siteRoutes.coffee
@@ -1,23 +1,30 @@
+{Logger} = require './util/logger'
+
 home = (req, res) ->
 	viewModel = {}
+  Logger.info "siteRoutes:home"
 	res.render 'home', viewModel

 about = (req, res) ->
 	viewModel = { title: "About Hector"}
+  Logger.info "siteRoutes:home"
 	res.render 'about', viewModel

 credits = (req, res) ->
   viewModel = { title: "Credits"}
+  Logger.info "siteRoutes:home"
   res.render 'credits', viewModel

 notFound = (req, res) ->
+  Logger.info "siteRoutes:notFound"
   res.render '404.ejs', { status: 404, message: 'Page not found' }

 blowUp = (req, res) ->
+  Logger.info "siteRoutes:blowUp (intentional)"
   a = b.x
   viewModel = { title: "About Hector"}
   res.render 'about', viewModel


prevent watch crash after npm install w/ custom config.paths

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -214,7 +214,7 @@

   paths.assets        ?= join('app', 'assets')

-  paths.config         = configPath       ? joinRoot 'config'
+  paths.config        ?= configPath       ? joinRoot 'config'
   paths.packageConfig ?= joinRoot 'package.json'

   conventions          = config.conventions  ?= {}


Prefer event.keyCode to event.which, and a few other suggested changes.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -20,10 +20,10 @@

 exports.keystrokeForKeyboardEvent = (event) ->
   unless KeyboardEventModifiers.has(event.keyIdentifier)
-    keyIsAscii = event.keyIdentifier.indexOf('U+') is 0
-    if isAscii(event.which) and keyIsAscii
-      key = keyFromCharCode(event.which)
-    else if keyIsAscii
+    keyIdentifierIsHexCharCode = event.keyIdentifier.indexOf('U+') is 0
+    if isAscii(event.keyCode) and keyIdentifierIsHexCharCode
+      key = keyFromCharCode(event.keyCode)
+    else if keyIdentifierIsHexCharCode
       hexCharCode = event.keyIdentifier[2..]
       charCode = charCodeFromHexCharCode(hexCharCode)
       key = keyFromCharCode(charCode)
@@ -79,9 +79,10 @@
   location = KeyboardEvent.DOM_KEY_LOCATION_STANDARD
   event.initKeyboardEvent('keydown', bubbles, cancelable, view,  keyIdentifier, location, ctrl, alt, shift, cmd)
   Object.defineProperty(event, 'target', get: -> target) if target?
-  if which or event.which is 0
+  if which? or event.which is 0
     # 0 is the default, and it's valid Ascii, but it's wrong.
     Object.defineProperty(event, 'which', get: -> which)
+    Object.defineProperty(event, 'keyCode', get: -> which)
   event

 normalizeKeystroke = (keystroke) ->


fix a merge error that messed up starting bup server

--- salvus/bup_server.coffee
+++ salvus/bup_server.coffee
@@ -2048,12 +2048,3 @@
 if program._name == 'bup_server.js'
     main()

-
-    process.addListener "uncaughtException", (err) ->
-        winston.error("Uncaught exception: #{err}")
-    daemon({pidFile:program.pidfile, outFile:program.logfile, errFile:program.logfile}, start_server)
-
-if program._name == 'bup_server.js'
-    main()
-
-


make credit card appear above subscription

--- salvus/page/billing.cjsx
+++ salvus/page/billing.cjsx
@@ -337,7 +337,7 @@
     render : ->
         <Row>
             <Col sm=6 smOffset=3>
-                <Well style={boxShadow:"5px 5px 5px lightgray", position:'absolute', zIndex:1}>
+                <Well style={boxShadow:"5px 5px 5px lightgray", position:'absolute', zIndex:2}>
                     {@render_error()}
                     {@render_payment_method_fields()}
                     {@render_payment_method_buttons()}


stop using js keyword "interface"; replace with "iface"

* breaks YUI compressor when trying to produce minified JS from the dev dist

--- src/synapse/hooks/jquery.coffee
+++ src/synapse/hooks/jquery.coffee
@@ -30,14 +30,14 @@
         get: (object, name, args...) ->
             [name, key] = name.split '.'
             if key? then args = [key].concat(args)
-            if (interface = @registry[name])
-                return interface.get.apply(object, args)
+            if (iface = @registry[name])
+                return iface.get.apply(object, args)

         set: (object, name, args...) ->
             [name, key] = name.split '.'
             if key? then args = [key].concat(args)
-            if (interface = @registry[name])
-                return interface.set.apply(object, args)
+            if (iface = @registry[name])
+                return iface.set.apply(object, args)

     # ### Built-In interfaces
     # Each setter and getter for compound interfaces are defined up front
@@ -271,8 +271,8 @@

         detectInterface: (object) ->
             for item in elementInterfaces
-                [selector, interface] = item
-                if object.is(selector) then return interface
+                [selector, iface] = item
+                if object.is(selector) then return iface
             return 'text'

         detectOtherInterface: (object) ->


fix commandline parameter options

fix commandline parameter options

--- src/git-timetracking.coffee
+++ src/git-timetracking.coffee
@@ -27,8 +27,8 @@
 	.option("-g, --group [regexp]", "group commit times by regexp", null)
 	.option("-u, --user <email>", "user email adress to filter")
 	#.option("-t, --time [time]", "git log since compatible time")
-	.option("-p, --pause <pause>", "max pause time in minutes (default: #{defaultPauseTime})", parsePauseTime)
-	.option("-i, --init <init>", "init time in minutes (default: #{defaultInitTime})", parseInitTime)
+	.option("-p, --pause [pause]", "max pause time in minutes (default: #{defaultPauseTime})", parsePauseTime)
+	.option("-i, --init [init]", "init time in minutes (default: #{defaultInitTime})", parseInitTime)

 zeroPadding = (str) ->
 	str = str.toString()
@@ -154,8 +154,8 @@
 		if not program.init?
 			program.init = defaultInitTime
 		if not program.user?
-			program.prompt 'Email: ', (email) ->
-				start email, path
+			console.log "Need user email address"
+			process.exit()
 		else
 			start program.user, path


Really fix exception handling for natives.

--- src/methods.coffee
+++ src/methods.coffee
@@ -775,8 +775,9 @@
       # this case, pop the stack anyway but don't push a return value.
       # YieldExceptions should just terminate the function without popping the
       # stack.
-      throw e unless e instanceof util.JavaException
-      return rs.meta_stack.pop()
+      if e instanceof util.JavaException
+        rs.meta_stack.pop()
+      throw e
     rs.meta_stack.pop()
     unless @return_type instanceof types.VoidType
       if @return_type.toString() == 'J' then rs.push rv # longs are stored as objects


getClientRects() occasionally returns an empty array in Firefox

--- src/trix/views/text_view.coffee
+++ src/trix/views/text_view.coffee
@@ -174,12 +174,13 @@
     selection = window.getSelection()
     if selection.rangeCount > 0
       rects = selection.getRangeAt(0).getClientRects()
-      rect = rects[rects.length - 1]
-
-      pageX = rect.right
-      pageY = rect.top + rect.height / 2
-
-      [pageX, pageY]
+      if rects.length > 0
+        rect = rects[rects.length - 1]
+
+        pageX = rect.right
+        pageY = rect.top + rect.height / 2
+
+        [pageX, pageY]

   getSelectedRange: ->
     return @lockedRange if @lockedRange


adding /[TM_4_0_Design:Dev](.*)commits/i

--- scripts/TM-Github.coffee
+++ scripts/TM-Github.coffee
@@ -55,7 +55,9 @@
       res.reply 'Request sent, take a look at https://ci.appveyor.com/project/DinisCruz/tm-ci-deploy where a new build will have just started'
       res.reply 'In a bit, the deployed website will be at https://tm-qa-6.azurewebsites.net'

-
+  robot.hear '[TM_4_0_Design:Dev](.*)commits', (res)->
+    res.reply 'New commit to TM_4_0_Design/Dev detected ...'
+    res.reply '...time to deploy to azure'

   robot.hear /show(.*)tm graph/i,(res) ->
     res.reply 'http://4.bp.blogspot.com/-7g82Ltxt7wI/VLqbd1CLOyI/AAAAAAAAKcc/kisGogPKcmo/s1600/Screen%2BShot%2B2014-11-02%2Bat%2B16.49.16.png'


namespaced Movable

namespaced Movable

--- src/movable.coffee
+++ src/movable.coffee
@@ -50,6 +50,5 @@
       if currentSpeed > I.maxSpeed
         I.velocity = I.velocity.scale(I.maxSpeed / currentSpeed)

-    I.x += I.velocity.x *
-    I.y += I.velocity.y
-
+    I.x += I.velocity.x * dt
+    I.y += I.velocity.y * dt


Pause blinking when moving the cursor or interacting with the mouse.

--- src/nvim/ui.coffee
+++ src/nvim/ui.coffee
@@ -22,6 +22,38 @@
   kn += 'A-' if e.altKey
   kn += button + '>'
   kn
+
+# borrowed from underscore@acf826fa42ba6
+debounce = (func, wait, immediate) ->
+  timeout = undefined
+  args = undefined
+  context = undefined
+  timestamp = undefined
+  result = undefined
+
+  later = ->
+    last = Date.now() - timestamp
+    if last < wait and last >= 0
+      timeout = setTimeout(later, wait - last)
+    else
+      timeout = null
+      if !immediate
+        result = func.apply(context, args)
+        if !timeout
+          context = args = null
+    return
+
+  ->
+    context = this
+    args = arguments
+    timestamp = Date.now()
+    callNow = immediate and !timeout
+    if !timeout
+      timeout = setTimeout(later, wait)
+    if callNow
+      result = func.apply(context, args)
+      context = args = null
+    result

 # visualize neovim's abstract-ui
 class UI extends EventEmitter
@@ -73,14 +105,28 @@
     @cursor.style.height = @char_height + 'px'

     if config.blink_cursor
-      @cursor.classList.add('blink')
+      @start_cursor_blink()
     else
-      @cursor.classList.remove('blink')
+      @stop_cursor_blink()
+
+  start_cursor_blink: ->
+    @cursor.classList.add 'blink'
+
+  stop_cursor_blink: ->
+    @cursor.classList.remove 'blink'

   init_event_handlers: ->
+    resumeBlink = debounce (=> @start_cursor_blink()), 250
+    pause_blink = =>
+      @stop_cursor_blink()
+      resumeBlink()
+
     document.addEventListener 'keydown', (e) =>
       keystroke = keystrokeForKeyboardEvent(e)
       @emit 'input', keystroke if keystroke
+
+      if config.blink_cursor
+        pause_blink()

     document.addEventListener 'mousedown', (e) =>
       return if not @mouse_enabled
@@ -92,6 +138,9 @@
           + ',' + Math.floor(e.clientY / @char_height) \
           + '>'

+      if config.blink_cursor
+        pause_blink()
+
     document.addEventListener 'mouseup', (e) =>
       return if not @mouse_enabled
       e.preventDefault()
@@ -106,6 +155,9 @@
           + ',' + Math.floor(e.clientY / @char_height) \
           + '>'

+      if config.blink_cursor
+        pause_blink()
+
     window.addEventListener 'resize', (e) =>
       if not @resize_timer
         @resize_timer = setTimeout =>
@@ -143,6 +195,9 @@

       @emit 'input', get_vim_button_name('ScrollWheel' + direction, e) \
         + '<' + cols + ',' + rows + '>'
+
+      if config.blink_cursor
+        pause_blink()

   get_color_string: (rgb) ->
     bgr = []


Add ability to scroll with the mouse.

Add ability to scroll with the mouse.

Fixes #12.

--- src/nvim/ui.coffee
+++ src/nvim/ui.coffee
@@ -9,6 +9,11 @@
 {keystrokeForKeyboardEvent} = require './key_handler'

 MOUSE_BUTTON_NAME = [ 'Left', 'Middle', 'Right' ]
+
+# neovim/vim scrolls by 3 rows for every ScrollWheel{Up,Down}
+# and 6 colums per ScrollWheel{Left,Right} (:h scroll-mouse-wheel)
+ROWS_PER_SCROLL = 3
+COLS_PER_SCROLL = 6

 get_vim_button_name = (button, e) ->
   kn = '<'
@@ -42,6 +47,8 @@

     @mouse_enabled = true
     @mouse_button_pressed = null
+    @wheelDeltaY = 0
+    @wheelDeltaX = 0

     @fg_color = '#fff'
     @bg_color = '#000'
@@ -103,6 +110,32 @@
             @emit 'resize', col, row

         , 250
+
+    @canvas.addEventListener 'wheel', (e) =>
+      return if not @mouse_enabled
+      e.preventDefault()
+
+      # Total distance scrolled in pixels
+      @wheelDeltaX += e.wheelDeltaX
+      @wheelDeltaY += e.wheelDeltaY
+
+      # Get the number of row/column scroll events to send. Reporting a single
+      # row scrolled will actually scroll by ROWS_PER_SCROLL, so divide accordingly
+      cols = Math.round(@wheelDeltaX / @char_width / COLS_PER_SCROLL)
+      rows = Math.round(@wheelDeltaY / @char_height / ROWS_PER_SCROLL)
+
+      if rows == 0 && cols == 0
+        return
+
+      if cols != 0
+        @wheelDeltaX = 0
+        direction = if cols > 0 then 'Left' else 'Right'
+      if rows != 0
+        @wheelDeltaY = 0
+        direction = if rows > 0 then 'Up' else 'Down'
+
+      @emit 'input', get_vim_button_name('ScrollWheel' + direction, e) \
+        + '<' + cols + ',' + rows + '>'

   get_color_string: (rgb) ->
     bgr = []


don't try to load state unless a state is actually passed

--- app/controllers/Main.coffee
+++ app/controllers/Main.coffee
@@ -22,7 +22,7 @@

   active: (params) ->
     @setup()
-    if typeof params.state is 'string'
+    if typeof params.state is 'string' and params.state.length > 0
       # Load passed state
       @loadState params.state


Allow arrays/objects/nothing as options in ButtonOptionFilter

--- backbone.tableview.coffee
+++ backbone.tableview.coffee
@@ -358,7 +358,17 @@
     initialize: ->
         super
         @options.options = _.map @options.options,
-            (option) => {name: @prettyName(option.name or option), value: option.value or option}
+            (option) =>
+                value = option
+                if _.isArray value
+                    name = value[0]
+                    value = value[1]
+                else if _.isObject value
+                    name = option.name
+                    value = option.value
+                else
+                    name = @prettyName option
+                {name: name, value: value}

     update: (e) =>
         @setData @id, e.currentTarget.value


bruteforce cardinality: better support for latin1 and unicode characters

--- src/scoring.coffee
+++ src/scoring.coffee
@@ -257,7 +257,10 @@
   # utilities --------------------------------------------------------------------

   calc_bruteforce_cardinality: (password) ->
-    [lower, upper, digits, symbols, unicode] = [false, false, false, false, false]
+    [lower, upper, digits, symbols, latin1_symbols, latin1_letters] = (
+      false for i in [0...6]
+    )
+    unicode_codepoints = []
     for chr in password.split('')
       ord = chr.charCodeAt(0)
       if 0x30 <= ord <= 0x39
@@ -268,14 +271,34 @@
         lower = true
       else if ord <= 0x7f
         symbols = true
-      else
-        unicode = true
+      else if 0x80 <= ord <= 0xBF
+        latin1_symbols = true
+      else if 0xC0 <= ord <= 0xFF
+        latin1_letters = true
+      else if ord > 0xFF
+        unicode_codepoints.push ord
     c = 0
     c += 10 if digits
     c += 26 if upper
     c += 26 if lower
     c += 33 if symbols
-    c += 100 if unicode
+    c += 64 if latin1_symbols
+    c += 64 if latin1_letters
+    if unicode_codepoints.length
+      min_cp = max_cp = unicode_codepoints[0]
+      for cp in unicode_codepoints[1..]
+        min_cp = cp if cp < min_cp
+        max_cp = cp if cp > max_cp
+      # if the range between unicode codepoints is small,
+      # assume one extra alphabet is in use (eg cyrillic, korean) and add a ballpark +40
+      #
+      # if the range is large, be very conservative and add +100 instead of the range.
+      # (codepoint distance between chinese chars can be many thousand, for example,
+      # but that cardinality boost won't be justified if the characters are common.)
+      range = max_cp - min_cp + 1
+      range = 40 if range < 40
+      range = 100 if range > 100
+      c += range
     c

   display_time: (seconds) ->


add comment

add comment

--- src/sequent.coffee
+++ src/sequent.coffee
@@ -24,6 +24,8 @@
         @readyLoops = []
         @readyLoopArgs = []
         @isCallbackExecuted = false
+
+        # if true, eliminate warnings for excessive done()
         @quiet = params?.quiet ? false

     rewind: ->


Remove redundant control block

Remove redundant control block

--- client/app/lib/activity/sidebar/sidebarmachinelist.coffee
+++ client/app/lib/activity/sidebar/sidebarmachinelist.coffee
@@ -64,8 +64,6 @@
     @machineBoxes.push machineBox
     @machineBoxesByMachineUId[uid] = machineBox

-    @show() if @machineBoxes.length is 1
-
     machineBox.once 'KDObjectWillBeDestroyed', =>
       @machineBoxes.splice @machineBoxes.indexOf(machineBox), 1
       delete @machineBoxesByMachineUId[machineBox.machine.uid]


remove notr while connecting

remove notr while connecting

The message is instead displayed in screen below logo
 as is updated if connection fails during loading of contacts

--- src/ui/views/controller.coffee
+++ src/ui/views/controller.coffee
@@ -22,7 +22,12 @@
             document.querySelector('.state_contacts').classList.remove("hide")
             el = null
         else
-            el = notr {html:conninfo.el.innerHTML, stay:0, id:'conn'}
+            document.querySelector('.state_connecting').innerHTML = connection.infoText().replace('…','')
+            if document.querySelector('.connecting.hide')?
+                el = notr {html:conninfo.el.innerHTML, stay:0, id:'conn'}
+            else
+                document.querySelector('.state_connecting').classList.remove("hide")
+                document.querySelector('.state_contacts').classList.add("hide")

 setLeftSize = (left) ->


opt: benchmark

opt: benchmark

--- benchmark/template.coffee
+++ benchmark/template.coffee
@@ -31,8 +31,13 @@
 		      memory: #{memFormat.join(' | ')}
 		"""

+	resolver = (resolve) ->
+		setTimeout ->
+			resolve()
+		, 1
+
 	asyncTask = ->
-		new Promise((resolve) -> resolve()).then checkEnd
+		new Promise(resolver).then checkEnd

 	i = countDown

@@ -41,6 +46,6 @@
 	while i--
 		asyncTask()

-	initTime = Date.now() - initStart
+	startResolution = Date.now()
+	initTime = startResolution - initStart

-	startResolution = Date.now()


Chat: Fix editing message

Chat: Fix editing message

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -258,9 +258,8 @@
             else if other_editors.size > 1
                 # Multiple editors
                 text = "#{other_editors.size} people are editing this message"
-
-        if @newest_content() == ''
-            text = "Deleted by #{@props.editor_name}"
+            else if @newest_content() == ''
+                text = "Deleted by #{@props.editor_name}"

         text ?= "Last edit by #{@props.editor_name}"
         color ?= "#888"


removed some launchers

removed some launchers

--- karma.conf.coffee
+++ karma.conf.coffee
@@ -22,7 +22,7 @@
     sauceLabs:
       testName: 'Hammer.js'
       startConnect: false
-      tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER
+      #tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER

     # For more browsers on Sauce Labs see:
@@ -35,12 +35,6 @@
       'SL_Firefox':
         base: 'SauceLabs',
         browserName: 'firefox'
-
-      'SL_Safari':
-        base: 'SauceLabs'
-        browserName: 'safari'
-        platform: 'Mac 10.8'
-        version: '6'

       'SL_IE_9':
         base: 'SauceLabs'
@@ -61,13 +55,6 @@
         version: '4.0'
         'device-orientation': 'portrait'

-      'SL_IOS_5':
-        base: 'SauceLabs'
-        browserName: 'iphone'
-        platform: 'OS X 10.6'
-        version: '5.0'
-        'device-orientation': 'portrait'
-
       'SL_IOS_6':
         base: 'SauceLabs'
         browserName: 'iphone'


Set undo keybinding to meta-y

Set undo keybinding to meta-y

--- keymaps/atom.cson
+++ keymaps/atom.cson
@@ -29,7 +29,7 @@
   'shift-backspace': 'core:backspace'
   'delete': 'core:delete'
   'meta-z': 'core:undo'
-  'meta-Z': 'core:redo'
+  'meta-y': 'core:redo'
   'meta-x': 'core:cut'
   'meta-c': 'core:copy'
   'meta-v': 'core:paste'


port a null usage to question mark

port a null usage to question mark

--- canvas/game.coffee
+++ canvas/game.coffee
@@ -848,7 +848,7 @@
             continue

           dist = atom.shape.body.position.get_dist_sqrd(@point_start)
-          if @closest_atom is null or dist < closest_dist
+          if not @closest_atom? or dist < closest_dist
             @closest_atom = atom
             closest_dist = dist


ManageSharedView: use klientUn/Share with permanent: yes

--- client/Main/managesharedview.coffee
+++ client/Main/managesharedview.coffee
@@ -115,7 +115,7 @@
       kite   = @machine.getBaseKite()
       method = if task is 'add' then 'klientShare' else 'klientUnshare'

-      kite[method] username: nickname
+      kite[method] { username: nickname, permanent: yes }

         .then -> updateUserList()


Uncomment commit model

Uncomment commit model

--- app/js/models/commit.coffee
+++ app/js/models/commit.coffee
@@ -1,2 +1,2 @@
-# class app.Models.Commit extends app.Models.Subject
-#   octicon: 'git-commit'
+class app.Models.Commit extends app.Models.Subject
+  octicon: 'git-commit'


cleaning up the ProofObligationView a bit

--- lib/ProofObligationView.coffee
+++ lib/ProofObligationView.coffee
@@ -3,19 +3,16 @@
 highlighter = require './utils/highlighter'

 class ProofObligationView extends View
-  constructor: (params) ->
+  initialize: (params) ->
     @obligation = params.obligation
     @highlightingInfo = params.highlightingInfo
-    super arguments
-
-  @content: ->
-    @pre class: 'idris-mode inline-block'
-
-  initialize: ->
     if @highlightingInfo?
       text = highlighter.highlight @obligation, @highlightingInfo
       @html text
     else
       @text @obligation

+  @content: ->
+    @pre class: 'idris-mode block'
+
 module.exports = ProofObligationView


cloneNode Polyfill

cloneNode Polyfill

--- src/utils/load-polyfills.coffee
+++ src/utils/load-polyfills.coffee
@@ -18,6 +18,14 @@
   "Object.defineProperty"       : -> Object.defineProperty?
   "HTMLImports"                 : -> document.createElement("link").import?
   "Element.prototype.classList" : -> document.documentElement.classList
+  "Element.prototype.cloneNode" : ->
+    test = ()->
+      test = document.createElement('input')
+      test.checked = true
+      result = test.cloneNode()
+      !!result.checked
+    this.document? && document.documentElement.cloneNode? && test()
+
   "Xdomain"                     : ->
     return true if new window.XMLHttpRequest().withCredentials?
     return true if window.XMLHttpRequest.supportsXDR == true


minor: wrapp patterns

minor: wrapp patterns

--- revca_track.coffee
+++ revca_track.coffee
@@ -183,9 +183,11 @@

   put: (pattern, x0=0, y0=0) ->
     newStates = []
+    w = @simulator.width
+    h = @simulator.height
     for [x,y] in pattern
-      x+=x0
-      y+=y0
+      x = mod (x+x0), width
+      y = mod (y+y0), height
       if @simulator.putCell(x,y)?
         newStates.push x
         newStates.push y


Do not include the Licence ending in domain invites

So the message reads like "University of Notre Dame has invited
you to join a team on Overleaf" instead of ""University of Notre
Dame licence..."

--- services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee
+++ services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee
@@ -41,9 +41,11 @@

 	createDomainInvite: (user, licence, callback) ->
 		logger.log {licence, email: user.email}, "Creating domain team invite"
+		inviterName = licence.name.replace(/\s+licence$/i, licence.name)
+
 		SubscriptionLocator.getSubscription licence.subscription_id, (error, subscription) ->
 			return callback(error) if error?
-			createInvite(subscription, user.email, licence.name, callback)
+			createInvite(subscription, user.email, inviterName, callback)

 	acceptInvite: (token, userId, callback) ->
 		logger.log {userId}, "Accepting invite"


updating link to preview generation server

--- dkobo/koboform/static/js/xlform/XLF.view.surveyApp.coffee
+++ dkobo/koboform/static/js/xlform/XLF.view.surveyApp.coffee
@@ -150,7 +150,7 @@
       @onEscapeKeydown = viewUtils.debugFrame.close
     else
       XLF.enketoIframe.fromCsv @survey.toCSV(),
-        previewServer: "http://kfdev.kobotoolbox.org"
+        previewServer: "http://kform.prod.kobotoolbox.org"
         onSuccess: => @onEscapeKeydown = XLF.enketoIframe.close
         onError: (errArgs...)=>
           @alert errArgs


A class name was changed

A class name was changed

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -27,7 +27,7 @@
     $(el).addClass 'sent_by_me'

 addMultilineToMessages = ->
-  $('.attachment_contents').each (_, el) ->
+  $('.attachment_flush_text').each (_, el) ->
     selection = $(el)
     if selection.html().match(/<br>/)
       selection.closest('.content').first().addClass('multiline_attachment')


Added simple push

Added simple push

Because sometimes all you have to do is push!

--- menus/git-plus.cson
+++ menus/git-plus.cson
@@ -21,6 +21,7 @@
         { 'label': 'Add & Commit', 'command': 'git-plus:add-and-commit'}
         { 'label': 'Add All & Commit', 'command': 'git-plus:add-all-and-commit'}
         { 'label': 'Add All & Commit & Push', 'command': 'git-plus:add-all-commit-and-push'}
+        { 'label': 'Push', 'command': 'git-plus:push' }
         { 'label': 'Log', 'command': 'git-plus:log' }
         { 'label': 'Merge', 'command': 'git-plus:merge'},
         { 'label': 'Pull Using Rebase', 'command': 'git-plus:pull-using-rebase'}


remove unnecessary call to async.series in OutputFileFinder

callback was previously async but is now synchronous, so high stack
usage.

--- app/coffee/OutputFileFinder.coffee
+++ app/coffee/OutputFileFinder.coffee
@@ -17,20 +17,12 @@
 			jobs = []
 			outputFiles = []
 			for file in allFiles
-				do (file) ->
-					jobs.push (callback) ->
-						if incomingResources[file]
-							return callback()
-						else
-							outputFiles.push {
-								path: file
-								type: file.match(/\.([^\.]+)$/)?[1]
-							}
-							callback()
-
-			async.series jobs, (error) ->
-				return callback(error) if error?
-				callback null, outputFiles
+				if !incomingResources[file]
+					outputFiles.push {
+						path: file
+						type: file.match(/\.([^\.]+)$/)?[1]
+					}
+			callback null, outputFiles

 	_getAllFiles: (directory, _callback = (error, fileList) ->) ->
 		callback = (error, fileList) ->


Add update_level closure for clean updating

--- src/layout/changelog.coffee
+++ src/layout/changelog.coffee
@@ -5,6 +5,11 @@

 get_level_copy = ->
   $.extend {}, custom_level.get()
+
+update_level = (change) ->
+  level = get_level_copy()
+  change level
+  custom_level.set level

 index_of_conditional = (list, condition) ->
   index = -1
@@ -24,16 +29,8 @@

   return unless level_sprite_index > -1

-  level = get_level_copy()
-  level.sprites[level_sprite_index][key] = value
-
-  custom_level.set level
-
-sprite_created = (world, sprite) ->
-  return unless level_sprite_index > -1
-
-  level = get_level_copy()
-  level.sprites[level_sprite_index]
+  update_level (level) ->
+    level.sprites[level_sprite_index][key] = value

 sprite_index = 0
 make_sprite = (world, sprite_dict) ->
@@ -44,20 +41,19 @@
     kind:     sprite_dict.kind
     position: sprite_dict.position

-  if level.sprites[index]?
-    $.extend level.sprites[index], sprite
-  else
-    level.sprites.push sprite
-
-  custom_level.set level
+  update_level (level) ->
+    if level.sprites[index]?
+      $.extend level.sprites[index], sprite
+    else
+      level.sprites.push sprite

 delete_sprite = (world, sprite) ->
   level_sprite_index = get_sprite_index sprite

   return unless level_sprite_index > -1

-  level = get_level_copy()
-  level.sprites.splice level_sprite_index, 1
+  update_level (level) ->
+    level.sprites.splice level_sprite_index, 1

   custom_level.set level


Fix recursive loading of paginated comments (again)

--- app/js/views/comments.coffee
+++ app/js/views/comments.coffee
@@ -12,7 +12,7 @@

   # Ajax callback to fetch next page of comments if the link exists.
   paginate: (data, options, xhr) =>
-    @fetch(link) if link = @nextLink(xhr.getResponseHeader("Link"))
+    @fetch(link.href) if link = @nextLink(xhr.getResponseHeader("Link"))

   # Parse the Link header, looking for rel=next
   nextLink: (header) =>


Start adding ordinal data support.

Start adding ordinal data support.

--- app/javascripts/correlation.coffee
+++ app/javascripts/correlation.coffee
@@ -1,4 +1,4 @@
-define ['angular', 'dc', 'lodash', 'crossfilter', 'd3', 'breast'], (ng, dc) ->
+define ['angular', 'dc', 'roman', 'lodash', 'crossfilter', 'd3', 'breast'], (ng, dc, roman) ->
   correlation = ng.module 'qiprofile.correlation', ['qiprofile.breast']

   correlation.factory 'Correlation', ['Breast', (Breast) ->
@@ -8,6 +8,8 @@
     #     * label - Appears in the dropdown picklists and as chart axis labels.
     #     * coll - The collection(s) for which the data type is valid. May be
     #         'all' or a list of specific collections.
+    #     * scale - If the data type has ordinal values (e.g. TNM stage) then
+    #         they are specified as an array, else null.
     #     * accessor - The data accessor.
     #
     #   The order of the data types and their handles must be exactly
@@ -72,11 +74,21 @@
           accessor: (tumor) ->
             return null unless tumor.extent?
             if tumor.extent.depth? then tumor.extent.depth else null
+      'breastTNMStage':
+          label: 'TNM Stage'
+          coll: [
+            'Breast'
+          ]
+          accessor: (tumor) ->
+            return null unless tumor.tnm?
+            stage = Breast.stage(tumor.tnm)
+            stage.replace(/^\d+/, roman.romanize)
       'recurrenceScore':
           label: 'Recurrence Score'
           coll: [
             'Breast'
           ]
+          scale: null
           accessor: (tumor) ->
             return null unless tumor.geneticExpression.normalizedAssay?
             Breast.recurrenceScore(tumor.geneticExpression.normalizedAssay)
@@ -186,12 +198,25 @@

     # The imaging data types.
     IMAGING_DATA_TYPES = [
-      "fxlKTrans"
-      "fxrKTrans"
-      "deltaKTrans"
-      "vE"
-      "tauI"
+      'fxlKTrans'
+      'fxrKTrans'
+      'deltaKTrans'
+      'vE'
+      'tauI'
     ]
+
+    # All possible values for ordinal data types.
+    ORDINAL_SCALES =
+      'breastTNMStage':
+        [
+         'IA'
+         'IIA'
+         'IIB'
+         'IIIA'
+         'IIIB'
+         'IIIC'
+         'IV'
+        ]

     # The chart layout parameters.
     CHART_LAYOUT_PARAMS =
@@ -231,7 +256,7 @@
           }
           {
             x: 'necrosisPercent'
-            y: 'vE'
+            y: 'vE'
           }
           {
             x: 'necrosisPercent'
@@ -339,31 +364,39 @@
       scales = new Object
       # Iterate over the valid data types.
       for key of choices
-        # Make a list of all values for the data type and obtain the max
-        #   and min values.
-        allValues = (dat[key] for dat in data)
-        max = _.max allValues
-        min = _.min allValues
-        diff = max - min
-        # If the values are not all the same, calculate a padding value based
-        #   the chart layout parameter setting, e.g. a setting of .2 will
-        #   give the chart 20% padding.
-        # If the values are all the same, calculate a padding value of an
-        #   appropriate resolution for that value. The initial "result" value
-        #   reflects the number of digits or decimal places of the scatterplot
-        #   values. Each chart tick mark above and below that value is then set
-        #   to 10 to the power of the result reduced by 1.
-        if diff != 0
-          pad = diff * CHART_LAYOUT_PARAMS.axisPadding
+        # If the data type has ordinal values, return the value array.
+        #   Otherwise, make a list of all values for the data type and obtain
+        #   the max and min values.
+        if key in Object.keys ORDINAL_SCALES
+          scales[key] =
+            range: ORDINAL_SCALES[key]
+            padding: null
+            isOrdinal: true
         else
-          max = Math.abs(max)
-          result = Math.ceil(Math.log(max) / Math.log(10))
-          if Math.abs(result) is Infinity then result = 0
-          pad = CHART_LAYOUT_PARAMS.ticks / 2 * Math.pow(10, result - 1)
-        # Add the range and padding for the data type to the scales object.
-        scales[key] =
-          range: [min, max]
-          padding: pad
+          allValues = (dat[key] for dat in data)
+          max = _.max allValues
+          min = _.min allValues
+          diff = max - min
+          # If the values are not all the same, calculate a padding value based
+          #   the chart layout parameter setting, e.g. a setting of .2 will
+          #   give the chart 20% padding.
+          # If the values are all the same, calculate a padding value of an
+          #   appropriate resolution for that value. The initial "result" value
+          #   reflects the number of digits or decimal places of the scatterplot
+          #   values. Each chart tick mark above and below that value is then set
+          #   to 10 to the power of the result reduced by 1.
+          if diff != 0
+            pad = diff * CHART_LAYOUT_PARAMS.axisPadding
+          else
+            max = Math.abs(max)
+            result = Math.ceil(Math.log(max) / Math.log(10))
+            if Math.abs(result) is Infinity then result = 0
+            pad = CHART_LAYOUT_PARAMS.ticks / 2 * Math.pow(10, result - 1)
+          # Add the range and padding for the data type to the scales object.
+          scales[key] =
+            range: [min, max]
+            padding: pad
+            isOrdinal: false
       # Return the scales object.
       scales

@@ -407,6 +440,7 @@
             d.tumorLength
             d.tumorWidth
             d.tumorDepth
+            d.breastTNMStage
             d.recurrenceScore
             d.ki67
             d.gstm1
@@ -428,14 +462,9 @@
         # The chart configuration.
         chart.width(CHART_LAYOUT_PARAMS.width)
           .height(CHART_LAYOUT_PARAMS.height)
-          .x(d3.scale.linear().domain(scales[xAxis].range))
-          .y(d3.scale.linear().domain(scales[yAxis].range))
-          .elasticX(true)
           .elasticY(true)
           .xAxisLabel(LABELS[xAxis])
           .yAxisLabel(LABELS[yAxis])
-          .xAxisPadding(scales[xAxis].padding)
-          .yAxisPadding(scales[yAxis].padding)
           .renderVerticalGridLines(true)
           .renderHorizontalGridLines(true)
           .symbolSize(CHART_LAYOUT_PARAMS.symbolSize)
@@ -447,6 +476,21 @@
         chart.yAxis().ticks(CHART_LAYOUT_PARAMS.ticks)
         chart.margins().left += CHART_LAYOUT_PARAMS.leftMargin

+        # Special configuration for X-axis ordinal scales.
+        #
+        # TODO - Ordinal scales are not displaying correctly, need to fix.
+        if scales[xAxis].isOrdinal
+          chart.x(d3.scale.ordinal().domain(scales[xAxis].range))
+            .xUnits(dc.units.ordinal())
+            .elasticX(false)
+        else
+          chart.x(d3.scale.linear().domain(scales[xAxis].range))
+            .xAxisPadding(scales[xAxis].padding)
+            .elasticX(true)
+
+        chart.y(d3.scale.linear().domain(scales[yAxis].range))
+        chart.yAxisPadding(scales[yAxis].padding)
+
       # Render all the charts.
       dc.renderAll()


do not discard case uin snippets

do not discard case uin snippets

--- app/scripts/filters/snippet.coffee
+++ app/scripts/filters/snippet.coffee
@@ -10,7 +10,9 @@
     parts = undefined
     theMatch = undefined
     trimmedString = undefined
-    string = string.replace(/(<([^>]+)>)/g, "").toLowerCase()
+    string = string.replace /(<([^>]+)>)/g, ""
+    theParts = (string.split ".").slice 0, 50
+    string = do string.toLowerCase
     parts = string.split(".").slice(0, 50)
     i = 0
     end = parts.length
@@ -24,7 +26,11 @@
         when "year"
           theMatch = "#{do today.getFullYear}"
       targetIndex = parts[i].indexOf(theMatch)
-      return parts[i].replace(theMatch, "" + theMatch + "")  if targetIndex isnt -1
+      if targetIndex isnt -1
+        ret = (theParts[i].substr 0, targetIndex) + ""
+        ret += (theParts[i].substr targetIndex, theMatch.length) + ""
+        ret += theParts[i].substr (targetIndex + theMatch.length), theParts[i].length
+        return ret
       i++
     trimmedString = string.substr(0, 100)
     trimmedString.substr(0, Math.min(trimmedString.length, trimmedString.lastIndexOf(" "))) + " [...]"


A custom error for JVerify

A custom error for JVerify

--- server/app/errors.coffee
+++ server/app/errors.coffee
@@ -1,5 +1,11 @@
 class AccessError extends Error
   constructor:(@message)->
+
+class PINExistsError extends Error
+  constructor:(@message)->
+    Error.call @
+    @name = 'PINExistsError'
+    @message = message

 class KodingError extends Error
   constructor:(message)->


Sim: Fix PP changes not being sent to client.

--- server/bw/pokemon.coffee
+++ server/bw/pokemon.coffee
@@ -100,9 +100,12 @@
     pp = Math.max(pp, 0)
     pp = Math.min(pp, @maxPP(move))
     @ppHash[move.name] = pp
-    @player?.tell(Protocol.CHANGE_PP, @battle.getPlayerIndex(@playerId),
-                  @team.pokemon.indexOf(this),
-                  @moves.indexOf(move), pp)
+    @battle?.tellPlayer(@playerId,
+      Protocol.CHANGE_PP,
+      @battle.getPlayerIndex(@playerId),
+      @team.pokemon.indexOf(this),
+      @moves.indexOf(move),
+      pp)
     pp

   resetAllPP: (pp) ->


operations: Notify should notify all users pertained by a subscription

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -1001,8 +1001,14 @@
         t.getNodeListeners @req.node, (err, listeners) =>
             if err
                 return cb err
-            if @req.extraListeners?
-                listeners = listeners.concat @req.extraListeners
+
+            # Always notify all users pertained by a subscription
+            # notification, even if just unsubscribed.
+            for update in @req
+                if update.type is 'subscription' and
+                   listeners.indexOf(update.jid) < 0
+                    listeners.push update.jid
+
             for listener in listeners
                 notification = Object.create(@req)
                 notification.listener = listener
@@ -1078,10 +1084,6 @@
             notifications = []
             if (notification = op.notification?())
                 for own node, notifications of groupByNode(notification)
-                    # Notify unsubscribed users too:
-                    if opName is 'unsubscribe-node'
-                        notification.extraListeners = getNodeUser(node)
-
                     notification.node = node
                     new Notify(router, notification).run (err) ->
                         if err


operations: BrowseNodes.transaction should use @fetchNodes()

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -248,7 +248,7 @@
 class BrowseNodes extends ModelOperation
     transaction: (t, cb) ->
         rsm = @req.rsm
-        t.listNodes (err, results) =>
+        @fetchNodes (err, results) =>
             if err
                 return cb err


operations: fix Notify filterSubscription & filterAffiliation invokation

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -1208,9 +1208,9 @@
                 req = @req.filter (update) =>
                     switch update.type
                         when 'subscription'
-                            @filterSubscription update
+                            PrivilegedOperation::filterSubscription update
                         when 'affiliation'
-                            @filterAffiliation update
+                            PrivilegedOperation::filterAffiliation update
                         else
                             yes
                 # Any left after filtering? Don't send empty


use one height to prevent cutoff on first post issue

--- src/main/coffee/main.coffee
+++ src/main/coffee/main.coffee
@@ -248,7 +248,7 @@
                 objSelf.scrollDisabled = false
                 $(window).scroll(objSelf._onScrollFn)
             $('html, body').animate({
-                scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - 8
+                scrollTop: newArticleOffset.top - 52 - 8
             }, 500, "swing", () -> setTimeout(doneFn, 0))
             e.preventDefault()

@@ -312,7 +312,7 @@
                objSelf.scrollDisabled = false
                $(window).scroll(objSelf._onScrollFn)
            $('html, body').animate({
-                scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - 8
+                scrollTop: newArticleOffset.top - 52 - 8
            }, 500, "swing", () -> setTimeout(doneFn, 0))

     _initializeKeyboardNavigation: ->


Remove products from /i18n

Remove products from /i18n

We don't show the translated product fields to users.  Leaving i18n product translation page in place for now.

--- app/views/i18n/I18NHomeView.coffee
+++ app/views/i18n/I18NHomeView.coffee
@@ -2,8 +2,6 @@
 template = require 'templates/i18n/i18n-home-view'
 CocoCollection = require 'collections/CocoCollection'
 Courses = require 'collections/Courses'
-Products = require 'collections/Products'
-Product = require 'models/Product'
 Article = require 'models/Article'

 LevelComponent = require 'models/LevelComponent'
@@ -43,9 +41,8 @@
     @campaigns = new CocoCollection([], { url: '/db/campaign?view=i18n-coverage', project: project, model: Campaign })
     @polls = new CocoCollection([], { url: '/db/poll?view=i18n-coverage', project: project, model: Poll })
     @courses = new Courses()
-    @products = new CocoCollection([], { url: '/db/products?view=i18n-coverage', project: project, model: Product })
     @articles = new CocoCollection([], { url: '/db/article?view=i18n-coverage', project: project, model: Article })
-    for c in [@thangTypes, @components, @levels, @achievements, @campaigns, @polls, @courses, @products, @articles]
+    for c in [@thangTypes, @components, @levels, @achievements, @campaigns, @polls, @courses, @articles]
       c.skip = 0

       c.fetch({data: {skip: 0, limit: PAGE_SIZE}, cache:false})


Fix classroom world map

Fix classroom world map

It would break if there are sessions loaded which are outside of
the course being looked at.

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -343,6 +343,8 @@
       defaultLanguage = @classroom.get('aceConfig').language
       for session in @sessions.slice()
         classroomLevel = classroomLevelMap[session.get('level').original]
+        if not classroomLevel
+          continue
         expectedLanguage = classroomLevel.get('primerLanguage') or defaultLanguage
         if session.get('codeLanguage') isnt expectedLanguage
           @sessions.remove(session)


Added a forward slash to tristans code... Why am I even pushing this...

--- filemanager/static/script/thumbloader.coffee
+++ filemanager/static/script/thumbloader.coffee
@@ -12,7 +12,7 @@
             handleViewedItem data

-$(document).ready(GetViewedItem("ajax/thumblist/1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10"))
+$(document).ready(GetViewedItem("/ajax/thumblist/1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10"))

 console.log "I like tea!"


disable lints on change

disable lints on change

--- lib/linter-languagetool.coffee
+++ lib/linter-languagetool.coffee
@@ -22,7 +22,7 @@
     return {
       name: 'languagetool'
       scope: 'file'
-      lintsOnChange: true
+      lintsOnChange: false
       grammarScopes: atom.config.get 'linter-languagetool.grammerScopes'
       lint: provider.lint
     }


Use update in tooltip

Use update in tooltip

--- lib/package-update-view.coffee
+++ lib/package-update-view.coffee
@@ -79,8 +79,8 @@
     @status.destroyTooltip()
     switch iconName
       when 'check'
-        @status.setTooltip(_.capitalize("#{@type} upgrade"))
+        @status.setTooltip(_.capitalize("#{@type} updated"))
       when 'alert'
-        @status.setTooltip(_.capitalize("#{@type} failed to upgrade"))
+        @status.setTooltip(_.capitalize("#{@type} failed to updated"))
       when 'cloud-download'
-        @status.setTooltip(_.capitalize("#{@type} upgraded"))
+        @status.setTooltip(_.capitalize("#{@type} updating"))


Fix Changelog link

Fix Changelog link

--- modules/help.coffee
+++ modules/help.coffee
@@ -17,12 +17,10 @@

     This bot is not yet public, though you can send me a DM if you want it on your server.

-    Changelog: https://gist.github.com/TheBITLINK/61f86a841f7d6fed896363d67ddd4d40
-
+    Changelog: https://thebitlink.gitbooks.io/focabot-docs/content/Changelog.html
     Command List: https://thebitlink.gitbooks.io/focabot-docs/content/Commands.html
     Audio Filters: https://thebitlink.gitbooks.io/focabot-docs/content/Filters.html
     Configuration: https://thebitlink.gitbooks.io/focabot-docs/content/Configuration.html
-    ```
     """
     msg.author.openDM().then (dm)=>
       dm.sendMessage reply


That didn't work

That didn't work

--- modules/tags.coffee
+++ modules/tags.coffee
@@ -13,7 +13,7 @@
     { @permissions } = @engine

     @registerCommand '+', {argSeparator: ' '}, (msg, args)=>
-      return if args.length < 2 or msg.author.bot
+      return if msg.author.bot or args.length < 2
       tag = new Tag {
         key: args[0].toLowerCase()
         reply: args.slice(1).join(' ')
@@ -23,7 +23,7 @@
         msg.reply 'Tag saved!'

     @registerCommand '-', {argSeparator: ' '}, (msg, args)=>
-      return if args.length < 1 or msg.author.bot
+      return if msg.author.bot or args.length < 1
       q = { by: msg.author.id, key: args[0] }
       if args.length > 1
         if args[1].toLowerCase() is 'all' and @permissions.isOwner msg.author
@@ -37,14 +37,14 @@
         result.delete() for result in results

     @registerCommand "!", { argSeparator: ' ' }, (msg, args)=>
-      return if args.length < 1 or msg.author.bot
+      return if msg.author.bot or args.length < 1
       Tag.filter({ key: args[0].toLowerCase() }).run().then (results)=>
         chance = new Chance()
         tag = chance.pickone results
         msg.channel.sendMessage tag.reply

     @registerCommand 'taginfo', { ownerOnly: true, argSeparator: ' ' }, (msg, args, data, bot)=>
-      return if args.length < 1 or msg.author.bot
+      return if msg.author.bot or args.length < 1
       Tag.filter({ key: args[0].toLowerCase() }).run().then (results)=>
         r = ""
         for tag in results