Load feed with delay, so the fact shows much faster

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -62,9 +62,10 @@

   showFact: (slug, fact_id, params={})->
-    @showStream() unless FactlinkApp.mainRegion.currentView?
-
     fact = new Fact id: fact_id
     fact.fetch
-      success: ->
-        FactlinkApp.DiscussionModalOnFrontend.openDiscussion fact
+      success: -> FactlinkApp.DiscussionModalOnFrontend.openDiscussion fact
+
+    setTimeout ->
+      @showStream() unless FactlinkApp.mainRegion.currentView?
+    , 1000


Close leftTopRegion when navigating to stream.

Fixes weird navigation bug when coming from tour#choos-channels

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -61,6 +61,8 @@
       @restoreChannelView channel_id, => new ChannelView(model: channel)

   getChannelActivities: (username, channel_id) ->
+    FactlinkApp.leftTopRegion.close()
+
     FactlinkApp.mainRegion.show(@channel_views)

     @loadChannel username, channel_id, (channel) =>


Do not send server events for non-serializable attributes

--- bokehjs/src/coffee/common/client.coffee
+++ bokehjs/src/coffee/common/client.coffee
@@ -352,6 +352,15 @@
     if @_current_patch? and @_should_suppress_on_change(@_current_patch, event)
       return

+    # Filter out changes to attributes that aren't server-visible
+    if event instanceof ModelChangedEvent and event.attr not of event.model.serializable_attributes()
+      return
+
+    # This would happen for example if someone adds a non-serializable model as a root
+    if 'model' of event and not event.model.serializable_in_document()
+      logger.error("non-serializable model should not be in an event #{event}")
+      return
+
     patch = Message.create('PATCH-DOC', { sessid: @id },
       @document.create_json_patch([event]))
     @connection.send(patch)


Use -> arrows

Use -> arrows

--- src/config.coffee
+++ src/config.coffee
@@ -335,7 +335,7 @@
       deprecate "Config::observe no longer supports options. #{message}"

     callback(_.clone(@get(keyPath))) unless options.callNow == false
-    @emitter.on 'did-change', (event) =>
+    @emitter.on 'did-change', (event) ->
       callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0

   # Essential: Add a listener for changes to a given key path. If `keyPath` is
@@ -355,7 +355,7 @@
       callback = keyPath
       keyPath = undefined

-    @emitter.on 'did-change', (event) =>
+    @emitter.on 'did-change', (event) ->
       callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0)

   ###


Fix config.set scope selector example

Fix config.set scope selector example

--- src/config.coffee
+++ src/config.coffee
@@ -561,7 +561,7 @@
   # atom.config.get('editor.tabLength', scope: ['source.js']) # => 4
   #
   # # Set ruby to 2
-  # atom.config.set('editor.tabLength', 2, scopeSelector: 'source.ruby') # => true
+  # atom.config.set('editor.tabLength', 2, scopeSelector: '.source.ruby') # => true
   #
   # # Notice it's only set to 2 in the case of ruby
   # atom.config.get('editor.tabLength') # => 4


Remove unused conditions call

Remove unused conditions call

--- src/config.coffee
+++ src/config.coffee
@@ -83,7 +83,6 @@
     fn option, value, args...

   validate: ->
-    conditions = @conditions()
     for option, value of @options
       # Skip options that default to null
       if _.isNull(value) and @constructor.DEFAULTS[option] is null


fix test

fix test

--- test/main.coffee
+++ test/main.coffee
@@ -7,7 +7,6 @@
   _id: new ObjectID("4fa202c54cc3da640c000001")
   string: "test"
   regex: /test/
-  date: new Date('1/2/1978')
   int: 42
   float: 33.3333
   bool: true
@@ -25,7 +24,7 @@
     it 'should work', (done) ->
       str = ton.stringify doc
       should.exist str
-      str.should.equal '{"_id":new ObjectID("4fa202c54cc3da640c000001"),"string":"test","regex":/test/,"date":new Date("1978-01-02T07:00:00.000Z"),"int":42,"float":33.3333,"bool":true,"code":new Code("console.log(i);", {"i":1}),"long":new Long(20, 0),"binary":new Binary(new Buffer("test"), "0"),"minkey":new MinKey(),"maxkey":new MaxKey(),"double":new Double(100),"ref":new DBRef("space", new ObjectID("4fa202c54cc3da640c000002"), "stuff"),"arr":[1,2,3]}'
+      str.should.equal '{"_id":new ObjectID("4fa202c54cc3da640c000001"),"string":"test","regex":/test/,"int":42,"float":33.3333,"bool":true,"code":new Code("console.log(i);", {"i":1}),"long":new Long(20, 0),"binary":new Binary(new Buffer("test"), "0"),"minkey":new MinKey(),"maxkey":new MaxKey(),"double":new Double(100),"ref":new DBRef("space", new ObjectID("4fa202c54cc3da640c000002"), "stuff"),"arr":[1,2,3]}'
       done()

   describe 'parse()', ->


feat: resolve module in a better way

feat: resolve module in a better way

--- tasks/i18n.coffee
+++ tasks/i18n.coffee
@@ -3,11 +3,7 @@

 module.exports = (grunt) ->

-  try
-    grunt.loadNpmTasks 'grunt-contrib-jade'
-  catch ups
-    grunt.loadTasks path.join __dirname, '../', 'node_modules/grunt-contrib-jade/tasks'
-
+  grunt.loadTasks path.resolve require.resolve('grunt-contrib-jade'), '../', 'tasks'
   grunt.renameTask 'jade', 'contrib-jade'

   grunt.registerMultiTask 'jade', 'Compile Jade template with internalization support', ->


deleted unnecessary dependency

deleted unnecessary dependency

--- test/test.coffee
+++ test/test.coffee
@@ -1,6 +1,5 @@
 existsAssign = require '..'
 should       = require 'should'
-objectAssign = require 'object-assign'

 describe 'existential default ::', ->


Add some pending tests to implement

Add some pending tests to implement

--- test/test.coffee
+++ test/test.coffee
@@ -33,3 +33,7 @@
       actual = _.map albums.typeahead('ra', band: 'Queen'), (a) -> a.get('name')

       expected.should.eql actual
+
+  describe 'Common Error States', ->
+    it 'should handle an empty collection'
+    it 'should require the typeaheadAttributes member'


Fixed a test error

Fixed a test error

--- test/test.coffee
+++ test/test.coffee
@@ -87,24 +87,24 @@

   testStore : (test) ->
     test.expect 2
-    test.ok(@myBucket.where({fluff : "fluff"})[0], 2, "Should contain two fluffs after setup")
+    test.equal(@myBucket.where({fluff : "fluff"}).length, 2, "Should contain two fluffs after setup")
     @myBucket.set({fluff : "fluff", miff : "miff"})
     @myBucket.store =>
       anotherBucket = new bucket.Bucket("./test.db")
       anotherBucket.load ->
-        test.ok(anotherBucket.where({fluff : "fluff"})[0], 3, "Should contain three fluffs after store and reload")
+        test.equal(anotherBucket.where({fluff : "fluff"}).length, 3, "Should contain three fluffs after store and reload")
         test.done()

   testStoreImpatient : (test) ->
     test.expect 3
-    test.ok(@myBucket.where({fluff : "fluff"})[0], 2, "Should contain two fluffs after setup")
+    test.equal(@myBucket.where({fluff : "fluff"}).length, 2, "Should contain two fluffs after setup")
     @myBucket.set({fluff : "fluff", miff : "miff"})
     @myBucket.store =>
       anotherBucket = new bucket.Bucket("./test.db")
       anotherBucket.load ->
-        test.ok(anotherBucket.where({fluff : "fluff"})[0], 3, "Should contain three fluffs after store and reload")
+        test.equal(anotherBucket.where({fluff : "fluff"}).length, 3, "Should contain three fluffs after store and reload")
         test.done()
-    test.ok(@myBucket.where({fluff : "fluff"})[0], 3, "Should contain three fluffs after store and reload")
+    test.equal(@myBucket.where({fluff : "fluff"}).length, 3, "Should contain three fluffs after store")

   testReplace : (test) ->
@@ -166,10 +166,10 @@
     test.expect 8
     @myBucket.set({fluff: "fluff", piff: "piff"})
     @myBucket.store ->
-    test.equals(@myBucket.where({fluff: "fluff"}).length, 3, "Should be three fluff objects");
+    test.equal(@myBucket.where({fluff: "fluff"}).length, 3, "Should be three fluff objects");
     @myBucket.deleteById "456"
     @myBucket.store ->
-    test.equals(@myBucket.where({fluff: "fluff"}).length, 2, "Should be two fluff objects");
+    test.equal(@myBucket.where({fluff: "fluff"}).length, 2, "Should be two fluff objects");
     @myBucket.set({id: "456", fluff: "fluff"})
     @myBucket.store ->
     @myBucket.set({id: "456", fluff: "fluff", diff: "diff"})
@@ -180,11 +180,11 @@
     test.equal @myBucket.getById("456").diff, "diff2", "the new 456 should have the diff property with value diff2"
     @myBucket.set({id: "456", fluff: "fluff"})
     @myBucket.store ->
-    test.equals(@myBucket.where({fluff: "fluff"}).length, 4, "Should be three fluff objects");
+    test.equal(@myBucket.where({fluff: "fluff"}).length, 4, "Should be three fluff objects");
     test.ok !@myBucket.getById("456").diff?, "teh new 456 shouldn't have the diff property"
     anotherBucket = new bucket.Bucket("./test.db")
     anotherBucket.load =>
-      test.equals(anotherBucket.where({fluff: "fluff"}).length, 4, "Should be three fluff objects in the saved file");
+      test.equal(anotherBucket.where({fluff: "fluff"}).length, 4, "Should be three fluff objects in the saved file");
       test.equal @myBucket.getById("789").diff, "hoff", "the 789 should have the diff property with value hoff"
       test.ok !anotherBucket.getById("456").diff?, "the new 456 shouldn't have the diff property in the saved file"
       test.done()


LikeView: ShowMoreDataModalView tags are rendered

--- client/Main/CommonViews/LikeView.coffee
+++ client/Main/CommonViews/LikeView.coffee
@@ -26,7 +26,7 @@
           data.fetchLikedByes {},
             sort : timestamp : -1
           , (err, likes) =>
-            new ShowMoreDataModalView {title:"Members who liked #{data.body}"}, likes
+            new ShowMoreDataModalView {title:"Members who liked #{@utils.expandTokens data.body, data}"}, likes
       , data

     @likeLink = new ActivityActionLink


pricing: set toggle button value on plan switch

--- client/Pricing/views/productform.coffee
+++ client/Pricing/views/productform.coffee
@@ -12,7 +12,6 @@
     @toggle        = new KDMultipleChoice
       cssClass     : "pricing-toggle"
       labels       : ["DEVELOPER", "TEAM"]
-      defaultValue : ["DEVELOPER"]
       multiple     : no
       callback     : =>
         KD.singleton("router").handleRoute switch @toggle.getValue()
@@ -20,10 +19,12 @@
             when "TEAM" then "/Pricing/Team"

   showDeveloperPlan: ->
+    @toggle.setValue "DEVELOPER"
     @developerPlan.show()
     @teamPlan.hide()

   showTeamPlan: ->
+    @toggle.setValue "TEAM"
     @developerPlan.hide()
     @teamPlan.show()


activity: cleanup main route handler file

--- client/activity/lib/routehandler.coffee
+++ client/activity/lib/routehandler.coffee
@@ -1,71 +1,10 @@
-kd = require 'kd'
-registerRoutes = require 'app/util/registerRoutes'
 lazyrouter = require 'app/lazyrouter'

