Model merged - needs more tests

Model merged - needs more tests

--- www/src/theoricus/mvc/model.coffee
+++ www/src/theoricus/mvc/model.coffee
@@ -1,132 +1,188 @@
 ArrayUtil = require 'theoricus/utils/array_util'
 Binder = require 'theoricus/mvc/lib/binder'
 Fetcher = require 'theoricus/mvc/lib/fetcher'
-# Factory = require 'theoricus/core/factory'
+Factory = require 'theoricus/core/factory'

 module.exports = class Model extends Binder

-  # _fields = []
-  # _collection = []
+  @_fields     = []
+  @_collection = []

-  # # SETUP METHODS ############################################################
+  # SETUP METHODS ############################################################

-  # @rest=( host, resources )->
-  #   [resources, host] = [host, null] unless resources?
-  #   for k, v of resources
-  #     @[k] = @_build_rest.apply @, [k].concat(v.concat host)
+  @rest = ( host, resources ) ->

-  # @fields=( fields )->
-  #   @_build_gs key, type for key, type of fields
+    [resources, host] = [host, null] unless resources?
+
+    for k, v of resources
+      @[k] = @_build_rest.apply @, [k].concat(v.concat host)
+
+  @fields = ( fields ) ->
+    @_build_gs key, type for key, type of fields

   # # SETUP HELPERS ############################################################

-  # ###
-  # Build a static rest call for the given params
+  ###
+  Builds a method to fetch the given service.

-  # @param [String] key
-  # @param [String] method
-  # @param [String] url
-  # @param [String] domain
-  # ###
-  # @_build_rest=( key, method, url, domain )->
-  #   ( args... )->
-  #     if key is "read" and _collection.length
-  #       found = ArrayUtil.find _collection, {id: args[0]}
-  #       return found.item if found?
+  Notice the method is being returned inside a private scope
+  that contains all the variables needed to fetch the data.
+
+
+  @param [String] key
+  @param [String] method
+  @param [String] url
+  @param [String] domain
+  ###
+  @_build_rest = ( key, method, url, domain ) ->
+    # console.log 'building ->', key, method, url, domain
+
+    return call = ( args... ) ->
+
+      if domain? && domain.substring( domain.length - 1 ) is "/"
+        domain = domain.substring 0, domain.length - 1
+
+      #console.log 'calling -->', key, method, url, domain, args

-  #     if args.length
-  #       if (typeof args[args.length-1] is 'object')
-  #         data = args.pop()
-  #       else
-  #         data = ''
+      # when asking to read a registry, check if it was already loaded
+      # if so, return the cached entry
+      if key is "read" and @_collection.length
+        found = ArrayUtil.find @_collection, {id: args[0]}
+        return found.item if found?
+
+      # when calling a method, you can pass as last argument
+      # one object that will be sent as data during the "ajax call"
+      if args.length
+        if (typeof args[args.length-1] is 'object')
+          data = args.pop()
+        else
+          data = ''

-  #     while (/:\w+/.exec url)?
-  #       url = url.replace /:\w+/, args.shift() || null
+      # creating a new variable for request url in order to do the replacing
+      # logic from scratch every time the method is called
+      # for some "weird" reason without this "hack" the url would
+      # build on top of the last built url, resulting in wrong addresses
+      r_url = url

-  #     url = "#{domain}/#{url}".replace /\/\//g, '/' if domain?
-  #     @_request method, url, data
+      # You can set variables on the URL using ":variable"
+      # and they'll be replace by the args you pass.
+      #
+      # i.e.
+      # @rest
+      #     'all' : [ 'GET', 'my/path/to/:id.json' ]
+      #
+      # called as MyModel.all( 66 )
+      # will result in a call to "my/path/to/66.json"
+      #
+      while (/:[a-z]+/.exec r_url)?
+        r_url = url.replace /:[a-z]+/, args.shift() || null

-  # ###
-  # Builds local getters/setters for the given params
+      # if domain is specified we prepend to the url
+      r_url = "#{domain}/#{r_url}" if domain?

-  # @param [String] field
-  # @param [String] type
-  # ###
-  # @_build_gs=( field, type )->
-  #   _val = null
-
-  #   classname = ("#{@}".match /function\s(\w+)/)[1]
-  #   stype = ("#{type}".match /function\s(\w+)/)[1]
-  #   ltype = stype.toLowerCase()
-
-  #   getter=-> _val
-  #   setter=(value)->
-
-  #     switch ltype
-  #       when 'string' then f = (typeof value is 'string')
-  #       when 'number' then f = (typeof value is 'number')
-  #       else f = (value instanceof type)
-
-  #     if f
-  #       _val = value
-  #       @update field, _val
-  #     else
-  #       prop = "#{classname}.#{field}"
-  #       msg = "Property '#{prop}' must to be #{stype}."
-  #       throw new Error msg
-
-  #   Object.defineProperty @::, field, get:getter, set:setter
+      @_request method, r_url, data

-  # ###
-  # General request method
+  ###
+  General request method

-  # @param [String] method  URL request method
-  # @param [String] url   URL to be requested
-  # @param [Object] data  Data to be send
-  # ###
-  # @_request=( method, url, data='' )->
-  #   fetcher = new Fetcher
+  @param [String] method  URL request method
+  @param [String] url   URL to be requested
+  @param [Object] data  Data to be send
+  ###
+  @_request = ( method, url, data='' ) ->
+    console.log "[Model] request", method, url, data

-  #   req = $.ajax url:url, type: method, data: data
+    fetcher = new theoricus.mvc.lib.Fetcher

-  #   req.done ( data )=>
-  #     fetcher.loaded = true
+    req =
+      url  : url
+      type : method
+      data : data
+
+    # if url contains .json, sets dataType to json
+    # this theoritically helps firefox ( and perhaps other browsers )
+    # to deal with the request
+    req.dataType = 'json' if /\.json/.test( url )
+
+    req = $.ajax req

-  #     @_instantiate data, ( records )->
-  #       fetcher.records = records
-  #       fetcher.onload?( fetcher.records )
+    req.done ( data )=>
+      fetcher.loaded = true
+      fetcher.records = @_instantiate data
+      fetcher.onload?( fetcher.records )

-  #   req.error ( error )=>
-  #     fetcher.error = true
-  #     if fetcher.onerror?
-  #       fetcher.onerror error
-  #     else
-  #       throw error
+    req.error ( error )=>
+      fetcher.error = true
+      if fetcher.onerror?
+        fetcher.onerror error
+      else
+          console.log error
+

-  #   fetcher
+    fetcher

-  # ###
-  # Instantiate new Model instances passing items as initial data
+  ###
+  Builds local getters/setters for the given params

-  # @param [Object] data  Data to be parsed
-  # ###
-  # @_instantiate=( data, fn )->
-  #   classname = ("#{@}".match /function\s(\w+)/)[1]
-  #   records = []
-  #   for record in (data = [].concat data)
-  #     Factory.model classname, record, ( model )->
-  #       records.push model
+  @param [String] field
+  @param [String] type
+  ###
+  @_build_gs = ( field, type ) ->
+    _val = null

-  #       if records.length is data.length
-  #         _collection = _collection.concat records
+    classname = ("#{@}".match /function\s(\w+)/)[1]
+    stype = ("#{type}".match /function\s(\w+)/)[1]
+    ltype = stype.toLowerCase()

-  #         if records.length is 1
-  #           fn records[0]
-  #         else
-  #           fn records+    getter = -> _val
+    setter = (value) ->
+
+      switch ltype
+        when 'string' then f = (typeof value is 'string')
+        when 'number' then f = (typeof value is 'number')
+        else f = (value instanceof type)
+
+      if f
+        _val = value
+        @update field, _val
+      else
+        prop = "#{classname}.#{field}"
+        msg = "Property '#{prop}' must to be #{stype}."
+        throw new Error msg
+
+    Object.defineProperty @::, field, get:getter, set:setter
+
+
+
+  ###
+  Instantiate one Model instance for each of the items present in data.
+
+  And array with 10 items will result in 10 new models, that will be
+  cached into @_collection variable
+
+  @param [Object] data  Data to be parsed
+  ###
+  @_instantiate = ( data ) ->
+    Factory = theoricus.core.Factory
+    classname = ("#{@}".match /function\s(\w+)/)[1]
+    records = []
+    for record in [].concat data
+      model = (Factory.model classname, record)
+      records.push model
+
+    ###
+    When calling the rest service multiple times, the collection variable keeps
+    the old data and duplicate the recordset between a rest call and another one.
+    For now, just flush the old collection when instantiate a new model instance
+    ###
+    @_collection = []
+
+    @_collection = ( @_collection || [] ).concat records
+
+    return if records.length is 1 then records[0] else records


update jyanken comment

update jyanken comment

--- scripts/bjjyanken.coffee
+++ scripts/bjjyanken.coffee
@@ -1,8 +1,8 @@
 # Description:
-#   ミサワ　ネタ
+#   美女じゃんけん
 #
 # Commands:
-#   hubot まだある - get ミサワ ネタ
+#   hubot じゃんけん {ぐー|ちょき|ぱー}

 module.exports = (robot) ->
   robot.respond /じゃんけん[ |　](.*)/i, (msg) ->


Try to preserve EXIF data on resize

Try to preserve EXIF data on resize

--- components/ResizeBuffer-node.coffee
+++ components/ResizeBuffer-node.coffee
@@ -47,19 +47,36 @@
       inputBuffer.metadata (err, metadata) ->
         if err
           return callback err
-        inputBuffer
-        .resize width, height
-        .withMetadata()
-        .withoutEnlargement()
-        .toBuffer (err, outputBuffer, info) ->
-          if err
-            return callback err
-          originalWidth = metadata.width
-          resizedWidth = info.width
-          factor = originalWidth / resizedWidth
-          out.buffer.send outputBuffer
-          out.factor.send factor
-          do callback
+        # Try to preserve the same format, if there's EXIF
+        if metadata.exif?
+          inputBuffer
+          .resize width, height
+          .withMetadata()
+          .withoutEnlargement()
+          .toBuffer (err, outputBuffer, info) ->
+            if err
+              return callback err
+            originalWidth = metadata.width
+            resizedWidth = info.width
+            factor = originalWidth / resizedWidth
+            out.buffer.send outputBuffer
+            out.factor.send factor
+            do callback
+        else
+          inputBuffer
+          .resize width, height
+          .withMetadata()
+          .withoutEnlargement()
+          .toFormat('png')
+          .toBuffer (err, outputBuffer, info) ->
+            if err
+              return callback err
+            originalWidth = metadata.width
+            resizedWidth = info.width
+            factor = originalWidth / resizedWidth
+            out.buffer.send outputBuffer
+            out.factor.send factor
+            do callback
     catch err
       return callback err


asset image resize on window resize

asset image resize on window resize

--- components/article/test/view.coffee
+++ components/article/test/view.coffee
@@ -31,6 +31,7 @@
       @ArticleView.__set__ 'CurrentUser', fabricate 'user'
       @fillwidth = sinon.spy(@ArticleView::, 'fillwidth')
       @imgsFillContainer = sinon.spy(@ArticleView::, 'imgsFillContainer')
+      @setupMaxImageHeights = sinon.spy(@ArticleView::, 'setupMaxImageHeights')
       @resetImageSetPreview = sinon.spy(@ArticleView::, 'resetImageSetPreview')
       stubChildClasses @ArticleView, this,
         ['initCarousel']
@@ -183,6 +184,10 @@
       @view.refreshWindowSize()
       @fillwidth.callCount.should.be.above 1

+    it 'calls setupMaxImageHeights on artworks', ->
+      @view.refreshWindowSize()
+      @setupMaxImageHeights.callCount.should.be.above 1
+
   describe '#embedMobileHeight', ->

     it 'sets iframe height to desktop height on large screens', ->


more context → cell

more context → cell

--- editor.coffee
+++ editor.coffee
@@ -99,7 +99,7 @@
   Up: (cm) ->
     cur = cm.getCursor()
     if cur.line is 0
-      previous_context = context_at_offset cm.lead_cell, -1
+      previous_context = input_cell_at_offset cm.lead_cell, -1
       if previous_context?
         previous_context.editor.focus()
       else
@@ -109,7 +109,7 @@
   Down: (cm) ->
     cur = cm.getCursor()
     if cur.line is cm.lineCount() - 1
-      next_context = context_at_offset cm.lead_cell, 1
+      next_context = input_cell_at_offset cm.lead_cell, 1
       if next_context?
         next_context.editor.focus()
       else
@@ -117,7 +117,7 @@
     else
       CodeMirror.Pass
   'Shift-Up': (cm) ->
-    previous_context = context_at_offset cm.lead_cell, -1
+    previous_context = input_cell_at_offset cm.lead_cell, -1
     if previous_context?
       cm.setValue previous_context.editor.getValue()
       cm.setCursor(line: cm.lineCount() - 1)
