Made cloud collection paginatable.

Made cloud collection paginatable.

--- app/assets/javascripts/collections/clouds.js.coffee
+++ app/assets/javascripts/collections/clouds.js.coffee
@@ -3,6 +3,19 @@
   model: Cloudsdale.Models.Cloud
   url: -> "/v1/clouds"

+  initialize: (models,options) ->
+
+    options ||= {}
+
+    @url = options.url if options.url
+    @topic = options.topic if options.topic
+
+    @page = null
+    @nextPage = 0
+    @prevPage = null
+    @totalResults = null
+    @timeThreshold  = null
+
   # Public: Tries to find the model inside of the collection
   # if it can't be found it tries to initialize it from the server.
   #
@@ -12,13 +25,41 @@
   # # => Cloud
   #
   # Returns an instance of Cloudsdale.Models.Cloud if found, otherwise undefined.
-  findOrInitialize: (args) ->
+  findOrInitialize: (args,options) ->
+
+    args = {} unless args
+    options = {} unless options

     cloud = @get(args.id)

-    unless cloud
+    if cloud
+      cloud.fetch() if options.fetch
+    else
       cloud = new Cloudsdale.Models.Cloud(args)
       cloud.fetch() if cloud.get('is_transient')
       @add(cloud)
+
+    return cloud

-    return cloud
+  fetchMore: (options) ->
+
+    options = {} unless options
+
+    xhr = @fetch
+      add: true
+      headers:
+        'X-Result-Page' : @nextPage if @nextPage
+        'X-Result-Time' : @timeThreshold if @timeThreshold
+
+      success: (collection,resp) =>
+        @page           = xhr.getResponseHeader('X-Result-Page')
+        @nextPage       = xhr.getResponseHeader('X-Result-Next')
+        @prevPage       = xhr.getResponseHeader('X-Result-Prev')
+        @totalResults   = xhr.getResponseHeader('X-Result-Total')
+        @timeThreshold  = xhr.getResponseHeader('X-Result-Time')
+        options.success(collection,resp) if options.success
+      error: (collection,resp) =>
+        options.error(collection,resp) if options.error
+
+  lastPage: ->
+    @nextPage == null


Changes line-width of full WDPA rendered on map

This way it doesn't look like europe is just one giant PA.

--- app/assets/javascripts/protected_area_map.js.coffee
+++ app/assets/javascripts/protected_area_map.js.coffee
@@ -6,7 +6,7 @@

   _addSelectedStyle: (args) ->
     opacity = if args.opacity? then args.opacity else .5
-    lineWidth = if args.lineWidth? then args.lineWidth else 1
+    lineWidth = if args.lineWidth? then args.lineWidth else 0.05
     args.cartocss += """
       ##{args.table}[#{args.attrName} = #{args.attrVal}]{
         line-color:#D41623;
@@ -59,9 +59,9 @@
     cartocss = """
       #wdpapoly_july2014_0{
         line-color:#40541b;
-        line-width:0.4;
+        line-width:0.05;
         polygon-fill:#83ad35;
