rewrite callback arg handling and much more, multiple fixes based on tests and coverage

--- lib/index.coffee
+++ lib/index.coffee
@@ -1,66 +1,82 @@
 fs       = require 'fs'
 corepath = require 'path'

-hasRoot = /// ^(
-  \/           # *nix style root
-  | [a-zA-Z]\: # windows drive spec
-  | \\         # no drive spec, but, is absolute from current drive
-) ///
+isFile = (path) -> path.isFile()
+isDir  = (path) -> path.isDir()

 class Path
+
   constructor: (path) ->
+
     @_the = {}
+
     if path?
+
       if 'string' is typeof path
         @path = path
         @parts = path.split corepath.sep # split into array
+
       else if Array.isArray path
         @path = path.join corepath.sep
         @parts = path.slice() # copy array
+
       else
         throw new Error 'Path requires either string or array as `path` to constructor'
+
     else
       @path = '.'
       @parts = [ '.' ]

-    @isAbsolute = hasRoot.test @path
+    @isAbsolute = corepath.isAbsolute @path
     @isRelative = not @isAbsolute

+
   reset: ->
-    delete @_the
-    delete @_stats
     @_the = {}
+    @_stats = null
+

   stats: (done) ->
+
     if done?
+
       fs.stat @path, (error, stats) =>
         @_stats = stats
         # get rid of error if it's a 'path doesnt exist' error
         if error?.code is 'ENOENT' then error = null
         done error, stats
+
     else
       try
-        delete @_stats
+        @_stats = null
         @_stats = fs.statSync @path
       catch error
         # errno is 34 in node 0.10.*
         # errno is -2 in node 0.12.* and 4.* ... so, use 'code'
+        ### istanbul ignore else ###
         if error.code is 'ENOENT' then return
         else throw error
+

   refresh: (done) ->
     @reset()
     @stats done

+
   toString: -> @path

+
   _fromStats: (done, getter) ->
+
     if done? then @stats (error, stats) ->
+
+      ### istanbul ignore if ###
       if error?
-        if error.errno is 34 then done undefined, getter()
+        if error.code is 'ENOENT' then done null, getter stats
         else done error
+
       else
-        done undefined, getter stats
+        done null, getter stats

     else getter @stats()

@@ -76,9 +92,9 @@
   filename: -> @_the.filename  ? (@_the.filename  = corepath.basename @basename(), @extname())
   extname : -> @_the.extension ? (@_the.extension = corepath.extname @basename())
   dirname : -> @_the.dirname   ? (@_the.dirname   = corepath.basename corepath.dirname @path)
-  extension: @extname

   parent: -> @_the.parent ? (@_the.parent = @to '..')
+

   up: (count = 1) ->
     count = Math.max 1, Math.min 20, count
@@ -86,68 +102,127 @@
     add += corepath.sep + '..' for i in [1...count]
     @to add

-  startsWith: (path) -> if path?.length then @path[...path.length] is path.toString() else false
-  endsWith  : (path) -> if path?.length then @path[-path.length...] is path.toString() else false
+
+  startsWith: (arg) ->
+
+    path =
+      if typeof arg is 'string' then arg
+      else if typeof arg?.path is 'string' then arg.path
+
+    path? and path.length <= @path.length and
+      path.length > 0 and path is @path[...path.length]
+
+
+  endsWith: (arg) ->
+
+    path =
+      if typeof arg is 'string' then arg
+      else if typeof arg?.path is 'string' then arg.path
+
+    path? and path.length <= @path.length and
+      path.length > 0 and path is @path[-path.length...]
+

   equals: (value) -> # value: string, regex, Path
+
     if value?
+
       if 'string' is typeof value then @path is value
-      else if value instanceof RegExp then value.test @path
-      else if value instanceof Path then  @path is value.path
-      else throw new TypeError 'equals() requires either string, regex, or Path'
+
+      # instanceof RegExp, check for `test` function instead
+      else if 'function' is typeof value.test then value.test @path
+
+      # instanceof Path, check for `path` string property instead
+      else if 'string' is typeof value.path then @path is value.path
+
+      # could return false, but, I think you meant to provide the right
+      # type and something went wrong, so, let's return an error.
+      else error: 'equals() requires: string, regex, or Path'
+
     else false

-  _mustExist: (msg) ->
-    unless @isReal() then throw new Error "path must exist #{msg}"
-
-  _optionsAndDone: (options, done) ->
-    if not done?
-      if 'function' is typeof options
-        done = options
-        options = null
-      else if options?
-        if options.done?
-          done = options.done
-          delete options.done
-    return options:options, done:done
-
-  _createStream: (options, done, creator) ->
-    {options, done} = @_optionsAndDone options, done
-    if done?
-      process.nextTick =>
+
+  _createStream: (options, creator) ->
+
+    if options?.done?
+      path = @path
+      done = options.done
+      process.nextTick ->
         try
-          done undefined, creator @path, options
+          done null, creator path, options
         catch error
+          ### istanbul ignore next ###
           done error
-    else
-      creator @path, options
-
-  reader: (options, done) ->
-    @_mustExist 'to read from it'
-    @_createStream options, done, (path, options = {}) ->
+
+    else creator @path, options
+
+
+  reader: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    unless @isFile()
+      error = error: 'reader() requires file'
+      if options?.done? then return options.done error
+      else return error
+
+    @_createStream options, (path, options) ->
       # passing `null` for options errors in node 0.12 and 4+.
-      # so, we'll ensure it's an object in the params
-      fs.createReadStream path, options
-
-  writer: (options, done) ->
-    @_createStream options, done, (path, options) ->
+      # so, we'll ensure it's an object
+      fs.createReadStream path, options ? {}
+
+
+  writer: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    if @isDir()
+      error = error: 'writer() requires non-directory'
+      if options?.done? then return options.done error
+      else return error
+
+    @_createStream options, (path, options) ->
       fs.createWriteStream path, options

