Add xhrFields support and beforeSend callbacks to ajax chosen

--- vendor/assets/javascripts/chosen/chosen.js.coffee
+++ vendor/assets/javascripts/chosen/chosen.js.coffee
@@ -358,6 +358,10 @@
       dataType: @ajax.dataType or "json"
       data: $.extend(@ajax.data || {}, data)
       async: @ajax.async or true
+      xhrFields: @ajax.xhrFields
+
+      beforeSend: (xhr) =>
+        @ajax.beforeSend(xhr) if typeof @ajax.beforeSend is "function"
       success: (data) =>
         @redraw_dropdown(data)


Minor style adjustments to componentsTagging

--- components.coffee
+++ components.coffee
@@ -689,16 +689,18 @@
                 style:
                     position: 'relative'
                     marginTop: if elemType == 'TableRow' then 0 else '1rem'
+                    padding: '1px'
                     border: '1px solid #ddd'
-                    borderRadius: '3px'
+                    borderRadius: '0 3px 3px'
                 'data-component': elemType,
                 $$ ('div'),
                     style:
                         position: 'absolute'
                         top: '-1rem'
+                        left: '-1px'
                         backgroundColor: 'aliceblue'
                         border: '1px solid #ddd'
-                        borderRadius: '3px'
+                        borderRadius: '3px 3px 0 0'
                         padding: '2px'
                         pointerEvents: 'none'
                         cursor: 'default'


changed @ to root

changed @ to root

--- web/scripts/codeland.coffee
+++ web/scripts/codeland.coffee
@@ -26,8 +26,8 @@

 root.startGame = (game) ->
     console.log("Starting #{game}")
