make sprite test-wide

make sprite test-wide

--- src/layout/test_layout.coffee
+++ src/layout/test_layout.coffee
@@ -12,6 +12,10 @@
   game_dict = game({name: 'example', level: 1})
   config = layout(game_dict)
   world = god(rx, config)
+  level = world.find_child()
+  grid = level.find_child('grid')
+  all_sprites = grid.find_child('sprites')
+  sprite = all_sprites.find_child()

   test "layout config", (t) ->
     t.ok game_dict, 'game_dict'
@@ -34,15 +38,15 @@
     t.ok sprite_dict = sprites.at(0), 'sprite'
     t.end()

-  test "layout children", (t) ->
+  test "layout sprite", (t) ->
     size = 64
     c = size / 2
     transform_result = "translate(#{size},#{size}) rotate(0 #{c} #{c})"

-    t.ok level = world.find_child(), 'active level'
-    t.ok grid = level.find_child('grid'), 'grid'
-    t.ok all_sprites = grid.find_child('sprites'), 'sprites'
-    t.ok sprite = all_sprites.find_child(), 'sprite'
+    t.ok level, 'active level'
+    t.ok grid, 'grid'
+    t.ok all_sprites, 'sprites'
+    t.ok sprite, 'sprite'
     console.log 'sprite', sprite, sprite.get_raw('x'), my.inspect(sprite)
     t.equal sprite.get('position').all().toString(), "1,1", 'sprite position'
     t.ok sprite.get('x') > 0, 'non-zero x'


fix bug: isAdminOnly() should check custom permission settings

--- src/lib/acl-conditions.coffee
+++ src/lib/acl-conditions.coffee
@@ -51,6 +51,8 @@

     isAdminOnly: ->
+        return false if Object.keys(@customPermissions).length > 0
+
         for name, regularPermissions of @basicPermissions
             return false if regularPermissions.length > 0
         return true


add healthcheck

add healthcheck

--- server.coffee
+++ server.coffee
@@ -15,7 +15,7 @@
 app = express()
 app.use bearerToken reqKey: 'bearerToken'

-app.get '/', (req, res) ->
+app.get '/healthcheck', (req, res) ->
   res.send online: true

 app.get '/triggers', triggerController.getTriggers


Changed onGeoloc

Changed onGeoloc

The notification will only be sent when you are in range of a different
beacon

--- server.coffee
+++ server.coffee
@@ -68,25 +68,29 @@

 # Get background location from player.
 exports.onGeoloc = (userId, geoloc) ->
-	#log '[onGeoloc()] Geoloc from ' + Plugin.userName(userId) + '('+userId+'): ', JSON.stringify(geoloc)
+	log '[onGeoloc()] Geoloc from ' + Plugin.userName(userId) + '('+userId+'): ', JSON.stringify(geoloc)
+	#Store lat and lng for other uses
+	Db.personal(userId).set 'location', 'latitude', geoloc.latitude
+	Db.personal(userId).set 'location', 'longitude', geoloc.longitude
 	recieved = new Date()/1000
-	if Db.shared.peek('gameState') is 1 and (recieved - (Db.shared.peek('lastNotification', userId) || 0))> 30*60
+	if Db.shared.peek('gameState') is 1 and (recieved - (Db.personal(userId).peek('lastNotification', 'recieved') || 0))> 60*60
 		beaconRadius = Db.shared.peek('game', 'beaconRadius')
-		found=false;
+		found= false;
 		#Check if user is in range of an enemy beacon, opening the app will capture the beacon
 		Db.shared.iterate 'game', 'beacons', (beacon)!->
 			if (parseInt(beacon.peek('owner'),10) != parseInt(getTeamOfUser(userId),10)) and !found
 				if distance(geoloc.latitude, geoloc.longitude, beacon.peek('location', 'lat'), beacon.peek('location', 'lng')) < beaconRadius
-					#send notifcation
-					Event.create
-						unit: 'inRange'
-						include: userId
-						text: 'You are in range of an enemy beacon, capture it now!'
-					found=true;
-		#Last notification send, so that the user will not be spammed with notifications
-		if found
-			Db.shared.set('lastNotification', userId, recieved)
-
+					found= true;
+					log 'beacon comparison: ', beacon.key() is Db.personal(userId).peek('lastNotification', 'beaconNumber')
+					if beacon.key() isnt Db.personal(userId).peek('lastNotification', 'beaconNumber')
+						#send notifcation
+						Event.create
+							unit: 'inRange'
+							include: userId
+							text: 'You are in range of an enemy beacon, capture it now!'
+						#Last notification send, so that the user will not be spammed with notifications
+						Db.personal(userId).set('lastNotification', 'recieved', recieved)
+						Db.personal(userId).set('lastNotification', 'beaconNumber', beacon.key())
 # Handle new users joining the happening
 exports.onJoin = (userId) ->
 	log '[onJoin()] userId='+userId+', users='+Plugin.userIds()


fix issue with joining a started game as a spectator

--- server.coffee
+++ server.coffee
@@ -118,7 +118,7 @@
         game = games[gid]
         if game
           if game.players.length > 1
-            requester.send(JSON.stringify({action: "notification", gameid: socket.gameid, text: "#{getUsername(socket)} left the game."}))
+            requester.send(JSON.stringify({action: "notification", gameid: gid, text: "#{getUsername(socket)} left the game."}))
           removePlayer(socket)

       when "join"
@@ -134,11 +134,11 @@
         if game
           game.spectators.push({user: socket.request.user, id: socket.id})
           socket.join(msg.gameid)
-          socket.gameid = gameid
-          socket.emit("netrunner", {type: "game", gameid: gameid, started: game.started})
+          socket.gameid = msg.gameid
+          socket.emit("netrunner", {type: "game", gameid: msg.gameid, started: game.started})
           lobby.emit('netrunner', {type: "games", games: games})
           if game.started
-            requester.send(JSON.stringify({action: "notification", gameid: socket.gameid, text: "#{getUsername(socket)} joined the game as a spectator."}))
+            requester.send(JSON.stringify({action: "notification", gameid: msg.gameid, text: "#{getUsername(socket)} joined the game as a spectator."}))
           else
             socket.broadcast.to(msg.gameid).emit 'netrunner',
               type: "say"


Fix log tailing

Fix log tailing

--- src/pow/server.coffee
+++ src/pow/server.coffee
@@ -20,11 +20,8 @@

   createApplicationPool: (config) ->
     pool = createPool config, size: 3, idle: idle
-
-    pool.on 'spawn', () ->
-      logStream pool.stdout
-      logStream pool.stderr
-
+    logStream pool.stdout
+    logStream pool.stderr
     pool

   applicationForConfig: (config) ->


unbind event fixed

unbind event fixed

--- src/quo.events.coffee
+++ src/quo.events.coffee
@@ -125,7 +125,7 @@

     _findHandlers = (element_id, event, fn, selector) ->
         (HANDLERS[element_id] or []).filter (handler) ->
-            handler and (not event or handler.event is event) and (not fn or handler.fn is fn) and (not selector or handler.selector is selector)
+            handler and (not event or handler.event is event) and (not fn or handler.callback is fn) and (not selector or handler.selector is selector)

     _createProxy = (event) ->
         proxy = $$.extend( originalEvent: event, event)


Fix Copy'n'Paste-Bug "sswagger"

Fix Copy'n'Paste-Bug "sswagger"

Somehow, there were two typos in the variable names. This fixes it.

--- SwaggerImporter.coffee
+++ SwaggerImporter.coffee
@@ -27,7 +27,7 @@

           # Add Queries
           if swaggerRequestParamValue.in == 'query' and swaggerRequestParamValue.type == 'string'
-            queries[swaggerRequestParamValue.name] = if swaggerRequestParamValue.default then swaggerRequestParamValue.default else sswaggerRequestParamValue.name
+            queries[swaggerRequestParamValue.name] = if swaggerRequestParamValue.default then swaggerRequestParamValue.default else swaggerRequestParamValue.name

           # Add Headers
           if swaggerRequestParamValue.in == 'header' and swaggerRequestParamValue.type == 'string'
@@ -35,7 +35,7 @@

           # Add Url Encoded
           if swaggerRequestParamValue.in == 'formData' and swaggerRequestParamValue.type == 'string'
-            formData[swaggerRequestParamValue.name] = if swaggerRequestParamValue.default then swaggerRequestParamValue.default else sswaggerRequestParamValue.name
+            formData[swaggerRequestParamValue.name] = if swaggerRequestParamValue.default then swaggerRequestParamValue.default else swaggerRequestParamValue.name

           # Add Body
           if swaggerRequestParamValue.in == 'body' #Only string


remove erroneous yey

remove erroneous yey

--- testApp/app/controllers/animation.coffee
+++ testApp/app/controllers/animation.coffee
@@ -35,7 +35,6 @@
     }

     anim.perform({}, {
-      onSuccess: -> alert "YEY"
       onFailure: -> navigator.notification.alert "..and it is failing with onFailure. great success."
     })


Fix cursor position on Android Firefox

Fix cursor position on Android Firefox

--- src/jquery.mobilePhoneNumber.coffee
+++ src/jquery.mobilePhoneNumber.coffee
@@ -1,6 +1,9 @@
 $ = jQuery

 supportSelectionEnd = 'selectionEnd' of document.createElement('input')
+
+# Timeout to work around cursor bugs in Android Firefox
+withTimeout = (fn) -> setTimeout(fn, 50)

 formatForPhone_ = (phone, defaultPrefix = null) ->
   if phone.indexOf('+') != 0 and defaultPrefix
@@ -84,11 +87,8 @@
   value = value.substring(0, @caret()) +
           String.fromCharCode(e.which) +
           value.substring(caretEnd, value.length)
-  selection = @caret()
-  selectionAtEnd = selection == @val().length
   format_.call(@, value, e)
-  if !selectionAtEnd
-    @caret(@val().length)
+  withTimeout => @caret(@val().length)

 formatUp_ = (e) ->
   checkForCountryChange_.call(@)
@@ -118,7 +118,7 @@
     e.preventDefault()
     @val(phone)
     if !selectionAtEnd
-      @caret(selection)
+      withTimeout => @caret(selection)

 formattedPhone_ = (phone, lastChar) ->
   if phone.indexOf('+') != 0 && @data('defaultPrefix')


Refactor and compact media-grid layout logic.

--- src/js/jquery.hlf.media-grid.coffee
+++ src/js/jquery.hlf.media-grid.coffee
@@ -70,7 +70,6 @@

       # Layout.
       @metrics = {}
-      @$metricsSamples = {}

       @on 'ready', =>
         @_updateMetrics()
@@ -130,6 +129,17 @@
       @_dimTimeout = null
       return

+    _getMetricSamples: ->
+      @selectByClass('sample')?.remove()
+      $item = @$sampleItem.clone()
+      $expanded = @$sampleItem.clone().addClass @classNames.expanded
+      $('<div>').addClass @classNames.sample
+        .css left: 0, position: 'absolute', right: 0, top: 0
+        .css visibility: 'hidden', zIndex: 0
+        .append $item, $expanded
+        .appendTo @$el
+      { $item, $expanded }
+
     _isBottomEdgeItem: (i) -> (i + 1) > (@$items.length - @metrics.rowSize)

     _isRightEdgeItem: (i) -> (i + 1) % @metrics.rowSize is 0
@@ -166,34 +176,22 @@

     _updateMetrics: (hard=on) ->
       if hard is on
-        @$metricsSamples.$item = @$sampleItem.clone()
-        @$metricsSamples.$expanded = @$sampleItem.clone().addClass @classNames.expanded
-        @$metricsSamples.$wrap?.remove()
-        @$metricsSamples.$wrap = $('<div>').addClass @classNames.sample
-          .css left: 0, position: 'absolute', right: 0, top: 0
-          .css visibility: 'hidden', zIndex: 0
-          .append @$metricsSamples.$item
-          .append @$metricsSamples.$expanded
-          .appendTo @$el
-
-      {$item, $expanded} = @$metricsSamples
+        {$item, $expanded} = @_getMetricSamples()
+        @metrics =
+          itemWidth: $item.outerWidth()
+          itemHeight: $item.outerHeight()
+          expandedWidth: $expanded.outerWidth()
+          expandedHeight: $expanded.outerHeight()
+
       gutter = Math.round parseFloat(@$sampleItem.css('margin-right'))
-      fullWidth = $item.outerWidth() + gutter
-      fullHeight = $item.outerHeight() + gutter
-
-      if hard is on then @metrics =
-        gutter: gutter
-        itemWidth: $item.outerWidth()
-        itemHeight: $item.outerHeight()
-        expandedWidth: $expanded.outerWidth()
-        expandedHeight: $expanded.outerHeight()
+      fullWidth = @metrics.itemWidth + gutter
+      fullHeight = @metrics.itemHeight + gutter

       @$el.css width: 'auto', height: 'auto'