-        polygon-opacity:0.4;}
+        polygon-opacity:0.7;}
     """
     sublayers = [
       sql: "select * from wdpapoly_july2014_0"


WebRTC: Send runtime:getruntime when opening connection

--- src/runtimes/webrtc.coffee
+++ src/runtimes/webrtc.coffee
@@ -52,6 +52,8 @@
         @handleMessage data.data
       console.log 'onmessage registered'
       @connecting = false
+      @send 'runtime', 'getruntime',
+        secret: @definition.secret
       @emit 'status',
         online: true
         label: 'connected'


Stable version

Stable version

--- src/coverphoto.coffee
+++ src/coverphoto.coffee
@@ -1,34 +1,153 @@
 do ($) ->
   class CoverPhoto
-    @cache: []
-    @cacheCount: 0
+    @cache: []; @cacheCount: 0
     @defaults:
       postUrl:  '/update_cover_photo'
-      editable: true
+      editable: false

     constructor: ({@el, @options}) ->
+      @templates = coverphotoTemplates
       @setup()
       @cache()
       @render()
+      @elements()
       @bindEvents()

     render: ->
-      @addEditButton() if @options.editable
+      @addForm()
+      @addActions() if @options.editable
+      @addImage(@options.currentImage) if @options.currentImage
+
+      $("canvas", @$el).attr "width",  @$el.width()
+      $("canvas", @$el).attr "height", @$el.height()

     bindEvents: ->
+      $(@$el).on "mouseleave", @hideActions
+      $(@$el).on "mouseenter", @showActions
+      $(@$el).on "mouseleave", @actionsContainer.selector, @hideActionsMenu
+      $(@$el).on "change",     @fileInput.selector,        @handleFileSelected
+      $(@$el).on "click",      @openMenuButton.selector,   @showActionsMenu
+      $(@$el).on "click",      @uploadButton.selector,     @startUpload
+      $(@$el).on "click",      @repositionButton.selector, @startReposition
+      $(@$el).on "click",      @saveEditButton.selector,   @saveEdit
+      $(@$el).on "click",      @cancelEditButton.selector, @cancelEdit

     setup: ->
       @options = $.extend(CoverPhoto.defaults, @options)
-      @$el = $(@el)
+      html = @templates["src/templates/container.jst"]()
+      @$el =  $(html).appendTo $(@el)

     cache: ->
       @$el.attr 'data-coverphoto-id', CoverPhoto.cacheCount
       CoverPhoto.cache[CoverPhoto.cacheCount] = @
       CoverPhoto.cacheCount++

-    addEditButton: ->
-      @$el.append """Change cover photo"""
+    elements: ->
+      @actionsContainer = $(".actions", @$el)
+      @actions          = $(".chooser", @$el)
+      @actionsMenu      = $(".chooser .menu", @$el)
+      @editMenu         = $(".edit", @$el)
+      @cancelEditButton = $(".edit .cancel a", @$el)
+      @saveEditButton   = $(".edit .save a", @$el)
+      @openMenuButton   = $(".chooser .open-menu a", @$el)
+      @uploadButton     = $(".chooser .upload a", @$el)
+      @repositionButton = $(".chooser .reposition a", @$el)
+      @form             = $("form", @$el)
+      @fileInput        = $("input[name='coverphoto[original]']", @$el)
+      @hiddenImageInput = $("input[name='coverphoto[cropped]']", @$el)
+      @canvas           = $("canvas", @$el)
+
+    addForm: ->
+      @$el.append @templates["src/templates/form.jst"] @options
+
+    addActions: ->
+      @$el.append @templates["src/templates/actions.jst"] @options
+
+    addImage: (imageData) ->
+      @originalImage = $(".coverphoto-photo-container img", @$el).attr("src")
+      $(".coverphoto-photo-container", @$el).remove()
+      @$el.append @templates["src/templates/image.jst"]({imageData})
+
+    hideActions: =>
+      @actions.fadeOut()
+
+    showActions: =>
+      @actions.fadeIn() unless @editMenu.is(":visible")
+
+    showActionsMenu: =>
+      @actionsMenu.show()
+      false
+
+    hideActionsMenu: (evt) =>
+      @actionsMenu.hide()
+
+    showEditMenu: =>
+      @editMenu.show()
+      @actions.hide()
+
+    hideEditMenu: =>
+      @editMenu.hide()
+      @actions.show()
+
+    startUpload: =>
+      @fileInput.click()
+      false
+
+    saveEdit: =>
+      console.log "Saving to #{@options.postUrl}"
+
+      @gatherImageData()
+      @form.submit()
+      @hideEditMenu()
+      @endReposition()
+      false
+
+    cancelEdit: =>
+      @addImage @originalImage if @originalImage
+      @hideEditMenu()
+      @endReposition()
+      false
+
+    startReposition: (evt = null) =>
+      image = $(".coverphoto-photo-container img", @$el)
+      makeImageDraggable = () =>
+        yMax = -(image.height() - image.parent().height() - 10)
+        image.draggable
+          axis: "y"
+          containment: [0,yMax,0,0]
+
+      if image.height() > 0
+        makeImageDraggable()
+      else
+        image.load makeImageDraggable
+
+      @showEditMenu()
+      @hideActionsMenu() if evt?
+      false
+
+    endReposition: =>
+      image = $(".coverphoto-photo-container img", @$el)
+      image.draggable("destroy")
+
+    handleFileSelected: (evt) =>
+      file = evt.target.files[0]
+      reader = new FileReader()
+      reader.onload = (evt) =>
+        @addImage(evt.target.result)
+        @startReposition()
+      reader.readAsDataURL(file)
+
+    gatherImageData: ->
+      image   = $(".coverphoto-photo-container img", @$el)
+      context = @canvas[0].getContext("2d")
+      width   = image.width()
+      height  = image.height()
+
+      context.drawImage(image[0], 0, image.position().top, width, height)
+
+      dataUrl = @canvas[0].toDataURL("image/png")
+      @hiddenImageInput.val(dataUrl)

   $.fn.CoverPhoto = (data) ->
     if data is 'get' and @.length is 1


Making Wolfram reply to the user instead of simply sending the response.

--- src/scripts/wolfram.coffee
+++ src/scripts/wolfram.coffee
@@ -11,6 +11,6 @@
     Wolfram.query msg.match[1], (e, result) ->
       # console.log result
       if result.length > 0
-        msg.send result[1]['subpods'][0]['value']
+        msg.reply result[1]['subpods'][0]['value']
       else
-        msg.send 'Hmm...not sure.  Maybe I don\'t understand the question.'
+        msg.reply 'Hmm...not sure.  Maybe I don\'t understand the question.'


fix(task): remove duplicated rename task call

--- tasks/i18n.coffee
+++ tasks/i18n.coffee
@@ -2,9 +2,6 @@
 _ = require 'lodash'

 module.exports = (grunt) ->
-
-  grunt.loadNpmTasks 'grunt-contrib-jade'
-  grunt.renameTask 'jade', 'contrib-jade'

   grunt.loadNpmTasks 'grunt-contrib-jade'
   grunt.renameTask 'jade', 'contrib-jade'


Don't let rcedit fail skip nsis signing

Don't let rcedit fail skip nsis signing

--- tasks/pack.coffee
+++ tasks/pack.coffee
@@ -444,7 +444,12 @@

       exePath = './dist/' + manifest.name + '-' + manifest.version + '-win32-nsis.exe'
       logMessage = 'rcedit ' + exePath + ' properties'
-      rcedit exePath, properties, utils.log callback, logMessage, JSON.stringify(properties)
+      fakeCallback = (err) ->
+        if err
+          console.log 'rcedit failed'
+          console.error err
+        callback()
+      rcedit exePath, properties, utils.log fakeCallback, logMessage, JSON.stringify(properties)

     # Sign the exe
     (callback) ->


engine: conditions plugin rely on disabled

--- packages/engine/src/plugins/conditions.coffee
+++ packages/engine/src/plugins/conditions.coffee
@@ -3,7 +3,10 @@

 module.exports = ->
   module: '@nikitajs/engine/src/plugins/conditions'
-  require: '@nikitajs/engine/src/metadata/raw'
+  require: [
+    '@nikitajs/engine/src/metadata/raw'
+    '@nikitajs/engine/src/metadata/disabled'
+  ]
   hooks:
     'nikita:session:normalize': (action, handler) ->
       # Ventilate conditions properties defined at root
@@ -21,12 +24,14 @@
         action = handler.call null, ...arguments
         action.conditions = conditions
         action
-    'nikita:session:action': (action, handler) ->
-      final_run = true
-      for k, v of action.conditions
-        local_run = await handlers[k].call null, action
-        final_run = false if local_run is false
-      if final_run then handler else (->)
+    'nikita:session:action':
+      before: '@nikitajs/engine/src/metadata/disabled'
+      handler: (action, handler) ->
+        final_run = true
+        for k, v of action.conditions
+          local_run = await handlers[k].call null, action
+          final_run = false if local_run is false
+        if final_run then handler else action.metadata.disabled = true

 handlers =
   if: (action) ->


Use correct jquery

Use correct jquery

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -3,7 +3,8 @@
 temp = require 'temp'

 _ = require 'underscore-plus'
-{$, View, WorkspaceView} = require 'atom'
+{$, View} = require 'atom-space-pen-views'
+{WorkspaceView} = require 'atom'
 fs = require 'fs-plus'
 Q = require 'q'


EditorPane: Remove duplicated bindChangeEvents method.

--- client/ide/lib/workspace/panes/ideeditorpane.coffee
+++ client/ide/lib/workspace/panes/ideeditorpane.coffee
@@ -147,13 +147,6 @@
         paneHash : @hash
         paneType : @getOptions().paneType
         file     : path: @file.path
-
-    return change
-
-
-  bindChangeListeners: ->
-
-    change = @getInitialChangeObject()

     return change


flow fingerprints -> flowprints

flow fingerprints -> flowprints

--- layout.coffee
+++ layout.coffee
@@ -53,7 +53,7 @@

                     <li <%=(if @page == "ffprints" then 'class=active' else "") %>>
-                      Flow fingerprints

+                      Flowprints

                     <li <%=(if @page == "bubbles" then 'class=active' else "") %> >
                     Bubbles


CADS-153 : Claim consent and declaration do-you-agree-no textarea other fixup charCounter.

--- c3/app/assets/javascripts/s_consent_and_declaration/g_declaration.coffee
+++ c3/app/assets/javascripts/s_consent_and_declaration/g_declaration.coffee
@@ -3,6 +3,7 @@
 val = (selector,text) -> if text? then $("##{selector}").val(text) else $("##{selector}").val()

 window.initEvents = (o) ->
+  $("#"+o.whyPerson).trigger("blur")
   if not S(o.informationFromPersonN).prop 'checked'
     hideWhyPersonWrapper(o)

@@ -17,4 +18,5 @@
   $("#whyPersonWrapper textarea").val("")

 showWhyPersonWrapper = (o) ->
+  $("#"+o.whyPerson).trigger("blur")
   S(o.whyPersonWrapper).slideDown(0).attr 'aria-hidden', 'false'


Make the rethink get_blob function read from gcloud if blob isn't available in database (and is marked as in gcloud).  Have option to put blob back in database.

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -2507,22 +2507,49 @@

     get_blob: (opts) =>
         opts = defaults opts,
-            uuid : required
-            cb   : required
-        @table('blobs').get(opts.uuid).run (err, x) =>
-            if err
-                opts.cb(err)
-            else
+            uuid       : required
+            save_in_db : false  # if true and blob isn't in db and is only in gcloud, copies to local db (for faster access e.g., 20ms versus 5ms -- i.e., not much faster; gcloud is FAST too.)
+            cb         : required   # cb(err) or cb(undefined, blob_value) or cb(undefined, undefined) in case no such blob
+        x = undefined
+        blob = undefined
+        async.series([
+            (cb) =>
+                @table('blobs').get(opts.uuid).run (err, _x) =>
+                    x = _x; cb(err)
+            (cb) =>
                 if not x
-                    opts.cb(undefined, undefined)
+                    # nothing to do -- blob not in db (probably expired)
+                    cb()
                 else if x.expire and x.expire <= new Date()
-                    opts.cb(undefined, undefined)   # no such blob anymore
-                    @table('blobs').get(opts.uuid).delete().run()   # delete it
+                    # the blob already expired -- background delete it
+                    @table('blobs').get(opts.uuid).delete().run()   # delete it (but don't wait for this to finish)
+                    cb()
+                else if x.blob?
+                    # blob not expired and is in database
+                    blob = x.blob
+                    cb()
+                else if x.gcloud
+                    # blob not available locally, but should be in a Google cloud storage bucket -- try to get it
+                    @gcloud().bucket(name: x.gcloud).read
+                        name : opts.uuid
+                        cb   : (err, _blob) =>
+                            if err
+                                cb(err)
+                            else
+                                blob = _blob
+                                cb()
+                                if opts.save_in_db
+                                    # also save in database so will be faster next time (again, don't wait on this)
+                                    @table('blobs').get(opts.uuid).update(blob : blob).run()
                 else
-                    opts.cb(undefined, x.blob)
-                    # We now also update the access counter/times for the blob, but of course
-                    # don't block on opts.cb above to do this.
-                    @touch_blob(uuid : opts.uuid)
+                    # blob not local and not in gcloud -- this shouldn't happen (just view this as "expired" by not setting blob)
+                    cb()
+        ], (err) =>
+            opts.cb(err, blob)
+            if blob?
+                # blob was pulled from db or gcloud, so note that it was accessed (updates a counter)
+                @touch_blob(uuid : opts.uuid)
+        )

     # Return gcloud API interface
     gcloud: () =>
@@ -2535,7 +2562,7 @@
             uuid   : required  # uuid=sha1-based uuid coming from blob
             bucket : BLOB_GCLOUD_BUCKET # name of bucket
             force  : false     # if true, upload even if already uploaded
-            cb     : required  # cb(err)
+            cb     : undefined  # cb(err)
         x = undefined
         async.series([
             (cb) =>
@@ -2568,11 +2595,11 @@
                     cb(); return
                 # successful upload to gcloud -- set x.gcloud
                 @table('blobs').get(opts.uuid).update(gcloud:opts.bucket).run(cb)
-        ], (err) => opts.cb(err))
+        ], (err) => opts.cb?(err))

     # Copied all blobs that will never expire to a google cloud storage bucket.
     #
-    #     errors={}; db.copy_all_blobs_to_gcloud(limit:1000,map_limit:25,cb:done(), repeat_until_done:true, errors:errors)
+    #     errors={}; db.copy_all_blobs_to_gcloud(limit:10000, map_limit:25,cb:done(), repeat_until_done:true, errors:errors)
     #
     copy_all_blobs_to_gcloud: (opts) =>
         opts = defaults opts,
@@ -2635,6 +2662,13 @@
             cb    : required   # cb(err)
         @table('blobs').getAll(opts.uuids...).replace(
             @r.row.without(expire:true)).run(opts.cb)
+
+    # If blob has been backed up offsite
+    #close_blob: (opts) =>
+    #    opts = defaults opts,
+    #        uuid : required   # uuid=sha1-based from blob
+    #        cb   : required   # cb(err)
+    #        @r.row.without(expire:true)).run(opts.cb)

     ###
     # User queries


help page: render LaTeX in LaTeX section, tweaking header

--- src/smc-webapp/r_help.cjsx
+++ src/smc-webapp/r_help.cjsx
@@ -267,6 +267,16 @@
     insert_sample_function : ->
         '$J_\\alpha(x) = \\sum\\limits_{m=0}^\\infty \\frac{(-1)^m}{m! \\, \\Gamma(m + \\alpha + 1)}{\\left({\\frac{x}{2}}\\right)}^{2 m + \\alpha}$'

+    componentDidMount : ->
+        @update_mathjax()
+
+    componentDidUpdate : ->
+        @update_mathjax()
+
+    update_mathjax: ->
+        el = ReactDOM.findDOMNode(@)
+        MathJax.Hub.Queue(["Typeset",MathJax.Hub,el]);
+
     render : ->

<Icon name='cubes' /> Getting started with <SiteName/>

@@ -377,7 +387,7 @@

                 </Panel>

-                <Panel header='Using LaTeX' eventKey='5'>
+                <Panel header={@get_panel_header('pencil-square-o', How to work with LaTeX)} eventKey='5'>

<Icon name='youtube-play' /> video1

<Icon name='youtube-play' /> video2


Add check for focus, improve placeholder behaviour to check if value is empty when in focus too for @jonastreub

--- framer/InputLayer.coffee
+++ framer/InputLayer.coffee
@@ -49,6 +49,9 @@
 		# Override text property setting the html
 		@html = ""

+		# Check if in focus
+		@_isFocused = false
+
 		# Default focus interaction
 		@input.onfocus = (e) =>

@@ -58,9 +61,8 @@
 			# Emit focus event
 			@emit(Events.InputFocus, event)

-		@input.onblur = (e) =>
-			if @value is ""
-				@_setPlaceholder()
+			@_isFocused = true
+

 		@input.onkeyup = (e) =>

@@ -80,13 +82,17 @@
 			if e.which is 8
 				@emit(Events.BackSpaceKey, event)

+			# Revert to placeholder
+			if @value is ""
+				@_setPlaceholder()

 	_setPlaceholder: =>
-		@input.value =
+		@input.placeholder =
 			if @text isnt "" and @text isnt "Type Something" then @text else "Placeholder"

-		@input.style.color =
-			if @color? then @color else "#aaa"
+		unless @_isFocused
+			@input.style.color =
+				if @color? then @color else "#aaa"

 	@define "value",
 		get: -> @input.value


cache isfrozen calls

cache isfrozen calls

--- src/data/table.coffee
+++ src/data/table.coffee
@@ -334,6 +334,10 @@
   project: (mappings, extend=yes) ->
     (new data.ops.Project @, mappings, extend)

+  # @param extend keep existing columns (if not overwritten by mappings)?
+  blockproject: (mappings, extend=yes, blocksize) ->
+    (new data.ops.BlockProject @, mappings, extend, blocksize)
+
   # @param alias name of the table column that will store the partitions
   partition: (cols, alias="table") ->
     (new data.ops.Partition @, cols, alias)
@@ -360,10 +364,12 @@

   isFrozen: ->
     return @frozen if @frozen
+    return @_isfrozen if @_isfrozen?
     for child in @children()
       if child.isFrozen()
         @frozen = yes
         break
+    @_isfrozen = @frozen
     @frozen

   melt: -> @


only load options for checks which have/need them

--- app/assets/javascripts/monitoring/monitoring.js.coffee
+++ app/assets/javascripts/monitoring/monitoring.js.coffee
@@ -60,8 +60,18 @@

     load_data: (data) ->
       needed = super(data)
-      @log "opts: ", @opts
-      needed = _.union(needed, @opts)
+
+      # only load opts for checks which have/need them
+      opts_needed = []
+      _.eachR @, @opts, (opt) ->
+        cmd = @commands.get(opt.id)
+        if ! _.isEmpty(cmd.get("options"))
+          opts_needed.push opt
+        else
+          opt.set(cmd.attributes, {silent: true})
+
+
+      needed = _.union(needed, opts_needed)
       @spinner = new _bv.Spinner($("div.command_opts").height(30).css({ padding: "20px" }))
       return needed


DataGraph: Don't require series argument.

--- app/assets/javascripts/pgdatagraph/datagraph.js.coffee
+++ app/assets/javascripts/pgdatagraph/datagraph.js.coffee
@@ -11,6 +11,7 @@
       { label: "last 2 weeks",  duration: "-2w" },
       { label: "last 24 hours", duration: "-1d" }
     ]
+    series: {}
     dateFormat: "M d, yy"
     renderer: "line"
     detailSmoothing: 10


Add unit test for #_getSqlQueryForRowsWithoutOID() helper method

--- test/suite.coffee
+++ test/suite.coffee
@@ -33,6 +33,15 @@
       done()

 describe 'query-helpers', ->
+  describe '#_getSqlQueryForRowsWithoutOID()', ->
+    it 'should return correct sql query for given table', (done) ->
+      sql = helpers._getSqlQueryForRowsWithoutOID config.from[1], config.oid
+
+      client.query sql, (err, data) ->
+        throw err if err
+        assert.equal data.rows.length, 5
+        done()
+
   describe '#insertOIDsForTables()', ->
     it 'should insert ObjectIDs for all rows without ObjectIDs', (done) ->
       helpers.insertOIDsForTables client, config.from, config.oid, (err, data) ->


#288 Solved bug when trying to DnD on before or after root node

--- client/src/app/game/board/board.coffee
+++ client/src/app/game/board/board.coffee
@@ -72,9 +72,15 @@
     link: (scope, element) ->

       updateModel = ->
-        treeJson = $.jstree._reference(element).get_json()[0]
-        reverted = boardConverter.revert(treeJson)
-        currentGame.version.board = reverted
+        treeJson = $.jstree._reference(element).get_json()
+        if treeJson.length isnt 1
+          scope.updateTree( boardConverter.convert(currentGame.version.board) ) # Refresh display from model
+          throw Error('Tree should have one and only one root')
+        else
+          treeJson = treeJson[0]
+          console.log treeJson
+          reverted = boardConverter.revert(treeJson)
+          currentGame.version.board = reverted

       scope.updateTree = (board) ->
         $(element).jstree


feeder headerNav implemented

feeder headerNav implemented

--- components/buttons/KDButtonView.coffee
+++ components/buttons/KDButtonView.coffee
@@ -21,7 +21,7 @@
     @setCallback options.callback
     @setTitle options.title
     @setIconClass options.iconClass if options.iconClass
-    @showIcon()                     if options.icon
+    @showIcon()                     if options.icon or options.iconOnly
     @setIconOnly options.iconOnly   if options.iconOnly
     @disable()                      if options.disabled


coffeescript strings

coffeescript strings

--- src/hubot/xmpp.coffee
+++ src/hubot/xmpp.coffee
@@ -131,7 +131,7 @@
     for str in strings
       console.log "Sending to #{user.room}: #{str}"

-      to = if user.type in ['direct', 'chat'] then user.room + '/' + user.id else user.room
+      to = if user.type in ['direct', 'chat'] then "#{user.room}/#{user.id}" else user.room

       message = new Xmpp.Element('message',
                   from: @options.username


Mock out Project::scan in specs that don't need a full project search

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -73,74 +73,45 @@
       beforeEach ->
         editor.trigger 'project-find:show'
         projectFindView.findEditor.setText('i(\\w)ems+')
+        spyOn(project, 'scan').andCallFake -> new $.Deferred().done()
+
+      it "escapes regex patterns by default", ->
+        projectFindView.trigger 'core:confirm'
+        expect(project.scan.argsForCall[0][0]).toEqual /i\(\\w\)ems\+/gi

       it "toggles regex option via an event and finds files matching the pattern", ->
         expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
         projectFindView.trigger 'project-find:toggle-regex-option'
         expect(projectFindView.regexOptionButton).toHaveClass('selected')
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          projectFindView.resultsView.scrollToBottom() # To load ALL the results
-          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(13)
-          expect(projectFindView.previewCount.text()).toBe "13 matches in 2 files"
+        expect(project.scan.argsForCall[0][0]).toEqual /i(\w)ems+/gi

       it "toggles regex option via a button and finds files matching the pattern", ->
         expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
         projectFindView.regexOptionButton.click()
         expect(projectFindView.regexOptionButton).toHaveClass('selected')
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          projectFindView.resultsView.scrollToBottom() # To load ALL the results
-          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(13)
-          expect(projectFindView.previewCount.text()).toBe "13 matches in 2 files"
+        expect(project.scan.argsForCall[0][0]).toEqual /i(\w)ems+/gi

     describe "case sensitivity", ->
       beforeEach ->
         editor.trigger 'project-find:show'
+        spyOn(project, 'scan').andCallFake -> new $.Deferred().done()
+        projectFindView.findEditor.setText('ITEMS')

       it "runs a case insensitive search by default", ->
-        projectFindView.findEditor.setText('ITEMS')
         projectFindView.trigger 'core:confirm'
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          projectFindView.resultsView.scrollToBottom() # To load ALL the results
-          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(13)
-          expect(projectFindView.previewCount.text()).toBe "13 matches in 2 files"
+        expect(project.scan.argsForCall[0][0]).toEqual /ITEMS/gi

       it "toggles case sensitive option via an event and finds files matching the pattern", ->
-        projectFindView.findEditor.setText('C')
         expect(projectFindView.caseOptionButton).not.toHaveClass('selected')
         projectFindView.trigger 'project-find:toggle-case-option'
         expect(projectFindView.caseOptionButton).toHaveClass('selected')
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(1)
-          expect(projectFindView.previewCount.text()).toBe "1 match in 1 file"
+        expect(project.scan.argsForCall[0][0]).toEqual /ITEMS/g

       it "toggles case sensitive option via a button and finds files matching the pattern", ->
-        projectFindView.findEditor.setText('C')
         expect(projectFindView.caseOptionButton).not.toHaveClass('selected')
         projectFindView.caseOptionButton.click()
         expect(projectFindView.caseOptionButton).toHaveClass('selected')
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(1)
-          expect(projectFindView.previewCount.text()).toBe "1 match in 1 file"
+        expect(project.scan.argsForCall[0][0]).toEqual /ITEMS/g

     describe "when core:confirm is triggered", ->
       beforeEach ->
@@ -169,19 +140,12 @@
             expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(13)
             expect(projectFindView.previewCount.text()).toBe "13 matches in 2 files"

-        it "with a paths filter displays the results and no errors", ->
+        it "only searches paths matching text in the path filter", ->
+          spyOn(project, 'scan').andCallFake -> new $.Deferred().done()
           projectFindView.pathsEditor.setText('*.js')
           projectFindView.trigger 'core:confirm'

-          waitsForPromise ->
-            searchPromise
-
-          runs ->
-            expect(projectFindView.resultsView).toBeVisible()
-            projectFindView.resultsView.scrollToBottom() # To load ALL the results
-            expect(projectFindView.errorMessages).not.toBeVisible()
-            expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(6)
-            expect(projectFindView.previewCount.text()).toBe "6 matches in 1 file"
+          expect(project.scan.argsForCall[0][1]['paths']).toEqual ['*.js']

         it "updates the results list when a buffer changes", ->
           projectFindView.trigger 'core:confirm'
@@ -210,33 +174,23 @@
       describe "when no results exist", ->
         beforeEach ->
           projectFindView.findEditor.setText('notintheprojectbro')
+          spyOn(project, 'scan').andCallFake -> new $.Deferred().done()
+

         it "displays no errors and no results", ->
           projectFindView.trigger 'core:confirm'
-
-          waitsForPromise ->
-            searchPromise
-
-          runs ->
-            expect(projectFindView.errorMessages).not.toBeVisible()
-            expect(projectFindView.resultsView).toBeVisible()
-            expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(0)
+          expect(projectFindView.errorMessages).not.toBeVisible()
+          expect(projectFindView.resultsView).toBeVisible()
+          expect(projectFindView.resultsView.find("li > ul > li")).toHaveLength(0)

     describe "history", ->
       beforeEach ->
         rootView.trigger 'project-find:show'
+        spyOn(project, 'scan').andCallFake -> new $.Deferred().done()
         projectFindView.findEditor.setText('sort')
         projectFindView.findEditor.trigger 'core:confirm'
-
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          projectFindView.findEditor.setText('items')
-          projectFindView.findEditor.trigger 'core:confirm'
-
-        waitsForPromise ->
-          searchPromise
+        projectFindView.findEditor.setText('items')
+        projectFindView.findEditor.trigger 'core:confirm'

       it "can navigate the entire history stack", ->
         expect(projectFindView.findEditor.getText()).toEqual 'items'


Fix tests for case sensitivity

Fix tests for case sensitivity

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -232,39 +232,52 @@
         projectFindView.trigger 'core:confirm'
         expect(atom.project.scan.argsForCall[0][0]).toEqual /i\(\\w\)ems\+/gi

-      it "toggles regex option via an event and finds files matching the pattern", ->
-        expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
-        projectFindView.trigger 'project-find:toggle-regex-option'
-        expect(projectFindView.regexOptionButton).toHaveClass('selected')
-        expect(atom.project.scan.argsForCall[0][0]).toEqual /i(\w)ems+/gi
-
-      it "toggles regex option via a button and finds files matching the pattern", ->
-        expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
-        projectFindView.regexOptionButton.click()
-        expect(projectFindView.regexOptionButton).toHaveClass('selected')
-        expect(atom.project.scan.argsForCall[0][0]).toEqual /i(\w)ems+/gi
+      describe "when search has not been run yet", ->
+        it "toggles regex option via an event but does not run the search", ->
+          expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
+          projectFindView.trigger 'project-find:toggle-regex-option'
+          expect(projectFindView.regexOptionButton).toHaveClass('selected')
+          expect(atom.project.scan).not.toHaveBeenCalled()
+
+      describe "when search has been run", ->
+        beforeEach ->
+          projectFindView.trigger 'core:confirm'
+          waitsForPromise -> searchPromise
+
+        it "toggles regex option via an event and finds files matching the pattern", ->
+          expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
+          projectFindView.trigger 'project-find:toggle-regex-option'
+          expect(projectFindView.regexOptionButton).toHaveClass('selected')
+          expect(atom.project.scan.mostRecentCall.args[0]).toEqual /i(\w)ems+/gi
+
+        it "toggles regex option via a button and finds files matching the pattern", ->
+          expect(projectFindView.regexOptionButton).not.toHaveClass('selected')
+          projectFindView.regexOptionButton.click()
+          expect(projectFindView.regexOptionButton).toHaveClass('selected')
+          expect(atom.project.scan.mostRecentCall.args[0]).toEqual /i(\w)ems+/gi

     describe "case sensitivity", ->
       beforeEach ->
         editor.trigger 'project-find:show'
         spyOn(atom.project, 'scan').andCallFake -> Q()
         projectFindView.findEditor.setText('ITEMS')
+        projectFindView.trigger 'core:confirm'
+        waitsForPromise -> searchPromise

       it "runs a case insensitive search by default", ->
-        projectFindView.trigger 'core:confirm'
         expect(atom.project.scan.argsForCall[0][0]).toEqual /ITEMS/gi

       it "toggles case sensitive option via an event and finds files matching the pattern", ->
         expect(projectFindView.caseOptionButton).not.toHaveClass('selected')
         projectFindView.trigger 'project-find:toggle-case-option'
         expect(projectFindView.caseOptionButton).toHaveClass('selected')
-        expect(atom.project.scan.argsForCall[0][0]).toEqual /ITEMS/g
+        expect(atom.project.scan.mostRecentCall.args[0]).toEqual /ITEMS/g

       it "toggles case sensitive option via a button and finds files matching the pattern", ->
         expect(projectFindView.caseOptionButton).not.toHaveClass('selected')
         projectFindView.caseOptionButton.click()
         expect(projectFindView.caseOptionButton).toHaveClass('selected')
-        expect(atom.project.scan.argsForCall[0][0]).toEqual /ITEMS/g
+        expect(atom.project.scan.mostRecentCall.args[0]).toEqual /ITEMS/g

     describe "when core:confirm is triggered", ->
       beforeEach ->


Make timeout 30s not 3s

Make timeout 30s not 3s

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -4,7 +4,7 @@
 {fs, $, RootView} = require 'atom'

 # Default to 30 second promises
-waitsForPromise = (fn) -> window.waitsForPromise timeout: 3000, fn
+waitsForPromise = (fn) -> window.waitsForPromise timeout: 30000, fn

 describe 'ProjectFindView', ->
   [editor, projectFindView, searchPromise] = []


Fix bug in turbolinks teardown

Fix bug in turbolinks teardown

--- vendor/assets/javascripts/consolation.js.coffee
+++ vendor/assets/javascripts/consolation.js.coffee
@@ -50,8 +50,8 @@
 class Consolation
   consoles: []
   constructor: () ->
-    @consoles = document.querySelectorAll('.consolation-console')
-    new ConsolationConsole(c) for c in @consoles
+    elements = document.querySelectorAll('.consolation-console')
+    @consoles.push(new ConsolationConsole(c)) for c in elements

 class AsyncRequestDelegate
   constructor: (@options) ->


Take care of some case-insensitive keywords

--- grammars/powershell.cson
+++ grammars/powershell.cson
@@ -74,17 +74,17 @@
   }
   {
     'comment': 'keywords that delimit flow blocks'
-    'match': '\\b(catch|else|elseif|except|finally|for|if|try|while|with|trap|begin|process|end)\\b'
+    'match': '\\b(?i:(catch|else|elseif|except|finally|for|if|try|while|with|trap|begin|process|end))\\b'
     'name': 'keyword.control.flow.powershell'
   }
   {
     'comment': 'keywords that alter flow from within a block'
-    'match': '\\b(break|continue|pass|raise|return|exit|yield)\\b'
+    'match': '\\b(?i:(break|continue|pass|raise|return|exit|yield))\\b'
     'name': 'keyword.control.flow.powershell'
   }
   {
     'comment': 'keyword operators that evaluate to True or False'
-    'match': '\\b(!|and|in|is|not|or)\\b'
+    'match': '\\b(?i:(!|and|in|is|not|or))\\b'
     'name': 'keyword.operator.logical.powershell'
   }
   {
@@ -108,7 +108,7 @@
       '1':
         'name': 'keyword.other.powershell'
     'comment': 'keywords that haven\'t fit into other groups (yet).'
-    'match': '\\b(as|assert|del|exec|print|param)\\b'
+    'match': '\\b(?i:(as|assert|del|exec|print|param))\\b'
   }
   {
     'match': '-le|-ge|-ne|-notlike|-not'
@@ -127,7 +127,7 @@
     'name': 'keyword.operator.assignment.powershell'
   }
   {
-    'begin': '^\\s*(function)\\s+(?=[A-Za-z_][A-Za-z0-9_]*\\s*(\\(?))'
+    'begin': '^\\s*([Ff]unction)\\s+(?=[A-Za-z_][A-Za-z0-9_]*\\s*(\\(?))'
     'beginCaptures':
       '1':
         'name': 'storage.type.function.powershell'


Add extern, default keywords

Add extern, default keywords

--- grammars/protosnirk.cson
+++ grammars/protosnirk.cson
@@ -34,7 +34,7 @@
     }
     'keyword_variable': {
         'name': 'keyword.control.protosnirk'
-        'match': '\\b(let|mut|const|static|provided|get|set|owned|shared|ref|self|typedef)\\b'
+        'match': '\\b(let|mut|const|static|provided|get|set|owned|shared|ref|self|typedef|default|extern)\\b'
     }
     'keyword_control': {
         'name': 'keyword.control.protosnirk'


Fix typo in comment

Fix typo in comment

--- grammars/jison.cson
+++ grammars/jison.cson
@@ -246,7 +246,7 @@
                 # GerHobbelt Jison supports %epsilon, ε (U+03B5 GREEK SMALL
                 # LETTER EPSILON), and these variants of the epsilon character:
                 # Ɛ U+0190 LATIN CAPITAL LETTER OPEN E
-                # ɛ U+0250 LATIN SMALL LETTER OPEN E
+                # ɛ U+025B LATIN SMALL LETTER OPEN E
                 # ϵ U+03F5 GREEK LUNATE EPSILON SYMBOL
                 name:  "keyword.other.empty.jison"
                 match: "(?:%(?:e(?:mpty|psilon))|\\b[\u0190\u025B\u03B5\u03F5])\\b"


Fix highlighting of enclosed functions in regexm and ustrregexm

--- grammars/stata.cson
+++ grammars/stata.cson
@@ -1357,7 +1357,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1411,7 +1411,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1463,7 +1463,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1506,7 +1506,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1542,7 +1542,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1583,7 +1583,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1714,7 +1714,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1778,7 +1778,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1840,7 +1840,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1883,7 +1883,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1922,7 +1922,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'
@@ -1963,7 +1963,7 @@
                 'include': '#macro-global'
               }
               {
-                'include': '#builtin_functions'
+                'include': '#functions'
               }
               {
                 'match': '[A-Za-z_][A-Za-z0-9_]{0,31}'


alert when program name is illegal

alert when program name is illegal

--- grammars/stata.cson
+++ grammars/stata.cson
@@ -285,7 +285,7 @@
   'match': '\\b(qui(e|et|etl|etly)?|n(o|oi|ois|oisi|oisil|oisily)?|cap(t|tu|tur|ture)?)\\b:?'
   }
   {
-    'match': '\\s*(pr(o|og|ogr|ogra|ogram)?)\\s+((de(f|fi|fin|fine)?|di(r)?|drop|l(i|is|ist)?)?\\s+)?([A-Za-z_][A-Za-z0-9_]{0,31})'
+    'match': '\\s*(pr(o|og|ogr|ogra|ogram)?)\\s+((di(r)?|drop|l(i|is|ist)?)\\s+)([A-Za-z_][A-Za-z0-9_]{0,31})'
     'captures':
       '1':
         'name': 'storage.type.function.stata'
@@ -293,6 +293,31 @@
         'name': 'storage.type.function.stata'
       '7':
         'name': 'entity.name.function.stata'
+  }
+  {
+    'begin': '\\s*(pr(o|og|ogr|ogra|ogram)?)\\s+(de(f|fi|fin|fine)?\\s+)?'
+    'beginCaptures':
+      '1':
+        'name': 'storage.type.function.stata'
+      '3':
+        'name': 'storage.type.function.stata'
+    'end': '(?=,|\\n|/)'
+    'patterns': [
+      {
+        'include': '#macro-local'
+      }
+      {
+        'include': '#macro-global'
+      }
+      {
+        'match': '([A-Za-z_][A-Za-z0-9_]{0,31})'
+        'name': 'entity.name.function.stata'
+      }
+      {
+        'match': '[^A-za-z_0-9,\\n/ ]+'
+        'name': 'invalid.illegal.name.stata'
+      }
+    ]
   }
   {
     'match': '\\b(form(a|at)?)\\s*([A-Za-z_][A-Za-z0-9_]{0,31})*\\s*(%)(\-)?(0)?([0-9]+)(\.)([0-9]+)(e|f|g)(c)?'


Use @ui hash

Use @ui hash

--- core/app/backbone/modules/modal.coffee
+++ core/app/backbone/modules/modal.coffee
@@ -10,13 +10,17 @@
     regions:
       modalRegion: '.modal-body'

+    ui:
+      modal: '.js-modal'
+      layer: '.js-layer'
+
     templateHelpers: =>
       title: @options.title

     onRender: ->
       @modalRegion.show @options.content_view
-      @$('.js-modal').show()
-      @$('.js-layer').show()
+      @ui.modal.show()
+      @ui.layer.show()

     stopPropagation: (e) ->
       e.stopPropagation()


Launch import on redirect with OAuth connector. Redirect to connected page (instead of konnector's detail page).

--- server/controllers/konnectors.coffee
+++ server/controllers/konnectors.coffee
@@ -121,6 +121,13 @@
         req.konnector.updateFieldValues { accounts: accounts }, (err) ->
             return next err if err

+            req.konnector.import (err, notifContent) ->
+                if err?
+                    log.error err
+                else
+                    handleNotification req.konnector, notifContent
+
+
             res.status(200).send """<!DOCTYPE html>
 <html>
 <head>