-    if not @visualMaster
-        @visualMaster = {
+    if not root.visualMaster
+        root.visualMaster = {
             container: {
                 width: 360,
                 height: 360
@@ -43,8 +43,8 @@
                 vicflag: ["img/vicflag1.png","img/vicflag2.png"]
             }
         }
-        @visualFrameRate = 17
-    @currentGame.finishGame() if @currentGame
+        root.visualFrameRate = 17
+    root.currentGame.finishGame() if root.currentGame

     gamediv = $(root.UIcont)
     tmp1 = document.getElementById("gameSelection")
@@ -56,8 +56,8 @@
     env = {
         key: game
         description : description
-        visualMaster: @visualMaster
-        frameRate: @visualFrameRate
+        visualMaster: root.visualMaster
+        frameRate: root.visualFrameRate
         gamediv : gamediv
         player : root.getPlayer()
         codeland : this
@@ -65,9 +65,9 @@

     managerString  = description?.manager ?= 'GameManager'

-    @currentGame = new window[managerString](env)
-
-    @currentGame.startGame()
+    root.currentGame = new window[managerString](env)
+
+    root.currentGame.startGame()

 deepcopy = (src) -> $.extend(true, {},src)

@@ -104,8 +104,8 @@
     return getPlayer().currentGame

 root.getPlayer = ->
-    @currentPlayer ?= root.load("CurrentPlayer")
-    @currentPlayer ?= {
+    root.currentPlayer ?= root.load("CurrentPlayer")
+    root.currentPlayer ?= {
         id : +(new Date())
         currentGame : ''
         first : ''
@@ -156,25 +156,6 @@
                 dir: 1
                 index: 0
             }
-            # guard: {
-            #     index: 2
-            #     x: 3
-            #     y: 0
-            #     dir: 2
-            #     ai: {
-            #         move: (gamestate) =>
-            #             [newx, newy] = gameState.computeStepInDirection(
-            #                 @dir, @x, @y)
-            #             hitEvent = gamestate.checkEvent(@x, @y)
-            #             if !hitEvent
-            #                 @x = newx
-            #                 @y = newy
-            #                 gamestate.gameVisual.gridMove @index, 1
-            #             else
-            #                 @dir = @dir ^ 2
-            #             return
-            #     }
-            # }
         }
     }
     visual : {
@@ -262,24 +243,24 @@

 root.getGameDescriptions = ->
-    @gameDescriptions ?= {
+    root.gameDescriptions ?= {
         sequence1 : _sequence1
         sequence2 : _sequence2
         sequence3 : _sequence3
         sequence4 : _sequence4
     }
 root.getGameSequence = ->
-    return @gameSequence if @gameSequence
-    @gameSequence = []
+    return root.gameSequence if root.gameSequence
+    root.gameSequence = []
     games = root.getGameDescriptions()
     addGame = (name) =>
-        return if $.inArray(name, @gameSequence)!=-1
+        return if $.inArray(name, root.gameSequence)!=-1
         doFirst = games[name].depends ?= []
         addGame g for g in doFirst
-        @gameSequence.push name
+        root.gameSequence.push name
         return
     addGame(g) for g,ignore of games
-    return @gameSequence
+    return root.gameSequence

 root.canPlay = (game) ->


add random jitter to cutoff time

add random jitter to cutoff time

--- services/document-updater/app/coffee/DeleteQueueManager.coffee
+++ services/document-updater/app/coffee/DeleteQueueManager.coffee
@@ -23,7 +23,7 @@
 module.exports = DeleteQueueManager =
     flushAndDeleteOldProjects: (options, callback) ->
         startTime = Date.now()
-        cutoffTime = startTime - options.min_delete_age
+        cutoffTime = startTime - options.min_delete_age + 100 * (Math.random() - 0.5)
         count = 0

         flushProjectIfNotModified = (project_id, flushTimestamp, cb) ->


fixes #17 : Create the logs directory if not exists.

--- src/startup.coffee
+++ src/startup.coffee
@@ -27,6 +27,9 @@

 processes = []

+if not Fs.existsSync Config.Paths.Logs
+    Fs.mkdirSync Config.Paths.Logs, '0755'
+
 if Config.Voicious.Enabled
     voiciousAccessLog = Fs.openSync (Path.join Config.Paths.Logs, 'voicious.access.log'), 'a+'
     voiciousErrorLog  = Fs.openSync (Path.join Config.Paths.Logs, 'voicious.error.log'), 'a+'
@@ -50,7 +53,7 @@
         process.stderr.write "#{data}"
         WriteLog restErrorLog, data
     processes.push voicious
-
+
 if Config.Websocket.Enabled
     wsAccessLog = Fs.openSync (Path.join Config.Paths.Logs, 'ws.access.log'), 'a+'
     wsErrorLog  = Fs.openSync (Path.join Config.Paths.Logs, 'ws.error.log'), 'a+'


Removes trailing characters

Removes trailing characters

--- src/client/templates.coffee
+++ src/client/templates.coffee
@@ -1,8 +1,8 @@
-define ['lib/hullbase', 'handlebars'], (Hull, Handlebars) ->
+define ['underscore', 'lib/hullbase', 'handlebars'], (_, Hull, Handlebars) ->

   #Compiles the template depending on its definition
   setupTemplate = (tplSrc, tplName) ->
-    engine = module.templateEngine
+    engine = module.templateEngine
     tplName = tplName.replace(/\//g,'.',)
     if (_.isFunction(tplSrc))
       compiled = engine.template tplSrc
@@ -11,7 +11,7 @@
     engine.registerPartial(tplName, compiled)
     compiled

-  # Handles the various priorities and locations of templates
+  # Handles the various priorities and locations of templates
   _getTemplateDefinition = (name, ref, widgetName, dom)->
     path = "#{ref}/#{name}"
     tplName = [widgetName, name.replace(/^_/, '')].join("/")
@@ -33,12 +33,12 @@
     module.define path, parsed
     parsed

-  module =
+  module =
     global: window
     require: require
     define: define
     templateEngine: Handlebars
-    getTemplateDefinition: _getTemplateDefinition
+    getTemplateDefinition: _getTemplateDefinition
     initialize: (app) ->
       app.core.template.load = (names=[], ref, format="hbs") ->
         undefinedTemplates = []
@@ -50,7 +50,7 @@
           tplDef = _getTemplateDefinition name, ref, widgetName, app.core.dom.find
           if tplDef
             ret[name] = tplDef
-          else
+          else
             undefinedTemplates.push([name, "text!#{ref}/#{name}.#{format}"])
         if undefinedTemplates.length > 0
           module.require(_.map(undefinedTemplates, (p) -> p[1]), ->


send data after join and message

send data after join and message

--- server/app/actors/user-actor.coffee
+++ server/app/actors/user-actor.coffee
@@ -27,10 +27,10 @@
     @room = ev.data.room
     @nick = ev.data.nick
     unbind(ev.socket, 'join')
-    @manager.globalBus.push { type: "BROADCAST", room: ev.data.room, key: "user-joined", data: "User #{@nick} joined to room #{ev.data.room}" }
+    @manager.globalBus.push { type: "BROADCAST", room: ev.data.room, key: "user-joined", data: {user:{ nick: ev.data.nick, id: @id } } }

   handleSendMessage: (ev) ->
     debug("User #{@id} sent message #{ev.data.message}")
-    @manager.globalBus.push { type: "BROADCAST", room: @room, key: "message", data: { message: "User #{@nick} sent message #{ev.data.message}" } }
+    @manager.globalBus.push { type: "BROADCAST", room: @room, key: "message", data: { user: { nick: @nick, id: @id }, message: ev.data.message } }

 module.exports = UserActor


Method chaining for .inject and .modules

--- src/udefine.coffee
+++ src/udefine.coffee
@@ -13,6 +13,8 @@
 # Root object hook
 # Use udefine in the closure and then bind to the specific objects afterwards
 do (root = if hasModule then {} else this) ->
+  # TODO: Find a better wording than platform and platforms
+  platforms = ['commonjs', 'globals']
   platform = if hasModule then 'commonjs' else 'globals'

   # Helper function to resolve a module (either function or object)
@@ -71,10 +73,17 @@

   udefine.inject.modules = {}

-  udefine.inject.add = (name) -> udefine.inject.modules[name] = undefined
-  udefine.inject.remove = (name) -> delete udefine.inject.modules[name]
+  udefine.inject.add = (name) ->
+    udefine.inject.modules[name] = undefined
+    @
+
+  udefine.inject.remove = (name) ->
+    delete udefine.inject.modules[name]
+    @

-  udefine.inject.clear = -> udefine.inject.modules = {}
+  udefine.inject.clear = ->
+    udefine.inject.modules = {}
+    @

   # TODO: Reflect if these two object could and should be merged together
   # Dependencies for browser (global object)
@@ -89,15 +98,21 @@
     globals: {}
     commonjs: {}

-    add: (name) ->
-      if typeof name is 'object'
-      udefine.modules[platform][name] = undefined
+    add: (name, all) ->
+      if all
+        udefine.modules[p][name] for p in platforms
+      else
+        udefine.modules[platform][name]
+
+      @
     remove: (name) ->
       if Object.hasOwnProperty.call udefine.modules.globals, name
         delete udefine.modules['globals'][name]

       if Object.hasOwnProperty.call udefine.modules.commonjs, name
         delete udefine.modules['commonjs'][name]
+
+      @

     get: ->
     set: ->


Don't show conversation view toggle on child posts that are reposts

--- assets/javascripts/views/post/repost.js.coffee
+++ assets/javascripts/views/post/repost.js.coffee
@@ -10,6 +10,12 @@

   parentPost: =>
     TentStatus.Models.Post.instances.all[@parent_view.post_cid]
+
+  conversationView: =>
+    view = @
+    while view && view.constructor.view_name != 'conversation'
+      view = view.parent_view
+    view

   fetchPost: (parent_post = @parentPost()) =>
     TentStatus.Models.Post.find { id: parent_post.get('content.id'), entity: parent_post.get('content.entity') }, {
@@ -29,4 +35,5 @@
   context: (post) =>
     _.extend super, {
       has_parent: true
+      is_conversation_view: !!@conversationView()
     }


Add redirect rules for old science.evolvingteachers.com

--- site/server/rewrites.coffee
+++ site/server/rewrites.coffee
@@ -99,12 +99,18 @@
   # File attachments paths
   { from: '/file/:id/:filename', to: '../../:id/:filename' }
   { from: '/render/:site/file/:id/:filename', to: '../../:id/:filename' }
+
+  # Redirected old URLs using a pattern
+  movedPattern '/render/:site/posts/:id/:slug', '/:type/:slug'

   # Redirect some direct paths
   # moved '/posts/some-old-path', '/some-new-path'
-
-  # Redirected old URLs using a pattern
-  movedPattern '/render/:site/posts/:id/:slug', '/:type/:slug'
+
+  # For science.evolvingteachers.com old urls
+  moved '/snc4m1-curriculum-course-material/', '/essay/snc4m1-curriculum-course-material'
+  moved '/snc3m1-svn3m1-curriculum-course-material/', '/collection/grade-11-science'
+  moved '/snc2d1-snc2p1-curriculum-course-material/', '/collection/grade-10-science'
+  moved '/snc1d1-snc1p1-curriculum-course-material/', '/collection/grade-9-science'

   # 404 not found
   { from: '/not-found', to: '_show/not_found' }


parse emoji shortcodes directly from mapping


Add 1px to the width so the more link doesnt wrap

--- lib/suggestion-list-element.coffee
+++ lib/suggestion-list-element.coffee
@@ -232,7 +232,7 @@

   updateUIForChangedProps: ->
     @scroller.style['max-height'] = "#{@maxVisibleSuggestions * @uiProps.itemHeight + @uiProps.paddingHeight}px"
-    @style.width = "#{@uiProps.width}px"
+    @style.width = "#{@uiProps.width + 1}px"
     if @suggestionListFollows is 'Word'
       @style['margin-left'] = "#{@uiProps.marginLeft}px"
     @updateDescription()


change debug info

change debug info

--- atmospheric/002-accumulated-heat/csmain.coffee
+++ atmospheric/002-accumulated-heat/csmain.coffee
@@ -20,7 +20,6 @@
     $ = (selector) -> bonzo(qwery(selector))

     domReady ->
-        tmp = null
         map = null
         frame = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

@@ -29,9 +28,8 @@
             data = event.data
             if data.msg
                 $("#msg").html(data.msg)
-                map = tmp
             else if data.trn
-                tmp = data.trn
+                map = data.trn

         invoke = ->
             worker.postMessage('start')
@@ -86,7 +84,7 @@

             avg = accumulator.avg
             data = accumulator.data
-            $("#msg").html('
' + li(data, avg, 16) + li(data, avg, 48) + li(data, avg, 80) + li(data, avg, 112) + '
')
+            $("#msg").html('
' + li(data, avg, 16 * 128) + li(data, avg, 48 * 128) + li(data, avg, 80 * 128) + li(data, avg, 112 * 128) + '
')
             time += tao
             counter = (counter + 1) % 30
         handle = setInterval(evolve, 3333)


Better wallet recovery

Better wallet recovery

--- app/src/tasks/wallet_open_task.coffee
+++ app/src/tasks/wallet_open_task.coffee
@@ -54,6 +54,7 @@
     l "Create Wallet"
     ledger.app.emit 'wallet:initialization:creation'
     ledger.tasks.WalletLayoutRecoveryTask.instance.on 'done', () =>
+      ledger.tasks.OperationsSynchronizationTask.instance.startIfNeccessary()
       done?(operation_consumption: yes)
     ledger.tasks.WalletLayoutRecoveryTask.instance.on 'fatal_error', () =>
       ledger.app.emit 'wallet:initialization:failed'


Fix highlighting of lines following `x X` commands

--- grammars/output.cson
+++ grammars/output.cson
@@ -146,8 +146,9 @@
 			# Arbitrary device control function generated by \X
 			name: "meta.x-escape.custom-control.roff.output"
 			begin: "\\G(X\\S*)"
-			end:   "^(?!\\+)"
+			end:   "^(?!\\+)(.*)"
 			beginCaptures: 1: name: "keyword.device.control.roff.output"
+			endCaptures:   1: patterns: [include: "#main"]
 			contentName: "string.raw.unquoted.heredoc.roff.output"
 			patterns: [{
 				name: "keyword.operator.line-continuation.gnu.roff.output"


make codemirror session in hub destroy itself if it gets locked for more than 30 seconds; if it doesn't do this suddenly clients can loose access; this is just programming around other issues and is temporary

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -2307,7 +2307,7 @@
             mesg : message.codemirror_get_session(path:@path, project_id:@project_id, session_uuid:@session_uuid)
             cb   : (err, resp) =>
                 if err
-                    winston.debug("local_hub --> hub: (connect) error -- #{err}, #{to_json(resp)}, trying to connect to '#{@path}' in #{@project_id}.")
+                    winston.debug("local_hub --> hub: (connect) error -- #{to_json(err)}, #{to_json(resp)}, trying to connect to '#{@path}' in #{@project_id}.")
                     if resp?.path? and resp?.path.indexOf("sage_server.port") != -1
                         err = "The Sage Worksheet server is not running.  The system may be very heavily loaded, you may have messed up a custom install of Sage, or caused problems by customizing your packages.  Make this work in the terminal:
 cd ~/.sagemathcloud\necho 'import sage_server, sage_salvus' | sage
 then restart the Sage Worksheet server.
Or contact wstein@uw.edu for help."
                     #@local_hub.reconnect()
@@ -2399,8 +2399,12 @@
         # Message from some client reporting new edits; we apply them,
         # generate new edits, and send those out so that the client
         # can complete the sync cycle.
+        MAX_LOCK_TIME_S = 30

         if @_sync_lock
+            if misc.walltime(@_sync_lock) > MAX_LOCK_TIME_S
+                 # something is very suspicious -- if we can't sync for this long give up.
+                 @destroy()
             client.push_to_client(message.codemirror_diffsync_retry_later(id:mesg.id))
             return

@@ -2417,7 +2421,7 @@
             setTimeout(f, 1000)
             return

-        @_sync_lock = true
+        @_sync_lock = misc.walltime()
         before = @diffsync_server.live
         ds_client.recv_edits    mesg.edit_stack, mesg.last_version_ack, (err) =>
             if err
@@ -2460,7 +2464,7 @@

     _sync: (cb) =>    # cb(err)
         winston.debug("codemirror session -- syncing with local hub")
-        @_sync_lock = true
+        @_sync_lock = misc.walltime()
         before = @diffsync_server.live
         @diffsync_server.push_edits (err) =>
             after = @diffsync_server.live


Credentials.default: add systemHookToken

--- config/credentials.default.coffee
+++ config/credentials.default.coffee
@@ -107,6 +107,7 @@
     applicationId: ''
     applicationSecret: ''
     redirectUri: "http://dev.koding.com:8090/-/oauth/gitlab/callback"
+    systemHookToken: 'some-random-token-here'
   facebook =
     clientId: ''
     clientSecret: ''


update less

update less

--- data/points/less.coffee
+++ data/points/less.coffee
@@ -2,6 +2,7 @@
 title: "LESS"
 language: [ "less" ]
 function: [ "css-preprocessor" ]
+theory: [ "color-space" ]
 relevance: [ "stylus", "sass", "css", "javascript" ]
 desc: """
 LESS是一个开源的动态CSS语言(CSS预处理器)。


Don't pile up more numbers

Don't pile up more numbers

--- scripts/rc.coffee
+++ scripts/rc.coffee
@@ -15,7 +15,7 @@
       if score == null
         return msg.reply "Never heard of '#{who}'"
       distance = Date.now() - stamp
-      msg.reply "Last I heard, #{who} was at #{score} #{prettyMs distance} ago"
+      msg.reply "#{who} was at #{score} something like #{prettyMs distance} ago"

   robot.respond /(?:rc)\s+(\d+.?\d*)$/i, (msg) ->
     who = msg.message.user.name.toLowerCase()


Fix issue reference

Fix issue reference

This sounds like #2035, #2305 is unrelated.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -607,7 +607,7 @@
   # image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty).
   #
   getVisibleClickable: (element) ->
-    # Get the tag name.  However, `element.tagName` can be an element (not a string, see #2305), so we guard
+    # Get the tag name.  However, `element.tagName` can be an element (not a string, see #2035), so we guard
     # against that.
     tagName = element.tagName.toLowerCase?() ? ""
     isClickable = false


errors on create with duplicate name

errors on create with duplicate name

--- spec/notice/hub_spec.coffee
+++ spec/notice/hub_spec.coffee
@@ -55,6 +55,17 @@
                 _hub().hubs['hub3'].should.equal hub3
                 done()

+        it 'error on create with duplicate name', (done) ->
+
+            Hub = hub()
+            parallel([
+                -> Hub.create 'hub1'
+                -> Hub.create 'hub1'
+            ])
+            .then (->), (error) ->
+
+                error.should.match /is already defined/
+                done()


Use XMLHttpRequest directly

Use XMLHttpRequest directly

jQuery uses getAllResponseHeaders function for getting response headers,
even for getResponseHeader. As far as I know, there is no easy way to
get original xhr object and call getResponseHeader on such object.
Because of that getResponseHeader is broken on firefox with CORS.

I rewrote Travis.ajax to use XMLHttpRequest directly in order to fix
this, the compatibility with jQuery.ajax is mostly handled.

--- assets/scripts/lib/travis/ajax.coffee
+++ assets/scripts/lib/travis/ajax.coffee
@@ -5,7 +5,7 @@

   DEFAULT_OPTIONS:
     accepts:
-      json: 'application/vnd.travis-ci.2+json'
+      json: 'application/json; version=2'

   get: (url, callback) ->
     @ajax(url, 'get', success: callback)
@@ -14,6 +14,7 @@
     @ajax(url, 'post', data: data, success: callback)

   needsAuth: (method, url) ->
+    return true if Travis.ajax.pro
     return true if method != 'GET'

     result = @publicEndpoints.find (pattern) ->
@@ -23,6 +24,7 @@

   ajax: (url, method, options) ->
     method = method.toUpperCase()
+
     endpoint = Travis.config.api_endpoint || ''
     options = options || {}

@@ -31,7 +33,7 @@
       options.headers ||= {}
       options.headers['Authorization'] ||= "token #{token}"

-    options.url = "#{endpoint}#{url}"
+    url = "#{endpoint}#{url}"
     options.type = method
     options.dataType = options.dataType || 'json'
     options.context = this
@@ -43,15 +45,56 @@
       options.contentType = options.contentType || 'application/json; charset=utf-8'

     success = options.success || (->)
-    options.success = (data) =>
+    options.success = (data, status, xhr) =>
       Travis.lookup('controller:flash').loadFlashes(data.flash) if data?.flash
       delete data.flash if data?
       success.apply(this, arguments)

     error = options.error || (->)
-    options.error = (data) =>
+    options.error = (data, status, xhr) =>
       Travis.lookup('controller:flash').pushObject(data.flash) if data?.flash
       delete data.flash if data?
       error.apply(this, arguments)

-    $.ajax($.extend(options, Travis.ajax.DEFAULT_OPTIONS))
+    options = $.extend(options, Travis.ajax.DEFAULT_OPTIONS)
+
+    if options.data && (method == "GET" || method == "HEAD")
+      params = []
+      for key, value of options.data
+        params.pushObject("#{key}=#{value}")
+
+      params = params.join '&'
+      delimeter = if url.indexOf('?') == -1 then '?' else '&'
+      url = url + delimeter + params
+
+    xhr = new XMLHttpRequest()
+
+    xhr.open(method, url)
+
+    if options.accepts
+      accepts = []
+      for key, value of options.accepts
+        accepts.pushObject(value)
+      xhr.setRequestHeader('Accept', accepts.join(', '))
+
+    if options.headers
+      for name, value of options.headers
+        xhr.setRequestHeader(name, value)
+
+    if options.contentType
+      xhr.setRequestHeader('Content-Type', options.contentType)
+
+    xhr.onreadystatechange = ->
+      if xhr.readyState == 4
+        contentType = xhr.getResponseHeader('Content-Type')
+        data = if contentType && contentType.match /application\/json/
+          jQuery.parseJSON(xhr.responseText)
+        else
+          xhr.responseText
+
+        if xhr.status >= 200 && xhr.status < 300
+          options.success.call(options.context, data, xhr.status, xhr)
+        else
+          options.error.call(data, xhr.status, xhr)
+
+    xhr.send(options.data)


Correcting Restart Script

Correcting Restart Script

--- src/build/coreos/restart.coffee
+++ src/build/coreos/restart.coffee
@@ -61,7 +61,7 @@
     # We'll need to iterate through the listed services.

     for i in [7..argv.length - 1]
-      text = "echo \"/usr/bin/fleetctl --tunnel #{config.coreos.address} destroy #{argv[i]}.service\"\n"
+      text = "/usr/bin/fleetctl --tunnel #{config.coreos.address} destroy #{argv[i]}.service\"\n"
       fs.appendFileSync "#{process.cwd()}/#{argv[4]}", "#{text}"

@@ -81,7 +81,7 @@

     # Finally, bring the service(s) back online.
     for i in [7..argv.length - 1]
-      text = "echo \"/usr/bin/fleetctl --tunnel #{config.coreos.address} start #{argv[i]}.service\"\n"
+      text = "/usr/bin/fleetctl --tunnel #{config.coreos.address} start #{argv[i]}.service\"\n"
       fs.appendFileSync "#{process.cwd()}/#{argv[4]}", "#{text}"


new members are added alphabetically

new members are added alphabetically

--- src/node_modules/group-members/group-members-controller.coffee
+++ src/node_modules/group-members/group-members-controller.coffee
@@ -1,3 +1,5 @@
+_ = require('lodash')
+
 module.exports = ($scope, group, users, memberships, MembershipModel, MembershipsStore) ->
   group.memberships = memberships
   $scope.users = users
@@ -10,7 +12,8 @@
       group_id: $scope.group.id
     })
     membership.save(MembershipsStore).then ->
-      memberships.push(membership)
+      index = _.sortedIndex memberships, membership, (m) -> m.user.name
+      memberships.splice(index, 0, membership)

   $scope.removeMember = (membership, index) ->
     MembershipsStore.remove(membership).then ->


chore: remove date

chore: remove date

--- app/controllers/box.coffee
+++ app/controllers/box.coffee
@@ -1,9 +1,5 @@
 module.exports = App.BoxController = Ember.ObjectController.extend
   isEditing: false
-
-  date: (->
-    (new Date).toString()
-  ).property().cacheable(false)

   notDeletedLinks: (->
     @get('links').filterBy('softDeleted', false)


fix: snippets error (#152)

fix: snippets error (#152)

--- snippets/language-coursescript.cson
+++ snippets/language-coursescript.cson
@@ -1633,6 +1633,7 @@
       Pic(id=$15): $16
       Audio(id=$17,dubbing=$18): $19
       Text: $20
+    """
   'AIXPA2AJet1':
     'prefix': 'aixpa2ajet1'
     'body': """
@@ -1728,9 +1729,9 @@
         Pic: $16
         Opts:
         Mix:
-          Audio(sound_effect=true, checked=true): $17
+          Audio(sound_effect=true): $17
           Animation:
-          Pic: $18
+          Pic(checked=true): $18
           coordinate: x=$19,y=$20
           Pic: $21
           coordinate: x=$22,y=$23


Added more stub functions for Texture

Added more stub functions for Texture

--- src/graphics/texture.coffee
+++ src/graphics/texture.coffee
@@ -6,6 +6,10 @@

     loadFromFile: (filename) ->

+    loadFromData: (data) ->
+
+    textureRect: new Elyssa.Rect()
+
     toString: ->
       filename: ''


Formatting.

Formatting.

--- src/views/accessibility.coffee
+++ src/views/accessibility.coffee
@@ -124,23 +124,24 @@
             else if _.keys(profiles).length
                 shortText = i18n.t('accessibility.no_data')

+            iconClass = if profileSet
+                p13n.getProfileElements(profiles).pop()['icon']
+            else
+                'icon-icon-wheelchair'
+
+            has_data: hasData
             profile_set: profileSet
-            icon_class:
-                if profileSet
-                    p13n.getProfileElements(profiles).pop()['icon']
-                else
-                    'icon-icon-wheelchair'
+            icon_class: iconClass
             shortcomings_pending: shortcomingsPending
             shortcomings_count: shortcomingsCount
             shortcomings: shortcomings
             groups: sentenceGroups
             details: details
             sentence_error: sentenceError
-            feedback: @getDummyFeedback()
             header_classes: headerClasses.join ' '
             collapse_classes: collapseClasses.join ' '
             short_text: shortText
-            has_data: hasData
+            feedback: @getDummyFeedback()

         getDummyFeedback: ->
             now = new Date()


change umask for socket file creation

change umask for socket file creation

--- server/app.coffee
+++ server/app.coffee
@@ -91,7 +91,14 @@
   seedData() unless app.get('env') is "test"
   port = retrievePort()

+  # replace default umask with 0000 and save
+  # original umask
+  defaultProcessUmask = process.umask(0o000)
+
   server = http.createServer(app).listen(port, (err) ->
+    # reset umask
+    process.umask(defaultProcessUmask)
+
     callback(err, server, port)
   )


Make server features discoverable

Make server features discoverable

--- src/xmpp/backend_pubsub.coffee
+++ src/xmpp/backend_pubsub.coffee
@@ -264,6 +264,10 @@
             # `pending' initialized with 1, to not miss the items=[] case
             done()

+    findFeatures: (jid, cb) =>
+        @infoCache.get jid, (err, results) ->
+            cb err, results?.features
+
     ##
     # @param user {String} Bare JID
     # @param cb {Function} callback(error, isAnonymous)


engine#add

engine#add

--- test/engine.coffee
+++ test/engine.coffee
@@ -62,7 +62,8 @@
 test "#add", ->
   engine = Engine()

-  engine.add "GameObject"
+  engine.add "GameObject",
+    test

 test "zSort", ->
   engine = Engine


Table widget testing

Table widget testing

--- widgets/table/table.coffee
+++ widgets/table/table.coffee
@@ -3,4 +3,5 @@
     onData: (data) ->
         console.log 'table widget data: ', data
         console.log 'table widget node: ', $(@node)
-        console.log 'table widget height: ', $(@node).height()
+        console.log 'table widget parent height: ', $(@node).parent.height()
+        console.log 'table widget parent: ', $(@node).parent


Table widget testing

Table widget testing

--- widgets/table/table.coffee
+++ widgets/table/table.coffee
@@ -3,5 +3,4 @@
     onData: (data) ->
         console.log 'table widget data: ', data
         console.log 'table widget node: ', $(@node)
-        console.log 'table widget parent height: ', $(@node).parent.height()
         console.log 'table widget parent: ', $(@node).parent


Single quotes for consistency.

Single quotes for consistency.

--- test/module.coffee
+++ test/module.coffee
@@ -15,7 +15,7 @@
         counter += 1

       @register 'echo', (done) ->
-        @exec "echo test", done
+        @exec 'echo test', done

     methods.should.have.keys 'hello', 'count', 'echo'


Fix level editor not loading after recent hotfix

--- app/lib/LevelLoader.coffee
+++ app/lib/LevelLoader.coffee
@@ -62,7 +62,7 @@
       @listenToOnce @level, 'sync', @onLevelLoaded

   onLevelLoaded: ->
-    if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course']
+    if not @sessionless and @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course']
       @sessionDependenciesRegistered = {}
     if (@courseID and @level.get('type', true) not in ['course', 'course-ladder']) or window.serverConfig.picoCTF
       # Because we now use original hero levels for both hero and course levels, we fake being a course level in this context.


Remove consol.log from the view_helper.

Remove consol.log from the view_helper.

--- app/lib/view_helper.coffee
+++ app/lib/view_helper.coffee
@@ -57,7 +57,7 @@
       _.extend(metadata, {required: md.required}) if md.required?
       _.extend(metadata, {label: md.label}) if md.label?
       _.extend(metadata, {domain: md.domain}) if md.domain?
-      console.log "property", property,"metadata", metadata
+      #console.log "property", property,"metadata", metadata
   return metadata

@@ -264,7 +264,7 @@

 # Example => button "transalationKey", icon="heart" id="12" class="btn-danger" type="submit"
 Handlebars.registerHelper "button",(text_key, options) ->
-  console.log "button", i18n.t(text_key)
+  #console.log "button", i18n.t(text_key)
   opt = options.hash or {}
   isScript = if typeof opt.isScript is "undefined" then true else opt.isScript
   cssClass = opt.class or ""


Add logging to register_plugin() and register_model()

--- bokehjs/src/coffee/common/base.coffee
+++ bokehjs/src/coffee/common/base.coffee
@@ -3,6 +3,8 @@
 window = {location: {href: "local"}} unless window?

 coffee = require "coffee-script"
+
+{logger} = require "./logging"

 # add some useful functions to underscore
 require("./custom").monkey_patch()
@@ -200,6 +202,7 @@
 }

 Collections.register_plugin = (plugin, locations) ->
+  logger.info("Registering plugin: #{plugin}")
   Collections.register_locations locations, errorFn = (name) ->
     throw new Error("#{name} was already registered, attempted to re-register in #{plugin}")

@@ -214,6 +217,8 @@
       errorFn?(name)

 Collections.register_model = (name, mod) ->
+  logger.info("Registering model: #{name}")
+
   compile = (code) ->
     body = coffee.compile(code, {bare: true, shiftLine: true})
     new Function("require", "module", "exports", body)


need to resolve refs on ranges before frame initialized

--- bokehjs/src/coffee/common/plot.coffee
+++ bokehjs/src/coffee/common/plot.coffee
@@ -203,9 +203,9 @@
         bds = v.glyph?.bounds?()
         if bds?
           bounds[k] = bds
-      for xr in _.values(@mget('extra_x_ranges')).concat(@mget('x_range'))
+      for xr in _.values(frame.get('x_ranges'))
         xr.update?(bounds, 0)
-      for yr in _.values(@mget('extra_y_ranges')).concat(@mget('y_range'))
+      for yr in _.values(frame.get('y_ranges'))
         yr.update?(bounds, 1)

       title = @mget('title')
@@ -288,11 +288,13 @@
       super(attrs, options)

       for xr in _.values(@get('extra_x_ranges')).concat(@get('x_range'))
+        xr = @resolve_ref(xr)
         plots = xr.get('plots')
         if _.isArray(plots)
           plots = plots.concat(@)
           xr.set('plots', plots)
       for yr in _.values(@get('extra_y_ranges')).concat(@get('y_range'))
+        yr = @resolve_ref(yr)
         plots = yr.get('plots')
         if _.isArray(plots)
           plots = plots.concat(@)


bugfix after #71 changes

bugfix after #71 changes

--- _src/js/views/facets/base.coffee
+++ _src/js/views/facets/base.coffee
@@ -6,7 +6,7 @@

 	initialize: ( options )=>
 		@sub = options.sub
-		@jQuery = @sub.jQuery
+		@jQuery = @sub?.jQuery or options?.main?.jQuery
 		@result = new SubResults()
 		return


Spec update

Spec update

--- spec/language-haskell-spec.coffee
+++ spec/language-haskell-spec.coffee
@@ -122,7 +122,8 @@
       data = 'main = (putStrLn :: String -> IO ()) ("Hello World" :: String)'
       {tokens} = grammar.tokenizeLine(data)
       expect(tokens).toEqual [
-        { value : 'main ', scopes : [ 'source.haskell' ] }
+        { value : 'main', scopes : [ 'source.haskell' ] }
+        { value : ' ', scopes : [ 'source.haskell' ] }
         { value : '=', scopes : [ 'source.haskell', 'keyword.operator.haskell' ] }
         { value : ' ', scopes : [ 'source.haskell' ] }
         { value : '(', scopes : [ 'source.haskell' ] }
@@ -155,7 +156,8 @@
       data = 'main = putStrLn "Hello World" :: IO ()'
       {tokens} = grammar.tokenizeLine(data)
       expect(tokens).toEqual [
-        { value : 'main ', scopes : [ 'source.haskell' ] }
+        { value : 'main', scopes : [ 'source.haskell' ] }
+        { value : ' ', scopes : [ 'source.haskell' ] }
         { value : '=', scopes : [ 'source.haskell', 'keyword.operator.haskell' ] }
         { value : ' ', scopes : [ 'source.haskell' ] }
         { value : 'putStrLn', scopes : [ 'source.haskell', 'support.function.prelude.haskell' ] }


update brain

update brain

--- core/brain.coffee
+++ core/brain.coffee
@@ -70,9 +70,14 @@
         @addTextRespond /^TIME$/i,"",(response)=>
             response.replyText "Server time is: #{new Moment().format()}"

+        @addTextRespond /^MEWBOT DIE$/i,"",(response)=>
+            response.replyText "Goodbye, curel world"
+            setTimeout ->
+                process.exit 0
+            ,1000
+
         @addTextRespond /(what are )?the (three |3 )?(rules|laws)/i,"",(response)=>
             response.replyText rebotRules
-


`data/schedules.ndjson`: add missing `id` field

--- build/03-schedules.coffee
+++ build/03-schedules.coffee
@@ -28,6 +28,7 @@
 		start = parseDate(data.start_date).valueOf()
 		end = parseDate(data.end_date).valueOf()
 		schedule = schedules[data.service_id] =
+			id:				parseInt data.service_id
 			start:			start
 			days: 			Math.round moment.duration(end - start).asDays()
 			available:		[]


added tests to LargeAttributeMeta

added tests to LargeAttributeMeta

--- test/driver/druidDriver.coffee
+++ test/driver/druidDriver.coffee
@@ -2,6 +2,7 @@
 expect = chai.expect
 utils = require('../utils')

+{AttributeMeta} = require('../../src/driver/attributeMeta')
 {FacetQuery, FacetFilter} = require('../../src/query')

 simpleLocator = require('../../src/locator/simpleLocator')
@@ -735,3 +736,42 @@
         expect(count).to.equal(1)
         done()

+  describe "acknowledges attribute metas", ->
+    querySpy = null
+    requesterSpy = (request, callback) ->
+      querySpy(request.query)
+      callback(null, [])
+      return
+
+    driver = druidDriver({
+      requester: requesterSpy
+      dataSource: 'wikipedia_editstream'
+      timeAttribute: 'time'
+      approximate: true
+      attributeMetas: {
+        page: AttributeMeta.fromSpec({
+          type: 'large'
+        })
+      }
+    })
+
+    it "does not send empty context", (done) ->
+      context = {}
+      query = new FacetQuery([
+        { operation: 'split', name: 'Page', bucket: 'identity', attribute: 'page' }
+        { operation: 'apply', name: 'Count', aggregate: 'sum', attribute: 'count' }
+      ])
+
+      count = 0
+      querySpy = (query) ->
+        count++
+        expect(query.context['doAggregateTopNMetricFirst']).to.be.true
+        return
+
+      driver {
+        context
+        query
+      }, (err, result) ->
+        expect(count).to.equal(1)
+        done()
+


shorten url changed to google

shorten url changed to google

--- core/utils.coffee
+++ core/utils.coffee
@@ -725,6 +725,23 @@
   getNextHighestZIndex:(context)->
    uniqid = context.data 'data-id'
    zIndexContexts[uniqid] if isNaN zIndexContexts[uniqid] then 0 else zIndexContexts[uniqid]++
+
+  shortenUrl: (url, callback)->
+    request = $.ajax "https://www.googleapis.com/urlshortener/v1/url",
+      type        : "POST"
+      contentType : "application/json"
+      data        : JSON.stringify {longUrl: url}
+      dataType    : "json"
+
+    request.done (data)=>
+      if data.id
+        url = data.id
+
+      callback url, data
+
+    request.error ->
+      log "url shorten error, returing self."
+      callback url

   # deprecated ends


:busts_in_silhouette::tennis: Updated in browser at strd6.github.io/editor

--- test/google_spreadsheet.coffee
+++ test/google_spreadsheet.coffee
@@ -2,5 +2,5 @@

 describe "Google Spreadsheet wrapper", ->
   it "loads spreadsheet from a given key", ->
-    sheet = Spreadsheet.load "0ArtCBkZR37MmdFJqbjloVEp1OFZLWDJ6M29OcXQ1WkE"
-    assert.ok(Object.keys(sheet) > 0)
+    Spreadsheet.load "0ArtCBkZR37MmdFJqbjloVEp1OFZLWDJ6M29OcXQ1WkE", (data) ->
+      assert.ok(Object.keys(data) > 0)


Add a comment.

Add a comment.

--- Example/CoffeeScripts/action_sheet_demo.coffee
+++ Example/CoffeeScripts/action_sheet_demo.coffee
@@ -11,6 +11,8 @@
         padding: 10
       )

+      # GridCells relayout their content after each add() by default.
+      # batchUpdates() temporarily disables automatic relayout for efficiency.
       gridCell.batchUpdates(->
         # Add the button that will show the action sheet.
         button = new TN.UI.Button(


Prove tags exist

Prove tags exist

--- test/spec/test_reactive.coffee
+++ test/spec/test_reactive.coffee
@@ -360,7 +360,8 @@
     beforeEach ->
       elem = rxv.tags.rect {x:10, y:20, height:100, width:100, fill:"blue", stroke: "red"}
     it 'should have the right tag', ->
-      expect(elem).not.toBeUndefined();
+      expect(elem).not.toBeUndefined()
+      console.log elem.toString()
       #expect(elt.is('rect')).toBe(true)
     it 'should have the set attributes', ->
       expect(true).toBe(true)


Less confusing name

Less confusing name

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -105,7 +105,7 @@
 		for layer in [@background, @phone, @viewport, @content, @screen]
 			layer.on "touchmove", (event) -> event.preventDefault()

-		@_context = new Framer.Context(parent:@content, name:"Device")
+		@_context = new Framer.Context(parent:@content, name:"DeviceScreen")
 		@_context.perspective = 1200

 	_update: =>


Update defaults

Update defaults

--- framer/Components/SliderComponent.coffee
+++ framer/Components/SliderComponent.coffee
@@ -36,7 +36,7 @@
 			borderRadius: 50
 			clip: false
 			width: 300
-			height: 10
+			height: 6
 			value: 0
 			knobSize: 30

@@ -45,8 +45,9 @@

 		@knob = new Knob
 			backgroundColor: "#fff"
-			shadowY: 1, shadowBlur: 3
-			shadowColor: "rgba(0, 0, 0, 0.35)"
+			shadowY: 2
+			shadowBlur: 4
+			shadowColor: "rgba(0, 0, 0, 0.2)"
 			name: "knob"

 		@fill = new Layer


Filippo asked for these features

Filippo asked for these features

--- dev/enc.iced
+++ dev/enc.iced
@@ -1,12 +1,20 @@
 {Encryptor} = require '../lib/enc'
 {Decryptor} = require '../lib/dec'

-key = new Buffer "this be the password"
-data = new Buffer ("this be the secret message" for i in [0..5]).join " -> "
+argv = require('optimist').alias('e', 'extra_bytes')
+   .usage("usage: $0 -e <extra bytes> [<key-in-hex>] [<data-in-hex>] ").argv
+
+key = if argv._.length > 0 then (new Buffer argv._[0], 'hex')
+else (new Buffer "this be the password")
+
+data = if argv._.length > 1 then (new Buffer argv._[1], 'hex')
+else (new Buffer ("this be the secret message" for i in [0..5]).join " -> ")

 enc = new Encryptor { key, version : 3 }
-dec = new Decryptor { key }
+if argv.e?
+  await enc.resalt { extra_keymaterial : argv.e }, defer err, keys
+  console.log "extra: #{keys.extra.toString('hex')}"
 await enc.run { data : new Buffer data }, defer err, ct
-console.log key.toString('hex')
-console.log ct.toString('hex')
+console.log "key: #{key.toString('hex')}"
+console.log "ciphertext: #{ct.toString('hex')}"
 process.exit(0)


Run tests during development

Run tests during development

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -54,7 +54,7 @@
             verbose: true

 gulp.task 'watch', ['build:distribution'], ->
-    gulp.watch 'src/**/*.coffee', ['build:distribution']
+    gulp.watch 'src/**/*.coffee', ['test', 'build:distribution']

 gulp.task 'clean', (cb) ->
     del ['dist', 'gen-js'], cb


fix($tests): force documentation format for expect tests

--- specs/units/environment.spec.coffee
+++ specs/units/environment.spec.coffee
@@ -178,10 +178,12 @@
   environmentMethod('expect').cannotBeCalledOutsideIt()

   runningSpecs('call with only a value')
+  .withOption('documentation', true)
   .shouldSucceedWith /10 should be equal to 10/, ->
     specify -> expect(10).to equal 10

   runningSpecs('call with a description and a value')
+  .withOption('documentation', true)
   .shouldSucceedWith /a number should be equal to 10/, ->
     specify -> expect('a number', 10).to equal 10


Spinner: one more test.

Spinner: one more test.

--- tests/tests.coffee
+++ tests/tests.coffee
@@ -227,6 +227,8 @@
 	@test.assertVisible '.js-loaders .loader-shade', 'Spinner: Loader shade is visible after click on a link'
 casper.thenClick '.js-loading-stop', ->
 	@test.assertEval (-> !(jQuery '.js-loaders').hasClass('is-loading')), 'Spinner: .is-loading class has been removed after click on a link'
+casper.wait 50, ->
+	@test.assertExists '.js-loaders .loader-shade', 'Spinner: HTML not removed until transitionend'
 casper.wait 250, ->
 	@test.assertNotExists '.js-loaders .loader-shade', 'Spinner: HTML was removed'
 	@test.assertEval (-> !(jQuery '.js-loaders').hasClass('loader-wrapper')), 'Spinner: .loader-wrapper class has been removed after click on a link'


added array shuffle method

added array shuffle method

--- tools/objects.coffee
+++ tools/objects.coffee
@@ -53,6 +53,18 @@
         next()

+    ##
+    ## shuffle the order of an array randomly
+    ##
+    shuffle: (arr) ->
+        i = arr.length
+        while --i > 0
+            j = ~~(Math.random() * (i + 1))
+            temp = arr[j]
+            arr[j] = arr[i]
+            arr[i] = temp
+
+        return arr

     ##
     ## extract an index out of an object (or array)


stupid bug fixed

stupid bug fixed

--- tools/objects.coffee
+++ tools/objects.coffee
@@ -17,12 +17,12 @@
         else if floyd.tools.objects.isObject obj
             proxy = {}
             objects.process obj,
-                next: (key, value)->
+                each: (key, value, next)->
                     if typeof value is 'function'
                         proxy[key] = objects.promisify value, obj
                     else
                         proxy[key] = value
-
+
                     next()

             return proxy


Adding missing variable.

Adding missing variable.

--- app/routes/index.coffee
+++ app/routes/index.coffee
@@ -10,7 +10,7 @@
         router = express.Router()
         proxyPort = app.get 'proxyPort'
         port = app.get 'port'
-        baseURL = "http://#{host}#{if proxyPort isnt 80 then (':' + port) else ''}/"
+        baseURL = "http://#{app.get('host')}#{if proxyPort isnt 80 then (':' + port) else ''}/"

         # GET home page.
         router.get '/', (req, res) ->


static pages: prerender urls handling optimization

--- deployment/nginx.coffee
+++ deployment/nginx.coffee
@@ -303,27 +303,17 @@
         proxy_set_header      X-Prerender-Token #{KONFIG.prerenderToken};

         set $prerender 0;
-        set $validation_result '';
         if ($http_user_agent ~* "baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
-          set $validation_result 'valid bot';
-        }
-        if ($args ~ "_escaped_fragment_") {
-          set $validation_result 'valid bot';
-        }
-        if ($request_uri ~ "\/(Pricing|About|Legal|Features)(\/|&|\?|$)") {
-          set $validation_result "${validation_result} & valid page";
-        }
-        if ($args ~ '_escaped_fragment_=(&|$)') {
-          set $validation_result "${validation_result} & valid page";
-        }
-        if ($request_uri ~ "^(\/)?$") {
-          set $validation_result "${validation_result} & valid page";
+          set $prerender 1;
+        }
+        if ($request_uri !~ "(^(\/)?$)|(\/(Pricing|About|Legal|Features)(\/|$))") {
+          set $prerender 0;
+        }
+        if ($args ~ "_escaped_fragment_=$|(\/(Pricing|About|Legal|Features)(\/|$))") {
+          set $prerender 1;
         }
         if ($http_user_agent ~ "Prerender") {
-          set $validation_result '';
-        }
-        if ($validation_result = 'valid bot & valid page') {
-          set $prerender 1;
+          set $prerender 0;
         }

         #resolve using Google's DNS server to force DNS resolution and prevent caching of IPs


catch errors in mockLocalStorage.getItem

--- src/mockLocalStorage.coffee
+++ src/mockLocalStorage.coffee
@@ -4,8 +4,11 @@
 localStorage = {}
 localStorage.getItem = (key) ->
   console.log "getItem(#{key})" if DEBUG
-  return localStorage[key]
   console.log "In localStorage: #{this.toString()}" if DEBUG
+  try
+    return localStorage[key]
+  catch error
+    console.log error if DEBUG

 localStorage.setItem = (key, value) ->
   console.log "setItem(#{key}, #{value})" if DEBUG


set source

set source

--- src/modules/keyboard.coffee
+++ src/modules/keyboard.coffee
@@ -39,7 +39,7 @@
     if range.isCollapsed()
       @quill.prepareFormat(format, value)
     else
-      @quill.formatText(range, format, value, 'user')
+      @quill.formatText(range, format, value, Quill.sources.USER)
     toolbar = @quill.getModule('toolbar')
     toolbar.setActive(format, value) if toolbar?

@@ -93,7 +93,7 @@
                        .insert("\t")
                        .delete(range.end - range.start)
                        .retain(@quill.getLength() - range.end)
-    @quill.updateContents(delta)
+    @quill.updateContents(delta, Quill.sources.USER)
     @quill.setSelection(range.start + 1, range.start + 1)


small corection

small corection

--- src/open-weather-map.coffee
+++ src/open-weather-map.coffee
@@ -60,8 +60,7 @@
           msg.send "Encountered an error :( #{err}"
           return
         json = JSON.parse body
-        msg.send "today we will have #{json.list[0].main.temp} #{named_unit} with #{json.list[0].weather[0].description} and feels like #{json.list[0].main.feels_like} #{named_unit}.\n Tomorrow we will have #{json.list[1].main.temp} #{named_unit} with #{json.list[1].weather[0].description} and feels like #{json.list[0].main.feels_like} #{named_unit}.\n The day after we will have #{json.list[2].main.temp} #{named_unit} with #{json.list[2].weather[0].description} and feels like #{json.list[2].main.feels_like} #{named_unit}"
-
+        msg.send "today we will have #{json.list[0].main.temp} #{named_unit} with #{json.list[0].weather[0].description} and feels like #{json.list[0].main.feels_like} #{named_unit}.\n Tomorrow we will have #{json.list[1].main.temp} #{named_unit} with #{json.list[1].weather[0].description} and feels like #{json.list[1].main.feels_like} #{named_unit}.\n The day after we will have #{json.list[2].main.temp} #{named_unit} with #{json.list[2].weather[0].description} and feels like #{json.list[2].main.feels_like} #{named_unit}.
   named_unit = switch
     when units == "metric" then "°C"
     when units == "imperial" then "°F"


Killing the Authentication VB after successful login

--- src/api/cloudbrowser.coffee
+++ src/api/cloudbrowser.coffee
@@ -503,6 +503,9 @@
                                                 bserver.redirect(query.redirectto)
                                             else
                                                 bserver.redirect(appUrl)
+                                                setTimeout () ->
+                                                    bserver.server.applicationManager.find(bserver.mountPoint).browsers.close(bserver)
+                                                , 500
                                 else callback(false)
                         else callback(false)

@@ -516,6 +519,9 @@
                 if not query.redirectto?
                     search += "&redirectto=" + appUrl
                 bserver.redirect( "http://" + config.domain + ":" + config.port + '/googleAuth' + search)
+                setTimeout () ->
+                    bserver.server.applicationManager.find(bserver.mountPoint).browsers.close(bserver)
+                , 500

             sendEmail : (toEmailID, subject, message, callback) ->
                 smtpTransport = Nodemailer.createTransport "SMTP",


Account Billing: Don't wait viewAppended event for creating views

--- client/account/lib/views/accountbilling.coffee
+++ client/account/lib/views/accountbilling.coffee
@@ -29,8 +29,10 @@
     { appStorageController } = kd.singletons
     @accountStorage          = appStorageController.storage 'Account', '1.0'

-
-  viewAppended: ->
+    @initViews()
+
+
+  initViews: ->

     @addSubView @subscriptionWrapper = new kd.CustomHTMLView
       tagName  : 'section'


fixed group activity permalinks

fixed group activity permalinks

--- client/landing/app/MainApp/kodingrouter.coffee
+++ client/landing/app/MainApp/kodingrouter.coffee
@@ -29,7 +29,7 @@

     @on 'AlreadyHere', -> log "You're already here!"
     @on 'Params', ({params, query})=>
-      #@utils.defer => KD.getSingleton('groupsController').changeGroup params.name
+      #@utils.defer => KD.getSingleton('groupsController').changeGroup params.name

   listen:->
     super
@@ -56,10 +56,10 @@
       if not ///^#{entrySlug}///.test(route) and entrySlug isnt '/koding'
         route =  entrySlug + route

-
+

     super route, options
-
+

   handleRoot =->
     # don't load the root content when we're just consuming a hash fragment
@@ -142,7 +142,7 @@
       new KDNotificationView title: err?.message or 'An unknown error has occured.'
       @handleNotFound route

-    if name
+    if name and not slug
       KD.remote.cacheable name, (err, models)=>
         if models?
         then onSuccess models
@@ -386,7 +386,7 @@
               run $ kd register renew on command line interface.

               """
               showModal title, content
-
+
       # top level names
       '/:name':do->
         open =(routeInfo, model)->


support code chunk for pandoc parser

support code chunk for pandoc parser

--- lib/md.coffee
+++ lib/md.coffee
@@ -960,6 +960,7 @@

     return pandocRender inputString, {args, projectDirectoryPath: option.projectDirectoryPath, fileDirectoryPath: option.fileDirectoryPath}, (error, html)->
       html = "<pre>#{error}</pre>" if error
+      # console.log(html)
       # format blocks
       $ = cheerio.load(html)
       $('pre').each (i, preElement)->
@@ -969,7 +970,7 @@
           codeBlock = $(preElement).children().first()
           classes = (codeBlock.attr('class')?.split(' ') or []).filter (x)-> x != 'sourceCode'
           lang = classes[0]
-          if $preElement.attr('class')?.match(/(mermaid|viz|dot|puml|plantuml|wavedrom)/)
+          if $preElement.attr('class')?.match(/(mermaid|viz|dot|puml|plantuml|wavedrom|\{(.+)\})/)
             lang = $preElement.attr('class')
           codeBlock.attr('class', 'language-' + lang)

@@ -977,6 +978,7 @@
   else # remarkable parser
     # parse markdown
     html = md.render(inputString)
+    # console.log(html)
     return finalize(html)

 module.exports = {


Apply syntax rule.

Apply syntax rule.

--- client/pricing/lib/views/singleplanview.coffee
+++ client/pricing/lib/views/singleplanview.coffee
@@ -96,7 +96,8 @@

     { appManager } = kd.singletons
     pricingView = appManager.get('Pricing').getView()
-    pricingView.once PaymentConstants.events.WORKFLOW_STARTED, @buyButton.bound 'hideLoader'
+    workflowStarted = PaymentConstants.events.WORKFLOW_STARTED
+    pricingView.once workflowStarted, @buyButton.bound 'hideLoader'

     @emit 'PlanSelected', {
       planTitle, monthPrice, yearPrice


Remove debugging code

Remove debugging code

--- client/skr/screens/sales-order/SalesOrder.cjsx
+++ client/skr/screens/sales-order/SalesOrder.cjsx
@@ -21,12 +21,7 @@
             })

     getInitialState: ->
-        isEditing: true
         commands: new Lanes.Screens.Commands(this, modelName: 'sales_order')
-
-    componentDidMount: ->
-        @sales_order.visible_id = '1'
-        @refs.finder.loadCurrentSelection()

     render: ->
         <div className="sales-order flex-vertically" >


Added support for non-unique configuration names across multiple projects

--- src/scripts/teamcity.coffee
+++ src/scripts/teamcity.coffee
@@ -13,8 +13,11 @@
 #   hubot show me builds - Show status of currently running builds
 #   hubot tc list projects - Show all available projects
 #   hubot tc list buildTypes - Show all available build types
+#   hubot tc list buildTypes of <project> - Show all available build types for the specified project
 #   hubot tc list builds <buildType> - Show the status of the last 5 builds
+#   hubot tc list builds of <buildType> of <project> - Show the status of the last 5 builds of the specified build type of the specified project
 #   hubot tc build start <buildType> - Adds a build to the queue for the specified build type
+#   hubot tc build start <buildType> of <project> - Adds a build to the queue for the specified build type of the specified project
 #
 # Author:
 #   Micah Martin
@@ -59,8 +62,12 @@
          projects = JSON.parse(body).project unless err
          callback err, msg, projects

-  getBuildTypes = (msg, callback) ->
-    url = "http://#{hostname}/httpAuth/app/rest/buildTypes"
+  getBuildTypes = (msg, project, callback) ->
+    projectSegment = ''
+    if project?
+      projectSegment = '/projects/name:' + encodeURIComponent project
+    url = "http://#{hostname}/httpAuth/app/rest#{projectSegment}/buildTypes"
+    console.log url
     msg.http(url)
       .headers(getAuthHeader())
       .get() (err, res, body) ->
@@ -68,8 +75,12 @@
          buildTypes = JSON.parse(body).buildType unless err
          callback err, msg, buildTypes

-   getBuilds = (msg, id, type, callback) ->
-    url = "http://#{hostname}/httpAuth/app/rest/buildTypes/#{type}:#{escape(id)}/builds"
+  getBuilds = (msg, project, configuration, callback) ->
+    projectSegment = ''
+    if project?
+      projectSegment = "/projects/name:#{encodeURIComponent(project)}"
+
+    url = "http://#{hostname}/httpAuth/app/rest#{projectSegment}/buildTypes/name:#{encodeURIComponent(configuration)}/builds"
     msg.http(url)
       .headers(getAuthHeader())
       .query(locator: ["lookupLimit:5","running:any"].join(","))
@@ -78,10 +89,10 @@
         builds = JSON.parse(body).build unless err
         callback err, msg, builds

-  mapNameToIdForBuildType = (msg, name, callback) ->
+  mapNameToIdForBuildType = (msg, project, name, callback) ->

     execute = (buildTypes) ->
-      buildType =  _.find buildTypes, (bt) -> return bt.name == name
+      buildType =  _.find buildTypes, (bt) -> return bt.name == name and (not project? or bt.projectName == project)
       if buildType
         return buildType.id

@@ -91,12 +102,20 @@
       callback(msg, result)
       return

-    getBuildTypes msg, (err, msg, buildTypes) ->
+    getBuildTypes msg, project, (err, msg, buildTypes) ->
       callback msg, execute(buildTypes)

   robot.respond /tc build start (.*)/i, (msg) ->
-    buildName = msg.match[1]
-    mapNameToIdForBuildType msg, buildName, (msg, buildType) ->
+    configuration = buildName = msg.match[1]
+    project = null
+    buildTypeRE = /(.*?) of (.*)/i
+
+    buildTypeMatches = buildName.match buildTypeRE
+    if buildTypeMatches?
+      configuration = buildTypeMatches[1]
+      project = buildTypeMatches[2]
+
+    mapNameToIdForBuildType msg, project, configuration, (msg, buildType) ->
       if not buildType
         msg.send "Build type #{buildName} was not found"
         return
@@ -111,8 +130,6 @@
           else
             msg.send "Dropped a build in the queue for #{buildName}. Run `tc list builds #{buildName}` to check the status"

-
-
   robot.respond /tc list (projects|buildTypes|builds) ?(.*)?/i, (msg) ->
     type = msg.match[1]

@@ -126,14 +143,31 @@
           msg.send message

       when "buildTypes"
-        getBuildTypes msg, (err, msg, buildTypes) ->
+        project = null
+        if option?
+          projectRE = /^\s*of (.*)/i
+          matches = option.match(projectRE)
+          if matches? and matches.length > 1
+            project = matches[1]
+
+        getBuildTypes msg, project, (err, msg, buildTypes) ->
           message = ""
           for buildType in buildTypes
-            message += buildType.name + "\n"
+            message += buildType.projectName + '/' + buildType.name + "\n"
           msg.send message

       when "builds"
-        getBuilds msg, option, "name", (err, msg, builds) ->
+        configuration = option
+        project = null
+
+        buildTypeRE = /^\s*of (.*?) of (.*)/i
+
+        buildTypeMatches = option.match buildTypeRE
+        if buildTypeMatches?
+          configuration = buildTypeMatches[1]
+          project = buildTypeMatches[2]
+
+        getBuilds msg, project, configuration, (err, msg, builds) ->
           if not builds
             msg.send "Could not find builds for #{option}"
             return
@@ -147,4 +181,4 @@
               status = if build.status == "SUCCESS" then "OK!" else "__FAILED__"
               message = "#{status} :: #{baseMessage}"

-            msg.send message+            msg.send message


Refactored bootstrap a little bit

Refactored bootstrap a little bit

--- src/server/bootstrap.coffee
+++ src/server/bootstrap.coffee
@@ -45,10 +45,15 @@
       (cb) => @runTriggers "before:init", cb
       (cb) => @runTriggers "after:init", cb
     ], (err) =>
-      callback() if callback
+      if err
+        App.Logger.error err
+
+      callback err if callback

   run: (options) ->
-    @init options, =>
+    @init options, (err) =>
+      return if err
+
       @start options.cb

   @run: (options) ->
@@ -130,7 +135,7 @@

     cb()

-  initTemplates: (cb) ->
+  initTemplates: (callback) ->
     # find all templates and save their content in a hash
     files = []
     @metadata().templates = {}
@@ -142,6 +147,8 @@

     loadTemplateFile = (file, cb) =>
       fs.readFile file, (err, content) =>
+        return cb err if err
+
         file = path.normalize(file)
         index = file
           .replace(path.normalize(dirname), "")
@@ -149,30 +156,30 @@
           .replace(/\/(server|shared)\//, "")

         @metadata().templates[index] = content.toString()
-        cb index
-
-    finder = findit dirname
-
-    # we received a file
-    finder.on "file", (file, stat) =>
-      files.push file
-
-    # we received all files.
-    finder.on "end", =>
-      async.eachSeries files,
-        readFile = (file, done) =>
-          loadTemplateFile file, (index) =>
-            done()
-
-        # we are done!
-        finished = (err) =>
-          cb()
+
+        cb err, index
+
+    async.series [
+      (cb) =>
+        finder = findit dirname
+
+        # we received a file
+        finder.on "file", (file, stat) =>
+          files.push file
+
+        # we received all files.
+        finder.on "end", cb
+
+      (cb) =>
+        async.eachSeries files, loadTemplateFile, cb
+    ], (err) =>
+      return callback err

     # watch for changes and automatically reload files
     if Salad.env is "development"
       gaze "#{dirname}/*/*/*.hbs", (err, watcher) =>
         watcher.on "changed", (file) =>
-          loadTemplateFile file, (index) =>
+          loadTemplateFile file, (err, index) =>
             content = @metadata().templates[index]
             Salad.Template.Handlebars.registerPartial index, content


Use the short variable for bucket name.

Use the short variable for bucket name.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -83,7 +83,7 @@
         db.putItem(request, cb)

       write_data: (cb) ->
-        path = options.amazon_s3_snapshots_bucket_name+'/'+docName+'-'+docData.v+'.snapshot'
+        path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
         headers = {}
         data = JSON.stringify(docData.snapshot)

@@ -272,7 +272,7 @@
         db.putItem(request, cb)

       write_data: (cb) ->
-        path = options.amazon_s3_snapshots_bucket_name+'/'+docName+'-'+docData.v+'.snapshot'
+        path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
         headers = {}
         data = JSON.stringify(docData.snapshot)


Add gzip compression to all JSON attributes.

The compression is optional and is backwards compatible. The server can
read operations that were written without compression. If timing is
enabled it reports timing and size differences involved in the
compression process.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -43,20 +43,27 @@
 #  operations_ro_concurrency: 1
 #  operations_rw_concurrency: 1
 #
-#  If you would like more detailed information about the requests going to
-#  Amazon you can enabled timing information be setting the timing option to
-#  true.
-#
-#  Example output:
-#  Dynamo[<number of milliseconds>,<cost in throughput of operation>] <type of operation>
-#  S3[<number of milliseconds>] <type of operation>
+# If you would like more detailed information about the requests going to
+# Amazon you can enabled timing information be setting the timing option to
+# true.
+#
+# Example output:
+# Dynamo[<number of milliseconds>,<cost in throughput of operation>] <type of operation>
+# S3[<number of milliseconds>] <type of operation>
+#
+# The 'compress' option controls whether the JSON blobs are stored using gzip
+# or not, it defaults to true. If this is disabled, there is a good chance that
+# your operations might be larger than the 64KB limit of dynamo, you have been
+# warned.

 util = require('util')
 async = require('async')
+zlib = require('zlib')

 defaultOptions =
   amazon_region: 'us-east-1'
   timing: false
+  compress: true
   s3_rw_concurrency: 1
   s3_ro_concurrency: 1
   snapshots_ro_concurrency: 1
@@ -97,6 +104,53 @@
       callback(error, results)
     )

+class BlobHandler
+  constructor: (@name, @encodeFunction, @decodeFunction, @logging) ->
+
+  encode: (data, callback) =>
+    start = new Date().getTime()
+    @encodeFunction data, (error, result) =>
+      elapsed = new Date().getTime() - start
+      console.log(@name+'Blob:Compressed['+data.length+','+result.length+','+elapsed+'ms]') if @logging
+      callback?(error, result)
+
+  decode: (data, callback) =>
+    start = new Date().getTime()
+    @decodeFunction data, (error, result) =>
+      elapsed = new Date().getTime() - start
+      console.log(@name+'Blob:Decompressed['+data.length+','+result.length+','+elapsed+'ms]') if @logging
+      callback?(error, result)
+
+class Compressor
+  constructor: (@compression, logging) ->
+    @gzip = new BlobHandler 'Gzip',
+      (data, callback) ->
+        zlib.gzip(new Buffer(data), callback)
+      (data, callback) ->
+        zlib.gunzip(data, callback)
+      logging
+
+    @base64 = new BlobHandler 'Base64',
+      (data, callback) ->
+        callback?(null, new Buffer(data, 'binary').toString('base64'))
+      (data, callback) ->
+        callback?(null, new Buffer(data, 'base64'))
+      logging
+
+  encode: (data, callback) =>
+    if @compression
+      @gzip.encode data, (error, result) =>
+        @base64.encode(result, callback)
+    else
+      callback?(null, data)
+
+  decode: (data, callback) =>
+    if @compression
+      @base64.decode data, (error, result) =>
+        @gzip.decode(result, callback)
+    else
+      callback?(null, data)
+
 module.exports = AmazonDb = (options) ->
   return new Db if !(this instanceof AmazonDb)

@@ -132,32 +186,43 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @create = (docName, docData, callback) ->
+    compressor = new Compressor(options['compress'], options['timing'])
     async.auto(
-      write_metadata: (cb) ->
+      compress_meta: (cb) ->
+        compressor.encode(JSON.stringify(docData.meta), cb)
+
+      write_metadata: ['compress_meta', (cb, results) ->
         request =
           TableName: snapshots_table,
           Item:
             doc: { S: docName },
             v: { N: docData.v.toString() },
-            meta: { S: JSON.stringify(docData.meta) },
+            meta: { S: results.compress_meta },
             type: { S: docData.type },
             created_at: { N: new Date().getTime().toString() }
           Expected:
             doc:
               Exists: false

+        # Mark a snapshot as being compressed
+        request.Item['c'] = { S: 't' } if options['compress']
+
         snapshots_rw_queue.push((c) ->
           db.putItem(request, c)
         , 'write Snapshot('+docName+'-'+docData.v+')', cb)
-
-      write_data: (cb) ->
+      ]
+
+      compress_data: (cb) ->
+        compressor.encode(JSON.stringify(docData.snapshot), cb)
+
+      write_data: ['compress_data', (cb, results) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
         headers = {}
-        data = JSON.stringify(docData.snapshot)

         s3_rw_queue.push((c) ->
-          s3.put(path, headers, data, c)
+          s3.put(path, headers, results.compress_data, c)
         , 'write Snapshot('+docName+'-'+docData.v+')', cb)
+      ]
     (error, results) ->
       if error?
         if error.message? and error.message.match 'The conditional request failed'
@@ -313,6 +378,18 @@
           s3.get('/'+snapshots_bucket+'/'+item.doc.S+'-'+item.v.N+'.snapshot', 'buffer', c)
         , 'query Snapshot('+item.doc.S+'-'+item.v.N+')', cb)
       ]
+
+      compressor: ['get_snapshot', 'get_data', (cb, results) ->
+        cb(null, new Compressor(results.get_snapshot.Items[0].c, options['timing']))
+      ]
+
+      snapshot: ['compressor', (cb, results) ->
+        results.compressor.decode(results.get_data.buffer.toString(), cb)
+      ]
+
+      meta: ['compressor', (cb, results) ->
+        results.compressor.decode(results.get_snapshot.Items[0].meta.S, cb)
+      ]
     (error, results) ->
       if error?
         if error == 'Document does not exist'
@@ -328,13 +405,13 @@
         item = results.get_snapshot.Items[0]

         try
-          snapshot = JSON.parse(results.get_data.buffer.toString())
+          snapshot = JSON.parse(results.snapshot)
         catch error
           snapshot = {}
           console.error('Failure: data was corrupt for Snapshot('+docName+'-'+item.v.N+')')

         try
-          meta = JSON.parse(item.meta.S)
+          meta = JSON.parse(results.meta)
         catch error
           meta = {}
           console.error('Failure: metadata was corrupt for Snapshot('+docName+'-'+item.v.N+')')
@@ -360,32 +437,43 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @writeSnapshot = (docName, docData, dbMeta, callback) ->
+    compressor = new Compressor(options['compress'], options['timing'])
     async.auto(
-      write_metadata: (cb) ->
+      compress_meta: (cb) ->
+        compressor.encode(JSON.stringify(docData.meta), cb)
+
+      write_metadata: ['compress_meta', (cb, results) ->
         request =
           TableName: snapshots_table,
           Item:
             doc: { S: docName },
             v: { N: docData.v.toString() },
-            meta: { S: JSON.stringify(docData.meta) },
+            meta: { S: results.compress_meta },
             type: { S: docData.type },
             created_at: { N: new Date().getTime().toString() }
           Expected:
             doc:
               Exists: false

+        # Mark a snapshot as being compressed
+        request.Item['c'] = { S: 't' } if options['compress']
+
         snapshots_rw_queue.push((c) ->
           db.putItem(request, c)
         , 'write Snapshot('+docName+'-'+docData.v+')', cb)
-
-      write_data: (cb) ->
+      ]
+
+      compress_data: (cb) ->
+        compressor.encode(JSON.stringify(docData.snapshot), cb)
+
+      write_data: ['compress_data', (cb, results) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
         headers = {}
-        data = JSON.stringify(docData.snapshot)

         s3_rw_queue.push((c) ->
-          s3.put(path, headers, data, c)
+          s3.put(path, headers, results.compress_data, c)
         , 'write Snapshot('+docName+'-'+docData.v+')', cb)
+      ]
     (error, results) ->
       if error?
         if error.message? and error.message.match 'The conditional request failed'
@@ -437,26 +525,37 @@
         callback?('Failed to fetch operations')
       else
         data = []
-        for operation in results.get_metadata.Items
-          try
-            op = JSON.parse(operation.op.S)
-          catch error
-            op = {}
-            console.error('Failure: data was corrupt for Operation('+docName+'-'+operation.v.N+')')
-
-          try
-            meta = JSON.parse(operation.meta.S)
-          catch error
-            meta = {}
-            console.error('Failure: metadata was corrupt for Operation('+docName+'-'+operation.v.N+')')
-
-          item = {
-            op: op
-            meta: meta
-          }
-          data.push(item)
-
-        callback? null, data
+        async.map(results.get_metadata.Items, (operation, cb) ->
+          compressor = new Compressor(operation.c?, options['timing'])
+          async.auto(
+            snapshot: (c) ->
+              compressor.decode(operation.op.S, c)
+
+            meta: (c) ->
+              compressor.decode(operation.meta.S, c)
+
+          (error, results) ->
+            try
+              op = JSON.parse(results.snapshot)
+            catch error
+              op = {}
+              console.error('Failure: data was corrupt for Operation('+docName+'-'+operation.v.N+')')
+
+            try
+              meta = JSON.parse(results.meta)
+            catch error
+              meta = {}
+              console.error('Failure: metadata was corrupt for Operation('+docName+'-'+operation.v.N+')')
+
+            item = {
+              op: op
+              meta: meta
+            }
+            cb(null, item)
+          )
+        (error, results) ->
+          callback? null, results
+        )
     )

   # Public: Write an operation to a document.
@@ -473,20 +572,30 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @writeOp = (docName, opData, callback) ->
+    compressor = new Compressor(options['compress'], options['timing'])
     async.auto(
-      write_metadata: (cb) ->
+      compress_op: (cb) ->
+        compressor.encode(JSON.stringify(opData.op), cb)
+
+      compress_meta: (cb) ->
+        compressor.encode(JSON.stringify(opData.meta), cb)
+
+      write_metadata: ['compress_op', 'compress_meta', (cb, results) ->
         request =
           TableName: operations_table,
           Item:
             doc: { S: docName },
             v: { N: opData.v.toString() },
-            op: { S: JSON.stringify(opData.op) },
-            meta: { S: JSON.stringify(opData.meta) },
+            op: { S: results.compress_op },
+            meta: { S: results.compress_meta },
+
+        # Mark a snapshot as being compressed
+        request.Item['c'] = { S: 't' } if options['compress']

         operations_rw_queue.push((c) ->
           db.putItem(request, c)
         , 'write Operation('+docName+'-'+opData.v+')', cb)
-
+      ]
     (error, results) ->
       if error?
         console.error('Failed to save Operation('+docName+'-'+opData.v+'): '+util.inspect(error))


update weixin

update weixin

--- core/core_modules/httpbind.coffee
+++ core/core_modules/httpbind.coffee
@@ -59,6 +59,7 @@
                 type = "all"
             if typeof callback isnt 'function'
                 throw new Error("callback is not a function")
+            @mew.logger.debug "#{@mew.name} bind http[#{type}] to #{path.toString()}"
             switch type
                 when "all"
                     @mew.brain.httpManager.app.all path,callback


CoffeeScript, Y U so silly?

CoffeeScript, Y U so silly?

--- assets/js/script.coffee
+++ assets/js/script.coffee
@@ -8,5 +8,5 @@
     $('.julian-day').tooltip
         title: ->
             jd = Number(this.textContent)
-            new Date((jd  - 2440587.5) * 86400000)
+            date = new Date((jd  - 2440587.5) * 86400000)
             date.toString()


Fix error in collections when destroy callback is missing

--- app/assets/javascripts/action_cable_notifications/collection.coffee
+++ app/assets/javascripts/action_cable_notifications/collection.coffee
@@ -135,7 +135,7 @@
       record = @data[index]
       if !@sync
         @data.splice(index, 1)
-        @callbacks.destroy.call(this, selector)
+        @callbacks?.destroy?.call(this, selector)

       upstream.call(this, "destroy", {id: record.id})
       record


Don't call trigger on remote page, unnecessary

--- core/app/backbone/client/facts_new_view.coffee
+++ core/app/backbone/client/facts_new_view.coffee
@@ -69,7 +69,6 @@
     persistentWheelView.on 'opinionSet', =>
       @ui.opinion_animation.hide()
       @closeOpinionHelptext()
-      parent?.remote?.trigger('opinionSet')

   renderShareNewFact: ->
     @factSharingOptions = new FactSharingOptions


Hopeful fix for weird date testing failures

--- tests/models/test_punch.coffee
+++ tests/models/test_punch.coffee
@@ -14,11 +14,13 @@
   new Project('#camp-fangamer')
 ]

+today = moment()
+
 describe 'Punch', ->
   describe '#parse(user, command, mode)', ->
     beforeEach ->
-      start = moment({hour:7,minute:0,second:0}).tz('America/New_York')
-      end = moment({hour:18,minute:0,second:0}).tz('America/New_York')
+      start = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 7}, 'America/New_York')
+      end = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 18}, 'America/New_York')
       timetable = new Timetable(start, end, moment.tz.zone('America/New_York'))
       timetable.setVacation(13, 0)
       timetable.setSick(5, 0)
@@ -189,8 +191,8 @@
       expect(punch.notes).to.be.empty
   describe '#out(punch)', ->
     beforeEach ->
-      start = moment('7:00 AM', 'hh:mm A').tz('America/New_York')
-      end = moment('6:00 PM', 'hh:mm A').tz('America/New_York')
+      start = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 7}, 'America/New_York')
+      end = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 18}, 'America/New_York')
       timetable = new Timetable(start, end, moment.tz.zone('America/New_York'))
       timetable.setVacation(13, 0)
       timetable.setSick(5, 0)