-activityPane = (callback) ->
-  {appManager} = kd.singletons
-  appManager.open 'Activity', (app) ->
-    view = app.getView()
-
-    kd.singleton('mainController').ready ->
-      view.open 'topic', 'public'
-      callback view.tabs.getPaneByName 'topic-public'
-
-handleChannel = (type, slug, callback) ->
-  callback    ?= (app) -> app.getView().open type, slug
-  {appManager, mainController} = kd.singletons
-  mainController.ready -> appManager.open 'Activity', callback
+handlers = require './routehandlers'

 module.exports = -> lazyrouter.bind 'activity', (type, info, state, path, ctx) ->

-  switch type
-    when "PublicFeed"
-      {params: {name}} = info
-      ctx.handleRoute "/#{if name then name else ''}/Activity/Public/Liked",
-        replaceState: yes
-        shouldPushState: no
-    when "PublicFeedMostLiked"
-      {params: {name}} = info
-      activityPane (pane) -> pane.open 'Most Liked'
-    when "PublicFeedMostRecent"
-      {params: {name}} = info
-      activityPane (pane) -> pane.open 'Most Recent'
-    when "PublicFeedSearch"
-      { params: {name}, query} = info
-      activityPane (pane) -> pane.open 'Search', query.q
-    when "AnnouncementFeed"
-      # TODO ~ i tried to unify this route and following 4 routes, couldnt manage
-      # to make it work, did not spend so much time on it and gave up, it would be
-      # better to have only one route for those 5
-      {params: {name, slug}} = info
-      handleChannel 'announcement', slug
-    when "TopicFeed"
-      {params:{name, slug}, query} = info
-      if slug is 'public'
-      then kd.singletons.router.handleRoute '/Activity/Public'
-      else handleChannel 'topic', slug
-    when "VerbosePost"
-      {params:{name, slug}, query} = info
-      handleChannel 'post', slug
-    when "PrivateMessage"
-      {params:{name, slug}, query} = info
-      handleChannel 'message', slug
-    when "Post"
-      {params:{name, slug}, query} = info
-      handleChannel 'post', slug
-    when "NewPrivateMessage"
-      handleChannel null, null, (app) -> app.getView().showNewMessageForm()
-    when "AllTopics"
-      handleChannel null, null, (app) -> app.getView().showAllTopicsModal()
-    when "FollowingTopics"
-      handleChannel null, null, (app) -> app.getView().showFollowingTopicsModal()
-    when "AllPrivateMessages"
-      handleChannel null, null, (app) -> app.getView().showAllChatsModal()
-    when "ActivityGeneric"
-      {params:{name, slug}, query} = info
-      # handle legacy topic routes
-      if query.tagged?
-        kd.getSingleton('router').handleRoute "/Activity/Topic/#{query.tagged}"
-      else
-        {router} = kd.singletons
-        router.handleRoute '/Activity/Public'
+  handle = (name) -> handlers["handle#{name}"](info, ctx, path, state)
+
+  handle type
+


follow linkgroup bug fixed

follow linkgroup bug fixed

--- client/app/CommonViews/LinkViews.coffee
+++ client/app/CommonViews/LinkViews.coffee
@@ -149,15 +149,15 @@
       click       : =>
         new FollowedModalView {group}, @getData()

-    sep = ''
+    sep = ' '
     if participants[0] instanceof bongo.api.JAccount
       sep = ', '
     switch totalCount
       when 0 then ""
       when 1 then "{{> @participant0}}"
       when 2 then "{{> @participant0}} and {{> @participant1}}"
-      when 3 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}}"
-      when 4 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}}#{sep}{{> @participant3}"
+      when 3 then "{{> @participant0}}#{sep}{{> @participant1}} and {{> @participant2}}"
+      when 4 then "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}} and {{> @participant3}}"
       else "{{> @participant0}}#{sep}{{> @participant1}}#{sep}{{> @participant2}} and {{> @more}}"

   render:->


meego mobile detection

meego mobile detection

--- src/device.coffee
+++ src/device.coffee
@@ -70,9 +70,12 @@

 device.fxosTablet = ->
   device.fxos() and _find 'tablet'
+
+device.meego = ->
+  _find 'meego'

 device.mobile = ->
-  device.androidPhone() or device.iphone() or device.ipod() or device.windowsPhone() or device.blackberryPhone() or device.fxosPhone()
+  device.androidPhone() or device.iphone() or device.ipod() or device.windowsPhone() or device.blackberryPhone() or device.fxosPhone() or device.meego()

 device.tablet = ->
   device.ipad() or device.androidTablet() or device.blackberryTablet() or device.windowsTablet() or device.fxosTablet()
@@ -150,6 +153,9 @@
   else
     _addClass "fxos mobile"

+else if device.meego()
+  _addClass "meego mobile"
+
 else
   _addClass "desktop"


direct: Fix noisy console.log

direct: Fix noisy console.log

--- src/direct.coffee
+++ src/direct.coffee
@@ -23,7 +23,6 @@

   _assertBroker: (callback) ->
     err = new Error "no broker connected #{@address}" if not @broker
-    console.log err, @address, @broker?
     return callback err if err

   ## Manipulating queues


Don't cache entire line length on lines > 160 chars

--- src/editor.coffee
+++ src/editor.coffee
@@ -1591,7 +1591,11 @@
       for char, i in content

         # Dont return right away, finish caching the whole line
-        returnLeft = left if index == column
+        if index == column
+          returnLeft = left
+
+          # Don't cache lines longer than 100 chars
+          return returnLeft if lineElement.innerText.length > 160
         oldLeft = left

         scopes = @scopesForColumn(tokenizedLine, index)


revert max_line_length

revert max_line_length

--- client/config.coffee
+++ client/config.coffee
@@ -5,8 +5,6 @@
       indentation:
         level: "error"
         value: 4
-      max_line_length:
-        value: 82

   # Edit the next line to change default build path.
   paths:


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -93,11 +93,11 @@
       objects: []
     }, defaults

-    intervalId = null
     frameAdvance = false

     queuedObjects = []

+    running = false
     startTime = +new Date()
     lastStepTime = -Infinity
     animLoop = (timestamp) ->


debugging

debugging

--- src/extend.coffee
+++ src/extend.coffee
@@ -28,9 +28,17 @@

   # setup specialized route handlers
   for verb in ['all', 'get', 'post', 'put', 'del']
+    # SNTX TODO delete console.logs
+    # SNTX STOP TODO get previous commit and console log req, res, etc..
+    console.log verb
     patch verb, (original) ->
       (path, handler) ->
         original path, (req, res, next) ->
+          console.log original.toString()
+          console.log path
+          #console.log req.name
+          #console.log res.name
+          #console.log next.name
           ctx =
             app: app
             body: req.body
@@ -50,10 +58,12 @@
   # Shortcut to add routes
   patch 'addRoutes', ->
     (routes) ->
+      #console.log "SNTX: inside addRoutes"
       if not Array.isArray routes
         routes = [routes]

       for route in routes
+        #console.log route.toString()
         extend app, route

   # Expose middleware


filter.remove*

filter.remove*

--- src/filter.coffee
+++ src/filter.coffee
@@ -43,6 +43,18 @@
         null  ## remove
   filter.remapField fold, field, old2new
   old2new
+
+filter.remove = (fold, field, index) ->
+  ###
+  Remove given index from given field ('vertices', 'edges', 'faces'), in place.
+  ###
+  filter.remapFieldSubset fold, field,
+    for i in [0...filter.numType fold, field]
+      i != index
+
+filter.removeVertex = (fold, index) -> filter.remove fold, 'vertices', index
+filter.removeEdge = (fold, index) -> filter.remove fold, 'edges', index
+filter.removeFace = (fold, index) -> filter.remove fold, 'faces', index

 filter.transform = (fold, matrix) ->
   ###


add class to form in linkimg dialog

add class to form in linkimg dialog

--- plugins/linkimg.coffee
+++ plugins/linkimg.coffee
@@ -20,7 +20,7 @@
             widget = this

             dialogId = "#{@options.uuid}-dialog"
-            dialog = jQuery "
<form action=\"#\" method=\"post\"><input class=\"url\" type=\"text\" name=\"url\" size=\"40\" value=\"http://\" /><input type=\"submit\" value=\"Insert\" /></form>
"
+            dialog = jQuery "
<form action=\"#\" method=\"post\" class=\"linkForm\"><input class=\"url\" type=\"text\" name=\"url\" size=\"40\" value=\"http://\" /><input type=\"submit\" value=\"Insert\" /></form>
"
             dialogSubmitCb = () ->
                 link = $(this).find(".url").val()
                 widget.options.editable.restoreSelection(widget.lastSelection)


Comment about overlay and ESC - we should really pull from bergie

--- plugins/overlay.coffee
+++ plugins/overlay.coffee
@@ -49,7 +49,7 @@
                     if widget.options.visible
                         widget.updateOverlay()

-                # abort editing when pressing ESCAPE
+                # abort editing when pressing ESCAPE --- This should be covered in hallo core, it's just not working yet
                 widget.options.editable.element.keydown (event, data) ->
                     if event.keyCode == 27
                         widget.options.editable.restoreOriginalContent()


Visual/edit modes: fine-tune entry/exit logic.

- When the user exits visual mode via escape, we reinstall the original
  selection (from launch).

--- content_scripts/mode_visual_edit.coffee
+++ content_scripts/mode_visual_edit.coffee
@@ -9,7 +9,7 @@
 # SuppressPrintable and CountPrefix are shared utility base classes.
 # Movement is a shared vim-like movement base class.
 #
-# The inheritance hierarchy is:
+# The class inheritance hierarchy is:
 # - Mode, SuppressPrintable, CountPrefix, Movement, [ VisualMode | CaretMode | EditMode ]
 # - Mode, SuppressPrintable, CountPrefix, Movement, VisualMode, VisualLineMode
 #
@@ -111,7 +111,7 @@
     if @options.parentMode
       @options.parentMode.launchSubMode mode, options
     else
-      new mode options
+      new mode extend options, initialRangeOnLaunch: @initialRangeOnLaunch

   # Return the character following (to the right of) the focus, and leave the selection unchanged.  Returns
   # undefined if there is no such character.
@@ -309,6 +309,11 @@
       @runMovementKeyChar @options.immediateMovement, @getCountPrefix()
       return

+    # Track the initial selection range.  We'll restore it if the user exits with Escape.
+    unless @options.parentMode
+      @initialRangeOnLaunch = options.initialRangeOnLaunch ||
+        (if @selection.type == "None" then "None" else @selection.getRangeAt 0)
+
     # This is the main keyboard-event handler for movements and commands.
     @push
       _name: "#{@id}/keypress"
@@ -348,7 +353,7 @@
             for [0...count]
               unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false
                 @setSelectionRange initialRange
-                HUD.showForDuration "No matches.", 1500
+                HUD.showForDuration("No matches for '" + query + "'", 1000)
                 return
             # The find was successfull. If we're in caret mode, then we should now have a selection, so we can
             # drop back into visual mode.
@@ -358,9 +363,21 @@
         @movements.N = (count) -> executeFind count, true
         @movements["/"] = ->
           @findMode = enterFindMode()
-          @findMode.onExit => new VisualMode
+          @findMode.onExit => @changeMode VisualMode
     #
     # End of Movement constructor.
+
+  exit: (event, target) ->
+    super event, target
+
+    unless @options.parentMode
+      # When the user exits via Escape, we reinstall the pre-launch selection (or, if there was none, just
+      # collapse the selection to the current anchor).
+      if @initialRangeOnLaunch? and event?.type == "keydown" and KeyboardUtils.isEscape event
+        if @initialRangeOnLaunch == "None"
+          @collapseSelectionToAnchor()
+        else
+          @setSelectionRange @initialRangeOnLaunch unless @initialRangeOnLaunch == "None"

   # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and
   # returns it.