@@ -129,18 +136,7 @@
 <body>
     <script type="text/javascript">
         window.onload = function() {
-            //refreshParent;
-            if(window.opener){
-              window.opener.location.reload();
-              setTimeout(function() {
-                  window.close();
-              }, 500);
-            }
-            else {
-              window.location.href =
-                  "../../../#/category/#{req.konnector.category}/" +
-                        "#{req.konnector.slug}"
-            }
+            window.location.href = "../../../#/connected"
         };
     </script>
 </body>
@@ -176,5 +172,3 @@
         konnector.connectUrl = konnectorModule.connectUrl

     return konnector
-
-


Basic downloading of materials

Basic downloading of materials

--- server/cron/downloadMaterials.coffee
+++ server/cron/downloadMaterials.coffee
@@ -92,10 +92,9 @@
         logger.error("Can't find statement for problem " + href)
         return undefined

-    id = document.getElementById("problem_id")
-    if not id
-        logger.error("Can't find id for problem " + href)
-        return undefined
+    re = new RegExp '.*view3.php\\?id=\\d+&chapterid=(\\d+)'
+    res = re.exec href
+    id = res[1]

     name = document.getElementsByTagName("title")[0]
     if not name
@@ -107,38 +106,45 @@
         text += "<div>" + tag.innerHTML + "</div>"

     material = new Material