@@ -215,8 +217,8 @@
                                           'camp-fangamer'
   describe '#toRawRow(name)', ->
     beforeEach ->
-      start = moment().hour(7).tz('America/New_York')
-      end = moment().hour(18).tz('America/New_York')
+      start = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 7}, 'America/New_York')
+      end = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 18}, 'America/New_York')
       timetable = new Timetable(start, end, moment.tz.zone('America/New_York'))
       timetable.setVacation(13, 0)
       timetable.setSick(5, 0)
@@ -232,8 +234,8 @@
       expect(raw).to.exist
   describe '#assignRow', ->
     beforeEach ->
-      start = moment().hour(7).tz('America/New_York')
-      end = moment().hour(18).tz('America/New_York')
+      start = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 7}, 'America/New_York')
+      end = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 18}, 'America/New_York')
       timetable = new Timetable(start, end, moment.tz.zone('America/New_York'))
       timetable.setVacation(13, 0)
       timetable.setSick(5, 0)
@@ -258,8 +260,8 @@
       expect(punch.row).to.exist
   describe '#isValid(user)', ->
     beforeEach ->
-      start = moment().hour(7)
-      end = moment().hour(18)
+      start = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 7}, 'America/New_York')
+      end = moment.tz({year: today.year(), month: today.month(), date: today.date(), hour: 18}, 'America/New_York')
       timetable = new Timetable(start, end, moment.tz.zone('America/New_York'))
       timetable.setVacation(13, 0)
       timetable.setSick(5, 0)