-  read: (options, done) ->
-    @_mustExist 'to read from it'
-    {options, done} = @_optionsAndDone options, done
-    fn = if done? then fs.readFile else fs.readFileSync
-    fn @path, options, done
-
-  write: (data, options, done) ->
-    {options, done} = @_optionsAndDone options, done
-    fn = if done? then fs.writeFile else fs.writeFileSync
-    fn @path, data, options, done
-
-  append: (data, options, done) ->
-    {options, done} = @_optionsAndDone options, done
-    fn = if done? then fs.appendFile else fs.appendFileSync
-    fn @path, data, options, done
+
+  read: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    unless @isFile()
+      error = error: 'read() requires file'
+      if options?.done? then return options.done error
+      else return error
+
+    if options?.done? then fs.readFile @path, options, options.done
+    else fs.readFileSync @path, options
+
+
+  write: (data, arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    if @isDir()
+      error = error: 'write() requires non-directory'
+      if options?.done? then return options.done error
+      else return error
+
+    if options?.done? then fs.writeFile @path, data, options, options.done
+    else fs.writeFileSync @path, data, options
+
+
+  append: (data, arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    if @isDir()
+      error = error: 'append() requires non-directory'
+      if options?.done? then return options.done error
+      else return error
+
+    if options?.done? then fs.appendFile @path, data, options, options.done
+    else fs.appendFileSync @path, data, options
+

   # stream: the stream to pipe the file's content to, or a path (call path.writer())
   # options: options for creating the reader and adding events to the reader
@@ -155,92 +230,165 @@
   # options.writer: the options to pass to writer()
   # options.events: the event/listener pairs to register with the reader
   pipe: (pathOrStream, options) ->
+
+    # get a reader for *this* path
     reader = @reader options?.reader
-    reader.on event, listener for event,listener of options?.events?.reader
-    pathOrStream = pathOrStream.writer options?.writer if pathOrStream instanceof Path
-    pathOrStream.on event, listener for event,listener of options?.events?.writer
-    return reader.pipe pathOrStream
-
-  list: (options, done) ->
-    @_mustExist 'to list directory'
-    {options, done} = @_optionsAndDone options, done
-    done ?= options?.all
-    # TODO: allow a `returnType` option which has us store result in an object
-    #       instead of an array?
-    if done?
-      fs.readdir @path, (error, array) =>
-        if error? then return done error
-        done? undefined, @_processList options, done, array
-    else
-      array = fs.readdirSync @path
-      @_processList options, done, array
-
-  _processList: (options, done, array) ->
+
+    if reader.error? then return reader
+
+    if options?.events?.reader?
+      reader.on event, listener for event, listener of options.events.reader
+
+    # if target is a path then get a writer for it
+    writer =
+      if pathOrStream instanceof Path then pathOrStream.writer options?.writer
+      else pathOrStream
+
+    if writer.error? then return writer
+
+    if options?.events?.writer?
+      writer.on event, listener for event, listener of options.events.writer
+
+    # finally pipe together and return result for chaining further
+    return reader.pipe writer
+
+
+  list: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    unless @isDir()
+      error = error: 'list() requires directory'
+      if options?.done? then return options.done error
+      else return error
+
+
+    if options?.done?
+      done = options.done
+      path = this
+      fs.readdir @path, (error, array) ->
+        ### istanbul ignore if ###
+        if error? then done error
+        else done null, path._processList options, array
+
+    else @_processList options, fs.readdirSync @path
+
+
+  _processList: (options, array) ->
+
     acceptString = options?.acceptString
     acceptPath   = options?.acceptPath
-    each   = options?.each
+    each         = options?.each
+
+    # store all the accepted paths
     paths = []
+
+    # count the rejected strings/paths
     rejectedStrings = rejectedPaths = 0

+    # check array with filters, report each, count rejects
     for string in array
+
+      # if there's no filter, or if the filter accepts
       if not acceptString? or acceptString string
+
         path = @to string
+
+        # if there's no filter, or if the filter accepts
         if not acceptPath? or acceptPath path
-          paths.push path
-          each? path:path
-        else
-          rejectedPaths++
-      else
-        rejectedStrings++
+
+          paths[paths.length] = path
+          # call each if it exists
+          each? {path}
+
+        else rejectedPaths++
+
+      else rejectedStrings++

     return paths:paths, rejected:{strings:rejectedStrings, paths:rejectedPaths}

-  files: (options, done) ->
-    {options, done} = @_optionsAndDone options, done
-    options ?= {}
-    options.acceptPath ?= (path) -> path.isFile()
-    @list options, done
-
-  dirs: (options, done) ->
-    {options, done} = @_optionsAndDone options, done
-    options ?= {}
-    options.acceptPath ?= (path) -> path.isDir()
-    @list options, done
-
-  # TODO: expect this path to be a file and reference file by name
-  #file: (path) ->
-  # read stats
-  # ensure isFile() is true
-
-  # TODO: expect this path to be a directory and reference directory by name
-  #dir: (path) ->
-  # read stats
-  # ensure isDir() is true
+
+  files: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    if options?
+
+      # clone their options
+      options = Object.assign {}, options
+
+      # wrap their filter so we can use `isFile` first
+      if options.acceptPath?
+        theirFilter = options.acceptPath
+        options.acceptPath = (path) -> path.isFile() and theirFilter path
+
+      # they aren't filtering, so, use ours directly
+      else options.acceptPath = isFile
+
+    # supply our files filter
+    else options = acceptPath: isFile
+
+    @list options
+
+
+  dirs: (arg) ->
+
+    options = if typeof arg is 'function' then done:arg else arg
+
+    if options?
+
+      # clone their options
+      options = Object.assign {}, options
+
+      # wrap their filter so we can use `isDir` first
+      if options.acceptPath?
+        theirFilter = options.acceptPath
+        options.acceptPath = (path) -> path.isDir() and theirFilter path
+
+      # they aren't filtering, so, use ours directly
+      else options.acceptPath = isDir
+
+    # supply our files filter
+    else options = acceptPath: isDir
+
+    @list options
+

   to: (path) ->
-    unless path? then throw new Error 'must specify path to to()'
+
     if 'string' is typeof path
       newPath = corepath.join @path, path
-      if newPath is @path then return this else return new Path newPath
-    else if Array.isArray path
-      new Path @parts.slice().concat path
-    else throw new Error 'to(path) requires either a string or array of strings'
+      if newPath is @path then this else new Path newPath
+
+    else if Array.isArray path then new Path @parts.concat path
+
+    else error: 'to(path) requires either a string or array of strings'
+

   resolve: (path) ->
     path = corepath.resolve @path, path
     if path is @path then this else new Path path

-  relativeTo: (to) ->
-    path = corepath.relative @path, to
-    if path is @path then this else new Path path
+
+  relativeTo: (path) ->
+    path = corepath.relative @path, path
+    if path.length < 1 then return this else new Path path
+

   normalize: () ->
     normalized = corepath.normalize @path
     if normalized is @path then this else new Path normalized

+
   subpath: (start, end) -> new Path (if @path is '/' then '' else @parts[start...end])

+
   part: (index) -> @parts[index]
+
+
+# alias:
+Path::extension = Path::extname
+

 # Use these ways:
 #  1a. {Path} = require 'fspath'


Added options to submit

Added options to submit

--- lib/index.coffee
+++ lib/index.coffee
@@ -123,7 +123,7 @@
     publish: (channel, data) ->
       redis.publish prefixChannel(channel), (if data then JSON.stringify data)

-    submit: (cName, docName, opData, callback) ->
+    submit: (cName, docName, opData, options, callback) ->
       #console.log 'submit opdata ', opData
       err = ot.checkOpData opData
       return callback? err if err


Simply init call

Simply init call

--- app/assets/javascripts/select2-rails.coffee
+++ app/assets/javascripts/select2-rails.coffee
@@ -3,5 +3,4 @@
     selector = if typeof selector == 'undefined' then '.select2-rails' else selector
     $(selector).select2()

-$(document).ready window.Binco.Select2.load
-$(document).on 'page:load', window.Binco.Select2.load
+$(document).on 'ready page:load', window.Binco.Select2.load


Added long behavior names to node description

--- src/treeLoader.coffee
+++ src/treeLoader.coffee
@@ -214,8 +214,13 @@
 		else if meta.category then imageName = meta.category
 		else imageName = 'default'

+	description = behavior.getDescription()
+	name = cNode.getTitle()
+	if name.length > 15
+		description = name + ': ' + description
+
 	tNode = {
-		text: {name: cNode.getTitle(), status: ' ', contact: ' ', desc: behavior.getDescription() }
+		text: {name: name, status: ' ', contact: ' ', desc: description }
 		image: './assets/behaviors/' + imageName + '.png'
 		collapsed: false
 		HTMLclass: 'none'			# running, failure, error, success


Undo a dodgy fix I made (the real fix for which was last commit)

--- src/types/json.coffee
+++ src/types/json.coffee
@@ -308,7 +308,7 @@
 			else
 				# -> noop if the other component is deleting the same object (or any
 				# parent)
-				return dest if c.p[common-1] == otherC.p[common-1]
+				return dest if c.p[common] == otherC.p[common]
 		else if otherC.oi != undefined
 			if c.oi != undefined and c.p[common] == otherC.p[common]
 				# client wins if we try to insert at the same place


guild tax loading faster

guild tax loading faster

--- src/coffee/controllers/player/player-guild-ctrl.coffee
+++ src/coffee/controllers/player/player-guild-ctrl.coffee
@@ -9,6 +9,9 @@

       player = CurrentPlayer.getPlayer()
       $scope.currentlyInGuild = player?.guild
+
+      $scope.editable.guildTaxRate = $scope.guild?.taxPercent
+      $scope.editable.selfTaxRate = player?.guildTax

       if $scope.currentlyInGuild
         $scope.setupGuildData()


reorder middlewares with comments

reorder middlewares with comments

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -60,10 +60,36 @@
   Backbone.sync.editRequest = (req) -> req.set('X-XAPP-TOKEN': artsyXappMiddlware.token)
   backboneCacheSync(Backbone.sync, REDIS_URL, DEFAULT_CACHE_TIME, NODE_ENV) if REDIS_URL

-  # Add up front middleware
+  # Inject sharify data before anything
   app.use sharify

-  # Setup Artsy XAPP & Passport middleware
+  # Development / Test only middlewares that compile assets, mount antigravity, and
+  # allow a back door to log in for tests.
+  if "development" is NODE_ENV
+    app.use express.errorHandler()
+    app.use require("stylus").middleware
+      src: path.resolve(__dirname, "../")
+      dest: path.resolve(__dirname, "../public")
+    app.use require("browserify-dev-middleware")
+      src: path.resolve(__dirname, "../")
+      transforms: [require("jadeify"), require('caching-coffeeify')]
+  if "test" is NODE_ENV
+    app.use "/__api", require("../test/helpers/integration.coffee").api
+    app.use (req, res, next) ->
+      return next() unless req.query['test-login']
+      req.user = new CurrentUser(
+        require('antigravity').fabricate('user', accessToken: 'footoken')
+      )
+      next()
+
+  # Proxy / redirect requests before they even have to deal with Force routing
+  app.use proxyGravity
+  app.use redirectMobile
+  app.use proxyReflection
+  app.use ensureSSL
+
+  # Setup Artsy XAPP & Passport middleware for authentication along with the
+  # body/cookie parsing middleware needed for that.
   app.use artsyXappMiddlware(
     artsyUrl: ARTSY_URL
     clientId: ARTSY_ID
@@ -86,36 +112,12 @@
     twitterSignupPath: '/force/users/auth/twitter/email'
   app.use errorHandler.socialAuthError

-  # General
+  # General helpers and express middleware
   app.use localsMiddleware
   app.use micrositeMiddleware
   app.use helpersMiddleware
   app.use express.favicon()
   app.use express.logger('dev')
-
-  # Development
-  if "development" is NODE_ENV
-    app.use express.errorHandler()
-    app.use require("stylus").middleware
-      src: path.resolve(__dirname, "../")
-      dest: path.resolve(__dirname, "../public")
-    app.use require("browserify-dev-middleware")
-      src: path.resolve(__dirname, "../")
-      transforms: [require("jadeify"), require('caching-coffeeify')]
-
-  # Test
-  if "test" is NODE_ENV
-    app.use "/__api", require("../test/helpers/integration.coffee").api
-    app.use (req, res, next) ->
-      return next() unless req.query['test-login']
-      user = new CurrentUser require('antigravity').fabricate('user', accessToken: 'footoken')
-      req.login user, next
-
-  # Proxy / redirect
-  app.use proxyGravity
-  app.use redirectMobile
-  app.use ensureSSL
-  app.use proxyReflection

   # Mount apps
   app.use require "../apps/home"
@@ -152,10 +154,11 @@
   app.get '/system/up', (req, res) ->
     res.send 200, { nodejs: true }

-  # More general middleware
+  # Static asset middleware
   app.use express.static(path.resolve __dirname, "../public")

-  # 404 and error handling middleware
+  # Finally 404 and error handling middleware when the request wasn't handeled
+  # successfully by anything above.
   app.use errorHandler.pageNotFound
   app.use '/force/users/sign_in', loginError
   app.post '/force/javascripterr', errorHandler.javascriptError


performance improvement

performance improvement


fix filter type issue

fix filter type issue


Avoid conflicts between lightboxes on keypress ESC

--- app/coffee/modules/common/lightboxes.coffee
+++ app/coffee/modules/common/lightboxes.coffee
@@ -39,7 +39,7 @@
 class LightboxService extends taiga.Service
     constructor: (@animationFrame, @q, @rootScope) ->

-    open: ($el, onClose, onEsc) ->
+    open: ($el, onClose, onEsc, ignoreEsc) ->
         @.onClose = onClose

         if _.isString($el)
@@ -66,14 +66,15 @@
             lightboxContent.show()
             defered.resolve()

-        docEl = angular.element(document)
-        docEl.on "keydown.lightbox", (e) =>
-            code = if e.keyCode then e.keyCode else e.which
-            if code == 27
-                if onEsc
-                    @rootScope.$applyAsync(onEsc)
-                else
-                    @.close($el)
+        if !ignoreEsc
+            docEl = angular.element(document)
+            docEl.on "keydown.lightbox", (e) =>
+                code = if e.keyCode then e.keyCode else e.which
+                if code == 27
+                    if onEsc
+                        @rootScope.$applyAsync(onEsc)
+                    else
+                        @.close($el)

         return defered.promise
@@ -892,7 +893,7 @@
             setStatus($scope.obj.status)
             render()
             $scope.lightboxOpen = true
-            lightboxService.open($el)
+            lightboxService.open($el, null, null, true)

         resetAttachments = () ->
             attachmentsToAdd = Immutable.List()
@@ -1020,7 +1021,7 @@
                 deleteAttachments(data).then () ->
                     createAttachments(data).then () ->
                         currentLoading.finish()
-                        lightboxService.close($el)
+                        close()
                         $rs[schema.model].getByRef(data.project, data.ref, schema.params).then (obj) ->
                             $rootScope.$broadcast(broadcastEvent, obj)
             promise.then null, (data) ->
@@ -1031,14 +1032,27 @@

         checkClose = () ->
             if !$scope.obj.isModified()
-                lightboxService.close($el)
+                close()
                 $scope.$apply ->
                     $scope.obj.revert()
             else
                 $confirm.ask(
-                    $translate.instant("LIGHTBOX.CREATE_EDIT.CONFIRM_CLOSE")).then (result) ->
+                    $translate.instant("LIGHTBOX.CREATE_EDIT.CONFIRM_CLOSE"))
+                    .then (result) ->
                         result.finish()
-                        lightboxService.close($el)
+                        close()
+
+        close = () ->
+            lightboxService.close($el)
+            $scope.lightboxOpen = false
+
+        docEl = angular.element(document)
+        docEl.on "keydown.lightbox-create-edit", (event) ->
+            if $scope.lightboxOpen
+                event.stopPropagation()
+                code = if event.keyCode then event.keyCode else event.which
+                if code == 27
+                    checkClose()

         $el.on "submit", "form", submit

@@ -1046,12 +1060,6 @@
             event.preventDefault()
             event.stopPropagation()
             checkClose()
-
-        $el.keydown (event) ->
-            event.stopPropagation()
-            code = if event.keyCode then event.keyCode else event.which
-            if code == 27
-                checkClose()

         $el.on "click", ".status-dropdown", (event) ->
             event.preventDefault()


cleaning up unused import

cleaning up unused import

--- js/src/service/SharingClient.coffee
+++ js/src/service/SharingClient.coffee
@@ -7,7 +7,6 @@
   'kryptnostic.directory-api'
   'kryptnostic.kryptnostic-engine'
   'kryptnostic.logger'
-  'kryptnostic.object-sharing-service'
   'kryptnostic.revocation-request'
   'kryptnostic.rsa-crypto-service'
   'kryptnostic.sharing-api'
@@ -28,7 +27,6 @@
   CryptoServiceLoader     = require 'kryptnostic.crypto-service-loader'
   CryptoServiceMarshaller = require 'kryptnostic.crypto-service-marshaller'
   KryptnosticEngine       = require 'kryptnostic.kryptnostic-engine'
-  ObjectSharingService    = require 'kryptnostic.object-sharing-service'
   RevocationRequest       = require 'kryptnostic.revocation-request'
   RsaCryptoService        = require 'kryptnostic.rsa-crypto-service'
   SharingRequest          = require 'kryptnostic.sharing-request'
@@ -54,7 +52,6 @@
       @cryptoServiceMarshaller = new CryptoServiceMarshaller()
       @cryptoServiceLoader     = new CryptoServiceLoader()
       @credentialLoader        = new CredentialLoader()
-      @objectSharingService    = new ObjectSharingService()

     #
     # shares an object with the given object ID with the given set of user UUIDs


Re-work tutorial for smaller puzzle

Re-work tutorial for smaller puzzle

--- src/views/game.coffee
+++ src/views/game.coffee
@@ -54,10 +54,8 @@
   hint: null

   initialize: ->
-    # Ensure 'this' has correct context
-    _.bindAll @, 'onActionStart', 'onActionMove', 'onActionEnd', 'doAction', 'checkCompleted', 'win', 'pause', 'changeAction', 'updateTimer', 'showTutorial', 'resize', 'hide', 'show'
-
-    # View is initialized hidden
+    _.bindAll(@, 'updateTimer')
+
     @elem = $(template())
     @render()

@@ -137,7 +135,9 @@
     @$('.vertical.clue').eq(@previousCol).removeClass('highlight')
     @$('.horizontal.clue').eq(@previousRow).removeClass('highlight')

-    # Unset the "mark lock" -- If user starts marking blocks, only allow marking for the duration of that touch, so that they don't accidentally un-mark a block
+    # Unset the "mark lock" -- If user starts marking blocks, only allow
+    # marking for the duration of that touch, so that they don't accidentally
+    # un-mark a block
     @actionLock = "none"

     # Clear out previous touch data
@@ -191,7 +191,8 @@
             x: block.offset().left
             y: block.offset().top

-        # Instantly call the update method - this method adds 1 second, which is why the previous values are -1 second
+        # Instantly call the update method - this method adds 1 second,
+        # which is why the previous values are -1 second
         @updateTimer()

         # Try to have some visual indication that you made a wrong move
@@ -200,12 +201,12 @@
     else if @action is 'mark'
       # Mark if block is empty
       if block.hasClass('marked') == false and block.hasClass('filled') == false and @actionLock != "remove"
-        block.addClass 'marked pulse'
+        block.addClass('marked')
         @actionLock = "mark"
         @trigger 'sfx:play', 'mark'
       # Remove mark if already marked
       else if block.hasClass('marked') == true and block.hasClass('filled') == false and @actionLock != "mark"
-        block.removeClass 'marked pulse'
+        block.removeClass('marked')
         @actionLock = "remove"
         @trigger 'sfx:play', 'mark'
       # Play a "click" noise if block is filled
@@ -225,14 +226,18 @@
       rowIndex = row * @cellCount + i
       columnIndex = i * @cellCount + col

-      if blocks.eq(rowIndex).hasClass('filled') then completedRowTotal++
-      if blocks.eq(columnIndex).hasClass('filled') then completedColumnTotal++
-
-      if @clues[rowIndex] is 1 then rowTotal++
-      if @clues[columnIndex] is 1 then columnTotal++
-
-    if rowTotal == completedRowTotal then @$('.horizontal.clue', @elem).eq(row).addClass('complete')
-    if columnTotal == completedColumnTotal then @$('.vertical.clue', @elem).eq(col).addClass('complete')
+      if blocks.eq(rowIndex).hasClass('filled')
+        completedRowTotal += 1
+      if blocks.eq(columnIndex).hasClass('filled')
+        completedColumnTotal += 1
+
+      if @clues[rowIndex] is 1 then rowTotal += 1
+      if @clues[columnIndex] is 1 then columnTotal += 1
+
+    if rowTotal is completedRowTotal
+      @$('.horizontal.clue', @elem).eq(row).addClass('complete')
+    if columnTotal is completedColumnTotal
+      @$('.vertical.clue', @elem).eq(col).addClass('complete')

   # Actions when player solves the puzzle
   win: ->
@@ -283,9 +288,7 @@
   # Go back to the level select screen
   pause: (e) ->
     e?.preventDefault()
-
-    # Handle a case when this "pause" method is called when user puts app into background
-    if @ignoreInput is true then return
+    return if @ignoreInput

     @ignoreInput = true

@@ -362,33 +365,50 @@
       # Check if user placed the correct rect
       switch @tutorialStep
         when 4
-          # Make sure first column is filled
+          # Ensure first column is filled correctly
           stepCompleted = true
-          for i in [0..9]
-            if blocks.eq(i * 10).hasClass('filled') is false then stepCompleted = false
-          success = stepCompleted
-        when 5
-          # Make sure last row is filled
-          stepCompleted = true
-          for i in [91..99]
+          for i in [0, 5, 10, 15, 20]
             if blocks.eq(i).hasClass('filled') is false then stepCompleted = false
           success = stepCompleted
         when 8
-          # Make sure first row/last column are marked
+          # Ensure 3rd column is filled correctly
           stepCompleted = true
-          for i in [1..9]
+          for i in [2, 12, 22]
+            if blocks.eq(i).hasClass('filled') is false then stepCompleted = false
+          success = stepCompleted
+        when 11
+          # Ensure 3rd column is marked correctly
+          stepCompleted = true
+          for i in [7, 17]
             if blocks.eq(i).hasClass('marked') is false then stepCompleted = false
-          for i in [0..8]
-            if blocks.eq(i * 10 + 9).hasClass('marked') is false then stepCompleted = false
           success = stepCompleted
-        when 10
+        when 12
+          # Ensure 4th column is filled correctly
           stepCompleted = true
-          for i in [1..9]
-            if blocks.eq(i * 10 + 1).hasClass('filled') is false then stepCompleted = false
+          for i in [3, 13, 18, 23]
+            if blocks.eq(i).hasClass('filled') is false then stepCompleted = false
           success = stepCompleted
+        when 13
+          # Ensure 5th column is all marked
+          stepCompleted = true
+          for i in [4, 9, 14, 19, 24]
+            if blocks.eq(i).hasClass('marked') is false then stepCompleted = false
+          success = stepCompleted
+        when 15
+          # Ensure 1st row is filled correctly
+          stepCompleted = true
+          for i in [0...4]
+            if blocks.eq(i).hasClass('filled') is false then stepCompleted = false
+          success = stepCompleted
+        when 16
+          # Ensure rest of squares marked correctly
+          stepCompleted = true
+          for i in [6, 7, 8, 11, 16, 17]
+            if blocks.eq(i).hasClass('marked') is false then stepCompleted = false
+          success = stepCompleted

       if success
-        @tutorialStep++
+        @tutorialStep += 1
         @showTutorial()

@@ -399,51 +419,32 @@
     actionWord = if ENV.mobile then "tap" else "click"
     capitalActionWord = if ENV.mobile then "Tap" else "Click"

-    text = [
-      "Welcome to Nonogram Madness! Nonograms are logic puzzles that reveal an image when solved." # 0
-      "Solve each puzzle using the numeric clues on the top and left of the grid." # 1
-      "Each number represents squares in the grid that are \"filled\" in a row or column." # 2
-      "Clues with multiple numbers represent a gap between filled squares." # 3
-      "Look at the first column. The clue is \"10\". #{capitalActionWord} the \"fill\" button then #{actionWord} all 10 squares." # 4
-      # Action
-      "Check out the last row. Its clue is also \"10\". #{capitalActionWord} all 10 squares in the bottom row." # 5
-      # Action
-      "Notice that when you complete a row or column, its clues dim slightly, so you know you're done." # 6
-      "You can also mark squares that you know are blank, then focus on other areas of the puzzle." #7
-      "#{capitalActionWord} the \"mark\" button and mark the row and column that are complete." #8
-      # Action
-      "You can now logically solve the rest of the rows and columns." #9
-      "Column #2 has 9 filled blocks, and the first block is empty. Go ahead and fill this column." #10
-      # Action
-      "I think you've got the hang of it. Try to finish the rest of the puzzle yourself!" # 11
-    ]
     text = [
       "Welcome to Nonogram Madness! Nonograms are logic puzzles that reveal an image when solved.",
       "Solve each puzzle using the numeric clues on the top and left of the grid.",
       "Each number represents squares in the grid that are \"filled\" in a row or column.",
       "Clues with multiple numbers mean a gap of one (or more) between filled squares.",
       "Look at the first column. The clue is \"5\". Tap \"fill\" then tap all 5 squares.",
-      # Action
+      # Action - 4
       "The second column is harder. We don't know where the two single filled squares are.",
       "Skip difficult rows or columns and come back to them later.",
       "Look at the third column. The clue is \"1 1 1\". There's a gap between each filled square.",
       "Make sure the \"fill\" button is selected, then fill in three squares with a gap between each.",
-      # Action
+      # Action - 8
       "You can use the \"mark\" action to protect blocks that are supposed to be empty.",
-      "Erase a marked square by tapping it again. Don't worry about making a mistake.",
+      "Erase a marked square by selecting it again.",
       "#{capitalActionWord} \"mark\" and mark the empty squares so you don't accidentally try to fill them in later.",
-      # Action
+      # Action - 11
       "Check out the fourth column. The clue is \"1 3\". Fill one square, leave a gap, then fill three more.",
-      # Action
+      # Action - 12
       "The fifth column is empty. \"Mark\" all those squares to show they don't need to be filled in.",
-      # Action
+      # Action - 13
       "Next, look at the horizontal clues. The first row has four sequential filled squares.",
       "Fill in the only open square in this row to complete it.",
-      # Action
+      # Action - 15
       "The second, third, and fourth rows are already complete. Mark all the open squares in them.",
-      # Action
-      "I think you've got the hang of it. Try to finish the rest of the puzzle yourself!"
-      # Action
+      # Action - 16
+      "I think you've got the hang of it. Finish the rest of the puzzle yourself!"
     ]

     blocks = @grid.find('.blank')
@@ -454,19 +455,26 @@
     # Show hint overlays here
     switch @tutorialStep
       when 4
-        for i in [0..9]
-          blocks.eq(i * 10).addClass 'hint'
-      when 5
-        for i in [91..99]
-          blocks.eq(i).addClass 'hint'
+        for i in [0, 5, 10, 15, 20]
+          blocks.eq(i).addClass('hint')
       when 8
-        for i in [1..9]
-          blocks.eq(i).addClass 'hint'
-        for i in [0..8]
-          blocks.eq(i * 10 + 9).addClass 'hint'
-      when 10
-        for i in [1..9]
-          blocks.eq(i * 10 + 1).addClass 'hint'
+        for i in [2, 12, 22]
+          blocks.eq(i).addClass('hint')
+      when 11
+        for i in [7, 17]
+          blocks.eq(i).addClass('hint')
+      when 12
+        for i in [3, 13, 18, 23]
+          blocks.eq(i).addClass('hint')
+      when 13
+        for i in [4, 9, 14, 19, 24]
+          blocks.eq(i).addClass('hint')
+      when 15
+        for i in [0...4]
+          blocks.eq(i).addClass('hint')
+      when 16
+        for i in [6, 8, 11, 16]
+          blocks.eq(i).addClass('hint')

     new DialogBox
       parent: @
@@ -478,8 +486,8 @@
           @ignoreInput = false

           # These steps indicate where the player has to take action
-          if [ 4, 5, 8, 10 ].indexOf(@tutorialStep) == -1 and @tutorialStep < text.length - 1
-            @tutorialStep++
+          if [4, 8, 11, 12, 13, 15, 16].indexOf(@tutorialStep) == -1 and @tutorialStep < text.length - 1
+            @tutorialStep += 1
             @showTutorial()
       }]


minor: removed log / codestyling

minor: removed log / codestyling

--- servers/lib/server/handlers/checktoken.coffee
+++ servers/lib/server/handlers/checktoken.coffee
@@ -7,14 +7,12 @@
   { JInvitation, JGroup } = koding.models
   { token }               = body

-  return res.status(400).send "token is required"  unless token
-
-  console.log token
+  return res.status(400).send 'token is required'  unless token

   JInvitation.byCode token, (err, data) ->
     if err
-      console.error "err while fetching token"
-      return res.status(500).send "internal server error"
+      console.error 'err while fetching token'
+      return res.status(500).send 'internal server error'

-    return res.status(404).send "invitation not found"  unless data
+    return res.status(404).send 'invitation not found'  unless data
     return res.status(200).send data


rename

rename

--- spec/vim-mode-plus-jasmine-increase-focus-spec.coffee
+++ spec/vim-mode-plus-jasmine-increase-focus-spec.coffee
@@ -4,7 +4,7 @@

 {getVimState} = requireFrom 'vim-mode-plus', 'spec/spec-helper'

-activatePackageViaActivationCommand = (name, fn) ->
+activatePackageByActivationCommand = (name, fn) ->
   activationPromise = atom.packages.activatePackage(name)
   fn()
   activationPromise
@@ -19,10 +19,8 @@
         '-': 'vim-mode-plus-user:jasmine-decrease-focus'
       , 100

-    activationPromise = null
-
     waitsForPromise ->
-      activatePackageViaActivationCommand 'vim-mode-plus-jasmine-increase-focus', ->
+      activatePackageByActivationCommand 'vim-mode-plus-jasmine-increase-focus', ->
         atom.workspace.open().then (editor) ->
           atom.commands.dispatch(editor.element, "vim-mode-plus-user:jasmine-increase-focus")


zendesk: typo

zendesk: typo

--- src/smc-hub/support.coffee
+++ src/smc-hub/support.coffee
@@ -197,7 +197,7 @@
                     @_zd.users.request 'POST', ['users', 'create_or_update'], user, (err, req, result) =>
                         if err
                             dbg("create_or_update user error: #{misc.to_json(err)}")
-                            if err.result?.type? == "Buffer"
+                            if err.result?.type == "Buffer"
                                 err_msg = err.result.data.map((c) -> String.fromCharCode(c)).join('')
                                 dbg("create_or_update zendesk message: #{err_msg}")
                             cb(err); return


change our github link to point directly at our bug list.

--- src/smc-webapp/r_help.cjsx
+++ src/smc-webapp/r_help.cjsx
@@ -168,12 +168,11 @@
     github :
         icon : 'github-square'
         href : 'https://github.com/sagemathinc/smc'
-        link : 'Complete source code'
-        text : <span>(SageMathCloud is 100% open source)</span>
+        link : 'SageMathCloud source code'
     github_issue_tracker :
         icon : 'exclamation-circle'
-        href : 'https://github.com/sagemathinc/smc/issues'
-        link : 'Github issue tracker'
+        href : 'https://github.com/sagemathinc/smc/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3AI-bug%20sort%3Acreated-asc%20-label%3Ablocked'
+        link : 'Known bugs in SageMathCloud'
     support_mailing_list :
         icon : 'life-ring'
         href : 'https://groups.google.com/forum/?fromgroups#!forum/sage-cloud'


Remove Icon from r_misc

Remove Icon from r_misc

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -26,6 +26,8 @@
 {UpgradeRestartWarning} = require('./upgrade_restart_warning')
 copy_to_clipboard = require('copy-to-clipboard')
 {reportException} = require('../webapp-lib/webapp-error-reporter')
+{Icon} = require('./icon')
+exports.Icon = Icon

 # injected by webpack, but not for react-static renderings (ATTN don't assign to uppercase vars!)
 smc_version = SMC_VERSION ? 'N/A'
@@ -117,115 +119,6 @@

 exports.Space = Space = ->
     <span> </span>
-
-# Font Awesome component -- obviously TODO move to own file
-# Converted from https://github.com/andreypopp/react-fa
-exports.Icon = Icon = rclass
-    displayName : 'Icon'
-
-    propTypes :
-        name       : rtypes.string
-        size       : rtypes.oneOf(['lg', '2x', '3x', '4x', '5x'])
-        rotate     : rtypes.oneOf(['45', '90', '135', '180', '225', '270', '315'])
-        flip       : rtypes.oneOf(['horizontal', 'vertical'])
-        spin       : rtypes.bool
-        pulse      : rtypes.bool
-        fixedWidth : rtypes.bool
-        stack      : rtypes.oneOf(['1x', '2x'])
-        inverse    : rtypes.bool
-        className  : rtypes.string
-        style      : rtypes.object
-        onClick    : rtypes.func
-        onMouseOver: rtypes.func
-        onMouseOut : rtypes.func
-
-    shouldComponentUpdate: (next) ->  # we exclude style changes for speed reasons (and style is rarely used); always update if there are children
-        return @props.children? or \
-               misc.is_different(@props, next, ['name', 'size', 'rotate', 'flip', 'spin', 'pulse', 'fixedWidth', \
-                                          'stack', 'inverse', 'className']) or \
-               not misc.is_equal(@props.style, next.style)
-
-
-    getDefaultProps: ->
-        name    : 'square-o'
-        onClick : ->
-
-    render_icon: ->
-        {name, size, rotate, flip, spin, pulse, fixedWidth, stack, inverse, className} = @props
-
-        i = name.indexOf('cc-icon')
-
-        if i != -1 and spin
-            # Temporary workaround because cc-icon-cocalc-ring is not a font awesome JS+SVG icon, so
-            # spin, etc., doesn't work on it.  There is a discussion at
-            # https://stackoverflow.com/questions/19364726/issue-making-bootstrap3-icon-spin
-            # about spinning icons, but it's pretty subtle and hard to get right, so I hope
-            # we don't have to implement our own.  Also see
-            # "Icon animation wobble foibles" at https://fontawesome.com/how-to-use/web-fonts-with-css
-            # where they say "witch to the SVG with JavaScript version, it's working a lot better for this".
-            name = 'fa-circle-notch'
-            i = -1
-
-        if i != -1
-            # A custom Cocalc font icon.  Don't even bother with font awesome at all!
-            classNames = name.slice(i)
-        else
-            left = name.slice(0,3)
-            if left == 'fas' or left == 'fab' or left == 'far'
-                # version 5 names are different!  https://fontawesome.com/how-to-use/use-with-node-js
-                # You give something like: 'fas fa-blah'.
-                classNames = name
-            else
-                # temporary until file_associations can be changed
-                if name.slice(0, 3) == 'cc-' and name isnt 'cc-stripe'
-                    classNames = "fab #{name}"
-                    # the cocalc icon font can't do any extra tricks
-                else
-                    # temporary until file_associations can be changed
-                    if name.slice(0, 3) == 'fa-'
-                        classNames = "fa #{name}"
-                    else
-                        classNames = "fa fa-#{name}"
-            # These only make sense for font awesome.
-            if size
-                classNames += " fa-#{size}"
-            if rotate
-                classNames += " fa-rotate-#{rotate}"
-            if flip
-                classNames += " fa-flip-#{flip}"
-            if fixedWidth
-                classNames += ' fa-fw'
-            if spin
-                classNames += ' fa-spin'
-            if pulse
-                classNames += ' fa-pulse'
-            if stack
-                classNames += " fa-stack-#{stack}"
-            if inverse
-                classNames += ' fa-inverse'
-
-        if className
-            classNames += " #{className}"
-        <i className={classNames} />
-
-    render: ->
-        # Wrap in a span for **two** reasons.
-        # 1. A reasonable one -- have to wrap the i, since when rendered using js and svg by new fontawesome 5,
-        # the click handlers of the <i> object are just ignored, since it is removed from the DOM!
-        # This is important the close button on tabs.
-        # 2. An evil one -- FontAwesome's javascript mutates the DOM.  Thus we put a random key in so,
-        # that React just replaces the whole part of the DOM where the SVG version of the icon is,
-        # and doesn't get tripped up by this.   A good example where this is used is when *running* Jupyter
-        # notebooks.
-        <span
-            onClick     = {@props.onClick}
-            onMouseOver = {@props.onMouseOver}
-            onMouseOut  = {@props.onMouseOut}
-            key         = {Math.random()}
-            style       = {@props.style}
-        >
-            {@render_icon()}
-        </span>

 # this Octicon icon class requires the CSS file in octicons/octicons/octicons.css (see landing.coffee)
 exports.Octicon = rclass


英文拼错了

英文拼错了

--- menus/atom-butterfly.cson
+++ menus/atom-butterfly.cson
@@ -40,7 +40,7 @@
         'label': 'New'
         'submenu': [
           {
-            'label': 'Porject...'
+            'label': 'Project...'
             'command': 'atom-butterfly:create-project'
           }
           {


Roughly desactivate suggestions

Roughly desactivate suggestions

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -647,165 +647,165 @@

             # Look for special commands
             if event?.which?
-                if event.which is 27 # ESC
-                    event.preventDefault() # Keep focus on code mirror
-                    @hide_suggestion_and_description()
-                    return true
-                else if event.which is 13 and (event.shiftKey is false and event.ctrlKey is false and event.metaKey is false)
-                    if event.type is 'keydown'
-                        if @current_highlighted_suggestion > -1
-                            event.preventDefault()
-                            @handle_keypress()
-                            return true
-
-                        previous_char = @get_previous_char()
-                        if previous_char of @matching_opening_bracket
-                            next_char = @get_next_char()
-                            if @matching_opening_bracket[previous_char] is next_char
-                                cursor = @codemirror.getCursor()
-                                @insert_next '\n'
-                                @codemirror.indentLine cursor.line+1, 'smart'
-                                @codemirror.setCursor cursor
-                                return false
-                else if event.which is 9 # If the user hit tab, we switch the highlighted suggestion
-                    event.preventDefault()
-                    if event.type is 'keydown'
-                        if @current_suggestions?.length > 0
-                            if @$('.suggestion_name_list').css('display') is 'none'
-                                @show_suggestion()
+                if @options.suggestions is true
+                    if event.which is 27 # ESC
+                        event.preventDefault() # Keep focus on code mirror
+                        @hide_suggestion_and_description()
+                        return true
+                    else if event.which is 13 and (event.shiftKey is false and event.ctrlKey is false and event.metaKey is false)
+                        if event.type is 'keydown'
+                            if @current_highlighted_suggestion > -1
+                                event.preventDefault()
+                                @handle_keypress()
                                 return true
-                            else
-                                # We can retrieve the content of codemirror only on keyup events. The users may write "r." then hit "d" then "tab" If the events are triggered this way
-                                # keydown d - keydown tab - keyup d - keyup tab
-                                # We want to only show the suggestions for r.d
-                                if @written_suggestion is null
-                                    cached_query = @query_first_part+@current_element+@query_last_part
+
+                            previous_char = @get_previous_char()
+                            if previous_char of @matching_opening_bracket
+                                next_char = @get_next_char()
+                                if @matching_opening_bracket[previous_char] is next_char
+                                    cursor = @codemirror.getCursor()
+                                    @insert_next '\n'
+                                    @codemirror.indentLine cursor.line+1, 'smart'
+                                    @codemirror.setCursor cursor
+                                    return false
+                    else if event.which is 9 # If the user hit tab, we switch the highlighted suggestion
+                        event.preventDefault()
+                        if event.type is 'keydown'
+                            if @current_suggestions?.length > 0
+                                if @$('.suggestion_name_list').css('display') is 'none'
+                                    @show_suggestion()
+                                    return true
                                 else
-                                    cached_query = @query_first_part+@written_suggestion+@query_last_part
-                                if cached_query isnt @codemirror.getValue() # We fired a keydown tab before a keyup, so our suggestions are not up to date
-                                    @current_element = @codemirror.getValue().slice @query_first_part.length, @codemirror.getValue().length-@query_last_part.length
-                                    regex = @create_safe_regex @current_element
-                                    new_suggestions = []
-                                    new_highlighted_suggestion = -1
-                                    for suggestion, index in @current_suggestions
-                                        if index < @current_highlighted_suggestion
-                                            new_highlighted_suggestion = new_suggestions.length
-                                        if regex.test(suggestion) is true
-                                            new_suggestions.push suggestion
-                                    @current_suggestions = new_suggestions
-                                    @current_highlighted_suggestion = new_highlighted_suggestion
-                                    if @current_suggestions.length > 0
-                                        @.$('.suggestion_name_list').empty()
-                                        for suggestion, i in @current_suggestions
-                                            @.$('.suggestion_name_list').append @template_suggestion_name
-                                                id: i
-                                                suggestion: suggestion
-                                        @ignored_next_keyup = true
+                                    # We can retrieve the content of codemirror only on keyup events. The users may write "r." then hit "d" then "tab" If the events are triggered this way
+                                    # keydown d - keydown tab - keyup d - keyup tab
+                                    # We want to only show the suggestions for r.d
+                                    if @written_suggestion is null
+                                        cached_query = @query_first_part+@current_element+@query_last_part
                                     else
-                                        @hide_suggestion_and_description()
-
-
-                                # Switch throught the suggestions
-                                if event.shiftKey
-                                    @current_highlighted_suggestion--
-                                    if @current_highlighted_suggestion < -1
-                                        @current_highlighted_suggestion = @current_suggestions.length-1
-                                    else if @current_highlighted_suggestion < 0
+                                        cached_query = @query_first_part+@written_suggestion+@query_last_part
+                                    if cached_query isnt @codemirror.getValue() # We fired a keydown tab before a keyup, so our suggestions are not up to date
+                                        @current_element = @codemirror.getValue().slice @query_first_part.length, @codemirror.getValue().length-@query_last_part.length
+                                        regex = @create_safe_regex @current_element
+                                        new_suggestions = []
+                                        new_highlighted_suggestion = -1
+                                        for suggestion, index in @current_suggestions
+                                            if index < @current_highlighted_suggestion
+                                                new_highlighted_suggestion = new_suggestions.length
+                                            if regex.test(suggestion) is true
+                                                new_suggestions.push suggestion
+                                        @current_suggestions = new_suggestions
+                                        @current_highlighted_suggestion = new_highlighted_suggestion
+                                        if @current_suggestions.length > 0
+                                            @.$('.suggestion_name_list').empty()
+                                            for suggestion, i in @current_suggestions
+                                                @.$('.suggestion_name_list').append @template_suggestion_name
+                                                    id: i
+                                                    suggestion: suggestion
+                                            @ignored_next_keyup = true
+                                        else
+                                            @hide_suggestion_and_description()
+
+
+                                    # Switch throught the suggestions
+                                    if event.shiftKey
+                                        @current_highlighted_suggestion--
+                                        if @current_highlighted_suggestion < -1
+                                            @current_highlighted_suggestion = @current_suggestions.length-1
+                                        else if @current_highlighted_suggestion < 0
+                                            @show_suggestion_without_moving()
+                                            @remove_highlight_suggestion()
+                                            @write_suggestion
+                                                suggestion_to_write: @current_element
+                                            @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
+                                            return true
+                                    else
+                                        @current_highlighted_suggestion++
+                                        if @current_highlighted_suggestion >= @current_suggestions.length
+                                            @show_suggestion_without_moving()
+                                            @remove_highlight_suggestion()
+                                            @write_suggestion
+                                                suggestion_to_write: @current_element
+                                            @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
+                                            @current_highlighted_suggestion = -1
+                                            return true
+                                    if @current_suggestions[@current_highlighted_suggestion]?
                                         @show_suggestion_without_moving()
-                                        @remove_highlight_suggestion()
+                                        @highlight_suggestion @current_highlighted_suggestion # Highlight the current suggestion
                                         @write_suggestion
-                                            suggestion_to_write: @current_element
+                                            suggestion_to_write: @current_suggestions[@current_highlighted_suggestion] # Auto complete with the highlighted suggestion
                                         @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
                                         return true
-                                else
-                                    @current_highlighted_suggestion++
-                                    if @current_highlighted_suggestion >= @current_suggestions.length
-                                        @show_suggestion_without_moving()
-                                        @remove_highlight_suggestion()
-                                        @write_suggestion
-                                            suggestion_to_write: @current_element
-                                        @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
-                                        @current_highlighted_suggestion = -1
-                                        return true
-                                if @current_suggestions[@current_highlighted_suggestion]?
-                                    @show_suggestion_without_moving()
-                                    @highlight_suggestion @current_highlighted_suggestion # Highlight the current suggestion
+                            else if @extra_suggestions? and @extra_suggestions.length > 0 and @extra_suggestion.start_body is @extra_suggestion.start_body
+                                # Trim suggestion
+                                if @extra_suggestion?.body?[0]?.type is 'string'
+                                    if @extra_suggestion.body[0].complete is true
+                                        @extra_suggestions = []
+                                    else
+                                        # Remove quotes around the table/db name
+                                        current_name = @extra_suggestion.body[0].name.replace(/^\s*('|")/, '').replace(/('|")\s*$/, '')
+                                        regex = @create_safe_regex current_name
+                                        new_extra_suggestions = []
+                                        for suggestion in @extra_suggestions
+                                            if regex.test(suggestion) is true
+                                                new_extra_suggestions.push suggestion
+                                        @extra_suggestions = new_extra_suggestions
+
+                                if @extra_suggestions.length > 0 # If there are still some valid suggestions
+                                    query = @codemirror.getValue()
+
+                                    # We did not parse what is after the cursor, so let's take a look
+                                    start_search = @extra_suggestion.start_body
+                                    if @extra_suggestion.body?[0]?.name.length?
+                                        start_search += @extra_suggestion.body[0].name.length
+
+                                    # Define @query_first_part and @query_last_part
+                                    # Note that ) is not a valid character for a db/table name
+                                    end_body = query.indexOf ')', start_search
+                                    @query_last_part = ''
+                                    if end_body isnt -1
+                                        @query_last_part = query.slice end_body
+                                    @query_first_part = query.slice 0, @extra_suggestion.start_body
+                                    lines = @query_first_part.split('\n')
+                                    # Because we may have slice before @cursor_for_auto_completion, we re-define it
+                                    @cursor_for_auto_completion =
+                                        line: lines.length-1
+                                        ch: lines[lines.length-1].length
+
+                                    if event.shiftKey is true
+                                        @current_highlighted_extra_suggestion--
+                                    else
+                                        @current_highlighted_extra_suggestion++
+
+                                    if @current_highlighted_extra_suggestion >= @extra_suggestions.length
+                                        @current_highlighted_extra_suggestion = -1
+                                    else if @current_highlighted_extra_suggestion < -1
+                                        @current_highlighted_extra_suggestion = @extra_suggestions.length-1
+
+                                    # Create the next suggestion
+                                    suggestion = ''
+                                    if @current_highlighted_extra_suggestion is -1
+                                        if @current_extra_suggestion?
+                                            if /^\s*'/.test(@current_extra_suggestion) is true
+                                                suggestion = @current_extra_suggestion+"'"
+                                            else if /^\s*"/.test(@current_extra_suggestion) is true
+                                                suggestion = @current_extra_suggestion+'"'
+                                    else
+                                        if /^\s*'/.test(@current_extra_suggestion) is true
+                                            string_delimiter = "'"
+                                        else if /^\s*"/.test(@current_extra_suggestion) is true
+                                            string_delimiter = '"'
+                                        else
+                                            move_outside = true
+                                            string_delimiter = "'"
+                                        suggestion = string_delimiter+@extra_suggestions[@current_highlighted_extra_suggestion]+string_delimiter
+
                                     @write_suggestion
-                                        suggestion_to_write: @current_suggestions[@current_highlighted_suggestion] # Auto complete with the highlighted suggestion
+                                        move_outside: move_outside
+                                        suggestion_to_write: suggestion
                                     @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
-                                    return true
-                        else if @extra_suggestions? and @extra_suggestions.length > 0 and @extra_suggestion.start_body is @extra_suggestion.start_body
-                            # Trim suggestion
-                            if @extra_suggestion?.body?[0]?.type is 'string'
-                                if @extra_suggestion.body[0].complete is true
-                                    @extra_suggestions = []
-                                else
-                                    # Remove quotes around the table/db name
-                                    current_name = @extra_suggestion.body[0].name.replace(/^\s*('|")/, '').replace(/('|")\s*$/, '')
-                                    regex = @create_safe_regex current_name
-                                    new_extra_suggestions = []
-                                    for suggestion in @extra_suggestions
-                                        if regex.test(suggestion) is true
-                                            new_extra_suggestions.push suggestion
-                                    @extra_suggestions = new_extra_suggestions
-
-                            if @extra_suggestions.length > 0 # If there are still some valid suggestions
-                                query = @codemirror.getValue()
-
-                                # We did not parse what is after the cursor, so let's take a look
-                                start_search = @extra_suggestion.start_body
-                                if @extra_suggestion.body?[0]?.name.length?
-                                    start_search += @extra_suggestion.body[0].name.length
-
-                                # Define @query_first_part and @query_last_part
-                                # Note that ) is not a valid character for a db/table name
-                                end_body = query.indexOf ')', start_search
-                                @query_last_part = ''
-                                if end_body isnt -1
-                                    @query_last_part = query.slice end_body
-                                @query_first_part = query.slice 0, @extra_suggestion.start_body
-                                lines = @query_first_part.split('\n')
-                                # Because we may have slice before @cursor_for_auto_completion, we re-define it
-                                @cursor_for_auto_completion =
-                                    line: lines.length-1
-                                    ch: lines[lines.length-1].length
-
-                                if event.shiftKey is true
-                                    @current_highlighted_extra_suggestion--
-                                else
-                                    @current_highlighted_extra_suggestion++
-
-                                if @current_highlighted_extra_suggestion >= @extra_suggestions.length
-                                    @current_highlighted_extra_suggestion = -1
-                                else if @current_highlighted_extra_suggestion < -1
-                                    @current_highlighted_extra_suggestion = @extra_suggestions.length-1
-
-                                # Create the next suggestion
-                                suggestion = ''
-                                if @current_highlighted_extra_suggestion is -1
-                                    if @current_extra_suggestion?
-                                        if /^\s*'/.test(@current_extra_suggestion) is true
-                                            suggestion = @current_extra_suggestion+"'"
-                                        else if /^\s*"/.test(@current_extra_suggestion) is true
-                                            suggestion = @current_extra_suggestion+'"'
-                                else
-                                    if /^\s*'/.test(@current_extra_suggestion) is true
-                                        string_delimiter = "'"
-                                    else if /^\s*"/.test(@current_extra_suggestion) is true
-                                        string_delimiter = '"'
-                                    else
-                                        move_outside = true
-                                        string_delimiter = "'"
-                                    suggestion = string_delimiter+@extra_suggestions[@current_highlighted_extra_suggestion]+string_delimiter
-
-                                @write_suggestion
-                                    move_outside: move_outside
-                                    suggestion_to_write: suggestion
-                                @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
-

                 # If the user hit enter and (Ctrl or Shift)
-                else if event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey)
+                if event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey)
                     @hide_suggestion_and_description()
                     event.preventDefault()
                     if event.type isnt 'keydown'
@@ -910,84 +910,85 @@
             if event?.which is 16 # We don't do anything with Shift.
                 return false

-            # Tab is an exception, we let it pass (tab bring back suggestions) - What we want is to catch keydown
-            if @ignore_tab_keyup is true and event?.which is 9
-                if event.type is 'keyup'
-                    @ignore_tab_keyup = false
-                return true
-
-            @current_highlighted_suggestion = -1
-            @current_highlighted_extra_suggestion = -1
-            @.$('.suggestion_name_list').empty()
-
-            # Valid step, let's save the data
-            @query_last_part = query_after_cursor
-
-            # If a selection is active, we just catch shift+enter
-            if @codemirror.getSelection() isnt ''
-                @hide_suggestion_and_description()
-                if event? and event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey) # If the user hit enter and (Ctrl or Shift or Cmd)
+            if @options.suggestions is true
+                # Tab is an exception, we let it pass (tab bring back suggestions) - What we want is to catch keydown
+                if @ignore_tab_keyup is true and event?.which is 9
+                    if event.type is 'keyup'
+                        @ignore_tab_keyup = false
+                    return true
+
+                @current_highlighted_suggestion = -1
+                @current_highlighted_extra_suggestion = -1
+                @.$('.suggestion_name_list').empty()
+
+                # Valid step, let's save the data
+                @query_last_part = query_after_cursor
+
+                # If a selection is active, we just catch shift+enter
+                if @codemirror.getSelection() isnt ''
                     @hide_suggestion_and_description()
-                    if event.type isnt 'keydown'
+                    if event? and event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey) # If the user hit enter and (Ctrl or Shift or Cmd)
+                        @hide_suggestion_and_description()
+                        if event.type isnt 'keydown'
+                            return true
+                        @execute_query()
+                        return true # We do not replace the selection with a new line
+                    # If the user select something and end somehwere with suggestion
+                    if event?.type isnt 'mouseup'
+                        return false
+                    else
                         return true
-                    @execute_query()
-                    return true # We do not replace the selection with a new line
-                # If the user select something and end somehwere with suggestion
-                if event?.type isnt 'mouseup'
-                    return false
-                else
-                    return true
-
-            @current_suggestions = []
-            @current_element = ''
-            @current_extra_suggestion = ''
-            @written_suggestion = null
-            @cursor_for_auto_completion = @codemirror.getCursor()
-
-            result =
-                status: null
-                # create_suggestion is going to fill to_complete and to_describe
-                #to_complete: undefined
-                #to_describe: undefined
-
-            # If we are in the middle of a function (text after the cursor - that is not an element in @char_breakers), we just show a description, not a suggestion
-            result_non_white_char_after_cursor = @regex.get_first_non_white_char.exec(query_after_cursor)
-
-            if result_non_white_char_after_cursor isnt null and not(result_non_white_char_after_cursor[1]?[0] of @char_breakers)
-                result.status = 'break_and_look_for_description'
-                @hide_suggestion()
-            else
-                result_last_char_is_white = @regex.last_char_is_white.exec(query_before_cursor[query_before_cursor.length-1])
-                if result_last_char_is_white isnt null
+
+                @current_suggestions = []
+                @current_element = ''
+                @current_extra_suggestion = ''
+                @written_suggestion = null
+                @cursor_for_auto_completion = @codemirror.getCursor()
+
+                result =
+                    status: null
+                    # create_suggestion is going to fill to_complete and to_describe
+                    #to_complete: undefined
+                    #to_describe: undefined
+
+                # If we are in the middle of a function (text after the cursor - that is not an element in @char_breakers), we just show a description, not a suggestion
+                result_non_white_char_after_cursor = @regex.get_first_non_white_char.exec(query_after_cursor)
+
+                if result_non_white_char_after_cursor isnt null and not(result_non_white_char_after_cursor[1]?[0] of @char_breakers)
                     result.status = 'break_and_look_for_description'
                     @hide_suggestion()
-
-            # Create the suggestion/description
-            @create_suggestion
-                stack: stack
-                query: query_before_cursor
-                result: result
-
-            if result.suggestions?.length > 0
-                for suggestion, i in result.suggestions
-                    @current_suggestions.push suggestion
-                    @.$('.suggestion_name_list').append @template_suggestion_name
-                        id: i
-                        suggestion: suggestion
-                @show_suggestion()
-                @hide_description()
-            else if result.description? and event?.type isnt 'mouseup'
-                @hide_suggestion()
-                @show_description result.description
-            else
-                @hide_suggestion_and_description()
-
-            if event?.which is 9 # Catch tab
-                # If you're in a string, you add a TAB. If you're at the beginning of a newline with preceding whitespace, you add a TAB. If it's any other case do nothing.
-                if @last_element_type_if_incomplete(stack) isnt 'string' and @regex.white_or_empty.test(@codemirror.getLine(@codemirror.getCursor().line).slice(0, @codemirror.getCursor().ch)) isnt true
-                    return true
                 else
-                    return false
+                    result_last_char_is_white = @regex.last_char_is_white.exec(query_before_cursor[query_before_cursor.length-1])
+                    if result_last_char_is_white isnt null
+                        result.status = 'break_and_look_for_description'
+                        @hide_suggestion()
+
+                # Create the suggestion/description
+                @create_suggestion
+                    stack: stack
+                    query: query_before_cursor
+                    result: result
+
+                if result.suggestions?.length > 0
+                    for suggestion, i in result.suggestions
+                        @current_suggestions.push suggestion
+                        @.$('.suggestion_name_list').append @template_suggestion_name
+                            id: i
+                            suggestion: suggestion
+                    @show_suggestion()
+                    @hide_description()
+                else if result.description? and event?.type isnt 'mouseup'
+                    @hide_suggestion()
+                    @show_description result.description
+                else
+                    @hide_suggestion_and_description()
+
+                if event?.which is 9 # Catch tab
+                    # If you're in a string, you add a TAB. If you're at the beginning of a newline with preceding whitespace, you add a TAB. If it's any other case do nothing.
+                    if @last_element_type_if_incomplete(stack) isnt 'string' and @regex.white_or_empty.test(@codemirror.getLine(@codemirror.getCursor().line).slice(0, @codemirror.getCursor().ch)) isnt true
+                        return true
+                    else
+                        return false

             return true


Remove relic from the past -- Related to #1222

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -2460,44 +2460,6 @@
                     @save_query
                         query: @raw_query
                         broken_query: false
-                else
-                    @execute_portion()
-
-        get_result_callback: (error, data) =>
-            if error?
-                @results_view.render_error(@query, error)
-                @save_query
-                    query: @raw_query
-                    broken_query: true
-                return false
-
-            if data isnt undefined
-                @current_results.push data
-                if @current_results.length < @limit and @cursor.hasNext() is true
-                    @cursor.next @get_result_callback
-                    return true
-
-            # if data is undefined or @current_results.length is @limit
-            @saved_data.cursor = @cursor # Let's save the cursor, there may be mor edata to retrieve
-            @saved_data.query = @query
-            @saved_data.results = @current_results
-            @saved_data.metadata =
-                limit_value: @current_results.length
-                skip_value: @skip_value
-                execution_time: new Date() - @start_time
-                query: @query
-                has_more_data: @cursor.hasNext()
-
-            @results_view.render_result
-                results: @current_results # The first parameter is null ( = query, so we don't display it)
-                metadata: @saved_data.metadata
-
-            # Successful query, let's save it in the history
-            @save_query
-                query: @raw_query
-                broken_query: false
-
-            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


Add an extra check for event.which is 91 // Related to #662

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -409,7 +409,7 @@
                     return true

                 char_to_insert = String.fromCharCode event.which
-                if char_to_insert?
+                if char_to_insert? and event.which isnt 91 # 91 map to [ on OS X
                     if @codemirror.getSelection() isnt ''
                         if (char_to_insert of @matching_opening_bracket or char_to_insert of @matching_closing_bracket)
                             @codemirror.replaceSelection ''


Fix tests

Fix tests

--- apps/personalize/test/client/artists.coffee
+++ apps/personalize/test/client/artists.coffee
@@ -40,9 +40,10 @@
   describe '#setupFollowButton', ->
     it 'sets up a FollowButton view that can be accessed later', ->
       artist  = new Artist fabricate 'artist'
-      view    = @view.setupFollowButton artist, $('

')
+      key     = _.uniqueId(artist.id)
+      view    = @view.setupFollowButton key, artist, $('

')
       view.constructor.name.should.equal 'FollowButton'
-      @view.followButtonViews[artist.id].should.equal view
+      @view.followButtonViews[key].should.equal view

   describe '#setSkipLabel', ->
     it 'sets the label to "next" if we are not quite done; "Done" if we are almost done', ->
@@ -96,10 +97,12 @@
       model = fabricate 'artist'
       _.isUndefined(@view.followButtonViews).should.be.ok
       @view.suggestions.add mockSuggestionSet(model)
-      suggested = @view.suggestions.at(0).get('suggestions').at(0)
-      _.isUndefined(@view.followButtonViews[suggested.id]).should.not.be.ok
+      suggestionSet   = @view.suggestions.at(0)
+      suggested       = suggestionSet.get('suggestions').at(0)
+      key             = "#{suggestionSet.id}_#{suggested.id}"
+      _.isUndefined(@view.followButtonViews[key]).should.not.be.ok
       @view.disposeSuggestionSet model
-      _.isNull(@view.followButtonViews[suggested.id]).should.be.ok
+      _.isNull(@view.followButtonViews[key]).should.be.ok

   describe '#renderSuggestions', ->
     it 'renders related artists based on following', ->


Allow to create search choice on fly with select2_bridge

--- app/assets/javascripts/ab_admin/components/select2_bridge.js.coffee
+++ app/assets/javascripts/ab_admin/components/select2_bridge.js.coffee
@@ -25,6 +25,10 @@
     if @el.data('tags')
       @options.tokenSeparators = [',']
       @options.tags = @el.data('tags')
+    if @el.data('data')
+      data = @el.data('data')
+      data = _.map(data, (el) -> {id: el, text: el.toString()} ) if data[0] && !_.isObject(data[0])
+      @options.data = data
     else if @el.data('collection')
       @options.data = @el.data('collection')
     else if @el.data('class')
@@ -32,6 +36,10 @@
     if @el.data('add')
       @initCreateChoiceOnce()
       @initCreateChoice()
+    if @el.data('create-search-choice')
+      @options.createSearchChoice = (term) ->
+        log 'createSearchChoice'
+        {id: term, text: term}
     @options

   runModal: ->
@@ -91,7 +99,7 @@
       placeholder: ' '
       allowClear: true
       escapeMarkup: (m) -> m
-    opts.minimumResultsForSearch = 10 unless fv.test || @el.data('add')
+    opts.minimumResultsForSearch = 10 unless fv.test || @el.data('add') || @el.data('create-search-choice')
     opts

   initAjaxInput: ->


Remove duplicate AdminIndexController

Remove duplicate AdminIndexController

--- app/assets/javascripts/controllers/admin_index_controller.js.coffee
+++ app/assets/javascripts/controllers/admin_index_controller.js.coffee
@@ -1,4 +0,0 @@
-ETahi.AdminIndexController = Ember.ArrayController.extend
-  actions:
-    searchUsers: ->
-      @store.find('adminJournalUser', {query: @get('searchQuery')})


Suppress JS error when we aren't in the report view.

--- app/assets/javascripts/anonymizer.js.coffee
+++ app/assets/javascripts/anonymizer.js.coffee
@@ -15,9 +15,10 @@
     @fake_to_real_map  = {}
     @counter = 0;
     @anonymous  = false;
-    $(@button_select).observe 'click', (evt) =>
-      @toggle()
-      evt.stop();
+    if $(@button_select)
+      $(@button_select).observe 'click', (evt) =>
+        @toggle()
+        evt.stop();

   record_fake_and_real: (real_name) ->
     fake = @real_to_fake_map[real_name]


Increase timeout when waiting for atom-workspace to exist

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@github.com>

--- spec/integration/startup-spec.coffee
+++ spec/integration/startup-spec.coffee
@@ -29,7 +29,7 @@
       runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 1000)

           .treeViewRootDirectories()
@@ -55,7 +55,7 @@
       runAtom ["#{filePath}:3"], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 1000)
           .waitForExist("atom-text-editor", 5000)
           .then (exists) -> expect(exists).toBe true
@@ -80,7 +80,7 @@
       runAtom ["#{filePath}:2:2"], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 1000)
           .waitForExist("atom-text-editor", 5000)
           .then (exists) -> expect(exists).toBe true
@@ -98,7 +98,7 @@
       runAtom ["#{filePath}:  "], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 1000)
           .waitForExist("atom-text-editor", 5000)
           .then (exists) -> expect(exists).toBe true
@@ -114,7 +114,7 @@
       runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 5000)

           # Opening another file reuses the same window and does not change the