@@ -132,11 +132,11 @@
   $document.empty()
   contexts = []

-context_at_offset = (context, offset) ->
-  index = contexts.indexOf context
+input_cell_at_offset = (cell, offset) ->
+  index = contexts.indexOf cell
   contexts[index + offset]

-get_available_context = ->
+get_available_input_cell = ->
   last = contexts[contexts.length - 1]
   if last?.is_clean()
     return last
@@ -149,7 +149,7 @@
   contexts.splice index, 1

 add_context = (code='') ->
-  cell = get_available_context()
+  cell = get_available_input_cell()
   if cell?
     cell.editor.setValue code
   else
@@ -281,7 +281,7 @@
     set_code: add_context
     run: run_in_available_context
     clear_output: -> clear_contexts()
-    previously_run: -> context_at_offset(input_cell, -1).editor.getValue()
+    previously_run: -> input_cell_at_offset(input_cell, -1).editor.getValue()
     hide_input: -> remove_cell input_cell
     value: (value) -> _lead_cli_value: value
     async: (fn) ->


SocialWorker: Tests: replace daisy in testhelper/index

--- workers/social/testhelper/index.coffee
+++ workers/social/testhelper/index.coffee
@@ -184,15 +184,15 @@
   userArray = []

   for i in [0...count]
-    queue.push ->
+    queue.push (next) ->
       JUser.createUser generateUserInfo(), (err, user_) ->
         expect(err).to.not.exist
         userArray.push user_
-        queue.next()
-
-  queue.push -> callback userArray
-
-  daisy queue
+        next()
+
+  queue.push (next) -> callback userArray
+
+  async.series queue

 expectAccessDenied = (caller, callee, args..., callback) ->


Indent HTML

Indent HTML

--- app.coffee
+++ app.coffee
@@ -106,38 +106,35 @@
   "get_register/*": (req, res) ->
     token = req.params[0]
     Auth.verifyRegistration(token)
-      .then (user) ->
-        res.send("""
-<!DOCTYPE html>
-
-<meta charset="utf-8">
-<title>Authentication Successful!</title>
-<link href='http://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
-
Success!
You can go back to Nitro to log in.

-
-          """)
-      .fail (err) ->
-        res.send """
-<!DOCTYPE html>
-
-<meta charset="utf-8">
-<title>Error!</title>
-<link href='http://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
-
Sorry, an error occured.
Please try again later.

-
-            """
+      .then (user) -> res.send("""
+        <!DOCTYPE html>
+
+        <meta charset="utf-8">
+        <title>Authentication Successful!</title>
+        <link href='http://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
+
Success!
You can go back to Nitro to log in.

+
+      """).fail (err) -> res.send """
+        <!DOCTYPE html>
+
+        <meta charset="utf-8">
+        <title>Error!</title>
+        <link href='http://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
+
Sorry, an error occured.
Please try again later.

+
+      """

   # -----


Make it more explicit a % is just for a single photo

--- app.coffee
+++ app.coffee
@@ -48,7 +48,7 @@
             sendSMS(sender, msg)
             return
         else if result.result
-            msg = "im like #{result.confidence}% turned on rn"
+            msg = "that pic makes me like #{result.confidence}% turned on rn"
             if result.confidence <= 15
                 msg += " :/"
             else if result.confidence >= 60


Sugar.js actually doesn't do what I want here.

--- app.coffee
+++ app.coffee
@@ -48,7 +48,7 @@

   # TODO: Dequeue player
   'close': (user) ->
-    userList.remove(user)
+    userList.remove((u) -> u == user)
     user.broadcast 'leave chatroom', user.toJSON()

   ###########


redis

redis

--- app.coffee
+++ app.coffee
@@ -11,6 +11,7 @@
 ejs = require 'ejs'
 CONFIG = require('./config').CONFIG
 server = require('http').Server(app)
+module.exports = server
 app.use session {
   store: new RedisStore host: CONFIG?.DB?.REDIS?.HOST
   port: CONFIG?.DB?.REDIS?.PORT


Debug mode is now enabled by passing --debug as an argument

--- app.coffee
+++ app.coffee
@@ -20,9 +20,12 @@
     next()

 # DebugMode
-DebugMode = on
+DebugMode = off
 app.__debug = ->
+  console.warn "\u001b[31mRunning in debug mode!\u001b[0m"
   DebugMode = on
+# Enable debug mode if passed as argument
+if "--debug" in process.argv then app.__debug()

 # GET and POST requests
 api =


Debugging

Debugging

--- app.coffee
+++ app.coffee
@@ -32,7 +32,7 @@

 app.configure 'development', ->
-  app.set 'connstr', APP_MONGODB_URL+'-dev'
+  app.set 'connstr', APP_MONGODB_URL
   app.use express.errorHandler
     dumpExceptions: true
     showStack: true
@@ -49,6 +49,8 @@
 db.on 'error', (console.error.bind console, 'connection error: ')
 db.on 'open', -> console.log 'Connected to '+(app.get 'connstr')
 mongoose.connect app.get 'connstr'
+
+console.log 'Connecting to: '+(app.get 'connstr')

 app.get '/', (req, res) -> res.render 'index.jade',
   title: 'Open Access Button'


fix setLocale to current request

fix setLocale to current request

--- app.coffee
+++ app.coffee
@@ -48,7 +48,7 @@
       supported = new locale.Locales(config.languages)
       currentLocal = new locale.Locales(req.query.lang).best(supported)
     res.locals.locale = currentLocal
-    req.setLocale = currentLocal
+    i18n.setLocale currentLocal
     next()
   app.locals
     __i: i18n.__


Allow serving of packages without version number

--- app.coffee
+++ app.coffee
@@ -191,7 +191,7 @@
 										currentVersion = { }
 										currentVersion.versionnumber = versionNumber
 										currentVersion.license = versionPackageXml.package.packageinformation[0].license?[0]
-
+
 										# the tar file is incorrectly named -> abort
 										if (currentVersion.versionnumber.replace new RegExp(' ', 'g'), '_') isnt path.basename versionFile, '.tar'
 											fileCallback "version number does not match file (#{currentVersion.versionnumber.replace ' ', '_'} != #{path.basename versionFile, '.tar'})"
@@ -210,7 +210,7 @@
 								do parsingFinished
 		, (err) ->
 			do updateWatcher
-
+
 			if err?
 				logger.log "crit", "Error reading package list: #{err}"
 				updating = no
@@ -226,11 +226,21 @@
 			updateTime = ((do lastUpdate.getTime) - updateStart) / 1e3
 			logger.log "info", "Finished update"
 			updating = no
-
+
 			# and finally call the callback
 			(callback true) if callback?

 app.all '/', (req, res) ->
+	host = config.basePath ? "#{req.protocol}://#{req.header 'host'}"
+
+	# redirect when ?packageName=com.example.wcf.test[&packageVersion=1.0.0_Alpha_15] was requested
+	if req.query?.packageName?
+		if req.query.packageVersion?
+			res.redirect 301, "#{host}/#{req.query.packageName}/#{req.query.packageVersion.replace (new RegExp ' ', 'g'), '_'}"
+		else
+			res.redirect 301, "#{host}/#{req.query.packageName}"
+		return
+
 	req.accepts 'xml'
 	res.type 'xml'

@@ -261,7 +271,7 @@
 			for versionNumber, version of _package.versions
 				writer.startElement 'version'
 				writer.writeAttribute 'name', versionNumber
-
+
 				# we do not support authentification
 				writer.writeAttribute 'accessible', "true"
 				writer.writeAttribute 'isCritical', if (/pl/i.test versionNumber) then "true" else "false"
@@ -279,10 +289,10 @@
 						do writer.endElement
 					do writer.endElement
 				writer.writeElement 'timestamp', String(Math.floor (do version.timestamp.getTime) / 1000)
-
+
 				# e.g. {{packageServerHost}}/com.example.wcf.test/1.0.0_Alpha_15
-				writer.writeElement 'file', "{{packageServerHost}}/#{_package.name}/#{versionNumber.replace(new RegExp(' ', 'g'), '_')}"
-
+				writer.writeElement 'file', "{{packageServerHost}}/#{_package.name}/#{versionNumber.replace (new RegExp ' ', 'g'), '_'}"
+
 				# try to extract license
 				if version.license
 					result = /^(.*?)(?:\s<(https?:\/\/.*)>)?$/.exec version.license
@@ -300,21 +310,37 @@
 		writer.writeComment "This list was presented by Tims Package Server #{serverVersion}"
 		do writer.endElement
 		do writer.endDocument
-	res.end (do writer.toString).replace /\{\{packageServerHost\}\}/g, config.basePath ? "#{req.protocol}://#{req.header 'host'}"
+	res.end (do writer.toString).replace /\{\{packageServerHost\}\}/g, host

 # package download requested
-app.all /^\/([a-z0-9_-]+\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+)\/([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)/i, (req, res) ->
+app.all /^\/([a-z0-9_-]+\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+)\/([0-9]+\.[0-9]+\.[0-9]+(?:_(?:a|alpha|b|beta|d|dev|rc|pl)_[0-9]+)?)\/?(?:\?.*)?$/i, (req, res) ->
 	res.attachment "#{req.params[0]}_#{req.params[1]}.tar"
-	res.sendfile "#{config.packageFolder}/#{req.params[0]}/#{req.params[1]}.tar"
+	res.sendfile "#{config.packageFolder}/#{req.params[0]}/#{req.params[1]}.tar", (err)  ->
+		if err?
+			res.statusCode = 404
+			do res.end
+
+# allow download without version number
+app.all /^\/([a-z0-9_-]+\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+)\/?(?:\?.*)?$/i, (req, res) ->
+	res.attachment "#{req.params[0]}.tar"
+	res.sendfile "#{config.packageFolder}/#{req.params[0]}/latest", (err) ->
+		if err?
+			res.statusCode = 404
+			do res.end

 # manual update via {{packageServerHost}}/update
 if config.enableManualUpdate
 	app.get '/update', (req, res) ->
 		logger.log "info", 'Manual update was requested'
 		readPackages ->
-			res.redirect 303, config.basePath ? '/'
+			res.redirect 303, config.basePath ? "#{req.protocol}://#{req.header 'host'}/"
+
+# throw 404 on any unknown route
+app.all '*', (req, res) ->
+	res.statusCode = 404
+	do res.end

 # Once the package list was successfully scanned once bind to the port
 readPackages ->
 	app.listen config.port
-	+


Fix error when deleting lamps without shadow.

--- engine/lamp.coffee
+++ engine/lamp.coffee
@@ -109,14 +109,14 @@
         if @context.is_webgl2
             fs = glsl100to300 fs
             fs_tex = glsl100to300 fs_tex, USE_TEXTURE: 1
-
+
         # regular shadows
         mat = new Material @context, @name+'_shadow', {
             fragment: fs, varyings, material_type: 'PLAIN_SHADER',
         }
         mat.is_shadow_material = true
         @_shadow_material = mat
-
+
         # clip alpha shadows with texture
         {blank_texture} = @context.render_manager
         mat = new Material @context, @name+'_alpha_shadow', {
@@ -148,6 +148,7 @@
         @shadow_fb = null
         @material?.destroy()
         @material = null
-        @shadow_texture.gl_tex = @context.render_manager.white_texture
+        @shadow_texture?.gl_tex = @context.render_manager.white_texture
+        return

 module.exports = {Lamp}


Group VMs by slug

Group VMs by slug

--- client/landing/app/MainApp/filetree/controllers/resourcescontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/resourcescontroller.coffee
@@ -14,9 +14,18 @@
     @reset()

   reset:->
+    cmp = (a, b)->
+      [groupA, vmA] = a.split('~')
+      [groupB, vmB] = b.split('~')
+      if groupA == groupB
+        vmA > vmB
+      else
+        groupA > groupB
+
     KD.singletons.vmController.resetVMData()
-    KD.singletons.vmController.fetchGroupVMs (err, vms)=>
+    KD.singletons.vmController.fetchVMs (err, vms)=>
       @removeAllItems()
+      vms.sort cmp
       @instantiateListItems vms  unless err
       @deselectAllItems()


Remove these for now, to affects anything. Will be fixed in another branch.

--- client/landing/app/MainApp/filetree/controllers/resourcescontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/resourcescontroller.coffee
@@ -65,7 +65,6 @@
     super options, vmName

     @vm = KD.getSingleton 'vmController'
-    @vm.on 'StateChanged', @bound 'checkVMState'

   viewAppended:->

@@ -103,8 +102,6 @@
     @addSubView @chevron = new KDCustomHTMLView
       tagName   : "span"
       cssClass  : "chevron"
-
-    @vm.info @getData(), @bound 'checkVMState'

   click:->
     KD.getSingleton("windowController").addLayer @delegate


If the client uses GAQ, track submissions on their account as well.

--- src/main/webapp/coffeescripts-hidden/load.coffee
+++ src/main/webapp/coffeescripts-hidden/load.coffee
@@ -20,6 +20,7 @@
 resourcesDomain = anchortab?.resourcesDomain || parser.hostname
 secureResourcesDomain = anchortab?.secureResourcesDomain || parser.hostname
 jqVersion = "1.9.1"
+clientUsesGaq = false

 window.anchortab = window.anchortab || {}
 window.anchortab.i18n = {}
@@ -83,6 +84,8 @@
     # After the script is loaded, we need to make sure to reassign our local
     # _gaq to the _gaq loaded by Google Analytics.
     loadScript analyticsSrc
+  else
+    clientUsesGaq = true

 isAcceptableJQuery = ->
   jQueryVersionPieces = jQuery.fn.jquery.split(".")
@@ -191,6 +194,9 @@
       gaqInfo = "Domain: " + document.domain
       _gaq.push ["at._trackEvent", "Submission", "Email Submitted", gaqInfo]

+      if clientUsesGaq
+        _gaq.push ["_trackEvent", "Anchor Tab Submission", "Email Submitted", gaqInfo]
+
       setTimeout ->
         $("#anchor-tab")
           .removeClass("visible")


Service filters: variable name changed to category_name, service_name variable added and custom_array added.

--- src/core/javascripts/controllers/service_list.js.coffee
+++ src/core/javascripts/controllers/service_list.js.coffee
@@ -20,8 +20,8 @@

   $scope.validator = ValidatorService

-  $scope.filters = {price: {}, name: null}
-
+  $scope.filters = {category_name: null, service_name: null, price: {}, custom_array_value: null}
+
   $scope.options = $scope.$eval($attrs.bbServices) or {}

   $scope.booking_item = $scope.$eval($attrs.bbItem) if $attrs.bbItem
@@ -182,14 +182,27 @@
   $scope.filterFunction = (service) ->
     if !service
       return false
-    else
-      return (!$scope.filters.name or service.category_id is $scope.filters.name.id) and (!service.price or
-        (service.price >= $scope.filters.price.min * 100 and service.price <= $scope.filters.price.max * 100 ))
+    $scope.service_array = []
+    $scope.custom_array = (match)->
+      if $scope.options.custom_filter
+        for item in service.extra[$scope.options.custom_filter]
+          if item is match
+            $scope.service_array.push service
+            return $scope.service_array
+      else
+        $scope.service_array = $scope.items
+        return $scope.service_array
+    return (!$scope.filters.category_name or service.category_id is $scope.filters.category_name.id) and
+      (!$scope.filters.service_name or service.name.includes($scope.filters.service_name)) and
+      (!$scope.filters.custom_array_value or $scope.custom_array($scope.filters.custom_array_value)) and
+      (!service.price or (service.price >= $scope.filters.price.min * 100 and service.price <= $scope.filters.price.max * 100 ))

   $scope.resetFilters = () ->
-    $scope.filters.name = null
+    $scope.filters.category_name = null
+    $scope.filters.service_name = null
     $scope.filters.price.min = null
     $scope.filters.price.max = null
+    $scope.filters.custom_array_value = null
     $scope.filterChanged()

   $scope.filterChanged = () ->


Fix mistype.

Fix mistype.

--- src/redux-axios-reducers.coffee
+++ src/redux-axios-reducers.coffee
@@ -126,7 +126,8 @@
     @default.data = []

   reduceSuccess: (state, action) ->
-    singleId = action.config.id or action.data.id
+    singleId = action.config.id or (
+      action.config.data and action.config.data.id)

     state.data = [] if action.config.method == 'get' and not singleId
     data = action.response.data


fixed issue 750

fixed issue 750

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -304,6 +304,19 @@
         text ?= "Last edit by #{@props.editor_name}"
         color ?= "#888"

+        if not @is_editing() and other_editors.size == 0
+            edit = "Last edit "
+            name = " by #{@props.editor_name}"
+

+                {edit}
+                <TimeAgo date={new Date(@props.message.get('history').peek()?.get('date'))} />
+                {name}
+

+        else
+

+                {text}
+

+
     show_user_name: ->

             {@props.sender_name}


Rename getScale to getLineItemScale to be more specific

--- app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee
+++ app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee
@@ -128,7 +128,7 @@
     $scope.selectedUnitsProduct = unitsProduct
     $scope.selectedUnitsVariant = unitsVariant

-  $scope.getScale = (lineItem) ->
+  $scope.getLineItemScale = (lineItem) ->
     if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume")
       VariantUnitManager.getScale(lineItem.units_variant.unit_value, lineItem.units_product.variant_unit)
     else
@@ -136,12 +136,12 @@

   $scope.sumUnitValues = ->
     sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
-      sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getScale(lineItem))
+      sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
     , 0

   $scope.sumMaxUnitValues = ->
     sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
-      sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getScale(lineItem))
+      sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem))
     , 0

   $scope.roundToThreeDecimals = (value) ->