Add partner_cities to special delayed scroll list

--- apps/collect/client.coffee
+++ apps/collect/client.coffee
@@ -117,7 +117,7 @@

   # Whenever params change, scroll to the top
   params.on 'change', ->
-    if _.keys(params.changedAttributes())[0] in ['major_periods', 'silent']
+    if _.keys(params.changedAttributes())[0] in ['major_periods', 'partner_cities', 'silent']
       delayedScroll()
     else
       $('html,body').animate { scrollTop: 0 }, 400


Remove mediator `trigger` and `off`, deprecate `on`.

--- src/chaplin/mediator.coffee
+++ src/chaplin/mediator.coffee
@@ -28,17 +28,19 @@

   # Mixin event methods from Backbone.Events,
   # create Publish/Subscribe aliases
-  mediator.subscribe   = mediator.on      = Backbone.Events.on
-  mediator.unsubscribe = mediator.off     = Backbone.Events.off
-  mediator.publish     = mediator.trigger = Backbone.Events.trigger
+  mediator.subscribe   = Backbone.Events.on
+  mediator.unsubscribe = Backbone.Events.off
+  mediator.publish     = Backbone.Events.trigger
+
+  # The `on` method should not be used,
+  # it is kept only for purpose of compatibility with Backbone.
+  mediator.on = mediator.subscribe

   # Initialize an empty callback list so we might seal the mediator later
   mediator._callbacks = null

   # Make properties readonly