@@ -131,7 +131,7 @@
           .waitForNewWindow(->
             @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome)
           , 5000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(0, 1000)
           .treeViewRootDirectories()
           .then ({value}) -> expect(value).toEqual([otherTempDirPath])
@@ -140,14 +140,14 @@
     it "remembers the state of the window", ->
       runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
         client
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(0, 3000)
           .execute -> atom.workspace.open()
           .waitForPaneItemCount(1, 3000)

       runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
         client
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .waitForPaneItemCount(1, 5000)

   describe "opening multiple directories simultaneously", ->
@@ -157,14 +157,14 @@

       runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: atomHome}, (client) ->
         client
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .treeViewRootDirectories()
           .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])

           # Opening one of those directories again reuses the same window and
           # does not change the project paths.
           .startAnotherAtom([nestedDir], ATOM_HOME: atomHome)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .treeViewRootDirectories()
           .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath])

@@ -247,6 +247,6 @@
       runAtom [remoteDirectory], {ATOM_HOME: atomHome}, (client) ->
         client
           .waitForWindowCount(1, 1000)
-          .waitForExist("atom-workspace", 5000)
+          .waitForExist("atom-workspace", 10000)
           .treeViewRootDirectories()
           .then ({value}) -> expect(value).toEqual([remoteDirectory])