steroids.chat.appgyver.com instead of direct ninchat.com

--- src/steroids/SupportChat.coffee
+++ src/steroids/SupportChat.coffee
@@ -7,7 +7,7 @@

   open: () ->
     encodedName = encodeURIComponent(@name)
-    chatURL = "https://ninchat.com/embed?name=#{encodedName}#/c/10opaevm008"
+    chatURL = "http://steroids.chat.appgyver.com?nickName=#{encodedName}"

     open(chatURL)


bugfix: show correct descuadre value when > 0

--- app/assets/javascripts/views/cierres_caja/cierre_caja_descuadre_cell_view.js.coffee
+++ app/assets/javascripts/views/cierres_caja/cierre_caja_descuadre_cell_view.js.coffee
@@ -14,7 +14,7 @@
     if @get("model.descuadre") > 0 # add a '+'
       i = text.indexOf(Bandango.currencyPrefix)
       j = i + Bandango.currencyPrefix.length
-      text = text.slice(i, j) + "+" + text.slice(j, text.length-1)
+      text = text.slice(i, j) + "+" + text.slice(j, text.length)
     text
   ).property("model.descuadre")


- [grunt] enable normalize for `grunt-webfotns` to work with svg icons of any size

--- tasks/grunt-webfont.coffee
+++ tasks/grunt-webfont.coffee
@@ -16,5 +16,8 @@
           baseClass: 'icon'
           classPrefix: 'icon--'
         stylesheet: 'scss'
+        # @note Normalize may yeild different results for different engines
+        #       https://github.com/sapegin/grunt-webfont/issues/222
+        normalize: true
         htmlDemo: false
         engine: 'node'


Close editor before opening new one, fix #915

--- src/resources/forms/orbeon/builder/resources/cell/label-hint-editor.coffee
+++ src/resources/forms/orbeon/builder/resources/cell/label-hint-editor.coffee
@@ -41,6 +41,9 @@
             # Update values in the DOM, without waiting for the server to send us the value
             setLabelHintHtml(isChecked)
             labelHintValue(newValue)
+            # Clean state
+            currentControl = null
+            currentLabelHint = null

     # Heuristic to close the editor based on click and focus events
     clickOrFocus = ({target}) ->
@@ -87,6 +90,8 @@

     # Click on label/hint
     $('.fb-main').on 'click', LabelHintSelector, ({currentTarget}) ->
+        # Close current editor, if there is one open
+        endEdit() if ! _.isNull(currentControl)
         currentLabelHint = $(currentTarget)
         # Find control for this label
         th = currentLabelHint.parents('th')


sourcemaps are broken

sourcemaps are broken

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -94,9 +94,9 @@
         .pipe(mold.transformSourcesRelativeTo('./src'))
         .pipe(source('index.js'))
         .pipe(buffer())
-        .pipe(sourcemaps.init(loadMaps: true))
+        #.pipe(sourcemaps.init(loadMaps: true))
         .pipe(if isDeploy(nodeEnv) then require('gulp-uglify')() else util.noop())
-        .pipe(sourcemaps.write('../maps'))
+        #.pipe(sourcemaps.write('../maps'))
         .pipe(gulp.dest('build/scripts'))
         .pipe(if lr then require('gulp-livereload')(lr) else util.noop())


scope

scope

--- app.coffee
+++ app.coffee
@@ -23,7 +23,7 @@
     timeLimit: 5
     action: '/ringonce'

-  do send_xml res, twiml
+  send_xml res, twiml

 app.post '/ringonce', (req, res) ->
   twiml = new twilio.TwimlResponse()
@@ -32,7 +32,7 @@
     timeout: 10
     timeLimit: 5
     action: '/ring'
-  do send_xml res, twiml
+  send_xml res, twiml

 app.post '/ring', (req, res) ->
   twiml = new twilio.TwimlResponse()


Fix OSX clipboard

Fix OSX clipboard

--- app.coffee
+++ app.coffee
@@ -1,4 +1,3 @@
-
 app = require 'app'
 BrowserWindow = require 'browser-window'
 path = require 'path-extra'
@@ -79,7 +78,45 @@
       'plugins': true
   # Default menu in v0.27.3
   if process.versions['electron'] >= '0.27.3'
-    mainWindow.setMenu null
+    if process.platform == 'darwin'
+      template = [
+        label: '程序'
+        submenu: [
+          label: '退出'
+          accelerator: 'Command+Q'
+          click: -> app.quit()
+        ]
+      ,
+        label: '编辑'
+        submenu: [
+          label: '撤销'
+          accelerator: 'Command+Z'
+          selector: 'undo:'
+        ,
+          label: '恢复'
+          accelerator: 'Shift+Command+Z'
+          selector: 'redo:'
+        ,
+          label: '剪切'
+          accelerator: 'Command+X'
+          selector: 'cut:'
+        ,
+          label: '复制'
+          accelerator: 'Command+C'
+          selector: 'copy:'
+        ,
+          label: '粘贴'
+          accelerator: 'Command+V'
+          selector: 'paste:'
+        ,
+          label: '全选'
+          accelerator: 'Command+A'
+          selector: 'selectAll:'
+        ]
+      ]
+      mainWindow.setMenu require('menu').buildFromTemplate(template)
+    else
+      mainWindow.setMenu null
   mainWindow.loadUrl "file://#{__dirname}/index.html"
   if process.env.DEBUG?
     mainWindow.openDevTools


Reverted change in route history for privateAuth

--- app.coffee
+++ app.coffee
@@ -52,7 +52,7 @@
 app.get '/api/mendeley/papers', api.mendeley.papers

 # redirect all others to the index (HTML5 history)
-app.get '*', routes.index
+app.get '*', routes.index unless app.get 'env' is 'staging'

 # Start server
 port = process.argv[2] or process.env.PORT or 3000;


add old routes

add old routes

--- app.coffee
+++ app.coffee
@@ -18,4 +18,8 @@
 app.use "/story", require "./routes/story"
 app.use "/section", require "./routes/section"
 app.use "/rss", require "./routes/rss"
+
+app.use "/sections", require "./routes/section"
+app.use "/index.php", require "./routes/index"
+app.use "/rss.xml", require "./routes/rss"
 module.exports = app


Attempt to fix Twilio validation

Attempt to fix Twilio validation

--- app.coffee
+++ app.coffee
@@ -9,7 +9,7 @@
 app.use BodyParser.urlencoded extended:true

 app.get '/sms', (req, res) ->
-    if !Twilio.validateExpressRequest(req, process.env.TWILIO_AUTHTOKEN)
+    if !Twilio.validateExpressRequest(req, process.env.TWILIO_AUTHTOKEN, url: "http://sextmachine.heroku.com/sms")
         res.status(403).send("Not a valid Twilio request")
         return


call sendMailToUser for alarm via mail

call sendMailToUser for alarm via mail

--- lib/alarm_manager.coffee
+++ lib/alarm_manager.coffee
@@ -1,11 +1,11 @@
 time = require 'time'
+CozyAdapter = require('jugglingdb-cozy-adapter')

 module.exports = class AlarmManager

     timeouts: {}

     constructor: (@timezone, @Alarm, @notificationhelper) ->
-
         # We load the existing alarms
         @Alarm.all (err, alarms) =>
             for alarm in alarms
@@ -23,30 +23,33 @@
             @removeAlarmCounter msg

     addAlarmCounter: (alarm) ->
-
         triggerDate = new time.Date(alarm.trigg)
         triggerDate.setTimezone(@timezone)
         now = new time.Date()
         now.setTimezone(@timezone)
-
         delta = triggerDate.getTime() - now.getTime()