-  utils.readonly mediator,
-    'subscribe', 'unsubscribe', 'publish',
-    'on', 'off', 'trigger'
+  utils.readonly mediator, 'subscribe', 'unsubscribe', 'publish', 'on'

   # Sealing the mediator
   # --------------------


Change _Universe to Universe

Change _Universe to Universe

--- admin/static/coffee/app.coffee
+++ admin/static/coffee/app.coffee
@@ -226,7 +226,7 @@
     window.last_update_tstamp = 0
     window.universe_datacenter = new Datacenter
         id: '00000000-0000-0000-0000-000000000000'
-        name: '_Universe'
+        name: 'Universe'

     # Load the data bootstrapped from the HTML template
     # reset_collections()


primary conversion for async

primary conversion for async

--- chrome/content/zotero-better-bibtex/debug-bridge.coffee
+++ chrome/content/zotero-better-bibtex/debug-bridge.coffee
@@ -56,12 +56,14 @@

 Zotero.BetterBibTeX.DebugBridge.methods.librarySize = -> Zotero.DB.valueQuery('select count(*) from items i where not i.itemID in (select d.itemID from deletedItems d)')

-Zotero.BetterBibTeX.DebugBridge.methods.exportToString = (translator, exportOptions) ->
-  translator = Zotero.BetterBibTeX.getTranslator(translator)
-  exportOptions ||= {}
-  return Zotero.BetterBibTeX.translate(translator, null, exportOptions)
+Zotero.BetterBibTeX.DebugBridge.methods.exportToString = (translator, displayOptions) ->
+  deferred = Q.defer()
+  Zotero.BetterBibTeX.translate(Zotero.BetterBibTeX.getTranslator(translator), null, displayOptions || {}, (result) ->
+    deferred.resolve(result)
+  )
+  return deferred