-        _id: "p" + id.innerHTML,
+        _id: "p" + id,
         order: order,
         type: "problem",
         text: text,
         materials: []
     await material.upsert()
     return material
-
-
-parseStatements = (activity, order) ->
-    if activity.children.length != 2
-        logger.error("Found resource with >2 children " + activity.innerHTML)
-        return undefined
-    a = activity.children[1]
-
-    document = await downloadAndParse(a.href)
+
+
+getProblemsHrefsFromStatements = (href) ->
+    document = await downloadAndParse(href)
     toc = document.getElementsByClassName("statements_toc_alpha")
     if toc.length > 1
-        logger.error("Found several tocs in statements " + a.href)
+        logger.error("Found several tocs in statements " + href)
         return undefined
     toc = toc[0]

-    hrefs = [a.href]
+    hrefs = []
     tags = toc.getElementsByTagName("a")
     for tag in tags
         if tag.href.startsWith("http://informatics.mccme.ru/mod/statements/view3.php")
             hrefs.push(tag.href)
         else
-            logger.error("Strange link in statements toc: " + tag.href + " " + a.href)
+            logger.error("Strange link in statements toc: " + tag.href + " " + href)
+
+    return hrefs
+
+parseStatements = (activity, order) ->
+    if activity.children.length != 2
+        logger.error("Found resource with >2 children " + activity.innerHTML)
+        return undefined
+    a = activity.children[1]
+
+    hrefs = await getProblemsHrefsFromStatements(a.href)
+    hrefs2 = await getProblemsHrefsFromStatements(hrefs[0])
+    hrefs.splice(0, 0, hrefs2[0])

     name = a.innerHTML
-    logger.info(name + ": found " + hrefs.length + " problems")
+    logger.info(name + ": found " + hrefs.length + " problems: " + hrefs.join(" "))

     materials = []
     for href, i in hrefs
@@ -154,9 +160,6 @@
         materials: materials
     await material.upsert()
     return material
-
-
-    return undefined

 parseActivity = (activity, order) ->


adding settings for pigments

adding settings for pigments

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -52,3 +52,5 @@
   "file-icons":
     forceShow: true
     coloured: false
+  pigments:
+    markerType: "square-dot"


account: align requires

account: align requires

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -15,13 +15,13 @@
   @trait __dirname, '../traits/notifying'
   @trait __dirname, '../traits/flaggable'

-  JAppStorage = require './appstorage'
-  JTag = require './tag'
-  CActivity = require './activity'
-  Graph     = require "./graph/graph"
-  @getFlagRole = 'content'
-  JName = require './name'
-
+  JAppStorage  = require './appstorage'
+  JTag         = require './tag'
+  CActivity    = require './activity'
+  Graph        = require "./graph/graph"
+  JName        = require './name'
+
+  @getFlagRole            = 'content'
   @lastUserCountFetchTime = 0

   {ObjectId, Register, secure, race, dash, daisy} = require 'bongo'


datadog: send alerts to #alerts channel

datadog: send alerts to #alerts channel

--- workers/social/lib/social/models/datadog.coffee
+++ workers/social/lib/social/models/datadog.coffee
@@ -24,7 +24,7 @@
     MachineStateFailed :
       title            : "vms.failed"
       text             : "VM start failed for user: %nickname%"
-      notify           : "@slack-_devops"
+      notify           : "@slack-alerts"
       tags             : ["user:%nickname%", "context:vms"]


fixed coffeelint errors on workers/social/lib/social/models/payment.coffee

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -1,5 +1,6 @@
-Bongo = require "bongo"
-{secure, signature, Base} = Bongo
+Bongo = require 'bongo'
+{ secure, signature, Base } = Bongo
+KodingError = require '../error'

 module.exports = class Payment extends Base
   @share()
@@ -29,64 +30,64 @@
           (signature Function)

-  { get, post, deleteReq } = require "./socialapi/requests"
-
-  socialProxyUrl = "/api/social"
-
-
-  @subscribe = secure (client, data, callback)->
+  { get, post, deleteReq } = require './socialapi/requests'
+
+  socialProxyUrl = '/api/social'
+
+
+  @subscribe = secure (client, data, callback) ->
     requiredParams = [
-      "token", "email", "planTitle", "planInterval", "provider"
+      'token', 'email', 'planTitle', 'planInterval', 'provider'
     ]

     @canUserPurchase client, (err, confirmed) ->
       return callback err  if err
-      return callback { message: ERR_USER_NOT_CONFIRMED }  unless confirmed
-
-      validateParams requiredParams, data, (err)->
+      return callback new KodingError ERR_USER_NOT_CONFIRMED  unless confirmed
+
+      validateParams requiredParams, data, (err) ->
         return callback err  if err

-        canChangePlan client, data.planTitle, (err)->
+        canChangePlan client, data.planTitle, (err) ->
           return callback err  if err

           data.accountId = getAccountId client
           url = "#{socialProxyUrl}/payments/subscribe"

-          post url, data, (err, response)->
+          post url, data, (err, response) ->
             callback err, response

-            data.status = if err then "$failed" else "$success"
-
-            SiftScience = require "./siftscience"
-            SiftScience.transaction client, data, (err)->
-              log "logging to SiftScience failed", err  if err
-
-
-  @subscriptions$ = secure (client, data, callback)->
+            data.status = if err then '$failed' else '$success'
+
+            SiftScience = require './siftscience'
+            SiftScience.transaction client, data, (err) ->
+              log 'logging to SiftScience failed', err  if err
+
+
+  @subscriptions$ = secure (client, data, callback) ->
     Payment.subscriptions client, data, callback

-  @subscriptions = (client, data, callback)->
+  @subscriptions = (client, data, callback) ->
     data.accountId = getAccountId client
     url = "#{socialProxyUrl}/payments/subscriptions?account_id=#{data.accountId}"

     get url, data, callback

-  @invoices = secure (client, data, callback)->
+  @invoices = secure (client, data, callback) ->
     data.accountId = getAccountId client
     url = "#{socialProxyUrl}/payments/invoices/#{data.accountId}"

     get url, data, callback