call setSort after collection is fetched

--- app/assets/javascripts/admin/views/workload/workload_list_view.coffee
+++ app/assets/javascripts/admin/views/workload/workload_list_view.coffee
@@ -26,11 +26,10 @@

   initialize : ->

-    @collection.setSort("availableTaskCount", "asc")
-
     @collection.fetch(
       silent : true
     ).done( =>
+      @collection.setSort("availableTaskCount", "asc")
       @collection.goTo(1)
     )


Adjust download menu animation

Adjust download menu animation

Always perform the text change ("download" <-> "cancel") when the text is on the far left, not necessarily immediately
after clicking.

--- app/assets/javascripts/download_menu.coffee
+++ app/assets/javascripts/download_menu.coffee
@@ -9,7 +9,7 @@
 window.hide_download_menu = ->
   $('#download-menu').animate { width: '0%' }, 200, ->
     $('#download-menu').hide()
-  $('#download-link').html download_text
+    $('#download-link').html download_text

 window.toggle_download_menu = ->
   if $('#download-link').html() == cancel_text


Hooking up cubism

Hooking up cubism

--- app/assets/javascripts/live-chart.js.coffee
+++ app/assets/javascripts/live-chart.js.coffee
@@ -41,8 +41,8 @@
       width: 235,
       height: 85,
       renderer: 'line',