-Zotero.BetterBibTeX.DebugBridge.methods.exportToFile = (translator, exportOptions, filename) ->
+Zotero.BetterBibTeX.DebugBridge.methods.exportToFile = (translator, displayOptions, filename) ->
   translation = new Zotero.Translate.Export()

   file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile)
@@ -71,22 +73,26 @@
   translator = Zotero.BetterBibTeX.getTranslator(translator)
   translation.setTranslator(translator)

-  exportOptions ||= {}
-  exportOptions.exportFileData = false
-  translation.setDisplayOptions(exportOptions)
+  displayOptions ||= {}
+  displayOptions.exportFileData = false
+  translation.setDisplayOptions(displayOptions)

-  status = 'working'
-  translation.setHandler('done', (obj, worked) -> status = if worked then 'ok' else 'error')
+  deferred = Q.defer()
+  translation.setHandler('done', (obj, worked) ->
+    deferred.resolve(!!worked)
+  )
   translation.translate()
-  thread = Zotero.BetterBibTeX.threadManager.currentThread
-  while status == 'working'
-    thread.processNextEvent(true)

-  return (status == 'ok')
+  return deferred

 Zotero.BetterBibTeX.DebugBridge.methods.library = ->
   translator = Zotero.BetterBibTeX.getTranslator('BetterBibTeX JSON')