-  @creditCard = secure (client, data, callback)->
+  @creditCard = secure (client, data, callback) ->
     data.accountId = getAccountId client
     url = "#{socialProxyUrl}/payments/creditcard/#{data.accountId}"

     get url, data, callback

-  @updateCreditCard = secure (client, data, callback)->
-    requiredParams = [ "token" , "provider"]
-
-    validateParams requiredParams, data, (err)->
+  @updateCreditCard = secure (client, data, callback) ->
+    requiredParams = [ 'token' , 'provider']
+
+    validateParams requiredParams, data, (err) ->
       return callback err  if err

       data.accountId = getAccountId client
@@ -94,113 +95,115 @@

       post url, data, callback

-  @canChangePlan = secure (client, data, callback)->
-    requiredParams = [ "planTitle" ]
-
-    validateParams requiredParams, data, (err)->
+  @canChangePlan = secure (client, data, callback) ->
+    requiredParams = [ 'planTitle' ]
+
+    validateParams requiredParams, data, (err) ->
       return callback err  if err

       canChangePlan client, data.planTitle, callback

-  @deleteAccount = (client, callback)->
+  @deleteAccount = (client, callback) ->
     accountId = getAccountId client
     url = "#{socialProxyUrl}/payments/customers/#{accountId}"

     deleteReq url, {}, callback

-  @getToken = (data, callback)->
-    requiredParams = [ "planTitle", "planInterval" ]
-
-    validateParams requiredParams, data, (err)->
+  @getToken = (data, callback) ->
+    requiredParams = [ 'planTitle', 'planInterval' ]
+
+    validateParams requiredParams, data, (err) ->
       return callback err  if err

       url = "#{socialProxyUrl}/payments/paypal/token"
       get url, data, callback

-  @logOrder = secure (client, raw, callback)->
-    SiftScience = require "./siftscience"
+  @logOrder = secure (client, raw, callback) ->
+    SiftScience = require './siftscience'
     SiftScience.createOrder client, raw, callback

-  @canUserPurchase = secure (client, callback)->
-    {connection : {delegate}} = client
-
-    if delegate.type isnt "registered"
-      return callback {message:"guests are not allowed"}
-
-    delegate.fetchUser (err, user)->
-      return callback err  if err
-      callback null, user.status is "confirmed"
-
-  validateParams = (requiredParams, data, callback)->
+  @canUserPurchase = secure (client, callback) ->
+    { connection : { delegate } } = client
+
+    if delegate.type isnt 'registered'
+      return callback new KodingError 'guests are not allowed'
+
+    delegate.fetchUser (err, user) ->
+      return callback err  if err
+      callback null, user.status is 'confirmed'
+
+  validateParams = (requiredParams, data, callback) ->
     for param in requiredParams
       if not data[param]
-        return callback {message: "#{param} is required"}
+        return callback new KodingError "#{param} is required"

     callback null

-  getAccountId = (client)->
+  getAccountId = (client) ->
     return client.connection.delegate.getId()

-  getUserName = (client)->
+  getUserName = (client) ->
     return client.connection.delegate.profile.nickname

-  prettifyFeature = (name)->
+  prettifyFeature = (name) ->
     switch name
-      when "alwaysOn"
-        "alwaysOn vms"
-      when "storage"
-        "GB storage"
-      when "total"
-        "total vms"
-      when "snapshots"
-        "total snapshots"
-
-  canChangePlan = (client, planTitle, callback)->
-    fetchPlan client, planTitle, (err, plan)->
-      return callback err  if err
-
-      fetchUsage client, (err, usage)->
+      when 'alwaysOn'
+        'alwaysOn vms'
+      when 'storage'
+        'GB storage'
+      when 'total'
+        'total vms'
+      when 'snapshots'
+        'total snapshots'
+
+  canChangePlan = (client, planTitle, callback) ->
+    fetchPlan client, planTitle, (err, plan) ->
+      return callback err  if err
+
+      fetchUsage client, (err, usage) ->
         return callback err  if err

-        for name in ["alwaysOn", "storage", "total", "snapshots"]
+        for name in ['alwaysOn', 'storage', 'total', 'snapshots']
           if usage[name] > plan[name]
             return callback {
-              "message"   : "Sorry, your request to downgrade can't be processed because you are currently using more resources than the plan you are trying to downgrade to allows."
-              "allowed"   : plan[name]
-              "usage"     : usage[name]
-              "planTitle" : planTitle
-              "name"      : prettifyFeature name
+              'message'   : "Sorry, your request to downgrade can't be processed because you are currently using more resources than the plan you are trying to downgrade to allows."
+              'allowed'   : plan[name]
+              'usage'     : usage[name]
+              'planTitle' : planTitle
+              'name'      : prettifyFeature name
             }

         callback null

-  fetchUsage = (client, callback)->
-    ComputeProvider = require "./computeproviders/computeprovider"
-    ComputeProvider.fetchUsage client, { provider: "koding" }, callback
-
-  fetchPlan = (client, planTitle, callback)->
-
-    plans   = require "./computeproviders/plans"
-
-    return callback {"message" : "plan not found"}  unless plans[planTitle]
-
-    {clone} = require 'underscore'
+  fetchUsage = (client, callback) ->
+    ComputeProvider = require './computeproviders/computeprovider'
+    ComputeProvider.fetchUsage client, { provider: 'koding' }, callback
+
+  fetchPlan = (client, planTitle, callback) ->
+
+    plans   = require './computeproviders/plans'
+
+    return callback new KodingError 'plan not found'  unless plans[planTitle]
+
+    { clone } = require 'underscore'
     plan    = clone plans[planTitle]

-    fetchReferrerSpace client, (err, space)->
+    fetchReferrerSpace client, (err, space) ->
       return callback err  if err

       plan.storage += space

       callback null, plan

-  fetchReferrerSpace = (client, callback)->
+  fetchReferrerSpace = (client, callback) ->
     originId = client.connection.delegate.getId()

     JReward = require './rewards'
     options = { unit: 'MB', type: 'disk', originId }

-    JReward.fetchEarnedAmount options, (err, amount)->
+    JReward.fetchEarnedAmount options, (err, amount) ->
       return callback err  if err?
       callback null, amount / 1000
+
+


JSession: updateClientIP: take the first part of the ip

--- workers/social/lib/social/models/session.coffee
+++ workers/social/lib/social/models/session.coffee
@@ -98,5 +98,8 @@
         callback null, session

   @updateClientIP = (clientId, ipAddress, callback)->
+
+    ipAddress = (ipAddress.split ',')[0]
+
     JSession.update {clientId: clientId}, {$set: clientIP: ipAddress}, (err)->
       callback err


Context arg is optional for Client #47

Context arg is optional for Client #47

--- wzk/resource/Client.coffee
+++ wzk/resource/Client.coffee
@@ -12,9 +12,9 @@
   ###*
     @constructor
     @param {wzk.net.XhrFactory} xhrFac
-    @param {string} context
-  ###
-  constructor: (@xhrFac, @context) ->
+    @param {string=} context
+  ###
+  constructor: (@xhrFac, @context = '') ->
     @headers = {}
     @headers[goog.net.XhrIo.CONTENT_TYPE_HEADER] = 'application/json'
     @headers['Accept'] = 'application/json'


Highlight .COMMENT lines as unquoted strings

These commands appear to be providing debugging feedback, going by their
usage in tests/fixtures/runoff/aplsf.rnh. If so, we don't want a command
pattern being highlighted like one.

--- grammars/runoff.cson
+++ grammars/runoff.cson
@@ -112,6 +112,19 @@
 			patterns: [include: "#arguments"]
 		},{

+			# "Comment" command (debugging feedback?)
+			name: "meta.function.debugging-comment.runoff"
+			begin: "(?i)((\\.)COMMENT)\\b"
+			end: "$|(;)|(?=!)"
+			contentName: "string.unquoted.runoff"
+			beginCaptures:
+				0: name: "keyword.function.name.runoff"
+				1: name: "entity.name.function.runoff"
+				2: name: "punctuation.definition.function.runoff"
+			endCaptures:
+				1: name: "punctuation.terminator.statement.runoff"
+		},{
+
 			# Generic command
 			name: "meta.function.runoff"
 			begin: "(?i)((\\.)(?:END\\s+)?([^;\\s\\-+]+))"


fix tests for pdf header passing

fix tests for pdf header passing

--- services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
+++ services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
@@ -137,7 +137,7 @@
 				statusCode: 204
 				headers: { "mock": "header" }
 			@req.method = "mock-method"
-
+			@req.headers = { 'Mock': 'Headers' }

 		describe "user with standard priority", ->

@@ -152,6 +152,7 @@
 						method: @req.method
 						url: "#{@settings.apis.clsi.url}#{@url}",
 						timeout: 60 * 1000
+						headers: @req.headers
 					)
 					.should.equal true

@@ -175,6 +176,7 @@
 						method: @req.method
 						url: "#{@settings.apis.clsi_priority.url}#{@url}",
 						timeout: 60 * 1000
+						headers: @req.headers
 					)
 					.should.equal true


Ignore errors

Ignore errors

--- src/browser/squirrel-update.coffee
+++ src/browser/squirrel-update.coffee
@@ -46,11 +46,11 @@
   installFileMenu = ->
     args = [fileKeyPath, '/ve', '/d', 'Open with Atom', '/f']
     spawnReg args, (error) ->
-      return callback(error) if error?
+      # return callback(error) if error?

       args = [fileKeyPath, '/v', 'Icon', '/d', process.execPath, '/f']
       spawnReg args, (error) ->
-        return callback(error) if error?
+        # return callback(error) if error?

         args = ["#{fileKeyPath}\\command", '/ve', '/d', process.execPath, '/f']
         spawnReg(args, callback)


cmdArgs -> args

cmdArgs -> args

--- src/browser/squirrel-update.coffee
+++ src/browser/squirrel-update.coffee
@@ -9,14 +9,13 @@
 # Spawn the Update.exe with the given arguments and invoke the callback when
 # the command completes.
 exports.spawn = (args, callback) ->
+  command = process.env.comspec or 'cmd.exe'
   args = args.map (arg) -> "\"#{arg.toString().replace(/"/g, '\\"')}\""
   if /\s/.test(updateDotExe)
     args.unshift("\"#{updateDotExe}\"")
   else
     args.unshift(updateDotExe)
-
-  args = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
-  command = process.env.comspec or 'cmd.exe'
+  args = ['/s', '/c', "\"#{args.join(' ')}\""]

   updateProcess = ChildProcess.spawn(command, args, windowsVerbatimArguments: true)


Load event-palette extension in defaultConfig

--- .atom/default-config.coffee
+++ .atom/default-config.coffee
@@ -1,3 +1,4 @@
+requireExtension 'event-palette'
 requireExtension 'autocomplete'
 requireExtension 'strip-trailing-whitespace'
 requireExtension 'fuzzy-finder'


change: Opening a file through the terminal shouldn't open it for all other collaborators, ESPECIALLY when collaborators are working on other files

--- salvus/page/console.coffee
+++ salvus/page/console.coffee
@@ -96,7 +96,8 @@
                 @opts.renderer = 'ttyjs'

         # The is_focused variable keeps track of whether or not the
-        # editor is focused.  This impacts the cursor, at least.
+        # editor is focused.  This impacts the cursor, and also whether
+        # messages such as open_file or open_directory are handled (see @init_mesg).
         @is_focused = false

         # Create the DOM element that realizes this console, from an HTML template.
@@ -191,17 +192,14 @@
         #console.log("init_mesg")
         @_ignore_mesg = false
         @terminal.on 'mesg', (mesg) =>
-            if @_ignore_mesg
+            if @_ignore_mesg or not @is_focused   # ignore messages when terminal not in focus (otherwise collaboration is confusing)
                 return
-            #console.log("got message '#{mesg}', length=#{mesg.length}")
             try
                 mesg = from_json(mesg)
                 switch mesg.event
                     when 'open_file'
-                        #console.log("now opening #{mesg.name}...", @opts.editor)
                         @opts.editor?.project_page.open_file(path:mesg.name, foreground:true)
                     when 'open_directory'
-                        #console.log("changing to directory #{mesg.name}")
                         @opts.editor?.project_page.chdir(mesg.name)
                         @opts.editor?.project_page.display_tab("project-file-listing")
             catch e


Add bind title directive.

Add bind title directive.

--- app/coffee/modules/base/bind.coffee
+++ app/coffee/modules/base/bind.coffee
@@ -57,6 +57,13 @@
             $el.attr("title", val)
     return {link:link}