-      dataURL: 'data.json',
-      onData: (d) -> d[0].data[0].y = 80; d
+      dataURL: 'api/meter_reads.json',
+      onData: (d) -> [{ name: 'New York', data: d}]
       onComplete: (transport) ->
         graph = transport.graph;
         detail = new Rickshaw.Graph.HoverDetail({ graph: graph });
@@ -50,16 +50,10 @@
         {
           name: 'New York',
           color: '#c05020',
-        }, {
-          name: 'London',
-          color: '#30c020',
-        }, {
-          name: 'Tokyo',
-          color: '#6060c0'
         }
       ]
     } );

 $ ->
-  # new LiveChart().cubism()
+  new LiveChart().cubism()


Re-indents comment in app/assets/javascripts/map_editor.js.coffee

--- app/assets/javascripts/map_editor.js.coffee
+++ app/assets/javascripts/map_editor.js.coffee
@@ -179,7 +179,7 @@

       this.map.on "draw:created", (e) =>
-#Attempt to add a geojson feature
+        #Attempt to add a geojson feature
         try
           feature = e.layer.toGeoJSON()

@@ -259,7 +259,7 @@

         if already_exist is true
-# Don't change the name if a different layer use this name
+          # Don't change the name if a different layer use this name
           $(e.currentTarget).closest('.leaflet-popup-content').find('.leaflet-popup-warning').removeClass 'hide'