-  return JSON.parse(Zotero.BetterBibTeX.translate(translator, null, { exportNotes: true, exportFileData: false }))
+
+  deferred = Q.defer()
+  Zotero.BetterBibTeX.translate(translator, null, { exportNotes: true, exportFileData: false }, (result) ->
+    deferred.resolve(JSON.parse(result))
+  )
+  return deferred

 Zotero.BetterBibTeX.DebugBridge.methods.setPreference = (name, value) -> Zotero.Prefs.set(name, value)


Notification: notifications are not correctly shown bug is fixed

--- client/Main/CommonViews/notificationlistitemview.coffee
+++ client/Main/CommonViews/notificationlistitemview.coffee
@@ -1,5 +1,7 @@

 class NotificationListItem extends KDListItemView
+
+  JView.mixin @prototype

   activityNameMap =
     comment : "status."
@@ -41,6 +43,7 @@

     @activityPlot = new KDCustomHTMLView tagName: "span"
     @timeAgoView  = new KDTimeAgoView null, @getLatestTimeStamp @getData().dummy
+    @prepareData()

   fetchActors: ->
@@ -63,10 +66,10 @@
         origin   : @actors[0]

     .catch (err) ->
-      warn err
+      warn err.description

-  viewAppended: ->
+  prepareData: ->
     promises = []
     promises.push @fetchActors()
     promises.push @getActivityPlot()
@@ -75,8 +78,7 @@
       @setTemplate @pistachio()
       @template.update()
     .catch (err) ->
-      warn err
-
+      warn err.description

   pistachio:->
     """
@@ -90,7 +92,6 @@
         </footer>
       </div>
     """
-

   getLatestTimeStamp:->
     return @getData().updatedAt


PrivateMessage: add protections for item deletion events as well

--- client/Social/Activity/views/privatemessage/pane.coffee
+++ client/Social/Activity/views/privatemessage/pane.coffee
@@ -48,6 +48,11 @@
     @prependMessage message, @listController.getItemCount()

+  removeMessage: (message) ->
+    return  unless @messageMap[message.id]
+    super message
+
+
   loadMessage: (message) -> @appendMessage message, 0

@@ -95,6 +100,12 @@
   messageRemoved: (item, index) ->

     {data} = item
+
+    # return if there is not already a
+    # mapped message so that we won't
+    # try to remove the same item again.
+    return  unless @messageMap[data.id]
+
     delete @messageMap[data.id]

     prevSibling = @listController.getListItems()[index-1]


shortcuts: use kd.Controller instead of events.EventEmitter :honeybee:

--- client/app/lib/shortcuts/index.coffee
+++ client/app/lib/shortcuts/index.coffee
@@ -1,6 +1,5 @@
 Shortcuts      = require 'shortcuts'
 defaults       = require './config'
-events         = require 'events'
 _              = require 'underscore'
 kd             = require 'kd'
 traverse       = require 'traverse'
@@ -15,7 +14,7 @@

 module.exports =

-class ShortcutsController extends events.EventEmitter
+class ShortcutsController extends kd.Controller

   klass = this

@@ -28,15 +27,14 @@
   # This also exposes convenience proxy methods to the underlying _keyconfig_
   # instance, and persists the state of a keyconfig#Collection to the app storage.
   #
-  constructor: (opts={}) ->
-
-    super()
+  constructor: (options={}, data) ->
+

     @_store  = null
     @_buffer = null

-    if opts.shortcuts instanceof Shortcuts
-      @shortcuts = opts.shortcuts
+    if options.shortcuts instanceof Shortcuts
+      @shortcuts = options.shortcuts
     else
       @shortcuts = new Shortcuts _.keys(defaults).reduce (acc, key) ->
           acc[key] = defaults[key].data