+BindTitleDirective = ->
+    link = ($scope, $el, $attrs) ->
+        $scope.$watch $attrs.tgTitleHtml, (val) ->
+            $el.attr("title", val) if val?
+
+    return {link:link}
+
 BindHtmlDirective = ->
     link = ($scope, $el, $attrs) ->
         $scope.$watch $attrs.tgBindHtml, (val) ->
@@ -64,11 +71,11 @@

     return {link:link}

-
 module = angular.module("taigaBase")
 module.directive("tgBoHtml", BindOnceHtmlDirective)
 module.directive("tgBoRef", BindOnceRefDirective)
 module.directive("tgBoSrc", BindOnceSrcDirective)
 module.directive("tgBoAlt", BindOnceAltDirective)
 module.directive("tgBoTitle", BindOnceTitleDirective)
+module.directive("tgBindTitle", BindTitleDirective)
 module.directive("tgBindHtml", BindHtmlDirective)


Cambio docs

Cambio docs

--- greedy/cambio.coffee
+++ greedy/cambio.coffee
@@ -1,3 +1,9 @@
+###*
+ * Cambio
+ * @param {number[]} Denominaciones de monedas
+ * @param {number} Monto a devolver
+ * @return {number} Cantidad de monedas necesarias
+###
 cambio = (monedas, monto) ->
   cantidad_monedas = 0
   indice = 0


Refactor Safari scroll patcher; ensure it works after PDF is reloaded.

--- services/web/public/coffee/ide/SafariScrollPatcher.coffee
+++ services/web/public/coffee/ide/SafariScrollPatcher.coffee
@@ -2,58 +2,79 @@
 ], () ->
 	class SafariScrollPatcher
 		constructor: (ide, $scope) ->
-			@isBootstrapped = false
-			@isOverAce = false
+			@isOverAce = false # Flag to control if the pointer is over Ace.
 			@pdfDiv = null
 			@aceDiv = null

+			# Start listening to PDF wheel events when the pointer leaves the PDF region.
+			# P.S. This is the problem in a nutshell: although the pointer is elsewhere,
+			# wheel events keep being dispatched to the PDF.
+			@handlePdfDivMouseLeave = () =>
+				console.log "dispatching to ace enabled"
+				@pdfDiv.addEventListener "wheel", @dispatchToAce
+
+			# Stop listening to wheel events when the pointer enters the PDF region. If
+			# the pointer is over the PDF, native behaviour is adequate.
+			@handlePdfDivMouseEnter = () =>
+				console.log "dispatching to ace disabled"
+				@pdfDiv.removeEventListener "wheel", @dispatchToAce
+
+			# Set the "pointer over Ace" flag as false, when the mouse leaves its area.
+			@handleAceDivMouseLeave = () =>
+				@isOverAce = false
+				console.log 'is over ace = ' + @isOverAce
+
+			# Set the "pointer over Ace" flag as true, when the mouse enters its area.
+			@handleAceDivMouseEnter = () =>
+				@isOverAce = true
+				console.log 'is over ace = ' + @isOverAce
+
+			# Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
+			# If elements are already defined, clear existing event listeners and do
+			# the process again (grab elements, set listeners).
+			@setListeners = () =>
+				@isOverAce = false
+
+				# If elements aren't null, remove existing listeners.
+				if @pdfDiv?
+					@pdfDiv.removeEventListener @handlePdfDivMouseLeave
+					@pdfDiv.removeEventListener @handlePdfDivMouseEnter
+
+				if @aceDiv?
+					@aceDiv.removeEventListener @handleAceDivMouseLeave
+					@aceDiv.removeEventListener @handleAceDivMouseEnter
+
+				# Grab elements.
+				@pdfDiv = document.querySelector ".pdfjs-viewer"	# Grab the PDF div.
+				@aceDiv = document.querySelector ".ace_content"	# Also the editor.
+
+				# Set hover-related listeners.
+				@pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave
+				@pdfDiv.addEventListener "mouseenter", @handlePdfDivMouseEnter
+				@aceDiv.addEventListener "mouseleave", @handleAceDivMouseLeave
+				@aceDiv.addEventListener "mouseenter", @handleAceDivMouseEnter
+
+			# Handler for wheel events on the PDF.
+			# If the pointer is over Ace, grab the event, prevent default behaviour
+			# and dispatch it to Ace.
+			@dispatchToAce = (e) =>
+				if @isOverAce
+					# If this is logged, the problem just happened: the event arrived
+					# here (the PDF wheel handler), but it should've gone to Ace.
+					console.log "Event was bound to the PDF, dispatching to Ace"
+
+					# Small timeout - if we dispatch immediately, an exception is thrown.
+					window.setTimeout(() =>
+						# Dispatch the exact same event to Ace (this will keep values
+						# values e.g. `wheelDelta` consistent with user interaction).
+						@aceDiv.dispatchEvent e
+					, 5)
+
+					# Avoid scrolling the PDF, as we assume this was intended to the
+					# editor.
+					e.preventDefault()
+
 			$scope.$on "loaded", () =>
-				console.log this
-				if !@isBootstrapped
-					console.log "bootstrapping"
-
-					@isBootstrapped = true;
-					@pdfDiv = document.querySelector ".pdfjs-viewer"	# Grab the PDF div.
-					@aceDiv = document.querySelector ".ace_content"	# Also the editor.
-					@isOverAce = false # Flag to control if the pointer is over Ace.
-
-					# Start listening to PDF wheel events when the pointer leaves the PDF region.
-					# P.S. This is the problem in a nutshell: although the pointer is elsewhere,
-					# wheel events keep being dispatched to the PDF.
-					@pdfDiv.addEventListener "mouseleave", () =>
-						@pdfDiv.addEventListener "wheel", dispatchToAce
-
-					# Stop listening to wheel events when the pointer enters the PDF region. If
-					# the pointer is over the PDF, native behaviour is adequate.
-					@pdfDiv.addEventListener "mouseenter", () =>
-						@pdfDiv.removeEventListener "wheel", dispatchToAce
-
-					# Set the "pointer over Ace" flag as false, when the mouse leaves its area.
-					@aceDiv.addEventListener "mouseleave", () =>
-						@isOverAce = false
-
-					# Set the "pointer over Ace" flag as true, when the mouse enters its area.
-					@aceDiv.addEventListener "mouseenter", () =>
-						@isOverAce = true
-
-					# Handler for wheel events on the PDF.
-					# If the pointer is over Ace, grab the event, prevent default behaviour
-					# and dispatch it to Ace.
-					dispatchToAce = (e) =>
-						if @isOverAce
-							# If this is logged, the problem just happened: the event arrived
-							# here (the PDF wheel handler), but it should've gone to Ace.
-							console.log "Event was bound to the PDF, dispatching to Ace"
-
-							# Small timeout - if we dispatch immediately, an exception is thrown.
-							window.setTimeout(() =>
-								# Dispatch the exact same event to Ace (this will keep values
-								# values e.g. `wheelDelta` consistent with user interaction).
-								@aceDiv.dispatchEvent e
-							, 5)
-
-							# Avoid scrolling the PDF, as we assume this was intended to the
-							# editor.
-							e.preventDefault()
+				@setListeners()


remove random and unused copy/paste cruft.

--- src/code/stores/nodes-store.coffee
+++ src/code/stores/nodes-store.coffee
@@ -51,5 +51,3 @@
   actions: nodeActions
   store: nodeStore
   mixin: mixin
-
-window.PaletteStore = module.exports


Repeater returns the whole error on the last fail

--- src/coffee/helpers/repeater.coffee
+++ src/coffee/helpers/repeater.coffee
@@ -57,7 +57,9 @@

     debug '(re)-trying task, %d remaining attempts', remainingAttempts
     if remainingAttempts is 0
-      defer.reject new Error "Failed to retry the task after #{@_options.attempts} attempts. Cause: #{lastError.stack}"
+      defer.reject
+        message: "Failed to retry the task after #{@_options.attempts} attempts."
+        error: lastError
     else
       task()
       .then (r) -> defer.resolve r


eventSet error message

eventSet error message

--- src/collections/assignments.coffee
+++ src/collections/assignments.coffee
@@ -97,7 +97,7 @@
 exports.createBulkAssignments = (eventSet, description, teamId, createAsMemberId, callback) ->
   unless eventSets.indexOf(eventSet) > -1
     throw new TSArgsError 'teamsnap.createBulkAssignments',
-    " `eventSet` must be provided."
+    " `eventSet` must be one of the following: " + eventSets.toString() + "."
   unless description.trim()
     return @reject 'You must provide a description for the assignments.',
       'description', callback


update class for selections

update class for selections

--- spec/lanes/components/select-field/SelectFieldSpec.coffee
+++ spec/lanes/components/select-field/SelectFieldSpec.coffee
@@ -9,6 +9,8 @@
     {id: 2, code: "FORD",  name: "Ford Motor Co."}
     {id: 3, code: "DODGE", name: "Chrysler/Dodge"}
 ]}
+
+VALUE_CLASS = '.form-control-static'

 Car = Lanes.Test.defineModel(
     props: {id: 'integer', code: 'string', brand_id: 'integer'}
@@ -29,10 +31,10 @@
     it "renders read only", (done) ->
         sf = LT.renderComponent(LC.SelectField,
              props:{model: @car, name: 'brand', labelField: 'name'})
-        expect(_.dom(sf).qs('.value').text).toBe('')
+        expect(_.dom(sf).qs(VALUE_CLASS).text).toBe('')
         @car.set(brand: @brands.at(0))
         _.defer ->
-            expect(_.dom(sf).qs('.value').text).toBe('General Motors')
+            expect(_.dom(sf).qs(VALUE_CLASS).text).toBe('General Motors')
             done()

     it "renders as edit", ->
@@ -83,7 +85,7 @@
             model: @car, name: 'brand', labelField: 'name',
             choices: @brands.models
         })
-        expect(_.dom(sf).qs('.value').text).toEqual('Chrysler/Dodge')
+        expect(_.dom(sf).qs(VALUE_CLASS).text).toEqual('Chrysler/Dodge')

     it 'renders label for multiple values', ->
         @car.carTypes = [ @brands.at(0), @brands.at(2) ]
@@ -91,7 +93,7 @@
             queryModel: Car, multiSelect: true, model: @car, name: 'carTypes'
             labelField: 'name', choices: @brands.models
         })
-        expect(_.dom(sf).qs('.value').text).toEqual('General Motors and Chrysler/Dodge')
+        expect(_.dom(sf).qs(VALUE_CLASS).text).toEqual('General Motors and Chrysler/Dodge')

     it "renders all choices", ->


Scroll to the top after each spec

Scroll to the top after each spec

--- spec_app/spec/javascripts/helpers/reset_history.js.coffee
+++ spec_app/spec/javascripts/helpers/reset_history.js.coffee
@@ -18,3 +18,6 @@
   # Store original URL and title so specs may use it
   @locationBeforeExample = location.href
   @titleBeforeExample = document.title
+
+afterEach ->
+  up.viewport.root.scrollTop = 0


fix(menus): Customize context menu for mailto #2061

--- src/components/evented-iframe.cjsx
+++ src/components/evented-iframe.cjsx
@@ -205,8 +205,12 @@
     linkTarget = @_getContainingTarget(event, {with: 'href'})
     if linkTarget
       href = linkTarget.getAttribute('href')
-      menu.append(new MenuItem({ label: "Open Link", click:( -> NylasEnv.windowEventHandler.openLink({href}) )}))
-      menu.append(new MenuItem({ label: "Copy Link", click:( -> clipboard.writeText(href) )}))
+      if href.startsWith('mailto')
+        menu.append(new MenuItem({ label: "Compose Message...", click:( -> NylasEnv.windowEventHandler.openLink({href}) )}))
+        menu.append(new MenuItem({ label: "Copy Email Address", click:( -> clipboard.writeText(href.split('mailto:').pop()) )}))
+      else
+        menu.append(new MenuItem({ label: "Open Link", click:( -> NylasEnv.windowEventHandler.openLink({href}) )}))
+        menu.append(new MenuItem({ label: "Copy Link", click:( -> clipboard.writeText(href) )}))
       menu.append(new MenuItem({ type: 'separator' }))

     # Menu actions for images


updated review icon jsx in component

updated review icon jsx in component

--- src/components/exercise/group.cjsx
+++ src/components/exercise/group.cjsx
@@ -63,10 +63,7 @@
       className = group.replace(' ', '_')
       labels = @getGroupLabel(group, related_content)
       isSpacedPractice = className is 'spaced_practice'
-      if isSpacedPractice
-        icon =
-      else
-        icon =
+      icon =

       spacedPracticeHeading =