close on error

close on error

--- lib/build/build-manager.coffee
+++ lib/build/build-manager.coffee
@@ -35,6 +35,7 @@
       buildStateView.setTask(task)

     .catch (err) ->
+      buildStateView.destroy()
       console.trace err.stack
       alert "err occur! #{err}"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/clamp.coffee
+++ test/clamp.coffee
@@ -13,6 +13,11 @@

   obj.incude Clamp

-  obj.clamp()
+  obj.clamp
+    x:
+      min: 0
+      max: 100
+
+

 module()


Add unit test for skipping inherited color properties

--- test/color.coffee
+++ test/color.coffee
@@ -30,4 +30,10 @@
       foreground: 'hello'
       background: 'world'

-    result.should.equal 'test'+    result.should.equal 'test'
+
+  it 'should only use its own keys', () ->
+    result = Impromptu.color 'test',
+      foreground: 'constructor'
+
+    result.should.equal 'test'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -85,6 +85,6 @@

 test "Color should parse arrays", ->
   color = Color([1, 24, 101])
-  equals color.r(), 196
-  equals color.g(), 86
-  equals color.b(), 85
+  equals color.r(), 1
+  equals color.g(), 24
+  equals color.b(), 101


testing commit messages

testing commit messages

--- test/mouse.coffee
+++ test/mouse.coffee
@@ -1,4 +1,4 @@
 test "Mouse", ->
-  #TODO
+  #TODO Real tests
   ok(true)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/point.coffee
+++ test/point.coffee
@@ -143,7 +143,7 @@
     p = Point(10, 10)
     p2 = p.clamp(5)