-        #console.info delta
-        #console.info triggerDate.toString()
-        #console.info now.toString()
-        #console.info "#{delta} ??"
         if delta > 0

             console.info "Notification in #{delta/1000} seconds."
             @timeouts[alarm._id] = setTimeout((
                 () =>
-                    resource = if alarm.related? then alarm.related
+                    if alarm.action is "DISPLAY"
+                        resource = if alarm.related? then alarm.related
+                        else
+                            app: 'agenda'
+                            url: "/" #TODO go to the alarm itself
+
+                        @notificationhelper.createTemporary
+                            text: "Reminder: #{alarm.description}"
+                            resource: resource
                     else
-                        app: 'agenda'
-                        url: "/" #TODO go to the alarm itself
+                        data =
+                            from: "Cozy Agenda"
+                            subject: "[Cozy-Agenda] Reminder"
+                            content: "Reminder: #{alarm.description}"

-                    @notificationhelper.createTemporary
-                        text: "Reminder: #{alarm.description}"
-                        resource: resource
+                        CozyAdapter.sendMailToUser data, (error, response) ->
+                            console.info error if error?

                 ), delta)


add proper keyboard shortcuts

add proper keyboard shortcuts

--- _assets/js/_keyboard.coffee
+++ _assets/js/_keyboard.coffee
@@ -17,4 +17,7 @@

   Mousetrap.bind 'v i m', ->
     console.log 'did you mean: emacs'
+
+  Mousetrap.bind 'e m a c s', ->
+    console.log 'did you mean: vim'
   return


Don't consolidate blocks when removing the last character in one

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -54,15 +54,16 @@
     blockList = @blockList.removeObjectsInRange(range)

     if blockList.length
-      leftIndex = locationRange.index
-      rightIndex = leftIndex + 1
-      leftBlock = blockList.getObjectAtIndex(leftIndex)
-      rightBlock = blockList.getObjectAtIndex(rightIndex)
-
-      if leftBlock and rightBlock
-        blockList = blockList.
-          removeObjectAtIndex(rightIndex).
-          replaceObjectAtIndex(leftBlock.consolidateWith(rightBlock), leftIndex)
+      unless @locationRangeEndsAtEndOfBlock(locationRange)
+        leftIndex = locationRange.index
+        rightIndex = leftIndex + 1
+        leftBlock = blockList.getObjectAtIndex(leftIndex)
+        rightBlock = blockList.getObjectAtIndex(rightIndex)
+
+        if leftBlock and rightBlock
+          blockList = blockList.
+            removeObjectAtIndex(rightIndex).
+            replaceObjectAtIndex(leftBlock.consolidateWith(rightBlock), leftIndex)
     else
       blockList = new Trix.SplittableList [new Trix.Block]

@@ -206,5 +207,9 @@
   locationRangeFromPosition: (position) ->
     new Trix.LocationRange @blockList.findIndexAndOffsetAtPosition(position)

+  locationRangeEndsAtEndOfBlock: (locationRange) ->
+    {offset, index} = locationRange.end
+    offset is @getBlockAtIndex(index).getLength()
+
   toJSON: ->
     @blockList.toJSON()


Redirect to the new about/press page

Redirect to the new about/press page

--- apps/page/index.coffee
+++ apps/page/index.coffee
@@ -15,7 +15,7 @@
 app.get '/privacy', routes.vanityUrl('privacy')
 app.get '/past-privacy', routes.vanityUrl('past-privacy')
 app.get '/security', routes.vanityUrl('security')
-app.get '/press', routes.vanityUrl('press')
+app.get '/press', (req, res) -> res.redirect '/about/page/press'
 app.get '/conditions-of-sale', routes.vanityUrl('conditions-of-sale')
 app.get '/auction-info', routes.vanityUrl('auction-info')
 app.get '/embed-terms', routes.vanityUrl('embed-terms')


Add "select-with-keyboard" functionality to route-details-typeahead. Improve usability. Closes #215.

--- src/views/route-settings.coffee
+++ src/views/route-settings.coffee
@@ -83,6 +83,7 @@
             event.preventDefault()
             $('#route-details').toggleClass('settings-open')
             $('.bootstrap-datetimepicker-widget').hide()
+            $('#route-details').trigger "shown"

     class TransportModeControlsView extends base.SMItemView
         template: 'transport-mode-controls'
@@ -232,6 +233,9 @@
             options.templates.empty = (ctx) -> jade.template 'typeahead-no-results', ctx
             @$searchEl.typeahead null, [options]

+            @$searchEl.on 'keyup', (e) =>
+                $('.tt-suggestion:first-child').trigger('click') if e.keyCode is 13
+
             selectAddress = (event, match) =>
                 @commit = true
                 switch $(event.currentTarget).attr 'data-endpoint'
@@ -246,8 +250,9 @@
                 $inputEl: @$searchEl
                 selectionCallback: selectAddress

-            # # TODO figure out why focus doesn't work
-            @$searchEl.focus()
+            # Focus on search-elem when #route-details has been opened
+            $('#route-details').on "shown", =>
+                @$searchEl.attr('tabindex', -1).focus()

         _locationNameAndLocking: (object) ->
             name: @model.getEndpointName object


Small user friendly adjustment

Small user friendly adjustment

--- src/views/route-settings.coffee
+++ src/views/route-settings.coffee
@@ -322,12 +322,15 @@
             ev.stopPropagation()
             if !@editing
                 @editing = true
-                @render()
                 switch $(ev.currentTarget).attr 'data-endpoint'
                     when 'origin'
-                        @_getInput('.transit-start input').focus()
+                        # This is to make users life easier by focusin cursor
+                        # to the end of the line
+                        value = @_getOriginInputText()
+                        @_getInput('.transit-start input').focus().val value
                     when 'destination'
-                        @_getInput('.transit-end input').focus()
+                        value = @_getDestinationInputText()
+                        @_getInput('.transit-end input').focus().val value

         setTimeMode: (ev) ->
             ev.stopPropagation()


Remove unneeded fat arrows

Remove unneeded fat arrows

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -43,15 +43,15 @@
     @subscribe $(window), 'unload', ->
       atom.storeWindowDimensions()

-    @subscribeToCommand $(window), 'window:toggle-full-screen', => atom.toggleFullScreen()
+    @subscribeToCommand $(window), 'window:toggle-full-screen', -> atom.toggleFullScreen()

-    @subscribeToCommand $(window), 'window:close', => atom.close()
+    @subscribeToCommand $(window), 'window:close', -> atom.close()

     @subscribeToCommand $(window), 'window:reload', =>
       @reloadRequested = true
       atom.reload()

-    @subscribeToCommand $(window), 'window:toggle-dev-tools', => atom.toggleDevTools()
+    @subscribeToCommand $(window), 'window:toggle-dev-tools', -> atom.toggleDevTools()

     @subscribeToCommand $(document), 'core:focus-next', @focusNext

@@ -95,7 +95,7 @@
     bindCommandToAction('core:redo', 'redo:')
     bindCommandToAction('core:select-all', 'selectAll:')

-  openLink: (event) =>
+  openLink: (event) ->
     location = $(event.target).attr('href')
     if location and location[0] isnt '#' and /^https?:\/\//.test(location)
       shell.openExternal(location)


Consolidated some code in app_view

Consolidated some code in app_view

--- app/app_view.coffee
+++ app/app_view.coffee
@@ -14,7 +14,7 @@
     'router:dashboardCreate': 'createDashboard'
     'router:dashboardRetrieve': 'loadDashboard'
     'router:viewSavedDashboards': 'showSaved'
-    'router:index': 'showIndex'
+    'router:index': 'createDashboardView'

   initialize: ->
     @appHeader = new AppHeader
@@ -105,10 +105,6 @@
     @appFocusView = @dashboardView
     @render()

-  showIndex: =>
-    @appFocusView = @dashboardView
-    @render()
-
   showSaved: =>
     if typeof @savedListView is 'undefined'
       User.current?.on 'loaded-dashboards', =>


Fix issue #13

Fix issue #13

--- app/app_view.coffee
+++ app/app_view.coffee
@@ -24,6 +24,7 @@
     # Main area views. Switched out when appropriate.
     @dashboardView = new DashboardView
     User.currentUser().always =>
+
       User.current.on 'loaded-dashboards', =>
         @savedListView = new SavedList { collection: User.current.dashboards }

@@ -39,7 +40,9 @@

   createDashboard: =>
     @dashboardModel = new DashboardModel
-    @createDashboardView()
+    @dashboardModel.on 'change', =>
+      window.location.hash = "/#{@dashboardModel.id}"
+      @createDashboardView()

   loadDashboard: (id) =>
     @dashboardModel = new DashboardModel { id: id }


disallow double post.

disallow double post.

--- server/Translator.coffee
+++ server/Translator.coffee
@@ -73,6 +73,11 @@
       _.extend( @, _.pick( client, 'content', 'parent_id' ) )
       @author_id = user_id
       throw 'Need to be logged in to post.' unless @author_id?