@@ -388,7 +388,7 @@
           when 'input'
             popup += "<input type='text' name='#{attribute.property_value}' class='updateAttributesSerieLabelInput' value='#{feature.properties[attribute.property_value] || ""}'/>"
           else
-# include label
+            # include label
             popup += "#{feature.properties[attribute.property_value] || ''}"
         popup += "
"

@@ -402,7 +402,7 @@
         layer.bindPopup popup, keepInView: true, maxWidth: 600, className: 'leaflet-popup-pane'

     colorize: (level) ->
-#levels rane is set to [-3,3]
+      #levels rane is set to [-3,3]
       minLevel = -3
       start = this.colors.indexOf(this.options.multiLevels.startColor)
       stop = this.colors.indexOf(this.options.multiLevels.stopColor)
@@ -475,7 +475,7 @@
               @map.addLayer(backgroundLayer) if layer.byDefault

           else
-# no backgrounds, set defaults
+            # no backgrounds, set defaults
             back = ['OpenStreetMap.HOT',"OpenStreetMap.Mapnik", "Thunderforest.Landscape", "Esri.WorldImagery"]

             baseLayers = {}
@@ -508,7 +508,7 @@
           console.log this.options.back
       this

-# Retuns data from a serie found with the given name
+    # Retuns data from a serie found with the given name
     _getSerieData: (name) ->
       if @options.show.series[name]?
         return @options.show.series[name]