-    e
+    equals

   test ".centroid", ->
     centroid = Point.centroid(


allow home to use websockets

allow home to use websockets

--- proxy.coffee
+++ proxy.coffee
@@ -149,13 +149,16 @@
     # and might break with a future version of express
     enableSocketRedirection: =>
         @server.on 'upgrade', (req, socket, head) =>
-            slug = @app._router.matchRequest(req).params.name
-            req.url = req.url.replace "/apps/#{slug}", ''
-            if @routes[slug]?
+            if slug = @app._router.matchRequest(req).params.name
+                req.url = req.url.replace "/apps/#{slug}", ''
+                port = @routes[slug]
+            else
+                port = @defaultPort
+
+            if port
                 @proxy.proxyWebSocketRequest req, socket, head,
                     host: 'localhost',
-                    port: @routes[slug]
-                    #@FIXME after merge with feature/autostart
+                    port: port
             else
                 socket.end "HTTP/1.1 404 NOT FOUND \r\n" +
                            "Connection: close\r\n", 'ascii'
@@ -271,7 +274,6 @@
                 console.log err
                 @sendError res, "Server error occured.", 500
             else if user is undefined or not user
-                console.log err if err
                 @sendError res, "Wrong password", 400
             else
                 passwordKeys.initializeKeys req.body.password, (err) =>


Add runtime protocol to supported list

Add runtime protocol to supported list

--- src/protocol.coffee
+++ src/protocol.coffee
@@ -53,6 +53,7 @@
       type: 'msgflo'
       version: '0.4'
       capabilities: [
+        'protocol:runtime'
         'protocol:component'
         'protocol:graph'
         'protocol:network'


Add some special version variations to test setup

--- test/suite.coffee
+++ test/suite.coffee
@@ -23,12 +23,12 @@
     },{
       maxHeight: 780
       maxWidth: 780
-      aspect: '4:3'
+      aspect: '3:2!h'
       suffix: '-medium'
     },{
       maxHeight: 320
       maxWidth: 320
-      aspect: '4:3'
+      aspect: '16:9!h'
       suffix: '-small'
     },{
       maxHeight: 100


Added watch_dep

Added watch_dep

--- test/suite.coffee
+++ test/suite.coffee
@@ -1,7 +1,7 @@
 Queue = require 'backbone-orm/lib/queue'

 option_sets = require('backbone-orm/test/option_sets')
-option_sets = option_sets.slice(0, 5)
+option_sets = option_sets.slice(0, 1)

 test_queue = new Queue(1)
 for options in option_sets
@@ -9,6 +9,6 @@
     console.log "\nBackbone REST: Running tests with options:\n", options
     queue = new Queue(1)
     queue.defer (callback) -> require('./unit/backbone_orm')(options, callback)
-    # queue.defer (callback) -> require('./unit/backbone_rest')(options, callback)
+    queue.defer (callback) -> require('./unit/backbone_rest')(options, callback)
     queue.await callback
 test_queue.await (err) -> console.log "Backbone REST: Completed tests"


rewrite point code

rewrite point code

points are now either rescaling spheres or point clouds

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -418,8 +418,7 @@
         @light.position.set 0, d, 0

     updateBoundingBox: (obj) ->
-        if not obj.geometry.boundingBox?
-            obj.geometry.computeBoundingBox()
+        obj.geometry.computeBoundingBox()
         if @boundingBox?
             @boundingBox.geometry.boundingBox.union obj.geometry.boundingBox
         else
@@ -515,93 +514,81 @@

     addSphere: (opts) ->
         opts = defaults opts,
-            loc         : [0,0,0]
-            radius      : 5
-            texture     : required
-            in_frame    : true
+            loc             : [0,0,0]
+            radius          : 5
+            texture         : required
+            in_frame        : true
+            _basic_material : false

         geometry = new THREE.SphereGeometry opts.radius, 32, 32

-        material = new THREE.MeshPhongMaterial
-            transparent : opts.texture.opacity < 1
-            side        : THREE.DoubleSide
-
-        material.color.setRGB       opts.texture.color...
-        material.ambient.setRGB     opts.texture.ambient...
-        material.specular.setRGB    opts.texture.specular...
-        material.opacity          = opts.texture.opacity
-
-        mesh = new THREE.Mesh geometry, material
+        if opts._basic_material
+            material = new THREE.MeshBasicMaterial
+                transparent : opts.texture.opacity < 1
+
+            material.color.setRGB       opts.texture.color...
+
+        else
+            material = new THREE.MeshPhongMaterial
+                transparent : opts.texture.opacity < 1
+                side        : THREE.DoubleSide
+
+            material.color.setRGB       opts.texture.color...
+            material.ambient.setRGB     opts.texture.ambient...
+            material.specular.setRGB    opts.texture.specular...
+            material.opacity          = opts.texture.opacity
+
+        sphere = new THREE.Mesh geometry, material
+        sphere.position.set opts.loc...
+
         if opts.in_frame
-            @updateBoundingBox mesh
-        @scene.add mesh
-        return mesh
+            @updateBoundingBox sphere
+        @scene.add sphere
+        return sphere

     addPoint: (opts) ->
         opts = defaults opts,
             loc         : [0,0,0]
             size        : 5
             texture     : required
+            use_cloud   : false
             in_frame    : true

-        if not @_points?
-            @_points = []
-
-        # IMPORTANT: Below we use sprites instead of the more natural/faster PointCloudMaterial.
-        # Why?  Because usually people don't plot a huge number of points, and PointCloudMaterial is SQUARE.
-        # By using sprites, our points are round, which is something people really care about.
-
-        switch @opts.renderer
-
-            when 'webgl'
-                width         = 50
-                height        = 50
-                canvas        = document.createElement 'canvas'
-                canvas.width  = width
-                canvas.height = height
-
-                context       = canvas.getContext '2d'  # get the drawing context
-                centerX       = width/2
-                centerY       = height/2
-                radius        = 25
-
-                context.beginPath()
-                context.arc centerX, centerY, radius, 0, 2*Math.PI, false
-                context.fillStyle = rgb_to_hex opts.texture.color
-                context.fill()
-
-                texture = new THREE.Texture canvas
-                texture.needsUpdate = true
-                spriteMaterial = new THREE.SpriteMaterial
-                    map     : texture
-                particle = new THREE.Sprite spriteMaterial
-
-                position = @aspect_ratio_scale opts.loc...
-                particle.position.set position...
-                @_points.push [particle, opts.size/200]
-
-            when 'canvas'
-                # inspired by http://mrdoob.github.io/three.js/examples/canvas_particles_random.html
-                PI2 = Math.PI * 2
-                program = (context) ->
-                    context.beginPath()
-                    context.arc 0, 0, 0.5, 0, PI2, true
-                    context.fill()
-                material = new THREE.SpriteCanvasMaterial
-                    color   : new THREE.Color opts.texture.color
-                    program : program
-                particle = new THREE.Sprite texture
-                position = @aspect_ratio_scale opts.loc...
-                particle.position.set position...
-                @_points.push [particle, 4*opts.size/@opts.width]
-
-            else
-                throw "bug -- unkown dynamic renderer type = #{@opts.renderer}"
-
-        if opts.in_frame
-            @updateBoundingBox particle
-        @scene.add particle
-        return particle
+        if opts.use_cloud
+            if not @_cloud
+                @_cloud = {}
+
+            key = opts.size+opts.texture.color+opts.in_frame
+
+            if not (cloud = @_cloud[key])?
+                material = new THREE.PointCloudMaterial
+                    size            : opts.size*2
+                    sizeAttenuation : false
+                material.color.setRGB opts.texture.color...
+                cloud = (@_cloud[key] = new THREE.PointCloud(
+                                            new THREE.Geometry(), material))
+                @scene.add cloud
+
+            cloud.geometry.vertices.push @vector(opts.loc...)
+
+            if opts.in_frame
+                @updateBoundingBox cloud
+            return cloud
+        else
+            if not @_points?
+                @_points = []
+
+            opts.radius = opts.size/400
+            opts._basic_material = true
+
+            delete opts.size
+            delete opts.use_cloud
+
+            point = @addSphere opts
+
+            @_points.push point
+
+            return point

     addIndexFaceSet: (opts) ->
         opts = defaults opts,
@@ -872,26 +859,25 @@
         if not new_pos and not force
             return

-        # rescale all text in scene
-        @rescale_objects()
+        # rescale all objects in scene
+        @rescaleObjects()

         @renderer.render @scene, @camera

-    _rescale_factor: ->
+    _rescaleFactor: ->
         if not @_center?
             return undefined
         else
             return @camera.position.distanceTo(@_center) / 3

-    rescale_objects: (force = false) ->
-        s = @_rescale_factor()
-        if not s? or (Math.abs(@_last_scale - s) < 0.000001 and not force)
-            return
-        @_last_scale = s
+    rescaleObjects: (force = false) ->
+        scale = @_rescaleFactor()
+        if not scale? or (Math.abs(@_lastScale - scale) < 0.000001 and not force)
+            return
         if @_points?
             for z in @_points
-                c = z[1]
-                z[0].scale.set s*c, s*c, s*c
+                z.scale.set scale, scale, scale
+        @_lastScale = scale

 math3d.render_3d_scene = (opts) ->
     opts = defaults opts,


Load biom files via biojs module (convert if required)

--- src/readFile.coffee
+++ src/readFile.coffee
@@ -1,6 +1,7 @@
 class readFile
 	reader = new FileReader()
 	progress = document.querySelector('.percent')
+	Biom = require('biojs-io-biom').Biom;

 	constructor: () ->
 		db.open(
@@ -89,23 +90,28 @@

 	# store new biom file to the browser indexeddb
 	readBlob: (file) =>
-		reader.onloadend = (evt) =>
+		reader.onloadend = (evt) =>
 			if evt.target.readyState == FileReader.DONE
 				# JSON.parse(reader.result)
-				biomToStore = {}
-				biomToStore.name = file.name
-				biomToStore.size = file.size
-				biomToStore.data = evt.target.result
-				d = new Date();
-				biomToStore.date = d.getUTCFullYear() + "-" + (d.getUTCMonth() + 1) + "-" + d.getUTCDate() + "T" + d.getUTCHours() + ":" + d.getUTCMinutes() + ":" + d.getUTCSeconds() + " UTC"
-				console.log @
-				if JSON.parse(biomToStore.data).format.indexOf("Biological Observation Matrix") != -1
-					@server.biom.add(biomToStore).done (item) =>
-						@currentData = item
-						setTimeout( "window.location.href = 'preview.html'", 2000)
-				else
-					alert "Incorrect biom format field! Please check your file content!"
-		reader.readAsBinaryString(file)
+				Biom.parse('', {conversionServer: 'convert.php', arrayBuffer: evt.target.result}).then(
+					(biom) =>
+						biomToStore = {}
+						biomToStore.name = file.name
+						biomToStore.size = file.size
+						biom.write().then(
+							(biomData) =>
+								biomToStore.data = biomData
+								d = new Date();
+								biomToStore.date = d.getUTCFullYear() + "-" + (d.getUTCMonth() + 1) + "-" + d.getUTCDate() + "T" + d.getUTCHours() + ":" + d.getUTCMinutes() + ":" + d.getUTCSeconds() + " UTC"
+								console.log @
+								@server.biom.add(biomToStore).done (item) =>
+									@currentData = item
+									setTimeout( "window.location.href = 'preview.html'", 2000)
+						)
+					(fail) =>
+						alert "Error loading biom file!"
+				)
+		reader.readAsArrayBuffer(file)

 	# list 10 most recent files uploaded to this browser
 	listRecentFiles: () =>


Add cron job for monitoring services

Add cron job for monitoring services

--- scripts/monitors.coffee
+++ scripts/monitors.coffee
@@ -34,6 +34,7 @@
       statusCode: null
       downedAt: null
       checkedAt: null
+      alertedAt: null
     }

     @adjustUrl: (url) ->
@@ -41,6 +42,37 @@
       unless /^https?:\/\//i.test url
         url = "http://" + _s.ltrim url, "/"
       url
+
+    @addSchedule: (url) ->
+      job = {
+        cronTime: "*/1 * * * *"
+        onTick: ->
+          monitor = new Monitor url
+          monitor.run (err) ->
+            if err
+              if monitor.data.alertedAt
+                alerted_at = moment monitor.data.alertedAt
+                if alerted_at.add(1, "h").isAfter moment()
+                  return
+
+              monitor.data.alertedAt = moment().format()
+              monitor.save()
+              if monitor.data.status is "missing"
+                robot.send {room: room},
+                  "#{monitor.url} へアクセスできません！" +
+                  "至急確認してください @channel\n" +
+                  monitor.toString()
+              else
+                robot.send {room: room},
+                  "#{monitor.url} で問題が発生しています！" +
+                  "至急確認してください @channel\n" +
+                  monitor.toString()
+        start: true
+      }
+      try
+        new CronJob job
+      catch error
+        console.log error

     constructor: (url) ->
       @url = Monitor.adjustUrl url
@@ -79,11 +111,14 @@
           callback? true

     toString: () ->
-      buffer = "#{@url}: *#{@data.status}* (";
+      buffer = "#{@url} : *#{@data.status}* (";
       buffer += "status code: #{@data.statusCode}, " if @data.statusCode
       buffer += "updated at: #{@data.checkedAt}"
       buffer += ", downed at: #{@data.downedAt} " if @data.downedAt
       buffer += ")"
+
+  for url, monitor of robot.brain.data.monitors
+    Monitor.addSchedule url

   robot.respond /(.+)を監視/i, (msg) ->
     url = Monitor.adjustUrl msg.match[1]
@@ -94,14 +129,18 @@
     monitor.save()
     monitor.run (err) ->
       if err
-        if monitor.data.status == "missing"
+        if monitor.data.status is "missing"
           msg.send "#{monitor.url} へアクセスできません。監視を解除します"
           monitor.delete()
+          return
         else
           msg.send "#{monitor.url} でエラーが起きています" +
             "（ステータス：#{monitor.data.statusCode}）"
       else
         msg.send "#{monitor.url} は正常に動作しています。"
+
+      #register cron job
+      Monitor.addSchedule monitor.url

   robot.respond /.*稼働状況.*/i, (msg) ->
     #getMonitor


more tweaks

more tweaks

--- scripts/snomedct.coffee
+++ scripts/snomedct.coffee
@@ -56,4 +56,4 @@
     .get() (err, response, body) ->

       data = JSON.parse body
-      res.send "For concept #{concept}, there are #{data.details.total} results and the first one has an SCTID of #{data.matches[0].conceptId} and the FSN, #{data.matches[0].fsn} with a URL/URI of http://snomed.info/id/#{data.matches[0].conceptId}"
+      res.send "For concept #{concept}, there are #{data.details.total} matching descriptions and the first one has an SCTID of #{data.matches[0].conceptId} and the FSN, #{data.matches[0].fsn} with a URL/URI of http://snomed.info/id/#{data.matches[0].conceptId}"


When traversing an array that we modify (like splice) during the loop, we should traverse it backwards

--- src/parser.coffee
+++ src/parser.coffee
@@ -62,7 +62,9 @@
                     response = null
                 cb(null, response)

-            for ad in response.ads
+            loopIndex = response.ads.length
+            while loopIndex--
+                ad = response.ads[loopIndex]
                 continue unless ad.nextWrapperURL?
                 do (ad) =>
                     if parentURLs.length >= 10 or ad.nextWrapperURL in parentURLs


Minor Player refactoring and documentation.

--- src/player.coffee
+++ src/player.coffee
@@ -1,7 +1,9 @@
 # Player
 # ======
 # Player configures and controls a sprite, tracks and updates state given user
-# interaction.
+# interaction. This builds off the stock Flixel / Phaser player, but tries to
+# make the movement more natural, which requires more animations, logic, and
+# physics.

 define [
   'phaser'
@@ -47,28 +49,7 @@
       @nextAction = 'none'
       @nextState = 'running'

-      # Horizontal
-
-      # - Revert to still. (Our acceleration updates funny.)
-      @acceleration.x = 0 unless @isInMidAir()
-
-      ###
-      - Basically handle switching direction, and running or being still
-        when not in the air. Note the player still runs in midair, but run
-        will behave differently.
-      ###
-
-      if @cursors.left.isDown
-        @_turn Direction.Left
-        @_run Direction.Left
-
-      else if @cursors.right.isDown
-        @_turn Direction.Right
-        @_run Direction.Right
-
-      else unless @isInMidAir()
-        if @velocity.x isnt 0 then @nextAction = 'stop'
-        else @nextState = 'still'
+      @_updateXMovement()

       @_updateAnimations()
       @_changeState @nextState
@@ -146,6 +127,27 @@
         when 'running' then @_changeAnimation 'run', no
         when 'still' then @_changeAnimation 17, no

+    _updateXMovement: ->
+      ###
+      Basically handle switching direction, and running or being still
+      when not in the air. Note the player still runs in midair, but run
+      will behave differently.
+      ###
+      # Revert to still. (Our acceleration updates funny.)
+      @acceleration.x = 0 unless @isInMidAir()
+
+      if @cursors.left.isDown
+        @_turn Direction.Left
+        @_run Direction.Left
+
+      else if @cursors.right.isDown
+        @_turn Direction.Right
+        @_run Direction.Right
+
+      else unless @isInMidAir()
+        if @velocity.x isnt 0 then @nextAction = 'stop'
+        else @nextState = 'still'
+
     _xOffset: (direction = @direction) -> direction * 10

   Player


Syntax fix.

Syntax fix.

--- src/pubsub.coffee
+++ src/pubsub.coffee
@@ -334,7 +334,7 @@
             else
               response = validation.value

-              else if response.seq?
+              if response.seq?
                 {resolve, reject} = @outstanding.get message.seq

                 if response.code == 200


stop observers if publication is stopped

--- server-transform.coffee
+++ server-transform.coffee
@@ -21,7 +21,7 @@
       return doc
     computation = null

-    cursor.observe
+    handle = cursor.observe
       added: (doc) ->
         publication.added collectionName, doc._id, transform(doc)

@@ -38,3 +38,7 @@

       removed: (doc) ->
         publication.removed collectionName, doc._id
+
+    publication.onStop ->
+      handle.stop()
+      computation.stop()


Update

Update

--- src/runner.coffee
+++ src/runner.coffee
@@ -8,9 +8,13 @@

   start: (path, port) ->
     console.log "Starting #{path} port: #{port}"
+    nodePath = which 'node'
     cd path
-    @forever.start ['npm', 'start'],
-      max: 1
-      silent: false
-      env:
-        PORT: port
+    try
+      @forever.start [nodePath, 'app.js'],
+        max: 1
+        silent: false
+        env:
+          PORT: port
+    catch error
+      console.log error


Added more tests. peillis/helpapp#3455

Added more tests. peillis/helpapp#3455

--- test/tests.coffee
+++ test/tests.coffee
@@ -22,6 +22,12 @@
       description: "asc"
     Array: [("title": "asc"), ("description": "desc")]
     String: "price"
+  params:
+    valid:
+      rpp: 30
+      page: 4
+    null:
+      rpp: null

 # Test doofinder
 describe 'doofinder', ->
@@ -172,53 +178,155 @@
     it 'search with no type', (done) ->
       response =
         field: "value"
-      scope = nock('http://localhost:3000').get('/5/search').query({hashid: "ffffffffffffffffffffffffffffffff", page:1, rpp:10, query:""}).reply(200, response)
-      # Response value is not importan, the importan here is the requested URL
-      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, null, 'localhost:3000'
-      client.search '', (err, res) ->
-        res.should.to.be.deep.equal response
-        done()
-
-    it 'search with type', (done) ->
-      response =
-        field: "value"
-      scope = nock('http://localhost:3000').get('/5/search').query({hashid: "ffffffffffffffffffffffffffffffff", page:1, rpp:10, query:"", type:"product"}).reply(200, response)
-      # Response value is not importan, the importan here is the requested URL
-      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'localhost:3000'
-      client.search '', (err, res) ->
-        res.should.to.be.deep.equal response
-        done()
-
-    it 'search with type and query', (done) ->
-      response =
-        field: "value"
-      scope = nock('http://localhost:3000').get('/5/search').query({hashid: "ffffffffffffffffffffffffffffffff", page:1, rpp:10, query:"querystring", type:"product"}).reply(200, response)
-      # Response value is not importan, the importan here is the requested URL
-      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'localhost:3000'
-      client.search 'querystring', (err, res) ->
-        res.should.to.be.deep.equal response
-        done()
-
-    it 'search with type query and params (sort type object of: object, array, string)', (done) ->
-      response =
-        field: "value"
-      scope = nock('http://localhost:3000').get('/5/search').query(
-        {
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
           hashid: "ffffffffffffffffffffffffffffffff",
           page:1,
           rpp:10,
-          query:"querystring",
-          type:"product"
-          sort: mock.sort.Object
-        }).reply(200, response)
-      console.log {sort: mock.sort.Object}
-      console.log {sort: mock.sort.Object}
-      # Response value is not importan, the importan here is the requested URL
-      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'localhost:3000'
+          query:""})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, null, 'fooserver'
+      client.search '', (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
+          hashid: "ffffffffffffffffffffffffffffffff",
+          page:1,
+          rpp:10,
+          query:"",
+          type:"product"})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search '', (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with multi-type', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search?hashid=ffffffffffffffffffffffffffffffff&type=product&type=category&page=1&rpp=10&query=')
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, ['product', 'category'], 'fooserver'
+      client.search '', (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type and query', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+      .get('/5/search')
+      .query({
+        hashid: "ffffffffffffffffffffffffffffffff",
+        page:1,
+        rpp:10,
+        query:"querystring",
+        type:"product"})
+      .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type query and params (sort type object of: object, array, string)', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search?hashid=ffffffffffffffffffffffffffffffff&type=product&page=1&rpp=10&query=querystring&sort%5Bprice%5D=asc&sort%5Btitle%5D=desc&sort%5Bdescription%5D=asc')
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
       client.search 'querystring', {sort: mock.sort.Object}, (err, res) ->
         res.should.to.be.deep.equal response
         done()

