Use bluebird long stacktraces in development

--- app/assets/main.coffee
+++ app/assets/main.coffee
@@ -1,4 +1,7 @@
 require './shim/phantomjs-bind'

 window.Promise = require 'bluebird'
+if process.env.NODE_ENV == 'development'
+  Promise.longStackTraces()
+
 window.Rdb = require('rdb/index')


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -2,11 +2,13 @@

     @accessor 'data'

-    #constructor: ->
+    constructor: ->
+        @instanceDataId = $(@node).attr('data-id')
+
     ready: ->
         #@testData = 123
         #@oldPieData = ''
-        @instanceDataId = $(@node).attr('data-id')
+        #@instanceDataId = $(@node).attr('data-id')
         #console.log('@instanceDataId',@instanceDataId)
         #@oldPieData[@instanceDataId] = ''
         window.oldPieData = window.oldPieData or {}


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -285,7 +285,8 @@
             .attrTween("d", (d,i) ->
                 console.log("d",d)
                 console.log("i",i)
-                i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
+                #i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
+                i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: piedata[i].startAngle, endAngle: piedata[i].endAngle})
                 return (t) ->
                     b = i(t)
                     return arc(b)


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -233,7 +233,7 @@

         path = pathGroup.selectAll("path.pie")
             #.data(data)
-            .data(pie)
+            .data(pie(data))

         path.enter().append("path")
             .attr("class", "pie")


Agent pie: test globalizing variables

Agent pie: test globalizing variables

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -19,13 +19,13 @@
         @height = (Dashing.widget_base_dimensions[1] * @container.data('sizey')) - 120

         # Calculated min dimension of the SVG area
-        @radius = Math.min(width, height) / 2
+        @radius = Math.min(@width, height) / 2

         # Outer radius of the pie
-        @radiuso = Math.min(width, height) / 3
+        @radiuso = Math.min(@width, height) / 3

         # Inner radius of the pie (zero = pie, non-zero = donut)
-        @radiusi = radiuso * 2 / 3
+        @radiusi = @radiuso * 2 / 3

         # Color scale for pie slices
         @color = d3.scale.category20()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -191,7 +191,7 @@
         arcs.append("svg:path").attr("fill", (d, i) -> color i)
           .attr("fill-opacity", 0.4).attr("d", arc)

-  render: (data) ->
+  renderX5: (data) ->
         #console.log("update pie", data);

         if !data
@@ -426,7 +426,7 @@
                 return "M" + (d.r-2) + "," + d.b + "L" + (d.l+2) + "," + d.b + " " + d.cx + "," + d.cy

         )
-  renderX5: (data) ->
+  render: (data) ->
         #console.log("update pie", data);

         if !data
@@ -452,6 +452,7 @@
         $(@node).children("svg").remove();

         svg = d3.select(@node).append("svg:svg")
+            .data([data])
             .attr("width", width)
             .attr("height", height)
             .append("svg:g")


vm: fix subscription fetcher procedure in `JVM#setAlwaysOn` function

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -160,7 +160,8 @@
             vm.update $set: alwaysOn: status, callback

       if group is "koding"
-        delegate.fetchSubscriptions$ client, tags: ["vm"], (err, subscriptions) ->
+        options = targetOptions: tags: $in: "vm"
+        delegate.fetchSubscriptions null, options, (err, subscriptions) ->
           noSyncSubscription = null
           activeSubscription = null


vm: hostname -> hostnameAlias

vm: hostname -> hostnameAlias

The reasoning is, the actual hostname of the vm can be changed from inside.  The so-called hostnameAlias will be a constant, reasonable identifier for the vm.  This is also to let us decouple the vmname from the domain name.

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -34,7 +34,7 @@
         type            : String
         default         : -> null
       name              : String
-      hostname          : String
+      hostnameAlias     : String
       users             : Array
       groups            : Array
       usage             : # TODO: usage seems like the wrong term for this.


Normalize file path when creating bundle to undo Karma windows path normalization

--- src/index.coffee
+++ src/index.coffee
@@ -71,7 +71,7 @@
     log.debug 'Processing "%s".', file.originalPath

     # Create a file-specific browserify bundle and apply the configuration.
-    fileBundle = browserify file.originalPath
+    fileBundle = browserify path.normalize file.originalPath
     applyConfig fileBundle, config

     # Override the bundle's default dependency handling, adding all dependencies


+ make the passed options higher prior * initTasks pass the needAutoInstall argument only

--- src/index.coffee
+++ src/index.coffee
@@ -49,13 +49,13 @@
         try
           require vPkgNames[i]
           Task i.name, i.options
-  initTasks: (aInitConfig, aOptions)->
+  initTasks: (aInitConfig, needAutoInstall)->
     vMissedTasks = []
     if isObject aInitConfig
       for name, opts of aInitConfig
         task = Task(name, opts)
         vMissedTasks.push name:name, options:opts unless task
-      if aOptions.autoInstall and vMissedTasks.length
+      if needAutoInstall and vMissedTasks.length
         initMissedTasks vMissedTasks
     return
   initLogger: (aOptions)->
@@ -86,6 +86,8 @@
   _executeSync: (aOptions)->
     cwd = aOptions.cwd
     folder = Resource cwd, aOptions
+    vNeedAutoInstall = aOptions.autoInstall
+    delete aOptions.autoInstall
     try
       folder.loadSync read:true#, recursive:true
     catch err
@@ -111,8 +113,12 @@
           return
       setPrototypeOf vFolder, folder
       folder = vFolder
+    delete aOptions.cwd
+    delete aOptions.src
+    delete aOptions.dest
+    folder.assign(aOptions)

     @folder = folder
     @initLogger folder
-    @initTasks folder['initConfig'], aOptions # the initialization configuration of tasks.
+    @initTasks folder['initConfig'], vNeedAutoInstall # the initialization configuration of tasks.
     @processSync folder


testing

testing

--- src/index.coffee
+++ src/index.coffee
@@ -37,7 +37,7 @@
       noShowLimit = moment(meetingStartTime).utc().add(@limitMinutes, 'minute')
       if moment().isBefore(noShowLimit)
         @validUntil = noShowLimit
-        console.log "Initializing valid until for: #{@validUntil}"
+        console.log "Initializing valid until for: #{@validUntil.toISOString()}"

       if (moment(@validUntil).isBefore(moment().utc()) && moment().utc().isAfter(noShowLimit))
         meetingId = _.get currentMeeting, 'meetingId'


Private exports hack

Private exports hack

--- src/index.coffee
+++ src/index.coffee
@@ -6,3 +6,11 @@
   ItemSerializer: require './item-serializer'
   ItemPath: require './item-path'
   DateTime: require './date-time'
+  AttributedString: require './attributed-string'
+
+
+  # Private exports
+  SpanBuffer: require './span-buffer'
+  Span: require './span-buffer/span'
+  shortid: require './shortid'
+  util: require './util'


* [bug] can not work on windows

* [bug] can not work on windows

--- src/index.coffee
+++ src/index.coffee
@@ -9,9 +9,8 @@
 require './init/load-config-folder'

 fs.cwd      = process.cwd
+fs.path     = path = require 'path.js'
 Resource.setFileSystem fs
-path = fs.path
-

 # the src is the file patterns for folder
 # the dest is the destination folder.


* [bug] debug executeSync result should return result only not results

--- src/index.coffee
+++ src/index.coffee
@@ -68,7 +68,7 @@
         try
           logger.status 'DEBUG', 'INVOKE', task.inspect(true) if logger
           aResult.push task.executeSync()
-          logger.status 'DEBUG', 'INVOKE', task.inspect(true), 'result=', aResult if logger
+          logger.status 'DEBUG', 'INVOKE', task.inspect(true), 'result=', aResult[aResult.length-1] if logger
           logger.status 'INVOKE', task.inspect(true), 'ok' if logger
         catch err
           result = @error err, aOptions, task.inspect(true)
@@ -83,7 +83,7 @@
           try
             logger.status 'DEBUG', 'INVOKE', task.inspect(true, v) if logger
             aResult.push task.executeSync(v)
-            logger.status 'DEBUG', 'INVOKE', task.inspect(true, v), 'result=', aResult if logger
+            logger.status 'DEBUG', 'INVOKE', task.inspect(true, v), 'result=', aResult[aResult.length-1] if logger
             logger.status 'INVOKE', task.inspect(true, v), 'ok' if logger
           catch err
             result = @error err, aOptions, task.inspect(true, v)


Added test for posts

Added test for posts

--- src/base.coffee
+++ src/base.coffee
@@ -1019,3 +1019,20 @@
             err.should.eql new Error()
             err.toString().should.eql 'Error: Invalid fields: age, desc'
             api.close(done)
+
+
+
+    it "should raise an error if a post attempts to put nonexisting fields", (done) ->
+      api = manikin.create()
+      mad = {
+        apa:
+          fields:
+            v1: 'string'
+      }
+
+      api.connect connectionData, noErr ->
+        api.load mad, noErr ->
+          api.post 'apa', { v1: '2', v2: '1' }, (err) ->
+            err.should.eql new Error()
+            err.toString().should.eql 'Error: Invalid fields: v2'
+            api.close(done)


Prevent recursion for now.

Prevent recursion for now.

--- src/invoke/invoke.coffee
+++ src/invoke/invoke.coffee
@@ -15,6 +15,7 @@
 invoke = (name, opts, cb) ->
   log.debug 'invoke'

+  # Prevent recursive calls
   return if invoked[name]
   invoked[name] = true


Logging sort order works properly.

Logging sort order works properly.

--- admin/static/coffee/cluster.coffee
+++ admin/static/coffee/cluster.coffee
@@ -403,7 +403,21 @@

 class LogEntries extends Backbone.Collection
     model: LogEntry
-    comparator: (log_entry) ->
+    comparator: (a, b) ->
+        if a.get('timestamp') < b.get('timestamp')
+            return 1
+        else if a.get('timestamp') > b.get('timestamp')
+            return -1
+        else if a.get('machine_uuid') <  b.get('machine_uuid')
+            return 1
+        else if a.get('machine_uuid') > b.get('machine_uuid')
+            return -1
+        else if a.get('message') < b.get('message')
+            return 1
+        else if a.get('message') > b.get('message')
+            return -1
+        else
+            return 0
         # sort strings in reverse order (return a negated string)
         #String.fromCharCode.apply String,
         #    _.map(log_entry.get('datetime').split(''), (c) -> 0xffff - c.charCodeAt())
@@ -669,6 +683,7 @@
         _m_collection = new LogEntries
         for json in log_entries
             entry = new LogEntry json
+            entry.set('machine_uuid',machine_uuid)
             _m_collection.add entry
             all_log_entries.push entry

@@ -709,7 +724,7 @@
         $.getJSON('/ajax/progress', set_progress)
         $.getJSON('/ajax/directory', set_directory)
         $.getJSON('/ajax/last_seen', set_last_seen)
-        #$.getJSON('/ajax/log/_?max_length=10', set_log_entries)
+        $.getJSON('/ajax/log/_?max_length=10', set_log_entries)

     # Override the default Backbone.sync behavior to allow reading diffs
     legacy_sync = Backbone.sync


Fix a bug with /me.

Fix a bug with /me.

--- admin/static/coffee/cluster.coffee
+++ admin/static/coffee/cluster.coffee
@@ -905,7 +905,7 @@
                     data.protocol = "memcached"
                 when 'datacenters' then collection = datacenters
                 when 'machines' then collection = machines
-                when 'me' then #do nothing for now
+                when 'me' then continue
                 else
                     console.log "Unhandled element update: " + update
                     return


Use the correct delimiters

Use the correct delimiters

--- src/jsonp.coffee
+++ src/jsonp.coffee
@@ -70,7 +70,7 @@
     return str

 objectToURI = (obj) ->
-    data = [encode(key) + '=' + encode value for key, value of obj]
+    data = (encode(key) + '=' + encode value for key, value of obj)
     return data.join '&'

 if define?.amd then define -> JSONP


Fix precondition activated debug message check

--- src/lib/Component.coffee
+++ src/lib/Component.coffee
@@ -214,7 +214,7 @@
       @deactivate context
       output.sendDone e

-    unless input.activated
+    unless context.activated
       if port.isAddressable()
         debug "#{@nodeId} #{ip.type} packet on #{port.name}[#{ip.index}] didn't match preconditions"
       else


Now that every key has a keychar, exclude modifiers specifically