@@ -531,7 +531,7 @@
               options = $.extend true, {}, @options.show.layerDefaults[layer.type], layer, parent: this
               renderedLayer = M.layer(layer, data, options)
               if renderedLayer and renderedLayer.valid()
-# Build layer group
+                # Build layer group
                 layerGroup = renderedLayer.buildLayerGroup(this, options)
                 layerGroup.name = layer.name
                 layerGroup.renderedLayer = renderedLayer
@@ -607,7 +607,7 @@

     _refreshEditionLayerGroup: ->
       if this.edition?
-#remove overlay
+        #remove overlay
         @layerSelector.removeLayer this.edition
         this.map.removeLayer this.edition
       if this.options.edit?
@@ -616,7 +616,7 @@
           polys = []
           this.edition = L.geoJson(this.options.edit, {
             onEachFeature: (feature, layer) =>
-#nested function cause geojson doesn't seem to pass binding context
+              #nested function cause geojson doesn't seem to pass binding context
               @onEachFeature(feature, layer)

             style: (feature) =>
@@ -633,14 +633,14 @@
       else
         this.edition = L.geoJson(this.options.edit, {
           onEachFeature: (feature, layer) =>
-#nested function cause geojson doesn't seem to pass binding context
+            #nested function cause geojson doesn't seem to pass binding context
             @onEachFeature(feature, layer)

           style: (feature) =>
             @featureStyling feature
         })

-      #      this.edition.setStyle this.options.editStyle
+      # this.edition.setStyle this.options.editStyle
       this.edition.addTo this.map
       this._refreshControls()
       this._saveUpdates()


Done removing brackets on the goal screen


attach subviews that require git in initialize

--- lib/changes/status-list-view.coffee
+++ lib/changes/status-list-view.coffee
@@ -36,6 +36,14 @@
     # XXX chained initialize, bad smell?
     @model.initialize()

+    # Subviews
+    @commitMessageView = new CommitMessageView
+    @commitMessageView.initialize({changesView: @changesView})
+    @commitMessageBox.appendChild(@commitMessageView)
+
+    @undoCommitView = new UndoCommitView
+    @undoCommitBox.appendChild(@undoCommitView)
+
   createdCallback: ->
     @subscriptions = new CompositeDisposable

@@ -46,13 +54,6 @@
     @unstagedNode     = @querySelector(".files.unstaged")
     @commitMessageBox = @querySelector(".commit-message-box")
     @undoCommitBox    = @querySelector(".undo-last-commit-box")
-
-    # Subviews
-    @commitMessageView = new CommitMessageView
-    @commitMessageBox.appendChild(@commitMessageView)
-
-    @undoCommitView = new UndoCommitView
-    @undoCommitBox.appendChild(@undoCommitView)

   attachedCallback: ->
     @handleEvents()


Fixed issue with space in link text

Fixed issue with space in link text


Refactored edit subject directive

Refactored edit subject directive

--- app/coffee/modules/common/components.coffee
+++ app/coffee/modules/common/components.coffee
@@ -438,56 +438,45 @@
 #############################################################################

 EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading) ->
-    viewTemplate = _.template("""
-        <%- item.subject %>
-        <% if (canEdit) { %>
+    template = """
+

+            {{ item.subject }}

-        <% } %>
-    """)
-
-    editTemplate = _.template("""
-        <input type="text" value="<%- item.subject %>" data-required="true" data-maxlength="500"/>
-
-    """)
+

+

+            <input type="text" ng-model="item.subject" data-required="true" data-maxlength="500"/>
+
+

+    """

     link = ($scope, $el, $attrs, $model) ->
-        editing = false
-        scope = $scope.$new()
-
-        render = ->
-            if editing
-                $el.html(editTemplate({item: scope.item}))
-            else
-                canEdit = $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
-                $el.html(viewTemplate({item: $model.$modelValue, canEdit: canEdit}))
-
         save = ->
-            $model.$modelValue.subject = $el.find('input').val()
+            $model.$modelValue.subject = $scope.item.subject
             $loading.start($el.find('.save-container'))
             promise = $repo.save($model.$modelValue)
             promise.then ->
                 $confirm.notify("success")
                 $rootscope.$broadcast("history:reload")
-                editing = false
-                $loading.finish($el.find('.save-container'))
-                render()
+                $el.find('div.edit-subject').hide()
+                $el.find('div.view-subject').show()
             promise.then null, ->
                 $confirm.notify("error")
+            promise.finally ->
                 $loading.finish($el.find('.save-container'))

-        $scope.$watch $attrs.ngModel, (item) ->
-            return if not item
-            render()
+        $scope.$watch $attrs.ngModel, (value) ->
+            return if not value
+            $scope.item = value
+            if $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
+                $el.find('div.view-subject span.edit').show()

         $scope.$on "$destroy", ->
             $el.off()

         $el.click ->
-            if not editing and $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
-                editing = true
-                scope.item = {subject: $model.$modelValue.subject}
-                render()
-                $el.find('input').focus()
+            $el.find('div.edit-subject').show()
+            $el.find('div.view-subject').hide()
+            $el.find('input').focus()

         $el.on "click", ".save", ->
             save()
@@ -496,14 +485,18 @@
             if event.keyCode == 13
                 save()
             else if event.keyCode == 27
-                editing = false
                 $model.$modelValue.revert()
-                render()
+                $el.find('div.edit-subject').hide()
+                $el.find('div.view-subject').show()
+
+        $el.find('div.edit-subject').hide()
+        $el.find('div.view-subject span.edit').hide()

     return {
         link: link
         restrict: "EA"
         require: "ngModel"
+        template: template
     }

 module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading",


Agregar filestyle con turbolinks page:load

--- app/assets/javascripts/cartas.js.coffee
+++ app/assets/javascripts/cartas.js.coffee
@@ -1,8 +1,16 @@
-jQuery ->
-
+# Hay que largarlo tanto cuando se carga la página como cuando turbolinks la
+# pide (el page:change)
+bindear = ->
   # mejora el estilo default de los file uploaders
   $("form.carta :file").filestyle(
     buttonText: 'Subir',
     classText: 'span9 filestyle',
     classButton: 'span3 filestyle'
   )
+
+$(document)
+  .on 'page:change', ->
+    bindear()
+
+jQuery ->
+  bindear()


sankey: link offset

sankey: link offset

--- app/assets/javascripts/d3/sankey.coffee
+++ app/assets/javascripts/d3/sankey.coffee
@@ -57,7 +57,6 @@

     height: => @value()

-
   Link: class extends Backbone.Model
     initialize: =>
       @module = D3.sankey
@@ -68,9 +67,19 @@
       @left.right_links.push this
       @right.left_links.push this

-    left_y: => @left.y_center()
-    right_y: => @right.y_center()
-    left_x: => @left.x_center() + @module.Node.width / 2
+    left_y:  =>
+      offset = 0
+      for link in @left.right_links
+        break if link == this
+        offset += link.height()
+      @left.y_center() + offset
+    right_y:  =>
+      offset = 0
+      for link in @right.left_links
+        break if link == this
+        offset += link.height()
+      @right.y_center() + offset
+    left_x:  => @left.x_center() + @module.Node.width / 2
     right_x: => @right.x_center() - @module.Node.width / 2

     path_points: =>
@@ -93,6 +102,8 @@
         @gquery.get('future_value') / 20000000000
       else
         @get 'value'
+
+    height: => @value()

   # This is the main chart class
   #