+    it 'search with type query and params (sort type array of: object, array, string)', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search?hashid=ffffffffffffffffffffffffffffffff&type=product&page=1&rpp=10&query=querystring&sort%5B0%5D%5Btitle%5D=asc&sort%5B1%5D%5Bdescription%5D=desc')
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', {sort: mock.sort.Array}, (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type query and params (sort type string of: object, array, string)', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
+            hashid: "ffffffffffffffffffffffffffffffff",
+            type: "product",
+            page: 1,
+            rpp: 10,
+            query: "querystring",
+            sort: "price"})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', {sort: mock.sort.String}, (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type query and params (param valid of: valid, null)', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
+            hashid: "ffffffffffffffffffffffffffffffff",
+            type: "product",
+            page: 4,
+            rpp: 30,
+            query: "querystring"})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', mock.params.valid, (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type query and params (param null of: valid, null)', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
+            hashid: "ffffffffffffffffffffffffffffffff",
+            type: "product",
+            page: 1,
+            rpp: 10,
+            query: "querystring"})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', mock.params.null, (err, res) ->
+        res.should.to.be.deep.equal response
+        done()
+
+    it 'search with type query and params and one sort of type string', (done) ->
+      response =
+        field: "value"
+      scope = nock('http://fooserver')
+        .get('/5/search')
+        .query({
+            hashid: "ffffffffffffffffffffffffffffffff",
+            type: "product",
+            page: 4,
+            rpp: 30,
+            sort: "price",
+            query: "querystring"})
+        .reply(200, response)
+      client = new doofinder.Client mock.request.hashid, mock.request.api_key, 5, 'product', 'fooserver'
+      client.search 'querystring', {rpp: mock.params.valid.rpp, page: mock.params.valid.page, sort: mock.sort.String}, (err, res) ->
+        res.should.to.be.deep.equal response
+        done()

     it 'hit', ->
       a = 1


Delete "move-to-end-of-word" command

Delete "move-to-end-of-word" command

This allows the changes made in aeeda345 to actually *work*, ffs.

Pushed too damn soon.

--- .config/keymap.cson
+++ .config/keymap.cson
@@ -71,7 +71,7 @@

 "atom-text-editor":
 	"cmd-l":          "grammar-selector:show"
-
+	"alt-f":          "unset!"

 # Editing
 "atom-text-editor:not([mini])":


Fixed broken variable. Silly me.

Fixed broken variable. Silly me.

--- src/tucker.coffee
+++ src/tucker.coffee
@@ -17,7 +17,7 @@
 #   ocean
 #   with thanks to arsenio