--- content_scripts/mode_key_handler.coffee
+++ content_scripts/mode_key_handler.coffee
@@ -59,7 +59,7 @@
       @reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
       @suppressEvent
     else
-      @reset() if keyChar
+      @reset() unless event.key in ["Control", "Shift", "Alt", "OS", "AltGraph"]
       @continueBubbling

   # This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).


Add some more map markup

Add some more map markup

--- web2/js/views/map.cjsx
+++ web2/js/views/map.cjsx
@@ -4,6 +4,19 @@

 #require 'leaflet/dist/leaflet.css'
 require '../../css/map.css'
+
+TypeFilter = React.createClass
+    render: ->
+

+

+                <input
+                    type="checkbox"
+                    name="type"
+                    id="type-#{@props.id}"
+                    />
+                {@props.label}
+

+

 Map = React.createClass
     displayName: 'Geocache Map'
@@ -62,34 +75,34 @@

                         Geocache Types
-

-

-                                <input
-                                    type="checkbox"
-                                    name="type"
-                                    id="type-traditional"
-                                    />
-                                Traditiional
-

-

-

-

-                                <input
-                                    type="checkbox"
-                                    name="type"
-                                    id="type-multi"
-                                    />
-                                Multi
-

-

+                        <TypeFilter id="traditional" label="Traditional"/>
+                        <TypeFilter id="multi" label="Multi-Cache"/>
+                        <TypeFilter id="letterbox" label="Letterbox"/>
+                        <TypeFilter id="event" label="Event-Cache"/>
+                        <TypeFilter id="mystery" label="Mystery"/>
+                        <TypeFilter id="webcam" label="Webcam"/>
+                        <TypeFilter id="cito" label="Cache In Trash Out"/>
+                        <TypeFilter id="earth" label="Earth-Cache"/>
+                        <TypeFilter id="mega" label="Mega-Event"/>
+                        <TypeFilter id="wherigo" label="Wherigo"/>

-                        Exclude finds by
+                        Exclude finds
                         <input
                             type="text"
                             id="username"
                             placeholder="Username"
                         />
+

+

+

+

+
+
foobert

+

+

+
+
signux


Update secondary timeout for multi commands too

--- services/document-updater/app/coffee/RedisBackend.coffee
+++ services/document-updater/app/coffee/RedisBackend.coffee
@@ -57,7 +57,7 @@

 class MultiClient
 	constructor: (@clients) ->
-		@SECONDARY_TIMEOUT = 200
+		@SECONDARY_TIMEOUT = 600

 	exec: (callback) ->
 		primaryError = null


Use different color schemes for dark or light editor schemes

--- services/web/public/coffee/track-changes/DiffView.coffee
+++ services/web/public/coffee/track-changes/DiffView.coffee
@@ -86,20 +86,27 @@
 			markerBackLayer = @aceEditor.renderer.$markerBack
 			markerFrontLayer = @aceEditor.renderer.$markerFront
 			lineHeight = @aceEditor.renderer.lineHeight
+
+			dark = @_isDarkTheme()
+
 			if entry.i? or entry.d?
 				hue = entry.meta.user.hue()
 				if entry.i?
-					@_addMarkerWithCustomStyle session, markerBackLayer, range, "inserted-change-background", false, """
-						background-color : hsl(#{hue}, 70%, 85%);
-					"""
+					if dark
+						style = "background-color : hsl(#{hue}, 100%, 28%);"
+					else
+						style = "background-color : hsl(#{hue}, 70%, 85%);"
+					@_addMarkerWithCustomStyle session, markerBackLayer, range, "inserted-change-background", false, style
 				if entry.d?