@@ -44,6 +42,8 @@
         , {}

     @_flushBuffer()
+
+    super options, data

   # Prepares buffer for the next batch of changes.


payment: remove unused code

payment: remove unused code

moar nonsense string conversion :)

--- client/landing/app/MainApp/payment/vmproductview.coffee
+++ client/landing/app/MainApp/payment/vmproductview.coffee
@@ -2,8 +2,7 @@
   constructor: (options = {}, data) ->
     super options, data

-  formatCurrency: (cents) ->
-    "#{ ( cents / 100 ).toFixed(2) }"
+  formatCurrency: (cents) -> ( cents / 100 ).toFixed 2

   pistachio: ->
     """


ChatView: Handle collaboration not initialized.

--- client/ide/views/chat/chatview.coffee
+++ client/ide/views/chat/chatview.coffee
@@ -23,9 +23,8 @@

     KD.singletons.appManager.require 'Activity', @bound 'createPanes'

-    @once 'CollaborationStarted', =>
-      @loaderView.destroy()
-      @unsetClass 'loading'
+    @once 'CollaborationStarted',        @bound 'removeLoader'
+    @once 'CollaborationNotInitialized', @bound 'removeLoader'

   createLoader: ->
@@ -44,6 +43,12 @@
     @addSubView @loaderView

+  removeLoader: ->
+
+    @loaderView.destroy()
+    @unsetClass 'loading'
+
+
   createPanes: ->

     channel   = @getData()
@@ -57,7 +62,7 @@
     @on 'ReceivedClickElseWhere', @bound 'hide'

     @settingsPane.forwardEvents this, [
-      'CollaborationStarted', 'CollaborationEnded'
+      'CollaborationStarted', 'CollaborationEnded', 'CollaborationNotInitialized'
       'ParticipantJoined', 'ParticipantLeft'
     ]


Add associationCollection class for use by has_many

--- client/lanes/models/Collection.coffee
+++ client/lanes/models/Collection.coffee
@@ -147,9 +147,28 @@

 Lanes.Models.SubCollection = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.SubCollection, SubCollection )

-
 Lanes.Models.BasicCollection = Lanes.lib.MakeBaseClass(
     Lanes.Vendor.Ampersand.Collection.extend(Lanes.Vendor.Ampersand.USCollection), BasicCollection
 )

 Lanes.Models.Collection = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.RestCollection, ModelsCollection )
+
+
+
+## Override a few methods on the standard collection to ensure that
+# models are fetched correctly and have the fk set when they're created
+class Lanes.Models.AssociationCollection extends Lanes.Models.Collection
+    constructor: (models,options)->
+        @model = options.model
+        @associationFilter = options.filter
+        super
+
+    _prepareModel: (attrs, options={})->
+        model = super
+        model.set(@associationFilter)
+        model
+
+    fetch: (options)->
+        options.query ||= {}
+        _.extend(options.query, @associationFilter)
+        super(options)


restore invitation to free trial

restore invitation to free trial

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -2070,6 +2070,10 @@
         <span>
             <Space/> If you have any questions at all, email <HelpEmailLink /> immediately.
             <b>
+                <Space/> Contact us if you are purchasing a course subscription and need a short trial
+                to test things out first.<Space/>
+            </b>
+            <b>
                 <Space/> Customized course plans are available.<Space/>
             </b>
             If you do not see a plan that fits your needs,


Do not force the icon options to be strings

Signed-off-by: Jessica González <52329a27ea5670aeec0efeeb0773bf1b4f1affa1@missallsunday.com>

--- src/summernoteDrafts.coffee
+++ src/summernoteDrafts.coffee
@@ -38,7 +38,7 @@

       context.memo 'button.sDraftsSave', () ->
         button = ui.button
-          contents: if options.sDrafts.saveIcon and typeof options.sDrafts.saveIcon is 'string' then options.sDrafts.saveIcon else lang.save
+          contents: if options.sDrafts.saveIcon then options.sDrafts.saveIcon else lang.save
           tooltip: lang.save
           click: (e) ->
             e.preventDefault()
@@ -109,7 +109,7 @@

     context.memo 'button.sDraftsLoad', () ->
       button = ui.button
-        contents: if options.sDrafts.loadIcon and typeof options.sDrafts.loadIcon is 'string' then options.sDrafts.loadIcon else lang.load
+        contents: if options.sDrafts.loadIcon then options.sDrafts.loadIcon else lang.load
         tooltip: lang.load
         click: (e) ->
           e.preventDefault()


Pass vector test

Pass vector test

--- src/test/test_config.coffee
+++ src/test/test_config.coffee
@@ -59,7 +59,7 @@
     t.ok sprite = sprites.find_child(), "missing sprite"
     console.log "directin vector", sprite, sprite.get('d')

-    t.equal sprite.get('v').all(), [1,0], "sprite has direction vector"
+    t.equal sprite.get('v').all().toString(), "1,0", "sprite has direction vector"
     t.equal sprite.get('i'), 2.5, "sprite has position"
     t.equal sprite.get('v_i'), 1, "sprite has direction"
     sprite.call('step')


Add #affixInside member and default vals for other props.

--- src/traits/Affixable.coffee
+++ src/traits/Affixable.coffee
@@ -35,6 +35,19 @@
 module.exports = class Affixable extends Foundation

   ###*
+  # The axis to affix inside. By default, the affixable will be positioned
+  # outside the element it's affixed to.
+  #
+  # @member {string} affixInside
+  # @member stout-ui/trait/Affixable#
+  ###
+  @property 'affixInside',
+    default: ''
+    type: 'string'
+    values: ['x', 'y', 'both', '']
+
+
+  ###*
   # The position to render this Affixable, relative it it's `affixTo` target.
   # This should be a string of the form, "center center", "top center",
   # "top left", etc.
@@ -43,6 +56,7 @@
   # @memberof stout-ui/traits/Affixable#
   ###
   @property 'affixPosition',
+    default: 'top left'
     type: 'string'

@@ -56,6 +70,7 @@
   # @memberof stout-ui/traits/Affixable#
   ###
   @property 'affixTo',
+    default: 'body'
     set: (target) ->
       if isString target
         document.querySelector(target)
@@ -113,15 +128,30 @@
     xmargin = mleft + mright
     ymargin = mtop + mbottom

-    switch posX
-      when 'left'   then left = targetR.left - r.width - (mleft + mright)
-      when 'right'  then left = targetR.right
-      when 'center' then left = targetCenterX - r.width / 2 - xmargin / 2
-
-    switch posY
-      when 'top'    then top = targetR.top - r.height - ymargin
-      when 'bottom' then top = targetR.bottom
-      when 'center' then top = targetCenterY - r.height / 2 - ymargin / 2
+    centerX = targetCenterX - r.width / 2 - xmargin / 2
+    centerY = targetCenterY - r.height / 2 - ymargin / 2
+
+    if @affixInside is 'x' or @affixInside is 'both'
+      switch posX
+        when 'left'   then left = targetR.left - mleft
+        when 'right'  then left = targetR.right - r.width - (mleft + mright)
+        when 'center' then left = centerX
+    else
+      switch posX
+        when 'left'   then left = targetR.left - r.width - (mleft + mright)
+        when 'right'  then left = targetR.right
+        when 'center' then left = centerX
+
+    if @affixInside is 'y' or @affixInside is 'both'
+      switch posY
+        when 'top'    then top = targetR.top
+        when 'bottom' then top = targetR.bottom - r.height - ymargin
+        when 'center' then top = centerY
+    else
+      switch posY
+        when 'top'    then top = targetR.top - r.height - ymargin
+        when 'bottom' then top = targetR.bottom
+        when 'center' then top = centerY

     # Calculate booleans flags to indicate if affixible would be off-screen.
     offScreenLeft   = left + mleft < 0


Update node.coffee

Update node.coffee

--- src/urlhandlers/node.coffee
+++ src/urlhandlers/node.coffee
@@ -1,11 +1,13 @@
 uri = require 'url'
 fs = require 'fs'
 http = require 'http'
+https = require 'https'
 DOMParser = require('xmldom').DOMParser

 class NodeURLHandler
     @get: (url, headers, timeout, cb) ->
         url = uri.parse(url)
+        httpModule = if url.protocol is 'https:' then https else http
         if url.protocol is 'file:'
             fs.readFile url.pathname, 'utf8', (err, data) ->
                 return cb(err) if (err)
@@ -13,20 +15,20 @@
                 cb(null, xml)
         else
             data = ''
-            options =
-            	host: url.hostname
-            	path: url.path
-            	port: url.port
-            	headers: headers
-            req = http.get options, (res) ->
-            	res.on 'data', (chunk) ->
+            options =
+                host: url.hostname
+                path: url.path
+                port: url.port
+                headers: headers
+            req = httpModule.get options, (res) ->
+                res.on 'data', (chunk) ->
                     data += chunk
                 res.on 'end', ->
                     xml = new DOMParser().parseFromString(data)
                     cb(null, xml)
             req.setTimeout timeout, () ->
-            	cb('Request timeout')
+                cb('Request timeout')
             req.on 'error', (err) ->
-            	cb(err)
-
-module.exports = NodeURLHandler+                cb(err)
+
+module.exports = NodeURLHandler


couple more small tests for unary minus

couple more small tests for unary minus

--- tests.coffee
+++ tests.coffee
@@ -139,6 +139,18 @@
   console print a
   """
   "-1"
+
+  """
+  a = -(1)
+  console print a
+  """
+  "-1"
+
+  """
+  a = -(-1)
+  console print a
+  """
+  "1"

   # this is fairly perverse and we
   # reject it. We could make sense of this if the


jupyter: update about

jupyter: update about

--- src/smc-webapp/jupyter/about.cjsx
+++ src/smc-webapp/jupyter/about.cjsx
@@ -36,6 +36,19 @@
                 </Ansi>

+    render_features: ->
+

+
 Full support for multiple people simultaneously editing a notebook, including multiple cursors and document-wide user-aware undo and redo,

+
 TimeTravel showing detailed history of how a notebook was created

+
 Raw file edit mode (synchronized editing of underlying JSON ipynb file)

+
 Uniform font sizing

+
 Code folding

+
 Cleaner more modern look with buttons and menus that better reflect state

+
 Sophisticated handling of large output (throttling, windowing, backend buffering)

+
 Background capture of execution output even if no user has the notebook open in their browser

+
 A purely client-side notebook viewer for easily sharing your work publicly

+

+
     render: ->
         <Modal show={@props.about} bsSize="large" onHide={@close} >
             <Modal.Header closeButton>
@@ -48,20 +61,17 @@

-                    CoCalc Jupyter notebook is a complete rewrite by SageMath, Inc.
+                    CoCalc Jupyter notebook is a complete open source rewrite by SageMath, Inc.
                     of the classical Jupyter notebook client from
                     the Jupyter project.
                     CoCalc Jupyter notebook maintains full compatibility with the file format
                     and general look and feel of the classical notebook.
-                    It improves on the classical notebook
-                    in that it supports multiple simultaneous users, TimeTravel showing the history of
-                    how a notebook was created, document-wide user-aware undo, a raw file edit mode,
-                    a cleaner more modern look with buttons and menus that better reflect state,
-                    more sophisticated handling of large output, and background
-                    capturing of output even if no user has the notebook open in their browser.
+                    It improves on the classical notebook as follows:
+                    {@render_features()}
                     Some functionality of classical
                     extensions and widgets are not yet supported (if you need
-                    something, let us know).
+                    something, let us know), and of course some of the above is
+                    also available in classical Jupyter via extensions.

Server Information


webapp: improve info text for select-file actions

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -961,14 +961,14 @@
         checked = @props.checked_files?.size ? 0
         total = @props.listing.length
         style =
-            color      : '#999'
+            color      : COLORS.GRAY
             height     : '22px'
-            margin     : '3px 20px'
+            margin     : '5px 3px'

         if checked is 0

                 {"#{total} #{misc.plural(total, 'item')}"}
-
 -- Check an entry below to see options.

+
 — Click on the checkbox on the left of a file to copy, move, delete, download, etc.

         else


add an id to satisfy react 0.14 screen reader warning.

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

     render_public_file_info_popover : ->
-        <Popover title='This file is being shared publicly'>
+        <Popover title='This file is being shared publicly' id='public_share' >
             <span style={wordWrap:'break-word'}>
                 Description: {@props.public_data.description}
             </span>