-excuses = [
+quotes = [
   'He\'s useless. He\'s absolutely useless. He\'s as useless as a marzipan dildo.',
   'You don\'t seem to understand that I\'m going to have to have to mop up a fucking hurricane of piss from all these neurotics.',
   'Feet off the furniture, you Oxbridge twat. You\'re not in a punt now.',
@@ -44,4 +44,4 @@
 module.exports = (robot) ->
   robot.respond /tucker me/i, (msg)->
     quote = msg.random quotes
-    msg.send excuse
+    msg.send quote


Only reset the top '.line'

Only reset the top '.line'

[Closes #20]

--- lib/highlight-line-view.coffee
+++ lib/highlight-line-view.coffee
@@ -115,7 +115,7 @@
               .css('border-bottom','')
               .css('margin-bottom','')
               .css('margin-top','')
-    $('.line').each (index, line) ->
+    $('.line[data-screen-row]').each (index, line) ->
       if $(line).attr('style')?.indexOf('background') isnt -1
         top = $(line).css('top')
         $(line).attr 'style', "position: absolute; top: #{top};" if top?


fixes

fixes

--- lms/static/coffee/src/staff_grading/staff_grading.coffee
+++ lms/static/coffee/src/staff_grading/staff_grading.coffee
@@ -418,7 +418,7 @@

     if @state == state_error
-      @set_button_text(gettext('Try loading again')
+      @set_button_text(gettext('Try loading again'))
       show_action_button = true

     else if @state == state_grading


Add API for configuring default events for non-editor windows

Used by collaboration for when joining fails but basic keybindings
and events should still work.

--- src/window.coffee
+++ src/window.coffee
@@ -40,6 +40,16 @@
   window.syntax = deserialize(atom.getWindowState('syntax')) ? new Syntax
   window.pasteboard = new Pasteboard
   window.keymap = new Keymap()
+
+
+# Set up the default event handlers and menus for a non-editor windows.
+#
+# This can be used by packages to have a minimum level of keybindings and
+# menus available when not using the standard editor window.
+window.setUpDefaultEvents = ->
+  windowEventHandler = new WindowEventHandler
+  keymap.loadBundledKeymaps()
+  ipc.sendChannel 'update-application-menu', keymap.keystrokesByCommandForSelector('body')

 # This method is only called when opening a real application window
 window.startEditorWindow = ->


Remove trailing white space

Remove trailing white space

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -18,7 +18,7 @@
   req.key = key
   req.usr = apiKeys[key]
   req.db = app.get 'db'
-
+
   res.setHeader 'Access-Control-Allow-Origin', '*'

   next()


Pass all the arguments on to the error callback

--- models/artwork_collection.coffee
+++ models/artwork_collection.coffee
@@ -64,7 +64,7 @@
       error: (error) =>
         unless sd.NODE_ENV == 'test'
           @_unsave artwork, options
-        options?.error? error
+        options?.error?.apply this, arguments
     false

   _save: (artwork, options) ->


Unified capturing mechanism. Closes #10.

--- tests/murq.coffee
+++ tests/murq.coffee
@@ -16,6 +16,23 @@

 murq = require '../lib-cov/murq'

+
+output = ''
+_log = console.log
+_write = process.stdout.write
+
+startCapture = ->
+	output = ''
+	console.log = (x) ->
+		output += "#{x}\n"
+	process.stdout.write = (x) ->
+		output += "#{x}"
+
+endCapture = ->
+	console.log = _log
+	process.stdout.write = _write
+
+
 @exports =
 	'should ignore blank lines': (test) ->
 		source = "\n\n\n\n\n\n\n\n     \n     \n"
@@ -65,14 +82,12 @@
 			PLN me
 			PRINTLN now
 		"""
-		output = ''
-		_log = console.log
-		console.log = (x) -> output += "#{x}\n"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		console.log = _log
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "Kill\nme\nnow\n"
 		test.done()

@@ -82,14 +97,12 @@
 			P me
 			PRINT now
 		"""
-		output = ''
-		_write = process.stdout.write
-		process.stdout.write = (x) -> output += "#{x}"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		process.stdout.write = _write
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "Killmenow"
 		test.done()

@@ -110,14 +123,12 @@
 			:lab
 			p 3
 		"""
-		output = ''
-		_write = process.stdout.write
-		process.stdout.write = (x) -> output += "#{x}"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		process.stdout.write = _write
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "13"
 		test.done()

@@ -128,14 +139,12 @@
 			end
 			p 3
 		"""
-		output = ''
-		_write = process.stdout.write
-		process.stdout.write = (x) -> output += "#{x}"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		process.stdout.write = _write
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "12"
 		test.done()

@@ -145,14 +154,12 @@
 			p #$
 			p me
 		"""
-		output = ''
-		_write = process.stdout.write
-		process.stdout.write = (x) -> output += "#{x}"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		process.stdout.write = _write
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, 'Kill me'
 		test.done()

@@ -160,14 +167,12 @@
 		source = """
 			p Kill#/$me
 		"""
-		output = ''
-		_write = process.stdout.write
-		process.stdout.write = (x) -> output += x
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		process.stdout.write = _write
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "Kill\nme"
 		test.done()

@@ -178,14 +183,12 @@
 			p
 			pln install
 		"""
-		output = ''
-		_log = console.log
-		console.log = (x) -> output += "#{x}\n"
-
-		quest = new murq.Quest(source)
-		quest.run()
-
-		console.log = _log
+		startCapture()
+
+		quest = new murq.Quest(source)
+		quest.run()
+
+		endCapture()
 		test.strictEqual output, "Make\n\ninstall\n"
 		test.done()


Designing new module-structure

Designing new module-structure


Mark last event timestamp

Mark last event timestamp

--- lib/rails/click_prepare.coffee
+++ lib/rails/click_prepare.coffee
@@ -34,15 +34,15 @@
   , true

 else
+  lastPreparedTimestamp = null
+
   # Hook into jQuery click event trigger
   $.event.special.click =
     # preDispatch is a new hook added in 1.7.2. Its not documented as
     # a real public API, but we'll abuse it anyway.
     preDispatch: (event) ->
-      # Only trigger prepare handlers if this is the target phase. Add
-      # special case for triggered events which do not correctly set
-      # eventPhase.
-      if event.eventPhase is 2 or (event.isTrigger and !event.currentTarget)
+      # Check if this event has dispatched a prepare event already
+      if event.timeStamp isnt lastPreparedTimestamp
         # Reuse the existing event instead of creating a new copy. Any
         # calls to preventDefault or stopPropagation will take
         # immediate effect.
@@ -50,4 +50,6 @@
         $.event.trigger event, [], event.target, false
         # Restore the old event type
         event.type = 'click'
+        # Mark event's timestamp id as prepared
+        lastPreparedTimestamp = event.timeStamp
       return


fix removing book

fix removing book

--- scripts/gh-book/opf-file.coffee
+++ scripts/gh-book/opf-file.coffee
@@ -421,7 +421,9 @@

     # Override the tree's removeMe (which just asks the parent to remove the child)
     removeMe: ->
-      allContent.remove(@).save()
+      require ['cs!gh-book/epub-container'], (EpubContainer) =>
+        EpubContainer::instance().removeChild(@)
+        allContent.save()

     newNode: (options) ->
       model = options.model


Don't mention product manager in app ownership

PMs no longer own applications.

https://github.com/alphagov/govuk-developer-docs/pull/693

--- scripts/govuk_app_owners.coffee
+++ scripts/govuk_app_owners.coffee
@@ -10,4 +10,4 @@
       .get() (err, response, body) ->
         if response.statusCode == 200
           data = JSON.parse(body)
-          res.reply "#{data.app_name} is owned by `#{data.product_manager}` (#{data.team})"
+          res.reply "#{data.app_name} is owned by #{data.team}"


Removed coffeescript file. Closes #38

Removed coffeescript file. Closes #38

--- dist/jquery.browser.js.coffee
+++ dist/jquery.browser.js.coffee
@@ -1,78 +0,0 @@
-#!
-# jQuery Browser Plugin v0.0.6
-# https://github.com/gabceb/jquery-browser-plugin
-#
-# Original jquery-browser code Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
-# http://jquery.org/license
-#
-# Modifications Copyright 2013 Gabriel Cebrian
-# https://github.com/gabceb
-#
-# Released under the MIT license
-#
-# Date: 2013-07-29T17:23:27-07:00
-#
-
-((jQuery, window, undefined_) ->
-  "use strict"
-  matched = undefined
-  browser = undefined
-
-  jQuery.uaMatch = (ua) ->
-    ua = ua.toLowerCase()
-    match = /(opr)[\/]([\w.]+)/.exec(ua) or /(chrome)[ \/]([\w.]+)/.exec(ua) or /(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) or /(webkit)[ \/]([\w.]+)/.exec(ua) or /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) or /(msie) ([\w.]+)/.exec(ua) or ua.indexOf("trident") >= 0 and /(rv)(?::| )([\w.]+)/.exec(ua) or ua.indexOf("compatible") < 0 and /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) or []
-    platform_match = /(ipad)/.exec(ua) or /(iphone)/.exec(ua) or /(android)/.exec(ua) or /(windows phone)/.exec(ua) or /(win)/.exec(ua) or /(mac)/.exec(ua) or /(linux)/.exec(ua) or /(cros)/i.exec(ua) or []
-    browser: match[3] or match[1] or ""
-    version: match[2] or "0"
-    platform: platform_match[0] or ""
-
-  matched = jQuery.uaMatch(window.navigator.userAgent)
-  browser = {}
-
-  if matched.browser
-    browser[matched.browser] = true
-    browser.version = matched.version
-    browser.versionNumber = parseInt(matched.version)
-
-  if matched.platform
-    browser[matched.platform] = true
-
-  # These are all considered mobile platforms, meaning they run a mobile browser
-  if browser.android or browser.ipad or browser.iphone or browser["windows phone"]
-    browser.mobile = true
-
-  # These are all considered desktop platforms, meaning they run a desktop browser
-  if browser.cros or browser.mac or browser.linux or browser.win
-    browser.desktop = true
-
-  # Chrome, Opera 15+ and Safari are webkit based browsers
-  if browser.chrome or browser.opr or browser.safari
-    browser.webkit = true
-
-  # IE11 has a new token so we will assign it msie to avoid breaking changes
-  if browser.rv
-    ie = "msie"
-
-    matched.browser = ie
-    browser[ie] = true
-
-  # Opera 15+ are identified as opr
-  if browser.opr
-    opera = "opera"
-
-    matched.browser = opera
-    browser[opera] = true
-
-  # Stock Android browsers are marked as safari on Android.
-  if browser.safari && browser.android
-    android = 'android'
-
-    matched.browser = android
-    browser[android] = true
-
-  # Assign the name and platform variable
-  browser.name = matched.browser
-  browser.platform = matched.platform
-
-jQuery.browser = browser
-) jQuery, window


Analysis View now prepends to bottom.

Analysis View now prepends to bottom.

It's above the status bar, basically.

--- lib/views/analysis_view.coffee
+++ lib/views/analysis_view.coffee
@@ -6,11 +6,10 @@
   items: []

   @content: ->
-    @div class: 'inline-block', =>
-      @div class: 'tool-panel panel-bottom padded', =>
-        @div class: 'pull-right', =>
-          @a href: '#', class: 'icon icon-x', rel: 'dismiss', click: 'dismiss'
-        @div class: 'dart-tools-analysis-tool-panel'
+    @div class: 'tool-panel panel-bottom padded', =>
+      @div class: 'pull-right', =>
+        @a href: '#', class: 'icon icon-x', rel: 'dismiss', click: 'dismiss'
+      @div class: 'dart-tools-analysis-tool-panel'

   initialize: =>
@@ -29,7 +28,7 @@
   attach: ->
     @updateState()
     @hide()
-    atom.workspaceView.appendToBottom(this)
+    atom.workspaceView.prependToBottom(this)

   updateState: ->
     panel = @find('.dart-tools-analysis-tool-panel')


minor fix in the composition system

minor fix in the composition system

--- library/nucleus/compose.coffee
+++ library/nucleus/compose.coffee
@@ -53,7 +53,7 @@
     # mapped over each node of the linear hierarchy of the class that
     # invokes the composition functionality to obtain a shadow of the
     # original class that can be later modified to modify hierarchy.
-    @cloner: cloner = module?.exports?.cloner = (subject) ->
+    @cloner: @classShadingEngine = cloner = (subject) ->
         noClass = "the suplied subject is not a valid class"
         assert _.isObject(subject?.__super__ or null), noClass
         subject = subject.watermark if subject.watermark or 0


Fix the SecureStub implementation

Fix the SecureStub implementation

--- library/nucleus/protect.coffee
+++ library/nucleus/protect.coffee
@@ -59,9 +59,12 @@
     # truthful boolean, the service will NOT call implementation.
     # Please be sure invoke the super implementation, if override!
     preprocess: (request, response, resource, domain) ->
-        protectedUrl = tools.urlWithHost yes
         return if super request, response, resource, domain
         return if encrypted = request?.connection?.encrypted
-        current = pathname: request.url, query: request.params
-        current.host = protectedUrl;  current.protocol = "https"
-        response.redirect url.format current; response.headersSent
+        protectedUrl = tools.urlWithHost yes
+        current = url.parse protectedUrl
+        current.pathname = request.url
+        current.query = request.params
+        compiled = url.format current
+        response.redirect compiled
+        response.headersSent


FSFile: let chunk operations to use new Machine instance

--- client/Finder/fs/fsfile.coffee
+++ client/Finder/fs/fsfile.coffee
@@ -26,7 +26,6 @@
     @fetchContents no, callback

   fetchRawContents: (callback)->
-
     kite = @getKite()
     kite.vmOn().then =>
       kite.fsReadFile path: FSHelper.plainPath @path
@@ -52,9 +51,8 @@

     .nodeify(callback)

-
-
   saveAs:(contents, name, parentPath, callback)->
+
     @emit "fs.saveAs.started"

     newPath = FSHelper.plainPath "#{parentPath}/#{name}"
@@ -70,8 +68,7 @@
       .then (actualPath) =>

         file = FSHelper.createFileInstance {
-          type   : 'file'
-          path   : actualPath
+          path : actualPath
           @machine
         }

@@ -101,7 +98,7 @@

     kite = @getKite()

-    ok = kite.startVm()
+    ok = kite.vmOn()
     .then =>

       kite.fsWriteFile {
@@ -150,6 +147,8 @@
       @abortRequested = yes
       callback? null, abort: yes

+    kite = @getKite()
+
     iterateChunks = =>

       unless chunkQueue.length
@@ -167,16 +166,20 @@
         iterateChunks()
         return

-      @vmController.run
-        method   : 'fs.writeFile'
-        vmName   : @vmName
-        withArgs : {path: FSHelper.plainPath(@path), content, append}
-      , (err, res) =>
-        return callback? err  if err
+      kite.fsWriteFile {
+        path: FSHelper.plainPath @path
+        content, append
+      }
+
+      .then (res) =>
         @emit "ChunkUploaded", res
         iterateChunks()

-    iterateChunks() if chunkQueue.length > 0
+      .catch (err) ->
+        callback? err  if err
+
+    iterateChunks()  if chunkQueue.length > 0
+

   abort: -> @emit "AbortRequested"

@@ -214,4 +217,3 @@
         @emit "fs.save.finished", null, response

         return response
-


show tool displayName in tool content header

--- client/code/router/main.coffee
+++ client/code/router/main.coffee
@@ -55,7 +55,7 @@
     window.tools.fetch
       success: =>
         model = window.tools.get tool
-        titleView = new Cu.View.Title {text: "My Tools / #{model.get 'displayName'}" }
+        titleView = new Cu.View.Title {text: "My Tools / #{model.get('manifest').displayName}" }
         contentView = new Cu.View.ToolContent {model: model}
         @titleView.showView titleView
         @appView.showView contentView


Classroom: Disabled including Classroom files.

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -763,14 +763,14 @@
   "app/CommonViews/workspace/collaborativeworkspace.coffee",

   # CLASSROOM
-  "app/Applications/Classroom.kdapplication/Views/classroomworkspace.coffee",
-  "app/Applications/Classroom.kdapplication/Views/classroomchapterlist.coffee",
-  "app/Applications/Classroom.kdapplication/Views/classroomchapterthumbview.coffee",
-  "app/Applications/Classroom.kdapplication/Views/classroomcoursethumbview.coffee",
-  "app/Applications/Classroom.kdapplication/Views/classroomcoursesview.coffee",
-  "app/Applications/Classroom.kdapplication/Views/classroomcourseview.coffee",
-  "app/Applications/Classroom.kdapplication/AppView.coffee",
-  "app/Applications/Classroom.kdapplication/AppController.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomworkspace.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomchapterlist.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomchapterthumbview.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomcoursethumbview.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomcoursesview.coffee",
+  # "app/Applications/Classroom.kdapplication/Views/classroomcourseview.coffee",
+  # "app/Applications/Classroom.kdapplication/AppView.coffee",
+  # "app/Applications/Classroom.kdapplication/AppController.coffee",

   "app/CommonViews/modalappslistitemview.coffee",

@@ -858,7 +858,7 @@
   "stylus/app.group.creation.styl",
   "stylus/app.user.styl",
   "stylus/app.markdown.styl",
-  "stylus/app.classroom.styl",
+  # "stylus/app.classroom.styl",
   "stylus/temp.styl",
   "stylus/third.workspace.styl",
   # "stylus/app.landing.styl",


payment: use unminified accounting.js

payment: use unminified accounting.js

I had to hack accounting.js because of retarded AMD

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -3,6 +3,7 @@
   # Libs
   "libs/async.js",
   "libs/md5-min.js",
+  "libs/accounting.js",

   # --- Application ---
   "app/MainApp/utils.coffee"


Grunt: CoffeeScript files are .coffee, not .js

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -7,7 +7,7 @@
         files: [
           expand: true     # Enable dynamic expansion.
           cwd: 'src/'      # Src matches are relative to this path.
-          src: ['**/*.js'] # Actual pattern(s) to match.
+          src: ['**/*.coffee'] # Actual pattern(s) to match.
           dest: 'lib/'     # Destination path prefix.
           ext: '.js'       # Dest filepaths will have this extension.
         ]


set includePath for less compilation

set includePath for less compilation

the includePath for less was missing, making an out of the box use of bootstrap impossible without adding it first. this has been corrected.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -156,6 +156,7 @@
           noUnderscores: no
           noIDs: no
           zeroUnits: no
+          includePath: ['vendor/bootstrap/less']

     coffeelint:
       src:


Adds Uglify reporting

Adds Uglify reporting

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -26,6 +26,7 @@
               ' * <%= pkg.homepage %>\n' +
               ' *\n' +
               ' */\n'
+        report: 'gzip'
         mangle:
           except: ['Revisionist']
       browser:


fix Gruntfile config for grunt-contrib-coffee according to https://github.com/gruntjs/grunt-contrib-coffee/issues/5#issuecomment-11376707

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -38,7 +38,7 @@
         files: 'test/pathfinder_test.js': 'test/pathfinder_test.coffee'
       test:
         files: 'test/dir/*.js': ['test/dir/**/*.coffee']
-        rename: (destBase, destPath, options) -> path.join(destBase, destPath)
+        rename: (destBase, destPath) -> destBase + destPath.replace(/\.coffee$/, '.js')
       tasks:
         files: 'tasks/*.js': ['tasks/*.coffee']


Refactoring

Refactoring

--- coffee/tasks/getTwitter.coffee
+++ coffee/tasks/getTwitter.coffee
@@ -1,4 +1,3 @@
-#work in progress
 request         = require "request"
 config          = require "config"
 sqlite3         = do require("sqlite3").verbose
@@ -14,8 +13,8 @@
 module.exports = (req, res) ->

 	db.serialize( ->
-		#db.run("CREATE TABLE channels (id, link, date)");
-		#db.run("CREATE TABLE published (id, video_link, date)");
+		#db.run("CREATE TABLE channels (id, user_name, date)");
+		#db.run("CREATE TABLE published (id, post_id, user_name, date)");
 		db.each(
 			"SELECT rowid AS id, user_name FROM #{config.twitter.database.accounts_table} ORDER BY date ASC LIMIT $limit"
 			$limit: 1
@@ -146,7 +145,7 @@
 										post_url += "&from_group=1"
 										post_url += "&attachments=#{image}/"
 										post_url += "&access_token=#{config.common.vk_token}"
-
+
 										request(
 											post_url,
 											(err, head, body) ->


More debugging calls

More debugging calls

--- tasks/pack.coffee
+++ tasks/pack.coffee
@@ -1,7 +1,7 @@
 request = require 'request'
 path = require 'path'
 args = require './args'
-fs = require 'fs'
+fs = require 'fs-extra-promise'

 asar = require 'asar'
 async = require 'async'
@@ -427,6 +427,25 @@
           console.log 'winInstaller errored'
           callback err

+    (callback) ->
+      try
+        fs.readdirSync './'
+      catch error
+        console.log 'err fs.readdirSync \'./\'', error
+      try
+        fs.readdirSync './dist'
+      catch error
+        console.log 'err fs.readdirSync \'./dist\'', error
+      try
+        fs.readdirSync '../'
+      catch error
+        console.log 'err fs.readdirSync \'../\'', error
+      try
+        fs.readdirSync '../dist'
+      catch error
+        console.log 'err fs.readdirSync \'../dist\'', error
+      callback()
+
     # Run makensis
     applySpawn (process.env.MAKENSIS_PATH or 'makensis.exe'), ['build/resources/win/installer.nsi']
   ], done


render children

render children

--- src/renderer/Renderer.coffee
+++ src/renderer/Renderer.coffee
@@ -7,13 +7,12 @@

 		@type = 'Renderer'

-		options = Bu.combineOptions(arguments, {
+		options = Bu.combineOptions arguments,
 			width: 800
 			height: 600
 			fps: 60
 			fillParent: off
 			border: on
-		})
 		@width = options.width
 		@height = options.height
 		@fps = options.fps
@@ -40,7 +39,7 @@
 		@dom.style.background = '#eee'
 		@dom.oncontextmenu = -> false

-		window.canvas = @dom;
+		window.canvas = @dom

 		onResize = (e) =>
 			canvasRatio = @dom.height / @dom.width
@@ -67,7 +66,7 @@
 			tickCount += 1
 			@triggerEvent 'update', {'tickCount': tickCount}
 			clearCanvas()
-			@drawShapes()
+			@drawShapes(@shapes)
 			@clipMeter.tick() if @clipMeter?

 		setInterval(tick, 1000 / @fps)
@@ -82,7 +81,7 @@
 			, 100

 		tickCount = 0
-		@isRunning = true;
+		@isRunning = true

 	pause: =>
@@ -99,8 +98,8 @@
 		@shapes.push shape

-	drawShapes: =>
-		for shape in @shapes
+	drawShapes: (shapes) =>
+		for shape in shapes
 			switch shape.type
 				when 'Point' then @drawPoint(shape)
 				when 'Line' then @drawLine(shape)
@@ -116,6 +115,8 @@
 				when 'Bounds' then @drawBounds(shape)
 				else
 					console.log('drawShapes(): unknown shape: ', shape)
+			@drawShapes shape.children
+		return this

 	drawPoint: (shape) ->


Outdated settings

Outdated settings

--- config/search.coffee
+++ config/search.coffee
@@ -2,12 +2,5 @@
   This affects the behaviour of BlackSam's Search and Search Indexing.
 ###
 module.exports = (cfg) ->
-  # This is the frequency with which the Torrents database gets
-  # checked for updates. This is not strict, timing starts when
-  # the last Torrent has been checked. If the database is of
-  # several thousands of Torrents, it could take several minutes
-  # to check them all, and that time is not part of this setting.
-  cfg.set 'indexer timespan', 1000 * 3600 # 1 hour
-
   # The number of results to show in Search pages.
   cfg.set 'search results per page', 20


minor tweak

minor tweak

--- elasticsearch.coffee
+++ elasticsearch.coffee
@@ -148,7 +148,7 @@
             console.log "indexed", gist.id
             return gistCb(err, response)
         else
-          gistCb()
+          return gistCb()
     else
       # post to elastic search
       client.index


Fix bare phrases in calm-down

Fix bare phrases in calm-down

Coffeescript's block regex syntax ignores whitespace, so recognizing whitespace in a phrase would require \s; but I just converted it to one line.

--- src/scripts/calm-down.coffee
+++ src/scripts/calm-down.coffee
@@ -16,11 +16,8 @@

   robot.respond /manatee|calm( me)?/i, (msg) -> msg.send manatee()

-  robot.hear ///
-    (calm down)|
-    (simmer down)|
-    (that escalated quickly)
-  ///i, (msg) -> msg.send manatee()
+  robot.hear /calm down|simmer down|that escalated quickly/i, (msg) ->
+    msg.send manatee()

   unless process.env.HUBOT_LESS_MANATEES
     robot.hear ///


added file information

added file information

--- src/scripts/trollicon.coffee
+++ src/scripts/trollicon.coffee
@@ -1,5 +1,6 @@
 # trollicon - Returns a trollicon image
 # by Adan Alvarado
+# used resources from : https://github.com/sagargp/trollicons Adium extension

 trollicons = {
   'gasp' : 'http://i.imgur.com/tYmuZ.png',


applyUpdate refactoring

applyUpdate refactoring

--- src/scripts/ui-scroll.coffee
+++ src/scripts/ui-scroll.coffee
@@ -304,7 +304,6 @@
 								# prepended items have to be inserted from the bottom up
 								for wrapper in prepended
 									wrapper.doInsert()
-								# doAdjustment(rid, finalize)
 								#log "top {actual=#{builder.topDataPos()} visible from=#{topVisiblePos()} bottom {visible through=#{bottomVisiblePos()} actual=#{builder.bottomDataPos()}}"
 								if shouldLoadBottom()
 									enqueueFetch(rid, true)
@@ -411,31 +410,23 @@
 						adapter.isLoading = false

 						applyUpdate = (wrapper, newItems) ->
-							animations = []
 							if angular.isArray newItems
-								if newItems.length
-									if newItems.length == 1 && newItems[0] == wrapper.scope[itemName]
-										# update inplace
+								ndx = wrapper.scope.$index
+								if ndx > first
+									oldItemNdx = ndx-first
+								else
+									# this is where the first item from the batch is prepended to the
+									# old item, but the rest of them are appended to it. the old item will be in this position
+									oldItemNdx = 1
+								for newItem,i in newItems
+									if newItem == wrapper.scope[itemName]
+										keepIt = true;
 									else
-										ndx = wrapper.scope.$index
-										if ndx > first
-											oldItemNdx = ndx-first
-										else
-											# this is where the first item from the batch is prepended to the
-											# old item, but the rest of them are appended to it. the old item will be in this position
-											oldItemNdx = 1
-										#replace items. First insert new items
-										for newItem,i in newItems
-											insert ndx+i, newItem
-										# now delete the old one
-										animations.push removeElement oldItemNdx
-								else
-									# delete the item
-									animations.push removeElement wrapper.scope.$index-first
-									next--
+										insert wrapper.scope.$index+i, newItem
+								unless keepIt
+									removeElement oldItemNdx
 								# re-index the buffer
 								item.scope.$index = first + i for item,i in buffer
-								animations

 						adapter.applyUpdates = (arg1, arg2) ->
 							animations = []
@@ -443,12 +434,12 @@
 							if angular.isFunction arg1
 								# arg1 is the updater function, arg2 is ignored
 								for wrapper in buffer.slice(0)  # we need to do it on the buffer clone
-									animations = animations.concat applyUpdate wrapper, arg1(wrapper.scope[itemName], wrapper.scope, wrapper.element)
+									applyUpdate wrapper, arg1(wrapper.scope[itemName], wrapper.scope, wrapper.element)
 							else
 								# arg1 is item index, arg2 is the newItems array
 								if arg1%1 == 0 # checking if it is an integer
 									if 0 <= arg1-first < buffer.length
-										animations = animations.concat applyUpdate buffer[arg1 - first], arg2
+										applyUpdate buffer[arg1 - first], arg2
 								else
 									throw new Error "applyUpdates - #{arg1} is not a valid index"
 							$q.all(animations).then ->


fix specs

fix specs

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -655,29 +655,29 @@

     describe "when numlock is on", ->
       it "translates numpad digits using KeyboardEvent.code", ->
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '1', code: 'Numpad0', modifierState: {NumLock: true}})), 'numpad0')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '2', code: 'Numpad1', modifierState: {NumLock: true}})), 'numpad1')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '3', code: 'Numpad2', modifierState: {NumLock: true}})), 'numpad2')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '4', code: 'Numpad3', modifierState: {NumLock: true}})), 'numpad3')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '5', code: 'Numpad4', modifierState: {NumLock: true}})), 'numpad4')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '6', code: 'Numpad5', modifierState: {NumLock: true}})), 'numpad5')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '0', code: 'Numpad0', modifierState: {NumLock: true}})), 'numpad0')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '1', code: 'Numpad1', modifierState: {NumLock: true}})), 'numpad1')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '2', code: 'Numpad2', modifierState: {NumLock: true}})), 'numpad2')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '3', code: 'Numpad3', modifierState: {NumLock: true}})), 'numpad3')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '4', code: 'Numpad4', modifierState: {NumLock: true}})), 'numpad4')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '5', code: 'Numpad5', modifierState: {NumLock: true}})), 'numpad5')
         assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '6', code: 'Numpad6', modifierState: {NumLock: true}})), 'numpad6')
         assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '7', code: 'Numpad7', modifierState: {NumLock: true}})), 'numpad7')
         assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '8', code: 'Numpad8', modifierState: {NumLock: true}})), 'numpad8')
         assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '9', code: 'Numpad9', modifierState: {NumLock: true}})), 'numpad9')

     describe "when numlock is off", ->
