WIP: Fleshing out specs for async tokenization

--- spec/app/tokenized-buffer-spec.coffee
+++ spec/app/tokenized-buffer-spec.coffee
@@ -67,121 +67,134 @@
       expect(tokenizedBuffer.lineForScreenRow(0).tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js'])
       expect(tokenizedBuffer.lineForScreenRow(11).tokens[1]).toEqual(value: 'return', scopes: ['source.js', 'keyword.control.js'])

-    describe "when the buffer changes", ->
-      describe "when lines are updated, but none are added or removed", ->
-        it "updates tokens for each of the changed lines", ->
-          range = new Range([0, 0], [2, 0])
-          buffer.change(range, "foo()\n7\n")
-
-          expect(tokenizedBuffer.lineForScreenRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.brace.round.js'])
-          expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.js'])
-          # line 2 is unchanged
-          expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 0, end: 2, delta: 0)
-
-        it "updates tokens for lines beyond the changed lines if needed", ->
-          buffer.insert([5, 30], '/* */')
-          changeHandler.reset()
-
-          buffer.insert([2, 0], '/*')
-          expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(5).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 2, end: 5, delta: 0)
-
-        it "resumes highlighting with the state of the previous line", ->
-          buffer.insert([0, 0], '/*')
-          buffer.insert([5, 0], '*/')
-
-          buffer.insert([1, 0], 'var ')
-          expect(tokenizedBuffer.lineForScreenRow(1).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
-      describe "when lines are both updated and removed", ->
-        it "updates tokens to reflect the removed lines", ->
-          range = new Range([1, 0], [3, 0])
-          buffer.change(range, "foo()")
-
-          # previous line 0 remains
-          expect(tokenizedBuffer.lineForScreenRow(0).tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js'])
-
-          # previous line 3 should be combined with input to form line 1
-          expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
-          expect(tokenizedBuffer.lineForScreenRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
-
-          # lines below deleted regions should be shifted upward
-          expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
-          expect(tokenizedBuffer.lineForScreenRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 1, end: 3, delta: -2)
-
-        it "updates tokens for lines beyond the changed lines if needed", ->
-          buffer.insert([5, 30], '/* */')
-          changeHandler.reset()
-
-          buffer.change(new Range([2, 0], [3, 0]), '/*')
-          expect(tokenizedBuffer.lineForScreenRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
-          expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 2, end: 5, delta: -1)
-
-      describe "when lines are both updated and inserted", ->
-        it "updates tokens to reflect the inserted lines", ->
-          range = new Range([1, 0], [2, 0])
-          buffer.change(range, "foo()\nbar()\nbaz()\nquux()")
-
-          # previous line 0 remains
-          expect(tokenizedBuffer.lineForScreenRow(0).tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.modifier.js'])
-
-          # 3 new lines inserted
-          expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
-          expect(tokenizedBuffer.lineForScreenRow(2).tokens[0]).toEqual(value: 'bar', scopes: ['source.js'])
-          expect(tokenizedBuffer.lineForScreenRow(3).tokens[0]).toEqual(value: 'baz', scopes: ['source.js'])
-
-          # previous line 2 is joined with quux() on line 4
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[0]).toEqual(value: 'quux', scopes: ['source.js'])
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
-
-          # previous line 3 is pushed down to become line 5
-          expect(tokenizedBuffer.lineForScreenRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 1, end: 2, delta: 2)
-
-        it "updates tokens for lines beyond the changed lines if needed", ->
-          buffer.insert([5, 30], '/* */')
-          changeHandler.reset()
-
-          buffer.insert([2, 0], '/*\nabcde\nabcder')
-          expect(tokenizedBuffer.lineForScreenRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
-          expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(5).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(6).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(7).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-          expect(tokenizedBuffer.lineForScreenRow(8).tokens[0].scopes).not.toBe ['source.js', 'comment.block.js']
-
-          expect(changeHandler).toHaveBeenCalled()
-          [event] = changeHandler.argsForCall[0]
-          delete event.bufferChange
-          expect(event).toEqual(start: 2, end: 5, delta: 2)
+    describe "when there is a buffer change starting in the tokenized region", ->
+      describe "when the change causes fewer than @chunkSize lines to be retokenized", ->
+        describe "when lines are updated, but none are added or removed", ->
+          it "updates tokens for each of the changed lines", ->
+            range = new Range([0, 0], [2, 0])
+            buffer.change(range, "foo()\n7\n")
+
+            expect(tokenizedBuffer.lineForScreenRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.brace.round.js'])
+            expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.js'])
+            # line 2 is unchanged
+            expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 0, end: 2, delta: 0)
+
+          it "updates tokens for lines beyond the changed lines if needed", ->
+            buffer.insert([5, 30], '/* */')
+            changeHandler.reset()
+
+            buffer.insert([2, 0], '/*')
+            expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(5).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 2, end: 5, delta: 0)
+
+          it "resumes highlighting with the state of the previous line", ->
+            buffer.insert([0, 0], '/*')
+            buffer.insert([5, 0], '*/')
+
+            buffer.insert([1, 0], 'var ')
+            expect(tokenizedBuffer.lineForScreenRow(1).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+
+        describe "when lines are both updated and removed", ->
+          it "updates tokens to reflect the removed lines", ->
+            range = new Range([1, 0], [3, 0])
+            buffer.change(range, "foo()")
+
+            # previous line 0 remains
+            expect(tokenizedBuffer.lineForScreenRow(0).tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js'])
+
+            # previous line 3 should be combined with input to form line 1
+            expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
+            expect(tokenizedBuffer.lineForScreenRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+
+            # lines below deleted regions should be shifted upward
+            expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
+            expect(tokenizedBuffer.lineForScreenRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 1, end: 3, delta: -2)
+
+          it "updates tokens for lines beyond the changed lines if needed", ->
+            buffer.insert([5, 30], '/* */')
+            changeHandler.reset()
+
+            buffer.change(new Range([2, 0], [3, 0]), '/*')
+            expect(tokenizedBuffer.lineForScreenRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
+            expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 2, end: 5, delta: -1)
+
+        describe "when lines are both updated and inserted", ->
+          it "updates tokens to reflect the inserted lines", ->
+            range = new Range([1, 0], [2, 0])
+            buffer.change(range, "foo()\nbar()\nbaz()\nquux()")
+
+            # previous line 0 remains
+            expect(tokenizedBuffer.lineForScreenRow(0).tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.modifier.js'])
+
+            # 3 new lines inserted
+            expect(tokenizedBuffer.lineForScreenRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js'])
+            expect(tokenizedBuffer.lineForScreenRow(2).tokens[0]).toEqual(value: 'bar', scopes: ['source.js'])
+            expect(tokenizedBuffer.lineForScreenRow(3).tokens[0]).toEqual(value: 'baz', scopes: ['source.js'])
+
+            # previous line 2 is joined with quux() on line 4
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[0]).toEqual(value: 'quux', scopes: ['source.js'])
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
+
+            # previous line 3 is pushed down to become line 5
+            expect(tokenizedBuffer.lineForScreenRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 1, end: 2, delta: 2)
+
+          it "updates tokens for lines beyond the changed lines if needed", ->
+            buffer.insert([5, 30], '/* */')
+            changeHandler.reset()
+
+            buffer.insert([2, 0], '/*\nabcde\nabcder')
+            expect(tokenizedBuffer.lineForScreenRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
+            expect(tokenizedBuffer.lineForScreenRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(5).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(6).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(7).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
+            expect(tokenizedBuffer.lineForScreenRow(8).tokens[0].scopes).not.toBe ['source.js', 'comment.block.js']
+
+            expect(changeHandler).toHaveBeenCalled()
+            [event] = changeHandler.argsForCall[0]
+            delete event.bufferChange
+            expect(event).toEqual(start: 2, end: 5, delta: 2)
+
+      describe "when the change causes more than @chunkSize lines to be retokenized", ->
+        describe "when the buffer change itself is larger than the chunk size", ->
+
+        describe "when the buffer change is smaller than the chunk size, but the scope of rehighlighting is larger", ->
+
+        describe "when the end of rehighlighted region is outside the tokenized region", ->
+          it "extends the boundary of the tokenized region and resumes tokenizing after it", ->
+
+
+    describe "when there is a buffer change in the untokenized region", ->
+      it "updates the untokenized screen lines to match the buffer but does not tokenize the lines", ->

     describe "when the buffer contains tab characters", ->


add Log pref

add Log pref

--- app/src/preferences/preferences.coffee
+++ app/src/preferences/preferences.coffee
@@ -23,6 +23,7 @@
     currency: 'USD'
     miningFee: '10000'
     blockchainExplorer: 'https://blockchain.info/'
+    logState: true

   ###
     Language
@@ -103,4 +104,16 @@
       ledger.storage.sync.set({preferences_blockchainExplorer: value})
       @prefs.blockchainExplorer = value
     else
-      throw new Error 'You must initialized your wallet'+      throw new Error 'You must initialized your wallet'
+
+
+  ###
+    Logs
+  ###
+  getLogState: () ->
+    @prefs.logState
+
+  setLogState: (value) ->
+    if typeof value is 'boolean'
+      throw new Error 'Log state must be a boolean'
+    @prefs.logState = value


calling beacons-map

calling beacons-map

--- app/assets/javascripts/actions/beacons_map.js.coffee
+++ app/assets/javascripts/actions/beacons_map.js.coffee
@@ -14,8 +14,8 @@
       handleWith: 60
       onColor: 'primary'
       size: 'small'
-    if $('#beacons-map').length > 0
-      map = new MicelloMap('rrVDMQe2qjBRuJP5dXv59kAeK77SD7','beacons-map')
+
+    map = new MicelloMap('rrVDMQe2qjBRuJP5dXv59kAeK77SD7','beacons-map')

     paramsMapper = new ParamsMapper('.map-view')


We didn't need that click handler, we just had bugs with the root url.

--- app/assets/javascripts/backbone/views/edit.js.coffee
+++ app/assets/javascripts/backbone/views/edit.js.coffee
@@ -2,16 +2,10 @@

   events:
     "submit form": "save"
-    "click a": "click_link"

   initialize: ->
     @options or= {}
-
-
-  click_link: (e) ->
-    e.preventDefault()
-    @set_page $(e.target).attr('href')

   set_page: (page) ->


programmatically focus canvas when selected from list

--- app/assets/javascripts/jax/controller-list.js.coffee
+++ app/assets/javascripts/jax/controller-list.js.coffee
@@ -6,6 +6,7 @@
   div link name, ->
     window.jax or= new Jax.Context "webgl"
     window.jax.redirectTo name
+    document.getElementsByTagName("canvas")[0].focus()
     false

 $(document).ready ->


Loop through form inputs

Loop through form inputs

--- app/assets/javascripts/search_autocomplete.js.coffee
+++ app/assets/javascripts/search_autocomplete.js.coffee
@@ -28,7 +28,6 @@
     @groupInputEl = @getElement('#group_id')
     @searchCodeInputEl = @getElement('#search_code')
     @repositoryInputEl = @getElement('#repository_ref')
-    @scopeInputEl = @getElement('#scope')

     @saveOriginalState()

@@ -51,6 +50,7 @@
       group_id: @groupInputEl.val()
       search_code: @searchCodeInputEl.val()
       repository_ref: @repositoryInputEl.val()
+      scope: @scopeInputEl.val()

       # Location badge
       _location: $.trim(@locationText.text())
@@ -191,20 +191,17 @@
     @locationBadgeEl.children().length

   resetSearchState: ->
-    # Remove scope
-    @scopeInputEl.val('')
-
-    # Remove group
-    @groupInputEl.val('')
-
-    # Remove project id
-    @projectInputEl.val('')
-
-    # Remove code search
-    @searchCodeInputEl.val('')
-
-    # Remove repository ref
-    @repositoryInputEl.val('')
+    inputs = Object.keys @originalState
+
+    for input in inputs
+
+      # _location isnt a input
+      break if input is '_location'
+
+      # renamed to avoid tests to fail
+      if input is 'project_id' then input = 'search_project_id'
+
+      @getElement("##{input}").val('')

   removeLocationBadge: ->
     @locationBadgeEl.empty()


fix weird version optionality

fix weird version optionality

--- src/kite/kite.coffee
+++ src/kite/kite.coffee
@@ -113,7 +113,7 @@
       username        : "#{ @options.username ? 'anonymous' }"
       environment     : "#{ @options.environment ? 'browser' }"
       name            : "browser"
-      version         : "1.0.#{ @options.version ? '0' }"
+      version         : "#{ @options.version ? '1.0.0' }"
       region          : "browser"
       hostname        : "browser"
       id              : uniqueId


Tap-to-open only in omni-mode vomnibar.

Tap-to-open only in omni-mode vomnibar.

Do not use tab-to-open the vominibar in tabs mode.  (It doesn't make any
sense.)

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -126,9 +126,12 @@
     if (action == "dismiss")
       @hide()
     else if action in [ "tab", "down" ]
-      if @input.value.trim().length == 0 and action == "tab" and not @seenTabToOpenCompletionList
-        @seenTabToOpenCompletionList = true
-        @update true
+      if action == "tab" and
+        @completer.name == "omni" and
+        not @seenTabToOpenCompletionList and
+        @input.value.trim().length == 0
+          @seenTabToOpenCompletionList = true
+          @update true
       else
         @selection += 1
         @selection = @initialSelectionValue if @selection == @completions.length


adding hsl constructor

adding hsl constructor

--- test/color.coffee
+++ test/color.coffee
@@ -195,6 +195,11 @@
   ok alpha1.equals(alpha4)
   ok alpha1.equals(alpha5)

+test "should parse colors input as hsl", ->
+  color = Color("hsl(300, 0.3, 0.6)")
+
+  log color
+
 test "should output proper toString", ->
   color = Color(5, 25, 125, 0.73)

@@ -225,15 +230,6 @@
   equals hsl[1].toFixed(2), "0.48"
   equals hsl[2].toFixed(2), "0.33"
   equals hsl[3], 1
-
-test "#hslToRgb", ->
-  color = Color(50, 10, 100)
-  hsl = color.toHsl()
-  rgb = color.hslToRgb(hsl)
-
-  equals rgb.r(), color.r()
-  equals rgb.g(), color.g()
-  equals rgb.b(), color.b()

 test "#lighten", ->
   color = Color(20, 45, 123)


cleanup index face set stuff

cleanup index face set stuff

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -533,97 +533,59 @@

         @scene.add particle

-    add_obj: (myobj) ->
-        vertices = myobj.vertex_geometry
-        for objects in [0...myobj.face_geometry.length]
-            face3 = myobj.face_geometry[objects].face3
-            face4 = myobj.face_geometry[objects].face4
-            face5 = myobj.face_geometry[objects].face5
-
-            geometry = new THREE.Geometry()
-
-
-            for k in [0...vertices.length] by 3
-                geometry.vertices.push @vector vertices.slice k, k+3
-
-            push_face3 = (a, b, c) ->
+    add_index_face_set: (obj) ->
+        geometry = new THREE.Geometry()
+
+        for vertex in obj.vertex_geometry
+            geometry.vertices.push @vector vertices
+
+        for points in obj.face_geometry.faces
+            a = points.shift()
+            b = points.shift()
+            while c = points.shift()
                 geometry.faces.push new THREE.Face3 a-1, b-1, c-1
-
-            # include all faces defined by 3 vertices (triangles)
-            for k in [0...face3.length] by 3
-                push_face3 face3[k], face3[k+1], face3[k+2]
-
-            # include all faces defined by 4 vertices (squares), which for THREE.js we must define using two triangles
-            push_face4 = (a, b, c, d) ->
-                push_face3 a, b, c
-                push_face3 a, c, d
-
-            for k in [0...face4.length] by 4
-                push_face4 face4[k], face4[k+1], face4[k+2], face4[k+3]
-
-            # include all faces defined by 5 vertices (???), which for THREE.js we must define using ten triangles (?)
-            for k in [0...face5.length] by 5
-                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+4]
-                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+3]
-                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+4]
-                push_face4 face5[k],   face5[k+2], face5[k+3], face5[k+4]
-                push_face4 face5[k+1], face5[k+2], face5[k+3], face5[k+4]
-
-            geometry.mergeVertices()
-            #geometry.computeCentroids()
-            geometry.computeFaceNormals()
-            #geometry.computeVertexNormals()
-            geometry.computeBoundingSphere()
-
-            #finding material key(mk)
-            name = myobj.face_geometry[objects].material_name
-            mk = 0
-            for item in [0..myobj.material.length-1]
-                if name is myobj.material[item].name
-                    mk = item
-                    break
-
-            if @opts.wireframe or myobj.wireframe
-                if myobj.color
-                    color = myobj.color
-                else
-                    c = myobj.material[mk].color
-                    color = "rgb(#{c[0]*255},#{c[1]*255},#{c[2]*255})"
-                if typeof myobj.wireframe is 'number'
-                    line_width = myobj.wireframe
-                else if typeof @opts.wireframe is 'number'
-                    line_width = @opts.wireframe
-                else
-                    line_width = 1
-
-                material = new THREE.MeshBasicMaterial
-                    wireframe          : true
-                    color              : color
-                    wireframeLinewidth : line_width
-                    side               : THREE.DoubleSide
-            else if not myobj.material[mk]?
-                console.log "BUG -- couldn't get material for ", myobj
-                material = new THREE.MeshBasicMaterial
-                    wireframe : false
-                    color     : "#000000"
+                b = c
+
+        geometry.mergeVertices()
+        #geometry.computeCentroids()
+        geometry.computeFaceNormals()
+        #geometry.computeVertexNormals()
+        geometry.computeBoundingSphere()
+
+        if @opts.wireframe or obj.wireframe
+            if typeof obj.wireframe is 'number'
+                line_width = obj.wireframe
+            else if typeof @opts.wireframe is 'number'
+                line_width = @opts.wireframe
             else
-
-                m = myobj.material[mk]
-
-                material =  new THREE.MeshPhongMaterial
-                    shininess   : "1"
-                    ambient     : 0x0ffff
-                    wireframe   : false
-                    transparent : m.opacity < 1
-
-                material.color.setRGB    m.color[0],    m.color[1],    m.color[2]
-                material.ambient.setRGB  m.ambient[0],  m.ambient[1],  m.ambient[2]
-                material.specular.setRGB m.specular[0], m.specular[1], m.specular[2]
-                material.opacity = m.opacity
-
-            mesh = new THREE.Mesh geometry, material
-            mesh.position.set 0, 0, 0
-            @scene.add mesh
+                line_width = 1
+
+            material = new THREE.MeshBasicMaterial
+                wireframe          : true
+                wireframeLinewidth : line_width
+                side               : THREE.DoubleSide
+
+            material.color.setRGB obj.color...
+        else if not obj.material?
+            console.log "BUG -- couldn't get material for ", obj
+            material = new THREE.MeshBasicMaterial
+                wireframe : false
+                color     : "#000000"
+        else
+            material =  new THREE.MeshPhongMaterial
+                shininess   : "1"
+                ambient     : 0x0ffff
+                wireframe   : false
+                transparent : obj.material.opacity < 1
+
+            material.color.setRGB    obj.material.color...
+            material.ambient.setRGB  obj.material.ambient...
+            material.specular.setRGB obj.material.specular...
+            material.opacity = obj.material.opacity
+
+        mesh = new THREE.Mesh geometry, material
+        mesh.position.set 0, 0, 0
+        @scene.add mesh

     # always call this after adding things to the scene to make sure track
     # controls are sorted out, etc.   Set draw:false, if you don't want to
@@ -740,21 +702,17 @@
         for obj in opts.obj
             switch obj.type
                 when 'text'
-                    @add_text
-                        pos           : obj.pos
-                        text          : obj.text
-                        color         : obj.color
-                        fontsize      : obj.fontsize
-                        fontface      : obj.fontface
-                        constant_size : obj.constant_size
+                    delete obj.type
+                    @add_text obj
                 when 'index_face_set'
+                    delete obj.type
                     if opts.wireframe?
                         obj.wireframe = opts.wireframe
-                    @add_obj obj
+                    @add_index_face_set obj
                     if obj.mesh and not obj.wireframe  # draw a wireframe mesh on top of the surface we just drew.
-                        obj.color='#000000'
+                        obj.material.color = [0, 0, 0]
                         obj.wireframe = obj.mesh
-                        @add_obj obj
+                        @add_index_face_set obj
                 when 'line'
                     delete obj.type
                     @add_line obj
@@ -762,8 +720,7 @@
                     delete obj.type
                     @add_point obj
                 else
-                    console.log "ERROR: no renderer for model number #{obj.id}"
-                    return
+                    console.log "ERROR: bad object type #{obj.type}"

         if opts.set_frame?
             @set_frame opts.set_frame
@@ -877,12 +834,12 @@

         scene.opts.parent = opts.element

-        scene.opts.callback = (error, plotter) ->
+        scene.opts.callback = (error, sceneobj) ->
             if not error
                 if scene.obj?
-                    plotter.add_3dgraphics_obj obj : scene.obj
-                plotter.finalize()
-            opts.callback? error, plotter
+                    sceneobj.add_3dgraphics_obj obj : scene.obj
+                sceneobj.finalize()
+            opts.callback? error, sceneobj

         new Math3dThreeJS scene.opts


remove social from tests

remove social from tests

--- test/index.coffee
+++ test/index.coffee
@@ -3,8 +3,7 @@
 rewire = require 'rewire'
 app = require '../example'
 sinon = require 'sinon'
-{ ARTSY_EMAIL, ARTSY_PASSWORD, TWITTER_EMAIL, TWITTER_PASSWORD,
-  FACEBOOK_EMAIL, FACEBOOK_PASSWORD } = require '../config'
+{ ARENA_EMAIL, ARENA_PASSWORD } = require '../config'

 describe 'Artsy Passport integration', ->

@@ -19,34 +18,6 @@
         .pressButton "Login", ->
           browser.html().should.include '<h1>Hello'
           browser.html().should.include ARTSY_EMAIL
-          done()
-
-  it 'can log in with facebook', (done) ->
-    Browser.visit 'http://localhost:5000', (e, browser) ->
-      browser.clickLink "Login via Facebook", ->
-        browser.location.href.should.include 'facebook.com'
-        browser
-          .fill('email', FACEBOOK_EMAIL)
-          .fill('pass', FACEBOOK_PASSWORD)
-          .pressButton 'Log In', -> done()
-
-  it 'can log in with twitter', (done) ->
-    Browser.visit 'http://localhost:5000', (e, browser) ->
-      browser.clickLink "Login via Twitter", ->
-        browser.location.href.should.include 'twitter.com'
-        browser
-          .fill('session[username_or_email]', TWITTER_EMAIL)
-          .fill('session[password]', TWITTER_PASSWORD)
-          .pressButton 'Authorize app', -> done()
-
-  it 'can sign up with email and password', (done) ->
-    Browser.visit 'http://localhost:5000', (e, browser) ->
-      browser
-        .fill('name', 'Foobar')
-        .fill('.signup [name="email"]', ARTSY_EMAIL)
-        .fill('.signup [name="password"]', ARTSY_PASSWORD)
-        .pressButton "Signup", ->
-          browser.html().should.include 'Personalize Flow for'
           done()

 describe 'Artsy Passport methods', ->
@@ -66,98 +37,6 @@
         (user.foo?).should.not.be.ok
         (user.bam?).should.not.be.ok
         done()
-
-  describe '#accessTokenCallback', ->
-
-    before ->
-      opts = @artsyPassport.__get__ 'opts'
-      opts.CurrentUser = ->
-      @accessTokenCallback = @artsyPassport.__get__ 'accessTokenCallback'
-      @request = @artsyPassport.__get__ 'request'
-
-    it 'is okay if there is no response object', ->
-      @accessTokenCallback(->)({ bad: 'news' }, null)
-
-    it 'sends error messages in an error object', (done) ->
-      @accessTokenCallback((err) ->
-        err.should.include 'Epic Fail'
-        done()
-      )(null, { body: error_description: 'Epic Fail' })
-
-
-    context 'With a "no account linked" error', ->
-
-      beforeEach ->
-        sinon.stub @request, 'post'
-        end = sinon.stub()
-        @done = sinon.stub()
-        @request.post.returns send: => end: end
-        @accessTokenCallback(@done, { xapp_token: 'foobar' })(
-          null,
-          { body: error_description: 'no account linked' }
-        )
-        @end = end.args[0][0]
-
-      afterEach ->
-        @request.post.restore()
-
-      it 'creates a user', ->
-        @request.post.args[0][0].should.include '/api/v1/user'
-
-      it 'passes a custom error for our socialSignup callback to redirect to login', ->
-        @end null, body: { name: 'Craig' }
-        @done.args[0][0].should.equal 'artsy-passport: created user from social'
-
-  describe '#socialSignup', ->
-
-    before ->
-      opts = @artsyPassport.__get__ 'opts'
-      opts.CurrentUser = ->
-      @socialSignup = @artsyPassport.__get__ 'socialSignup'
-      @err = ''
-      @req = { query: {} }
-      @res = { redirect: sinon.stub() }
-      @next = sinon.stub()
-
-    it 'passes on unless the error is our custom error message', ->
-      @socialSignup('twitter')('Some other error', @req, @res, @next)
-      @next.called.should.be.ok
-
-    it 'strips out the expired auth query params before logging in', ->
-      @req.query.code = 'foo'
-      @req.query.oauth_token = 'bar'
-      @req.query.oauth_verifier = 'baz'
-      @socialSignup('twitter')('artsy-passport: created user from social', @req, @res, @next)
-      @res.redirect.args[0][0].should.not.include 'foo'
-      @res.redirect.args[0][0].should.not.include 'bar'
-      @res.redirect.args[0][0].should.not.include 'baz'
-
-    it 'redirects to log in', ->
-      @socialSignup('twitter')('artsy-passport: created user from social', @req, @res, @next)
-      @res.redirect.args[0][0].should.include '/users/auth/twitter'
-
-  describe '#submitTwitterLastStep', ->
-
-    before ->
-      opts = @artsyPassport.__get__ 'opts'
-      @submitTwitterLastStep = @artsyPassport.__get__ 'submitTwitterLastStep'
-      @request = @artsyPassport.__get__ 'request'
-      @req = { query: {}, user: { get: -> 'access-foo-token' } }
-      @res = { redirect: sinon.stub() }
-      @next = sinon.stub()
-
-    it 'creates a user', (done) ->
-      @request.put = (url) ->
-        url.should.include 'api/v1/me'
-        send: (data) ->
-          data.email.should.equal 'foo@bar.com'
-          data.email_confirmation.should.equal 'foo@bar.com'
-          done()
-          end: ->
-      @req.param = -> 'foo@bar.com'
-      @submitTwitterLastStep @req, @res, @next
-
-    it 'logs in the JSON from the PUT call'

   describe '#afterLocalAuth', ->


remove reset password method from user records interface

--- app/records-interfaces/user-records-interface.coffee
+++ app/records-interfaces/user-records-interface.coffee
@@ -7,6 +7,3 @@

     confirmAccount: (params)->
       @remote.post('confirm_account', params)
-
-    resetPassword: (params) ->
-      @remote.post('reset_password', params)


Add tracking of combo level concept completion

Everytime the course victory modal opens, track how many concept
goals there are and which ones are successful.

--- app/views/play/level/modal/CourseVictoryModal.coffee
+++ app/views/play/level/modal/CourseVictoryModal.coffee
@@ -58,7 +58,16 @@
         @course = new Course()
         @supermodel.trackRequest @course.fetchForCourseInstance(@courseInstanceID)

-    window.tracker?.trackEvent 'Play Level Victory Modal Loaded', category: 'Students', levelSlug: @level.get('slug'), []
+    properties = {
+      category: 'Students',
+      levelSlug: @level.get('slug')
+    }
+    concepts = @level.get('goals').filter((g) => g.concepts).map((g) => g.id)
+    if concepts.length
+      goalStates = @session.get('state').goalStates
+      succeededConcepts = concepts.filter((c) => goalStates[c]?.status is 'success')
+      _.assign(properties, {concepts, succeededConcepts})
+    window.tracker?.trackEvent 'Play Level Victory Modal Loaded', properties, []
     if @level.isProject()
       @galleryURL = urls.projectGallery({ @courseInstanceID })


Make url() and urlRoot() usage consistent.

--- src/models.coffee
+++ src/models.coffee
@@ -42,6 +42,7 @@
         return Backbone.$.ajax.call Backbone.$, request

     class FilterableCollection extends Backbone.Collection
+        urlRoot: -> BACKEND_BASE
         initialize: (options) ->
             @filters = {}
         setFilter: (key, val) ->
@@ -53,9 +54,13 @@
             @
         clearFilters: ->
             @filters = {}
+        hasFilters: ->
+            _.size(@filters) > 0
+        setFilters: (filterableCollection) ->
+            @filters = _.clone filterableCollection.filters
         url: ->
             obj = new @model
-            uri = URI "#{BACKEND_BASE}/#{obj.resourceName}/"
+            uri = URI "#{@urlRoot()}/#{obj.resourceName}/"
             uri.search @filters
             return uri.toString()

@@ -454,8 +459,6 @@
         origin: -> 'clicked'
         isPending: ->
             false
-        urlRoot: ->
-            "#{BACKEND_BASE}/#{@resourceName}"
         parse: (response, options) ->
             data = super response, options
             street = data.street
@@ -798,13 +801,10 @@
             uri.toString()

     class LinkedEventsModel extends SMModel
-        urlRoot: ->
-            return "#{LINKEDEVENTS_BASE}/#{@resourceName}/"
+        urlRoot: -> LINKEDEVENTS_BASE

     class LinkedEventsCollection extends SMCollection
-        url: ->
-            obj = new @model
-            return "#{LINKEDEVENTS_BASE}/#{obj.resourceName}/"
+        urlRoot: -> LINKEDEVENTS_BASE

         parse: (resp, options) ->
             @fetchState =
@@ -838,15 +838,10 @@
         sync: (method, model, options) ->
             _.defaults options, emulateJSON: true, data: extensions: true
             super method, model, options
-        resourceNamePlural: ->
-            "#{@resourceName}s"
-        urlRoot: ->
-            return "#{OPEN311_BASE}/#{@resourceNamePlural()}"
+        urlRoot: -> OPEN311_BASE

     class FeedbackItem extends Open311Model
-        resourceName: 'request'
-        url: ->
-            return "#{@urlRoot()}/#{@id}.json"
+        resourceName: 'requests.json'
         parse: (resp, options) ->
             if resp.length == 1
                 return super resp[0], options
@@ -856,6 +851,7 @@
         # incoming feedback

     class FeedbackList extends FilterableCollection
+        urlRoot: -> OPEN311_BASE
         fetch: (options) ->
             options = options or {}
             _.defaults options,
@@ -863,9 +859,6 @@
                 data: extensions: true
             super options
         model: FeedbackItem
-        url: ->
-            obj = new @model
-            return "#{OPEN311_BASE}/#{obj.resourceNamePlural()}.json"

     class FeedbackMessage extends SMModel
         # outgoing feedback


* clone default property value if the value is object when initializing + recreate argument to defineProperties, defaluts to false

--- src/normal.coffee
+++ src/normal.coffee
@@ -3,6 +3,7 @@
 isObject        = require 'util-ex/lib/is/type/object'
 isString        = require 'util-ex/lib/is/type/string'
 extend          = require 'util-ex/lib/_extend'
+cloneObject     = require 'util-ex/lib/clone-object'
 getPrototypeOf  = require 'inherits-ex/lib/getPrototypeOf'
 PropertyManager = require './abstract'
 getkeys         = Object.keys
@@ -59,10 +60,13 @@
         vAttrs[k]= v
     vAttrs

-  defineProperties: (aProperties) ->
-    vAttrs = defineObjectProperties @, aProperties, false
+  defineProperties: (aProperties, recreate = false) ->
+    vAttrs = defineObjectProperties @, aProperties, recreate
     for k, v of vAttrs
-      defineProperty @, k, v.value, v
+      value = v.value
+      if !v.get and !v.set and v.clone isnt false and isObject(value)
+        value = cloneObject value
+      defineProperty @, k, value, v
     return

   assignPropertyTo: (dest, src, name, value, attrs, skipDefaultValue, isExported)->


working on permalink issues.

working on permalink issues.

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -496,7 +496,6 @@

   eventFillCartFromQueryString: ->
     query = window.location.search.substring(1)
-    console.log query
     if query.length > 0
       # ?0_order=world_none%2Ceye_120hz_binocular%2Clicense_commercial&0_qty=3&1_order=world_hr%2Ceye_120hz_binocular%2Clicense_commercial&1_qty=2&2_order=product_support_6&2_qty=1
       LocalStorage.clear()
@@ -517,8 +516,7 @@
   eventOrderLinkSuccessPage: =>
     if $("#Success").length > 0
       data = @_getOrderPermalink()
-      console.log data
-      url = window.location.origin + "/cart?" + $.param(data)
+      url = window.location.origin + "/cart/?" + $.param(data)
       link = "permalink"
       html = "
You can always revisit your order with this #{link}.
"
       $(".Banner-item.u-textCenter").append(html)


Webterm: move session id to to front

Webterm: move session id to to front

--- client/Terminal/webtermview.coffee
+++ client/Terminal/webtermview.coffee
@@ -90,7 +90,7 @@
       sizeX       : @terminal.sizeX
       sizeY       : @terminal.sizeY
       joinUser    : myOptions.joinUser  ? delegateOptions.joinUser
-      session     : myOptions.session   ? delegateOptions.session ? @sessionId
+      session     : @sessionId ? myOptions.session ? delegateOptions.session
       mode        : myOptions.mode      ? 'create'

   getVMName:->


Oh gaaawwd, really fix the dependencies

Oh gaaawwd, really fix the dependencies

--- assets/scripts/app/models/build.coffee
+++ assets/scripts/app/models/build.coffee
@@ -47,8 +47,8 @@

   finishedAt: (->
     unless @get('notStarted')
-      @get('_finishedAt', 'notStarted')
-  ).property('_finishedAt')
+      @get('_finishedAt')
+  ).property('_finishedAt', 'notStarted')

   requiredJobs: (->
     @get('jobs').filter (data) -> !data.get('allowFailure')


Support unpublishing a specific package version

--- src/unpublish.coffee
+++ src/unpublish.coffee
@@ -17,16 +17,22 @@

     options.usage """
       Usage: apm unpublish [<package_name>]
+             apm unpublish <package_name>@<package_version>

-      Remove a published package from the atom.io registry. The package in the
-      current working directory will be unpublished if no package name is
-      specified.
+      Remove a published package from the atom.io registry or unpublish a given
+      version of a package.
+
+      The package in the current working directory will be used if no package
+      name is specified.
     """
     options.alias('h', 'help').describe('help', 'Print this usage message')
     options.alias('f', 'force').boolean('force').describe('force', 'Do not prompt for confirmation')

-  unpublishPackage: (packageName, callback) ->
-    process.stdout.write "Unpublishing #{packageName} "
+  unpublishPackage: (packageName, packageVersion, callback) ->
+    packageLabel = packageName
+    packageLabel += "@#{packageVersion}" if packageVersion
+
+    process.stdout.write "Unpublishing #{packageLabel} "

     auth.getToken (error, token) =>
       if error?
@@ -40,6 +46,8 @@
           authorization: token
         json: true

+      options.uri += "/versions/#{packageVersion}" if packageVersion
+
       request.del options, (error, response, body={}) =>
         if error?
           @logFailure()
@@ -52,25 +60,37 @@
           @logSuccess()
           callback()

-  promptForConfirmation: (packageName, callback) ->
+  promptForConfirmation: (packageName, packageVersion, callback) ->
     prompt = readline.createInterface(process.stdin, process.stdout)
-    prompt.question "Are you sure you want to unpublish #{packageName}? (yes) ", (answer) =>
+
+    packageLabel = packageName
+    packageLabel += "@#{packageVersion}" if packageVersion
+
+    prompt.question "Are you sure you want to unpublish #{packageLabel}? (yes) ", (answer) =>
       prompt.close()
       answer = if answer then answer.trim().toLowerCase() else 'yes'
       if answer is 'y' or answer is 'yes'
-        @unpublishPackage(packageName, callback)
+        @unpublishPackage(packageName, packageVersion, callback)

   run: (options) ->
     {callback} = options
     options = @parseOptions(options.commandArgs)

     [name] = options.argv._
-    unless name?
+
+    atIndex = name?.indexOf('@')
+    if atIndex > 0
+      version = name.substring(atIndex + 1)
+      name = name.substring(0, atIndex)
+
+    unless name
       try
-        {name} = JSON.parse(fs.readFileSync('package.json')) ? {}
-      name ?= path.basename(process.cwd())
+        name = JSON.parse(fs.readFileSync('package.json'))?.name
+
+    unless name
+      name = path.basename(process.cwd())

     if options.argv.force
-      @unpublishPackage(name, callback)
+      @unpublishPackage(name, version, callback)
     else
-      @promptForConfirmation(name, callback)
+      @promptForConfirmation(name, version, callback)


Changed msg to res

Changed msg to res

--- src/wikipedia.coffee
+++ src/wikipedia.coffee
@@ -18,34 +18,34 @@
 WIKI_EN_URL = "https://en.wikipedia.org/wiki"

 module.exports = (robot) ->
-    robot.respond /wiki search (.+)/i, id: "wikipedia.search", (msg) ->
+    robot.respond /wiki search (.+)/i, id: "wikipedia.search", (res) ->
         params =
             action: 'opensearch'
             format: 'json'
             limit: 5
-            search: msg.match[1]
+            search: res.match[1]

-        wikiRequest msg, params, (object) ->
+        wikiRequest res, params, (object) ->
             if object[1].length is 0
-                msg.reply "No articles were found using search query: \"#{msg.match[1]}\". Try a different query."
+                res.reply "No articles were found using search query: \"#{res.match[1]}\". Try a different query."
                 return

             for article in object[1]
-                msg.send "#{article}: #{createURL(article)}"
+                res.send "#{article}: #{createURL(article)}"

-    robot.respond /wiki summary (.+)/i, id: "wikipedia.summary", (msg) ->
+    robot.respond /wiki summary (.+)/i, id: "wikipedia.summary", (res) ->
         params =
             action: 'query'
             exintro: true
             explaintext: true
             format: 'json'
             prop: 'extracts'
-            titles: msg.match[1]
+            titles: res.match[1]

-        wikiRequest msg, params, (object) ->
+        wikiRequest res, params, (object) ->
             for id, article of object.query.pages
                 if id is -1
-                    msg.reply "The article you have entered (\"#{msg.match[1]}\") does not exist. Try a different article."
+                    res.reply "The article you have entered (\"#{res.match[1]}\") does not exist. Try a different article."
                     return

                 if article.extract is ""
@@ -53,19 +53,19 @@
                 else
                     summary = article.extract.split(". ")[0..1].join ". "

-                msg.send "#{article.title}: #{summary}."
-                msg.reply "Original article: #{createURL(article.title)}"
+                res.send "#{article.title}: #{summary}."
+                res.reply "Original article: #{createURL(article.title)}"
                 return

 createURL = (title) ->
     "#{WIKI_EN_URL}/#{encodeURIComponent(title)}"

-wikiRequest = (msg, params = {}, handler) ->
-    msg.http(WIKI_API_URL)
+wikiRequest = (res, params = {}, handler) ->
+    res.http(WIKI_API_URL)
         .query(params)
-        .get() (err, res, body) ->
+        .get() (err, httpRes, body) ->
             if err
-                msg.reply "An error occurred while attempting to process your request: #{err}"
+                res.reply "An error occurred while attempting to process your request: #{err}"
                 return robot.logger.error err

             handler JSON.parse(body)


Remove duplicate subscription to added pane items in Workspace

--- src/workspace.coffee
+++ src/workspace.coffee
@@ -84,7 +84,6 @@
     @openers = []
     @destroyedItemURIs = []
     @consumeServices(@packageManager)
-    @subscribeToAddedItems()

   subscribeToEvents: ->
     @subscribeToActiveItem()


FIX leader exit

FIX leader exit

--- src/application/webserver/routes/index.coffee
+++ src/application/webserver/routes/index.coffee
@@ -183,7 +183,7 @@
   MConnZookeeperHandler.getMasterData().then (masterdata) ->
     request = require("request")
     console.log masterdata
-    request(masterdata.serverdata.serverurl + "/v1/exit")
+    request(masterdata.serverdata.serverurl + "/v1/exit/node")
     res.end()

 # GET /v1/exit/node -> Kill the requested instance


use the protocol requested

use the protocol requested

--- src/controllers/credentials-controller.coffee
+++ src/controllers/credentials-controller.coffee
@@ -22,10 +22,11 @@

       editUrl = url.format
         host: "#{@deviceEditorUrl}/#{device.uuid}"
+        protocol: req.headers['x-forwarded-proto'] || req.protocol
         query:
           token: device.token
           protocol: req.meshbluAuth.protocol
-          hostname: req.meshbluAuth.host
+          hostname: req.meshbluAuth.hostname
           port: req.meshbluAuth.port
           callbackURL: "#{@serviceUrl}/device/configured"


Disable feedback package

Disable feedback package

--- atom/config.cson
+++ atom/config.cson
@@ -29,6 +29,9 @@
     'themes': [
       'soda-light-ui'
       'solarized-light-syntax'
+    ]
+    'disabledPackages': [
+      'feedback'
     ]
   'auto-copyright':
     'buffer': 1


Atom - Scroll past the end

Atom - Scroll past the end

--- atom/config.cson
+++ atom/config.cson
@@ -20,6 +20,7 @@
       "atom-dark-syntax"
     ]
   editor:
+    scrollPastEnd: true
     tabLength: 4
   "exception-reporting":
     userId: "82a26126-5c31-4531-b32c-a1877d4677e0"


More re-ordering tweaks

More re-ordering tweaks

--- atom/config.cson
+++ atom/config.cson
@@ -3,7 +3,6 @@
     enabled: true
   core:
     audioBeep: false
-    ]
     projectHome: "/Users/randy/src"
   editor:
     fontFamily: "Hack"
@@ -17,14 +16,15 @@
   "expand-region": {}
   "git-blame": {}
   "git-diff":
-    showIconsInEditorGutter: true
     rspecAllCommand: "bundle exec rspec --tty spec"
     rspecFileCommand: "bundle exec rspec --tty {relative_path}"
     rspecSingleCommand: "bundle exec rspec --tty {relative_path}:{line_number}"
+    showIconsInEditorGutter: true
   "git-plus": {}
   linter:
     errorPanelHeight: 26
   "linter-eslint": {}
+  "linter-rubocop": {}
   "linter-sass-lint":
     noConfigDisable: true
   "markdown-deluxe":


Set preferred window dimensions on Atom init

--- atom/init.coffee
+++ atom/init.coffee
@@ -25,6 +25,18 @@
   workspaceView = atom.views.getView(atom.workspace)
   atom.commands.dispatch(workspaceView, 'package-sync:sync')

+# Set preferred size of current Atom window while preserving window position;
+# assumes Tree View is open
+setPreferredWindowDimensions = ->
+  originalWindowDimensions = atom.getWindowDimensions()
+  atom.setWindowDimensions({
+    width: 1060,
+    height: 710,
+    x: originalWindowDimensions.x,
+    y: originalWindowDimensions.y
+  })
+
+setPreferredWindowDimensions()
 # Explicitly define PATH and detect project virtualenvs
 process.env.PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
 if process.env.VIRTUAL_ENV_NAME


FIX: Correct student group filter to limit when in group

--- src/app/common/filters/filters.coffee
+++ src/app/common/filters/filters.coffee
@@ -201,9 +201,10 @@
   (input, project, groupSet) ->
     # Filter by tutorial if keep groups in same class
     return unless input? && groupSet? && project?
+    grp = project.groupForGroupSet(groupSet)
+    return [grp] if grp
     return input unless groupSet.keep_groups_in_same_class
-    if groupSet.keep_groups_in_same_class
-      _.filter(input, (group) -> project.isEnrolledIn(group.tutorial_id))
+    _.filter(input, (group) -> project.isEnrolledIn(group.tutorial_id))
 )

 .filter('paginateAndSort', ($filter) ->


Updates

Updates

--- src/bio/neural/backPropagation.coffee
+++ src/bio/neural/backPropagation.coffee
@@ -19,18 +19,9 @@

 transferDerivative = (o) -> o * (1.0 - o)

-# def forward_propagate(net, vector)
-#   net.each_with_index do |layer, i|
-#     input=(i==0)? vector : Array.new(net[i-1].size){|k|net[i-1][k][:output]}
-#     layer.each do |neuron|
-#       neuron[:activation] = activate(neuron[:weights], input)
-#       neuron[:output] = transfer(neuron[:activation])
-#     end
-#   end
-#   return net.last[0][:output]
-# end
 forwardPropagate = (n, v) ->
     n.forEach (l, i) ->
+        #     input=(i==0)? vector : Array.new(net[i-1].size){|k|net[i-1][k][:output]}
         inp = if i is 0 then vector else
         l.forEach (ne) ->
             ne.activation = activate(ne.weights, inp)
@@ -52,52 +43,26 @@
                         p += nne.weights[j] * nne.delta
                     , 0

+calculateErrorDerivativesForWeights = (n, v) ->
+    n.forEach (l, i) ->
+        #     input=(i==0)? vector : Array.new(net[i-1].size){|k|net[i-1][k][:output]}
+        inp = if i == 0 then vector else
+        l.forEach (ne) ->
+            inp.forEach (s, j) ->
+                ne.deriv[j] += ne.delta * s
+            ne.deriv[ne.deriv.length - 1] += ne.delta

+updateWeights = (n, lr, m) ->
+    n.forEach (l) ->
+        l.forEach (ne) ->
+            ne.weights.forEach (w, i) ->
+                d = lr * ne.deriv[i] + ne.ldelta[i] * m
+                n.weights[i] += d
+                n.ldelta[i] = d
+                n.deriv[i] = 0.0

-# def backward_propagate_error(network, expected_output)
-#   network.size.times do |n|
-#     index = network.size - 1 - n
-#     if index == network.size-1
-#       neuron = network[index][0] # assume one node in output layer
-#       error = (expected_output - neuron[:output])
-#       neuron[:delta] = error * transfer_derivative(neuron[:output])
-#     else
-#       network[index].each_with_index do |neuron, k|
-#         sum = 0.0
-#         # only sum errors weighted by connection to the current k'th neuron
-#         network[index+1].each do |next_neuron|
-#           sum += (next_neuron[:weights][k] * next_neuron[:delta])
-#         end
-#         neuron[:delta] = sum * transfer_derivative(neuron[:output])
-#       end
-#     end
-#   end
-# end
+trainNetwork = (n, d, ni, i, lr) ->

-# def calculate_error_derivatives_for_weights(net, vector)
-#   net.each_with_index do |layer, i|
-#     input=(i==0)? vector : Array.new(net[i-1].size){|k|net[i-1][k][:output]}
-#     layer.each do |neuron|
-#       input.each_with_index do |signal, j|
-#         neuron[:deriv][j] += neuron[:delta] * signal
-#       end
-#       neuron[:deriv][-1] += neuron[:delta] * 1.0
-#     end
-#   end
-# end
-
-# def update_weights(network, lrate, mom=0.8)
-#   network.each do |layer|
-#     layer.each do |neuron|
-#       neuron[:weights].each_with_index do |w, j|
-#         delta = (lrate * neuron[:deriv][j]) + (neuron[:last_delta][j] * mom)
-#         neuron[:weights][j] += delta
-#         neuron[:last_delta][j] = delta
-#         neuron[:deriv][j] = 0.0
-#       end
-#     end
-#   end
-# end

 # def train_network(network, domain, num_inputs, iterations, lrate)
 #   correct = 0


changing db.coffee

changing db.coffee

--- db.coffee
+++ db.coffee
@@ -13,6 +13,10 @@
 	]
 	starsEarned: Number
 	followers: Number
+	location: String
+	following: Number
+	name: String
+	hireable: Boolean

 exports.Repository = mongoose.model "Repository",
 	fullName: type: String, index: true


Indentation

Indentation

--- src/coffee/bootstrap.offcanvas.coffee
+++ src/coffee/bootstrap.offcanvas.coffee
@@ -15,9 +15,7 @@

             @nav.closest('.navbar-offcanvas').on 'click', =>
                 if @dropdown.is('.shown')
-                    @dropdown.removeClass('shown')
-                      .closest('.active')
-                      .removeClass('active')
+                    @dropdown.removeClass('shown').closest('.active').removeClass('active')

         #   Private: Click event on link
         _clickEvent: (e) =>


[Autosuggest] Guard against missing stitch when rehydrating mobile

--- src/mobile/components/layout/bootstrap.coffee
+++ src/mobile/components/layout/bootstrap.coffee
@@ -47,11 +47,10 @@

   # Setup jQuery plugins
   require 'jquery-on-infinite-scroll'
-  mountStitch()
+  if sd.stitch?.renderQueue?
+    mountStitch()

 mountStitch = ->
-  globalReactModules = require('../../../desktop/lib/global_react_modules.tsx')
-  hydrateStitch = require('@artsy/stitch/dist/internal/hydrate').hydrate
   hydrateStitch({
     sharifyData: sd
     modules: globalReactModules


EagerRuntime is no longer chatty

EagerRuntime is no longer chatty

--- src/eager-runtime/EagerRuntime.coffee
+++ src/eager-runtime/EagerRuntime.coffee
@@ -119,21 +119,20 @@
 	dispatch = (hash, destination) =>
 		currentChar = @playfield.getAt destination.x, destination.y
 		partial = getPointer.bind null, destination, @playfield.getSize()
-		if currentChar == '_'
-			buildEdge hash, partial '<'
-			buildEdge hash, partial '>'
-		else if currentChar == '|'
-			buildEdge hash, partial '^'
-			buildEdge hash, partial 'v'
-		else if currentChar == '?'
-			buildEdge hash, partial '^'
-			buildEdge hash, partial 'v'
-			buildEdge hash, partial '<'
-			buildEdge hash, partial '>'
-		else if currentChar == '@'
-			console.log 'exit'
-		else
-			console.log "unknown char #{currentChar}"
+
+		switch currentChar
+			when '_'
+				buildEdge hash, partial '<'
+				buildEdge hash, partial '>'
+			when '|'
+				buildEdge hash, partial '^'
+				buildEdge hash, partial 'v'
+			when '?'
+				buildEdge hash, partial '^'
+				buildEdge hash, partial 'v'
+				buildEdge hash, partial '<'
+				buildEdge hash, partial '>'
+
 		return


binary search (coffee script)

binary search (coffee script)

--- src/main/coffeescript/bsearch0.coffee
+++ src/main/coffeescript/bsearch0.coffee
@@ -1,10 +1,37 @@
 describe("binary search", ->
   it("can find index of element in array", ->
     expect(indexOf(1, [])).toEqual(-1)
+
+    expect(indexOf(0, [1])).toEqual(-1)
+    expect(indexOf(1, [1])).toEqual(0)
+    expect(indexOf(2, [1])).toEqual(-1)
+
+    expect(indexOf(0, [1, 2])).toEqual(-1)
+    expect(indexOf(1, [1, 2])).toEqual(0)
+    expect(indexOf(2, [1, 2])).toEqual(1)
+    expect(indexOf(3, [1, 2])).toEqual(-1)
+
+    expect(indexOf(0, [1, 2, 3])).toEqual(-1)
+    expect(indexOf(1, [1, 2, 3])).toEqual(0)
+    expect(indexOf(2, [1, 2, 3])).toEqual(1)
+    expect(indexOf(3, [1, 2, 3])).toEqual(2)
+    expect(indexOf(4, [1, 2, 3])).toEqual(-1)
+
+    expect(indexOf(0, [1, 2, 3, 4])).toEqual(-1)
+    expect(indexOf(1, [1, 2, 3, 4])).toEqual(0)
+    expect(indexOf(2, [1, 2, 3, 4])).toEqual(1)
+    expect(indexOf(3, [1, 2, 3, 4])).toEqual(2)
+    expect(indexOf(4, [1, 2, 3, 4])).toEqual(3)
+    expect(indexOf(5, [1, 2, 3, 4])).toEqual(-1)
   )
 )

-indexOf = ->
-  -1
+indexOf = (value, array, shift = 0) ->
+  if array.length == 0 then return -1

-number = 42+  midIndex = Math.floor(array.length / 2)
+  midValue = array[midIndex]
+
+  if value == midValue then midIndex + shift
+  else if value < midValue then indexOf(value, array[0...midIndex], shift)
+  else indexOf(value, array[(midIndex + 1)..], shift + midIndex + 1)


Remove redundant Spree api key

Remove redundant Spree api key

--- backend/app/assets/javascripts/spree/backend/line_items.js.coffee
+++ backend/app/assets/javascripts/spree/backend/line_items.js.coffee
@@ -40,7 +40,6 @@
     data:
       line_item:
         quantity: quantity
-      token: Spree.api_key
   ).done (msg) ->
     window.Spree.advanceOrder()


remove the admin links

remove the admin links

--- client/app/MainApp/avatararea/avatarareagroupswitcherpopup.coffee
+++ client/app/MainApp/avatararea/avatarareagroupswitcherpopup.coffee
@@ -12,7 +12,6 @@
       startWithLazyLoader : yes

     @listController.on "AvatarPopupShouldBeHidden", @bound 'hide'
-

     @avatarPopupContent.addSubView switchToTitle = new KDView
       height   : "auto"
@@ -83,20 +82,7 @@
           title   : "Opens in a new browser window."
           delayIn : 300

-  viewAppended:->
-    JView::viewAppended.call this
-
-    {group:{slug}, roles} = @getData()
-
-    dashboardHref = "/#{slug}/Dashboard"
-
-    if 'admin' in roles
-      @addSubView new CustomLinkView
-        title     : 'Admin dashboard'
-        href      : dashboardHref
-        click     : (event)->
-          event.preventDefault()
-          KD.getSingleton('router').handleRoute dashboardHref
+  viewAppended: JView::viewAppended

   pistachio: ->
     {roles} = @getData()


Better calculation of minutes to milliseconds for timeout; Smaller message to tell user to ignore expected http 404 error

--- SingularityUI/app/views/TaskPoller.coffee
+++ SingularityUI/app/views/TaskPoller.coffee
@@ -10,13 +10,13 @@
 Utils = require '../utils'

 vex = require 'vex.dialog'
+moment = require 'moment'

 interval = (a, b) -> setInterval(b, a)  # f u javascript
 timeout = (a, b) -> setTimeout(b, a)

-TIMEOUT_MINUTES = 1 # Modify this
-TIMEOUT_SECONDS = TIMEOUT_MINUTES * 60 # Don't modify this (unless you want a timeout less than a minute)
-TIMEOUT_MILLISECONDS = TIMEOUT_SECONDS * 1000 # Don't modify this
+TIMEOUT_MINUTES = 1
+TIMEOUT_MILLISECONDS = moment.duration(TIMEOUT_MINUTES, 'minutes').asMilliseconds()

 POLLING_TYPES = ['autoTail', 'browse-to-sandbox']

@@ -92,13 +92,8 @@
     fetchTask: =>
         taskFetch = @task.fetch()
         taskFetch.error (error) ->
-            Utils.ignore404 error
-            if error.status is 404
-                console.log '''
-                    The task poller has not found the task yet.
-                    Unfortunatley, some browsers automatically log all http errors.
-                    You may safely ignore the above 404.
-                '''
+            Utils.ignore404 error # 404 is expected unless the task is in a terminal state
+            console.log 'The above 404 was expected and may safely be ignored.' if error.status is 404
         taskFetch.success @taskFound

     browseToSandbox: =>


IE8 JS fix for string `trim()`

IE8 JS fix for string `trim()`

--- app/assets/javascripts/admin/feedback.js.coffee
+++ app/assets/javascripts/admin/feedback.js.coffee
@@ -1,7 +1,7 @@
 jQuery ->
   # Truncate the feedback
   $(".feedback-description").each ->
-    full_text = $(this).text().trim()
+    full_text = $.trim($(this).text())
     if full_text.length >= 200
       short_text = full_text.substr(0, 200)
       short_text_div = "
#{short_text}...
"


Fix issue where multiple tags are not decoded properly when editing an existing preset. Fixes #22.

--- app/assets/javascripts/device_filters.js.coffee
+++ app/assets/javascripts/device_filters.js.coffee
@@ -35,16 +35,22 @@

 window.initFilters = (id) ->
   f = $("##{id}")
-  query = JSON.parse(f.children('input[name=query]').attr('value'))
   container_selector = "##{id} > .filters_container"
   container = $(container_selector)

-  for q,v of query
-    if v instanceof Object
-      for op,v2 of v
-        addFilter(container, q, op, v2, false)
-    else
-      addFilter(container, q, '', v, false)
+  recursive = (query) ->
+    for q,v of query
+      if q[0] == '$' # this is an operator
+        if q != '$and' # only $and operator is supported
+          throw new Error("The logical operator #{q} is not supported in precondition query.")
+        recursive(v2) for v2 in v
+      else if v instanceof Object
+        for op,v2 of v
+          addFilter(container, q, op, v2, false)
+      else
+        addFilter(container, q, '', v, false)
+
+  recursive(JSON.parse(f.children('input[name=query]').attr('value')))

   popup = """
      +


removing buggy tap events form product form

--- app/assets/javascripts/mobile/_mobile.js.coffee
+++ app/assets/javascripts/mobile/_mobile.js.coffee
@@ -238,14 +238,10 @@
 if gon.page is 'products_new' or gon.page is 'products_create' or gon.page is 'products_edit' or gon.page is 'products_update'
     $ ->
         ($ '.mobile-form')
-            .on 'tap', '.add-button', (e) ->
-                return
             .on 'click', '.add-button', (e) ->
+                # tap submits the form on iOS
                 e.preventDefault()
                 render_nested_product_attribute_input ($ @)
-
-            .on 'tap', '.remove-attribute', (e) ->
-                return
             .on 'click', '.attribute-wrapper', (e) ->
                 id = ($ @).find('.remove-attribute').data('id')
                 attribute = ($ @).find('.remove-attribute').data('attribute')


remove js debug messages

remove js debug messages

--- app/assets/javascripts/modules/analytics.coffee
+++ app/assets/javascripts/modules/analytics.coffee
@@ -7,6 +7,4 @@
         metric1: place.formatted_address
       )
   pageView: ->
-    console.log 'sending pageview'
-    console.log location.href
     ga?('send', 'pageview')


fixed bug in lengthTracker method

fixed bug in lengthTracker method

--- app/coffee/drm-validation/drm-validation.coffee
+++ app/coffee/drm-validation/drm-validation.coffee
@@ -58,7 +58,8 @@
             that = $ @
             value = $.trim that.val()
             length = value.length
-            lengthNotice = $ ".form-length-notice"
+            lengthNotice = that.nextUntil(':input', '.form-length-notice')
+
             createMessage = (length) ->
                 message = if length == 1 then "#{length} character" else "#{length} characters"
                 return message


Minor improvements on project profile controller.

--- app/coffee/modules/admin/project-profile.coffee
+++ app/coffee/modules/admin/project-profile.coffee
@@ -53,11 +53,14 @@

         promise = @.loadInitialData()

-        promise.then () =>
+        promise.then =>
             @appTitle.set("Project profile - " + @scope.sectionName + " - " + @scope.project.name)

-        promise.then null, ->
-            console.log "FAIL" #TODO
+        promise.then null, (xhr) =>
+            if xhr and xhr.status == 404
+                @location.path("/not-found")
+                @location.replace()
+            return @q.reject(xhr)

     loadProject: ->
         return @rs.projects.get(@scope.projectId).then (project) =>
@@ -76,10 +79,6 @@
         promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
             @scope.projectId = data.project
             return data
-
-        promise.then null, =>
-            @location.path("/not-found")
-            @location.replace()

         return promise.then(=> @.loadProject())


fix budgets overview controller based on previous change

--- app/scripts/controllers/budgets/overview.coffee
+++ app/scripts/controllers/budgets/overview.coffee
@@ -1,4 +1,4 @@
 `// @ngInject`
 window.Cobudget.Controllers.BudgetOverview = ($scope, $rootScope, BudgetLoader) ->
   BudgetLoader.init($rootScope)
-  BudgetLoader.loadFromURL()
+  BudgetLoader.loadAll()


Use derived width and height for all node shapes.

--- app/scripts/viewmodels/drawings/elements.coffee
+++ app/scripts/viewmodels/drawings/elements.coffee
@@ -20,18 +20,18 @@
       path

     createPath: (options, node) =>
-      width = @getOption(options.size.width, node, 100)
-      height = @getOption(options.size.height, node, 100)
+      width = @getOption(options.size.width, node, 100) if options.size
+      height = @getOption(options.size.height, node, 100) if options.size

       switch options.figure
         when "rounded"
           rect = new paper.Rectangle(0, 0, width, height)
           new paper.Path.RoundRectangle(rect, new paper.Size(10, 10))
         when "ellipse"
-          rect = new paper.Rectangle(0, 0, options.size.width*2, options.size.height*2)
+          rect = new paper.Rectangle(0, 0, width*2, height*2)
           new paper.Path.Oval(rect)
         when "rectangle"
-          new paper.Path.Rectangle(0, 0, options.size.width, options.size.height)
+          new paper.Path.Rectangle(0, 0, width, height)
         when "polygon"
           options.sides or= 3
           options.radius or= 50


Cloud Functions 0.0.2

Cloud Functions 0.0.2

--- build/tasks/install-unpublished-packages.coffee
+++ build/tasks/install-unpublished-packages.coffee
@@ -36,7 +36,7 @@
         installPackage 'spark', 'release-notes-spark', '0.36.0', ->
           installPackage 'spark', 'language-spark', '0.3.0', ->
             installPackage 'spark', 'exception-reporting', '0.20.0', ->
-              installPackage 'spark', 'spark-dev-cloud-functions', '0.0.1', ->
+              installPackage 'spark', 'spark-dev-cloud-functions', '0.0.2', ->
                 installPackage 'spark', 'spark-dev-cloud-variables', '0.0.1', ->
                   installPackage 'spark', 'metrics', '0.40.0', ->
                     done()


Closes #20. Setting menupopup's attribute  to true seems to fix the issue

--- extension/packages/button.coffee
+++ extension/packages/button.coffee
@@ -93,6 +93,7 @@

   menupopup = doc.createElement 'menupopup'
   menupopup.id = MENUPOPUP_ID
+  menupopup.setAttribute 'ignorekeys', true
   menupopup.appendChild hbox
   menupopup.appendChild itemPreferences
   menupopup.appendChild itemHelp


Enable a way of unbinding listeners

Enable a way of unbinding listeners

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -26,15 +26,24 @@
   # routines will also listen for changes on the element to propagate them back
   # to the model.
   bind: =>
-    Rivets.config.adapter.subscribe @model, @keypath, (value) => @set value
+    Rivets.config.adapter.subscribe @model, @keypath, @set

     if Rivets.config.preloadData
       @set Rivets.config.adapter.read @model, @keypath

     if @type in bidirectionals
-      @el.addEventListener 'change', (e) =>
-        el = e.target or e.srcElement
-        Rivets.config.adapter.publish @model, @keypath, getInputValue el
+      @el.addEventListener('change', @publish)
+
+  publish: (e) =>
+    el = e.target or e.srcElement
+    Rivets.config.adapter.publish @model, @keypath, getInputValue el
+
+  # Unsubscribes from the model and the element
+  unbind: =>
+    Rivets.config.adapter.unsubscribe @model, @keypath, @set
+
+    if @type in bidirectionals
+      @el.removeEventListener('change', @publish)

 # A collection of bindings for a parent element.
 class Rivets.View
@@ -144,4 +153,4 @@
 if module?
   module.exports = rivets
 else
-  @rivets = rivets+  @rivets = rivets


The event listener receives an Event object, not the element from which the event originated.

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -31,7 +31,8 @@
     Rivets.config.adapter.subscribe @context, @keypath, (value) => @set value

     if @type in bidirectionals
-      @el.addEventListener 'change', (el) =>
+      @el.addEventListener 'change', (e) =>
+        el = e.target or e.srcElement
         Rivets.config.adapter.publish @context, @keypath, getInputValue el

 # Parses and stores the binding data for an entire view binding.


return commits in their requested order

return commits in their requested order

--- lib/argumenta/storage/postgres_store.coffee
+++ lib/argumenta/storage/postgres_store.coffee
@@ -236,13 +236,13 @@
   # Add a commit to the store.
   # @api public
   addCommit: (commit, callback) ->
-    @session (err, session) ->
-      return callback err if err
-
-      selectQuery = Queries.selectCommitBySha1(commit.sha1())
-      session.query selectQuery, (err, result) ->
-        return callback err if err
-        return callback null if result.rows.length > 0
+    sha1 = commit.sha1()
+    @getCommits [sha1], (err, commits) =>
+      return callback err if err
+      return callback null if commits.length > 0
+
+      @session (err, session) ->
+        return callback err if err

         queries = [
           Queries.insertObject(commit)
@@ -258,10 +258,15 @@
     @query query, (err, result) ->
       return callback err if err

+      data = {}
+      for row in result.rows
+        row.parent_sha1s = Queries.parseArray(row.parent_sha1s)
+        data[row.commit_sha1] = row
+
       commits = []
-      for row in result.rows
-        row.parent_sha1s = Queries.parseArray(row.parent_sha1s)
-        commits.push new Commit( row )
+      for hash in hashes
+        row = data[hash]
+        commits.push new Commit( row ) if row

       return callback null, commits

@@ -272,10 +277,15 @@
     @query query, (err, result) ->
       return callback err if err

+      data = { byTarget: {} }
+      for row in result.rows
+        row.parent_sha1s = Queries.parseArray(row.parent_sha1s)
+        data.byTarget[row.target_sha1] = row
+
       commits = []
-      for row in result.rows
-        row.parent_sha1s = Queries.parseArray(row.parent_sha1s)
-        commits.push new Commit( row )
+      for hash in targetHashes
+        row = data.byTarget[hash]
+        commits.push new Commit( row ) if row

       return callback null, commits


Attempting to get DiffDOM working...

Attempting to get DiffDOM working...

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -157,7 +157,9 @@
       nodesToKeep.push(findNodesMatchingKeys(currentBody, options.keep)...) if options.keep
       swapNodes(body, nodesToKeep, keep: true)

-    document.body = body
+    diff = new diffDOM
+    bod = diff.diff(document.body, body)
+    diff.apply document.body, bod
     CSRFToken.update csrfToken if csrfToken?
     setAutofocusElement()
     changedNodes = [body]
@@ -200,8 +202,6 @@
   return changedNodes

 onNodeRemoved = (node) ->
-  if typeof jQuery isnt 'undefined'
-    jQuery(node).remove()
   triggerEvent(EVENTS.AFTER_REMOVE, node)

 executeScriptTags = (selector) ->


up.browser.loadPage fallback now uses up.proxy.config.wrapMethodParam instead of "_method"

--- lib/assets/javascripts/up/browser.js.coffee
+++ lib/assets/javascripts/up/browser.js.coffee
@@ -28,7 +28,7 @@
         $field = $('<input type="hidden">')
         $field.attr(field.name, field.value)
         $field.appendTo($form)
-      addField(name: '_method', value: method)
+      addField(name: up.proxy.config.wrapMethodParam, value: method)
       if csrfField = up.rails.csrfField()
         addField(csrfField)
       u.each u.requestDataAsArray(options.data), addField


Removed twipsy.

Removed twipsy.

--- client/Framework/core/KDWindowController.coffee
+++ client/Framework/core/KDWindowController.coffee
@@ -72,8 +72,8 @@
     @layers = layers = []

     document.body.addEventListener 'mousedown', (e)=>
-      $('.twipsy').remove() # temporary for beta
-      lastLayer = layers[layers.length-1]
+      # $('.twipsy').remove() # temporary for beta
+      lastLayer = layers.last

       if lastLayer and $(e.target).closest(lastLayer?.$()).length is 0
         # log lastLayer, "ReceivedClickElsewhere"


Client: Activity: DesktopNotifications: Export loadMessage from message actions

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -583,6 +583,7 @@

 module.exports = {
+  loadMessage
   loadMessages
   loadMessageBySlug
   ensureMessage


Use @1x cards for now

Use @1x cards for now

--- resources/assets/coffee/react/_components/beatmapset-panel.coffee
+++ resources/assets/coffee/react/_components/beatmapset-panel.coffee
@@ -36,7 +36,7 @@

     div className: 'beatmap object_link shadow-hover', objectid: beatmap.beatmapset_id,
       div className: 'beatmap-panel',
-        a href:'https://osu.ppy.sh/s/'+beatmap.beatmapset_id, target: '_blank', className: 'thumb', style: {backgroundImage: 'url(' + beatmap.images['card@2x'] + ')'},
+        a href: 'https://osu.ppy.sh/s/'+beatmap.beatmapset_id, target: '_blank', className: 'thumb', style: {backgroundImage: 'url(' + beatmap.images['card'] + ')'},
           div className: 'bottom_left',
             div className: 'title',
               span {}, beatmap.title


Add delay for sending emails

Add delay for sending emails

--- src/server.coffee
+++ src/server.coffee
@@ -9,6 +9,8 @@
         email:
           enabled: false
           send: @_doSendEmail.bind(@)
+          # The delay (ms) between sent emails. Useful to prevent spamming recipients and being rejected as spam.
+          delay: 5000
           getTemplate: -> @_getDefaultEmailTemplate()
           getTemplateData: (args) -> args
           renderTemplate: (template, templateData) -> _.template(template)(templateData)
@@ -41,6 +43,7 @@
     Logger.debug('Sending email notification', users, event)

     _.each users, (user) ->
+      Promises.runSync -> Q.delay(emailConfig.delay)
       emailConfig.send user: user, event: event

   _doSendEmail: (args) ->


IDE: Resize after toggling the fullscreen.

--- client/ide/views/statusbar/statusbarmenu.coffee
+++ client/ide/views/statusbar/statusbarmenu.coffee
@@ -34,7 +34,9 @@
     fsText   = if mainView.isFullscreen() then 'Exit' else 'Enter'

     items.Shortcuts  = callback: -> appManager.tell 'IDE', 'showShortcutsView'
-    items["#{fsText} Fullscreen"] = callback: -> mainView.toggleFullscreen()
+    items["#{fsText} Fullscreen"] = callback: ->
+      mainView.toggleFullscreen()
+      appManager.tell 'IDE', 'doResize'
     items.Contribute = callback: -> KD.utils.createExternalLink 'https://github.com/koding/IDE'
     items.Quit       = callback: ->
       appManager.quitByName 'IDE'


Enable pre-indexing of references

Enable pre-indexing of references

--- services/web/app/coffee/Features/Project/ProjectController.coffee
+++ services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -219,7 +219,7 @@
 					allowedFreeTrial = !!subscription.freeTrial.allowed || true

 				# HACK: don't do it for now
-				# ReferencesSearchHandler.indexProjectReferences project, ->  # don't need to wait on this
+				ReferencesSearchHandler.indexProjectReferences project, ->  # don't need to wait on this

 				logger.log project_id:project_id, "rendering editor page"
 				res.render 'project/editor',


Fix tests for fullscale URLs

Fix tests for fullscale URLs

--- spec/ToFullscale.coffee
+++ spec/ToFullscale.coffee
@@ -5,7 +5,8 @@
 else
   ToFullscale = require 'noflo-image/components/ToFullscale.js'

-describe.only 'ToFullscale component', ->
+describe 'ToFullscale component', ->
+  @timeout 5*1000
   c = null
   url = null
   newUrl = null
@@ -28,7 +29,6 @@
         url.send 'http://www.esa.int/var/esa/storage/images/esa_multimedia/images/2014/04/glowing_jewels_in_the_galactic_plane/14491843-1-eng-GB/Glowing_jewels_in_the_Galactic_Plane.jpg'

     describe 'with Flickr images', ->
-      @timeout 5*1000
       it 'should return correct URL for non-sized', (done) ->
         newUrl.on 'data', (image) ->
           chai.expect(image).to.equal 'https://farm8.staticflickr.com/7395/12952090783_ce023450da_b.jpg'
@@ -63,12 +63,12 @@
     describe 'with WordPress.com images', ->
       it 'should return correct URL for non-sized', (done) ->
         newUrl.on 'data', (image) ->
-          chai.expect(image).to.equal 'http://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg'
+          chai.expect(image).to.equal 'https://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg'
           done()
         url.send 'http://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg'
       it 'should return correct URL for thumbnails', (done) ->
         newUrl.on 'data', (image) ->
-          chai.expect(image).to.equal 'http://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg'
+          chai.expect(image).to.equal 'https://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg'
           done()
         url.send 'http://tctechcrunch2011.files.wordpress.com/2013/07/henri-bergius3.jpg?w=400'

@@ -89,17 +89,17 @@
     describe 'with Wikimedia Commons thumbnails', ->
       it 'should return correct URL for non-sized', (done) ->
         newUrl.on 'data', (image) ->
-          chai.expect(image).to.equal 'http://upload.wikimedia.org/wikipedia/commons/7/7a/India_-_Varanasi_green_peas_-_2714.jpg'
+          chai.expect(image).to.equal 'https://upload.wikimedia.org/wikipedia/commons/7/7a/India_-_Varanasi_green_peas_-_2714.jpg'
           done()
         url.send 'http://upload.wikimedia.org/wikipedia/commons/7/7a/India_-_Varanasi_green_peas_-_2714.jpg'
       it 'should return correct URL for thumbnails', (done) ->
         newUrl.on 'data', (image) ->
-          chai.expect(image).to.equal 'http://upload.wikimedia.org/wikipedia/commons/7/7a/India_-_Varanasi_green_peas_-_2714.jpg'
+          chai.expect(image).to.equal 'https://upload.wikimedia.org/wikipedia/commons/7/7a/India_-_Varanasi_green_peas_-_2714.jpg'
           done()
         url.send 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/7a/India_-_Varanasi_green_peas_-_2714.jpg/700px-India_-_Varanasi_green_peas_-_2714.jpg'
       it 'should return correct URL for thumbnails with escaped characters', (done) ->
         newUrl.on 'data', (image) ->
-          chai.expect(image).to.equal 'http://upload.wikimedia.org/wikipedia/commons/1/1f/Vistas_desde_la_iglesia_de_San_Pedro%2C_Riga%2C_Letonia%2C_2012-08-07%2C_DD_01.JPG'
+          chai.expect(image).to.equal 'https://upload.wikimedia.org/wikipedia/commons/1/1f/Vistas_desde_la_iglesia_de_San_Pedro%2C_Riga%2C_Letonia%2C_2012-08-07%2C_DD_01.JPG'
           done()
         url.send 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Vistas_desde_la_iglesia_de_San_Pedro%2C_Riga%2C_Letonia%2C_2012-08-07%2C_DD_01.JPG/1000px-Vistas_desde_la_iglesia_de_San_Pedro%2C_Riga%2C_Letonia%2C_2012-08-07%2C_DD_01.JPG'


Add spec for finding markers based on invalidation strategy

--- spec/marker-spec.coffee
+++ spec/marker-spec.coffee
@@ -612,10 +612,15 @@

     beforeEach ->
       marker1 = buffer.markRange([[0, 0], [0, 3]], class: 'a')
-      marker2 = buffer.markRange([[0, 0], [0, 5]], class: 'a', invalidate: 'surround')
+      marker2 = buffer.markRange([[0, 0], [0, 5]], class: 'a', invalidate: 'overlap')
       marker3 = buffer.markRange([[0, 4], [0, 7]], class: 'a')
       marker4 = buffer.markRange([[0, 0], [0, 7]], class: 'b', invalidate: 'never')

     it "can find markers based on custom properties", ->
       expect(buffer.findMarkers(class: 'a')).toEqual [marker2, marker1, marker3]
       expect(buffer.findMarkers(class: 'b')).toEqual [marker4]
+
+    it "can find markers based on their invalidation strategy", ->
+      expect(buffer.findMarkers(invalidate: 'surround')).toEqual [marker1, marker3]
+      expect(buffer.findMarkers(invalidate: 'overlap')).toEqual [marker2]
+      expect(buffer.findMarkers(invalidate: 'never')).toEqual [marker4]


update session timeout check

update session timeout check

--- firefox/lib/lixian.coffee
+++ firefox/lib/lixian.coffee
@@ -165,7 +165,7 @@
 	#########

 	is_session_timed_out_response: (text) ->
-		text == '''<script>document.cookie ="sessionid=; path=/; domain=xunlei.com"; document.cookie ="lx_sessionid=; path=/; domain=vip.xunlei.com";top.location='http://lixian.vip.xunlei.com/task.html?error=1'</script>'''
+		text == '''<script>document.cookie ="sessionid=; path=/; domain=xunlei.com"; document.cookie ="lx_sessionid=; path=/; domain=jiayuan.xunlei.com";top.location='http://lixian.vip.xunlei.com/task.html?error=1'</script>'''

 	get_id_from_cookie: ->
 		@get_cookie('xunlei.com', 'userid')


Adding comment.

Adding comment.

--- src/timber.coffee
+++ src/timber.coffee
@@ -1,5 +1,11 @@
 #
 # Timber is a javascript game engine.
+#
+# It uses the following units for it's (very basic) physics
+# calculations:
+#
+#   length = pixels
+#   time   = ms
 #


Optimize svg template (href attribute)

Optimize svg template (href attribute)

--- app/coffee/modules/common.coffee
+++ app/coffee/modules/common.coffee
@@ -372,7 +372,7 @@
 Svg = () ->
     template = """
     <svg class="{{ 'icon ' + svgIcon }}" style="fill: {{ svgFill }}">
-        <use xlink:href="" ng-attr-xlink:href="{{ '#' + svgIcon }}">
+        <use xlink:href="" ng-attr-xlink:href="{{ '#' + svgIcon }}" href="{{ '#' + svgIcon }}">
             <title ng-if="svgTitle">{{svgTitle}}</title>
             <title ng-if="svgTitleTranslate">{{svgTitleTranslate | translate: svgTitleTranslateValues}}</title>
         </use>


tgCheckPermission directive

tgCheckPermission directive

tg-class-permission="{'xx': 'modify_us'}" the directive add 'xx' if the user has the modify_us permisssion

--- app/coffee/modules/common.coffee
+++ app/coffee/modules/common.coffee
@@ -59,6 +59,42 @@
     return {link:link}

 module.directive("tgCheckPermission", CheckPermissionDirective)
+
+#############################################################################
+## Add class based on permissions
+#############################################################################
+
+ClassPermissionDirective = ->
+    name = "tgClassPermission"
+
+    link = ($scope, $el, $attrs) ->
+        checkPermissions = (project, className, permission) ->
+            negation = permission[0] == "!"
+
+            permission = permission.slice(1) if negation
+
+            if negation && project.my_permissions.indexOf(permission) == -1
+                $el.addClass(className)
+            else if project.my_permissions.indexOf(permission) != -1
+                $el.addClass(className)
+            else
+                $el.removeClass(className)
+
+        tgClassPermissionWatchAction = (project) ->
+            if project
+                unbindWatcher()
+
+                classes = $scope.$eval($attrs[name])
+
+                for className, permission of classes
+                    checkPermissions(project, className, permission)
+
+
+        unbindWatcher = $scope.$watch "project", tgClassPermissionWatchAction
+
+    return {link:link}
+
+module.directive("tgClassPermission", ClassPermissionDirective)

 #############################################################################
 ## Animation frame service, apply css changes in the next render frame


fixes for reports export and pagination

fixes for reports export and pagination

--- app/assets/javascripts/app/reports.coffee
+++ app/assets/javascripts/app/reports.coffee
@@ -42,10 +42,14 @@

       load: (event, ui) ->
         self.fix_bad_dates(ui.panel)
+        self.update_export_urls()
     })

   build_query_string: ->
     "?" + @$element.serialize()
+
+  tab_url: (tab) ->
+    $(tab).data('base-href') + @build_query_string()

   init_form: ->
     $('#status_filter').chosen() if $('#status_filter').length
@@ -55,8 +59,7 @@
       self.update_parameters()

   update_href: (tab) ->
-    base_url = tab.data('base-href')
-    tab.find('a').attr('href', base_url + @build_query_string())
+    tab.find('a').attr('href', @tab_url(tab))

   # Make sure to update the date params in case they were empty or invalid
   fix_bad_dates: (panel) ->
@@ -65,10 +68,15 @@

   init_pagination: ->
     self = this
-    $(document).on 'click', '.pagination a', ->
-      self.current_tab().find('a').attr('href', $(this).attr('href'))
+    $(document).on 'click', '.pagination a', (evt) ->
+      evt.preventDefault()
+      $(self.current_tab()).find('a').attr('href', $(this).attr('href'))
       self.refresh_tab()
-      false
+
+  update_export_urls: ->
+    url = @tab_url(@current_tab())
+    $('#export').attr('href', url + '&export_id=report&format=csv');
+    $('#export-all').attr('href', url + '&export_id=report_data&format=csv');

 $ ->
   window.report = new TabbableReports($('#refresh-form'))


OX-77 Number of bug fixes dealing with trees, especially deleting nodes

--- app/assets/javascripts/model/route.coffee
+++ app/assets/javascripts/model/route.coffee
@@ -359,12 +359,9 @@
       point.treeId = @activeTree.treeId
       @trees[@activeTree.treeIndex] = point
       @activeTree = point
+      @activeTree.parent = null
     @activeNode = point
     @lastActiveNodeId = @activeNode.id
-    #console.log @activeTree.toString()
-    #console.log @getNodeList()
-    #for item in @getNodeList()
-    #  console.log item.id

   getActiveNodeId : ->
     @lastActiveNodeId
@@ -407,8 +404,11 @@
       if tree.treeId == id
         @activeTree = tree
         break
-    @activeNode = @activeTree
-    @lastActiveNodeId = @activeNode.id
+    if @activeTree.isSentinel
+      @activeNode = null
+    else
+      @activeNode = @activeTree
+      @lastActiveNodeId = @activeNode.id
     @push()

   createNewTree : ->
@@ -419,6 +419,7 @@
     sentinel.treeId = @treeIdCount++
     # save Index, so we can access it once we have the root element
     sentinel.treeIndex = @trees.length
+    sentinel.isSentinel = true
     @trees.push(sentinel)
     @activeTree = sentinel
     @activeNode = null
@@ -439,6 +440,7 @@
       @activeNode.remove(id)
       @lastActiveNodeId = @activeNode.id
     else
+      # Root is deleted
       @deleteActiveTree()
     @push()

@@ -455,8 +457,9 @@
     # to create one.
     if @trees.length == 0
       @createNewTree()
-    @activeTree = @trees[0]
-    @activeNode = null
+    else
+      # just set the last tree to be the active one
+      @setActiveTree(@trees[@trees.length - 1].treeId)
     @push()

   getTree : (id) ->


shorten tooltip

shorten tooltip

--- app/assets/javascripts/pop-over.js.coffee
+++ app/assets/javascripts/pop-over.js.coffee
@@ -20,4 +20,4 @@
 $ ->
   $(".pie").tooltip
     placement: "top",
-    title: "Proposal activity. Click to see summary."+    title: "Click to see more."


follow good practice, this fixes lighting toolbar as well

--- app/assets/jax/controllers/oz3d.js.coffee
+++ app/assets/jax/controllers/oz3d.js.coffee
@@ -22,8 +22,7 @@
     @lighto.camera.position = @light.camera.position unless @light instanceof Jax.Light.Directional

   key_released: (e) ->
-    # FIXME this is bad practice, use World#removeLight
-    @world.lights.length = 0
+    @world.removeLight @light
     switch ++@lightType % 3
       when 0 then @light = new Jax.Light.Point       @lightOptions
       when 1 then @light = new Jax.Light.Directional @lightOptions


Wrap link into help-block.

Wrap link into help-block.

--- app/client/js/react/pages/editsong.coffee
+++ app/client/js/react/pages/editsong.coffee
@@ -11,7 +11,7 @@
     @constructor
   ###
   constructor: (routes, store) ->
-    {div,form,input,textarea,a,button} = React.DOM
+    {p,div,form,input,textarea,a,button} = React.DOM

     editMode = false
     song = null
@@ -50,11 +50,11 @@
                 placeholder: EditSong.MSG_WRITE_LYRICS_HERE
                 ref: 'lyrics'
                 value: song.lyrics
-              a
-                className: 'help-block'
-                href: 'http://linkesoft.com/songbook/chordproformat.html'
-                target: '_blank'
-              , EditSong.MSG_HOW_TO_WRITE_LYRICS
+              p className: 'help-block',
+                a
+                  href: 'http://linkesoft.com/songbook/chordproformat.html'
+                  target: '_blank'
+                , EditSong.MSG_HOW_TO_WRITE_LYRICS
             div className: 'form-group horizontal',
               button
                 className: 'btn btn-default'


refactor: remove unused code

refactor: remove unused code

--- app/components/vnc-viewer.coffee
+++ app/components/vnc-viewer.coffee
@@ -81,7 +81,10 @@
     doNotRunAPIScan: ->
       @set "isApiScanEnabled", false
       @send "isApiScanEnabled"
-      @send "closeModal"
+
+    runAPIScan: ->
+      @set "isApiScanEnabled", true
+      @send "isApiScanEnabled"

     isApiScanEnabled: ->
       isApiScanEnabled = @get "isApiScanEnabled"
@@ -92,6 +95,7 @@
         isApiScanEnabled: isApiScanEnabled
       @get("ajax").post apiScanOptions, data: data
       .then (data)->
+        @send "closeModal"
         that.send "dynamicScan"
         that.get("notify").success "Starting the scan"
       .catch (error) ->
@@ -120,21 +124,4 @@
     closeModal: ->
       @set "showAPIScanModal", false

-    runAPIScan: ->
-      @set "isApiScanEnabled", true
-      isApiScanEnabled = @get "isApiScanEnabled"
-      project_id = @get "file.project.id"
-      apiScanOptions = [ENV.host,ENV.namespace, ENV.endpoints.apiScanOptions, project_id].join '/'
-      data =
-        isApiScanEnabled: isApiScanEnabled
-      @get("ajax").post apiScanOptions, data: data
-      .then (data)->
-        that.send "closeModal"
-        that.send "dynamicScan"
-        that.get("notify").success "Successfully added the url filter & Starting the scan"
-      .catch (error) ->
-        for error in error.errors
-          that.get("notify").error error.detail?.message
-
-
 `export default VncViewerComponent`


Annotation -> Discussion

Annotation -> Discussion

--- app/js/jail_iframe/classes/buttons.coffee
+++ app/js/jail_iframe/classes/buttons.coffee
@@ -31,7 +31,7 @@
 class FactlinkJailRoot.ShowButton extends Button
   content: """

-
Show Annotation

+
Show Discussion

Loading...

   """
@@ -40,7 +40,7 @@
 class FactlinkJailRoot.CreateButton extends Button
   content: """

-
Add Annotation

+
Create Discussion

Loading...

   """


feat: broadcast message to HUBOT_WAKEUP_CHANNEL if triggered from http

closes #1

--- src/wakeup.coffee
+++ src/wakeup.coffee
@@ -3,6 +3,7 @@
 #
 # Configuration:
 #   HUBOT_WAKEUP_HTTP_ENABLED - trigger wakeup with a http request? ( 1|0 )
+#   HUBOT_WAKEUP_CHANNEL - the room/channel to send messages to
 #
 # Commands:
 #   hubot kick <machine> - Send a WOL package to `<machine>`
@@ -40,9 +41,8 @@
 	#
 	# Send the package
 	#
-	wakeup = (machine, msg) ->
+	wakeup = (machine, callback) ->
 		machineList = robot.brain.get('wakeup') or null
-		msg = msg or reply : ->

 		# Is it a machine we know?
 		if machineList? && machineList[machine]?
@@ -55,11 +55,11 @@
 			# This is the magical line, that sends the packages
 			wol.wake mac, (error) ->
 				if error
-					msg.reply "shoot! #{error}"
+					callback "shoot! #{error}"
 				else
-					msg.reply "kicked #{mac} his shiny metal ass"
+					callback "kicked #{mac} his shiny metal ass"
 		else
-			msg.reply "'#{mac}' is not a mac adres, nor a machine I know"
+			callback "'#{mac}' is not a mac adres, nor a machine I know"
 			return

@@ -116,19 +116,24 @@
 	# Send a WOL package
 	#
 	robot.respond /kick (.*)/i, (msg) ->
-		wakeup msg.match[1], msg
+		wakeup msg.match[1], (message) ->
+			msg.reply message

 	#
 	# Listen to GET http requests
 	#
 	robot.router.get '/wakeup/kick/:mac', (req, res) ->
-		enabled = process.env.HUBOT_WAKEUP_HTTP_ENABLED
+		enabled = process.env.HUBOT_WAKEUP_HTTP_ENABLED

 		if enabled == "true" or enabled == "1"
 			mac = req.params.mac
-			res.reply = res.end
-			wakeup mac, res
+			channel = process.env.HUBOT_WAKEUP_CHANNEL or "#general"
+
+			wakeup mac, (message) ->
+				robot.send {room: channel}, message
+				res.end message
+				return
 		else
 			res.end "http disabled"


Added ConnectionObserver class, basic leave-behind alerts

--- client/mobile/app/coffee/modules/device/model.coffee
+++ client/mobile/app/coffee/modules/device/model.coffee
@@ -1,4 +1,40 @@
 charMap = require './charMap' # TODO - rethink this approach
+
+# # # # #
+
+# Observes connection and sends leave-behind alerts
+class ConnectionObserver extends Marionette.Object
+
+  initialize: (options) =>
+    @device = options.device
+    setTimeout(@checkPoll, 2000)
+
+  checkPoll: =>
+    return @startPolling() if @device.get('connected')
+    @stopPolling()
+
+  startPolling: =>
+    return if @interval
+    @interval = setInterval( @device.readRSSI, 500 )
+    setInterval(@sendAlert, 500)
+
+  # Send Alerts
+  sendAlert: =>
+    # console.log @device.get('rssi')
+
+    # TODO - RSSI threshold should be a user setting
+    if @device.get('rssi') < -80
+
+      # Leave - behind alerts
+      # TODO - different sound?
+      # TODO - haptic feedback intensity?
+      plugins.deviceFeedback.haptic()
+      plugins.deviceFeedback.acoustic()
+
+  stopPolling: =>
+    return unless @interval
+    clearInterval(@interval)
+    delete @interval

 # # # # #

@@ -7,9 +43,11 @@
   connect: ->
     window.device = @ # TODO - remove
     Backbone.Radio.channel('bluetooth').request('connect', @)
+    @connectionObserver = new ConnectionObserver({ device: @ }) # TODO - move this elsewhere?

   disconnect: ->
     Backbone.Radio.channel('bluetooth').request('disconnect', @)
+    @connectionObserver.stopPolling() # TODO - RSSI polling

   isConnected: ->
     Backbone.Radio.channel('bluetooth').request('is:connected', @)


restore back button for cover image link on overview page

--- apps/fair/components/browse/router.coffee
+++ apps/fair/components/browse/router.coffee
@@ -68,4 +68,4 @@
     @view.artistsAZ()

   browse: (id) =>
-    @navigate "#{id}/browse/booths", trigger: true
+    @navigate "#{id}/browse/booths", trigger: true, replace: true


Doesnt matter but move this fetch first for consistency

--- apps/galleries_institutions/routes.coffee
+++ apps/galleries_institutions/routes.coffee
@@ -46,8 +46,8 @@

 @partners = (req, res) ->
   Q.allSettled([
+    fetchFeaturedProfiles('partners:featured-galleries')
     fetchFeaturedProfiles('partners:featured-institutions')
-    fetchFeaturedProfiles('partners:featured-galleries')
   ]).then (results) ->
     [featuredGalleries, featuredInstitutions] = _.pluck results, 'value'
     featuredGalleries.add(featuredInstitutions.models)


add_lodcurve: attach tooltip to body; attaching to parent doesn't work

 - can't put the tooltip div inside an SVG; need to put it in the
   parent div; but here just put it in the body of the page

--- src/add_lodcurve.coffee
+++ src/add_lodcurve.coffee
@@ -120,7 +120,8 @@
                         .on "mouseout", ->
                                                        d3.select(this).attr("opacity", 0)

-         parent = d3.select(svg.node().parentNode)
+         # parent = d3.select(svg.node().parentNode)
+         parent = d3.select("body")
          markertip = d3panels.tooltip_create(parent, markerSelect, {tipclass:"blah"}, (d,i) ->
                                               [d.name, " LOD = #{d3.format('.2f')(d.lod)}"])


fix(contenteditable): Fix undefined callback on plugins

--- src/components/contenteditable/contenteditable.cjsx
+++ src/components/contenteditable/contenteditable.cjsx
@@ -214,7 +214,7 @@
   _runPluginHandlersForEvent: (method, event, args...) =>
     executeCallback = (plugin) =>
       return if not plugin[method]?
-      callback = callback.bind(plugin)
+      callback = plugin[method].bind(plugin)
       @atomicEdit(callback, event, args...)

     for plugin in @props.plugins


Remove extra space

Remove extra space

Objects should not have a trailing space in their definition.

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -47,7 +47,7 @@

   resourcePath = path.resolve(resourcePath)

-  {resourcePath, version, devMode }
+  {resourcePath, version, devMode}

 setupCoffeeScript = ->
   CoffeeScript = null


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -1,5 +1,5 @@
 ###*
-The Cooldown module provides a declarative way to manage cooldowns on
+The Cl module provides a declarative way to manage cooldowns on
 GameObject's properties.

 <code><pre>