What is spaced practice?


updating underscore.coffee to 0.5.8

updating underscore.coffee to 0.5.8

--- examples/underscore.coffee
+++ examples/underscore.coffee
@@ -47,7 +47,7 @@

   # Current version.
-  _.VERSION: '0.5.7'
+  _.VERSION: '0.5.8'

   # ------------------------ Collection Functions: ---------------------------
@@ -58,7 +58,7 @@
     index: 0
     try
       return obj.forEach(iterator, context) if obj.forEach
-      if _.isArray(obj) or _.isArguments(obj)
+      if _.isNumber(obj.length)
         return iterator.call(context, obj[i], i, obj) for i in [0...obj.length]
       iterator.call(context, val, key, obj) for key, val of obj
     catch e
@@ -147,7 +147,7 @@
   # Determine if a given value is included in the array or object,
   # based on '==='.
   _.include: (obj, target) ->
-    return _.indexOf(obj, target) isnt -1 if _.isArray(obj)
+    return _.indexOf(obj, target) isnt -1 if obj and _.isFunction(obj.indexOf)
     for key, val of obj
       return true if val is target
     false


Just push state updated if chat is enabled.

--- app/assets/javascripts/app/controllers/chat.coffee
+++ app/assets/javascripts/app/controllers/chat.coffee
@@ -17,7 +17,7 @@
     if preferences && preferences.chat && preferences.chat.max_windows
       @maxChatWindows = parseInt(preferences.chat.max_windows)

-    @pushStateStarted = false
+    @pushStateIntervalOn = undefined
     @messageCounter = 0
     @meta =
       active: false
@@ -34,9 +34,8 @@
         App.Collection.loadAssets(data.assets)
       @meta = data
       @updateMeta()
-      if !@pushStateStarted
-        @pushStateStarted = true
-        @interval(@pushState, 30000, 'pushState')
+      if @pushStateIntervalOn is undefined
+        @startPushState()
     )

     # add new chat window
@@ -61,6 +60,15 @@
       App.WebSocket.send(event:'chat_status_agent')
     )

+  startPushState: =>
+    return if @pushStateIntervalOn
+    @pushStateIntervalOn = true
+    @interval(@pushState, 55000, 'pushState')
+
+  stopPushState: =>
+    @pushStateIntervalOn = false
+    @clearInterval('pushState')
+
   pushState: =>
     App.WebSocket.send(
       event:'chat_agent_state'
@@ -113,23 +121,20 @@
     if state is undefined
       return @meta.active

+    @meta.active = state
+
     # check if min one chat is active
     if state
+      @startPushState()
       preferences = @Session.get('preferences')
       if !preferences || !preferences.chat || !preferences.chat.active || _.isEmpty(preferences.chat.active)
         @notify(
           type: 'error'
           msg:  App.i18n.translateContent('To be able to chat you need to select min. one chat topic in settings!')
         )
-
-    @meta.active = state
-
-    # write state
-    App.WebSocket.send(
-      event:'chat_agent_state'
-      data:
-        active: @meta.active
-    )
+    else
+      @stopPushState()
+      @pushState()

   updateNavMenu: =>
     delay = ->
@@ -498,9 +503,10 @@
         label: label
         time: time

-  addStatusMessage: (message) ->
+  addStatusMessage: (message, args) ->
     @body.append App.view('customer_chat/chat_status_message')
       message: message
+      args: args

     @scrollToBottom()


Added iconTextClass to improve ticket popover.

--- app/assets/javascripts/app/models/ticket.js.coffee
+++ app/assets/javascripts/app/models/ticket.js.coffee
@@ -44,6 +44,9 @@
   iconTitle: (user) ->
     App.TicketState.find( @state_id ).displayName()

+  iconTextClass: (user) ->
+    "level-#{ @level() }"
+
   iconActivity: (user) ->
     if @owner_id == user.id
       return 'important'


Improved error handling.

Improved error handling.

--- app/assets/javascripts/app/models/ticket.js.coffee
+++ app/assets/javascripts/app/models/ticket.js.coffee
@@ -33,17 +33,29 @@

     # customer
     if data.customer_id
-      data.customer = App.User.find( data.customer_id )
+      if !App.User.exists( data.customer_id )
+        console.error("Can't find user for data.customer_id #{data.customer_id} for ticket #{data.id}")
+      else
+        data.customer = App.User.find( data.customer_id )

     # owner
     if data.owner_id
-      data.owner = App.User.find( data.owner_id )
+      if !App.User.exists( data.owner_id )
+        console.error("Can't find user for data.owner_id #{data.owner_id} for ticket #{data.id}")
+      else
+        data.owner = App.User.find( data.owner_id )

     # add created & updated
     if data.created_by_id
-      data.created_by = App.User.find( data.created_by_id )
+      if !App.User.exists( data.created_by_id )
+        console.error("Can't find user for data.created_by_id #{data.created_by_id} for ticket #{data.id}")
+      else
+        data.created_by = App.User.find( data.created_by_id )
     if data.updated_by_id
-      data.updated_by = App.User.find( data.updated_by_id )
+      if !App.User.exists( data.updated_by_id )
+        console.error("Can't find user for data.updated_by_id #{data.updated_by_id} for ticket #{data.id}")
+      else
+        data.updated_by = App.User.find( data.updated_by_id )

     data


:new: [message-registry-spec] Add a new spec about returning same objects onDidUpdateMessages

--- spec/message-registry-spec.coffee
+++ spec/message-registry-spec.coffee
@@ -72,6 +72,25 @@
         linterRegistry.lint({onChange: false, editorLinter}).then ->
           expect(wasUpdated).toBe(true)
           linterRegistry.deactivate()
+    it 'provides the same objects when they dont change', ->
+      wasUpdated = false
+      {linterRegistry, editorLinter} = getLinterRegistry()
+      linterRegistry.onDidUpdateMessages (linterInfo) ->
+        messageRegistry.set(linterInfo)
+        messageRegistry.updatePublic()
+      disposable = messageRegistry.onDidUpdateMessages ({added}) ->
+        expect(added.length).toBe(1)
+        obj = added[0]
+        disposable.dispose()
+        messageRegistry.onDidUpdateMessages ({messages}) ->
+          wasUpdated = true
+          expect(messages[0]).toBe(obj)
+      waitsForPromise ->
+        linterRegistry.lint({onChange: false, editorLinter}).then( ->
+          return linterRegistry.lint({onChange: false, editorLinter})
+        ).then ->
+          expect(wasUpdated).toBe(true)
+          linterRegistry.deactivate()

   describe '::deleteEditorMessages', ->
     it 'removes messages for that editor', ->


Remove the awkward _.pick

Remove the awkward _.pick

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -692,8 +692,8 @@

         buffer.setTextInRange([[7, 0], [8, 65]], '        one\n        two\n        three\n        four')

-        changeEvent = _.pick(changeHandler.mostRecentCall.args[0], 'start', 'end', 'delta')
-        expect(changeEvent).toEqual(start: 5, end: 8, delta: 2)
+        delete changeHandler.argsForCall[0][0].bufferChange
+        expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 8, delta: 2)

         expect(tokenizedBuffer.lineForScreenRow(5).indentLevel).toBe 4
         expect(tokenizedBuffer.lineForScreenRow(6).indentLevel).toBe 4
@@ -710,8 +710,8 @@

         buffer.setTextInRange([[7, 0], [8, 65]], '    ok')

-        changeEvent = _.pick(changeHandler.mostRecentCall.args[0], 'start', 'end', 'delta')
-        expect(changeEvent).toEqual(start: 5, end: 8, delta: -1)
+        delete changeHandler.argsForCall[0][0].bufferChange
+        expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 8, delta: -1)

         expect(tokenizedBuffer.lineForScreenRow(5).indentLevel).toBe 2
         expect(tokenizedBuffer.lineForScreenRow(6).indentLevel).toBe 2


geoip: location field is added to pistachio

--- servers/lib/crawler/staticpages/activity.coffee
+++ servers/lib/crawler/staticpages/activity.coffee
@@ -57,6 +57,12 @@
 getActivityContent = (activityContent)->
   slugWithDomain = "#{uri.address}/Activity/Post/#{activityContent.slug}"
   {body, nickname, fullName, hash, avatar, createdAt, commentCount, likeCount} = activityContent
+  location = activityContent?.payload?.location
+  if location
+    location = "from #{location}"
+  else
+    location = ""
+
   avatarImage   = createAvatarImage hash, avatar
   createdAt     = createCreationDate createdAt, activityContent.slug
   author        = createAuthor fullName, nickname
@@ -80,6 +86,7 @@

           #{fullName}
           #{createdAt}
+          #{location}

           #{body}


Changes on query manager

Changes on query manager

--- src/quo2.query.coffee
+++ src/quo2.query.coffee
@@ -36,12 +36,10 @@
     $$.fn.closest = (selector, context) ->
         node = @[0]
         candidates = $$(selector)
+
         node = null  unless candidates.length
-
-        # if (!candidates.length) node = null;
-        # while (node && candidates.indexOf(node) < 0) {
-        #     node = node !== context && node !== document && node.parentNode;
-        # }
+        while node and candidates.indexOf(node) > 0
+            node = node isnt context and node isnt document and node.parentNode

         $$ node


font rendering optimizations

font rendering optimizations

--- src/scene/font.coffee
+++ src/scene/font.coffee
@@ -23,7 +23,7 @@
       x = (@texture.x + (xx * @size))
       y = (@texture.y + (yy * @size))

-      @chars[String.fromCharCode(c)] = new Fz2D.Rect(x, y, @size, @size)
+      @chars[String.fromCharCode(c)] = new Fz2D.BBox(x, y, @size, @size)

     @invalid_char = @chars[String.fromCharCode(end)]

@@ -108,7 +108,7 @@

       char = @chars[c] || @invalid_char

-      ctx.draw(@texture, char.x, char.y, char.w, char.h, xx, yy, char.w, char.h)
+      ctx.draw(@texture, char.x, char.y, char.w, char.h, xx, yy, char.w, char.h, char.hw, char.hh, 0.0, 1.0)

       xx += char.w


Add descriptions to ActionWatcher spec.

Add descriptions to ActionWatcher spec.