+      already_replied =
+        parent_id: @parent_id,
+        author_id: @author_id
+      if @parent_id? and Posts.findOne( already_replied )?
+        throw 'Can\'t reply twice to the same post.'

       #Alert post author that his post was replied to
       Notifications.insert( {


Fix recharge move message

Fix recharge move message

--- server/attachment.coffee
+++ server/attachment.coffee
@@ -598,7 +598,7 @@
     battle.recordMove(id, battle.getMove("Recharge"))

   beforeMove: (battle, move, user, targets) =>
-    battle.message "#{user.name} needs to recharge!"
+    battle.message "#{user.name} must recharge!"
     return false

   endTurn: (battle) =>


background distributeSSHKeys

background distributeSSHKeys

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -184,12 +184,12 @@
           if err?
             console.warn "SSH key distribution error"
             err = null
-          box.installTool {user: user, toolName: attributes.tool}, (err) ->
-            if err?
-              console.warn err
-              return callback {500, error: "Error installing tool: #{err}"}
-            view = _.findWhere dataset.views, box: box.name
-            callback null, view
+        box.installTool {user: user, toolName: attributes.tool}, (err) ->
+          if err?
+            console.warn err
+            return callback {500, error: "Error installing tool: #{err}"}
+          view = _.findWhere dataset.views, box: box.name
+          callback null, view

 # Render login page
 app.get '/login/?', (req, resp) ->
@@ -439,7 +439,7 @@

 app.post '/api/:user/datasets/?', checkUserRights, (req, resp) ->
   user = req.user.effective
-  console.log "AFDASDASD POST dataset user", user
+  console.log "POST dataset user", user
   User.canCreateDataset user, (err, can) ->
     if err?
       console.log "USER #{user} CANNOT CREATE DATASET"
@@ -470,20 +470,20 @@
           if err?
             console.warn "SSH key distribution error"
             err = null
-          console.log "TOOL dataset.tool #{dataset.tool} body.tool #{body.tool}"
-          box.installTool {user: user, toolName: body.tool}, (err) ->
-            if err?
-              console.warn err
-              return resp.send 500, error: "Error installing tool: #{err}"
-            Dataset.findOneById dataset.box, req.user.effective.shortName, (err, dataset) ->
-              console.warn err if err?
-              resp.send 200, dataset
-              addView user, dataset,
-                tool: 'datatables-view-tool'
-                displayName: 'View in a table' # TODO: use tool object
-              , (err, view) ->
-                if err?
-                  console.warn "Error creating DT view: #{err}"
+        console.log "TOOL dataset.tool #{dataset.tool} body.tool #{body.tool}"
+        box.installTool {user: user, toolName: body.tool}, (err) ->
+          if err?
+            console.warn err
+            return resp.send 500, error: "Error installing tool: #{err}"
+          Dataset.findOneById dataset.box, req.user.effective.shortName, (err, dataset) ->
+            console.warn err if err?
+            resp.send 200, dataset
+            addView user, dataset,
+              tool: 'datatables-view-tool'
+              displayName: 'View in a table' # TODO: use tool object
+            , (err, view) ->
+              if err?
+                console.warn "Error creating DT view: #{err}"

 app.post '/api/:user/datasets/:dataset/views/?', checkUserRights, (req, resp) ->
   user = req.user.effective


Add missing error checks

Add missing error checks

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -276,6 +276,9 @@
   return next() if req.user.effective.shortName == req.params.user

   User.findByShortName req.params.user, (err, switchingTo) ->
+    if err?
+      return resp.send 500, error: "Unknown error #{err}"
+
     if switchingTo?.canBeReally and req.user.real.shortName in switchingTo.canBeReally
       next()
     else
@@ -342,7 +345,15 @@
     _.extend options, pageTitles.PageTitles[options.page]
     options.subnav ?= 'subnav'
     fs.readFile "client/template/#{options.subnav}.eco", (err, subnavTemplate) ->
+      if err?
+        console.warn "Template #{options.page} not found when rendering server side"
+        return resp.send 500, {error: "Template not found: #{err}"}
+
       fs.readFile "client/template/nav.eco", (err, navTemplate) ->
+        if err?
+          console.warn "Template #{options.page} not found when rendering server side"
+          return resp.send 500, {error: "Template not found: #{err}"}
+
         getSessionUsersFromDB req.user, (usersObj) ->
           resp.render 'index',
               title: options.title or 'ScraperWiki'
@@ -402,6 +413,10 @@
           view = _.findWhere dataset.views, box: box.name
           view.state = 'installed'
           dataset.save (err) ->
+            if err?
+              console.warn err
+              return callback {500, error: "Error installing tool: #{err}"}
+
             callback null, view

 switchUser = (req, resp) ->
@@ -422,6 +437,9 @@

 getToken = (req, resp) ->
   Token.find req.params.token, (err, token) ->
+    if err?
+      return resp.send 500, { error: "Error #{err}" }
+
     if token?.shortName
       return resp.send 200, { token: token.token, shortName: token.shortName }
     else
@@ -454,6 +472,9 @@
     if token?.shortName and req.body.password?
       # TODO: token expiration
       User.findByShortName token.shortName, (err, user) ->
+        if err?
+          return resp.send 500, error: "Unknown error: #{err}"
+
         if user?
           user.setPassword req.body.password, ->
             # Password successfully set!
@@ -572,6 +593,10 @@
       plan = result.subscription.plan
       console.log 'Subscribed to', plan.plan_code
       user.setAccountLevel plan.plan_code, (err) ->
+        if err?
+          resp.send 500, success: "Unknown error #{err}"
+          return
+
         if req.user?.effective
           req.user.effective = getSessionUser user
         req.session.save()
@@ -643,11 +668,19 @@
 listTools = (req, resp) ->
   #console.log "listTools real:", req.user.real.shortName, "effective:", req.user.effective.shortName
   Tool.findForUser req.user.effective.shortName, (err, tools) ->
+    if err?
+      resp.send 500, 'Unknown error #{err}'
+      return
+
     resp.send 200, tools

 postTool = (req, resp) ->
   body = req.body
   Tool.findOneByName body.name, (err, tool) ->
+    if err?
+      resp.send 500, 'Unknown error #{err}'
+      return
+
     isNew = not tool?
     if tool is null
       publicBool = (body.public is "true")
@@ -695,6 +728,10 @@

 updateUser = (req, resp) ->
   User.findByShortName req.user.real.shortName, (err, user) ->
+    if err?
+      resp.send 500, error: err
+      return
+
     console.log "updateUser body is", req.body
     # The attributes that we can set via this API.
     canSet = ['acceptedTerms', 'canBeReally', 'datasetDisplay']
@@ -843,6 +880,10 @@

 addSSHKey = (req, resp) ->
   User.findByShortName req.user.effective.shortName, (err, user) ->
+    if err?
+      resp.send 500, error: err
+      return
+
     if not req.body.key?
       return resp.send 400, error: 'Specify key'
     user.sshKeys.push req.body.key.trim()
@@ -855,6 +896,10 @@

 listSSHKeys = (req, resp) ->
   User.findByShortName req.user.effective.shortName, (err, user) ->
+    if err?
+      resp.send 500, error: err
+      return
+
     resp.send 200, user.sshKeys

 googleAnalytics = (req, resp, next) ->
@@ -964,11 +1009,19 @@
 switchContextIfRequiredAndAllowed = (req, resp, next) ->
   datasetID = req.params[0]
   Dataset.findOneById datasetID, (err, dataset) ->
+    if err?
+      resp.status 500
+      return
+
     if dataset
       if dataset.user == req.user.effective.shortName
         return next()
       else
         User.findByShortName dataset.user, (err, switchingTo) ->
+          if err?
+            resp.status 500
+            return
+
           if switchingTo
             if switchingTo?.canBeReally and req.user.real.shortName in switchingTo.canBeReally
               req.user.effective = getSessionUser switchingTo


whoops

whoops

--- resources/assets/coffee/_classes/store-supporter-tag.coffee
+++ resources/assets/coffee/_classes/store-supporter-tag.coffee
@@ -97,7 +97,7 @@
         username: data.username
         cardHtml: data.card_html

-    .fail (xhr, data, status) =>
+    .fail (xhr) =>
       @user = null
       if xhr.status == 401
         osu.popup osu.trans('errors.logged_out'), 'danger'


Don't show "supporter required" message when creating user page and user is already supporter. Resolves issue #180.

--- resources/assets/coffee/react/profile-page/user-page.coffee
+++ resources/assets/coffee/react/profile-page/user-page.coffee
@@ -41,11 +41,11 @@
         dangerouslySetInnerHTML:
           __html: Lang.get('users.show.page.description')

-      el 'p',
-        className: 'profile-page-new-content'
-        dangerouslySetInnerHTML:
-          __html: Lang.get('users.show.page.restriction_info')
-
+      if !@props.user.isSupporter
+        el 'p',
+          className: 'profile-page-new-content'
+          dangerouslySetInnerHTML:
+            __html: Lang.get('users.show.page.restriction_info')

   pageShow: =>
     el 'div', dangerouslySetInnerHTML:


Fix opts not passed to buildJsPath

Fix opts not passed to buildJsPath

--- services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -152,7 +152,7 @@
 				path = Path.join(jsPath, jsFile)
 				return "#{Settings.webpack.host}:#{Settings.webpack.port}/public#{path}"
 			else
-				return res.locals.buildJsPath(jsFile)
+				return res.locals.buildJsPath(jsFile, opts)

 		res.locals.buildCssPath = (cssFile, opts)->
 			path = Path.join("/stylesheets/", cssFile)


use KodingError in socialapi.coffee

use KodingError in socialapi.coffee

--- workers/social/lib/social/cache/socialapi.coffee
+++ workers/social/lib/social/cache/socialapi.coffee
@@ -2,6 +2,7 @@
 SocialChannel  = require '../models/socialapi/channel'
 SocialMessage  = require '../models/socialapi/message'
 JAccount       = require '../models/account'
+KodingError    = require '../error'

 module.exports = (options = {}, callback) ->
@@ -51,7 +52,7 @@
     # fetch channel first
     SocialChannel.byName client, { name: channelName }, (err, data) ->
       return cb err if err
-      return cb { message: 'channel is not set' } unless data?.channel
+      return cb new KodingError 'channel is not set'  unless data?.channel

       # then fetch activity with channel id
       SocialChannel.fetchActivities client, { id: data.channel.id, sessionToken }, cb
@@ -59,7 +60,7 @@
   fetchProfileFeed = (client, params, cb) ->
     JAccount.one { 'profile.nickname': entryPoint.slug }, (err, account) ->
       return cb err if err
-      return cb { message: 'account not found' } unless account
+      return cb new KodingError 'account not found'  unless account
       SocialChannel.fetchProfileFeed client, { targetId: account.socialApiId }, cb

   fetchActivitiesForNavigatedURL = (params, cb) ->
@@ -69,7 +70,7 @@
       when 'Topic'           then fetchChannelActivities params.slug, cb
       when 'Message'         then SocialChannel.fetchActivities client, { id: params.slug, sessionToken }, cb
       when 'Post'            then SocialMessage.bySlug client, { slug: params.slug }, cb
-      when 'Announcement'    then cb { message: 'announcement not implemented' }
+      when 'Announcement'    then cb new KodingError 'announcement not implemented'
       else fetchGroupActivities cb

   reqs = [


fix autocomplete test

fix autocomplete test

--- spec/javascripts/lib/components/autocomplete_spec.js.coffee
+++ spec/javascripts/lib/components/autocomplete_spec.js.coffee
@@ -561,7 +561,8 @@
         describe 'creating a basic link item', ->
           testingMap =
             title: 'title',
-            uri: 'uri'
+            uri: 'uri',
+            type: 'type'

           newConfig = {id: 'my_search', uri: '/search', map: testingMap}

@@ -577,7 +578,9 @@
             anchor = item.childNodes[0]
             expect(anchor.tagName).toBe 'A'
             expect(anchor.getAttribute('href')).toBe SEARCH_RESULTS[0].uri
-            expect(anchor.className).toBe 'autocomplete__result__link'
+
+            expect(anchor.className).toBe 'autocomplete__result__link icon--white--before icon--place--pin--before autocomplete__result__typed'
+            console.log(anchor)

           it 'should contain an anchor tag containing the highlighted search term', ->
             anchor = item.childNodes[0]


Remove debug code.

Remove debug code.

--- app/client/js/routes.coffee
+++ app/client/js/routes.coffee
@@ -11,7 +11,7 @@
   constructor: ->
     super()
     @home = new este.Route '/', Routes.MSG_HOME
-    @newSong = new este.Route '/songs/new/:operation?', Routes.MSG_NEW_SONG
+    @newSong = new este.Route '/songs/new', Routes.MSG_NEW_SONG

     @list = [
       @home


fix run-now icon

fix run-now icon

--- SingularityUI/app/models/Request.coffee
+++ SingularityUI/app/models/Request.coffee
@@ -31,9 +31,9 @@
         data.deleted = data.state is 'DELETED'

         data.hasActiveDeploy = data.activeDeploy? or data.requestDeployState?.activeDeploy?
-        data.canBeRunNow = data.state is 'ACTIVE' and (data.scheduled or data.onDemand) and data.hasActiveDeploy
+        data.canBeRunNow = data.state is 'ACTIVE' and data.type in ['SCHEDULED', 'ON_DEMAND'] and data.hasActiveDeploy
         data.canBeBounced = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN']
-        data.canBeScaled = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN'] and data.hasActiveDeploy and data.daemon
+        data.canBeScaled = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN'] and data.hasActiveDeploy and data.type in ['WORKER', 'SERVICE']

         data


Ignore errors fetching racks and scale anyway

--- SingularityUI/app/models/Request.coffee
+++ SingularityUI/app/models/Request.coffee
@@ -271,11 +271,18 @@
                 @callScale scaleData.data, scaleData.bounce, scaleData.incremental, scaleData.message, scaleData.duration, scaleData.callback

     checkScaleEvenNumberRacks: (data, bounce, incremental, message, duration, callback) =>
-        return 0 unless @attributes.request.rackSensitive
-        if not @racks
-            @racks = new Racks []
-            @racks.fetch { async: false }
-        return data.instances %% @racks.length
+        mod = data.instances %% @racks.length
+        if mod
+            @promptScaleEvenNumberRacks
+                callback: callback
+                data: data
+                mod: mod
+                bounce: bounce
+                incremental: incremental
+                message: message
+                duration: duration
+        else
+            @callScale data, bounce, incremental, message, duration, callback

     promptScale: (callback, stuff) =>
         vex.dialog.open
@@ -303,16 +310,16 @@
                 message = $('.vex #scale-message').val()
                 duration = $('.vex #scale-expiration').val()
                 if !duration or (duration and @_validateDuration(duration, @promptScale, callback))
-                    mod = @checkScaleEvenNumberRacks data, bounce, incremental, message, duration, callback
-                    if mod
-                        @promptScaleEvenNumberRacks
-                            callback: callback
-                            data: data
-                            mod: mod
-                            bounce: bounce
-                            incremental: incremental
-                            message: message
-                            duration: duration
+                    if @attributes.request.rackSensitive
+                        if @racks
+                            @checkScaleEvenNumberRacks data, bounce, incremental, message, duration, callback
+                        else
+                            @racks = new Racks []
+                            @racks.fetch
+                                success: () => @checkScaleEvenNumberRacks data, bounce, incremental, message, duration, callback
+                                error: () =>
+                                    app.caughtError() # Since we scale anyway, don't show the error
+                                    @callScale data, bounce, incremental, message, duration, callback
                     else
                         @callScale data, bounce, incremental, message, duration, callback


initialize properties in the constructor

Former-commit-id: b3c9d96e7d7eb46974e5439af5139bcfbeb2d21c [formerly 3f97aa5785e49e124745ff982c133c3bbd3ace09]
Former-commit-id: f52725d8564bb8ceec3115dda9d96a9f9811008f

--- js/seq.coffee
+++ js/seq.coffee
@@ -1,8 +1,8 @@
 define [], ->
   class Seq

-    meta = {}
     constructor: (@seq, @name, @id) ->
+      meta = {}

     # Returns the reverse complement sequence.
     #reverse_complement: ->


Add comments

Add comments

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -1502,7 +1502,7 @@
                 @.$('.loading_query_img').css 'display', 'none'
                 @results_view.render_error(@query, err)

-        # Function that execute the query
+        # Function that execute the queries in a synchronous way.
         execute_query: =>
             # The user just executed a query, so we reset cursor_timed_out to false
             @saved_data.cursor_timed_out = false
@@ -1514,12 +1514,10 @@
             @raw_query = @codemirror.getValue()
             @query = @replace_new_lines_in_query @raw_query # Save it because we'll use it in @callback_multilples_queries

-            # Display the loading gif
-
             # Execute the query
             try
                 # Separate queries
-                @non_rethinkdb_query = '' # Store the non statements that don't return a rethinkdb query (like "var a = 1;")
+                @non_rethinkdb_query = '' # Store the statements that don't return a rethinkdb query (like "var a = 1;")
                 @index = 0 # index of the query currently being executed
                 @queries = @separate_queries @query
                 @raw_queries = @separate_queries @raw_query
@@ -1536,6 +1534,7 @@
                 @.$('.loading_query_img').hide()
                 @results_view.render_error(@query, err)

+        # A portion is one query of the whole input.
         execute_portion: =>
             @saved_data.cursor = null
             while @queries[@index]?
@@ -1573,6 +1572,8 @@
                             last_non_query: true
                         @results_view.render_error(@raw_queries[@index-1], error)

+        # Create a callback for when a query returns
+        # We tag the callback to make sure that we display the results only of the last query executed by the user
         generate_rdb_global_callback: (id_execution) =>
             rdb_global_callback = (error, cursor) =>
                 if @id_execution is id_execution # We execute the query only if it is the last one
@@ -1620,6 +1621,8 @@

             return rdb_global_callback

+        # Create a callback used in cursor.next()
+        # We tag the callback to make sure that we display the results only of the last query executed by the user
         generate_get_result_callback: (id_execution) =>
             get_result_callback = (error, data) =>
                 if @id_execution is id_execution
@@ -1655,6 +1658,8 @@

             return get_result_callback

+        # Evaluate the query
+        # We cannot force eval to a local scope, but "use strict" will declare variables in the scope at least
         evaluate: (query) =>
             "use strict"
             return eval(query)
@@ -1733,8 +1738,8 @@
                         stack.push char
                     else if char of @stop_char.closing
                         if stack[stack.length-1] isnt @stop_char.closing[char]
-                            #TODO Give back line and char
-                            throw "Syntax error, missing opening brackets for #{char}"
+                            #TODO Give back line and char?
+                            throw "Syntax error, missing opening bracket for #{char}"
                         else
                             stack.pop()
                     else if char is ';' and stack.length is 0


-turbolinks

-turbolinks

--- app/assets/javascripts/enjoy/cms.coffee
+++ app/assets/javascripts/enjoy/cms.coffee
@@ -1,7 +1,7 @@
 #= require jquery
 #= require jquery_ujs

-#= require turbolinks
+# require turbolinks

 #= require ../head.load.js
 #= require ../jquery.placeholder.js


Delete forums.js.coffee

Delete forums.js.coffee

--- app/assets/javascripts/forums.js.coffee
+++ app/assets/javascripts/forums.js.coffee
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/


Prevent initial sorting and allow to disable sorting all together

--- app/assets/javascripts/istart.js.coffee
+++ app/assets/javascripts/istart.js.coffee
@@ -36,6 +36,8 @@
             localStorage.setItem 'DataTables_'+window.location.pathname, JSON.stringify(oData)
           "fnStateLoad": (oSettings) ->
             JSON.parse localStorage.getItem('DataTables_'+window.location.pathname)
+          "bSort": !table.data("datatable-sort-no")?
+          "aaSorting": []
           "aoColumnDefs": [
             { "bSortable": false, "aTargets": [ "sort-no" ] },
             { "sType": "string",  "aTargets": [ "sort-as-string" ] },


populate placeholders on load

populate placeholders on load

--- app/assets/javascripts/legacy.js.coffee
+++ app/assets/javascripts/legacy.js.coffee
@@ -1,16 +1,23 @@
 jQuery ($) ->
   # For missing placeholder support (IE9)
   unless 'placeholder' in document.createElement('input')
-    $('.malmo-form [placeholder]').focus ->
+    $('.malmo-form input[placeholder]').focus ->
       if $(@).val() is $(@).attr('placeholder')
         $(@).val('')
         $(@).removeClass('placeholder')
     .blur ->
-      if $(@).val() is '' or $(@).val() is $(@).attr('placeholder')
-        $(@).addClass('placeholder')
-        $(@).val($(@).attr('placeholder'))
+      setPlaceholder(this)

-    $('[placeholder]').parents('form').submit ->
-      $(@).find('[placeholder]').each ->
+    setPlaceholder = (self) ->
+      if $(self).val() is '' or $(self).val() is $(self).attr('placeholder')
+        $(self).addClass('placeholder')
+        $(self).val($(self).attr('placeholder'))
+
+    # Set on load
+    $('input[placeholder]').each ->
+      setPlaceholder(@)
+
+    $('input[placeholder]').parents('form').submit ->
+      $(@).find('input[placeholder]').each ->
         if $(@).val() is $(@).attr('placeholder')
           $(@).val('')


implement oneOf for objects

implement oneOf for objects

--- index_.coffee
+++ index_.coffee
@@ -131,6 +131,10 @@

   # type = object
   object: (schema, depth = 0) ->
+    if schema.oneOf
+      oneof_schema = type: schema.oneOf
+      return @oneOf oneof_schema, depth
+
     o = {}
     for key, prop of schema.properties
       continue  unless @options.method is 'all' or (type(schema.required) is 'array' and key in schema.required)


As the number increases, increment it by smaller steps

--- app/assets/javascripts/static.js.coffee
+++ app/assets/javascripts/static.js.coffee
@@ -2,8 +2,8 @@
 # All this logic will automatically be available in application.js.
 # You can use CoffeeScript in this file: http://coffeescript.org/

-roundUpToThousand = (value) ->
-  return 1000 * Math.ceil(value / 1000)
+roundUpBy = (value, round_to) ->
+  return round_to * Math.ceil(value / round_to)

 $ ->
   animateMetric = $(".metric-box strong").each (index) ->
@@ -18,6 +18,11 @@

     tween.addEventListener 'change', (event) ->
       # if the number is under 90 of the target, increment number by thousands
-      $this.counter = roundUpToThousand($this.counter) if $this.counter < $target_count * 0.9
+      if $this.counter < $target_count * 0.7
+        $this.counter = roundUpBy($this.counter, 1000)
+      else if $this.counter < $target_count * 0.8
+        $this.counter = roundUpBy($this.counter, 100)
+      else if $this.counter < $target_count * 0.99
+        $this.counter = roundUpBy($this.counter, 10)
       $this.text(Math.round($this.counter).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","))
       return


add bearing to movement sync

add bearing to movement sync

--- app/assets/javascripts/workspace.coffee
+++ app/assets/javascripts/workspace.coffee
@@ -161,7 +161,7 @@

   setMoveEndHandler: () =>
     @map.on 'moveend', =>
-      @remote.broadcast('move', { center: @map.getCenter(), zoom: @map.getZoom() })
+      @remote.broadcast('move', { center: @map.getCenter(), zoom: @map.getZoom(), bearing: @map.getBearing() })

   moveTo: (data, remoteCmd = false) =>
     @map.off('moveend')


Map view by state now scaled, zooms in on smaller states and out on larger ones.


Temporary fix for district rep rendering problem (Issue #21)

--- app/scripts/controllers/district.coffee
+++ app/scripts/controllers/district.coffee
@@ -32,15 +32,16 @@
       else console.log "Error: ", error

     $scope.setDistrictData = (newVals, oldVals) ->
+      #TODO: See about reimplementing the behavior below or caching another way.
       # Old and new values are passed in to see if there has been a change in state.
-      if newVals.state is oldVals.state
-        $scope.district_reps.pop()
-        ApiGet.congress "legislators?state=#{$scope.state_district.state}&district=#{$scope.state_district.district}", (error, data) ->
-          if not error
-            $scope.district_reps.push(data[0])
-          else console.log "Error: ", error
-          , this
-      else
+      # if newVals.state is oldVals.state
+      #   $scope.district_reps.pop()
+      #   ApiGet.congress "legislators?state=#{$scope.state_district.state}&district=#{$scope.state_district.district}", (error, data) ->
+      #     if not error
+      #       $scope.district_reps.push(data[0])
+      #     else console.log "Error: ", error
+      #     , this
+      # else
         $scope.district_reps = []
         ApiGet.congress "legislators?state=#{$scope.state_district.state}&title=Sen", (error, data) ->
           if not error


nodes are 'g' elements

nodes are 'g' elements

--- layout.coffee
+++ layout.coffee
@@ -59,14 +59,14 @@

     node = g.selectAll('.node')
         .data(nodes)
-      .enter().append('circle')
+      .enter().append('g')
         .attr('class', 'node')
+
+    node.append('circle')
         .attr('r', 5)
         .style('fill', (d) -> color(d.group))
-        .call(force.drag)
-
-    node.append('title')
-        .text((d) -> d.name)
+        .append('title')
+          .text((d) -> d.name)

     force.on 'tick', ->
       link.attr('x1', (d) -> d.source.x)
@@ -74,7 +74,7 @@
           .attr('x2', (d) -> d.target.x)
           .attr('y2', (d) -> d.target.y)

-      node.attr('cx', (d) -> d.x)
+      node.attr('transform', (d) -> "translate(#{d.x},#{d.y})")
           .attr('cy', (d) -> d.y)


resurrect some code

resurrect some code

--- js/app.coffee
+++ js/app.coffee
@@ -122,6 +122,9 @@
           "&filters%5B0%5D%5Bcomparison%5D=contains&filters%5B0%5D%5Bvalue%5D=" +
           path[1]

+  refreshBuildState: () =>
+    VM.loadProjects()
+    VM.selected().refresh_fn() if VM.selected().refresh_fn
   loadProjects: () =>
     $.getJSON '/api/v1/projects', (data) =>
       projects = (new CI.inner.Project d for d in data)


remove edu discount from cart list for w3d

--- assets/coffeescript/product_list.coffee
+++ assets/coffeescript/product_list.coffee
@@ -297,7 +297,6 @@
       sub_products:
         world_camera: world_cam_data.w3d
         eye_camera: eye_cam_data.enone
-        discount: edu_discount
       img: [pupil_img_path,'pupil_w3d_enone.jpg'].join('/')
     pupil_w3d_e120:
       cost: 1890
@@ -305,7 +304,6 @@
       sub_products:
         world_camera: world_cam_data.w3d
         eye_camera: eye_cam_data.e120
-        discount: edu_discount
       img: [pupil_img_path,'pupil_w3d_e120.jpg'].join('/')
     pupil_w3d_e120b:
       cost: 2590
@@ -313,7 +311,6 @@
       sub_products:
         world_camera: world_cam_data.w3d
         eye_camera: eye_cam_data.e120b
-        discount: edu_discount
       img: [pupil_img_path,'pupil_w3d_e120b.jpg'].join('/')

     pupil_w3d_enone_edu:


Use the new additions to the gutter api

Use the new additions to the gutter api

--- lib/git-diff-view.coffee
+++ lib/git-diff-view.coffee
@@ -3,6 +3,7 @@
 module.exports =
 class GitDiffView
   _.extend @prototype, Subscriber
+  classes: ['git-line-added', 'git-line-modified', 'git-line-removed']

   constructor: (@editor) ->
     @gutter = @editor.gutter
@@ -51,7 +52,7 @@

   removeDiffs: =>
     if @gutter.hasGitLineDiffs
-      @gutter.removeClassFromAllLines('git-line-added git-line-modified git-line-removed')
+      @gutter.removeClassFromAllLines(klass) for klass in @classes
       @gutter.hasGitLineDiffs = false

   renderDiffs: =>
@@ -64,10 +65,10 @@
     for {oldStart, newStart, oldLines, newLines} in hunks
       if oldLines is 0 and newLines > 0
         for row in [newStart...newStart + newLines]
-          linesHighlighted |= !!@gutter.addClassToLine(row - 1, 'git-line-added')
+          linesHighlighted |= @gutter.addClassToLine(row - 1, 'git-line-added')
       else if newLines is 0 and oldLines > 0
-        linesHighlighted |= !!@gutter.addClassToLine(newStart - 1, 'git-line-removed')
+        linesHighlighted |= @gutter.addClassToLine(newStart - 1, 'git-line-removed')
       else
         for row in [newStart...newStart + newLines]
-          linesHighlighted |= !!@gutter.addClassToLine(row - 1, 'git-line-modified')
+          linesHighlighted |= @gutter.addClassToLine(row - 1, 'git-line-modified')
     @gutter.hasGitLineDiffs = linesHighlighted


Use the same filter options when loading private feed teasers

--- client/landing/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppController.coffee
@@ -174,6 +174,9 @@
             KD.remote.reviveFromSnapshots data, (err, instances)->
               # console.log instances
               callback instances
+
+    # KD.remote.api.CActivity.some selector, options, (err, activities) ->
+    #   callback activities
     #
     # KD.remote.api.CActivity.teasers selector, options, (err, activities) =>
     #   if not err and activities?
@@ -222,13 +225,27 @@
         createdAt : -1

     if not options.skip < options.limit
-      @fetchTeasers selector, options, (activities)=>
-        if activities
-          for activity in activities
-            @activityListController.addItem activity
-          callback? activities
-        else
-          callback?()
+      if @activityListController._state is 'public'
+        @fetchTeasers selector, options, (activities)=>
+          if activities
+            for activity in activities
+              @activityListController.addItem activity
+            callback? activities
+          else
+            callback?()
+
+      else if @activityListController._state is 'private'
+        KD.whoami().fetchFeeds (err, feeds) =>
+          for feed in feeds
+            continue unless feed.title is 'followed'
+            feed.fetchActivities selector, options, (err, activities) =>
+              if not err and activities?
+                for activity in activities
+                  @activityListController.addItem activity
+                callback? activities
+              else
+                callback?()
+

   loadSomeTeasersIn:(sourceIds, options, callback)->
     KD.remote.api.Relationship.within sourceIds, options, (err, rels)->
@@ -243,28 +260,7 @@
     controller.noActivityItem.hide()

     if show is 'private'
-      # _counter = 0
-      # controller._state = 'private'
-      # controller.itemsOrdered.forEach (item)=>
-      #   if not controller.isInFollowing(item.data)
-      #     item.hide()
-      #     _counter++
-      # if _counter is controller.itemsOrdered.length
-      #   controller.noActivityItem.show()
-      controller.removeAllItems()
-      controller.showLazyLoader no
-
-      KD.whoami().fetchFeeds (err, feeds) =>
-        for feed in feeds
-          continue unless feed.title is 'followed'
-          feed.fetchActivities (err, activities) =>
-            for activity in activities
-              @activityListController.addItem activity
-            controller.isLoading = no
-            controller.hideLazyLoader()
-            #callback?()
-
-      return no
+      controller._state = 'private'

     else if show is 'public'
       controller._state = 'public'
@@ -416,8 +412,8 @@
   addItem:(activity, index, animation = null) ->
     @noActivityItem.hide()
     # log "ADD:", activity
-    if (@_state is 'private' and @isInFollowing activity) or @_state is 'public'
-      @getListView().addItem activity, index, animation
+    #if (@_state is 'private' and @isInFollowing activity) or @_state is 'public'
+    @getListView().addItem activity, index, animation

   unhide = (item)-> item.show()


remove quaint "emit" stack

remove quaint "emit" stack

--- client/landing/app/Applications/Groups.kdapplication/Views/groupview.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/groupview.coffee
@@ -150,10 +150,6 @@

     if "admin" in roles
       @tabView.showHandleContainer()
-      @emit 'SettingsSelected'
-      @emit 'PermissionsSelected'
-      @emit 'MembersSelected'
-      @emit 'MembershipPolicySelected'

   privateGroupOpenHandler: GroupsAppController.privateGroupOpenHandler


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/function_extensions.coffee
+++ test/function_extensions.coffee
@@ -23,7 +23,7 @@

 test "#once", ->
   onceScore = ->
-
+    ( ->

 test "#returning", ->
   x = 0


Color and Size Fixes

Color and Size Fixes

--- docs/components/docs/DocsBody.cjsx
+++ docs/components/docs/DocsBody.cjsx
@@ -32,7 +32,7 @@
       content:
         fontSize: '17px'
         lineHeight: '24px'
-        color: 'rgba(0,0,0,.57)'
+        color: 'rgba(0,0,0,.47)'

       animate:
         marginTop: '-240px'
@@ -99,9 +99,9 @@

             <style>{"
               .docsBody h1{
-                font-size: 32px;
+                font-size: 38px;
                 font-weight: 200;
-                color: rgba(0,0,0,.67);
+                color: rgba(0,0,0,.77);
                 margin: 0;
                 padding-top: 20px;
               }
@@ -109,9 +109,14 @@
               .docsBody h2{
                 font-size: 24px;
                 font-weight: 400;
-                color: rgba(0,0,0,.67);
+                color: rgba(0,0,0,.57);
                 padding-top: 20px;
                 margin-top: 0;
+              }
+              .docsBody h3{
+                font-weight: normal;
+                font-size: 20px;
+                color: rgba(0,0,0,.67);
               }
             "}</style>


Fix arc() implicit return not appropriate for chaining.

--- lib/mixins/vector.coffee
+++ lib/mixins/vector.coffee
@@ -155,6 +155,8 @@

       # render segment
       @bezierCurveTo cp1x, cp1y, cp2x, cp2y, ax, ay
+
+    return @

   polygon: (points...) ->
     @moveTo points.shift()...


protect device against null uuid

protect device against null uuid

--- lib/models/device.coffee
+++ lib/models/device.coffee
@@ -28,7 +28,11 @@
     @fetch (error, attributes) =>
       return callback error if error?

-      hashedToken = @_hashToken token
+      try
+        hashedToken = @_hashToken token
+      catch error
+        return callback error
+
       debug 'storeToken', token, hashedToken
       tokenData = createdAt: new Date()
       @update $set: {"meshblu.tokens.#{hashedToken}" : tokenData}, callback
@@ -37,7 +41,11 @@
     @fetch (error, attributes) =>
       return callback error if error?

-      hashedToken = @_hashToken token
+      try
+        hashedToken = @_hashToken token
+      catch error
+        return callback error
+
       @update $unset : {"meshblu.tokens.#{hashedToken}"}, callback

   verifyToken: (token, callback=->) =>
@@ -61,7 +69,11 @@
               callback null, true

   verifyNewToken: (token, callback=->) =>
-    hashedToken = @_hashToken token
+    try
+      hashedToken = @_hashToken token
+    catch error
+      return callback error
+
     @devices.findOne uuid: @uuid, "meshblu.tokens.#{hashedToken}": {$exists: true}, (error, device) =>
       return callback error if error?
       callback null, !!device
@@ -168,8 +180,12 @@
     @devices.findOne uuid: @uuid, (error, data) =>
       return callback error if error?
       delete data.meshblu.hash if data?.meshblu?.hash
+      try
+        token = @_hashToken JSON.stringify(data)
+      catch error
+        return callback error
       params = $set :
-        'meshblu.hash': @_hashToken JSON.stringify(data)
+        'meshblu.hash': token
       debug 'updating hash', @uuid, params
       @devices.update uuid: @uuid, params, callback

@@ -208,6 +224,8 @@
     new Error message.replace("MongoError: ")

   _hashToken: (token) =>
+    throw new Error 'Invalid Device UUID' unless @uuid?
+
     hasher = crypto.createHash 'sha256'
     hasher.update token
     hasher.update @uuid


cli no default template

cli no default template

--- cli.coffee
+++ cli.coffee
@@ -12,7 +12,6 @@
  .default 'title', 'Created with Wallapatta'

  .describe 'template', 'Template'
- .default 'template', './templates/page'

  .describe 'blog', 'Blog YAML file'
  .describe 'book', 'Book YAML file'


hide empty month title

hide empty month title

--- public/theme/portalshit/script.coffee
+++ public/theme/portalshit/script.coffee
@@ -29,15 +29,21 @@

     $('#categories li a').on('click', ->
       $articles = $('li.year ul li')
+      $monthTitles = $('li.year h3')
       if $(@).hasClass('clicked')
         $(@).removeClass('clicked')
         $articles.show()
+        $monthTitles.show()
       else
         $('#categories li a').removeClass('clicked')
         $(@).addClass('clicked')
         $articles.hide()
+        $monthTitles.hide()
         categoryName = $(@).data('category-name')
         for element in $('.category a')
           if $(element).text() == categoryName
             $(element).closest('li').show()
+        for elem in $('li.year ul')
+          if $(elem).find('li:visible').length > 0
+            $(elem).find('li:visible').parent().parent().find('h3').show()
     )


Refactor patterns for conditional requests

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -126,10 +126,19 @@
 	# Numeric literal
 	number:
 		name: "constant.numeric.roff"
-		match: "(?!\\d+(?:/|[cfimnPpsuvz]\\w))(\\|)?(?:(?<!\\w)[-+])?(?:\\d+(?:\\.\\d*)?|\\.\\d+|(?<=[-+])\\.)([cfimnPpsuvz])?"
+		match: "(?!\\d+(?:/|[CDMPTcimnpstuvz]\\w))(\\|)?(?:(?<!\\w)[-+])?(?:\\d+(?:\\.\\d*)?|\\.\\d+|(?<=[-+])\\.)([CDMPTcimnpstuvz])?"
 		captures:
 			1: name: "keyword.operator.absolute.roff"
-			2: name: "keyword.other.unit.roff"
+			2: patterns: [include: "#units"]
+
+
+	# Units of measurement
+	units:
+		patterns: [
+			{match: "[Pcimnpuv]", name: "keyword.other.unit.roff"}
+			{match: "z",          name: "keyword.other.unit.gnu.roff"}
+			{match: "[CDMTst]",   name: "keyword.other.unit.heirloom.roff"}
+		]

 	# "Double-quoted string"
@@ -203,18 +212,30 @@
 		]

+	# Pair of strings separated by an arbitrary delimiter
+	"2-part-string":
+		name: "string.quoted.other.arbitrary-delimiter.2-part.roff"
+		match: "\\G(.)((?:(?!\\1).)*)(\\1)((?:(?!\\1).)*)(\\1)"
+		captures:
+			1: name: "punctuation.definition.string.begin.roff",  patterns: [include: "#c0"]
+			2: name: "meta.segment.1.left.roff",                  patterns: [include: "#escapes"]
+			3: name: "punctuation.definition.string.middle.roff", patterns: [include: "#c0"]
+			4: name: "meta.segment.2.right.roff",                 patterns: [include: "#escapes"]
+			5: name: "punctuation.definition.string.end.roff",    patterns: [include: "#c0"]
+
+
 	# Group of strings delimited with an arbitrary character
 	"3-part-title":
-		name: "string.3-part.other.roff"
+		name: "string.quoted.other.arbitrary-delimiter.3-part.roff"
 		match: '\\G[ \\t]*(.)((?:(?!\\1).)*)(\\1)((?:(?!\\1).)*)(\\1)((?:(?!\\1).)*)(\\1)'
 		captures:
-			1: name: "punctuation.definition.string.begin.roff", patterns: [include: "#c0"]
-			2: name: "entity.string.left.roff",                  patterns: [include: "#escapes"]
-			3: name: "punctuation.definition.string.begin.roff", patterns: [include: "#c0"]
-			4: name: "entity.string.centre.roff",                patterns: [include: "#escapes"]
-			5: name: "punctuation.definition.string.end.roff",   patterns: [include: "#c0"]
-			6: name: "entity.string.right.roff",                 patterns: [include: "#escapes"]
-			7: name: "punctuation.definition.string.end.roff",   patterns: [include: "#c0"]
+			1: name: "punctuation.definition.string.outer.begin.roff", patterns: [include: "#c0"]
+			2: name: "meta.segment.1.left.roff",                       patterns: [include: "#escapes"]
+			3: name: "punctuation.definition.string.inner.begin.roff", patterns: [include: "#c0"]
+			4: name: "meta.segment.2.centre.roff",                     patterns: [include: "#escapes"]
+			5: name: "punctuation.definition.string.inner.end.roff",   patterns: [include: "#c0"]
+			6: name: "meta.segment.3.right.roff",                      patterns: [include: "#escapes"]
+			7: name: "punctuation.definition.string.outer.end.roff",   patterns: [include: "#c0"]

@@ -228,7 +249,7 @@
 				(aln|als|asciify|backtrace|blm|boxa|box|brp|cflags|chop|close|composite|color
 				|cp|devicem|device|do|ecs|ecr|evc|fam|fchar|fcolor|fschar|fspecial|ftr|fzoom
 				|gcolor|hcode|hla|hlm|hpfa|hpfcode|hpf|hym|hys|itc|kern|length|linetabs|lsm
-				|mso|nop|nroff|opena|open|pev|pnr|psbb|pso|ptr|pvs|rchar|rfschar|rj
+				|mso|nroff|opena|open|pev|pnr|psbb|pso|ptr|pvs|rchar|rfschar|rj
 				|rnn|schar|shc|shift|sizes|special|spreadwarn|stringdown|stringup|sty
 				|substring|tkf|tm1|tmc|trf|trin|trnt|troff|unformat|vpt|warnscale|warn
 				|writec|writem|write)
@@ -341,25 +362,94 @@
 				2: name: "entity.function.name.roff"
 			patterns: [include: "#param-group"]

+		},{
+			# Requests that execute other requests
+			name:  "meta.function.request.$2.roff"
+			begin: "(?:^|\\G)([.'])[ \\t]*((el|ie|if|while)|(nop))(?=$|[\\s\\(\"'\\\\!\\x02-\\a\\x7F])"
+			end:   "(?<!\\\\)$"
+			beginCaptures:
+				1: name: "punctuation.definition.request.roff"
+				3: name: "keyword.control.flow.roff"
+				4: name: "entity.function.name.gnu.roff"
+			patterns: [{
+				# Condition
+				name:  "meta.condition.roff"
+				begin: "(?<=ie|if|while)\\G[ \\t]*(?>(!)[ \\t]*)?"
+				end:   "(?!\\G)"
+				beginCaptures:
+					1: name: "keyword.operator.logical.not.roff"
+				patterns: [{
+					# One-character built-in comparison name
+					name: "constant.language.builtin-comparison.$1.roff"
+					match: "\\G([notevh])"
+				},{
+					# GNU extensions to validate an identifier
+					match: "\\G([cdFmrS])[ \\t]*((?:[^ \\t\\\\]|\\\\(?!E?[\"#]).)+)"
+					captures:
+						1: name: "constant.language.builtin-comparison.$1.gnu.roff"
+						2: name: "entity.name.roff", patterns: [include: "#escapes"]
+				},{
+					# Arithmetic
+					name:  "meta.equation.roff"
+					begin: "\\G(?=\\|?[\\(\\d\\\\])"
+					end:   "(?<=\\))[CDMPTcimnpstuvz]?(?!\\s*[-+*&:^?=/|\\d<>\\(\\)])|(?=[.']|\\\\{)|(?<!\\\\)$"
+					endCaptures: 0: patterns: [include: "#units"]
+					patterns: [include: "#arithmetic"]
+				},{
+					# String/variable comparison
+					name:  "meta.string-comparison.roff"
+					begin: "\\G(?=([^\\d\\s\\\\]).*?\\1.*?\\1)"
+					end:   "(?!\\G)"
+					patterns: [include: "#2-part-string"]
+				},{
+					match: "\\\\+(?=$)"
+					captures:
+						0: patterns: [include: "#continuous-newline"]
+				}, include: "#escapes"]
+			},{
+				# Action
+				begin: "(?=[.'])"
+				end:   "(?!\\G)"
+				patterns: [
+					# Clamp requests that have a chance of spillage
+					match: "\\G([.'][ \\t]*(?:ig|ul|(?:de|am)i?1?)(?=$|\\s).*)"
+					captures:
+						1: patterns: [include: "#requests"]
+					{match: "\\\\+$", captures: 0: patterns: [include: "#continuous-newline"]}
+					{include: "#requests"}
+					{include: "#macros"}
+					{include: "$self"}
+				]
+			},{
+				match: "\\\\+$"
+				captures:
+					0: patterns: [include: "#continuous-newline"]
+			}, include: "$self"]
 		}
-		{ include: "#conditionals" }
 		{ include: "#definition"   }
 		{ include: "#ignore"       }
 		{ include: "#underlines"   }
 		{
 			# Register assignment
 			name: "meta.function.request.$2.roff"
-			begin: "(?:^|\\G)([.'])[ \\t]*(nr)[ \\t]*(?:(%|ct|dl|dn|dw|dy|ln|mo|nl|sb|st|yr)|(c\\.)|(\\${2}|\\.[$aAbcdfFhHijklLnopRTstuvVwxyz])|(\\.[CgmMOPUxyY])|(\\S+))?[ \\t]*(.*)$"
+			begin: "(?:^|\\G)([.'])[ \\t]*(nr)[ \\t]*(\\S*)[ \\t]*(.*)$"
 			end: "(?<!\\\\)$"
 			beginCaptures:
 				1: name: "punctuation.definition.request.roff"
 				2: name: "storage.type.var.roff"
-				3: name: "constant.language.predefined.register.roff"
-				4: name: "constant.language.predefined.register.gnu.roff"
-				5: name: "invalid.illegal.readonly.register.roff"
-				6: name: "invalid.illegal.readonly.register.gnu.roff"
-				7: name: "variable.parameter.roff", patterns: [include: "#escapes"]
-				8: patterns: [{include: "#arithmetic"}, {include: "#param-group"}]
+				3: patterns: [
+					{match: "%|ct|dl|dn|dw|dy|ln|mo|nl|sb|st|yr|c\\.",   name: "support.variable.predefined.register.roff"}
+					{match: "\\$\\$|\\.[$aAbcdfFhHijklLnopRTstuvVwxyz]", name: "invalid.illegal.readonly.register.roff"}
+					{match: "\\.[CgmMOPUxyY]",                           name: "invalid.illegal.readonly.register.gnu.roff"}
+					name: "entity.name.register.roff"
+					match: ".+"
+					captures:
+						0: patterns: [include: "#escapes"]
+				]
+				4: patterns: [
+					{include: "#arithmetic"}
+					{include: "#param-group"}
+				]
 		},{

 			# String definition
@@ -391,167 +481,6 @@
 				{include: "#params"}
 			]
 		}]
-
-
-
-	# Conditional input
-	conditionals:
-		patterns: [{
-
-			# Conditional: If
-			begin: """(?x)(?:^|\\G)
-				([.'])         [ \\t]*  # 1: punctuation.definition.request.roff
-				(ie|if|while)  [ \\t]*  # 2: keyword.control.roff
-				(!)?                    # 3: keyword.operator.logical
-
-				(?:
-					# One-character built-in comparison name
-					([notevh])       # 4: constant.language.builtin-comparison.$4.roff
-
-					|
-
-					# GNU extensions
-					([cdFmrS])       # 5: constant.language.builtin-comparison.$5.gnu.roff
-
-					# Name being validated
-					[ \\t]*
-					((?:[^ \\t\\\\]|\\\\(?!E?["#]).)+) # 6: Include “#escapes”
-
-					|
-
-					# Arithmetic
-					(                # 7: meta.equation.roff
-
-						# Starts with a bracket
-						(\\()        # 8:  punctuation.definition.begin.roff
-						(.*?)        # 9:  Include “#arithmetic”
-						(\\))        # 10: punctuation.definition.end.roff
-
-						# Anything else affixed to it
-						(            # 11: Include “#arithmetic”
-							(?:
-								[^\\s\\(] | # Operators/numbers
-								\\(.*?\\)   # More brackets
-							)*
-						)
-
-						|
-
-						# Doesn’t start with a bracket
-						(?:
-							# Starts with a long-form string/register
-							(\\|?\\\\+E?[n*]\\(\\S{2}) # 12: Include “#escapes”
-							|
-							# Starts with a digit or backslash
-							(?=\\d|\\\\)
-						)
-						([^\\s\\(]*) # 13: Sandwiched mathematical junk
-
-						(?: # Possible embedded brackets
-							(\\()    # 14: punctuation.definition.begin.roff
-							(.*?)    # 15: Include “#arithmetic”
-							(\\))    # 16: punctuation.definition.end.roff
-						)?
-						(?: # Possible trailing digits/operators
-							[^\\s\\(]*?
-							\\d+
-						)?
-
-						# Ends with a...
-						(?<=
-							# Digit
-							\\d |
-
-							# Unit suffix
-							(?<=[\\d.])
-							[A-Za-z] |
-
-							# Closing bracket
-							[\\)\\]] |
-
-							# String/register: Long-form
-							\\\\  [n*] \\( \\S{2} |
-							\\\\E [n*] \\( \\S{2} |
-
-							# String/register: Short-form
-							\\\\  [n*] \\S |
-							\\\\E [n*] \\S
-						)
-					)
-
-					|
-
-					# String/variable comparison
-					([^\\d\\s\\\\])  # 17: punctuation.definition.string.begin.roff
-					(                # 18: variable.parameter.operand.left.roff
-						(.*?)        # 19: Include “#escapes”
-					)
-					(\\17)           # 20: punctuation.definition.string.roff
-					(                # 21: variable.parameter.operand.right.roff
-						(.*?)        # 22: Include “#escapes”
-					)
-					(\\17)           # 23: punctuation.definition.string.end.roff
-
-					|
-
-					# Anything not recognised
-					(\\S)            # 24: meta.operand.single.roff
-				)?
-
-				(.*)                 # 25: Include “#conditional-innards”
-			"""
-			end: "$"
-			beginCaptures:
-				0:  name: "meta.function.request.$2.roff"
-				1:  name: "punctuation.definition.request.roff"
-				2:  name: "keyword.control.roff"
-				3:  name: "keyword.operator.logical"
-				4:  name: "constant.language.builtin-comparison.$4.roff"
-				5:  name: "constant.language.builtin-comparison.$5.gnu.roff"
-				6:  patterns: [include: "#escapes"]
-				7:  name: "meta.equation.roff"
-				8:  name: "punctuation.definition.begin.roff"
-				9:  patterns: [include: "#arithmetic"]
-				10: name: "punctuation.definition.end.roff"
-				11: patterns: [include: "#arithmetic"]
-				12: patterns: [include: "#escapes"]
-				13: patterns: [include: "#arithmetic"]
-				14: name: "punctuation.definition.begin.roff"
-				15: patterns: [include: "#arithmetic"]
-				16: name: "punctuation.definition.end.roff"
-				17: name: "punctuation.definition.string.begin.roff", patterns: [include: "#escapes"]
-				18: name: "variable.parameter.operand.left.roff"
-				19: patterns: [include: "#escapes"]
-				20: name: "punctuation.definition.string.roff"
-				21: name: "variable.parameter.operand.right.roff"
-				22: patterns: [include: "#escapes"]
-				23: name: "punctuation.definition.string.end.roff", patterns: [include: "#escapes"]
-				24: name: "meta.operand.single.roff"
-				25: patterns: [include: "#conditional-innards"]
-		},{
-
-			# Conditional: Else
-			begin: "(?:^|\\G)([.'])[ \\t]*(el)\\s*(.*)"
-			end:   "$"
-			beginCaptures:
-				0: name: "meta.function.request.$2.roff"
-				1: name: "punctuation.definition.request.roff"
-				2: name: "keyword.control.roff"
-				3: patterns: [include: "#conditional-innards"]
-		}]
-
-	"conditional-innards":
-		patterns: [{
-			begin: "(?:\\G|^)\\s*(\\\\E?\\{(?:\\\\E?(?=\\R|$))?)?\\s*(.*)"
-			end:   "$"
-			beginCaptures:
-				1: name: "punctuation.section.conditional.begin.roff"
-				2: patterns: [
-					{include: "#requests"}
-					{include: "#macros"}
-					{include: "$self"}
-				]
-		}, include: "$self"]

@@ -576,9 +505,9 @@
 			{
 				# Change default scaling indicator (GNU)
 				name: "meta.scaling-indicator.gnu.roff"
-				match: "(?:\\G|(?<=^|\\())([cfimnPpsuvz])(;)"
+				match: "(?:\\G|(?<=^|\\())([CDMPTcimnpstuvz])(;)"
 				captures:
-					1: name: "keyword.other.unit.roff"
+					1: patterns: [include: "#units"]
 					2: name: "punctuation.separator.semicolon.roff"
 			}
 		]
@@ -899,14 +828,6 @@
 				3: name: "punctuation.definition.escape.roff"
 		},{

-			# \*[name].macro - Adhoc matching to improve tbl's "\*[3trans]" hack (GNU)
-			begin: "(?:^|\\G)(\\\\\\*\\[[^\\]]+\\])\\s*(\\.\\w+.*)"
-			end: "$"
-			beginCaptures:
-				1: patterns: [include: "#escapes"]
-				2: patterns: [include: "#conditional-innards"]
-		},{
-
 			# \*[xx args…] - Interpolate string "xx" with optional arguments (GNU)
 			name: "constant.character.escape.function.interpolate-string.gnu.roff"
 			begin: "((?:((\\\\)E)|(\\\\))\\*(\\[))"
@@ -1357,19 +1278,19 @@
 		},{

 			# Conditional input: Begin
-			name: "meta.function.begin.roff"
 			match: "(?:(?:^|\\G)(\\.|'+)[ \\t]*)?(\\\\\\{(?:\\\\(?=\\R|$))?)"
 			captures:
 				1: name: "punctuation.definition.request.roff"
-				2: name: "punctuation.section.conditional.begin.roff"
+				2: name: "constant.character.escape.conditional.block.roff"
+				3: name: "punctuation.section.conditional.begin.roff"
 		},{

 			# Conditional input: End
-			name: "meta.function.end.roff"
-			match: "(?:(?:^|\\G)(\\.|'+)[ \\t]*)?(\\\\\\}(?:\\\\(?=\\R|$))?)"
+			match: "(?:(?:^|\\G)(\\.|'+)[ \\t]*)?((\\\\\\}(?:\\\\(?=\\R|$))?))"
 			captures:
 				1: name: "punctuation.definition.request.roff"
-				2: name: "punctuation.section.conditional.end.roff"
+				2: name: "constant.character.escape.conditional.block.roff"
+				3: name: "punctuation.section.conditional.end.roff"
 		},{

 			# Device control function