+
       rowSize = parseInt ((@$el.outerWidth() + gutter) / fullWidth), 10
       colSize = Math.ceil @$items.length / rowSize
-      $.extend @metrics,
-        rowSize: rowSize
-        colSize: colSize
+      $.extend @metrics, { gutter, rowSize, colSize },
         wrapWidth: fullWidth * rowSize
         wrapHeight: fullHeight * colSize
       return


add javascript snippets

add javascript snippets

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -35,3 +35,46 @@
         assert.end()
       })
       '''
+  'pipe':
+    'prefix': 'pipe#functional'
+    'body': '''
+      // pipe(...functions: [...Function]) => initial => result
+      // Create a pipeline of functions to iterate over left to right
+      const pipe = (...functions) => inital => functions.reduce((accumulator, current) => current(accumulator), initial)
+      '''
+  'compose':
+    'prefix': 'compose#functional'
+    'body': '''
+      // compose(...functions: [...Function]) => initial => result
+      // Create a pipeline of functions to iterate over right to left
+      creates a pipeline of functions with the output of one function connected to the input of the next.
+      const compose = (...functions) => initial => functions.reduceRight((accumulator, current) => current(accumulator), initial)
+    '''
+  'curry':
+    'prefix': 'curry#functional'
+    'body': '''
+      // curry(f: Function, arr: Array) => result
+      // Autocurry, return a function that takes multiple parameters one at a time
+      const curry = (f, arr = []) =>
+        (...args) => (
+          a => a.length === f.length ? f(...a) : curry(f, a)
+        )([...arr, ...args])
+    '''
+  'reduce':
+     'prefix': 'reduce#functional'
+     'body': '''
+        ${1:array}.reduce((accumulator, current) => {
+          ${2:// Body}
+        }, ${3:initial})
+     '''
+  'reducer':
+    'prefix': 'reducer'
+    'body': '''
+      const ${1:myReducer} = (state = {}, action = {}) => {
+        const {type, payload} = action
+        switch (type) {
+          case '${2:FOO}': return {...state, ...payload}
+          default: return state
+        }
+      }
+    '''


Add additional snippets, mostly C++

Add additional snippets, mostly C++

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -1,6 +1,6 @@
 # Your snippets

-# Control structures for all C-based languages
+# C-based languages

 '.source.c,
  .source.cpp,
@@ -40,6 +40,69 @@
     'prefix': 'finally'
     'body': "finally {\n\t$1\n}"

+# C and C++
+
+'.source.c,
+ .source.cpp':
+
+  'include guard':
+    'prefix': 'once'
+    'body': "#pragma once\n"
+
+  'include':
+    'prefix': 'inc'
+    'body': "#include $1"
+
+  'include iostream':
+    'prefix': 'incio'
+    'body': "#include <iostream>\n"
+
+  'using namespace std':
+    'prefix': 'using'
+    'body': "using namespace std;\n"
+
+  'for':
+    'prefix': 'for'
+    'body': "for (int ${1:i} = ${2:0}; ${1:i} < ${3:end}; ${1:i} += ${4:1}) {\n\t$5\n}"
+
+  'function':
+    'prefix': 'f'
+    'body': "${1:void} ${2:fname}($3) {\n\t$4\n}"
+
+  'function method':
+    'prefix': 'fm'
+    'body': "${1:void} ${2:Class}::${3:fname}($4) {\n\t$5\n}"
+
+  'constructor definition':
+    'prefix': 'ctor'
+    'body': "${1:Class}::${1:Class}($2) {\n\t$3\n}"
+
+  'destructor definition':
+    'prefix': 'dtor'
+    'body': "${1:Class}::~${1:Class}() {\n\t$3\n}"
+
+  'class':
+    'prefix': 'class'
+    'body': "class ${1:Class} {\n\t$3\n};"
+
+  'this pointer access':
+    'prefix': 'this'
+    'body': 'this->'
+
+  'cin':
+    'prefix': 'cin'
+    'body': "cin >> $1;"
+
+  'cout':
+    'prefix': 'cout'
+    'body': "cout << $1;"
+
+  'cerr':
+    'prefix': 'cerr'
+    'body': "cerr << $1;"
+
+# JavaScript
+
 '.source.js':

   # Functions
@@ -66,7 +129,7 @@
     'prefix': 'expect'
     'body': "expect($1).${2:toBe}($3);"

-# Python 2 and 3 blocks/statements
+# Python 2 and 3

 '.source.python':

@@ -174,6 +237,14 @@
     'prefix': 'kv'
     'body': "'${1:key}' => ${2:value}$3"

+  'define constant':
+    'prefix': 'def'
+    'body': "define('$1', ${2:''});"
+
+  'display errors':
+    'prefix': 'derr'
+    'body': "ini_set('display_errors', '1');"
+
 # Inline PHP Blocks (meant to be intermixed with HTML)

 '.text.html.php':


WIP - Freesheet object change

WIP - Freesheet object change

--- src/main/js/web/FreesheetWeb.coffee
+++ src/main/js/web/FreesheetWeb.coffee
@@ -4,9 +4,11 @@
 PageFunctions = require './PageFunctions'
 TableWorksheet = require '../worksheet/TableWorksheet'

+freesheet = new Freesheet()
+
 loadScripts = -> $("script").filter( -> $(this).attr('type') == 'text/freesheet').each (i, el) ->
   scriptEl = $(this)
-  sheet = Freesheet.createSheet(scriptEl.attr('id'));
+  sheet = freesheet.createSheet(scriptEl.attr('id'));
   sheet.addFunctions PageFunctions
   sheet.load(scriptEl.text());

@@ -20,7 +22,7 @@
     worksheetEl = $("""

""").appendTo container
     new TableWorksheet(worksheetEl, sheet)

-  worksheets = (newWorksheet(sheet) for sheet in Freesheet.sheets())
+  worksheets = (newWorksheet(sheet) for sheet in freesheet.sheets())

   container.on 'click', 'button.show', ->
     container.removeClass('hidden')
@@ -33,7 +35,7 @@
   createWorksheets()

 freesheetWeb = { loadScripts, createWorksheets, loadScriptsIntoWorksheets }
-freesheetWeb[key] = value for key, value of Freesheet
+freesheetWeb[key] = value for key, value of freesheet

 if typeof module != 'undefined'
   module.exports = freesheetWeb


No need this.

No need this.

--- client/landing/app/Applications/Topics.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Topics.kdapplication/AppController.coffee
@@ -8,7 +8,6 @@
     ,options
     super options,data
     @listItemClass = TopicsListItemView
-    @_searchValue = ""
     @controllers = {}

   bringToFront:()->


Fixed a minor issue with a splat

Fixed a minor issue with a splat

--- src/runtime.js.coffee
+++ src/runtime.js.coffee
@@ -64,7 +64,7 @@
       messages = [messages]

     for controller in @active
-      @narrowcast controller, messages, args
+      @narrowcast controller, messages, args...


View for fuzzy completion

View for fuzzy completion

Towards #2

--- design.coffee
+++ design.coffee
@@ -2,8 +2,10 @@
   views:
     by_watchers:
       map: (doc) -> emit doc.watchers, doc.description if doc.watchers
+
     by_language:
       map: (doc) -> emit doc.language, doc.description if doc.language
+
     by_prefix:
       map: (doc) ->
         return unless doc.owner and doc.name
@@ -20,6 +22,51 @@
           # emit all combined prefixes
           for d, j in doc.name
             emit [prefix + '/' + doc.name.substr(0, j+1).toLowerCase(), doc.watchers], doc.description
+        return
+
+    by_fuzzy:
+      map: (doc) ->
+        return unless doc.owner and doc.name
+
+        combinations = (arr, max) ->
+          combine = (n, src, got, all) ->
+            if n is 0
+              all[all.length] = got if got.length > 0
+              return
+            for item, j in src
+              combine(n - 1, src.slice(j + 1), got.concat([item]), all)
+            return
+          all = []
+          max = arr.length unless max and max < arr.length
+          for i in [1..max]
+            combine(i, arr, [], all)
+          all
+
+        prefixVariations = (words, max, got=[]) ->
+          current = words[0]
+          max_length = if max and max < current.length then max else current.length
+          if words.length > 1
+            all = []
+            for i in [1..max_length]
+              all = all.concat(prefixVariations(words.slice(1), max, got.concat([current.substr(0,i)])))
+            all
+          else
+            got.concat([current.substr(0,i)]).join('') for i in [1..max_length]
+
+        # only split on capital letters if there's no _ or - in the id
+        id = doc._id.indexOf('_') == -1 && doc._id.indexOf('-') == -1 && doc._id.replace(/([A-Z][^A-Z])/g, '-$1') || doc._id
+        words = id.toLowerCase().split(/[^a-z0-9]+/).filter (s) -> s.length > 0
+
+        for combination in combinations(words, 7)
+          max = if combination.length < 5 then undefined else 10-combination.length
+          for key in prefixVariations(combination, max)
+            emit [key, doc.watchers], doc.description
+
+        # making sure the full names are emitted
+        if words.length > 7 or combination.length > 4 and words.some((w) -> w.length > 10-words.length)
+          emit [doc._id.toLowerCase().replace(/[^a-z0-9]/g, ''), doc.watchers], doc.description
+
+        return

   lists:
     complete: (head, req) ->


sync: add a 1-minute ping that is used entirely for determining clock skew

--- salvus/client.coffee
+++ salvus/client.coffee
@@ -313,8 +313,11 @@
                 @emit("data", channel, data)
         @_connected = false

-        # start pinging -- not used/needed with primus
-        #@_ping()
+        # start pinging -- not used/needed for primus, but *is* needed for getting information about server_time
+        # In particular, this ping time is not reported to the user and is not used as a keep-alive, hence it
+        # can be fairly long.
+        @_ping_interval = 60000
+        @_ping()

     _ping: () =>
         if not @_ping_interval?
@@ -324,12 +327,31 @@
             message : message.ping()
             timeout : 20  # 20 second timeout
             cb      : (err, pong) =>
-                # console.log(err, pong)
+                #console.log(err, pong)
                 if not err and pong?.event == 'pong'
+                    @_last_pong = {server:pong.now, local:new Date()}
+                    # See the function server_time below; subtract @_clock_skew from local time to get a better
+                    # estimate for server time.
+                    @_clock_skew = @_last_ping - 0 + ((@_last_pong.local - @_last_ping)/2) - @_last_pong.server
+                    localStorage.clock_skew = @_clock_skew
                     latency = new Date() - @_last_ping
-                    @emit "ping", latency
+                    # We do not emit this, since that's now handled by primus.
+                    #@emit "ping", latency
                 # try again later
                 setTimeout(@_ping, @_ping_interval)
+
+    server_time: =>
+        # Add _clock_skew to our local time to get a better estimate of the actual time on the server.
+        # This can help compensate in case the user's clock is wildly wrong, e.g., by several minutes,
+        # or even hours due to totally wrong time (e.g. ignoring time zone), which is relevant for
+        # some algorithms including sync which uses time.  Getting the clock right up to a small multiple
+        # of ping times is fine for our application.
+        if not @_clock_skew?
+            # try localStorage
+            if localStorage.clock_skew?
+                @_clock_skew = parseFloat(localStorage.clock_skew)
+        return new Date(new Date() - (@_clock_skew ? 0))
+

     ping_test: (opts) =>
         opts = defaults opts,


Fix bug. Use shift to get the first callback and not unshift

--- drivers/javascript/src/cursor.coffee
+++ drivers/javascript/src/cursor.coffee
@@ -39,7 +39,7 @@

             # If there's no more data let's notify the waiting callback
             if not @hasNext()
-                cb = @_cbQueue.unshift()
+                cb = @_cbQueue.shift()
                 cb new RqlDriverError "No more rows in the cursor."
             else


Fix #3. Site menu items.

Fix #3. Site menu items.

--- demo/coffee/app.coffee
+++ demo/coffee/app.coffee
@@ -11,12 +11,12 @@

 widgets  = [
     { name: 'Form Builder', template: 'formbuilder' }
-    { name: 'Select Widget', template: 'select' }
-    { name: 'MultiSelect Widget', template: 'multiselect' }
+    { name: 'Select', template: 'select' }
+    { name: 'MultiSelect', template: 'multiselect' }
     { name: 'Radio Group', template: 'radio' }
-    { name: 'Checkboxes', template: 'checkbox' }
-    { name: 'Date/Time Widgets', template: 'datetime' }
-    { name: 'fsList', template: 'list' }
+    { name: 'Checkbox Group', template: 'checkbox' }
+    { name: 'Date/Time', template: 'datetime' }
+    { name: 'List', template: 'list' }
     # { name: 'fsInput', template: 'input' }
 ]


bug where it was inadvertantly using global variable

--- src/prong/track/trackHeader.coffee
+++ src/prong/track/trackHeader.coffee
@@ -35,7 +35,7 @@
             .text('x')

         closeButton.on 'click', (d) =>
-            sequence.removeTrack(d)
+            trackHeader.sequence().removeTrack(d)
             #console.log('remove track', d)

     return d3.rebind(trackHeader, commonProperties(), 'sequence')


Remove unnecessary logging from heat demand chart

--- app/assets/javascripts/d3/heat_demand_and_production.coffee
+++ app/assets/javascripts/d3/heat_demand_and_production.coffee
@@ -18,17 +18,11 @@
       @series = @model.target_series().concat(@getSeries())
       data = @series.map(@getSerie)

-      # console.log @demandKey
-      # console.log data
-
       demandVals = data.find((serie) => serie.key == @demandKey).values
       production = data.find((serie) => serie.key == @productionKey)

       surplusVals = new Array(demandVals.length)
       deficitVals = new Array(demandVals.length)
-
-      console.log(demandVals.slice(22, 30))
-      console.log(production.values.slice(22, 30))

       # Remove from the production series any time when production exceeds
       # demand.


Make sure formatting runs after value setting on effective_price

--- app/assets/javascripts/effective_price/initialize.js.coffee
+++ app/assets/javascripts/effective_price/initialize.js.coffee
@@ -18,6 +18,10 @@
   if event.key && event.key.length == 1 && event.metaKey == false && allowed.indexOf(event.key) == -1
     event.preventDefault()

+# Assign the hidden input a value of 100x value
+$(document).on 'change keyup', "input[type='text'].effective_price", (event) ->
+  value = (parseFloat($(event.target).val().replace(',', '')) || 0.00) * 100.00
+  $(event.target).siblings("input[type='hidden']").first().val(value.toFixed(0))

 # Format the value for display as currency (USD)
 $(document).on 'change', "input[type='text'].effective_price", (event) ->
@@ -30,8 +34,3 @@
     value = ''

   $(event.target).val(value)
-
-# Assign the hidden input a value of 100x value
-$(document).on 'change keyup', "input[type='text'].effective_price", (event) ->
-  value = (parseFloat($(event.target).val().replace(',', '')) || 0.00) * 100.00
-  $(event.target).siblings("input[type='hidden']").first().val(value.toFixed(0))


Trigger scroll event on order-summary to fix the affix position

When clicking on credit card payment option the order summary was not updading
its position.

--- app/assets/javascripts/neighborly/modules/payment.js.coffee
+++ app/assets/javascripts/neighborly/modules/payment.js.coffee
@@ -30,3 +30,5 @@
               return (this.bottom = $('footer').outerHeight(true) + 65)
           }
         })
+
+        $('.order-summary').trigger('scroll')


Adds the possibility of getting Spock saying 'KHAAAAN' from Star Trek Into Darkness if you khanify

--- src/scripts/meme_generator.coffee
+++ src/scripts/meme_generator.coffee
@@ -153,8 +153,15 @@
     memeResponder robot, meme

   robot.respond /(memegen )?k(?:ha|ah)nify (.*)/i, (msg) ->
-    memeGenerator msg, 6443, 1123022, "", khanify(msg.match[2]), (url) ->
-      msg.send url
+    if Math.random() > 0.5
+      # Kirk khan
+      memeGenerator msg, 6443, 1123022, "", khanify(msg.match[2]), (url) ->
+        msg.send url
+    else
+      # Spock khan
+      memeGenerator msg, 2103732, 8814557, "", khanify(msg.match[2]), (url) ->
+        msg.send url
+

   robot.respond /(memegen )?(IF .*), ((ARE|CAN|DO|DOES|HOW|IS|MAY|MIGHT|SHOULD|THEN|WHAT|WHEN|WHERE|WHICH|WHO|WHY|WILL|WON\'T|WOULD)[ \'N].*)/i, (msg) ->
     memeGenerator msg, 17, 984, msg.match[2], msg.match[3] + (if msg.match[3].search(/\?$/)==(-1) then '?' else ''), (url) ->


Added an optional memegen to the beginning of all commands

--- src/scripts/meme_generator.coffee
+++ src/scripts/meme_generator.coffee
@@ -35,77 +35,77 @@
   unless robot.brain.data.memes?
     robot.brain.data.memes = [
       {
-        regex: /(Y U NO) (.+)/i,
+        regex: /(memegen )?(Y U NO) (.+)/i,
         generatorID: 2,
         imageID: 166088
       },
       {
-        regex: /(I DON'?T ALWAYS .*) (BUT WHEN I DO,? .*)/i,
+        regex: /(memegen )?(I DON'?T ALWAYS .*) (BUT WHEN I DO,? .*)/i,
         generatorID: 74,
         imageID: 2485
       },
       {
-        regex: /(.*)(O\s?RLY\??.*)/i,
+        regex: /(memegen )?(.*)(O\s?RLY\??.*)/i,
         generatorID: 920,
         imageID: 117049
       },
       {
-        regex: /(.*)(SUCCESS|NAILED IT.*)/i,
+        regex: /(memegen )?(.*)(SUCCESS|NAILED IT.*)/i,
         generatorID: 121,
         imageID: 1031
       },
       {
-        regex: /(.*) (ALL the .*)/i,
+        regex: /(memegen )?(.*) (ALL the .*)/i,
         generatorID: 6013,
         imageID: 1121885
       },
       {
-        regex: /(.*) (\w+\sTOO DAMN .*)/i,
+        regex: /(memegen )?(.*) (\w+\sTOO DAMN .*)/i,
         generatorID: 998,
         imageID: 203665
       },
       {
-        regex: /(GOOD NEWS EVERYONE[,.!]?) (.*)/i,
+        regex: /(memegen )?(GOOD NEWS EVERYONE[,.!]?) (.*)/i,
         generatorID: 1591,
         imageID: 112464
       },
       {
-        regex: /(NOT SURE IF .*) (OR .*)/i,
+        regex: /(memegen )?(NOT SURE IF .*) (OR .*)/i,
         generatorID: 305,
         imageID: 84688
       },
       {
-        regex: /(YO DAWG .*) (SO .*)/i,
+        regex: /(memegen )?(YO DAWG .*) (SO .*)/i,
         generatorID: 79,
         imageID: 108785
       },
       {
-        regex: /(ALL YOUR .*) (ARE BELONG TO US)/i,
+        regex: /(memegen )?(ALL YOUR .*) (ARE BELONG TO US)/i,
         generatorID: 349058,
         imageID: 2079825
       },
       {
-        regex: /(.*) (FUCK YOU)/i,
+        regex: /(memegen )?(.*) (FUCK YOU)/i,
         generatorID: 1189472,
         imageID: 5044147
       },
       {
-        regex: /(.*) (You'?re gonna have a bad time)/i,
+        regex: /(memegen )?(.*) (You'?re gonna have a bad time)/i,
         generatorID: 825296,
         imageID: 3786537
       },
       {
-        regex: /(one does not simply) (.*)/i,
+        regex: /(memegen )?(one does not simply) (.*)/i,
         generatorID: 274947,
         imageID: 1865027
       },
       {
-        regex: /(grumpy cat) (.*),(.*)/i,
+        regex: /(memegen )?(grumpy cat) (.*),(.*)/i,
         generatorID: 1590955,
         imageID: 6541210
       },
       {
-        regex: /(it looks like you're|it looks like you) (.*)/i,
+        regex: /(memegen )?(it looks like you're|it looks like you) (.*)/i,
         generatorID: 20469,
         imageID: 1159769
       }
@@ -114,30 +114,30 @@
   for meme in robot.brain.data.memes
     memeResponder robot, meme

-  robot.respond /add meme \/(.+)\/i,(.+),(.+)/i, (msg) ->
+  robot.respond /(memegen )?add meme \/(.+)\/i,(.+),(.+)/i, (msg) ->
     meme =
-      regex: new RegExp(msg.match[1], "i")
-      generatorID: parseInt(msg.match[2])
-      imageID: parseInt(msg.match[3])
+      regex: new RegExp(msg.match[2], "i")
+      generatorID: parseInt(msg.match[3])
+      imageID: parseInt(msg.match[4])

     robot.brain.data.memes.push meme
     memeResponder robot, meme

-  robot.respond /k(?:ha|ah)nify (.*)/i, (msg) ->
-    memeGenerator msg, 6443, 1123022, "", khanify(msg.match[1]), (url) ->
+  robot.respond /(memegen )?k(?:ha|ah)nify (.*)/i, (msg) ->
+    memeGenerator msg, 6443, 1123022, "", khanify(msg.match[2]), (url) ->
       msg.send url

-  robot.respond /(IF .*), ((ARE|CAN|DO|DOES|HOW|IS|MAY|MIGHT|SHOULD|THEN|WHAT|WHEN|WHERE|WHICH|WHO|WHY|WILL|WON\'T|WOULD)[ \'N].*)/i, (msg) ->
-    memeGenerator msg, 17, 984, msg.match[1], msg.match[2] + (if msg.match[2].search(/\?$/)==(-1) then '?' else ''), (url) ->
+  robot.respond /(memegen )?(IF .*), ((ARE|CAN|DO|DOES|HOW|IS|MAY|MIGHT|SHOULD|THEN|WHAT|WHEN|WHERE|WHICH|WHO|WHY|WILL|WON\'T|WOULD)[ \'N].*)/i, (msg) ->
+    memeGenerator msg, 17, 984, msg.match[2], msg.match[3] + (if msg.match[3].search(/\?$/)==(-1) then '?' else ''), (url) ->
       msg.send url

-  robot.respond /((Oh|You) .*) ((Please|Tell) .*)/i, (msg) ->
-    memeGenerator msg, 542616, 2729805, msg.match[1], msg.match[3], (url) ->
+  robot.respond /(memegen )?((Oh|You) .*) ((Please|Tell) .*)/i, (msg) ->
+    memeGenerator msg, 542616, 2729805, msg.match[2], msg.match[4], (url) ->
       msg.send url

 memeResponder = (robot, meme) ->
   robot.respond meme.regex, (msg) ->
-    memeGenerator msg, meme.generatorID, meme.imageID, msg.match[1], msg.match[2], (url) ->
+    memeGenerator msg, meme.generatorID, meme.imageID, msg.match[2], msg.match[3], (url) ->
       msg.send url

 memeGenerator = (msg, generatorID, imageID, text0, text1, callback) ->


webapp/files: tiny download button same icon as elsewhere and matching colors

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -247,13 +247,13 @@
                 <TimeAgo date={(new Date(@props.time * 1000)).toISOString()} style={color:'#666'}/>
                 <span className='pull-right' style={color:'#666'}>
                     {human_readable_size(@props.size)}
-                    <Button style={marginLeft: '1em'}
+                    <Button style={marginLeft: '1em', background:'transparent'}
                             bsStyle='default'
                             bsSize='xsmall'
                             target='_blank'
                             href="#{href_download}"
                             onClick = {(e)->e.stopPropagation()}>
-                        {String.fromCharCode("8615")}
+                        <Icon name='cloud-download' style={color: '#666'} />
                     </Button>
                 </span>
             </Col>


adjust delete text to be clearer

adjust delete text to be clearer

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -985,7 +985,8 @@
             </Row>
             <Row style={marginBottom:'10px'}>
                 <Col sm=12>
-                    Backups of your files are available in the
+                    Deleting a file immediately deletes it from disk freeing up space; however, older
+                    backups of your files may still be available in the
                     e.preventDefault(); @props.actions.open_directory('.snapshots')}>~/.snapshots directory.
                 </Col>
             </Row>


Bring back detailed errors

Bring back detailed errors

--- modules/player/commands.coffee
+++ modules/player/commands.coffee
@@ -52,7 +52,11 @@
                 player.play()
       catch e
         console.error e
-        m.reply 'Something went wrong.'
+        m.reply '', false, {
+          color: 0xAA3300
+          # Windows 10 installer flashbacks
+          description: e.message.split('YouTube said:')[1] or 'Something went wrong.'
+        }

     # Skip
     @registerCommand 'skip', (m, args, d, player)=>


KernelManager: Remove method `getLanguageMappings`

--- lib/kernel-manager.coffee
+++ lib/kernel-manager.coffee
@@ -41,7 +41,7 @@

     getTrueLanguage: (language) ->
         if language?
-            languageMappings = @getLanguageMappings()
+            languageMappings = @getConfigJson 'languageMappings'
             languageMatches = _.filter languageMappings,
                 (trueLanguage, languageKey) ->
                     return languageKey?.toLowerCase() is language.toLowerCase()
@@ -61,8 +61,6 @@
     setConfigJson: (key, value, merge=false) ->
         value = _.merge @getConfigJson(key), value if merge
         atom.config.set "Hydrogen.#{key}", JSON.stringify value
-
-    getLanguageMappings: -> @getConfigJson('languageMappings')

     getKernelInfoForLanguage: (grammarLanguage) ->
         kernels = @getAvailableKernels()


Fix bad typo

Fix bad typo

Replace `Backbone.utilsBackbone.utils` with `Backbone.utils`

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -194,7 +194,7 @@
   #   e.g.
   #   @delegate('click', 'button.confirm', @confirm)
   delegate: (eventName, second, third) ->
-    if Backbone.utilsBackbone.utils and Backbone.utilsBackbone.utils?.delegate
+    if Backbone.utils and Backbone.utils?.delegate
       return Backbone.utils.delegate(this, eventName, second, third)
     if typeof eventName isnt 'string'
       throw new TypeError 'View#delegate: first argument must be a string'
@@ -251,7 +251,7 @@

   # Remove all handlers registered with @delegate.
   undelegate: (eventName, second, third) ->
-    if Backbone.utilsBackbone.utils and Backbone.utilsBackbone.utils?.undelegate
+    if Backbone.utils and Backbone.utils?.undelegate
       return Backbone.utils.undelegate(this, eventName, second, third)
     if eventName
       if typeof eventName isnt 'string'


snap: some fixes to creation of bup_dir on very first startup.

--- salvus/snap.coffee
+++ salvus/snap.coffee
@@ -968,16 +968,21 @@
     # ensure "fsck -g" works on the archive, delete tmp files, etc.

 ensure_active_bup_dir_exists = (cb) ->
-    fs.exists "#{bup_dir}/active", (exists) ->
+    active_file = "#{bup_dir}/active"
+    fs.exists active_file, (exists) ->
         if exists
             cb()
         else
-            u = uuid.v4()
-            fs.writeFileSync("#{bup_dir}/active", u)
-            bup
-                args    : ['init']
-                bup_dir : "#{bup_dir}/#{u}"
-                cb      : cb
+            misc_node.ensure_containing_directory_exists active_file, (err) ->
+                if err
+                    cb("FATAL -- unable to create #{bup_dir}")
+                    return
+                u = uuid.v4()
+                bup
+                    args    : ['init']
+                    bup_dir : "#{bup_dir}/#{u}"
+                    cb      : cb
+                fs.writeFileSync(active_file, u)

 # Generate or read uuid of this server, which is the longterm identification
@@ -1121,11 +1126,11 @@
     winston.info "starting server..."
     async.series([
         (cb) ->
+            initialize_snap_dir(cb)
+        (cb) ->
             create_server (err,port) ->
                 listen_port = port
                 cb(err)
-        (cb) ->
-            initialize_snap_dir(cb)
         (cb) ->
             initialize_server_uuid(cb)
         (cb) ->


ensure done callback is fired once rsync is finished

--- app/templates/gulp/tasks/rsync.coffee
+++ app/templates/gulp/tasks/rsync.coffee
@@ -1,15 +1,12 @@
 gulp         = require "gulp"
+gutil        = require "gulp-util"
 extend       = require "extend"
-rsync        = require "rsyncwrapper"
-gutil        = require "gulp-util"
+rsync        = require("rsyncwrapper")

 conf = require '../gulpconfig'
 secret = require '../secrets'

-
-
-
-_rsyncDo = (rsyncOpts = {}) ->
+processRsync = (done, rsyncOpts = {}) ->
   rsyncOpts = extend {
     port: conf.ssh.port
     ssh: true
@@ -23,8 +20,9 @@
   rsync rsyncOpts, (error, stdout, stderr, cmd) ->
     gutil.log error if error
     gutil.log cmd, stderr, stdout
+    done()

-_rsyncPrepare = (prop, isToRemote = true, rsyncOpts = {}) ->
+prepareRsync = (done, prop, isToRemote = true, rsyncOpts = {}) ->
   ["dest", "src"].forEach (curr) ->
     remoteHost = "#{secret.username}@#{secret.domain}:"
     rsyncOpts[curr] = if rsyncOpts[curr] then rsyncOpts[curr] else conf.rsync[prop][curr]
@@ -34,24 +32,46 @@

   rsyncOpts.exclude = conf.rsync[prop].exclude || ""

-  _rsyncDo(rsyncOpts)
+  processRsync(done, rsyncOpts)

-# dry-run down
-gulp.task "rsync:downdry", () ->
-  _rsyncPrepare "down", false, dryRun: true
+#*------------------------------------*\
+#     $RSYNC DOWN DRY RUN
+#*------------------------------------*/
+gulp.task "rsync:downdry", (done) ->
+  prepareRsync done, "down", false, dryRun: true

+
+
+
+
+#*------------------------------------*\
+#     $RSYNC DOWN
+#*------------------------------------*/
 # sync down
-gulp.task "rsync:down", () ->
-  _rsyncPrepare "down", false
+gulp.task "rsync:down", (done) ->
+  prepareRsync done, "down", false

+
+
+
+
+#*------------------------------------*\
+#     $RSYNC TO PROD DRY RUN
+#*------------------------------------*/
 # dry-run sync to prod
-gulp.task "rsync:updry", () ->
-  _rsyncPrepare "up", true, dryRun: true
+gulp.task "rsync:updry", (done) ->
+  prepareRsync done, "up", true, dryRun: true

-# sync to production
-gulp.task "rsync:up", () ->
-  _rsyncPrepare "up"
+
+
+
+
+#*------------------------------------*\
+#     $RSYNC TO PROD
+#*------------------------------------*/
+gulp.task "rsync:up", (done) ->
+  prepareRsync done, "up"


added angry smily

added angry smily

--- src/plugins/faction/fallback.coffee
+++ src/plugins/faction/fallback.coffee
@@ -14,7 +14,10 @@

             template = [
                 ['@{player} 哼 ']
-                ['{smily:掀桌}']
+                [
+                    '{smily:掀桌}'
+                    '{smily:angry}'
+                ]
             ]

         else if /卖.*?萌/.test r.body


fix off by one pixel

fix off by one pixel

--- tests/scripts/unit/modules/cursor.coffee
+++ tests/scripts/unit/modules/cursor.coffee
@@ -25,28 +25,28 @@
       cursorManager.setCursor('id', 2, 'Test', 'red')
       cursor = cursorManager.container.querySelector('.cursor')
       expect(cursor.style.left).to.equal('17px')
-      expect(parseInt(cursor.style.top)).to.be.within(0, 1)
+      expect(parseInt(cursor.style.top)).to.be.within(-1, 1)
     )

     it('should set at leaf boundary', ->
       cursorManager.setCursor('id', 4, 'Test', 'red')
       cursor = cursorManager.container.querySelector('.cursor')
       expect(cursor.style.left).to.equal('29px')
-      expect(parseInt(cursor.style.top)).to.be.within(0, 1)
+      expect(parseInt(cursor.style.top)).to.be.within(-1, 1)
     )

     it('should set at newline', ->
       cursorManager.setCursor('id', 11, 'Test', 'red')
       cursor = cursorManager.container.querySelector('.cursor')
       expect(cursor.style.left).to.equal('0px')
-      expect(parseInt(cursor.style.top)).to.be.within(15, 16)
+      expect(parseInt(cursor.style.top)).to.be.within(14, 16)
     )

     it('should set at beginning', ->
       cursorManager.setCursor('id', 0, 'Test', 'red')
       cursor = cursorManager.container.querySelector('.cursor')
       expect(cursor.style.left).to.equal('0px')
-      expect(parseInt(cursor.style.top)).to.be.within(0, 1)
+      expect(parseInt(cursor.style.top)).to.be.within(-1, 1)
     )

     it('should set at end', ->
@@ -85,7 +85,7 @@
       expect(cursor.style.left).to.equal('9px')
       editor.insertAt(0, 'A', { bold: true })
       expect(parseInt(cursor.style.left)).to.be.within(18, 19)
-      expect(parseInt(cursor.style.top)).to.be.within(0, 1)
+      expect(parseInt(cursor.style.top)).to.be.within(-1, 1)
     )

     it('should append after external edit', ->


fix dates of empty course

fix dates of empty course

--- app/assets/javascripts/components/timeline/timeline.cjsx
+++ app/assets/javascripts/components/timeline/timeline.cjsx
@@ -184,6 +184,8 @@
           week={{is_new: false}}
           blocks=[]
           empty_timeline=true
+          start={@props.course.timeline_start}
+          end={moment(@props.course.timeline_start).add(6, 'day')}
         />
       )


Add test

Add test

--- src/test/coffee/logger_test.coffee
+++ src/test/coffee/logger_test.coffee
@@ -2,6 +2,15 @@
 log = new Logger 3, 'Test'

 describe 'Logger', ->
+  describe '#d', ->
+    it 'should log as debug level without error', ->
+      log.d 'something'
   describe '#i', ->
     it 'should log as info level without error', ->
       log.i 'something'
+  describe '#w', ->
+    it 'should log as warn level without error', ->
+      log.w 'something'
+  describe '#e', ->
+    it 'should log as error level without error', ->
+      log.e 'something'


Remove unnecesary empty parameter list on coffeescript file

--- app/assets/javascripts/legislation_annotatable.js.coffee
+++ app/assets/javascripts/legislation_annotatable.js.coffee
@@ -84,7 +84,7 @@
         annotation_id: pid
         annotation_url: target.closest(".legislation-annotatable").data("legislation-annotatable-base-url")

-  isMobile: () ->
+  isMobile: ->
     return window.innerWidth <= 652

   viewerExtension: (viewer) ->


Manually resize the input box after triggering submit.

--- app/assets/javascripts/web/views/messages_view.js.coffee
+++ app/assets/javascripts/web/views/messages_view.js.coffee
@@ -17,7 +17,13 @@
     @textArea().bind 'keydown', (event) =>
       if (event.which == 13) and (event.shiftKey == false)
         event.preventDefault()
-        @form().trigger('submit')
+
+        if @textArea().val().replace(/^[(\r\n|\n|\r)\s]+/ig,"") == ""
+          @textArea().val("")
+          @textArea().trigger('autosize.resize')
+        else
+          @form().trigger('submit') unless @textArea().val() == ""
+          @textArea().trigger('autosize.resize')
         return false

   willDestroyElement: ->


Fix observing downloads.

Fix observing downloads.

--- application/chrome/content/wesabe/download/Player.coffee
+++ application/chrome/content/wesabe/download/Player.coffee
@@ -162,8 +162,7 @@
         unless folder.exists
           folder.create()

-        statement = statements.child(uuid()).asFile
-
+        statement = folder.child(uuid()).asFile
         statement.write data

         metadata = @job.nextDownloadMetadata or {}


Adds better if statement

Adds better if statement

--- apps/partner/client/shows_grid.coffee
+++ apps/partner/client/shows_grid.coffee
@@ -107,7 +107,7 @@
           @remainingUpcoming = @remainingUpcoming.concat moreShows.upcoming().models
           @remainingPast = @remainingPast.concat moreShows.past().models
           shows = @getRemainingShows type #get updated shows list from fetch
-          displayMore = if shows.length < 30 then false else true
+          displayMore = shows.length > 30
     else
       displayMore = false


fix indent

fix indent

--- lib/server.storage.coffee
+++ lib/server.storage.coffee
@@ -5,8 +5,8 @@
 _redisClient = redis.createClient()

 exports.createSessionStore = (express) ->
-   RedisStore = require("connect-redis")(express)
-   new RedisStore(client: _redisClient)
+  RedisStore = require("connect-redis")(express)
+  new RedisStore(client: _redisClient)

 exports.getAuthenticatedUsers = (callback) ->
   _redisClient.smembers("authenticatedUsers", callback)


improve bubbling of errors

improve bubbling of errors

--- src/textorum/relaxng/objects.coffee
+++ src/textorum/relaxng/objects.coffee
@@ -135,7 +135,7 @@
         throw new Error("ack node missing")
       _nodelog node, "checking #{this} against", node, [this, node]
       res = @_check(node, descend)
-      _nodelog node, "result for #{this} againt", node, "is #{res}", [this, node, res]
+      _nodelog node, "result for #{this} against", node, "is #{res}", [this, node, res]
       return res

     _check: (node, descend) =>
@@ -146,6 +146,8 @@
       "<UNDEFINED PATTERN>"
     nullable: =>
       false
+    require: =>
+      return this
     contains: (nodeName) =>
       throw new RNGException("Cannot call 'contains(#{nodeName})' on pattern '#{@toString()}'")
     dereference: =>
@@ -172,7 +174,7 @@
     constructor: (@message, @pattern, @childNode, @priority) ->
     toString: =>
       if @message
-        "notAllowed /* # #{@message} */"
+        "notAllowed { #{@message} }"
       else
         "notAllowed"
     _check: (node, descend) =>
@@ -184,7 +186,7 @@
     constructor: (@message, @pattern, @childNode, @priority) ->
     toString: =>
       if @message
-        "missingContent /* # #{@message} */"
+        "missingContent { #{@message} }"
       else
         "missingContent"

@@ -212,45 +214,67 @@
       @pattern1.contains(nodeName) or @pattern2.contains(nodeName)
     nullable: =>
       @pattern1.nullable() or @pattern2.nullable()
+    require: =>
+      p1 = @pattern1.require()
+      if p1 instanceof GoodElement
+        return p1
+      p2 = @pattern2.require()
+      if p2 instanceof GoodElement
+        return p2
+      if p1 instanceof NotAllowed
+        if p2 instanceof Empty
+          return p2
+        return new Choice(p1, new MissingContent("Missing: #{p2}", p2))
+      if p2 instanceof NotAllowed
+        if p1 instanceof Empty
+          return p1
+        return new Choice(new MissingContent("Missing: #{p1}"), p2)
+      return p2
+
     _check: (node, descend) =>
       if @pattern1 instanceof Empty and @pattern2 instanceof Attribute
         return @pattern1
       if @pattern2 instanceof Empty and @pattern1 instanceof Attribute
         return @pattern2
-
       if @pattern1 instanceof GoodElement
         return @pattern1
       if @pattern2 instanceof GoodElement
         return @pattern2
-      if @pattern1 instanceof NotAllowed
-        return @pattern2.check(node, descend)
-      if @pattern2 instanceof NotAllowed
-        return @pattern1.check(node, descend)
+      # if @pattern1 instanceof NotAllowed
+      #   return @pattern2.check(node, descend)
+      # if @pattern2 instanceof NotAllowed
+      #   return @pattern1.check(node, descend)
       p1 = @pattern1.check(node, descend)
       p2 = @pattern2.check(node, descend)
+      if @pattern2 + "" is "c-idp9392+"
+        console.log "tick"
       if p1 instanceof NotAllowed and p2 instanceof NotAllowed
         failed = new Choice(p1, p2)
         return new NotAllowed("choice failed: #{failed}", failed, node)
-      if p2 instanceof Empty and p1 instanceof Empty
-        return p1
-      if p1 instanceof NotAllowed and @pattern1 instanceof Empty
+      if p2.require() instanceof Empty and p1.require() instanceof Empty
+        if p1 instanceof Empty
+          return p2
+        if p2 instanceof Empty
+          return p1
+        return new Choice(p1, p2)
+      if p1 instanceof NotAllowed and @pattern1.require() instanceof Empty
         return new Choice(@pattern1, p2)
-      if p2 instanceof NotAllowed and @pattern2 instanceof Empty
+      if p2 instanceof NotAllowed and @pattern2.require() instanceof Empty
         return new Choice(p1, @pattern2)
-      if p2 instanceof NotAllowed
-        return p1
       return new Choice(p1, p2)
     attrCheck: (node) =>
-      if @pattern1 instanceof NotAllowed or @pattern2 instanceof Empty
+      if @pattern1 instanceof NotAllowed or @pattern1 instanceof Empty
         return @pattern2.attrCheck(node)
-      if @pattern2 instanceof NotAllowed or @pattern1 instanceof Empty
+      if @pattern2 instanceof NotAllowed or @pattern2 instanceof Empty
         return @pattern1.attrCheck(node)
       p1 = @pattern1.attrCheck(node)
       if p1 instanceof NotAllowed
         return @pattern2.attrCheck(node)
       p2 = @pattern2.attrCheck(node)
-      if p2 instanceof NotAllowed
-        return p1
+      if p2 instanceof NotAllowed or p2 instanceof Empty
+        return p1
+      if p1 instanceof Empty
+        return p2
       return (new Choice(p1, p2)).attrCheck(node)

   class Interleave extends Pattern
@@ -261,6 +285,21 @@
       "(#{@pattern1} & #{@pattern2})"
     nullable: =>
       @pattern1.nullable() and @pattern2.nullable()
+    require: =>
+      p1 = @pattern1.require()
+      if p1 instanceof Empty
+        return @pattern2.require()
+      p2 = @pattern2.require()
+      if p2 instanceof Empty
+        return p1
+      if p1 instanceof GoodElement and p2 instanceof GoodElement
+        return p1
+      if p1 instanceof NotAllowed
+        return p1
+      if p2 instanceof NotAllowed
+        return p2
+      return p2
+
     _check: (node, descend) =>
       if @pattern1 instanceof NotAllowed or @pattern2 instanceof Empty
         return @pattern1.check(node, descend)
@@ -289,6 +328,23 @@
       "(#{@pattern1}, #{@pattern2})"
     nullable: =>
       @pattern1.nullable() and @pattern2.nullable()
+    require: =>
+      p1 = @pattern1.require()
+      p2 = @pattern2.require()
+      if p1 instanceof GoodElement
+        if p2 instanceof Empty
+          return p1
+      if p2 instanceof GoodElement
+        if p1 instanceof Empty
+          return p2
+      if p1 instanceof Empty and p2 instanceof Empty
+        return p1
+      if p1 instanceof Empty
+        return new MissingContent("missing group element 2", this)
+      if p2 instanceof Empty
+        return new MissingContent("missing group element 1", this)
+      return new MissingContent("missing both group elements", this)
+
     _check: (node, descend) =>
       if @pattern2 instanceof GoodElement
         return new Empty("null branch")
@@ -313,10 +369,6 @@
       if p2 instanceof NotAllowed
         # Return nullabled group
         return new Group(p1, @pattern2)
-      if p1 instanceof Empty
-        return p2
-      if p2 instanceof Empty
-        return p1
       return new Group(p1, p2)
     attrCheck: (node) =>
       p1 = @pattern1.attrCheck(node)
@@ -335,14 +387,16 @@
       "#{@pattern}+"
     nullable: =>
       @pattern.nullable()
+    require: =>
+      return new MissingContent("missing #{this}", this)
     _check: (node, descend) =>
       p1 = @pattern.check(node, descend)
       if p1 instanceof NotAllowed
         return p1
-      return new Group(p1, new Choice(new Empty(), this))
+      return new Choice(new Empty("oneOrMore satisfied"), this)
     attrCheck: (node) =>
       p1 = @pattern.attrCheck(node)
-      return (new Group(p1, new Choice(new Empty(), this)))
+      return (new Choice(new Empty("oneOrMore satisfied"), this))

   class List extends Pattern
     constructor: (pattern) ->
@@ -475,16 +529,17 @@
                   continue
               when Node.COMMENT_NODE
                 continue
+            curPattern = nextPattern
             _nodelog node, "==> checking child", child, "against", nextPattern
             nextPattern = nextPattern.check(child, descend)
-            _nodelog node, "child result of", child, "was", nextPattern
+            _nodelog node, "child result of", child, "against", curPattern, "was", nextPattern
             if nextPattern instanceof NotAllowed
               return nextPattern
         # TODO: Detect missing elements
-        if nextPattern.nullable()
-          return new GoodElement(@name, @pattern)
+        if nextPattern.require() instanceof Empty
+          return new GoodElement(@name, nextPattern)
         else
-          return nextPattern
+          return nextPattern.require()
       else
         emptyPattern = new Empty("not descending into #{h.getLocalName(node)}", @pattern, node)
         return new GoodElement(emptyPattern, node)


Remove deprecated calls to editor spec

Remove deprecated calls to editor spec

--- spec/editor-spec.coffee
+++ spec/editor-spec.coffee
@@ -7,9 +7,12 @@
     buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t"))

   beforeEach ->
-    editor = atom.project.openSync('sample.js', autoIndent: false)
-    buffer = editor.buffer
-    lineLengths = buffer.getLines().map (line) -> line.length
+    waitsForPromise ->
+      atom.project.open('sample.js', autoIndent: false).then (o) -> editor = o
+
+    runs ->
+      buffer = editor.buffer
+      lineLengths = buffer.getLines().map (line) -> line.length

     waitsForPromise ->
       atom.packages.activatePackage('language-javascript')
@@ -17,7 +20,7 @@
   describe "when the editor is deserialized", ->
     it "restores selections and folds based on markers in the buffer", ->
       editor.setSelectedBufferRange([[1, 2], [3, 4]])
-      editor.addSelectionForBufferRange([[5, 6], [7, 5]], isReversed: true)
+      editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true)
       editor.foldBufferRow(4)
       expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()

@@ -31,15 +34,20 @@
       editor2.destroy()

   describe "when the editor is constructed with an initialLine option", ->
-    it "and positions the cursor on the specified line", ->
-      editor = atom.project.openSync('sample.js', initialLine: 2)
-      buffer = editor.buffer
-      expect(editor.getCursor().getBufferPosition().row).toEqual 2
+    it "positions the cursor on the specified line", ->
+      editor = null
+
+      waitsForPromise ->
+        atom.workspace.open('sample.less', initialLine: 5).then (o) -> editor = o
+
+      runs ->
+        buffer = editor.buffer
+        expect(editor.getCursor().getBufferPosition().row).toEqual 5

   describe ".copy()", ->
     it "returns a different edit session with the same initial state", ->
       editor.setSelectedBufferRange([[1, 2], [3, 4]])
-      editor.addSelectionForBufferRange([[5, 6], [7, 8]], isReversed: true)
+      editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true)
       editor.foldBufferRow(4)
       expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()

@@ -57,21 +65,31 @@

   describe "config defaults", ->
     it "uses the `editor.tabLength`, `editor.softWrap`, and `editor.softTabs` config values", ->
+      editor1 = null
+      editor2 = null
       atom.config.set('editor.tabLength', 4)
       atom.config.set('editor.softWrap', true)
       atom.config.set('editor.softTabs', false)
-      editor1 = atom.project.openSync('a')
-      expect(editor1.getTabLength()).toBe 4
-      expect(editor1.getSoftWrap()).toBe true
-      expect(editor1.getSoftTabs()).toBe false
-
-      atom.config.set('editor.tabLength', 100)
-      atom.config.set('editor.softWrap', false)
-      atom.config.set('editor.softTabs', true)
-      editor2 = atom.project.openSync('b')
-      expect(editor2.getTabLength()).toBe 100
-      expect(editor2.getSoftWrap()).toBe false
-      expect(editor2.getSoftTabs()).toBe true
+
+      waitsForPromise ->
+        atom.workspace.open('a').then (o) -> editor1 = o
+
+      runs ->
+        expect(editor1.getTabLength()).toBe 4
+        expect(editor1.getSoftWrap()).toBe true
+        expect(editor1.getSoftTabs()).toBe false
+
+        atom.config.set('editor.tabLength', 100)
+        atom.config.set('editor.softWrap', false)
+        atom.config.set('editor.softTabs', true)
+
+      waitsForPromise ->
+        atom.workspace.open('b').then (o) -> editor2 = o
+
+      runs ->
+        expect(editor2.getTabLength()).toBe 100
+        expect(editor2.getSoftWrap()).toBe false
+        expect(editor2.getSoftTabs()).toBe true

   describe "title", ->
     describe ".getTitle()", ->
@@ -683,7 +701,7 @@
         expect(selection1.isReversed()).toBeFalsy()

       it "merges selections when they intersect when moving up", ->
-        editor.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]]], isReversed: true)
+        editor.setSelectedBufferRanges([[[0,9], [0,13]], [[1,10], [1,20]]], reversed: true)
         [selection1, selection2] = editor.getSelections()

         editor.selectUp()
@@ -694,7 +712,7 @@
         expect(selection1.isReversed()).toBeTruthy()

       it "merges selections when they intersect when moving left", ->
-        editor.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]], isReversed: true)
+        editor.setSelectedBufferRanges([[[0,9], [0,13]], [[0,14], [1,20]]], reversed: true)
         [selection1, selection2] = editor.getSelections()

         editor.selectLeft()
@@ -1245,10 +1263,14 @@
         expect(selection.isEmpty()).toBeTruthy()

     it "does not share selections between different edit sessions for the same buffer", ->
-      editor2 = atom.project.openSync('sample.js')
-      editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
-      editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]])
-      expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges()
+      editor2 = null
+      waitsForPromise ->
+        atom.project.open('sample.js').then (o) -> editor2 = o
+
+      runs ->
+        editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
+        editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]])
+        expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges()

   describe "buffer manipulation", ->
     describe ".insertText(text)", ->
@@ -2131,10 +2153,14 @@

       it "does not explode if the current language mode has no comment regex", ->
         editor.destroy()
-        editor = atom.project.openSync(null, autoIndent: false)
-        editor.setSelectedBufferRange([[4, 5], [4, 5]])
-        editor.toggleLineCommentsInSelection()
-        expect(buffer.lineForRow(4)).toBe "    while(items.length > 0) {"
+
+        waitsForPromise ->
+          atom.workspace.open(null, autoIndent: false).then (o) -> editor = o
+
+        runs ->
+          editor.setSelectedBufferRange([[4, 5], [4, 5]])
+          editor.toggleLineCommentsInSelection()
+          expect(buffer.lineForRow(4)).toBe "    while(items.length > 0) {"

       it "uncomments when the line lacks the trailing whitespace in the comment regex", ->
         editor.setCursorBufferPosition([10, 0])
@@ -2453,14 +2479,17 @@

   describe "soft-tabs detection", ->
     it "assigns soft / hard tabs based on the contents of the buffer, or uses the default if unknown", ->
-      editor = atom.project.openSync('sample.js', softTabs: false)
-      expect(editor.getSoftTabs()).toBeTruthy()
-
-      editor = atom.project.openSync('sample-with-tabs.coffee', softTabs: true)
-      expect(editor.getSoftTabs()).toBeFalsy()
-
-      editor = atom.project.openSync(null, softTabs: false)
-      expect(editor.getSoftTabs()).toBeFalsy()
+      waitsForPromise ->
+        atom.workspace.open('sample.js', softTabs: false).then (editor) ->
+          expect(editor.getSoftTabs()).toBeTruthy()
+
+      waitsForPromise ->
+        atom.workspace.open('sample-with-tabs.coffee', softTabs: true).then (editor) ->
+          expect(editor.getSoftTabs()).toBeFalsy()
+
+      waitsForPromise ->
+        atom.workspace.open(null, softTabs: false).then (editor) ->
+          expect(editor.getSoftTabs()).toBeFalsy()

   describe ".indentLevelForLine(line)", ->
     it "returns the indent level when the line has only leading whitespace", ->
@@ -2484,16 +2513,21 @@

   describe "when a better-matched grammar is added to syntax", ->
     it "switches to the better-matched grammar and re-tokenizes the buffer", ->
+      editor.destroy()
+
       jsGrammar = atom.syntax.selectGrammar('a.js')
       atom.syntax.removeGrammar(jsGrammar)

-      editor2 = atom.project.openSync('sample.js', autoIndent: false)
-      expect(editor2.getGrammar()).toBe atom.syntax.nullGrammar
-      expect(editor2.lineForScreenRow(0).tokens.length).toBe 1
-
-      atom.syntax.addGrammar(jsGrammar)
-      expect(editor2.getGrammar()).toBe jsGrammar
-      expect(editor2.lineForScreenRow(0).tokens.length).toBeGreaterThan 1
+      waitsForPromise ->
+        atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o
+
+      runs ->
+        expect(editor.getGrammar()).toBe atom.syntax.nullGrammar
+        expect(editor.lineForScreenRow(0).tokens.length).toBe 1
+
+        atom.syntax.addGrammar(jsGrammar)
+        expect(editor.getGrammar()).toBe jsGrammar
+        expect(editor.lineForScreenRow(0).tokens.length).toBeGreaterThan 1

   describe "auto-indent", ->
     copyText = (text, {startColumn}={}) ->
@@ -2780,10 +2814,15 @@
       expect(editor.shouldPromptToSave()).toBeFalsy()
       buffer.setText('changed')
       expect(editor.shouldPromptToSave()).toBeTruthy()
-      editor2 = atom.project.openSync('sample.js', autoIndent: false)
-      expect(editor.shouldPromptToSave()).toBeFalsy()
-      editor2.destroy()
-      expect(editor.shouldPromptToSave()).toBeTruthy()
+
+      editor2 = null
+      waitsForPromise ->
+        atom.project.open('sample.js', autoIndent: false).then (o) -> editor2 = o
+
+      runs ->
+        expect(editor.shouldPromptToSave()).toBeFalsy()
+        editor2.destroy()
+        expect(editor.shouldPromptToSave()).toBeTruthy()

   describe "when the edit session contains surrogate pair characters", ->
     it "correctly backspaces over them", ->
@@ -2879,12 +2918,15 @@

     describe "when the grammar is added", ->
       it "retokenizes existing buffers that contain tokens that match the injection selector", ->
-        editor = atom.project.openSync('sample.js')
-        editor.setText("// http://github.com")
-
-        {tokens} = editor.lineForScreenRow(0)
-        expect(tokens[1].value).toBe " http://github.com"
-        expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
+        waitsForPromise ->
+          atom.workspace.open('sample.js').then (o) -> editor = o
+
+        runs ->
+          editor.setText("// http://github.com")
+
+          {tokens} = editor.lineForScreenRow(0)
+          expect(tokens[1].value).toBe " http://github.com"
+          expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]

         waitsForPromise ->
           atom.packages.activatePackage('language-hyperlink')
@@ -2896,12 +2938,15 @@

       describe "when the grammar is updated", ->
         it "retokenizes existing buffers that contain tokens that match the injection selector", ->
-          editor = atom.project.openSync('sample.js')
-          editor.setText("// SELECT * FROM OCTOCATS")
-
-          {tokens} = editor.lineForScreenRow(0)
-          expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
-          expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
+          waitsForPromise ->
+            atom.workspace.open('sample.js').then (o) -> editor = o
+
+          runs ->
+            editor.setText("// SELECT * FROM OCTOCATS")
+
+            {tokens} = editor.lineForScreenRow(0)
+            expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
+            expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]

           waitsForPromise ->
             atom.packages.activatePackage('package-with-injection-selector')


Prepare BuildView for ember-cli

Prepare BuildView for ember-cli

--- assets/scripts/app/views/build.coffee
+++ assets/scripts/app/views/build.coffee
@@ -1,9 +1,14 @@
-Travis.reopen
-  BuildView: Travis.View.extend
+require 'helpers/helpers'
+
+colorForState = Travis.Helpers.colorForState
+
+View = Travis.View.extend
     classNameBindings: ['color', 'loading']
     buildBinding: 'controller.build'
     loadingBinding: 'controller.loading'

     color: (->
-      Travis.Helpers.colorForState(@get('build.state'))
+      colorForState(@get('build.state'))
     ).property('build.state')
+
+Travis.BuildView = View


:racehorse: Set spec directory once

:racehorse: Set spec directory once

Prevents an IPC call on each beforeEach getting the load settings

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -31,11 +31,12 @@
 jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions
 jasmine.getEnv().defaultTimeoutInterval = 5000

+specDirectory = atom.getLoadSettings().specDirectory ? __dirname
+specProjectPath = path.join(specDirectory, 'fixtures')
+
 beforeEach ->
   $.fx.off = true
-
-  specDirectory = atom.getLoadSettings().specDirectory ? __dirname
-  window.project = new Project(path.join(specDirectory, 'fixtures'))
+  window.project = new Project(specProjectPath)

   window.resetTimeouts()
   atom.packageStates = {}


Add the logger facility and explicitly add several throw points

--- bokehjs/src/coffee/models/transforms/interpolator.coffee
+++ bokehjs/src/coffee/models/transforms/interpolator.coffee
@@ -1,6 +1,7 @@
 _ = require "underscore"
 Transform = require "./transform"
 p = require "../../core/properties"
+{logger} = require "../../core/logging"

 class Interpolator extends Transform.Model

@@ -22,12 +23,12 @@
   sort: (descending = false) ->
     # Verify that all necessary objects exist...
     if typeof(@get('x')) != typeof(@get('y'))
-      # ToDo: Throw a resonable error here
+      throw Error('The parameters for x and y must be of the same type, either both strings which define a column in the data source or both arrays of the same length')
       return

     if typeof(@get('x')) == 'object'
       if @get('x').length != @get('y').length
-        # ToDo: Throw a resonable error here
+        throw Error('The arrays for x and y must be the same length')
         return
     else
       if @get('x').length == 0
@@ -37,7 +38,7 @@
         return

       if @get('data') == null
-        # ToDo: Throw a resonable error here.
+        throw Error('If the x and y parameters are not specified as an array, the data parameter is reqired.')
         return

     # Stop processing this if the dirty flag is not set


reactivity: chat input: search button handler

--- client/activity/lib/components/chatinputwidget/index.coffee
+++ client/activity/lib/components/chatinputwidget/index.coffee
@@ -180,6 +180,18 @@

   handleSearchButtonClick: (event) ->

+    searchMarker = '/s '
+    { value }    = @state
+
+    if value.indexOf(searchMarker) is -1
+      value = searchMarker + value
+      @setState { value }
+
+    textInput = React.findDOMNode this.refs.textInput
+    textInput.focus()
+
+    @refs.searchDropup.checkTextForQuery { value }
+

   renderEmojiDropup: ->


flux/env: fix acceptInvitation handler in store

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/flux/environment/stores/machinesstore.coffee
+++ client/app/lib/flux/environment/stores/machinesstore.coffee
@@ -2,6 +2,7 @@
 toImmutable          = require 'app/util/toImmutable'
 immutable            = require 'immutable'
 actions              = require '../actiontypes'
+globals              = require 'globals'

 module.exports = class MachinesStore extends KodingFluxStore

@@ -66,7 +67,15 @@

   acceptInvitation: (machines, id ) ->

-    machines.setIn [id, 'isApproved'], yes
+    machine = machines.get id
+
+    machine = machine.update 'users', (users) ->
+      users.map (user) ->
+        if user.get('id') is globals.userId
+        then user.set 'approved', yes
+        else user
+
+    machines.set id, machine

   rejectInvitation: (machines, id) ->


payment: add boilerplate for removePaymentMethod

--- client/landing/app/MainApp/payment/paymentcontroller.coffee
+++ client/landing/app/MainApp/payment/paymentcontroller.coffee
@@ -4,7 +4,7 @@
     KD.getSingleton('groupsController').getCurrentGroup()

   getBalance: (type, callback)->
-
+
     { JPaymentPlan } = KD.remote.api

     if type is 'user'
@@ -23,6 +23,10 @@
       else
         JPaymentPlan.setUserAccount newData, callback

+
+  removePaymentMethod: (accountCode) ->
+    { JPayment } = KD.remote.api
+    JPayment.removePaymentMethod accountCode

   getSubscription: do ->
     findActiveSubscription = (subs, planCode, callback) ->
@@ -96,7 +100,7 @@
     return @modal

   fetchBillingInfo: (type, callback) ->
-
+
     { JPaymentPlan } = KD.remote.api

     switch type
@@ -106,11 +110,10 @@
         JPaymentPlan.fetchAccountDetails callback

   updateBillingInfo: (billingInfo, callback) ->
-
+
     { JPayment } = KD.remote.api

     JPayment.setBillingInfo billingInfo, (err, result)->
-      debugger

   createBillingInfoModal:(type, billingInfo) ->
@@ -130,7 +133,7 @@
       return @utils.defer => callback null, @countries, @countryOfIp

     ip = $.cookie 'clientIPAddress'
-
+
     JPayment.fetchCountryDataByIp ip, (err, @countries, @countryOfIp) =>
       callback err, @countries, @countryOfIp


:bug: fix sending analytics when no ids are present (#1605)

--- src/scripts/helpers/handlers/analytics.coffee
+++ src/scripts/helpers/handlers/analytics.coffee
@@ -16,6 +16,10 @@
     # Send the current page to every analytics service
     sendAnalytics: (accounts, fragment = Backbone.history.fragment) ->

+      # Use the default analytics ID in settings if no account is specified
+      unless accounts
+        accounts = [settings.analyticsID]
+
       # temporarily support a single field for analytics (sometimes it is `null`)
       unless Array.isArray(accounts)
         console.log 'Consider calling analytics.sendAnalytics with an Array'
@@ -25,8 +29,6 @@

         accounts.forEach (account) =>
           if not /^\//.test(fragment) then fragment = '/' + fragment
-          # Use the default analytics ID in settings if no account is specified
-          account ?= settings.analyticsID

           # TODO investigate if we need specific names for our tracker name
           trackerName = @getTrackerName(account)


messaging

messaging

--- scripts/heroku-deploy.coffee
+++ scripts/heroku-deploy.coffee
@@ -97,7 +97,8 @@
   for appName, appData of VALID_APPS
     do (appName, appData) ->
       list += "* ** #{appName}** deploys #{appData.defaultBlobRef} to staging"
-      list += " and supports promoting to production using pipelines" if appData.pipelineId?
+      list += " and supports promoting to production using pipelines" if appData.pipelineId?
+      list += " and must be pushed directly to production" if !appData.pipelineId?
       list += "\n"

   list


update

update

--- scripts/chart.coffee
+++ scripts/chart.coffee
@@ -6,7 +6,7 @@
 #   "phantomjs": "^1.9.12"
 #
 # Commands:
-#   hubot chart <type> <data> -- Generate a chart image
+#   hubot chart <type> <data> -- Generate a chart image (cf. Chart.js)
 #
 # Author:
 #   yulii


req flash

req flash

--- src/controllers/tokens.coffee
+++ src/controllers/tokens.coffee
@@ -24,19 +24,19 @@
         html: ()->
           User.findOne {email: req.body.email}, (err, user)->
             if user
-              if user.password_valid(req.body.password)
+              if user.password_correct(req.body.password)
                 req.session.user_id = user._id
                 req.logged_in = true
-                res.flash "flash", "welcome"
+                req.flash "flash", "welcome"
                 res.redirect "/home"

               else
                 res.statusCode = 401
-                res.flash "flash", "password not correct"
+                req.flash "flash", "password not correct"
                 res.redirect "tokens/new"
             else
               res.statusCode = 404
-              res.flash "flash", "User not found"
+              req.flash "flash", "User not found"
               res.redirect "tokens/new"
       )


added hearts back in

added hearts back in

--- scripts/karma.coffee
+++ scripts/karma.coffee
@@ -120,4 +120,4 @@
     subject = msg.match[2].toLowerCase()
     outputSubject = subject.charAt(0).toUpperCase() + subject.slice(1)
     karma.doubleIncrement subject
-    msg.send "#{msg.message.user.name} and #{outputSubject} sitting in a tree... (Karma: #{karma.get(subject)})"+    msg.send "#{karma.doubleIncrementResponse()} #{outputSubject} #{karma.doubleIncrementResponse()} (Karma: #{karma.get(subject)})"


Kenny everywhere..

Kenny everywhere..

@beefknuckleback is master computist

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -90,6 +90,11 @@
   if /arbo/i.test(name)
     # Bouncers are hard or something, whatever.
     name = 'arbo'
+
+  if name == 'sflios'
+    # He visited this one time
+    # http://pixxx.wtf.cat/image/0Q3B34330v0c/12188116_10205398142892807_4244989619067012464_o.jpg
+    name = 'solios'

   return name


bug : was not delaying properly

bug : was not delaying properly

--- scripts/neige.coffee
+++ scripts/neige.coffee
@@ -32,10 +32,14 @@
         messages.push "En bas: #{onSlope.surfaceBottom}, #{onSlope.lowerDepth} cm"
         messages.push "En haut: #{onSlope.surfaceTop}, #{onSlope.upperDepth} cm"

+        delay = (s, i) ->
+          setTimeout () ->
+            console.log(s)
+          , 100 * i
+
         for s, i in messages
-          setTimeout () ->
-            msg.send s
-          , 10 * i
+          delay s, i
+
   #
   #
   # robot.router.post '/hubot/chatsecrets/:room', (req, res) ->


fixed console debug in circles example

fixed console debug in circles example

--- example/circles.coffee
+++ example/circles.coffee
@@ -70,11 +70,11 @@
         $('body').append el

     $('#start').live 'click', ->
-        console.log "animation paused."
+        console.log "animation resumed."
         running = yes

     $('#stop').live 'click', ->
-        console.log "animation resumed."
+        console.log "animation paused."
         running = no

 console.log 'coffeescript loaded.'


more label tests

more label tests

--- test/arguejs.coffee
+++ test/arguejs.coffee
@@ -58,7 +58,7 @@
             symmetric = new Arguejs.ArgumentFramework { '0' : ['1'], '1' : ['0'] }
             # catch labelling with missing arguments
             symmetric.isCompleteLabelling(new Arguejs.Labelling(['0'],[],[])).should.be.false
-            # expected labellings
+            # check expected labellings
             symmetric.isCompleteLabelling(new Arguejs.Labelling(['0'],['1'],[])).should.be.true
             symmetric.isCompleteLabelling(new Arguejs.Labelling(['1'],['0'],[])).should.be.true
             symmetric.isCompleteLabelling(new Arguejs.Labelling([],[],['0','1'])).should.be.true
@@ -67,14 +67,28 @@
             basic = new Arguejs.ArgumentFramework { '0' : ['1'], '1' : ['2'], '2' : [] }
             # check that the label undec doesnt serve as a wildcard
             basic.isCompleteLabelling(new Arguejs.Labelling([],[],['0','1','2'])).should.be.false
-            # expected labelling
+            # check expected labelling
             basic.isCompleteLabelling(new Arguejs.Labelling(['0','2'],['1'],[])).should.be.true
             done()
-    describe 'Generation', ->
+    describe.skip 'Generation', ->
+        it 'basic', (done) ->
+            af = new Arguejs.ArgumentFramework { 'A' : [], 'B' : ['A'] }
+            labellings = af.completeLabellings()
+            labellings.should.have.length 1
+            labellings.should.include new Arguejs.Labelling(['A'],['B'],[])
+            done()
+        it 'symmetric defeat', (done) ->
+            af = new Arguejs.ArgumentFramework { 'A' : ['B'], 'B' : ['A'] }
+            labellings = af.completeLabellings()
+            labellings.should.have.length 3
+            labellings.should.include new Arguejs.Labelling(['A'],['B'],[])
+            labellings.should.include new Arguejs.Labelling(['B'],['A'],[])
+            labellings.should.include new Arguejs.Labelling([],[],['A','B'])
+            done()
         it 'canonical', (done) ->
             af = new Arguejs.ArgumentFramework { 'A' : [], 'B' : ['A'], 'C' : ['B','D'], 'D' : ['C'], 'E': ['D'] }
             labellings = af.completeLabellings()
             labellings.should.have.length 2
-            #labellings.should.include new Arguejs.Labelling(['A'],['B'],['C','D','E'])
-            #labellings.should.include new Arguejs.Labelling(['A','C','E'],['B','D'],[])
+            labellings.should.include new Arguejs.Labelling(['A'],['B'],['C','D','E'])
+            labellings.should.include new Arguejs.Labelling(['A','C','E'],['B','D'],[])
             done()


Quick-n-dirty conditional minification

Quick-n-dirty conditional minification

--- docpad.coffee
+++ docpad.coffee
@@ -27,3 +27,13 @@
   less:
     parseOptions:
       paths: ["#{__dirname}/node_modules"]
+  webpack:
+    optimize:
+      minimize: true
+
+@environments =
+  development:
+    plugins:
+      webpack:
+        optimize:
+          minimize: false


Adjust docpad queries for windoze paths. Thanks @SeiferTim.

--- docpad.coffee
+++ docpad.coffee
@@ -132,7 +132,7 @@
 		getting_started: (database) ->
 			query =
 				write: true
-				relativeOutDirPath: $startsWith: 'documentation/documentation/00_getting_started'
+				relativeOutDirPath: $endsWith: '00_getting_started'
 				body: $ne: ""
 			sorting = [categoryDirectory:1, filename:1]

@@ -155,7 +155,7 @@
 		community: (database) ->
 			query =
 				write: true
-				relativeOutDirPath: $startsWith: 'documentation/documentation/01_community'
+				relativeOutDirPath: $endsWith: '01_community'
 				body: $ne: ""
 			sorting = [categoryDirectory:1, filename:1]

@@ -178,7 +178,7 @@
 		handbook: (database) ->
 			query =
 				write: true
-				relativeOutDirPath: $startsWith: 'documentation/documentation/02_handbook'
+				relativeOutDirPath: $endsWith: '02_handbook'
 				body: $ne: ""
 			sorting = [categoryDirectory:1, filename:1]

@@ -201,7 +201,7 @@
 		resources: (database) ->
 			query =
 				write: true
-				relativeOutDirPath: $startsWith: 'documentation/documentation/03_resources'
+				relativeOutDirPath: $endsWith: '03_resources'
 				body: $ne: ""
 			sorting = [categoryDirectory:1, filename:1]


simple tests around hasArg method

simple tests around hasArg method

--- test/decking.coffee
+++ test/decking.coffee
@@ -109,3 +109,23 @@

       it "throws the expected error", ->
         expect(@e.message).to.eql "ENOENT, no such file or directory './decking.json'"
+
+  describe "hasArg", ->
+    beforeEach ->
+      @decking = new Decking
+
+    describe "When the argument exists", ->
+      beforeEach ->
+        @decking.args = ["foo", "myarg"]
+        @result = @decking.hasArg "myarg"
+
+      it "returns true", ->
+        expect(@result).to.be.true
+
+    describe "When the argument does not exist", ->
+      beforeEach ->
+        @decking.args = ["foo", "myarg"]
+        @result = @decking.hasArg "somearg"
+
+      it "returns true", ->
+        expect(@result).to.be.false


Fixing newline at end of file.

Fixing newline at end of file.

--- test/js-test.coffee
+++ test/js-test.coffee
@@ -20,4 +20,4 @@

     Then -> expect(@result).to.contain("Another great test")
     And -> expect(@result).not.to.contain("this is a comment")
-    And -> expect(@result).not.to.contain("and is this")+    And -> expect(@result).not.to.contain("and is this")


Set TODO for checking return value of sendx

--- src/util/world.coffee
+++ src/util/world.coffee
@@ -74,6 +74,7 @@
     handlers = @handlers_for(key)
     handlers.push callback

+  # TODO: Figure out who is expecting a return value from send
   send: (key, args, callback) ->
     for handler in @handlers_for(key)
       console.log "send: #{key}, #{args}} to handler", handler


updates for percent_played typo

updates for percent_played typo

--- src/videojs.ga.coffee
+++ src/videojs.ga.coffee
@@ -128,7 +128,7 @@
       for percent in [0..99] by percentsPlayedInterval
         if percentPlayed >= percent && percent not in percentsAlreadyTracked

-          if "percentsPlayed" in eventsToTrack && percentPlayed != 0
+          if "percent_played" in eventsToTrack && percentPlayed != 0
             sendbeacon( getEventName('percent_played'), true, percent )

           if percentPlayed > 0


re-enable account deletion, with an additional warning

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -674,11 +674,12 @@
             return
         <ErrorDisplay error={@props.account_deletion_error} />

-    # Change this line to `render: ->` and delete the other render function below to reenable automatic account deletion
-    render_original: ->
+    render: ->
         <Well style={marginTop: '26px', textAlign:'center', fontSize: '15pt', backgroundColor: 'darkred', color: 'white'}>
             Are you sure you want to DELETE YOUR ACCOUNT?

             You will immediately lose access to all of your projects, and any subscriptions will be canceled.

+

+            Do NOT delete your account if you are a current student in a course on CoCalc! Why?

             To DELETE YOUR ACCOUNT, enter your first and last name below.
             <FormGroup>
@@ -710,8 +711,8 @@
             {@render_error()}
         </Well>

-    # Remove this function and rename the function above to reenable account deletion
-    render: ->
+    # Make this the render function to disable account deletion
+    xxx_render: ->
         <Well  style={marginTop: '26px', textAlign:'center', fontSize: '12pt'}>
             To delete your account, contact us at help@sagemath.com{" "}
             or open a support request by clicking "Help" in the top right menu.


fix typo

fix typo

--- client/app/models/event.coffee
+++ client/app/models/event.coffee
@@ -313,7 +313,7 @@
         return @get 'attendees'

-    # Prepare data brefore sync
+    # Prepare data before sync.
     prepare: ->
         # Remove trailing slash from any cozy url in attendees
         attendees = @get('attendees') or []


include config with api

include config with api

--- client/lanes/remote/api.coffee
+++ client/lanes/remote/api.coffee
@@ -1,8 +1,9 @@
 ##=require ../vendor/standalone
 ##=require ../lib/all
 ##=require ../models
+##=require ../Config
 ##=require ../extension
 ##=require ../react
 ##=require_self

-Lanes.config = {api_path: null}
+# Lanes.config = {api_path: null}


Sinon server path assertion helper

Sinon server path assertion helper

--- client/test/src/helpers.coffee
+++ client/test/src/helpers.coffee
@@ -5,13 +5,6 @@
   $('#test-container').html(view.el)

 window.Helpers.SinonServer ||= {}
-
-Helpers.assertCalledOnce = (sinonObj) ->
-  assert.ok(
-    sinonObj.calledOnce,
-    "Expected spy/stub to be called once but was called
-      #{sinonObj.callCount} times"
-  )

 Helpers.viewHasSubViewOfClass = (view, subViewClassName) ->
   subViewExists = false
@@ -27,6 +20,25 @@
     { "Content-Type": "application/json" },
     JSON.stringify(jsonData)
   )
+
+Helpers.Assertions ||= {}
+
+Helpers.assertCalledOnce = (sinonObj) ->
+  assert.ok(
+    sinonObj.calledOnce,
+    "Expected spy/stub to be called once but was called
+      #{sinonObj.callCount} times"
+  )
+
+Helpers.Assertions.assertPathVisited = (server, path) ->
+  requestedUrls = _.map(server.requests, (req)-> req.url)
+  matchCount = 0
+  for url in requestedUrls
+    if path.test url
+      matchCount++
+
+  assert matchCount > 0,
+    "Expected at least one request to #{path}"

 window.Factory ||= {}


Force-store outcome if download fails

Force-store outcome if download fails

--- server/api/setOutcome.coffee
+++ server/api/setOutcome.coffee
@@ -66,8 +66,13 @@
     await User.updateUser(submit.user)

 export default setOutcome = (req, res) ->
-    await postToInformatics(req, res)
-    success = await updateData(req, res)
-    if not success and req.body.result
+    success = false
+    try
+        await postToInformatics(req, res)
+        success = await updateData(req, res)
+    catch e
+        logger.info "Can't update informatics status " + href + " " + i + " will re-download"
+        logger.info e.message
+    if not success and (req.body.result or req.body.comment)
         await storeToDatabase(req, res)
     res.send('OK')


Fixed force-storing submit result

Fixed force-storing submit result

--- server/api/setOutcome.coffee
+++ server/api/setOutcome.coffee
@@ -44,9 +44,12 @@

 storeToDatabase = (req, res) ->
     logger.info("Force-storing to database result #{req.params.submitId}")
-    [fullSubmitId, runId, problemId] = req.params.submitId.match(/(\d+r\d+)p(\d+)/)
+    [fullSubmitId, runId, problemId] = req.params.submitId.match(/(\d+r\d+)(p\d+)/)
     submit = await Submit.findById(req.params.submitId)
     problem = await Problem.findById(problemId)
+    if req.body.result in ["AC", "IG", "DQ"]
+        submit.outcome = req.body.result
+        submit.force = true
     if req.body.comment
         rndId = Math.floor(Math.random() * 1000000)
         newComment = new SubmitComment
@@ -56,12 +59,9 @@
             userId: submit.user
             text: req.body.comment
             time: new Date()
-            outcome: outcome
+            outcome: submit.outcome
         await newComment.upsert()
         submit.comments.push(req.body.comment)
-    if req.body.result in ["AC", "IG"]
-        submit.outcome = req.body.result
-        submit.force = true
     await submit.upsert()
     await User.updateUser(submit.user)


Go straight into the game, until level selection finished.

--- src/game/states/script.coffee
+++ src/game/states/script.coffee
@@ -101,7 +101,9 @@
       @nextDialog()
       @index++
     else
-      @game.state.start 'next_constellation'
+      # TODO enable game level selection
+      # @game.state.start 'next_constellation'
+      @game.state.start 'game'

   nextDialog:  ->
     console.log(@index)


add key bindings

add key bindings

--- client/components/draft_input/draft_input_caption.coffee
+++ client/components/draft_input/draft_input_caption.coffee
@@ -72,30 +72,35 @@

   onPaste: (text, html) ->
     { editorState } = @state
+    unless html
+      html = '<div>' + text + '</div>'
     html = convertFromHTML(html)
     convertedHtml = html.contentBlocks.map (contentBlock) =>
       unstyled = @stripPastedStyles contentBlock
       if unstyled.getType() isnt 'unstyled' or 'LINK'
         unstyled = unstyled.set('type', 'unstyled')
       return unstyled
+    blockMap = ContentState.createFromBlockArray(convertedHtml, html.entityMap).blockMap
     Modifier.removeInlineStyle(editorState.getCurrentContent(), editorState.getSelection(), 'font-weight')
-    @onChange(EditorState.push(editorState, ContentState.createFromBlockArray(convertedHtml), 'insert-fragment'))
+    newState = Modifier.replaceWithFragment(editorState.getCurrentContent(), editorState.getSelection(), blockMap)
+    this.onChange(EditorState.push(editorState, newState, 'insert-fragment'))
     return true

   stripPastedStyles: (contentBlock) ->
     characterList = contentBlock.getCharacterList().map (character) ->
-      if character.get('style') is 'CODE' or 'BOLD'
+      if character.get('style') isnt 'ITALIC'
         character.set('style', character.get('style').clear())
     unstyled = contentBlock.set('characterList', characterList)
     return unstyled

   promptForLink: (e) ->
-    e.preventDefault()
-    selection = @state.editorState.getSelection()
+    { editorState } = @state
+    e.preventDefault()
+    selection = editorState.getSelection()
     selectionTarget = null
     if !selection.isCollapsed()
       selectionTarget = @stickyLinkBox getVisibleSelectionRect(window)
-      contentState = @state.editorState.getCurrentContent()
+      contentState = editorState.getCurrentContent()
       startKey = selection.getStartKey()
       startOffset = selection.getStartOffset()
       blockWithLinkAtBeginning = contentState.getBlockForKey(startKey)
@@ -146,6 +151,24 @@
         editorState: RichUtils.toggleLink(editorState, selection, null)
       })

+  handleLinkReturn: (e) ->
+    e.preventDefault()
+    if e.key is 'Enter'
+      @confirmLink(e)
+
+  handleReturn: (e) ->
+    e.preventDefault()
+    return true
+
+  handleKeyCommand: (e) ->
+    if e is 'italic'
+      { editorState } = @state
+      newState = RichUtils.handleKeyCommand editorState, e
+      if newState
+        @onChange newState
+        return true
+    return false
+
   printUrlInput: ->
     if @state.showUrlInput
       div {
@@ -162,6 +185,7 @@
           onChange: @onURLChange
           className: 'bordered-input'
           placeholder: 'Paste or type a link'
+          onKeyPress: @handleLinkReturn
         }
         if @state.urlValue?.length
           button {
@@ -190,6 +214,8 @@
           spellCheck: true
           onChange: @onChange
           handlePastedText: @onPaste
+          handleReturn: @handleReturn
+          handleKeyCommand: @handleKeyCommand
         }
       div { className: 'draft-caption__actions'},
         button {


Fix reviewview resetdecoration

Fix reviewview resetdecoration

--- client/landing/app/CommonViews/reviews/reviewview.coffee
+++ client/landing/app/CommonViews/reviews/reviewview.coffee
@@ -6,13 +6,10 @@

     @setClass "review-container"
     @createSubViews data
-    # @resetDecoration()
     @decorateCommentedState()
     @attachListeners()

   render:->
-    # FIXME GG
-    # @resetDecoration()
     @decorateCommentedState()

   createSubViews:(data)->
@@ -56,18 +53,6 @@
     @reviewList.on "CommentCountClicked", =>
       @reviewList.emit "AllCommentsLinkWasClicked"

-    @listenTo
-      KDEventTypes : "CommentViewShouldReset"
-      listenedToInstance : @reviewList
-      callback : @resetDecoration
-
-  resetDecoration:->
-    post = @getData()
-    if post.repliesCount is 0
-      @decorateNoCommentState()
-    else
-      @decorateCommentedState()
-
   decorateNoCommentState:->
     @unsetClass "active-comment"
     @unsetClass "commented"


Do not show any notification comes from a guest

--- client/landing/app/MainApp/notificationcontroller.coffee
+++ client/landing/app/MainApp/notificationcontroller.coffee
@@ -118,6 +118,10 @@
       KD.remote.api[subject.constructorName][method] args, callback

     KD.remote.cacheable actor.constructorName, actor.id, (err, actorAccount)=>
+      # Ignore all guest notifications
+      # https://app.asana.com/0/1177356931469/7014047104322
+      return  if actorAccount.type is 'unregistered'
+
       fetchSubjectObj (err, subjectObj)=>
         actorName = KD.utils.getFullnameFromAccount actorAccount
         options.title = switch actionType


(fix) removed failing XCode version parse

--- src/steroids/Simulator.coffee
+++ src/steroids/Simulator.coffee
@@ -39,7 +39,6 @@

       xcodeVersionSession.on "exit", ->
         valid = xcodeVersionSession.stdout.match(minimumXcodeVersion)
-        version = xcodeVersionSession.stdout.split("\n").splice(0, 1)
         if valid
           resolve()
         else


Remove obsolete module require statement

--- client/app/lib/util/trackEvent.coffee
+++ client/app/lib/util/trackEvent.coffee
@@ -1,4 +1,3 @@
-kd = require 'kd'
 globals = require 'globals'
 isLoggedIn = require './isLoggedIn'


delete url fixed

delete url fixed

--- client/app/models/result_model.coffee
+++ client/app/models/result_model.coffee
@@ -2,7 +2,7 @@
 	rootUrl: "search"

 	methodUrl : {
-		'delete': '/delete'
+		'delete': 'delete'
 	}

 	sync : (method, model, options) ->


Extend length of time OutputView is displayed


Focus annotation when linking highlights

--- client/lib/annotator/annotator.coffee
+++ client/lib/annotator/annotator.coffee
@@ -132,9 +132,13 @@
       local: true
       'publication._id': Session.get 'currentPublicationId'
     ,
+      $set:
+        editing: true
       $addToSet:
         'references.highlights':
           _id: id
+
+    $('.annotation-content-editor').focus()

   updateLocation: =>
     # This is our annotations


Change auto-inspection formatting to .6f.

--- src/ext/modules/routines.coffee
+++ src/ext/modules/routines.coffee
@@ -80,9 +80,9 @@
   else
     number

-format4fi = (number) ->
+format6fi = (number) ->
   if number
-    number.toFixed(4).replace(/\.0+$/, '')
+    number.toFixed(6).replace(/\.0+$/, '')
   else
     number

@@ -129,7 +129,7 @@
   for element, i in source
     target[i] = if element?
       if isNumber element
-        format4fi element
+        format6fi element
       else
         element
     else
@@ -435,7 +435,7 @@

   inspectRawObject_ = (name, origin, description, obj) -> ->
     vectors = for k, v of obj
-      createList k, [ if v is null then undefined else if isNumber v then format4fi v else v ]
+      createList k, [ if v is null then undefined else if isNumber v then format6fi v else v ]

     createDataframe name, vectors, (sequence 1), null,
       description: description
@@ -561,7 +561,7 @@
             else
               console.log "WARNING: dropping [#{k}] from inspection:", v
           else
-            record[k] = if isNumber v then format4fi v else v
+            record[k] = if isNumber v then format6fi v else v

     return


DE2919: add tooltippy thing to custom roles YAY! I made a UI change without breaking stuff (I think)

--- sli/admin-tools/admin-rails/app/assets/javascripts/custom_roles.js.coffee
+++ sli/admin-tools/admin-rails/app/assets/javascripts/custom_roles.js.coffee
@@ -86,6 +86,7 @@
 createLabel = (type, name) ->
   label = $('#labelUi').clone()
   label.find("span").text(name)
+  label.find("span").prop("title", name)
   label.children().addClass(type)
   return label.children()


Update angular-daterangepicker.coffee

Update angular-daterangepicker.coffee

--- coffee/angular-daterangepicker.coffee
+++ coffee/angular-daterangepicker.coffee
@@ -113,10 +113,14 @@
     _init = ->
       # disable autoUpdateInput, can't handle empty values without it.  Our callback here will
       # update our $viewValue, which triggers the $parsers
-      el.daterangepicker angular.extend(opts, {autoUpdateInput: false}), (start, end) ->
+      el.daterangepicker angular.extend(opts, {autoUpdateInput: false})
+
+       el.on 'apply.daterangepicker', (ev, picker) ->
         $scope.$apply () ->
-          $scope.model = if opts.singleDatePicker then start else {startDate: start, endDate: end}

+          newStartDate = picker.startDate
+          newEndDate = picker.endDate
+          $scope.model = if opts.singleDatePicker then start else {startDate: newStartDate, endDate: newEndDate}
       # Needs to be after daterangerpicker has been created, otherwise
       # watchers that reinit will be attached to old daterangepicker instance.
       _picker = el.data('daterangepicker')


better association detection (1)

better association detection (1)

--- src/chinchilla/action_operation.coffee
+++ src/chinchilla/action_operation.coffee
@@ -93,7 +93,7 @@
         _.each object, (value, key) ->
           return if key == '$associations'

-          if _.isArray(value) || (_.isPlainObject(value) && value['@id'])
+          if (_.isArray(value) && _.isPlainObject(_.first(value))) || (_.isPlainObject(value) && value['@id'])
             object.$associations[key] = _.clone(value)
             delete object[key]