-					@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
-						background-color : hsl(#{hue}, 70%, 95%);
-					"""
-					@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, """
-						height: #{Math.round(lineHeight/2) - 1}px;
-						border-bottom: 2px solid hsl(#{hue}, 70%, 40%);
-					"""
+					if dark
+						bgStyle = "background-color: hsl(#{hue}, 100%, 20%);"
+						fgStyle = "border-bottom: 2px solid hsl(#{hue}, 100%, 60%);"
+					else
+						bgStyle = "background-color: hsl(#{hue}, 70%, 95%);"
+						fgStyle = "border-bottom: 2px solid hsl(#{hue}, 70%, 40%);"
+					fgStyle += "; height: #{Math.round(lineHeight/2) - 1}px;"
+					@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, bgStyle
+					@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-foreground", true, fgStyle

 		_addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
 			session.addMarker range, klass, (html, range, left, top, config) ->
@@ -108,6 +115,14 @@
 				else
 					markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
 			, foreground
+
+		_isDarkTheme: () ->
+			rgb = $(".ace_editor").css("background-color");
+			[m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
+			r = parseInt(r, 10)
+			g = parseInt(g, 10)
+			b = parseInt(b, 10)
+			return r + g + b < 3 * 128

 		insertNameTag: () ->
 			@$nameTagEl = $("

")
@@ -154,9 +169,10 @@
 			height = @$ace.height()

 			hue = entry.meta.user.hue()
-			css = {
-				"background-color" : "hsl(#{hue}, 70%, 90%)";
-			}
+			if @_isDarkTheme()
+				css = { "background-color" : "hsl(#{hue}, 100%, 20%)"; }
+			else
+				css = { "background-color" : "hsl(#{hue}, 70%, 90%)"; }

 			if position.pageX + @$nameTagEl.width() < @$ace.width()
 				css["left"] = position.pageX
@@ -175,7 +191,7 @@
 			@$nameTagEl.css css

 		_hideNameTag: () ->
-			@$nameTagEl.hide()
+			@$nameTagEl?.hide()

 		updateVisibleNames: (e) ->
 			visibleName = false


start taking better advantage of lodash

start taking better advantage of lodash

--- functions/championify.coffee
+++ functions/championify.coffee
@@ -30,8 +30,9 @@
 cl = (text) ->
   m = moment().format('HH:mm:ss')
   m = ('['+m+'] | ') + text
-  # console.log(m)
+  console.log(m)
   $('#progress').prepend(''+text+'
')
+

 # TODO: I think we can access Electrons package.json instead directly in the browser, making this useless.
 ###*
@@ -142,7 +143,7 @@
  * @param {Array} Array of strings of Champs from Riot.
  * @callback {Function} Callback.
 ###
-asyncRequestChamps = (champs, cb) ->
+requestChamps = (champs, cb) ->
   async.eachLimit champs, 2, (champ, acb) ->
     requestPage {champ: champ}, () ->
       acb null
@@ -156,14 +157,14 @@
  * @param {Object} Champion object created by asyncRequestChamps.
  * @callback {Function} Callback.
 ###
-requestPage = (obj, cb) ->
-  champ = obj.champ
+requestPage = (champ_info, cb) ->
+  champ = champ_info.champ
   url = 'http://champion.gg/champion/'+champ

-  if obj.position
-    url = url + '/' + obj.position
+  if champ_info.position
+    url = url + '/' + champ_info.position
   else
-    cl 'Processing: '+obj.champ
+    cl 'Processing: '+champ_info.champ

   async.retry 3, (_cb) ->
     hlp.ajaxRequest url, (body) ->
@@ -173,7 +174,7 @@
     if err
       window.undefinedBuilds.push(champ)
       return cb()
-    processChamp(obj, body, cb)
+    processChamp(champ_info, body, cb)

 ###*
@@ -182,8 +183,8 @@
  * @param {String} Body of Champion.GG page.
  * @callback {Function} Callback.
 ###
-processChamp = (obj, body, cb) ->
-  champ = obj.champ
+processChamp = (champ_info, body, cb) ->
+  champ = champ_info.champ

   $c = cheerio.load(body)
   gg = hlp.compileGGData($c)
@@ -243,7 +244,7 @@

   # Process the skills table and return an array in order.
-  processSkills: (skills) ->
+  processSkills = (skills) ->
     keys = {
       '1': 'Q'
       '2': 'W'
@@ -280,14 +281,14 @@

   # TODO: Make this better with Lodash.
-  # Converts array of items from ChampionGG in to useable object from LoL Blocks.
-  arrayToBuilds: (arr) ->
+  # Converts array of items from ChampionGG in to useable object from LoL Blocks.
+  arrayToBuilds = (arr) ->
     build = []

     arr = _.map arr, (e) ->
       return e.id.toString()

-    obj = arr.reduce (acc, curr) ->
+    count_obj = arr.reduce (acc, curr) ->
       if typeof acc[curr] == 'undefined'
         acc[curr] = 1
       else
@@ -299,7 +300,7 @@
       a.indexOf(v) == i

     arr.forEach (e) ->
-      count = obj[e]
+      count = count_obj[e]
       if e == '2010'  # Nugget biscuit nugget in a biscuit.
         e = '2003'
       build.push {id: e, count: count}
@@ -335,8 +336,16 @@

     return builds

-  # TODO: Use lodash templating for type key.
+
   # Generates item set for Combinded sets (with both Most Frequent and Highest Wins on one page)
+  templates = {
+    combindedStart: _.template('Frequent/Highest Start (<%- wins %> wins - <%- games %> games)')
+    combinedCore: _.template('Frequent/Highest Core (<%- wins %> wins - <%- games %> games)')
+    freqStart: _.template('Most Frequent Starters (<%- wins %> wins - <%- games %> games)')
+    freqCore: _.template('Most Frequent Core Build (<%- wins %> wins - <%- games %> games)')
+    highestStart: _.template('Highest Win % Starters (<%- wins %> wins - <%- games %> games)')
+    highestCore: _.template('Highest Win % Core Build (<%- wins %> wins - <%- games %> games)')
+  }
   normalItemSets = () ->
     builds = []

@@ -344,34 +353,34 @@
     if JSON.stringify(freqStart.build) == JSON.stringify(highestStart.build)
       builds.push {
         items: freqStart.build
-        type: 'Frequent/Highest Start ('+freqStart.wins+' wins - '+freqStart.games+ ' games)'
+        type: templates.combindedStart({wins: freqStart.wins, games: freqStart.games})
       }

     else
       builds.push {
         items: freqStart.build
-        type: 'Most Frequent Starters ('+freqStart.wins+' wins - '+freqStart.games+ ' games)'
+        type: templates.freqStart({wins: freqStart.wins, games: freqStart.games})
       }
       builds.push {
         items: highestStart.build
-        type: 'Highest Win % Starters ('+highestStart.wins+' wins - '+highestStart.games+ ' games)'
+        type: templates.highestStart({wins: highestStart.wins, games: highestStart.games})
       }

     # If freqCore and highestCore are the same, only push once.
     if JSON.stringify(freqCore.build) == JSON.stringify(highestCore.build)
       builds.push {
         items: freqCore.build
-        type: 'Frequent/Highest Core ('+freqCore.wins+' wins - '+freqCore.games+ ' games)'
+        type: templates.combinedCore({wins: freqCore.wins, games: freqCore.games})
       }

     else
       builds.push {
         items: freqCore.build
-        type: 'Most Frequent Core Build ('+freqCore.wins+' wins - '+freqCore.games+ ' games)'
+        type: templates.freqCore({wins: freqCore.wins, games: freqCore.games})
       }
       builds.push {
         items: highestCore.build
-        type: 'Highest Win % Core Build ('+highestCore.wins+' wins - '+highestCore.games+ ' games)'
+        type: templates.highestCore({wins: highestCore.wins, games: highestCore.games})
       }

     # Add trinkets and consumables, if enabled.
@@ -386,20 +395,20 @@

     mfBuild.push {
       items: freqStart.build
-      type: 'Most Frequent Starters ('+freqStart.wins+' wins - '+freqStart.games+ ' games)'
+      type: templates.freqStart({wins: freqStart.wins, games: freqStart.games})
     }
     mfBuild.push {
       items: freqCore.build
-      type: 'Most Frequent Core Build ('+freqCore.wins+' wins - '+freqCore.games+ ' games)'
+      type: templates.freqCore({wins: freqCore.wins, games: freqCore.games})
     }

     hwBuild.push {
       items: highestStart.build
-      type: 'Highest Win % Starters ('+highestStart.wins+' wins - '+highestStart.games+ ' games)'
+      type: templates.highestStart({wins: highestStart.wins, games: highestStart.games})
     }
     hwBuild.push {
       items: highestCore.build
-      type: 'Highest Win % Core Build ('+highestCore.wins+' wins - '+highestCore.games+ ' games)'
+      type: templates.highestCore({wins: highestCore.wins, games: highestCore.games})
     }

     mfBuild = trinksCon(mfBuild)
@@ -416,8 +425,7 @@
       blocks: build
     }

-    window.champData[champ][positionForFile] = hlp.mergeObj(defaultSchema, newObj)
-
+    window.champData[champ][positionForFile] = _.merge({}, [defaultSchema, newObj])

   # Save data to Global object for saving to disk later.
   # We do this incase people cancel the function half way though.
@@ -441,7 +449,8 @@

   # TODO: Lodash map.
   # Now we execute for the other positions for the champs, if there are any.
-  if !obj.position and positions.length > 0
+
+  if !champ_info.position and positions.length > 0
     positions = positions.map (e) ->
       return {champ: champ, position: e}

@@ -475,7 +484,7 @@
     getSettings
     getRiotVer
     getChamps
-    asyncRequestChamps
+    requestChamps
     deleteOldBuilds
     saveToFile
     notProcessed


labels and ids #2

labels and ids #2

--- src/plugins/noflo.coffee
+++ src/plugins/noflo.coffee
@@ -9,6 +9,8 @@
     # Modify behavior of other Dataflow plugins
     @dataflow.plugins.source.listeners false
     @dataflow.plugins.log.listeners false
+
+    window.df = @dataflow

   registerGraph: (graph, runtime, callback) ->
     dfGraph = @dataflow.loadGraph {}
@@ -84,18 +86,44 @@
         graph.nofloGraph.removeEdge edge.from.node, edge.from.port, edge.to.node, edge.to.port

   subscribeDataflowNode: (node, graph) ->
-    unless node.nofloNode
-      # Ensure IDs are strings
-      id = node.id + ''
-      node.nofloNode = graph.nofloGraph.getNode id
-      unless node.nofloNode
-        node.nofloNode = graph.nofloGraph.addNode id, node.type,
-          x: node.get 'x'
-          y: node.get 'y'
+
+    randomString = (num) ->
+      unless num?
+        num = 60466176 # 36^5
+      num = Math.floor( Math.random() * num );
+      return num.toString(36);
+
+    unless node.nofloNode?
+      # Ensure IDs are unique strings
+      # id = node.id+""
+      id = node.type + '_' + randomString()
+      while graph.nofloGraph.getNode(id)?
+        id = node.type + '_' + randomString()
+      node.set("nofloId", id)
+      # Sync label
+      label = node.get("label");
+      unless label?
+        label = node.type
+      nofloNode = graph.nofloGraph.addNode id, node.type,
+        x: node.get 'x'
+        y: node.get 'y'
+        label: label
+
+      # Reference one another
+      node.nofloNode = nofloNode
+      nofloNode.dataflowNode = node
+

     node.on 'change:label', (node, newName) ->
+      # Change label
+      node.nofloNode.metadata.label = newName
+      # Change id
       oldName = node.nofloNode.id
-      graph.nofloGraph.renameNode oldName, newName + ''
+      # Ensure unique
+      newId = newName + '_' + randomString()
+      while graph.nofloGraph.getNode(newId)?
+        newId = newName + '_' + randomString()
+      graph.nofloGraph.renameNode oldName, newId
     node.on 'change:x change:y', ->
       node.nofloNode.metadata.x = node.get 'x'
       node.nofloNode.metadata.y = node.get 'y'
@@ -177,20 +205,23 @@
   addNode: (node, graph) ->
     return unless node

+    return if node.dataflowNode?
+
+    return if graph.nodes.findWhere({nofloId: node.id})?
+
     @addNodeRuntime node, graph.nofloGraph.runtime

-    unless node.dataflowNode
-      # Load the component
-      dfNode = graph.nofloGraph.runtime.getComponentInstance node.component,
-        id: node.id
-        label: node.id
-        x: ( if node.metadata.x? then node.metadata.x else 300 )
-        y: ( if node.metadata.y? then node.metadata.y else 300 )
-        parentGraph: graph
-
-      # Reference one another
-      node.dataflowNode = dfNode
-      node.nofloNode = node
+    # Load the component
+    dfNode = graph.nofloGraph.runtime.getComponentInstance node.component,
+      id: node.id
+      label: ( if node.metadata.label? then node.metadata.label else node.id )
+      x: ( if node.metadata.x? then node.metadata.x else 500 )
+      y: ( if node.metadata.y? then node.metadata.y else 300 )
+      parentGraph: graph
+
+    # Reference one another
+    node.dataflowNode = dfNode
+    dfNode.nofloNode = node

     graph.nodes.add dfNode

@@ -213,7 +244,7 @@
         parentGraph: graph
         source: edge.from
         target: edge.to
-        route: if edge.metadata.route then edge.metadata.route else 0
+        route: if edge.metadata.route? then edge.metadata.route else 0

       # Reference one another
       dfEdge.nofloEdge = edge


Added a Skateboard for level 8

Added a Skateboard for level 8


Fix require statement for clark

Fix require statement for clark

--- src/scripts/clark.coffee
+++ src/scripts/clark.coffee
@@ -13,7 +13,7 @@
 # Author:
 #   ajacksified

-clark = require('clark').clark
+clark = require 'clark'

 module.exports = (robot) ->
   robot.respond /clark (.*)/i, (msg) ->


Simplified hound commands, moved from slash command to chat response

--- src/scripts/hound.coffee
+++ src/scripts/hound.coffee
@@ -127,126 +127,114 @@
                 hound status for the morning"
     Logger.logToChannel response, 'ibizan-diagnostics'

-  robot.router.post '/ibizan/diagnostics/hound', (req, res) ->
-    body = req.body
-    if body.token is process.env.SLASH_HOUND_TOKEN
-      comps = body?.text?.split(' ') || []
-      scope = comps[0] || 'self'
-      if scope is Organization.name
-        scope = 'org'
-      else if scope is body.user_name
-        scope = 'self'
-      else if scope isnt 'self' and scope isnt 'org'
-        if not isNaN(comps[0]) and
-           (comps[1] is 'hour' or comps[1] is 'hours')
-          comps = ['self', comps.join(' ')]
-        else if comps.length > 2
-          comps = ['self', comps.slice(1).join(' ')]
-        else
-          comps = ['self', comps[0]]
-        scope = comps[0]
-      action = comps[1] || 'info'
-
-      if scope is 'self'
-        user = Organization.getUserBySlackName body.user_name
-        if not user
-          # TODO: Improve output
-          err = "User not found"
-          res.status 500
-        else if match = action.match /((0+)?(?:\.+[0-9]*) hours?)|(0?1 hour)|(1+(?:\.+[0-9]*)? hours)|(0?[2-9]+(?:\.+[0-9]*)? hours)|([1-9][0-9]+(?:\.+[0-9]*)? hours)/i
-          block_str = match[0].replace('hours', '').replace('hour', '').trimRight()
-          block = parseFloat block_str
-          user.settings.fromSettings {
-            houndFrequency: block
-          }
-          res.status 200
-          response = "Hounding frequency set to be every #{block} hours during your active time."
-        else if action is 'start'
-          user.settings.fromSettings {
-            shouldHound: true,
-            shouldResetHound: true
-          }
-          res.status 200
-          response = "Hounding is on."
-        else if action is 'stop'
-          user.settings.fromSettings {
-            shouldHound: false
-          }
-          res.status 200
-          response = "Hounding is off for the rest of today."
-        else if action is 'enable'
-          user.settings.fromSettings {
-            shouldHound: true,
-            shouldResetHound: true
-          }
-          res.status 200
-          response = "Enabled hounding."
-        else if action is 'disable'
-          user.settings.fromSettings {
-            shouldHound: false,
-            shouldResetHound: false
-          }
-          res.status 200
-          response = "Disabled hounding. You will not be hounded until you turn this setting back on."
-        else if action is 'reset'
-          user.settings.fromSettings {
-            houndFrequency: Organization.houndFrequency
-          }
-          res.status 200
-          response = "Reset your hounding status to organization defaults (#{Organization.houndFrequency} hours)."
-        else
-          status = if user.settings.shouldHound then 'on' else 'off'
-          status = if user.settings.shouldResetHound then status else 'disabled'
-          if status is 'on'
-            status += ", and is set to ping every #{user.settings.houndFrequency} hours while active"
-          res.status 200
-          response = "Hounding is #{status}."
-      else if scope is 'org'
-        if not Organization.ready()
-          res.status 500
-          response = "Organization is not ready for operation"
-        if match = action.match /((0+)?(?:\.+[0-9]*) hours?)|(0?1 hour)|(1+(?:\.+[0-9]*)? hours)|(0?[2-9]+(?:\.+[0-9]*)? hours)|([1-9][0-9]+(?:\.+[0-9]*)? hours)/i
-          block_str = match[0].replace('hours', '').replace('hour', '').trimRight()
-          block = parseFloat block_str
-          Organization.setHoundFrequency(+block.toFixed(2))
-          res.status 200
-          response = "Hounding frequency set to every #{block} hours for #{Organization.name}, time until next hound reset."
-        else if action is 'start'
-          Organization.shouldHound = true
-          Organization.shouldResetHound = true
-          res.status 200
-          response = "Hounding is on for the organization."
-        else if action is 'stop'
-          Organization.shouldHound = false
-          res.status 200
-          response = "Hounding is off for the organization."
-        else if action is 'enable'
-          Organization.shouldHound = true
-          Organization.shouldResetHound = true
-          res.status 200
-          response = "Enabled hounding for #{Organization.name}."
-        else if action is 'disable'
-          Organization.shouldHound = false
-          Organization.shouldResetHound = false
-          res.status 200
-          response = "Disabled hounding for #{Organization.name}."
-        else if action is 'reset'
-          Organization.resetHounding()
-          res.status 200
-          response = "Reset hounding status for all #{Organization.name} employees."
-        else
-          status = if Organization.shouldHound then 'on' else 'off'
-          status = if Organization.shouldResetHound then status else 'disabled'
-          if status is 'on'
-            status += ", and is set to ping every #{Organization.houndFrequency} hours while active"
-          res.status 200
-          response = "Hounding is #{status}."
-      else
-        res.status 500
-        response = "Bad command: #{body.text}"
+  robot.respond /hound (.*)/i, (res) ->
+    command = res.match[1]
+    comps = command.split(' ') || []
+    scope = comps[0] || 'self'
+    if scope is Organization.name
+      scope = 'org'
+    else if scope is res.message.user.name
+      scope = 'self'
+    else if scope isnt 'self' and scope isnt 'org'
+      if not isNaN(comps[0]) and
+         (comps[1] is 'hour' or comps[1] is 'hours')
+        comps = ['self', comps.join(' ')]
+      else if comps.length > 2
+        comps = ['self', comps.slice(1).join(' ')]
+      else
+        comps = ['self', comps[0]]
+      scope = comps[0]
+    action = comps[1] || 'unknown'
+
+    if scope is 'self'
+      user = Organization.getUserBySlackName res.message.user.name
+      if not user
+        user.directMessage "#{res.message.user.name} is not a valid user", Logger
+        Logger.addReaction 'x', res.message
+      else if match = action.match /((0+)?(?:\.+[0-9]*) hours?)|(0?1 hour)|(1+(?:\.+[0-9]*)? hours)|(0?[2-9]+(?:\.+[0-9]*)? hours)|([1-9][0-9]+(?:\.+[0-9]*)? hours)/i
+        block_str = match[0].replace('hours', '').replace('hour', '').trimRight()
+        block = parseFloat block_str
+        user.settings.fromSettings {
+          houndFrequency: block
+        }
+        user.directMessage "Hounding frequency set to be every #{block} hours during your active time.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'start' or action is 'on' or action is 'enable'
+        user.settings.fromSettings {
+          shouldHound: true,
+          shouldResetHound: true
+        }
+        user.directMessage "Hounding is now *on*.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'stop' or action is 'off' or action is 'disable'
+        user.settings.fromSettings {
+          shouldHound: false,
+          shouldResetHound: false
+        }
+        user.directMessage "Hounding is now *off*. You will not be hounded until you turn this setting back on.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'pause'
+        user.settings.fromSettings {
+          shouldHound: false,
+          shouldResetHound: true
+        }
+        user.directMessage "Hounding is now *paused*. Hounding will resume tomorrow.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'reset'
+        user.settings.fromSettings {
+          houndFrequency: Organization.houndFrequency
+        }
+        user.directMessage "Reset your hounding status to organization defaults *(#{Organization.houndFrequency} hours)*.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'status' or action is 'info'
+        status = if user.settings.shouldHound then 'on' else 'off'
+        status = if user.settings.shouldResetHound then status else 'disabled'
+        if status is 'on'
+          status += ", and is set to ping every *#{user.settings.houndFrequency} hours* while active"
+        user.directMessage "Hounding is #{status}.", Logger
+        Logger.addReaction 'dog2', res.message
+      else
+        user.directMessage "I couldn't understand you. Try something like `hound (self/org) (on/off/pause/reset/status/X hours)`", Logger
+        Logger.addReaction 'x', res.message
+    else if scope is 'org'
+      if not Organization.ready()
+        user.directMessage "Organization is not ready", Logger
+        Logger.addReaction 'x', res.message
+      if match = action.match /((0+)?(?:\.+[0-9]*) hours?)|(0?1 hour)|(1+(?:\.+[0-9]*)? hours)|(0?[2-9]+(?:\.+[0-9]*)? hours)|([1-9][0-9]+(?:\.+[0-9]*)? hours)/i
+        block_str = match[0].replace('hours', '').replace('hour', '').trimRight()
+        block = parseFloat block_str
+        Organization.setHoundFrequency(+block.toFixed(2))
+        user.directMessage "Hounding frequency set to every #{block} hours for #{Organization.name}, time until next hound reset.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'start' or action is 'enable' or action is 'on'
+        Organization.shouldHound = true
+        Organization.shouldResetHound = true
+        user.directMessage "Hounding is now *on* for the organization.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'stop' or action is 'disable' or action is 'off'
+        Organization.shouldHound = false
+        Organization.shouldResetHound = false
+        user.directMessage "Hounding is now *off* for the organization. Hounding status will not reset until it is reactivated.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'pause'
+        Organization.shouldHound = false
+        Organization.shouldResetHound = true
+        user.directMessage "Hounding is now *paused* for the organization. Hounding will resume tomorrow.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'reset'
+        Organization.resetHounding()
+        user.directMessage "Reset hounding status for all #{Organization.name} employees.", Logger
+        Logger.addReaction 'dog2', res.message
+      else if action is 'status' or action is 'info'
+        status = if Organization.shouldHound then 'on' else 'off'
+        status = if Organization.shouldResetHound then status else 'disabled'
+        if status is 'on'
+          status += ", and is set to ping every #{Organization.houndFrequency} hours while active"
+        user.directMessage "Hounding is #{status}.", Logger
+        Logger.addReaction 'dog2', res.message
+      else
+        user.directMessage "I couldn't understand you. Try something like `hound (self/org) (on/off/pause/reset/status/X hours)`", Logger
+        Logger.addReaction 'x', res.message
     else
-      res.status 401
-      response = "Bad token in Ibizan configuration"
-    res.json {
-      "text": response
-    }+      user.directMessage "I couldn't understand you. Try something like `hound (self/org) (on/off/pause/reset/status/X hours)`", Logger
+      Logger.addReaction 'x', res.message


Revert "httpd.coffee - register routes with robot.name"

This reverts commit 0fbcf742c4d97d1c1071144446d2d03efb0b9baf.

--- src/scripts/httpd.coffee
+++ src/scripts/httpd.coffee
@@ -21,22 +21,22 @@

 module.exports = (robot) ->

-  robot.router.get "/#{robot.name}/version", (req, res) ->
+  robot.router.get "/hubot/version", (req, res) ->
     res.end robot.version

-  robot.router.post "/#{robot.name}/ping", (req, res) ->
+  robot.router.post "/hubot/ping", (req, res) ->
     res.end "PONG"

-  robot.router.get "/#{robot.name}/time", (req, res) ->
+  robot.router.get "/hubot/time", (req, res) ->
     res.end "Server time is: #{new Date()}"

-  robot.router.get "/#{robot.name}/info", (req, res) ->
+  robot.router.get "/hubot/info", (req, res) ->
     child = spawn('/bin/sh', ['-c', "echo I\\'m $LOGNAME@$(hostname):$(pwd) \\($(git rev-parse HEAD)\\)"])

     child.stdout.on 'data', (data) ->
       res.end "#{data.toString().trim()} running node #{process.version} [pid: #{process.pid}]"
       child.stdin.end()

-  robot.router.get "/#{robot.name}/ip", (req, res) ->
+  robot.router.get "/hubot/ip", (req, res) ->
     robot.http('http://ifconfig.me/ip').get() (err, r, body) ->
       res.end body


Promise the routes

Promise the routes

--- src/server/server.coffee
+++ src/server/server.coffee
@@ -40,12 +40,12 @@
 app.use passport.initialize()
 app.use passport.session()

-routes.forEach (rt) ->
-	app[rt.verb] rt.path, rt.fn
-
-app.use exceptionHandler
-
 database.init()
+	.then ->
+		return Promise.all routes.map (rt) ->
+			return Promise.resolve app[rt.verb] rt.path, rt.fn
+	.then ->
+		return Promise.resolve app.use exceptionHandler
 	.then ->
 		return Promise.resolve app.listen port
 	.then ->


Blog link Link to the BHL blog.

Blog link
Link to the BHL blog.

--- project.coffee
+++ project.coffee
@@ -65,7 +65,7 @@

   externalLinks:
     Talk: 'http://talk.penguinwatch.org'
-    Blog: 'http://blog.zooniverse.org'
+    Blog: 'http://blog.biodiversitylibrary.org/'
     Twitter: 'http://twitter.com/the_zooniverse'

   tasks:


Changed unicode arrows to long versions

Changed unicode arrows to long versions


AttachedPopover - Minor bugfix.

AttachedPopover - Minor bugfix.

--- lib/services/attached-popover.coffee
+++ lib/services/attached-popover.coffee
@@ -52,11 +52,10 @@
      * Shows the popover with the specified text after the specified delay (in miliiseconds). Calling this method
      * multiple times will cancel previous show requests and restart.
      *
-     * @param {string} text       The text to display.
      * @param {int}    delay      The delay before the tooltip shows up (in milliseconds).
      * @param {int}    fadeInTime The amount of time to take to fade in the tooltip.
     ###
-    showAfter: (text, delay, fadeInTime = 100) ->
+    showAfter: (delay, fadeInTime = 100) ->
         @timeoutId = setTimeout(() =>
-            @show(text, fadeInTime)
+            @show(fadeInTime)
         , delay)


Simplify ID uniqueness check

Simplify ID uniqueness check

--- src/css-selector-generator.coffee
+++ src/css-selector-generator.coffee
@@ -41,7 +41,7 @@
     return false if /^\d/.exec id

     # ID must be unique
-    return false unless document.querySelectorAll("##{id}").length is 1
+    return document.querySelectorAll("##{id}").length is 1

     true


TODO: Implement remaining 46 inputType methods

--- src/trix/controllers/level_2_input_controller.coffee
+++ src/trix/controllers/level_2_input_controller.coffee
@@ -19,11 +19,17 @@
       console.log("[#{event.type}] #{JSON.stringify(event.data)}")
       Promise.resolve().then(console.groupEnd)

-  insertText: (event) ->
-    textAdded = event.data
-    @delegate?.inputControllerWillPerformTyping()
-    @responder?.insertString(textAdded)
-    {textAdded}
+  # https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes
+
+  deleteByComposition: (event) ->
+
+  deleteByCut: (event) ->
+
+  deleteByDrag: (event) ->
+
+  deleteCompositionText: (event) ->
+
+  deleteContent: (event) ->

   deleteContentBackward: (event) ->
     textDeleted = [event.getTargetRanges()...].map(staticRangeToRange).join("")
@@ -31,6 +37,94 @@
     @responder?.deleteInDirection("backward")
     {textDeleted}

+  deleteContentForward: (event) ->
+
+  deleteEntireSoftLine: (event) ->
+
+  deleteHardLineBackward: (event) ->
+
+  deleteHardLineForward: (event) ->
+
+  deleteSoftLineBackward: (event) ->
+
+  deleteSoftLineForward: (event) ->
+
+  deleteWordBackward: (event) ->
+
+  deleteWordForward: (event) ->
+
+  formatBackColor: (event) ->
+
+  formatBold: (event) ->
+
+  formatFontColor: (event) ->
+
+  formatFontName: (event) ->
+
+  formatIndent: (event) ->
+
+  formatItalic: (event) ->
+
+  formatJustifyCenter: (event) ->
+
+  formatJustifyFull: (event) ->
+
+  formatJustifyLeft: (event) ->
+
+  formatJustifyRight: (event) ->
+
+  formatOutdent: (event) ->
+
+  formatRemove: (event) ->
+
+  formatSetBlockTextDirection: (event) ->
+
+  formatSetInlineTextDirection: (event) ->
+
+  formatStrikeThrough: (event) ->
+
+  formatSubscript: (event) ->
+
+  formatSuperscript: (event) ->
+
+  formatUnderline: (event) ->
+
+  historyRedo: (event) ->
+
+  historyUndo: (event) ->
+
+  insertCompositionText: (event) ->
+
+  insertFromComposition: (event) ->
+
+  insertFromDrop: (event) ->
+
+  insertFromPaste: (event) ->
+
+  insertFromYank: (event) ->
+
+  insertHorizontalRule: (event) ->
+
+  insertLineBreak: (event) ->
+
+  insertLink: (event) ->
+
+  insertOrderedList: (event) ->
+
+  insertParagraph: (event) ->
+
+  insertReplacementText: (event) ->
+
+  insertText: (event) ->
+    textAdded = event.data
+    @delegate?.inputControllerWillPerformTyping()
+    @responder?.insertString(textAdded)
+    {textAdded}
+
+  insertTranspose: (event) ->
+
+  insertUnorderedList: (event) ->
+
 staticRangeToRange = (staticRange) ->
   range = document.createRange()
   range.setStart(staticRange.startContainer, staticRange.startOffset)


createIdentityClass was a bad idea

createIdentityClass was a bad idea

--- app/react_abuse.coffee
+++ app/react_abuse.coffee
@@ -34,7 +34,7 @@
   render: ->
     React.DOM.div {}, @state.value

-SimpleLayoutComponent = createIdentityClass
+SimpleLayoutComponent = React.createClass
   displayName: 'SimpleLayoutComponent'
   mixins: [React.addons.PureRenderMixin]
   render: ->


fix cursor problem in edit fields

fix cursor problem in edit fields

--- lib/views/edit/table-row-view.coffee
+++ lib/views/edit/table-row-view.coffee
@@ -10,8 +10,6 @@
     @selectButton = $$ ->
       @input {type: 'checkbox', class: "select input-checkbox"}
     @selectButton.click (e) => @clickSelect(e);
-    #@selectButton.on 'mousedown', (e) -> e.preventDefault();
-    #@selectButton.on 'mouseup', (e) -> e.preventDefault();
     jselect.append(@selectButton);
     @appendChild(selectElement);

@@ -24,8 +22,6 @@
       editor.getModel().setSoftTabs(true);
       editor.getModel().setSoftWrapped(true);
       editor.getModel().setLineNumberGutterVisible(false);
-      #editor = $$ ->
-      #  @textarea {class: "input-textarea", rows: 1}
       jtd = $(td);
       jtd.append(editor);
       @appendChild(td);
@@ -40,10 +36,10 @@
     jdelete.append(deleteButton);
     @appendChild(deleteElement);

-    #$(@).on 'click', (e) => @tableView.move(@) if @ == e.currentTarget
-    $(@).click (e) => @tableView.move(@) if @ == e.currentTarget
-    #$(@).on 'mousedown', (e) -> e.preventDefault();
-    #$(@).on 'mouseup', (e) -> e.preventDefault();
+    $(@).click (e) =>
+      if @.name == e.target.name or @.name == e.target.parentElement?.name
+        @tableView.move(@)
+        e.stopPropagation()

   setChecked: (checkbox, checked) ->
     if checkbox?
@@ -57,8 +53,6 @@
     e.stopPropagation()

   select: (value) ->
-    #@selectButton.prop("checked", value)
-    #@selectButton.val(value)
     @setChecked @selectButton, value
     @select_(value)

@@ -79,13 +73,11 @@
       value = values[i]
       if !value?
         value = ''
-      #$(@editors[i]).val(value)
       @editors[i].getModel().setText(value);

   getValues: ->
     values = [];
     for editor in @editors
-      #values.push($(editor).val())
       values.push(editor.getModel().getText());
     return values;


Refactor styleguideFilePath

Refactor styleguideFilePath

--- index.coffee
+++ index.coffee
@@ -63,7 +63,7 @@
       cssVinyl = new Vinyl
         cwd: file.cwd
         base: file.base
-        path: "#{styleguideFilePath}.css"
+        path: styleguideCssFilePath
         contents: new Buffer($('style').html())
       @push cssVinyl


setInterval to save.

setInterval to save.

--- index.coffee
+++ index.coffee
@@ -74,14 +74,19 @@

 player = null

+save = ->
+  localForage.setItem (md5 player.game), player.core.serialize() if player
+
 stop = ->
   player.stop()
-  localForage.setItem (md5 player.game), player.core.serialize()
+  save()
   player.core.unload_game()
   player.core.deinit()
   player = null

-addEventListener 'beforeunload', ->
+window.setInterval save, 10000
+
+window.addEventListener 'beforeunload', ->
   stop() if player

 load = (file) ->
@@ -114,7 +119,7 @@
         document.getElementById('draghint').classList.remove 'hidden'
     reader.readAsArrayBuffer file

-addEventListener 'drop', (event) ->
+window.addEventListener 'drop', (event) ->
   event.preventDefault()
   document.getElementById('draghint').classList.remove 'hover'
   if event.dataTransfer.files.length > 0
@@ -122,17 +127,17 @@
   # add URI support
   false

-addEventListener 'dragover', (event) ->
+window.addEventListener 'dragover', (event) ->
   event.preventDefault()
   document.getElementById('draghint').classList.add 'hover'
   false

-addEventListener 'dragleave', (event) ->
+window.addEventListener 'dragleave', (event) ->
   event.preventDefault()
   document.getElementById('draghint').classList.remove 'hover'
   false

-addEventListener 'click', (event) ->
+window.addEventListener 'click', (event) ->
   document.getElementById('chooser').click() if not player

 document.getElementById('chooser').addEventListener 'change', ->


add debug messages

add debug messages

--- index.coffee
+++ index.coffee
@@ -29,6 +29,7 @@
     options = @getDefaultRequestOptions()

     @request.get "#{@urlBase}/v2/devices/#{deviceUuid}", options, (error, response, body) ->
+      debug "device", error, body
       return callback error if error?
       return callback new Error(body.error.message) if body?.error?
       return callback new Error(body.message || body) if response.statusCode != 200
@@ -40,6 +41,7 @@
     options.qs = query

     @request.get "#{@urlBase}/devices", options, (error, response, body) ->
+      debug "devices", error, body
       return callback error if error?
       return callback new Error(body.error) if body?.error?

@@ -50,6 +52,7 @@
     options.qs = query

     @request.get "#{@urlBase}/mydevices", options, (error, response, body) ->
+      debug "mydevices", error, body
       return callback error if error?
       return callback new Error(body.error) if body?.error?

@@ -59,6 +62,7 @@
     options = @getDefaultRequestOptions()

     @request.post "#{@urlBase}/devices/#{deviceUuid}/tokens", options, (error, response, body) ->
+      debug "generateAndStoreToken", error, body
       return callback error if error?
       return callback new Error(body.error.message) if body?.error?

@@ -70,6 +74,7 @@

     debug 'POST', "#{@urlBase}/messages", options
     @request.post "#{@urlBase}/messages", options, (error, response, body) ->
+      debug "message", error, body
       return callback error if error?
       return callback new Error(body.error) if body?.error?

@@ -79,6 +84,7 @@
     options = @getDefaultRequestOptions()

     @request.del "#{@urlBase}/devices/#{deviceUuid}/tokens/#{deviceToken}", options, (error, response, body) ->
+      debug "revokeToken", error, body
       return callback error if error?
       return callback new Error(body.error.message) if body?.error?
       callback null


Update to new plugin syntax.

Update to new plugin syntax.

--- index.coffee
+++ index.coffee
@@ -1,12 +1,14 @@
+impromptu = require 'impromptu'
+git = require 'impromptu-git'
+
 async = require 'async'
 read = require 'read'
 request = require 'request'

-module.exports = (Impromptu, register, github) ->
-  git = @module.require 'impromptu-git'
+module.exports = impromptu.plugin.create (github) ->
   rGitHubUrl = /^(?:git@github.com:|https:\/\/github.com\/)([^\/]+)\/([^\/]+)\.git/

-  register '_parseRemoteUrl',
+  github.register '_parseRemoteUrl',
     update: (done) ->
       git.remoteUrl (err, url) ->
         return done err if err
@@ -18,22 +20,22 @@
           user: results[1]
           repo: results[2]

-  register 'isGitHub',
+  github.register 'isGitHub',
     update: (done) ->
       github._parseRemoteUrl (err, results) ->
         done err, !!results

-  register 'remoteUser',
+  github.register 'remoteUser',
     update: (done) ->
       github._parseRemoteUrl (err, results) ->
         done err, results?.user

-  register 'remoteRepo',
+  github.register 'remoteRepo',
     update: (done) ->
       github._parseRemoteUrl (err, results) ->
         done err, results?.repo

-  register 'token',
+  github.register 'token',
     update: (done) ->
       process.env.IMPROMPTU_GITHUB_TOKEN

@@ -52,7 +54,7 @@
       options.qs.access_token = token
       request options, done

-  register 'ci',
+  github.register 'ci',
     cache: 'repository'
     expire: 300
     update: (done) ->
@@ -68,7 +70,7 @@
             return done err, '' unless body and body.length
             done err, body[0].state

-  register 'pullRequest',
+  github.register 'pullRequest',
     cache: 'repository'
     expire: 300
     update: (done) ->


Comment out Update.com spawning

Comment out Update.com spawning

--- index.coffee
+++ index.coffee
@@ -50,16 +50,16 @@
     spawn {cmd, args}, (error, result, code) ->
       return done(error) if error?

-      nupkgPath = path.join(outputDirectory, "#{metadata.name}.#{metadata.version}.nupkg")
-
-      cmd = path.resolve(__dirname, '..', 'vendor', 'Update.com')
-      args = [
-        '--releasify'
-        nupkgPath
-        '--releaseDir'
-        outputDirectory
-        '--loadingGif'
-        loadingGif
-      ]
-
-      spawn {cmd, args}, (error, result, code) -> done(error)
+      # nupkgPath = path.join(outputDirectory, "#{metadata.name}.#{metadata.version}.nupkg")
+      #
+      # cmd = path.resolve(__dirname, '..', 'vendor', 'Update.com')
+      # args = [
+      #   '--releasify'
+      #   nupkgPath
+      #   '--releaseDir'
+      #   outputDirectory
+      #   '--loadingGif'
+      #   loadingGif
+      # ]
+      #
+      # spawn {cmd, args}, (error, result, code) -> done(error)


Adding support for custom IP when validating request

--- index.coffee
+++ index.coffee
@@ -64,10 +64,7 @@
 # REQUEST_END

   validateRequest:(req, ip)->
-    if ip is true
-      return @validate(req.body['g-recaptcha-response'],req.ip)
-    else
-      return @validate(req.body['g-recaptcha-response'])
+    return @validate(req.body['g-recaptcha-response'],ip)

   translateErrors:(errorCodes)->
     if Array.isArray errorCodes


Remove sharejs from index

Remove sharejs from index

--- index.coffee
+++ index.coffee
@@ -6,7 +6,6 @@
 nconf = require("nconf")
 authom = require("authom")
 request = require("request")
-sharejs = require("share")
 lactate = require("lactate")
 path = require("path")
 xmlbuilder = require("xmlbuilder")
@@ -68,10 +67,6 @@
   "url": nconf.get("url")
   "package": pkginfo
   "bootstrap": null
-
-sharejs.server.attach app,
-  db:
-    type: "none"

 app.use app.router


Return closest to original form.

Return closest to original form.

--- src/core/javascripts/controllers/duration_list.js.coffee
+++ src/core/javascripts/controllers/duration_list.js.coffee
@@ -54,6 +54,7 @@

   $scope.selectDuration = (dur, route) =>
+    $scope.duration = dur
     $scope.bb.current_item.setDuration(dur.value)
     if $scope.$parent.$has_page_control
       return


Builder: a more obvious log for framework compilation message

--- Builder.coffee
+++ Builder.coffee
@@ -67,10 +67,17 @@
     @config ?= require('koding-config-manager').load("main.#{options.configFile}")
     cmd = "cd client/Framework && npm i && gulp compile --buildVersion=#{@config.client.version} --outputDir=../../website/a/"
     exec cmd, (err, stdout, stderr)->
-      console.warn "------------------------ FRAMEWORK COMPILED -------------------------- "
-      console.warn " To use watcher for Framework use following command in different tab:  "
-      console.log  " $ #{cmd.replace 'compile ', ''} "
-      console.warn "------------------------ FRAMEWORK COMPILED -------------------------- "
+      console.log """\n\n
+      ################################### FRAMEWORK COMPILED #################################
+
+
+       To use watcher for Framework use following command in different tab:
+       $ #{cmd.replace 'compile ', ''}
+
+
+      ################################### FRAMEWORK COMPILED #################################
+      \n
+      """

   buildClient: (options) ->
     @config ?= require('koding-config-manager').load("main.#{options.configFile}")


remove a stupid br

remove a stupid br

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -1562,7 +1562,6 @@

     render_share_defn: ->

-

             Shared items are visible to anybody.


Changed front end eligibility requirements.

--- picoCTF-web/web/coffee/registration.coffee
+++ picoCTF-web/web/coffee/registration.coffee
@@ -10,24 +10,25 @@

 setRequired = ->
-    $('#user-registration-form :input').each () ->
+    $('#user-registration-form :input').each () ->
         if not $(this).is(':checkbox')
             if not $(this).is(':radio')
-                $(this).prop('required', $(this).is(":visible"))
-
-
+                $(this).prop('required', $(this).is(":visible"))
+
+
 checkEligibility = ->
     is_us = $("#country-select").val() in ["US", ""]
     is_k12 = $("#background-select").val() in ["student_el", "student_ms", "student_hs", "student_home"]
     is_student = $("#background-select").val() in ["student_el", "student_ms", "student_hs", "student_home", "student_undergrad", "student_grad"]
-
-    if not (is_us and is_k12)
+
+    # This should be changed to reflect your eligibility requirements
+    if not is_k12
         $("#eligibility-warning").show()
         $("#adviser-group").hide()
     else
         $("#eligibility-warning").hide()
         $("#adviser-group").show()
-
+
     if is_student
         $("#school-group").show()
         $("#button-adviser").hide()
@@ -36,14 +37,14 @@
     else
         $("#school-group").hide()
         $("#button-adviser").show()
-
+
     setRequired()

 submitRegistration = (e) ->
   e.preventDefault()

   registrationData = $("#user-registration-form").serializeObject()
-  creatingNewTeam = $("#registration-new-team-page").is(":visible")
+  creatingNewTeam = $("#registration-new-team-page").is(":visible")
   creatingTeacherAccount = $("#registration-adviser-page").is(":visible")
   registrationData["create-new-team"] = creatingNewTeam
   registrationData["create-new-teacher"] = creatingTeacherAccount
@@ -93,15 +94,15 @@
   # between these pages. If one gets longer or shorter, we need to tweek it

   offset = 0 # Not sure why this value is necessary. Check later
-  $("#button-new-team").click () ->
+  $("#button-new-team").click () ->
     #$("#stretch-box").css("min-height", $("#stretch-box").height()+offset)
     $("#registration-join-team-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
         $("#registration-adviser-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
-            $("#registration-new-team-page").show "slide", { direction: "up" }, pageTransitionSpeed, () ->
+            $("#registration-new-team-page").show "slide", { direction: "up" }, pageTransitionSpeed, () ->
                 ga('send', 'event', 'Registration', 'Switch', 'NewTeam')
                 setRequired()

-  $("#button-join-team").click () ->
+  $("#button-join-team").click () ->
     #$("#stretch-box").css("min-height", $("#stretch-box").height()+offset)
     $("#registration-new-team-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
         $("#registration-adviser-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
@@ -109,7 +110,7 @@
                 ga('send', 'event', 'Registration', 'Switch', 'JoinTeam')
                 setRequired()

-  $("#button-adviser").click () ->
+  $("#button-adviser").click () ->
     #$("#stretch-box").css("min-height", $("#stretch-box").height()+offset)
     $("#registration-new-team-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
         $("#registration-join-team-page").hide "slide", { direction: "up" }, pageTransitionSpeed, () ->
@@ -117,10 +118,10 @@
                 ga('send', 'event', 'Registration', 'Switch', 'Teacher')
                 setRequired()

-
+
   $("#country-select").on "change", checkEligibility
   $("#background-select").on "change", checkEligibility
-
+
   $("#country-select").html('
         <option value="">Country...</option>
         <option value="US">United States of America</option>
@@ -355,7 +356,7 @@
         <option value="UG">Uganda</option>
         <option value="UA">Ukraine</option>
         <option value="AE">United Arab Emirates</option>
-        <option value="GB">United Kingdom</option>
+        <option value="GB">United Kingdom</option>
         <option value="UY">Uruguay</option>
         <option value="UZ">Uzbekistan</option>
         <option value="VU">Vanuatu</option>


Reapply ops to track changes as current user when reconnecting

--- services/web/public/coffee/ide/editor/Document.coffee
+++ services/web/public/coffee/ide/editor/Document.coffee
@@ -360,8 +360,8 @@
 			@ranges.track_changes = @doc.track_changes
 			for op in @doc.getInflightOp() or []
 				@ranges.setIdSeed(@doc.track_changes_id_seeds.inflight)
-				@ranges.applyOp(op)
+				@ranges.applyOp(op, { user_id: @track_changes_as })
 			for op in @doc.getPendingOp() or []
 				@ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
-				@ranges.applyOp(op)
+				@ranges.applyOp(op, { user_id: @track_changes_as })
 			@ranges.emit "redraw"


flip module order, common is the root dependency and comes first

--- tasks/fresh/modules/steroids-module-compile-views.coffee
+++ tasks/fresh/modules/steroids-module-compile-views.coffee
@@ -31,7 +31,7 @@
           { view, module }

       context.modules = if context.module isnt 'common'
-          [context.module, 'common']
+          ['common', context.module]
         else
           ['common']


auth worker: send auth.leave messages to load-balanced kites

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -22,12 +22,12 @@
     @reroutingExchange    ?= 'routing-control'
     @notificationExchange ?= 'notification'
     @services = {}
-    @clients  = {
+    @clients  =
       bySocketId    : {}
       byExchange    : {}
       byRoutingKey  : {}
-    }
     @counts   = {}
+    @waitingAuthWhos = {}

   bound: require 'koding-bound'

@@ -72,11 +72,10 @@

   cycleClient: (client) ->
     {routingKey} = client
-    @bongo.respondToClient routingKey, {
+    @bongo.respondToClient routingKey,
       method      : 'cycleChannel'
       arguments   : []
       callbacks   : {}
-    }

   removeClient: (rest...) ->
     if rest.length is 1
@@ -89,11 +88,10 @@

   addClient: (socketId, exchange, routingKey, sendOk=yes) ->
     if sendOk
-      @bongo.respondToClient routingKey, {
+      @bongo.respondToClient routingKey,
         method    : 'auth.authOk'
         arguments : []
         callbacks : {}
-      }
     clientsBySocketId   = @clients.bySocketId[socketId]     ?= []
     clientsByExchange   = @clients.byExchange[exchange]     ?= []
     clientsByRoutingKey = @clients.byRoutingKey[routingKey] ?= []
@@ -105,11 +103,10 @@
   rejectClient:(routingKey, message)->
     console.log 'rejecting', routingKey
     return console.trace()  unless routingKey?
-    @bongo.respondToClient routingKey, {
+    @bongo.respondToClient routingKey,
       method    : 'error'
       arguments : [message: message ? 'Access denied']
       callbacks : {}
-    }

   setSecretNames:(routingKey, publishingName, subscribingName)->
     setSecretNamesEvent = "#{routingKey}.setSecretNames"
@@ -135,12 +132,20 @@

   sendAuthJoin: (options) ->
     { socketId, serviceUniqueName, routingKey } = options
-    options.callback = => @addClient socketId, serviceUniqueName, routingKey
+    options.callback = =>
+      key = getWaitingAuthWhoKey options
+      socketId ?= @waitingAuthWhos[key]
+      delete @waitingAuthWhos[key]
+      @addClient socketId, serviceUniqueName, routingKey
     options.method = 'auth.join'
     @sendAuthMessage options

+  getWaitingAuthWhoKey = (o) ->
+    "#{o.username}!#{o.correlationName}!#{o.serviceGenericName}"
+
   sendAuthWho: (options) ->
     options.method = 'auth.who'
+    @waitingAuthWhos[getWaitingAuthWhoKey options] = options.socketId
     @sendAuthMessage options

   fetchReroutingExchange:(callback)->
@@ -189,11 +194,10 @@
       exchange.publish routingKey, { event, contents }

   respondServiceUnavailable: (routingKey) ->
-    @bongo.respondToClient routingKey, {
+    @bongo.respondToClient routingKey,
       method    : 'error'
       arguments : [message: 'Service unavailable!', code:503]
       callbacks : {}
-    }

   join: do ->


Move require to top of function

Move require to top of function

--- src/file.coffee
+++ src/file.coffee
@@ -120,10 +120,11 @@
   #
   # Returns a promise that resovles to a String.
   read: (flushCache) ->
+    Q ?= require 'q'
+
     if not @exists()
       promise = Q(null)
     else if not @cachedContents? or flushCache
-      Q ?= require 'q'
       deferred = Q.defer()
       promise = deferred.promise
       content = []


incorrect coffeescript loop variable fixed

--- src/tags/checkout.coffee
+++ src/tags/checkout.coffee
@@ -168,7 +168,7 @@
         return (@ctx.coupon.amount || 0)
       else
         discount = 0
-        for i, item in @ctx.order.items
+        for item in @ctx.order.items
           if item.productId == @ctx.coupon.productId
             discount += (@ctx.coupon.amount || 0) * item.quantity
         return discount


Prevent errors if jQuery is on compatibility mode

--- app/assets/javascripts/peek.coffee
+++ app/assets/javascripts/peek.coffee
@@ -2,68 +2,70 @@

 requestId = null

-getRequestId = ->
-  if requestId? then requestId else $('#peek').data('request-id')
+do ($ = jQuery) ->

-peekEnabled = ->
-  $('#peek').length
-
-updatePerformanceBar = (results) ->
-  for key of results.data
-    for label of results.data[key]
-      $("[data-defer-to=#{key}-#{label}]").text results.data[key][label]
-  $(document).trigger 'peek:render', [getRequestId(), results]
-
-initializeTipsy = ->
-  $('#peek .peek-tooltip, #peek .tooltip').each ->
-    el = $(this)
-    gravity = if el.hasClass('rightwards') || el.hasClass('leftwards')
-      $.fn.tipsy.autoWE
-    else
-      $.fn.tipsy.autoNS
-
-    el.tipsy
-      gravity: gravity
-
-toggleBar = (event) ->
-  return if $(event.target).is ':input'
-
-  if event.which == 96 && !event.metaKey
-    wrapper = $('#peek')
-    if wrapper.hasClass 'disabled'
-      wrapper.removeClass 'disabled'
-      document.cookie = "peek=true; path=/";
-    else
-      wrapper.addClass 'disabled'
-      document.cookie = "peek=false; path=/";
-
-fetchRequestResults = ->
-  $.ajax '/peek/results',
-    data:
-      request_id: getRequestId()
-    success: (data, textStatus, xhr) ->
-      updatePerformanceBar data
-    error: (xhr, textStatus, error) ->
-      # Swallow the error
-
-$(document).on 'keypress', toggleBar
-
-$(document).on 'peek:update', initializeTipsy
-$(document).on 'peek:update', fetchRequestResults
-
-# Fire the event for our own listeners.
-$(document).on 'pjax:end', (event, xhr, options) ->
-  if xhr?
-    requestId = xhr.getResponseHeader 'X-Request-Id'
-
-  if peekEnabled()
-    $(this).trigger 'peek:update'
-
-# Also listen to turbolinks page change event
-$(document).on 'page:change', ->
-  if peekEnabled()
-    $(this).trigger 'peek:update'
-
-$ ->
-  if peekEnabled()
-    $(this).trigger 'peek:update'
+  getRequestId = ->
+    if requestId? then requestId else $('#peek').data('request-id')
+
+  peekEnabled = ->
+    $('#peek').length
+
+  updatePerformanceBar = (results) ->
+    for key of results.data
+      for label of results.data[key]
+        $("[data-defer-to=#{key}-#{label}]").text results.data[key][label]
+    $(document).trigger 'peek:render', [getRequestId(), results]
+
+  initializeTipsy = ->
+    $('#peek .peek-tooltip, #peek .tooltip').each ->
+      el = $(this)
+      gravity = if el.hasClass('rightwards') || el.hasClass('leftwards')
+        $.fn.tipsy.autoWE
+      else
+        $.fn.tipsy.autoNS
+
+      el.tipsy
+        gravity: gravity
+
+  toggleBar = (event) ->
+    return if $(event.target).is ':input'
+
+    if event.which == 96 && !event.metaKey
+      wrapper = $('#peek')
+      if wrapper.hasClass 'disabled'
+        wrapper.removeClass 'disabled'
+        document.cookie = "peek=true; path=/";
+      else
+        wrapper.addClass 'disabled'
+        document.cookie = "peek=false; path=/";
+
+  fetchRequestResults = ->
+    $.ajax '/peek/results',
+      data:
+        request_id: getRequestId()
+      success: (data, textStatus, xhr) ->
+        updatePerformanceBar data
+      error: (xhr, textStatus, error) ->
+        # Swallow the error
+
+  $(document).on 'keypress', toggleBar
+
+  $(document).on 'peek:update', initializeTipsy
+  $(document).on 'peek:update', fetchRequestResults
+
+  # Fire the event for our own listeners.
+  $(document).on 'pjax:end', (event, xhr, options) ->
+    if xhr?
+      requestId = xhr.getResponseHeader 'X-Request-Id'
+
+    if peekEnabled()
+      $(this).trigger 'peek:update'
+
+  # Also listen to turbolinks page change event
+  $(document).on 'page:change', ->
+    if peekEnabled()
+      $(this).trigger 'peek:update'
+
+  $ ->
+    if peekEnabled()
+      $(this).trigger 'peek:update'


Only save token when set

Only save token when set

--- src/login.coffee
+++ src/login.coffee
@@ -66,6 +66,8 @@
         Q(state)

   saveToken: ({token}) =>
+    throw new Error("Token is required") unless token
+
     process.stdout.write('Saving token to Keychain ')
     auth.saveToken(token)
     process.stdout.write '\u2713\n'.green


Pass returned value to fulfilled promise

--- src/ubret/promise.coffee
+++ src/ubret/promise.coffee
@@ -29,7 +29,7 @@
     return unless @state is 'fulfilled' and not _.isEmpty(@onFulfilledQueue)
     thenable = @onFulfilledQueue.shift()
     try
-      value = thenable.func(@reason)
+      value = thenable.func(@value)
       @resolveChild(thenable.promise, value)
     catch e
       @resolveChild(thenable.promise, e)


remove loading even when no entities are going to be fetched [fix #640]

--- src/ui/dispatcher.coffee
+++ src/ui/dispatcher.coffee
@@ -43,6 +43,7 @@

     require('./version').check()

+    viewstate.setContacts true
     viewstate.setState viewstate.STATE_NORMAL

 handle 'chat_message', (ev) ->


had a typo.. argh!

had a typo.. argh!

--- src/ui/views/menu.coffee
+++ src/ui/views/menu.coffee
@@ -268,39 +268,39 @@
             submenu: [
               {
                   label: 'Conversation 1'
-                  accelerator: getAccelarator('conversation1')
+                  accelerator: getAccelerator('conversation1')
                   click: -> action 'selectConvIndex', 0
               }, {
                   label: 'Conversation 2'
-                  accelerator: getAccelarator('conversation2')
+                  accelerator: getAccelerator('conversation2')
                   click: -> action 'selectConvIndex', 1
               }, {
                   label: 'Conversation 3'
-                  accelerator: getAccelarator('conversation3')
+                  accelerator: getAccelerator('conversation3')
                   click: -> action 'selectConvIndex', 2
               }, {
                   label: 'Conversation 4'
-                  accelerator: getAccelarator('conversation4')
+                  accelerator: getAccelerator('conversation4')
                   click: -> action 'selectConvIndex', 3
               }, {
                   label: 'Conversation 5'
-                  accelerator: getAccelarator('conversation5')
+                  accelerator: getAccelerator('conversation5')
                   click: -> action 'selectConvIndex', 4
               }, {
                   label: 'Conversation 6'
-                  accelerator: getAccelarator('conversation6')
+                  accelerator: getAccelerator('conversation6')
                   click: -> action 'selectConvIndex', 5
               }, {
                   label: 'Conversation 7'
-                  accelerator: getAccelarator('conversation7')
+                  accelerator: getAccelerator('conversation7')
                   click: -> action 'selectConvIndex', 6
               }, {
                   label: 'Conversation 8'
-                  accelerator: getAccelarator('conversation8')
+                  accelerator: getAccelerator('conversation8')
                   click: -> action 'selectConvIndex', 7
               }, {
                   label: 'Conversation 9'
-                  accelerator: getAccelarator('conversation9')
+                  accelerator: getAccelerator('conversation9')
                   click: -> action 'selectConvIndex', 8
               }
             ]


addressed #34: prepend 'require', 'exports', and 'module' to deps list when CommonJS-like define() flavor is detected

--- src/parse.coffee
+++ src/parse.coffee
@@ -47,7 +47,7 @@
           if node.arguments[0].type == "FunctionExpression" and
           node.arguments[0].params.length > 0

-            deps = []
+            deps = ['require', 'exports', 'module']
             walk.simple(node.arguments[0], CallExpression : (node) ->
               if node.callee.name == "require" or node.callee.name == "requirejs"
                 deps.push(node.arguments[0].value)


contestants can see all popularity

contestants can see all popularity

--- controllers/teams.coffee
+++ controllers/teams.coffee
@@ -55,7 +55,7 @@
   #   1. only allow sorting by popularity for non-contestants
   #   2. only show contestants scores for teams they've voted on
   # else, if voting is finished, show everybody everything
-  if app.enabled('voting') # && !req.user?.admin
+  if app.enabled('voting') and sort isnt 'popularity' # && !req.user?.admin
     if req.user?.contestant
       req.user.votedOnTeamIds (err, teamIds) ->
         return next(err) if err


point

point

--- src/point.coffee
+++ src/point.coffee
@@ -334,13 +334,13 @@
     Floor the x and y values, returning a new point.

-       point = Point(3.4, 5.8).floor()
-
-       point.x
-    => 3
-
-       point.y
-    => 5
+    point = Point(3.4, 5.8).floor()
+
+    point.x
+    # => 3
+
+    point.y
+    # => 5

     @name floor


point

point

--- src/point.coffee
+++ src/point.coffee
@@ -220,18 +220,17 @@
     Scale this Point (Vector) by a constant amount.

-       point = Point(5, 6).scale(2)
-
-       point.x
-    => 10
-
-       point.y
-    => 12
+    point = Point(5, 6).scale(2)
+
+    point.x
+    # => 10
+
+    point.y
+    # => 12

     @name scale
     @methodOf Point#
-
     @param {Number} scalar The amount to scale this point by.
     @returns {Point} A new point, this * scalar.
     ###


Make the workaround for pouchdb bug more generic

--- src/pouch.coffee
+++ src/pouch.coffee
@@ -50,11 +50,14 @@
         if typeof params is 'function'
             callback = params
             params = include_docs: true
+        # XXX Pouchdb does sometimes send us undefined values in docs.
+        # It's rare and we didn't find a way to extract a proper test case.
+        # So, we keep a workaround and hope that this bug will be fixed.
         @db.query query, params, (err, res) ->
             if err
                 callback err
             else
-                docs = (row.doc for row in res.rows)
+                docs = (row.doc for row in res.rows when row.doc?)
                 callback null, docs

     # Return all the files with this checksum
@@ -81,12 +84,7 @@
                 startkey: "#{path}"
                 endkey: "#{path}/\ufff0"
                 include_docs: true
-        @getAll 'byPath', params, (err, docs) ->
-            # XXX Pouchdb does sometimes send us undefined values in docs.
-            # It's rare and we didn't find a way to extract a proper test case.
-            # So, we keep a workaround and hope that this bug will be fixed.
-            docs = (doc for doc in docs when doc?) unless err
-            callback err, docs
+        @getAll 'byPath', params, callback

     # Return the file/folder with this remote id
     byRemoteId: (id, callback) ->


workaround for CSL particle parser

workaround for CSL particle parser

--- resource/translators/reference.coffee
+++ resource/translators/reference.coffee
@@ -123,6 +123,13 @@

             else
               Zotero.BetterBibTeX.CSL.parseParticles(name)
+
+              # temporary workaround for https://bitbucket.org/fbennett/citeproc-js/issues/183/particle-parser-returning-non-dropping
+              if name['non-dropping-particle'] and !name['dropping-particle']
+                if (new RegExp("\\s#{name['non-dropping-particle']}(\\s|$)")).test(creator.firstName)
+                  if !(new RegExp("\\s#{name['non-dropping-particle']}(\\s|$)")).test(creator.lastName)
+                    name['dropping-particle'] = name['non-dropping-particle']
+                    delete name['non-dropping-particle']

               source = XRegExp.replace((creator.firstName || '') + (creator.lastName || ''), @nonLetters, '')
               parsed = XRegExp.replace((part || '' for part in [name.given, name.family, name.suffix, name['non-dropping-particle'], name['dropping-particle']]).join(''), @nonLetters, '')


0.0.16

0.0.16

+ small fix

--- www_src/AppRate.coffee
+++ www_src/AppRate.coffee
@@ -49,7 +49,8 @@
 		localeObj = locales[preferences.useLanguage] or locales["en"]
 		displayAppName = localeObj.displayAppName or preferences.displayAppName
 		for key, value of localeObj
-			localeObj[key] = value.toString().replace(/%@/g, displayAppName)
+			if typeof value == 'string' or value instanceof String
+				localeObj[key] = value.replace(/%@/g, displayAppName)
 		localeObj


Case insensitive test for names conflicts for tables

--- admin/static/coffee/namespaces/index.coffee
+++ admin/static/coffee/namespaces/index.coffee
@@ -393,7 +393,7 @@
                 template_error.type = 'table'
             else # And a name that doesn't exist
                 for namespace in namespaces.models
-                    if namespace.get('name') is formdata.name and namespace.get('database') is formdata.database
+                    if namespace.get('name').toLowerCase() is formdata.name.toLowerCase() and namespace.get('database').toLowerCase() is formdata.database.toLowerCase()
                         input_error = true
                         template_error.namespace_exists = true
                         break


Replace 'live' with 'on' syntax.

Replace 'live' with 'on' syntax.

--- app/assets/javascripts/datepicker.js.coffee
+++ app/assets/javascripts/datepicker.js.coffee
@@ -5,13 +5,13 @@
 #------------------------------------------------------------------------------
 (($) ->

-  $('input.date').live 'click focus', ->
+  $(document).on 'click focus', 'input.date', ->
     $(this).datepicker({
       showOn: 'focus',
       changeMonth: true,
       dateFormat: 'yy-mm-dd'})

-  $('input.datetime').live 'click focus', ->
+  $(document).on 'click focus', 'input.datetime', ->
     $(this).datetimepicker({
       showOn: 'focus',
       changeMonth: true,


Fixing create task popup, enabling blocked textarea only when needed

--- app/coffee/modules/common/lightboxes.coffee
+++ app/coffee/modules/common/lightboxes.coffee
@@ -176,9 +176,7 @@
             return $log.error "No watch attribute on tg-blocking-message-input directive"

         $scope.$watch $attrs.watch, (value) ->
-            return if value is undefined
-
-            if value == true
+            if value is not undefined and value == true
                 $el.find(".blocked-note").show(400)
             else
                 $el.find(".blocked-note").hide(400)


OCD ordering/spacing

OCD ordering/spacing

--- src/robot.coffee
+++ src/robot.coffee
@@ -1,10 +1,11 @@
-Fs           = require 'fs'
-Log          = require 'log'
-Path         = require 'path'
-Url          = require 'url'
-
-Brain        = require './brain'
-User         = require './user'
+Fs      = require 'fs'
+Url     = require 'url'
+Log     = require 'log'
+Path    = require 'path'
+Connect = require 'connect'
+
+User    = require './user'
+Brain   = require './brain'

 HUBOT_DEFAULT_ADAPTERS = [ "campfire", "shell" ]

@@ -23,8 +24,7 @@
     @listeners   = []
     @loadPaths   = []
     @enableSlash = false
-    connect      = require 'connect'
-    @connect     = connect()
+    @connect     = Connect()

     @logger      = new Log process.env.HUBOT_LOG_LEVEL or "info"


Allow callbacks after tween finishes

Allow callbacks after tween finishes

--- src/shape.coffee
+++ src/shape.coffee
@@ -20,9 +20,9 @@

     # Default graphical options; changing these requires using setters/getters,
     # since the cache would need to be redrawn
-    @_color    = '#fff';
-    @_border   = { width: 0, color: '#fff' }
-    @_shadow   = { x: 0, y: 0, blur: 0, color: '#fff' }
+    @_color = '#fff';
+    @_border = { width: 0, color: '#fff' }
+    @_shadow = { x: 0, y: 0, blur: 0, color: '#fff' }
     @_vertices = 4
     @_size = { width: 10, height: 10 }
     @speed = 1
@@ -247,7 +247,9 @@
       tween.time += delta * 1000 # (delta is in seconds)
       tween.time = tween.duration if tween.time > tween.duration
       @[tween.property] = tween.easingFunc(tween.time, tween.start, tween.change, tween.duration) # time,begin,change,duration
-      @tweens.splice(i, 1) if tween.time == tween.duration
+      if tween.time == tween.duration
+        @tweens.splice(i, 1)
+        tween.callback() if typeof tween.callback == 'function'

     @velocity.x += @acceleration.x
     @velocity.y += @acceleration.y
@@ -265,7 +267,7 @@
     return false if @ == other
     Math.abs(@position.x - other.position.x) < @size.width / 2 + other.size.width / 2 && Math.abs(@position.y - other.position.y) < @size.height / 2 + other.size.height / 2

-  tween: (property, target, duration = 500, easing = 'linearNone') ->
+  tween: (property, target, duration = 500, easing = 'linearNone', callback) ->
     # TODO: How to handle compound properties, such as @position?
     # context = @
     # if property.indexOf('.') != -1
@@ -282,5 +284,6 @@
       change: target - @[property]
       duration: duration
       easingFunc: Easie[easing]
+      callback: callback

 module.exports = Shape


Fail gracefully if someone provides a old integration token

--- src/slack.coffee
+++ src/slack.coffee
@@ -16,6 +16,7 @@

     @robot.logger.info Util.inspect(options)
     return @robot.logger.error "No services token provided to Hubot" unless options.token
+    return @robot.logger.error "v2 services token provided, please follow the upgrade instructions" unless (options.token.substring(0, 5) == 'xoxb-')

     @options = options


fix reason too long

fix reason too long

--- vue/src/shared/models/stance_model.coffee
+++ vue/src/shared/models/stance_model.coffee
@@ -64,8 +64,8 @@
     @participant()

   reasonTooLong: ->
-    !@poll().allowLongReason && @reason.length > 500
-
+    !@poll().allowLongReason && @reason && @reason.length > 500
+
   stanceChoice: ->
     head @sortedChoices()


improved search for members splitting the search string by space and assuming last name to be last part and first name whole except last part. This should cover namings in western cultures such as Hans Mustermann or Halil Ibrahim Koklu. See comment for explanation. Better implementation possible with MongoDB 2.4 and its aggregation framework

--- workers/social/lib/social/models/account/index.coffee
+++ workers/social/lib/social/models/account/index.coffee
@@ -449,11 +449,23 @@

   @findSuggestions = (client, seed, options, callback)->
     {limit, blacklist, skip}  = options
+    ### TODO:
+    It is highly dependent to culture and there are even onces w/o the concept
+    of first and last names. For now we assume last part of the seed is the lastname
+    and the whole except last part is the first name. Not ideal but covers more
+    than previous implementation. This implementation would fail if I type my
+    two firstnames only, it will assume second part is my lastname.
+    Ideal solution is to check the seed against firstName + ' ' + lastName instead of
+    deciding ourselves which parts of the search are for first or last name.
+    MongoDB 2.4 and bongo implementation of aggregate required to use $concat
+    ###
+    names = seed.toString().split('/')[1].replace('^','').split ' '
+    names.push names.first if names.length is 1
     @some {
       $or : [
           ( 'profile.nickname'  : seed )
-          ( 'profile.firstName' : seed )
-          ( 'profile.lastName'  : seed )
+          ( 'profile.firstName' : new RegExp '^'+names.slice(0, -1).join(' '), 'i' )
+          ( 'profile.lastName'  : new RegExp '^'+names.last, 'i' )
         ],
       _id     :
         $nin  : blacklist


MainController: this is the last guard, if user somehow gets the guestuser

JAccount, in the client side we are cleaning up the cookie and reloading the
page. This shouldn't happen but in case of disturbance in the force. This will
also block anyone to open koding with guestuser account.

--- client/Core/maincontroller.coffee
+++ client/Core/maincontroller.coffee
@@ -129,8 +129,17 @@

-  accountChanged:(account, firstLoad = no)->
-    account = KD.remote.revive account  unless account instanceof KD.remote.api.JAccount
+  accountChanged: (account, firstLoad = no)->
+
+    unless account instanceof KD.remote.api.JAccount
+      account = KD.remote.revive account
+
+    # this is last guard that we can take for guestuser issue ~ GG
+    if account.profile?.nickname is "guestuser"
+      Cookies.expire 'clientId'
+      window.location.href = '/'
+      return
+
     KD.userAccount = account
     connectedState.connected = yes


KiteCache: invalidate kite caches when they are older than a day

--- client/Main/kite/kitecache.coffee
+++ client/Main/kite/kitecache.coffee
@@ -40,6 +40,7 @@
       query = @generateQueryString query

     delete storage[signed query]
+    return

   @cache = (query, kite) ->
@@ -49,6 +50,8 @@

     queryString = @generateQueryString query
     kite = proxifyTransport kite
+    kite.cachedAt = +new Date()
+
     LocalStorage.setValue (signed queryString), (JSON.stringify kite)

@@ -62,9 +65,26 @@

     try
       kite = JSON.parse kite
+
     catch e
-      warn "parse failed", e
-      @unset query
-      kite = null
+      warn "[KiteCache] PARSE ERROR", e
+      return @unset query
+
+    if kite.cachedAt?
+
+      cachedAt = new Date kite.cachedAt
+      now      = new Date
+
+      # Add 24 hours on top of cached at date
+      cachedAt.setHours cachedAt.getHours() + 24
+
+      if cachedAt < now
+        warn "[KiteCache] CACHE FOUND BUT OUTDATED, REMOVING..."
+        return @unset queryString
+
+    else
+      warn "[KiteCache] CACHE FOUND BUT DOESNT HAVE TIMESTAMP, REMOVING..."
+      return @unset queryString
+

     return kite


#383 also do not close app on macOS.

#383 also do not close app on macOS.

--- src/main.coffee
+++ src/main.coffee
@@ -142,7 +142,7 @@
         # reinstate app window when login finishes
         prom = login(loginWindow)
         .then (rs) ->
-          global.forceClose = true
+          global.forceClose = false
           loginWindow.removeAllListeners 'closed'
           loginWindow.close()
           mainWindow.show()


SocialWorker: JSystemStatus: remove forceReload event and its usage

--- workers/social/lib/social/models/systemstatus.coffee
+++ workers/social/lib/social/models/systemstatus.coffee
@@ -17,8 +17,6 @@
           (signature Object, Function)
         stopCurrentSystemStatus:
           (signature Function)
-        forceReload:
-          (do signature)
         healthCheck  :
           (signature Function)
         checkRealtimeUpdates:
@@ -30,7 +28,7 @@
           (signature Function)

     sharedEvents     :
-      static         : ['forceReload', 'restartScheduled']
+      static         : ['restartScheduled']
       instance       : ['restartCanceled']

     schema           :
@@ -59,13 +57,6 @@
     kodingErr

   { log } = console
-
-  @forceReload = secure (client) ->
-    { connection:{ delegate } } = client
-    unless delegate.checkFlag('super-admin')
-      log 'status: not authorized to stop a system status'
-      return
-    @emit 'forceReload'

   @stopCurrentSystemStatus = secure (client, callback = -> ) ->
     { connection:{ delegate } } = client


Changed: when a task's content is empty, it's not saved

--- client/app/views/task_view.coffee
+++ client/app/views/task_view.coffee
@@ -77,7 +77,7 @@

     startPeriodicSave: ->
         @focusInterval = setInterval () =>
-            @saveDescription()
+            @saveDescription() if @$('input').val() isnt ""
         , 2000

     stopPerdiodicSave: ->


IDE: fix PR comments

IDE: fix PR comments

--- client/ide/RealTimeManager.coffee
+++ client/ide/RealTimeManager.coffee
@@ -154,80 +154,84 @@
     # delete key from root map
     model.getRoot().delete key

+  textInserted:(string, e) ->
+    return  if @isDisposed
+    @emit 'TextInsertedIntoString', string, e
+
+  textDeleted:(string, e)=>
+    return  if @isDisposed
+    @emit 'TextDeletedFromString', string, e
+
+  bindStringListeners: (string) ->
+    string.addEventListener gapi.drive.realtime.EventType.TEXT_INSERTED, @binder string, @textInserted
+    string.addEventListener gapi.drive.realtime.EventType.TEXT_DELETED, @binder string, @textDeleted
+
+  unbindStringListeners: (string) ->
+    string.removeEventListener gapi.drive.realtime.EventType.TEXT_INSERTED, @binder string, @textInserted
+    string.removeEventListener gapi.drive.realtime.EventType.TEXT_DELETED, @binder string, @textDeleted
+
+
+  mapValueChanged: (map, v) ->
+    return  if @isDisposed
+    @emit 'MapValueChanged', map, v
+
+  bindMapListeners: (map) ->
+    map.addEventListener gapi.drive.realtime.EventType.VALUE_CHANGED, @binder map, @mapValueChanged
+
+  unbindMapListeners: (map) ->
+    map.removeEventListener gapi.drive.realtime.EventType.VALUE_CHANGED, @binder map, @mapValueChanged
+
+  listValueAdded: (list, v)->
+    return  if @isDisposed
+    @emit 'ValuesAddedToList', list, v
+
+  listValueRemoved: (list, v)->
+    return  if @isDisposed
+    @emit 'ValuesRemovedFromList', list, v
+
+  listValueSet: (list, e)->
+    return  if @isDisposed
+    @emit 'ListValuesSet', list, e
+
+  bindListListeners: (list) ->
+    list.addEventListener gapi.drive.realtime.EventType.VALUES_ADDED, @binder list, @listValueAdded
+    list.addEventListener gapi.drive.realtime.EventType.VALUES_REMOVED, @binder list, @listValueRemoved
+    list.addEventListener gapi.drive.realtime.EventType.VALUES_SET, @binder list, @listValueSet
+
+  unbindListListeners: (list) ->
+    list.removeEventListener gapi.drive.realtime.EventType.VALUES_ADDED, @binder list, @listValueAdded
+    list.removeEventListener gapi.drive.realtime.EventType.VALUES_REMOVED, @binder list, @listValueRemoved
+    list.removeEventListener gapi.drive.realtime.EventType.VALUES_SET, @binder list, @listValueSet
+
+
+  values = {}
+
+  binder: (collaborativeObj, callback)->
+    # all kind of collaborativeObjs have id
+    throw new Error "id is not set" if not collaborativeObj.id
+
+    values[collaborativeObj.id] or= (v) ->
+      callback.call this, collaborativeObj, v
+
+    return values[collaborativeObj.id].bind this

   unbindRealtimeListeners: do (instances = []) ->
     (instance, type) ->
-      return  if @isDisposed
+      return  if instances.indexOf(instance) > -1 or @isDisposed
       instances.push instance
       switch type
-        when 'string'
-          @unbindStringListeners instance
-        when 'map'
-          @unbindMapListeners instance
-        when 'list'
-          @unbindListListeners instance
+        when 'string' then @unbindStringListeners instance
+        when 'map' then @unbindMapListeners instance
+        when 'list' then @unbindListListeners instance

   bindRealtimeListeners: do (instances = []) ->
     (instance, type) ->
-      return  if @isDisposed
+      return  if instances.indexOf(instance) > -1 or @isDisposed
       instances.push instance
       switch type
-        when 'string'
-          @bindStringListeners instance
-        when 'map'
-          @bindMapListeners instance
-        when 'list'
-          @bindListListeners instance
-
-  textInserted:(string, e) ->
-    return  if @isDisposed
-    @emit 'TextInsertedIntoString', string, e
-
-  textDeleted:(string, e)=>
-    return  if @isDisposed
-    @emit 'TextDeletedFromString', string, e
-
-  bindStringListeners: (string) ->
-    string.addEventListener gapi.drive.realtime.EventType.TEXT_INSERTED, (e) => @textInserted string, e
-    string.addEventListener gapi.drive.realtime.EventType.TEXT_DELETED, (e) => @textDeleted string, e
-
-  unbindStringListeners: (string) ->
-    string.removeEventListener gapi.drive.realtime.EventType.TEXT_INSERTED, (e) => @textInserted string, e
-    string.removeEventListener gapi.drive.realtime.EventType.TEXT_DELETED, (e) => @textDeleted string, e
-
-
-  mapValueChanged: (map, v) ->
-    return  if @isDisposed
-    @emit 'MapValueChanged', map, v
-
-  bindMapListeners: (map) ->
-    map.addEventListener gapi.drive.realtime.EventType.VALUE_CHANGED, (v) => @mapValueChanged map, v
-
-  unbindMapListeners: (map) ->
-    map.removeEventListener gapi.drive.realtime.EventType.VALUE_CHANGED, (v) => @mapValueChanged map, v
-
-  listValueAdded: (list, v)->
-    return  if @isDisposed
-    @emit 'ValuesAddedToList', list, v
-
-  listValueRemoved: (list, v)->
-    return  if @isDisposed
-    @emit 'ValuesRemovedFromList', list, v
-
-  listValueSet:(list, v)=>
-    return  if @isDisposed
-    @emit 'ListValuesSet', list, v
-
-  bindListListeners: (list) ->
-    list.addEventListener gapi.drive.realtime.EventType.VALUES_ADDED, (s) => @listValueAdded list, s
-    list.addEventListener gapi.drive.realtime.EventType.VALUES_REMOVED, (s) => @listValueRemoved list, s
-    list.addEventListener gapi.drive.realtime.EventType.VALUES_SET, (s) => @listValueSet list, s
-
-
-  unbindListListeners: (list) ->
-    list.removeEventListener gapi.drive.realtime.EventType.VALUES_ADDED, (s) => @listValueAdded list, s
-    list.removeEventListener gapi.drive.realtime.EventType.VALUES_REMOVED, (s) => @listValueRemoved list, s
-    list.removeEventListener gapi.drive.realtime.EventType.VALUES_SET, (s) => @listValueSet list, s
+        when 'string' then  @bindStringListeners instance
+        when 'map' then @bindMapListeners instance
+        when 'list' then @bindListListeners instance

   getCollaborators: -> return @getRealtimeDoc().getCollaborators()


Added padding_color option, as well as new padding symbol ◦

--- cakes.coffee
+++ cakes.coffee
@@ -45,6 +45,7 @@

 	options = _.extend
 		padding:false
+		padding_color:'green'
 	, options

 	if type in ['describe', 'it']
@@ -65,7 +66,7 @@
 			global[ui[type]] label, cb

 		if options.padding
-			global[ui.describe] '|'.green, ->
+			global[ui.describe] '◦'[options.padding_color], ->
 				fn()
 		else
 			fn()
@@ -80,8 +81,13 @@
 	dic 'describe', "\n    Scenario: %s".green, arguments

-gwt = (label, args)->
+gwt = (label, args, options)->
 	[title, cb] = args_wash args
+
+	options = _.extend
+		padding:true
+		pending:true
+	, options

 	###
 	#log 'gwt', title, cb.length
@@ -93,7 +99,7 @@
 		callback = cb
 	###

-	dic 'it', label, [title,cb], padding:true,pending:true
+	dic 'it', label, [title,cb], options

 exports.Given = ->
 	gwt "Given:".yellow+" %s", arguments
@@ -115,10 +121,10 @@

 exports.And = ->
-	gwt "  and".grey+"  %s", arguments
+	gwt "  and".grey+"  %s", arguments, padding_color:'black'

 exports.But = ->
-	gwt "  But".grey+"  %s", arguments
+	gwt "  But".grey+"  %s", arguments, padding_color:'black'

 exports.Step = ->