-      it "doesn't translate numpad digits", ->
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '1', code: 'Numpad0', modifierState: {NumLock: false}})), 'insert')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '2', code: 'Numpad1', modifierState: {NumLock: false}})), 'end')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '3', code: 'Numpad2', modifierState: {NumLock: false}})), 'down')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '4', code: 'Numpad3', modifierState: {NumLock: false}})), 'pagedown')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '5', code: 'Numpad4', modifierState: {NumLock: false}})), 'left')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '6', code: 'Numpad5', modifierState: {NumLock: false}})), 'clear')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '6', code: 'Numpad6', modifierState: {NumLock: false}})), 'right')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '7', code: 'Numpad7', modifierState: {NumLock: false}})), 'home')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '8', code: 'Numpad8', modifierState: {NumLock: false}})), 'up')
-        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: '9', code: 'Numpad9', modifierState: {NumLock: false}})), 'pageup')
+      it "doesn't translate numpad digits using KeyboardEvent.code", ->
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'Insert', code: 'Numpad0', modifierState: {NumLock: false}})), 'insert')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'End', code: 'Numpad1', modifierState: {NumLock: false}})), 'end')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'ArrowDown', code: 'Numpad2', modifierState: {NumLock: false}})), 'down')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'PageDown', code: 'Numpad3', modifierState: {NumLock: false}})), 'pagedown')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'ArrowLeft', code: 'Numpad4', modifierState: {NumLock: false}})), 'left')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'Clear', code: 'Numpad5', modifierState: {NumLock: false}})), 'clear')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'ArrowRight', code: 'Numpad6', modifierState: {NumLock: false}})), 'right')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'Home', code: 'Numpad7', modifierState: {NumLock: false}})), 'home')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'ArrowUp', code: 'Numpad8', modifierState: {NumLock: false}})), 'up')
+        assert.equal(keymapManager.keystrokeForKeyboardEvent(buildKeydownEvent({key: 'PageUp', code: 'Numpad9', modifierState: {NumLock: false}})), 'pageup')

     describe "when the Dvorak QWERTY-⌘ layout is in use on macOS", ->
       it "uses the US layout equivalent when the command key is held down", ->