--- src/Witness/Scripts/specs/ViewModels/ActionWatcher.coffee
+++ src/Witness/Scripts/specs/ViewModels/ActionWatcher.coffee
@@ -5,7 +5,7 @@

 describe "ActionWatcher with Action",
 {
-	"given ActionWatcher of an Action with a description": ->
+	"given an ActionWatcher of an Action with a description": ->
 		@action = new Witness.Action (->), [], "description"
 		@watcher = new Witness.ViewModels.ActionWatcher @action

@@ -15,34 +15,34 @@
 			description: should.be "description"
 },
 {
-	given: ->
+	"given an ActionWatcher where the action throws an error": ->
 		@action = new Witness.Action (-> throw new Error "action failed")
 		@watcher = new Witness.ViewModels.ActionWatcher @action

-	when: ->
+	"when it is run": ->
 		@action.run {}, (->), (->)

 	then:
 		watcher: should.haveStatus "failed"
 },
 {
-	given: ->
+	"given an ActionWatcher where the action passes": ->
 		@action = new Witness.Action (->)
 		@watcher = new Witness.ViewModels.ActionWatcher @action

-	when: ->
+	"when it is run": ->
 		@action.run {}, (->), (->)

 	then:
 		watcher: should.haveStatus "passed"
 },
 {
-	given: ->
+	"given an ActionWatcher that has been run": ->
 		@action = new Witness.Action (->)
 		@watcher = new Witness.ViewModels.ActionWatcher @action
 		@action.run {}, (->), (->)

-	when: ->
+	"when it is reset": ->
 		@watcher.reset()

 	then:
@@ -51,7 +51,7 @@

 describe "ActionWatcher with AsyncAction",
 {
-	given: ->
+	"given an ActionWatcher with an AsyncAction that calls done": ->
 		@action = new Witness.AsyncAction (-> @done()), [], "action-name"
 		@watcher = new Witness.ViewModels.ActionWatcher @action

@@ -61,34 +61,34 @@
 			description: should.be "action-name"
 },
 {
-	given: ->
+	"given an ActionWatcher with an AsyncAction that calls done": ->
 		@action = new Witness.AsyncAction (-> @done())
 		@watcher = new Witness.ViewModels.ActionWatcher @action

-	when: ->
+	"when it is run": ->
 		@action.run {}, (->), (->)

 	then:
 		watcher: should.haveStatus "passed"
 },
 {
-	given: ->
+	"given an ActionWatcher with an AsyncAction that fails": ->
 		@action = new Witness.AsyncAction (-> @fail())
 		@watcher = new Witness.ViewModels.ActionWatcher @action

-	when: ->
+	"when is is run": ->
 		@action.run {}, (->), (->)

 	then:
 		watcher: should.haveStatus "failed"
 },
 {
-	given: ->
+	"given an ActionWatcher with an AsyncAction and has been run": ->
 		@action = new Witness.AsyncAction (-> @done())
 		@watcher = new Witness.ViewModels.ActionWatcher @action
 		@action.run {}, (->), (->)

-	when: ->
+	"when it is reset": ->
 		@watcher.reset()

 	then:


Autocomplete: various improvements

Autocomplete: various improvements

- fixes where it doesnt fetch the same query again
- fixes showing dropdown on empty value

--- src/components/autocomplete/autocompletecontroller.coffee
+++ src/components/autocomplete/autocompletecontroller.coffee
@@ -341,22 +341,20 @@

   updateDropdownContents:->
     inputView = @getView()
-    value     = inputView.getValue()
-    @hideDropdown() if value is ''
-
-
-    if value isnt "" and @dropdownPrefix isnt value
-
-      @dropdownPrefix = value
-      @showFetching()
-
-      @fetch KD.utils.debounce 177, (data) =>
-
-        if data.length > 0
-          @refreshDropDown data
-          @showDropdown()
-        else
-          @showNoDataFound()
+    value     = inputView.getValue().trim()
+
+    return @hideDropdown() if value is ''
+
+    @dropdownPrefix = value
+    @showFetching()
+
+    @fetch KD.utils.debounce 177, (data) =>
+
+      if data.length > 0
+        @refreshDropDown data
+        @showDropdown()
+      else
+        @showNoDataFound()

   keyUpOnInputView:(event)->
     return if event.keyCode in [9,38,40] #tab


AutoComplete: do nothing on blur

AutoComplete: do nothing on blur

--- src/components/autocomplete/autocompletecontroller.coffee
+++ src/components/autocomplete/autocompletecontroller.coffee
@@ -33,9 +33,6 @@
     super options, data

     mainView.on 'focus', @bound 'updateDropdownContents'
-    mainView.on 'blur',  =>
-      @dropdown.removeAllItems()
-      @hideDropdown()

     @lastPrefix          = null
     @selectedItemData    = []


Prevent pager from reissuing a request (fix #54)

Also, make it more “async friendly” (re #55)

--- app/scripts/directives/pager.coffee
+++ app/scripts/directives/pager.coffee
@@ -12,10 +12,10 @@
     @watchScope()

   getOptions: ->
-    @options.noCount = @scope.nocount == true
+    @options.noCount = @scope.nocount
     @options.visiblePageCount = if @options.noCount then 1 else @scope.visiblepagecount || 10
     @options.pageSize = @scope.pagesize || 10
-    @options.pageCount = Math.ceil(@scope.pagecount / @options.pageSize)
+    @options.pageCount = if @scope.pagecount? then Math.ceil(@scope.pagecount / @options.pageSize) else null
     @options.currentPage = @scope.current || 1
     @options.onPageChange = @scope.pagechange

@@ -27,31 +27,42 @@
   bindEvents: ->
     @elements.pages.on 'click', 'a', (ev) =>
       link = $(ev.currentTarget)
-      page = link.attr('data-current-page')
-      @options.currentPage = parseInt(page, 10)
+      page = parseInt(link.attr('data-current-page'), 10)
+      return if page == @options.currentPage
+      @options.currentPage = page
       @options.onPageChange(@options.currentPage, @options.pageSize)
       @updatePager()

     @elements.previous.on 'click', (ev) =>
+      return if @options.currentPage == 1
       @options.currentPage -= @options.visiblePageCount
-      @options.currentPage = 1 if @options.currentPage <= 1
+      @options.currentPage = 1 if @options.currentPage < 2
       @options.onPageChange(@options.currentPage, @options.pageSize)
       @updatePager()

     @elements.next.on 'click', (ev) =>
+      return if @options.pageCount? and @options.currentPage >= @options.pageCount
       @options.currentPage += @options.visiblePageCount
-      @options.currentPage = @options.pageCount if @options.currentPage > @options.pageCount
+      @options.currentPage = @options.pageCount if @options.pageCount? and @options.currentPage > @options.pageCount
       @options.onPageChange(@options.currentPage, @options.pageSize)
       @updatePager()

   watchScope: ->
-    @scope.$watch('pagecount', =>
+    @scope.$watch('pagecount', (tits) =>
       @options.pageCount = Math.ceil(@scope.pagecount / @options.pageSize)
+      @getOptions()
+      @gatherElements()
       @updatePager()
     )
-    @scope.$watch('pagesize', =>
+    @scope.$watch('pagesize', (tits) =>
       @options.pageSize = @scope.pagesize || 10
-      @options.pageCount = Math.ceil(@scope.pagecount / @options.pageSize)
+      @getOptions()
+      @gatherElements()
+      @updatePager()
+    )
+    @scope.$watch('nocount', (tits) =>
+      @getOptions()
+      @gatherElements()
       @updatePager()
     )

@@ -73,18 +84,18 @@
       end = @options.currentPage + halfPages

     start = 1 if start < 0
-    end = @options.pageCount if end > @options.pageCount
+    if @options.pageCount? and end > @options.pageCount
+      end = @options.pageCount

     for i in [start..end]
-      kls = ''
-      kls = ' current' if i == @options.currentPage
+      kls = if i == @options.currentPage then ' current' else ''
       @elements.pages.append("
#{ i }
")

-    @elements.previous.addClass('hidden') if @options.currentPage <= 1
-    @elements.previous.removeClass('hidden') unless @options.currentPage <= 1
+    @elements.previous.addClass('hidden') if @options.currentPage < 2
+    @elements.previous.removeClass('hidden') unless @options.currentPage < 2

-    @elements.next.addClass('hidden') if @options.currentPage >= @options.pageCount
-    @elements.next.removeClass('hidden') unless @options.currentPage >= @options.pageCount
+    @elements.next.addClass('hidden') if @options.pageCount? and @options.currentPage >= @options.pageCount
+    @elements.next.removeClass('hidden') unless @options.pageCount? and @options.currentPage >= @options.pageCount

 angular.module('holmesApp')
   .directive('pager', () ->


Update horizon

Update horizon

--- app/scripts/levelblocks/City.coffee
+++ app/scripts/levelblocks/City.coffee
@@ -18,9 +18,9 @@
     #@add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Edge, Color').attr(w: @delta.x, h: height + 80).color('#000080'))
     #@add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Edge, Color').attr(w: @delta.x, h: height + 80).color('#000080'))

-    height = 45
+    height = 85
     @add(0, @level.visibleHeight - 10, Crafty.e('2D, Canvas, Edge, Color').attr(w: @delta.x, h: 90).color('#000080'))
-    @add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Color').attr(w: @delta.x, h: height + 10, z: -1).color('#000080'))
+    @add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Color').attr(w: @delta.x, h: height + 10, z: -3).color('#000080'))

     height = 25
     @add(0, @level.visibleHeight - 200, Crafty.e('2D, Canvas, Color').color('#202020').attr({ z: -1, w: shipLength, h: 300 }))
@@ -58,8 +58,8 @@
         @splash.attr(alpha: 1).tween(alpha: 0, w: 30, x: @splash.x - 10, h: 30, y: @splash.y - 30, 750)

-    @addBackground(-600, @level.visibleHeight - 105, Crafty.e('2D, Canvas, Color').color('#3030B0').attr({ z: -3, w: ((@delta.x + 600) * .5) + 1, h: 105 }), .5)
-    @addBackground(-300, @level.visibleHeight - 155, Crafty.e('2D, Canvas, Color').color('#6060E0').attr({ z: -4, w: ((@delta.x + 300) * .25) + 1, h: 155 }), .25)
+    @addBackground(-600, @level.visibleHeight - 125, Crafty.e('2D, Canvas, Color').color('#3030B0').attr({ z: -5, w: ((@delta.x + 600) * .5) + 1, h: 105 }), .5)
+    @addBackground(-300, @level.visibleHeight - 155, Crafty.e('2D, Canvas, Color').color('#6060E0').attr({ z: -6, w: ((@delta.x + 300) * .25) + 1, h: 155 }), .25)

     @addBackground(0, @level.visibleHeight + 40, Crafty.e('2D, Canvas, Color').color('#000040').attr({ z: 3, w: ((@delta.x + 300)) + 1, h: 185 }), 1.25)

@@ -138,11 +138,11 @@

   generate: ->
     super
-    height = 45
+    height = 85
     @add(0, @level.visibleHeight - 10, Crafty.e('2D, Canvas, Edge, Color').attr(w: @delta.x, h: 10).color('#000080'))
-    @add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Color').attr(w: @delta.x, h: height - 10, z: -1).color('#000080'))
-    @addBackground(0, @level.visibleHeight - 105, Crafty.e('2D, Canvas, Color').color('#3030B0').attr({ z: -3, w: (@delta.x * .5) + 1, h: 105 }), .5)
-    @addBackground(0, @level.visibleHeight - 155, Crafty.e('2D, Canvas, Color').color('#6060E0').attr({ z: -4, w: (@delta.x * .25) + 1, h: 155 }), .25)
+    @add(0, @level.visibleHeight - height, Crafty.e('2D, Canvas, Color').attr(w: @delta.x, h: height - 10, z: -3).color('#000080'))
+    @addBackground(0, @level.visibleHeight - 125, Crafty.e('2D, Canvas, Color').color('#3030B0').attr({ z: -5, w: (@delta.x * .5) + 1, h: 105 }), .5)
+    @addBackground(0, @level.visibleHeight - 155, Crafty.e('2D, Canvas, Color').color('#6060E0').attr({ z: -6, w: (@delta.x * .25) + 1, h: 155 }), .25)

     y = (Math.random() * 20) + 40
     @addBackground(200, y, Crafty.e('2D, Canvas, Color').color('#FFFFFF').attr({ z: -2, w: 100, h: 25 }), .5)


fixed issue where curl method double normalized angle on reset

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -527,7 +527,6 @@
   curl: (angle, options = {}) ->
     options = extendObj options, @_curlDefaults
     {anchor} = options
-    angle = @_normalizeAngle(angle) /  @_getPanelType anchor

     if anchor isnt @lastAnchor
       return @reset =>
@@ -537,6 +536,10 @@
         , 0

     @lastAngle = angle = @_normalizeAngle angle
+    angle /=  @_getPanelType anchor
+
+    if anchor is 'right' or anchor is 'bottom'
+      angle = -angle

     if anchor is 'left' or anchor is 'right'
       rotation = [0, 1, 0, angle]


check if shading is enabled before appending shader elements

--- oridomi.coffee
+++ oridomi.coffee
@@ -459,7 +459,8 @@

       mask = cloneEl maskProto, true, 'mask' + classSuffix
       mask.children[0].style[metric] = count * 100 + '%'
-      mask.appendChild shaderProtos[anchor] for anchor in anchorSet
+      if @_shading
+        mask.appendChild shaderProtos[anchor] for anchor in anchorSet

       proto = cloneEl panelProto, false, 'panel' + classSuffix
       proto.appendChild mask


update test

update test

--- test.coffee
+++ test.coffee
@@ -1,16 +1,16 @@
-#assert
-assert = require 'assert'
-#should
-should = require 'should'
-
 #$
 require './index'

 #test
-test = (obj, str) -> it str, -> obj.should.be.ok
+test = (bool, str) -> $.info (if bool then 'success' else 'error'), str

-#unit test script lines
-$.info 'info', 'unit test started'
+#describe
+describe = (title, fn) ->
+  #info
+  $.log ''
+  $.info 'info', title
+  #callback
+  fn?()

 #$
 describe '$', ->


add ghc 7.8.3 to supported versions list


Added whitespace option.

Added whitespace option.

--- src/tasks/dust.coffee
+++ src/tasks/dust.coffee
@@ -36,6 +36,7 @@
 			relative: no
 			wrapper: "amd"
 			optimizers: {}
+			whitespace: no
 			wrapperOptions:
 				packageName: ""
 				returning: "dust"
@@ -45,6 +46,9 @@

 		# override optimizers
 		optimizers.replace options.optimizers if options.optimizers?
+
+		# add whitespace parameter
+		dust.config.whitespace = options.whitespace

 		if options.amd
 			grunt.log.error """Notice: option "amd" is deprecated and will be removed in next version.""".yellow