:lipstick:

:lipstick:

--- spec/keymap-manager-spec.coffee
+++ spec/keymap-manager-spec.coffee
@@ -85,7 +85,7 @@
           expect(events[0].type).toBe 'y-command'
           expect(events[0].target).toBe elementA

-      describe "if matching binding's command is 'native!'", ->
+      describe "if the matching binding's command is 'native!'", ->
         it "terminates without preventing the browser's default action", ->
           keymapManager.addKeymap "test",
             ".b":


Render: Skip rendering if shader didn't compile.

--- engine/render.coffee
+++ engine/render.coffee
@@ -399,6 +399,8 @@
             if material_override?
                 mat = material_override
             shader = mat.get_shader(mesh)
+            if not shader._program?
+                continue
             shader.use()

             if mat.double_sided == @cull_face_enabled


Format numbers on home page, closes #38

Format numbers on home page, closes #38

--- app/controllers/home.coffee
+++ app/controllers/home.coffee
@@ -1,5 +1,9 @@
 Controller = require 'zooniverse/controllers/base-controller'
 Api = require 'zooniverse/lib/api'
+
+formatNumber = (n) ->
+  # TODO: Localize this.
+  n.toString().replace /(\d)(?=(\d{3})+(?!\d))/g, '$1,'

 class Home extends Controller
   className: 'home-page'
@@ -14,6 +18,6 @@

   onProjectLoad: (project) =>
     for name in ['classification', 'bubble', 'cluster', 'ego', 'galaxy']
-      @counterValues.filter(".#{name}").html project["#{name}_count"] || '0'
+      @counterValues.filter(".#{name}").html formatNumber project["#{name}_count"] || '0'

 module.exports = Home


request generator to keep ajax under control

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -52,12 +52,9 @@

     # Downloads a meta file.
     $.ajax(
-      type: "get"
-      crossDomain: true
-      dataType: "json"
-      url: "https://api-content.dropbox.com/1/files/sandbox/meta.seed"
-      beforeSend: (xhr) ->
-        xhr.setRequestHeader "Authorization", "Bearer " + Sync.oauth.access_token
+      Sync.generateRequest
+        request: "download"
+        filename: "meta"
     ).done((data) ->
       console.log(JSON.parse(Sync.exportData()), data, Sync.queue)
       result = Sync.merger(JSON.parse(Sync.exportData()), data, Sync.queue)
@@ -103,26 +100,21 @@
                 dbrequests[itemid].onsuccess = (e) =>
                   # Upload file to my butt.
                   $.ajax(
-                    type: "put"
-                    crossDomain: true
-                    contentType: "application/octet-stream"
-                    dataType: "text"
-                    url: "https://api-content.dropbox.com/1/files_put/sandbox/" + itemid + ".seed"
-                    data: e.target.result
-                    beforeSend: (xhr) ->
-                      xhr.setRequestHeader 'Authorization', 'Bearer ' + Sync.oauth.access_token
+                    Sync.generateRequest
+                      request: "upload"
+                      filename: itemid
+                      dataType: "text"
+                      data: e.target.result
                   ).done (data) ->
                     deferred.resolve()

               when "download"
                 # Download from my butt
                 $.ajax(
-                  type: "get"
-                  crossDomain: true
-                  dataType: "text"
-                  url: "https://api-content.dropbox.com/1/files/sandbox/" + item[1] + ".seed"
-                  beforeSend: (xhr) ->
-                    xhr.setRequestHeader "Authorization", "Bearer " + Sync.oauth.access_token
+                  Sync.generateRequest
+                    request: "download"
+                    filename: item[1]
+                    dataType: "text"
                 ).done (data) ->
                   # We now have to store the new data in the database.
                   trans = Sync.db.transaction(["notes"], "readwrite")
@@ -134,20 +126,14 @@

               when "destroy"
                 dbrequests[item[1]] = store.delete(item[1])
-                file = item[1] + ".seed"
+                file = item[1]

                 # Delete in the butt
                 $.ajax(
-                  type: "post"
-                  crossDomain: true
-                  dataType: "json"
-                  url: "https://api.dropbox.com/1/fileops/delete"
-                  data: {
-                    root: "sandbox"
-                    path: file
-                  }
-                  beforeSend: (xhr) ->
-                    xhr.setRequestHeader "Authorization", "Bearer " + Sync.oauth.access_token
+                  Sync.generateRequest
+                    request: "destroy"
+                    filename: file
+                    dataType: "text"
                 ).done (data) ->
                   deferred.resolve()

@@ -162,14 +148,10 @@

           # Send it to my butt yo
           $.ajax(
-            type: "put"
-            crossDomain: true
-            contentType: "application/octet-stream"
-            dataType: "json"
-            url: "https://api-content.dropbox.com/1/files_put/sandbox/meta.seed"
-            data: JSON.stringify(result[0])
-            beforeSend: (xhr) ->
-              xhr.setRequestHeader 'Authorization', 'Bearer ' + Sync.oauth.access_token
+            Sync.generateRequest
+              request: "upload"
+              filename: "meta"
+              data: JSON.stringify(result[0])
           ).done (data) ->
             console.log("all done! Delete the queue")
             Sync.queue = {"Note": {}, "Notebook": {}}
@@ -189,13 +171,11 @@

             # make ajax request based on inputs from current loop element
             promises.push($.ajax(
-                type: "put"
-                crossDomain: true
-                contentType: "application/octet-stream"
-                url: "https://api-content.dropbox.com/1/files_put/sandbox/" + e.id + ".seed"
-                data: data
-                beforeSend: (xhr) ->
-                  xhr.setRequestHeader 'Authorization', 'Bearer ' + Sync.oauth.access_token
+                Sync.generateRequest
+                  request: "upload"
+                  filename: e.id
+                  dataType: "text"
+                  data: JSON.stringify(result[0])
               )
             )

@@ -204,19 +184,42 @@
           console.log 'All files uploaded, uploading the meta file'

           $.ajax(
-            type: "put"
-            crossDomain: true
-            contentType: "application/octet-stream"
-            dataType: "json"
-            url: "https://api-content.dropbox.com/1/files_put/sandbox/meta.seed"
-            data: Sync.exportData()
-            beforeSend: (xhr) ->
-              xhr.setRequestHeader 'Authorization', 'Bearer ' + Sync.oauth.access_token
+            Sync.generateRequest
+              request: "upload"
+              filename: "meta"
+              data: Sync.exportData()
           ).done (data) ->
             console.log("all done! Delete the queue")
             Sync.queue = {"Note": {}, "Notebook": {}}
             Sync.saveQueue()

+  generateRequest: (opts) ->
+    if Sync.oauth.service is "dropbox"
+      params =
+        crossDomain: true
+        dataType: opts.dataType || "json"
+        beforeSend: (xhr) ->
+          xhr.setRequestHeader "Authorization", "Bearer " + Sync.oauth.access_token
+
+      if opts.request is "download"
+        params.type = "get"
+        params.url = "https://api-content.dropbox.com/1/files/sandbox/" + opts.filename + ".seed"
+
+      else if opts.request is "upload"
+        params.type = "put"
+        params.contentType = opts.contentType || "application/octet-stream"
+        params.url = "https://api-content.dropbox.com/1/files_put/sandbox/" + opts.filename + ".seed"
+        data: opts.data
+
+      else if opts.request is "destroy"
+        params.type = "post"
+        params.url = "https://api.dropbox.com/1/fileops/delete"
+        params.data = {
+          root: "sandbox"
+          path: opts.filename + ".seed"
+        }
+
+      params # return

   connect: (fn) ->


Automatic Sync

Automatic Sync

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -12,6 +12,14 @@
   oauth: JSON.parse localStorage.oauth or '{"service": "undefined"}'
   queue: JSON.parse localStorage.Queue or '{"Note": {}, "Notebook": {}}'
   syncMeta: {}
+
+  # Run a Sync on a Timeout
+  # Clears if another request is put through
+  timeoutSync: ->
+    clearTimeout Sync.timeoutId
+    Sync.timeoutId = setTimeout ->
+      Sync.doSync() if localStorage.oauth
+    , 5000

   # Hold pending actions
   pending: []
@@ -562,6 +570,7 @@
       Sync.addToQueue(event, record)
       console.log '%c> Calling change: ' + event, 'background: #eee'
       Sync.defer(this, @saveLocal)
+      Sync.timeoutSync()

     Sync.connect()

@@ -595,6 +604,7 @@
     request = store.put(content, @id)

     request.onsuccess = (e) =>
+      Sync.timeoutSync()
       callback() if callback

   loadNote: (callback) ->


mark plotting from zoomed and unzoomed states

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -23,6 +23,8 @@
       @mark.draw(e)

   plotPoints: (xMin = @smallestX, xMax = @largestX) ->
+    @xMin = xMin
+    @xMax = xMax
     @clearCanvas()
     for point in @data
       x = ((point.x - xMin) / (xMax - xMin)) * @canvas.width
@@ -30,11 +32,9 @@
       @ctx.fillStyle = "#fff"
       @ctx.fillRect(x, y,2,2)

-    # @scale = 1 + (xMax - xMin) / @largestX
-
     for mark in @marks.all
-      scaledMin = ((@toDataXCoord(mark.canvasXMin) - xMin) / (xMax - xMin)) * @canvas.width
-      scaledMax = ((@toDataXCoord(mark.canvasXMax) - xMin) / (xMax - xMin)) * @canvas.width
+      scaledMin = ((mark.dataXMin - xMin) / (xMax - xMin)) * @canvas.width
+      scaledMax = ((mark.dataXMax - xMin) / (xMax - xMin)) * @canvas.width

       mark.element.style.width = (scaledMax-scaledMin) + "px"
       mark.element.style.left = (scaledMin) + "px"
@@ -47,11 +47,7 @@

   toCanvasXCoord: (dataPoint) -> ((dataPoint - @smallestX) / (@largestX - @smallestX)) * @canvas.width

-  # toDataXCoord: (domCoord) -> ((domCoord - @canvas.getBoundingClientRect().left)/ @canvas.width) * (@largestX - @smallestX)
-
-  toDataXCoord: (canvasPoint) -> (canvasPoint / @canvas.width) * (@largestX - @smallestX)
-
-  toDomXCoord: (dataPoint) -> ((dataPoint) * (@largestX - @smallestX)) + @canvas.getBoundingClientRect().left
+  toDataXCoord: (canvasPoint) -> ((canvasPoint / @canvas.width) * (@xMax - @xMin)) + @xMin

 class Marks
   constructor: -> @all = []
@@ -168,5 +164,4 @@

   pointerYInElement: (e) -> e.offsetY || e.clientY - e.target.offsetTop + window.pageYOffset - @canvas.getBoundingClientRect().top

-
 module?.exports = CanvasGraph: CanvasGraph, Marks: Marks, Mark: Mark