Migrate2015: Update test, list-group, button

--- test/list-group-spec.cjsx
+++ test/list-group-spec.cjsx
@@ -1,39 +0,0 @@
-expect = require "expect.js"
-React = require "react"
-ReactTestUtils = require "react/lib/ReactTestUtils"
-ReactDOM = require "react-dom"
-ListGroup = require "../src/list-group.cjsx"
-ListItem = require "../src/list-item.cjsx"
-
-describe "listgroup", () ->
-
-  it "Should create default element", () ->
-    instance = ReactTestUtils.renderIntoDocument <ListGroup />
-    node = ReactDOM.findDOMNode instance
-
-    expect(node.nodeName).to.be("UL")
-    expect(node.className).to.be("list-group")
-
-
-  it "Should hide img element if no image prop", () ->
-    tree = (
-      <ListGroup>
-        <ListItem title="item" />
-      </ListGroup>
-    )
-
-    instance = ReactTestUtils.renderIntoDocument tree
-    items = ReactTestUtils.scryRenderedDOMComponentsWithTag instance, "img"
-    expect(items.length).to.be(0)
-
-
-  it "Should hide img element if image prop exists", () ->
-    tree = (
-      <ListGroup>
-        <ListItem title="item" image="no.png" />
-      </ListGroup>
-    )
-
-    instance = ReactTestUtils.renderIntoDocument tree
-    items = ReactTestUtils.scryRenderedDOMComponentsWithTag instance, "img"
-    expect(items.length).to.be(1)


Rearranged some things

Rearranged some things

--- app/index.coffee
+++ app/index.coffee
@@ -5,26 +5,18 @@

 translate = require 't7e'

+enUs = require 'lib/en_us'
+translate.load enUs
+
 Api = require 'zooniverse/lib/api'
 User = require 'zooniverse/models/user'
-TopBar = require 'zooniverse/controllers/top-bar'
-googleAnalytics = require 'zooniverse/lib/google-analytics'
-LanguageManager = require 'zooniverse/lib/language-manager'
-
-Navigation = require 'controllers/navigation'
-HomePage = require 'controllers/home_page'
-ClassifyPage = require 'controllers/classify_page'
-AboutPage = require 'controllers/about_page'
-ProfilePage = require 'controllers/profile_page'
-
-enUs = require 'lib/en_us'
-translate.load enUs

 new Api project: 'planet_four'

 app = {}
 app.container = $('#app')

+Navigation = require 'controllers/navigation'
 app.navigation = new Navigation
 app.navigation.el.appendTo app.container

@@ -32,10 +24,10 @@
   className: "app-main #{Stack::className}"

   controllers:
-    home: HomePage
-    classify: ClassifyPage
-    about: AboutPage
-    profile: ProfilePage
+    home: require 'controllers/home_page'
+    classify: require 'controllers/classify_page'
+    about: require 'controllers/about_page'
+    profile: require 'controllers/profile_page'

   routes:
     '/home': 'home'
@@ -48,6 +40,7 @@
 app.stack.classify.classificationTools.el.appendTo app.navigation.classificationToolsContainer
 app.stack.el.appendTo app.container

+LanguageManager = require 'zooniverse/lib/language-manager'
 languageManager = new LanguageManager
   translations:
     en: label: 'English', strings: enUs
@@ -58,12 +51,14 @@
   translate.load strings
   translate.refresh()

+TopBar = require 'zooniverse/controllers/top-bar'
 app.topBar = new TopBar
 app.topBar.el.prependTo 'body'

 Route.setup()
 User.fetch()

+googleAnalytics = require 'zooniverse/lib/google-analytics'
 new googleAnalytics account: 'UA-1224199-41'

 module.exports = app


Better progress bar logic

Better progress bar logic

--- lib/integrated-learn-environment.coffee
+++ lib/integrated-learn-environment.coffee
@@ -90,9 +90,9 @@
         console.log @progressBarPopup
         window.progressBar = @progressBarPopup

-      @progressBarPopup.item.getElementsByClassName('learn-progress-bar')[0].setAttribute 'style', 'width:' + value * 100 + '%;'
-
-      if value > 0.995
+      if value >= 0 && value < 1
+        @progressBarPopup.item.getElementsByClassName('learn-progress-bar')[0].setAttribute 'style', 'width:' + value * 100 + '%;'
+      else
         @progressBarPopup.destroy()
         @progressBarPopup = null


zip code test

zip code test


fix whitespace

fix whitespace

--- libraries/metrics/memory.coffee
+++ libraries/metrics/memory.coffee
@@ -73,7 +73,7 @@
 		#Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket)

 		if global.gc? && readyToGc()
-			gcTime = (executeAndTime	global.gc).toFixed(2)
+			gcTime = (executeAndTime global.gc).toFixed(2)
 			memAfterGc = inMegaBytes process.memoryUsage()
 			deltaMem = updateMemoryStats(memBeforeGc, memAfterGc)
 			logger.log {gcTime, memBeforeGc, memAfterGc, deltaMem, gcInterval, CpuTimeBucket}, "global.gc() forced"


Makes sense.

Makes sense.

--- library/KeyPairOperation.coffee
+++ library/KeyPairOperation.coffee
@@ -17,7 +17,7 @@
   salt: ->
     NaCl.util.decodeUTF8(@emailAddress)

-  # Hash digest of the `secret()` to increase its potential complexity (hand wave?).
+  # Hash digest of the `secret()` to increase its potential complexity.
   hashDigestOfSecret: ->
     (new BLAKE2s length: 32).update(@secret()).digest()


Number#seconds

Number#seconds

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -93,5 +93,9 @@
   equals factors.first(), -1
   equals factors.product(), -3

+test "#seconds", ->
+  equals 1.second, 1000
+  equals 3.seconds, 3000
+
 module undefined


tests(macPopover): Added more tests on scope creation and removal

--- test/unit/popover.spec.coffee
+++ test/unit/popover.spec.coffee
@@ -94,6 +94,12 @@
       it "should show and add popover to list", ->
         expect(popover.popoverList.length).toBe 1

+      it "should create a new scope", ->
+        popoverObj = popover.popoverList[0]
+        scope      = popoverObj.popover.scope()
+
+        expect($rootScope.$id).not.toBe scope.$id
+
       it "should create a scope with additional data", ->
         popoverObj = popover.popoverList[0]
         scope      = popoverObj.popover.scope()
@@ -198,11 +204,29 @@

         expect(trigger.hasClass("active")).toBeFalsy()

-      it "should not destroy scope", ->
+      it "should not destroy original scope", ->
         popover.hide "test"
         $rootScope.$digest()

+        # HACK: To invoke the leave callback
+        $animate.queue[0].args[1]()
+
         expect(destroy).not.toHaveBeenCalled()
+
+      it "should destroy popover scope", ->
+        popoverObj = popover.popoverList[0]
+        scope      = popoverObj.popover.scope()
+
+        destroyed = jasmine.createSpy "destroy"
+        scope.$on "$destroy", destroyed
+
+        popover.hide "test"
+        $rootScope.$digest()
+
+        # HACK: To invoke the leave callback
+        $animate.queue[0].args[1]()
+
+        expect(destroyed).toHaveBeenCalled()

   describe "popover directive", ->
     $compile = null


failing test for >>

failing test for >>

another @aroben spot!

--- test/unit/test_updates.coffee
+++ test/unit/test_updates.coffee
@@ -87,7 +87,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -100,7 +100,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -111,7 +111,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [ ] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -122,7 +122,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [ ] inner complete
+      >> - [ ] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -133,7 +133,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [ ] ordered complete
       > 0. [ ] ordered incomplete
@@ -144,7 +144,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -155,7 +155,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [x] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -166,7 +166,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [x] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete
@@ -177,7 +177,7 @@
       - [#{@nbsp}] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [x] ordered incomplete
@@ -188,7 +188,7 @@
       - [x] incompleteNBSP
       > - [x] quoted complete
       > - [ ] quoted incomplete
-      > > - [x] inner complete
+      >> - [x] inner complete
       > > - [ ] inner incomplete
       > 0. [x] ordered complete
       > 0. [ ] ordered incomplete


Tests:IDE:Terminal: Added terminal session test.

--- tests/src/ide/terminal.coffee
+++ tests/src/ide/terminal.coffee
@@ -94,3 +94,39 @@
       .waitForElementVisible   paneSelector + ' .terminal.active',20000 # Assertion
       .assert.containsText     '.application-tabview .terminal.active .terminal-pane', userName # Assertion
       .end()
+
+
+  terminateSession: (browser) ->
+
+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
+
+    openNewTerminalMenu(browser)
+
+    getSessionData = =>
+
+      selector = '.kdcontextmenu:visible ul + li:not(.disabled)'
+      matcher  = /session-(\w+)\S+/
+
+      return $(selector).eq(0).attr('class').match(matcher)
+
+    browser.execute getSessionData, [], (result) =>
+
+      [cssClass, sessionId] = result.value
+      sessionListSelector   = '.kdcontextmenu ul ul:nth-of-type(1).expanded'
+
+      browser
+        .waitForElementVisible  '.' + cssClass, 25000
+        .moveToElement          '.' + cssClass, 10, 10
+        .pause                  1000
+        .click                  '.context-list-wrapper ul > ul.expanded ul.expanded .terminate'
+        .pause                  5000
+
+        openNewTerminalMenu(browser)
+
+        browser
+          .pause   1000
+          .getText sessionListSelector, (result) =>
+            assert.equal(result.value.indexOf(sessionId), -1)
+
+            browser.end()


Break test into more granular sections

Break test into more granular sections

--- test/kissmetrics.coffee
+++ test/kissmetrics.coffee
@@ -21,12 +21,14 @@
 		km.should.have.property 'set'
 		km.should.have.property 'alias'

+describe 'Record', ->
 	it 'should record events', ->
 		expectedOutput = /^GET \/?e\?_n=test%20event&_k=apiKey&_p=evan%40example\.com HTTP\/1\.1/

 		km = new KM 'apiKey', 'evan@example.com'
 		km.record('test event').lastQuery.output.pop().match(expectedOutput).should.be.ok

+describe 'Set', ->
 	it 'should set properties', ->
 		expectedOutput = /GET \/?s\?place=home&_k=apiKey&_p=evan%40example\.com HTTP\/1\.1/

@@ -45,12 +47,14 @@
 		km = new KM 'apiKey', 'evan@example.com'
 		km.set({foo: 'Safe', _n: 'Blocked'}).lastQuery.output.pop().match(expectedOutput).should.be.ok

+describe 'Alias', ->
 	it 'should alias people', ->
 		expectedOutput  = /GET \/?a\?_n=notevan%40example.com&_k=apiKey&_p=evan%40example\.com HTTP\/1\.1/

 		km = new KM 'apiKey', 'evan@example.com'
 		km.alias('notevan@example.com').lastQuery.output.pop().match(expectedOutput).should.be.ok

+describe 'Client API', ->
 	it 'should be chainable', ->
 		expectedOutput  = /GET \/?a\?_n=notevan%40example.com&_k=apiKey&_p=evan%40example\.com HTTP\/1\.1/


加入class include()的测试

加入class include()的测试

--- test/baseSpec.coffee
+++ test/baseSpec.coffee
@@ -13,18 +13,6 @@
             sub = M.BaseObj.extend(subProto)

             sub.should.have.property(member) for member of  subProto
-
-        it "should call initialize if defined", ->
-            initialize = sinon.spy()
-
-            AnimalProto =
-                initialize: initialize
-
-            Animal = M.BaseObj.extend(AnimalProto)
-
-            Monkey = Animal.extend()
-
-            initialize.calledOn(Monkey).should.be.true

     describe "BaseObj.base()", ->
         baseFunc = base = a = b = aspy = null
@@ -121,5 +109,31 @@
                 subInstance.Func()
                 aFuncSpy.called.should.be.true
                 bFuncSpy.called.should.be.true
+
+    describe "class include module", ->
+        it "should include module", ->
+
+            m1 =
+                p1: ->
+                onIncluded: sinon.spy()
+
+            m2 =
+                p2: ->
+
+            A = M.BaseClass.extend
+                modules: [m1, m2]
+
+            A.prototype.should.have.property("p1", m1.p1)
+            A.prototype.should.have.property("p2", m2.p2)
+
+            new A()
+            m1.onIncluded.called.should.be.true
+
+

+
+
+
+
+


bind => on

bind => on

--- test/bindable.coffee
+++ test/bindable.coffee
@@ -1,8 +1,7 @@
 module "Bindable"

 test "#bind and #trigger", 1, ->
-  o = Core()
-  o.include("Bindable")
+  o = Core().include("Bindable")

   o.bind("test", -> ok true)


BW&XY: Chill Drive = Ice-type Techno Blast.

--- test/bw/moves.coffee
+++ test/bw/moves.coffee
@@ -1781,6 +1781,13 @@
       move = @battle.getMove('Techno Blast')
       type = move.getType(@battle, @p1, @p2)
       type.should.equal "Electric"
+
+    it "is Ice-type if the user holds a Chill Drive", ->
+      shared.create.call this,
+        team1: [Factory("Magikarp", item: "Chill Drive")]
+      move = @battle.getMove('Techno Blast')
+      type = move.getType(@battle, @p1, @p2)
+      type.should.equal "Ice"

     it "is Normal-type otherwise", ->
       shared.create.call(this)


Update tooltip plugin

Update tooltip plugin

Heads up: top style has some bugs!

--- vendor/assets/javascripts/mice/tooltip.coffee
+++ vendor/assets/javascripts/mice/tooltip.coffee
@@ -97,7 +97,7 @@

     show: ->
       e = $.Event('show.bs.'+@type)
-      if @hasContent() and @enabled
+      if @getTitle() and @enabled
         @$element.trigger e

         inDom =  $.contains(document.documentElement, @$element[0])
@@ -156,11 +156,8 @@
       marginLeft = parseInt($tip.css('margin-left'), 10)

       # we must check for NaN for ie 8/9
-      marginTop  = 0 if isNaN(marginTop)
-      marginLeft = 0 if isNaN(marginLeft)
-
-      offset.top  = offset.top  + marginTop
-      offset.left = offset.left + marginLeft
+      offset.top  = offset.top  + if isNaN(marginTop)  then 0 else marginTop
+      offset.left = offset.left + if isNaN(marginLeft) then 0 else marginLeft

       # $.fn.offset doesn't round pixel values
       # so we use setOffset directly with our own function B-0
@@ -189,11 +186,7 @@
       @arrow().css(position, if delta then (50 * (1 - delta / dimension) + '%') else '')

     setContent: ->
-      $tip  = @tip()
-      title = @getTitle()
-
-      $tip.find('.tooltip-inner')[if @options.html then 'html' else 'text'](title)
-      $tip.removeClass('fade in top bottom left right')
+      @tip().find('.inner')[if @options.html then 'html' else 'text'](@getTitle()).removeClass('fade in top bottom left right')

     hide: ->
       that = @
@@ -217,11 +210,7 @@
       @

     fixTitle: ->
-      $e = @$element
-      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') if $e.attr('title') or typeof ($e.attr('data-original-title')) != 'string'
-
-    hasContent: ->
-      @getTitle()
+      @$element.attr('data-original-title', @$element.attr('title') or '').removeAttr('title') if @$element.attr('title') or typeof (@$element.attr('data-original-title')) != 'string'

     getPosition: ($element) ->
       $element  = $element or @$element
@@ -237,7 +226,7 @@
     getCalculatedOffset: (placement, pos, actualWidth, actualHeight) ->
       return if placement == 'bottom' then { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } else
              if placement == 'top'    then { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } else
-             if placement == 'left'   then { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } else { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+             if placement == 'left'   then { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } else { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }

     getViewportAdjustedDelta: (placement, pos, actualWidth, actualHeight) ->
       delta = { top: 0, left: 0 }
@@ -249,31 +238,28 @@
       if /right|left/.test placement
         topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
         bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
-        if topEdgeOffset < viewportDimensions.top
+        if topEdgeOffset < viewportDimensions.top #top overflow
           delta.top = viewportDimensions.top - topEdgeOffset
-        else if bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height
+        else if bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height #bottom overflow
           delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
       else
         leftEdgeOffset  = pos.left - viewportPadding
         rightEdgeOffset = pos.left + viewportPadding + actualWidth
-        if leftEdgeOffset < viewportDimensions.left
+        if leftEdgeOffset < viewportDimensions.left #left overflow
           delta.left = viewportDimensions.left - leftEdgeOffset
-        else if rightEdgeOffset > viewportDimensions.width
+        else if rightEdgeOffset > viewportDimensions.width #right overflow
           delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset

       delta

     getTitle: ->
-      $e = @$element
-      o  = @options
-      title = $e.attr('data-original-title') or (if typeof o.title == 'function' then o.title.call($e[0]) else  o.title)
-      title
+      @$element.attr('data-original-title') or (if typeof @options.title == 'function' then @options.title.call(@$element[0]) else  @options.title)

     tip: ->
       @$tip = @$tip or $(@options.template)

     arrow: ->
-      @$arrow = @$arrow or @tip().find('.tooltip-arrow')
+      @$arrow = @$arrow or @tip().find('.arrow')

     validate: ->
       if !@$element[0].parentNode
@@ -291,7 +277,7 @@
       @enabled = !@enabled

     toggle: (e) ->
-      self = this
+      self = @
       if e
         self = $(e.currentTarget).data('bs.' + @type)
         if !self
@@ -307,27 +293,25 @@

   $.fn.tooltip = (option) ->
     @each ->
-      $this   = $(@)
-      data    = $this.data('bs.tooltip')
-      options = typeof option == 'object' and option
+      $element = $(@)
+      data     = $element.data('bs.tooltip')
+      options  = typeof option == 'object' and option

       return if !data and option == 'destroy'
-      $this.data('bs.tooltip', (data = new Tooltip(@, options))) if !data
+
+      $element.data('bs.tooltip', (data = new Tooltip(@, options))) if !data
+
       data[option]() if typeof option == 'string'

-  old = $.fn.tooltip

   $.fn.tooltip.Constructor = Tooltip

-  $.fn.tooltip.noConflict = ->
-    $.fn.tooltip = old
-    @

   $.fn.tooltip.defaults =
     animation: true
     placement: 'top'
     selector: false
-    template: '

'
+    template: '

'
     trigger: 'hover focus'
     title: ''
     delay: 0


Remove ng-resource include (keep the bower component for now)

--- vendor/assets/javascripts/qrious-ui.js.coffee
+++ vendor/assets/javascripts/qrious-ui.js.coffee
@@ -5,6 +5,5 @@
 #= require angular-bootstrap/ui-bootstrap-tpls
 #= require angular-bootstrap-colorpicker/js/bootstrap-colorpicker-module
 #= require angular-file-upload/angular-file-upload
-#= require angular-resource/angular-resource.js
 #= require angularjs-rails-resource/angularjs-rails-resource.js
 #= require_self


Add version test for minimap v3

Add version test for minimap v3

--- lib/minimap-find-and-replace.coffee
+++ lib/minimap-find-and-replace.coffee
@@ -9,7 +9,7 @@
     return @deactivate() unless findPackage? and minimapPackage?

     minimap = require(minimapPackage.path)
-    return @deactivate() unless minimap.versionMatch('2.x')
+    return @deactivate() unless minimap.versionMatch('3.x')

     @binding = new MinimapFindAndReplaceBinding findPackage, minimapPackage


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -98,7 +98,7 @@

   equals obj.I.age, 1, "should successfully update"

-test "use existing value of instance variable", ->
+test "use existing value of instance variable as de", ->
   obj = GameObject()

   obj.include(Cooldown)


Swap order of `ctrl-w` and `ctrl-f4`

Swap order of `ctrl-w` and `ctrl-f4`

This allows `Ctrl-W` to appear in the system menu again until
the atom-shell issue with function keys is fixed.

Refs atom/atom-shell#485
Closes #3283

--- keymaps/win32.cson
+++ keymaps/win32.cson
@@ -25,8 +25,8 @@
   'ctrl-n': 'application:new-file'
   'ctrl-s': 'core:save'
   'ctrl-S': 'core:save-as'
+  'ctrl-f4': 'core:close'
   'ctrl-w': 'core:close'
-  'ctrl-f4': 'core:close'
   'ctrl-z': 'core:undo'
   'ctrl-shift-z': 'core:redo'
   'ctrl-y': 'core:redo'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/distance.coffee
+++ test/distance.coffee
@@ -7,6 +7,7 @@

   ok player.distance

-test 'it should proxy to Point.distanc'
+test 'it should proxy to Point.distance', ->
+  player

 module()


tween with arbitrary easing function

tween with arbitrary easing function

--- test/drawable.coffee
+++ test/drawable.coffee
@@ -32,9 +32,11 @@
   equal object2.I.alpha, 0.5

 test "scale", ->
-  object = GameObject
+  object = GameObject()

   transform = object.transform()

+  object = GameObject()
+
 module()


Avoid defective CoffeeScript existence test.

--- server/server.coffee
+++ server/server.coffee
@@ -84,7 +84,7 @@
 # Start MongoDB, if necessary...
 spawn 'mongod', MONGODB_PORT, ->
   # ...then the REST app...
-  cmd = if env? then "qirest --#{ env }" else 'qirest'
+  cmd = if env then "qirest --#{ env }" else 'qirest'
   spawn cmd, EVE_PORT, ->
     #...then the Express server.
     port = server.get 'port'


bug fix -- paste broken on Firefox OS X.

--- salvus/page/console.coffee
+++ salvus/page/console.coffee
@@ -281,7 +281,7 @@
         Terminal.colors[257] = Terminal.defaultColors.fg

     client_keydown: (ev) =>
-        #console.log("client_keydown")
+        #console.log("client_keydown", ev)
         if ev.ctrlKey and ev.shiftKey
             switch ev.keyCode
                 when 190       # "control-shift->"
@@ -290,7 +290,8 @@
                 when 188       # "control-shift-<"
                     @_decrease_font_size()
                     return false
-        if (ev.metaKey or ev.ctrlKey) and (ev.keyCode in [17, 91, 93, 223])  # command or control key (could be a paste coming)
+        if (ev.metaKey or ev.ctrlKey) and (ev.keyCode in [17, 86, 91, 93, 223, 224])  # command or control key (could be a paste coming)
+            #console.log("resetting hidden textarea")
             #console.log("clear hidden text area paste bin")
             # clear the hidden textarea pastebin, since otherwise
             # everything that the user typed before pasting appears


Don't encode associated models

Don't encode associated models

--- src/batman.coffee
+++ src/batman.coffee
@@ -2115,9 +2115,12 @@
     encoders = @_batman.get('encoders')
     unless !encoders or encoders.isEmpty()
       encoders.forEach (key, encoder) =>
+        # Don't encode associations
+        return if @constructor.associations?.has(key)
+
         val = @get key
         if typeof val isnt 'undefined'
-          encodedVal = encoder(@get key)
+          encodedVal = encoder(val)
           if typeof encodedVal isnt 'undefined'
             obj[key] = encodedVal

@@ -2295,6 +2298,14 @@
       hash = new Batman.Hash
       @storage.set type, hash
     hash.set association, association.label
+
+  has: (name) ->
+    ret = false
+    @storage.forEach (type, hash) ->
+      hash.forEach (association, label) ->
+        if label is name
+          ret = true
+    ret

   clearRelations: (base) ->
     @storage.forEach (type, hash) ->


Updated test

Updated test

--- test/mixedice.coffee
+++ test/mixedice.coffee
@@ -1,10 +1,10 @@
 require 'udefine/global'
-mixer = require '../dist/mixedice'
+mixedice = require '../dist/mixedice'
 {expect} = require 'chai'

 describe 'mixedice', ->
   it 'empty parameters', ->
-    result = mixer()
+    result = mixedice()
     expect(result).to.be('undefined')


Fix error handling in brunch.new.

Fix error handling in brunch.new.

--- src/brunch.coffee
+++ src/brunch.coffee
@@ -52,22 +52,22 @@
       callback result

 exports.new = (rootPath, buildPath, callback = (->)) ->
-  templatePath = path.join __dirname, 'template', 'base'
+  templatePath = path.join __dirname, '..', 'template', 'base'
   path.exists rootPath, (exists) ->
     if exists
       return helpers.logError "[Brunch]: can\'t create project:
 directory \"#{rootPath}\" already exists"

     mkdirp rootPath, 0755, (error) ->
-
+      return helpers.logError "[Brunch]: Error #{error}" if error?
       mkdirp buildPath, 0755, (error) ->
-        helpers.logError "[Brunch]: Error #{error}"
+        return helpers.logError "[Brunch]: Error #{error}" if error?
         helpers.recursiveCopy templatePath, rootPath, ->
           helpers.log '[Brunch]: created brunch directory layout'
           helpers.log '[Brunch]: installing npm packages...'
-          exec "npm install #{rootPath}", (error, stderr, stdout) ->
+          exec "pushd . && cd #{rootPath} && npm install && popd", (error) ->
             if error?
-              helpers.logError "[Brunch]: npm error: #{stderr}"
+              helpers.logError "[Brunch]: npm error: #{error}"
               return callback error
             helpers.log '[Brunch]: installed npm package brunch-extensions'
             callback()


Added test for the word class

Added test for the word class

--- test/rte-test.coffee
+++ test/rte-test.coffee
@@ -9,7 +9,7 @@
 chai.use(sinonChai)
 chai.config.includeStack = true

-[Rte, Selection] = require '../lib/y-rte'
+[Rte, Selection, Word] = require '../lib/y-rte'

 describe 'Rich Text type should', ->
@@ -116,7 +116,7 @@
     rte1.delta delta

 describe 'Selection object should', ->
-  sel = sel2 = rte = null
+  sel = sel2 = rte = word = null

   it 'be initialized with three parameters', ->
     rte = new Rte "Zero One two three four five"
@@ -143,3 +143,16 @@
     sel2.should.have.deep.property 'startPos.pos', 0
     sel2.should.have.deep.property 'endPos.word', 2
     sel2.should.have.deep.property 'endPos.pos', 1
+
+describe 'Word objects should', ->
+  sel = sel2 = rte = word = null
+  it 'be linked correctly to selections', ->
+    rte = new Rte "This is a test"
+    sel = new Selection 0, 6, rte
+    word = rte.getWord(0)
+    word.left[0].equals(sel).should.be.true
+    word.right.length.should.equals 0
+
+    word = rte.getWord(1)
+    word.left.length.should.equals 0
+    word.right[0].equals(sel).should.be.true


Removing unneeded logger.

Removing unneeded logger.

--- src/canvas.coffee
+++ src/canvas.coffee
@@ -10,11 +10,8 @@
     # Create a canvas linked to the canvas element with the given
     # DOM id.
     constructor : (id) ->
-        @logger = new wolf.Logger("wolf.Canvas")
-        @logger.debug("creating canvas with id #{id}")
         @id = id

-        # Set-up the canvas.
         @canvas = document.getElementById(@id)
         throw new Error("No element with id: #{id}") if not @canvas


docpad.coffee: remove unused getThumbCustom()

--- docpad.coffee
+++ docpad.coffee
@@ -79,10 +79,6 @@
       # Merge the document keywords with the site keywords
       @site.keywords.concat(@document.keywords or []).join(', ')

-    getThumbCustom: (test) ->
-      url = "/images/demos/"
-      path = "#{url}#{test} "
-
     getPagerNext: (collection) ->
       docsCollection = @getCollection(collection)
       for item,index in docsCollection.models


Track pending messages and add to history once confirmed

--- src/client.coffee
+++ src/client.coffee
@@ -30,6 +30,7 @@
     @socketUrl      = null
     @ws             = null
     @_messageID     = 0
+    @_pending       = {}

   #
   # Logging in and connection management functions
@@ -210,7 +211,13 @@
         if message.reply_to
           if message.ok
             console.log "Message "+message.reply_to+" was sent"
-            # TODO: Now add to history
+            if @_pending[message.reply_to]
+              m = @_pending[message.reply_to]
+              channel = @getChannelGroupOrDMByID m
+              if channel
+                channel.addMessage m
+
+              delete @_pending[message.reply_to]
           else
             console.error "Error sending message "+message.reply_to+": "+message.error.msg
             # TODO: resend?
@@ -227,6 +234,7 @@
       console.error "Cannot send when not connected"
     else
       message.id = ++@_messageID
+      @_pending[message.id] = message
       @ws.send JSON.stringify(message)

   _apiCall: (method, params, callback) ->


Fix everything else for lodash 4.x

Fix everything else for lodash 4.x

--- src/client.coffee
+++ src/client.coffee
@@ -76,7 +76,7 @@
     .reduce(((a, b) -> a + b), 0)) / 100
   goodIndex = null
   $("#selection option").attr "disabled", (i) ->
-    if _.contains(paths, Paths[i].abbr)
+    if _.includes(paths, Paths[i].abbr)
       yes
     else
       goodIndex ?= i
@@ -263,8 +263,8 @@
       _.each duplicates, (otherGame, idx2) ->
         if idx2 isnt idx
           otherGame.drop = yes
-      game.acfSource = _.pluck duplicates, 'acfSource'
-      game.acfDest = _.pluck duplicates, 'acfDest'
+      game.acfSource = _.map duplicates, 'acfSource'
+      game.acfDest = _.map duplicates, 'acfDest'
       game
     .reject 'drop'
     .value()


Make more public friendly

Make more public friendly

--- src/coffee.coffee
+++ src/coffee.coffee
@@ -1,11 +1,11 @@
-# Description:
-#   Kira Caffienation Support Tools
+# Description
+#   A hubot script for announcing & claiming coffee
 #
-# Dependencies:
-#    "dateformat": "1.0.x"
+# Configuration:
+#   HUBOT_COFFEE_DIBS_LIMIT Set the dibs limit (Default 6)
+#   HUBOT_COFFEE_DIBS_DURATION Set the dibs time limit in seconds (Default 600 seconds (10 minutes))
 #
 # Commands:
-#   hubot coffee [cups] - What you need to make [cups] cups of coffee.
 #   hubot brewing - Start a new brewing lineup
 #   hubot fresh pot - Announce that a pot is finished
 #   hubot fp - Alias for: hubot fresh pot
@@ -13,15 +13,24 @@
 #   hubot coffee balance [user] - Check your coffee balance (defaults to you)
 #   hubot coffee top [n] - See the top coffee balances (defaults to 10)
 #   hubot coffee bottom [n] - See the bottom coffee balances (defaults to 10)
-
-# water = 3/16 * cups
-# scoops = .75 * cups + 2
+#
+#
+# Author:
+#   brent@kiratalent.com, Kira Talent Inc.

 dateformat = require 'dateformat'
-StatusEmoji = require './status-emoji'

 dibsLimit = process.env.HUBOT_COFFEE_DIBS_LIMIT || 6
-dibsDuration = process.env.HUBOT_COFFEE_DIBS_DURATION || (60*10)
+dibsDuration = process.env.HUBOT_COFFEE_DIBS_DURATION || (60 * 10)
+
+class StatusEmoji
+  success: [':dancer:', ':relaxed:', ':grin:', ':smile:', ':sunglasses:'],
+  pending: [':worried:', ':see_no_evil:', ':no_mouth:', ':neutral_face:']
+  failure: [':cry:', ':angry:', ':fearful:', ':sob:', ':disappointed:']
+
+  random: (status) ->
+    emojis = @[status] || []
+    return emojis[Math.floor(Math.random() * emojis.length)]

 freshPots = [
   "http://stream1.gifsoup.com/view6/3131142/chug-coffee-o.gif",
@@ -59,18 +68,10 @@

   brewing = {}
   robot.brain.on 'loaded', =>
-    robot.brain.data.coffee ?= { brewing: {} }
+    robot.brain.data.coffee ?= {brewing: {}}
     brewing = robot.brain.data.coffee.brewing ?= {}

   coffeeconomy = new Coffeeconomy robot
-
-  robot.respond /coffee (\d+)$/i, (msg) ->
-    cups = msg.match[1]
-
-    water = (3/16) * cups
-    scoops = (0.75 * cups) + 2
-
-    msg.send "You need #{scoops} level scoops and #{water} L of water."

   robot.respond /coffee balance(?:\s+@?([^ ]+)\s*)?$/i, (msg) ->
     clientName = msg.match[1] || msg.message.user.name
@@ -81,27 +82,35 @@
       return

     if not coffeeconomy.hasAccount client
-      msg.send "#{client.name} hasn't opened an account by `#{robot.alias}brewing` or `#{robot.alias}dibs`ing yet! #{statusEmoji.random('failure')}"
+      msg.send "#{client.name} hasn't opened an account by `#{robot.alias}brewing` or " +
+          "`#{robot.alias}dibs`ing yet! #{statusEmoji.random('failure')}"
       return
     msg.send coffeeconomy.accountFormatter coffeeconomy.account client

   robot.respond /coffee top(?:\s+(\d+))?$/i, (msg) ->
     limit = parseInt msg.match[1]?.trim() || 10
     count = 1
-    msg.send "These are the top :coffee: accounts:\n" +
-        (coffeeconomy.topListEntryFormatter count++, account for account in coffeeconomy.top limit).join("\n")
+    topList = (
+      coffeeconomy.topListEntryFormatter count++, account\
+      for account in coffeeconomy.top limit
+    )
+    msg.send "These are the top :coffee: accounts:\n #{topList.join('\n')}"

   robot.respond /coffee bottom(?:\s+(\d+))?$/i, (msg) ->
     limit = parseInt msg.match[1]?.trim() || 10
     total = coffeeconomy.length()

-    msg.send "These are the bottom :coffee: accounts:\n" +
-        (coffeeconomy.topListEntryFormatter total--, account for account in coffeeconomy.bottom limit).join("\n")
+    bottomList = (
+      coffeeconomy.topListEntryFormatter total--, account\
+      for account in coffeeconomy.bottom limit
+    )
+
+    msg.send "These are the bottom :coffee: accounts:\n #{bottomList.join('\n')}"

   robot.respond /brewing\s*$/i, (msg) ->
     if isBrewing()
-      msg.send "A brew is already underway by (@#{brewing.barista})! #{statusEmoji.random('failure')}\n" +
-        freeSpots()
+      msg.send "A brew is already underway by (@#{brewing.barista})! " +
+          "#{statusEmoji.random('failure')}\n" + freeSpots()
       return

     createBrew msg
@@ -126,18 +135,20 @@

     brewing.dibs.push(dibber)

-    threadedMsg(msg).send "Ok @#{dibber}, you grabbed :coffee: ##{brewing.dibs.length}! #{statusEmoji.random('success')}"
+    threadedMsg(msg).send "Ok @#{dibber}, you grabbed :coffee: ##{brewing.dibs.length}! " +
+        "#{statusEmoji.random('success')}"

   isBrewing = ->
     return brewing.dibs?

   threadedMsg = (msg) ->
-    # Ensure that the vote response sticks to the original thread
-    metadata = msg.envelope.metadata || {}
-    metadata.room = brewing.messageMetadata.room if brewing.messageMetadata.room
-    metadata.thread_id = brewing.messageMetadata.thread_id if brewing.messageMetadata.thread_id
-    metadata.message_id = brewing.messageMetadata.message_id if brewing.messageMetadata.message_id
-    msg.envelope.metadata = metadata
+# Ensure that the vote response sticks to the original thread if using the flowdock adapter
+    if robot.adatperName = 'flowdock'
+      metadata = msg.envelope.metadata || {}
+      metadata.room = brewing.messageMetadata.room if brewing.messageMetadata.room
+      metadata.thread_id = brewing.messageMetadata.thread_id if brewing.messageMetadata.thread_id
+      metadata.message_id = brewing.messageMetadata.message_id if brewing.messageMetadata.message_id
+      msg.envelope.metadata = metadata
     msg

   handleNoBrew = (msg) ->
@@ -148,49 +159,55 @@
     brewing =
       barista: barista
       dibs: [barista],
-      messageMetadata: msg.envelope.message.metadata,
+      messageMetadata: msg.envelope.message?.metadata,

     robot.brain.save()

     msg.send "@team Brew started by @#{brewing.barista}! #{statusEmoji.random('success')}\n" +
-      "To grab a spot use: #{robot.alias}dibs\n" +
-      "To end the brew use: #{robot.alias}fresh pot"
+        "To grab a spot use: #{robot.alias}dibs\n" +
+        "To end the brew use: #{robot.alias}fresh pot"

   endBrew = (msg) ->
-
     teamNotify = brewing.dibs.length < dibsLimit

-    expiry = new Date (new Date()).getTime() + dibsDuration*1000
+    expiry = new Date (new Date()).getTime() + dibsDuration * 1000

     claims = []
     for i in [1..dibsLimit]
-      dibber = brewing.dibs[i-1]
+      dibber = brewing.dibs[i - 1]

       if dibber
         client = robot.brain.userForName dibber

         if dibber == brewing.barista
-          coffeeconomy.deposit client, dibsLimit-1
+          coffeeconomy.deposit client, dibsLimit - 1
         else
           coffeeconomy.withdraw client, 1

         account = coffeeconomy.account client

-        claims.push("**@#{dibber}** " +
-          "#{if dibber == brewing.barista then '_(barista)_ :chart_with_upwards_trend:' else ':chart_with_downwards_trend:'} " +
-          "#{coffeeconomy.balanceFormatter account.balance}")
+        dibberLabel = "**@#{dibber}**"
+
+        if dibber == brewing.barista
+          txIcon = ":chart_with_upwards_trend:"
+          dibberLabel += " _(barista)_"
+        else
+          txIcon = ":chart_with_downwards_trend:"
+
+        claims.push("#{dibberLabel} #{txIcon} " +
+            "#{coffeeconomy.balanceFormatter account.balance}")
       else
         claims.push "_Unclaimed!_"

     threadMsg = threadedMsg(msg)
     threadMsg.send "#{if teamNotify then '@team: ' else ''}Fresh Pot!!! #{msg.random freshPots} " +
-      ":coffee: Claims (valid until #{dateformat expiry, 'h:MM:ss tt'}):\n" +
-      ("#{(parseInt index)+1}. #{claim}" for index, claim of claims).join("\n")
+        ":coffee: Claims (valid until #{dateformat expiry, 'h:MM:ss tt'}):\n" +
+        ("#{(parseInt index) + 1}. #{claim}" for index, claim of claims).join("\n")

     if claims.length > 1
       setTimeout ->
         threadMsg.send "Claims have expired!"
-      , dibsDuration*1000
+      , dibsDuration * 1000

     brewing = {}
     robot.brain.save()
@@ -201,7 +218,7 @@

 class Coffeeconomy
-  constructor: (@robot, brain_ns='coffeeconomy') ->
+  constructor: (@robot, brain_ns = 'coffeeconomy') ->
     @brain = @robot.brain
     @storage = {}
     @brain.on 'loaded', =>
@@ -218,7 +235,8 @@
     @brain.userForId accountNumber

   accountFormatter: (account) ->
-    "**#{@clientFormatter account.accountNumber}**: #{@balanceFormatter account.balance} (#{@accountDetailsFormatter account})"
+    "**#{@clientFormatter account.accountNumber}**: #{@balanceFormatter account.balance} " +
+      "(#{@accountDetailsFormatter account})"

   shortDateFormatter: (date) ->
     dateformat date, "mmm d yyyy h:MMtt"
@@ -233,7 +251,9 @@
     "#{balance} :coffee:#{unless balance == 1 then 's' else ''}"

   accountDetailsFormatter: (account) ->
-    "_+#{account.totals.deposited}_ / _-#{account.totals.withdrawn}_, _Txs:_ #{account.transactions}, _Opened:_ #{@shortDateFormatter account.created}#{if account.updated then ", _Updated:_ #{@longDateFormatter account.updated}" else ""}"
+    "_+#{account.totals.deposited}_ / _-#{account.totals.withdrawn}_, " +
+      "_Txs:_ #{account.transactions}, _Opened:_ #{@shortDateFormatter account.created}" +
+      " #{if account.updated then ", _Updated:_ #{@longDateFormatter account.updated}" else ""}"

   topListEntryFormatter: (index, account) ->
     "_#{index}._ **#{@clientFormatter account.accountNumber}** #{@balanceFormatter account.balance}"
@@ -253,7 +273,10 @@
       account.totals.withdrawn -= amount
     account.transactions++
     @saveAccount account
-    @robot.logger.info "#{amount} :coffee:s #{if amount > 0 then 'deposited to' else 'withdrawn from'} #{client.name} (#{client.id})"
+
+    txType = if amount > 0 then 'deposited to' else 'withdrawn from'
+
+    @robot.logger.info "#{amount} :coffee:s #{txType} #{client.name} (#{client.id})"

   balance: (client) ->
     (@account client).balance
@@ -279,9 +302,15 @@
     @brain.save()

   orderedAccounts: (descending = true, limit = false) ->
-    accounts = ( { accountNumber: accountNumber, balance: account.balance } for accountNumber, account of @storage )
-    accounts.sort -> arguments[if descending then 1 else 0].balance - arguments[if descending then 0 else 1].balance
-    accounts[...(limit||accounts.length())]
+    accounts = ( {
+      accountNumber: accountNumber,
+      balance: account.balance
+    } for accountNumber, account of @storage )
+
+    first = if descending then 1 else 0
+    second = if descending then 0 else 1
+    accounts.sort -> arguments[first].balance - arguments[second].balance
+    accounts[...(limit || accounts.length())]

   top: (limit = 10) ->
     @orderedAccounts true, limit


Fixing typo

Fixing typo

--- src/device.coffee
+++ src/device.coffee
@@ -7,18 +7,18 @@

 window.Device =

-  addEvent: (element, event, callback) ->
+  addEvent: (element, eventName, callback) ->
     if element.addEventListener
-      element.addEventListener event, callback, false
+      element.addEventListener eventName, callback, false
     else if element.attachEvent
-      element.attachEvent "on#{event}", callback
+      element.attachEvent "on#{eventName}", callback
     else
-      element["on#{event}"] = callback
+      element["on#{eventName}"] = callback

-  removeEvent: (element, event, callback) ->
+  removeEvent: (element, eventName, callback) ->
     if element.removeEventListener
-      element.removeEventListener event, callback, false
+      element.removeEventListener eventName, callback, false
     else if element.detachEvent
-      element.detachEvent "on" + event, callback
+      element.detachEvent "on" + eventName, callback
     else
-      element["on#{event}"] = null
+      element["on#{eventName}"] = null


config model handles creating new ab split campaign

--- app/assets/javascripts/backbone/models/config.js.coffee
+++ app/assets/javascripts/backbone/models/config.js.coffee
@@ -10,4 +10,111 @@
       true
     else
       false
+
+  createCampaign: ->
+    config = this
+    config.updateStatus "Fetching campaigns", 10
+    @fetchCampaigns ->
+      config.updateStatus "Merging campaigns", 40
+      config.createSplit (result)->
+        config.updateStatus "Finished", 100
+        config.set 'web_url', result
+        config.trigger 'create:finished'
+
+
+
+  fetchCampaigns: (cb)->
+    done = 2
+    $([@get('pickA'),@get('pickB')]).each ->
+      campaign = this
+      Mailchimp.API.call 'campaignContent', {cid: campaign.id, for_archive: false}, (content)->
+        campaign.set 'content', content
+        done--
+        if done is 0 then cb()
+
+
+
+  createSplit: (cb)->
+    config = this
+    @split = new Chimpenstein.Models.Campaign
+      type: 'absplit'
+      options:
+        list_id: @get('pickA').get('list_id')
+        title: "A-B Split Combination :: It's ALIVE!!"
+        subject: @get('pickA').get('subject')
+        from_email: @get('pickA').get('from_email')
+        from_name: @get('pickA').get('from_name')
+        to_name: @get('pickA').get('to_name')
+      content:
+        html: @generateHTMLContent()
+        text: @generateTextContent()
+      type_opts:
+        split_test: 'subject'
+        pick_winner: 'opens'
+        subject_a: @get('pickA').get('subject')
+        subject_b: @get('pickB').get('subject')
+    @split.create (id)->
+      config.updateStatus 'Cleaning up', 80
+      Mailchimp.API.call 'campaigns', {filters: {campaign_id: id}}, (res) ->
+        web_id = res.data[0].web_id
+        cb("https://#{Chimpenstein.dc}.admin.mailchimp.com/campaigns/wizard/absplit?id=#{web_id}")
+
+
+  generateHTMLContent: ->
+    pickA = $(@get('pickA').get('content').html
+              .replace(/<body/, "<div class='body'")
+              .replace(/html/, "div class='html'")
+              .replace(/head/, "div class='head'")
+              .replace(/\/body/, "\/div")
+              .replace(/\/head/, "\/div")
+              .replace(/\/html/, "\/div"))
+    pickB = $(@get('pickB').get('content').html
+              .replace(/<body/, "<div class='body'")
+              .replace(/html/, "div class='html'")
+              .replace(/head/, "div class='head'")
+              .replace(/\/body/, "\/div")
+              .replace(/\/head/, "\/div")
+              .replace(/\/html/, "\/div"))
+
+    Chimpenstein.pickA = pickA
+    Chimpenstein.pickB = pickB

+    """
+
+
+      *|GROUP:A|*
+      #{pickA.find('.head').html()}
+      *|END:GROUP|*
+
+      *|GROUP:B|*
+      #{pickB.find('.head').html()}
+      *|END:GROUP|*
+
+
+    *|GROUP:A|*
+
+      #{pickA.find('.body').html()}
+
+    *|END:GROUP|*
+
+    *|GROUP:B|*
+
+      #{pickB.find('.body').html()}
+
+    *|END:GROUP|*
+
+
+    """
+  generateTextContent: ->
+    """
+    *|GROUP:A|*
+    #{@get('pickA').get('content').text}
+    *|END:GROUP|*
+    *|GROUP:B|*
+    #{@get('pickB').get('content').text}
+    *|END:GROUP|*
+    """
+
+
+  updateStatus: (message,percentage)->
+    @trigger 'status:updated', message, percentage


ignore ResizeObserver loop limit exceeded error

--- vue/src/shared/helpers/boot.coffee
+++ vue/src/shared/helpers/boot.coffee
@@ -20,7 +20,8 @@
         Sentry.init
           ignoreErrors: [
             "Avoided redundant navigation to current location",
-            "NotFoundError: Node.removeChild"
+            "NotFoundError: Node.removeChild",
+            "ResizeObserver loop limit exceeded"
           ]
           dsn: AppConfig.sentry_dsn
           integrations: [


added test for updated callback args

added test for updated callback args

--- tests/Graph.spec.coffee
+++ tests/Graph.spec.coffee
@@ -301,6 +301,12 @@
     callback = jasmine.createSpy()
     graph.forEachNode callback
     expect(callback.callCount).toBe 6
+  it "should pass nodeObject and nodeId to the callback", ->
+    callback = jasmine.createSpy()
+    graph.forEachNode callback
+    expect(callback.mostRecentCall.args.length).toBe 2
+    expect(callback.mostRecentCall.args[0] instanceof Object).toBeTruthy
+    expect(callback.mostRecentCall.args[1]).toBe "6"

 describe "Traverse through each edge", ->
   graph = new Graph()


Example group for history-related examples of up.follow()

--- spec_app/spec/javascripts/up/link_spec.js.coffee
+++ spec_app/spec/javascripts/up/link_spec.js.coffee
@@ -64,282 +64,284 @@
         next => @respondWith '
second-div
'
         next => expect($container.text()).toBe('first-linksecond-div')

-      it 'adds history entries and allows the user to use the back and forward buttons', asyncSpec (next) ->
-        up.history.config.enabled = true
-
-        waitForBrowser = 300
-
-        # By default, up.history will replace the  tag when
-        # the user presses the back-button. We reconfigure this
-        # so we don't lose the Jasmine runner interface.
-        up.history.config.restoreTargets = ['.container']
-
-        respondWith = (html, title) =>
-          @respondWith
-            status: 200
-            contentType: 'text/html'
-            responseText: "

#{html}

"
-            responseHeaders: { 'X-Up-Title': title }
-
-#          followAndRespond = ($link, html, title) ->
-#            promise = up.follow($link)
-#            respondWith(html, title)
-#            promise
-
-        up.fragment.config.autoHistory = true
-
-        $link1 = $fixture('a[href="/one"][up-target=".target"]')
-        $link2 = $fixture('a[href="/two"][up-target=".target"]')
-        $link3 = $fixture('a[href="/three"][up-target=".target"]')
-        $container = $fixture('.container')
-        $target = $fixture('.target').appendTo($container).text('original text')
-
-        up.follow($link1)
-
-        next =>
-          respondWith('text from one', 'title from one')
-
-        next =>
-          expect($('.target')).toHaveText('text from one')
-          expect(location.pathname).toEqual('/one')
-          expect(document.title).toEqual('title from one')
-
-          up.follow($link2)
-
-        next =>
-          respondWith('text from two', 'title from two')
-
-        next =>
-          expect($('.target')).toHaveText('text from two')
-          expect(location.pathname).toEqual('/two')
-          expect(document.title).toEqual('title from two')
-
-          up.follow($link3)
-
-        next =>
-          respondWith('text from three', 'title from three')
-
-        next =>
-          expect($('.target')).toHaveText('text from three')
-          expect(location.pathname).toEqual('/three')
-          expect(document.title).toEqual('title from three')
-
-          history.back()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from two', 'restored title from two')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from two')
-          expect(location.pathname).toEqual('/two')
-          expect(document.title).toEqual('restored title from two')
-
-          history.back()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from one', 'restored title from one')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from one')
-          expect(location.pathname).toEqual('/one')
-          expect(document.title).toEqual('restored title from one')
-
-          history.forward()
-
-        next.after waitForBrowser, =>
-          # Since the response is cached, we don't have to respond
-          expect($('.target')).toHaveText('restored text from two')
-          expect(location.pathname).toEqual('/two')
-          expect(document.title).toEqual('restored title from two')
-
-      it 'renders history when the user clicks on a link, goes back and then clicks on the same link (bugfix)', asyncSpec (next) ->
-        up.history.config.enabled = true
-        up.history.config.restoreTargets = ['.target']
-        waitForBrowser = 300
-
-        linkHTML = """
-          label
-        """
-        target = fixture('.target', text: 'old text')
-        link = fixture('a[href="/next"][up-target=".target"][up-history=true]')
-        up.history.replace('/original')
-
-        next ->
-          expect(up.history.location).toMatchURL('/original')
-          expect('.target').toHaveText('old text')
-
-          Trigger.clickSequence(link)
-
-        next ->
-          jasmine.respondWithSelector('.target', text: 'new text')
-
-        next ->
-          expect(up.history.location).toMatchURL('/next')
-          expect('.target').toHaveText('new text')
-
-          history.back()
-
-        next.after waitForBrowser, ->
-          jasmine.respondWithSelector('.target', text: 'old text')
-
-        next ->
-          expect(up.history.location).toMatchURL('/original')
-          expect('.target').toHaveText('old text')
-
-          Trigger.clickSequence(link)
-
-        next ->
-          # Response was already cached
-          expect(up.history.location).toMatchURL('/next')
-          expect('.target').toHaveText('new text')
-
-      it 'does not add additional history entries when linking to the current URL', asyncSpec (next) ->
-        up.history.config.enabled = true
-
-        # By default, up.history will replace the  tag when
-        # the user presses the back-button. We reconfigure this
-        # so we don't lose the Jasmine runner interface.
-        up.history.config.restoreTargets = ['.container']
-
-        up.fragment.config.autoHistory = true
-
-        up.network.config.cacheExpiry = 0
-
-        waitForBrowser = 150
-
-        respondWith = (text) =>
-          @respondWith """
-

-
#{text}

-

+      describe 'history', ->
+
+        it 'adds history entries and allows the user to use the back and forward buttons', asyncSpec (next) ->
+          up.history.config.enabled = true
+
+          waitForBrowser = 300
+
+          # By default, up.history will replace the  tag when
+          # the user presses the back-button. We reconfigure this
+          # so we don't lose the Jasmine runner interface.
+          up.history.config.restoreTargets = ['.container']
+
+          respondWith = (html, title) =>
+            @respondWith
+              status: 200
+              contentType: 'text/html'
+              responseText: "

#{html}

"
+              responseHeaders: { 'X-Up-Title': title }
+
+  #          followAndRespond = ($link, html, title) ->
+  #            promise = up.follow($link)
+  #            respondWith(html, title)
+  #            promise
+
+          up.fragment.config.autoHistory = true
+
+          $link1 = $fixture('a[href="/one"][up-target=".target"]')
+          $link2 = $fixture('a[href="/two"][up-target=".target"]')
+          $link3 = $fixture('a[href="/three"][up-target=".target"]')
+          $container = $fixture('.container')
+          $target = $fixture('.target').appendTo($container).text('original text')
+
+          up.follow($link1)
+
+          next =>
+            respondWith('text from one', 'title from one')
+
+          next =>
+            expect($('.target')).toHaveText('text from one')
+            expect(location.pathname).toEqual('/one')
+            expect(document.title).toEqual('title from one')
+
+            up.follow($link2)
+
+          next =>
+            respondWith('text from two', 'title from two')
+
+          next =>
+            expect($('.target')).toHaveText('text from two')
+            expect(location.pathname).toEqual('/two')
+            expect(document.title).toEqual('title from two')
+
+            up.follow($link3)
+
+          next =>
+            respondWith('text from three', 'title from three')
+
+          next =>
+            expect($('.target')).toHaveText('text from three')
+            expect(location.pathname).toEqual('/three')
+            expect(document.title).toEqual('title from three')
+
+            history.back()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from two', 'restored title from two')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from two')
+            expect(location.pathname).toEqual('/two')
+            expect(document.title).toEqual('restored title from two')
+
+            history.back()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from one', 'restored title from one')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from one')
+            expect(location.pathname).toEqual('/one')
+            expect(document.title).toEqual('restored title from one')
+
+            history.forward()
+
+          next.after waitForBrowser, =>
+            # Since the response is cached, we don't have to respond
+            expect($('.target')).toHaveText('restored text from two')
+            expect(location.pathname).toEqual('/two')
+            expect(document.title).toEqual('restored title from two')
+
+        it 'renders history when the user clicks on a link, goes back and then clicks on the same link (bugfix)', asyncSpec (next) ->
+          up.history.config.enabled = true
+          up.history.config.restoreTargets = ['.target']
+          waitForBrowser = 300
+
+          linkHTML = """
+            label
           """
-
-        $link1 = $fixture('a[href="/one"][up-target=".target"]')
-        $link2 = $fixture('a[href="/two"][up-target=".target"]')
-        $container = $fixture('.container')
-        $target = $fixture('.target').appendTo($container).text('original text')
-
-        up.follow($link1)
-
-        next =>
-          respondWith('text from one')
-
-        next =>
-          expect($('.target')).toHaveText('text from one')
-          expect(location.pathname).toEqual('/one')
-
-          up.follow($link2)
-
-        next =>
-          respondWith('text from two')
-
-        next =>
-          expect($('.target')).toHaveText('text from two')
-          expect(location.pathname).toEqual('/two')
-
-          up.follow($link2)
-
-        next =>
-          respondWith('text from two')
-
-        next =>
-          expect($('.target')).toHaveText('text from two')
-          expect(location.pathname).toEqual('/two')
-
-          history.back()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from one')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from one')
-          expect(location.pathname).toEqual('/one')
-
-          history.forward()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from two')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from two')
-          expect(location.pathname).toEqual('/two')
-
-      it 'does add additional history entries when linking to the current URL, but with a different hash', asyncSpec (next) ->
-        up.history.config.enabled = true
-
-        # By default, up.history will replace the  tag when
-        # the user presses the back-button. We reconfigure this
-        # so we don't lose the Jasmine runner interface.
-        up.history.config.restoreTargets = ['.container']
-
-        up.fragment.config.autoHistory = true
-
-        up.network.config.cacheExpiry = 0
-
-        waitForBrowser = 150
-
-        respondWith = (text) =>
-          @respondWith """
-

-
#{text}

-

-          """
-
-        $link1 = $fixture('a[href="/one"][up-target=".target"]')
-        $link2 = $fixture('a[href="/two"][up-target=".target"]')
-        $link2WithHash = $fixture('a[href="/two#hash"][up-target=".target"]')
-        $container = $fixture('.container')
-        $target = $fixture('.target').appendTo($container).text('original text')
-
-        up.follow($link1)
-
-        next =>
-          respondWith('text from one')
-
-        next =>
-          expect($('.target')).toHaveText('text from one')
-          expect(location.pathname).toEqual('/one')
-          expect(location.hash).toEqual('')
-
-          up.follow($link2)
-
-        next =>
-          respondWith('text from two')
-
-        next =>
-          expect($('.target')).toHaveText('text from two')
-          expect(location.pathname).toEqual('/two')
-          expect(location.hash).toEqual('')
-
-          up.follow($link2WithHash)
-
-        next =>
-          respondWith('text from two with hash')
-
-        next =>
-          expect($('.target')).toHaveText('text from two with hash')
-          expect(location.pathname).toEqual('/two')
-          expect(location.hash).toEqual('#hash')
-
-          history.back()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from two')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from two')
-          expect(location.pathname).toEqual('/two')
-          expect(location.hash).toEqual('')
-
-          history.forward()
-
-        next.after waitForBrowser, =>
-          respondWith('restored text from two with hash')
-
-        next =>
-          expect($('.target')).toHaveText('restored text from two with hash')
-          expect(location.pathname).toEqual('/two')
-          expect(location.hash).toEqual('#hash')
+          target = fixture('.target', text: 'old text')
+          link = fixture('a[href="/next"][up-target=".target"][up-history=true]')
+          up.history.replace('/original')
+
+          next ->
+            expect(up.history.location).toMatchURL('/original')
+            expect('.target').toHaveText('old text')
+
+            Trigger.clickSequence(link)
+
+          next ->
+            jasmine.respondWithSelector('.target', text: 'new text')
+
+          next ->
+            expect(up.history.location).toMatchURL('/next')
+            expect('.target').toHaveText('new text')
+
+            history.back()
+
+          next.after waitForBrowser, ->
+            jasmine.respondWithSelector('.target', text: 'old text')
+
+          next ->
+            expect(up.history.location).toMatchURL('/original')
+            expect('.target').toHaveText('old text')
+
+            Trigger.clickSequence(link)
+
+          next ->
+            # Response was already cached
+            expect(up.history.location).toMatchURL('/next')
+            expect('.target').toHaveText('new text')
+
+        it 'does not add additional history entries when linking to the current URL', asyncSpec (next) ->
+          up.history.config.enabled = true
+
+          # By default, up.history will replace the  tag when
+          # the user presses the back-button. We reconfigure this
+          # so we don't lose the Jasmine runner interface.
+          up.history.config.restoreTargets = ['.container']
+
+          up.fragment.config.autoHistory = true
+
+          up.network.config.cacheExpiry = 0
+
+          waitForBrowser = 150
+
+          respondWith = (text) =>
+            @respondWith """
+

+
#{text}

+

+            """
+
+          $link1 = $fixture('a[href="/one"][up-target=".target"]')
+          $link2 = $fixture('a[href="/two"][up-target=".target"]')
+          $container = $fixture('.container')
+          $target = $fixture('.target').appendTo($container).text('original text')
+
+          up.follow($link1)
+
+          next =>
+            respondWith('text from one')
+
+          next =>
+            expect($('.target')).toHaveText('text from one')
+            expect(location.pathname).toEqual('/one')
+
+            up.follow($link2)
+
+          next =>
+            respondWith('text from two')
+
+          next =>
+            expect($('.target')).toHaveText('text from two')
+            expect(location.pathname).toEqual('/two')
+
+            up.follow($link2)
+
+          next =>
+            respondWith('text from two')
+
+          next =>
+            expect($('.target')).toHaveText('text from two')
+            expect(location.pathname).toEqual('/two')
+
+            history.back()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from one')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from one')
+            expect(location.pathname).toEqual('/one')
+
+            history.forward()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from two')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from two')
+            expect(location.pathname).toEqual('/two')
+
+        it 'does add additional history entries when linking to the current URL, but with a different hash', asyncSpec (next) ->
+          up.history.config.enabled = true
+
+          # By default, up.history will replace the  tag when
+          # the user presses the back-button. We reconfigure this
+          # so we don't lose the Jasmine runner interface.
+          up.history.config.restoreTargets = ['.container']
+
+          up.fragment.config.autoHistory = true
+
+          up.network.config.cacheExpiry = 0
+
+          waitForBrowser = 150
+
+          respondWith = (text) =>
+            @respondWith """
+

+
#{text}

+

+            """
+
+          $link1 = $fixture('a[href="/one"][up-target=".target"]')
+          $link2 = $fixture('a[href="/two"][up-target=".target"]')
+          $link2WithHash = $fixture('a[href="/two#hash"][up-target=".target"]')
+          $container = $fixture('.container')
+          $target = $fixture('.target').appendTo($container).text('original text')
+
+          up.follow($link1)
+
+          next =>
+            respondWith('text from one')
+
+          next =>
+            expect($('.target')).toHaveText('text from one')
+            expect(location.pathname).toEqual('/one')
+            expect(location.hash).toEqual('')
+
+            up.follow($link2)
+
+          next =>
+            respondWith('text from two')
+
+          next =>
+            expect($('.target')).toHaveText('text from two')
+            expect(location.pathname).toEqual('/two')
+            expect(location.hash).toEqual('')
+
+            up.follow($link2WithHash)
+
+          next =>
+            respondWith('text from two with hash')
+
+          next =>
+            expect($('.target')).toHaveText('text from two with hash')
+            expect(location.pathname).toEqual('/two')
+            expect(location.hash).toEqual('#hash')
+
+            history.back()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from two')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from two')
+            expect(location.pathname).toEqual('/two')
+            expect(location.hash).toEqual('')
+
+            history.forward()
+
+          next.after waitForBrowser, =>
+            respondWith('restored text from two with hash')
+
+          next =>
+            expect($('.target')).toHaveText('restored text from two with hash')
+            expect(location.pathname).toEqual('/two')
+            expect(location.hash).toEqual('#hash')

       describe 'scrolling', ->


fine tuning the parameter editor box, removed logging of box position

--- web/scripts/playerCodeEditor.coffee
+++ web/scripts/playerCodeEditor.coffee
@@ -179,10 +179,7 @@
             editorOffset = jQuery('#ace-editor').position()
             gutterOffset = @editor.editor.renderer.$gutterLayer.gutterWidth + \
                 @editor.editor.renderer.$gutterLayer.$padding.left
-            console.log editorOffset
-            console.log gutterOffset
-            console.log "+ X: #{row * 12}, + Y: #{rowLength * 6}"
-            @parameterPopUp.css 'top', row * 12 + editorOffset.top
+            @parameterPopUp.css 'top', row * 12 + editorOffset.top - 3
             @parameterPopUp.css 'left', rowLength * 6 + gutterOffset + editorOffset.left

             @parameterPopUp.show()


the wrappers are broken somehow, fixing

the wrappers are broken somehow, fixing

--- web/scripts/playerCodeEditor.coffee
+++ web/scripts/playerCodeEditor.coffee
@@ -120,8 +120,8 @@
             inputsDiv = jQuery('#insertButtons')
             text.insertLines currentRow, [line]

-            # if text.getLength() == 2 and text.getLine(currentRow + 1) == ""
-            #     text.removeNewLine currentRow
+            if text.getLength() == 2 and text.getLine(currentRow + 1) == ""
+                text.removeNewLine currentRow

             @UpdateCommandsStatus()
         return


profile pic configuration: release camera

fixes CNVS-12086

test plan:
  * enable profiles and avatars
  * open user settings
  * edit profile picture
  * select "take a picture"
  * allow access to your camera
  * leave "take a picture"
  * verify that your camera is no longer in use

Change-Id: I675cb7b71869d55d529f85fce34c028a396d0893
Reviewed-on: https://gerrit.instructure.com/32511
Reviewed-by: Jon Willesen <eb5ce5e6a76c243326fecfa94534101600f87bd9@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: Steven Shepherd <e794458448443812fb2685675aab3bbd1a3891bc@instructure.com>
Product-Review: Braden Anderson <a6cc2cef7d25f27847876bdb81651755fcebacf0@instructure.com>

--- app/coffeescripts/views/profiles/TakePictureView.coffee
+++ app/coffeescripts/views/profiles/TakePictureView.coffee
@@ -51,13 +51,14 @@
     teardown: ->
       delete @img
       delete @preview
+      @stream?.stop()

     startMedia: ->
       @getUserMedia(video: true, @displayMedia, $.noop)

-    displayMedia: (stream) =>
+    displayMedia: (@stream) =>
       @$video.removeClass('pending')
-      @$video.attr('src', window.URL.createObjectURL(stream))
+      @$video.attr('src', window.URL.createObjectURL(@stream))
       @$video.on('onloadedmetadata loadedmetadata', _.once(@onMediaMetadata).bind(this))

     onMediaMetadata: (e) ->


Display total queue duration

Display total queue duration

--- modules/player/hud.coffee
+++ modules/player/hud.coffee
@@ -15,7 +15,7 @@
     Now playing in **#{qI.voiceChannel.name}**:
     >`#{qI.title}` (#{@parseTime qI.duration}) #{@parseFilters qI.filters}

-    Requested by: **#{@getDisplayName qI.requestedBy}**
+    Requested by: **#{@getDisplayName qI.requestedBy}**
     """

   swapItems: (user, items, indexes)=>
@@ -24,7 +24,7 @@
     ```fix
     * #{indexes[1]+1} -> #{indexes[0]+1}
       #{items[0].title}
-
+
     * #{indexes[0]+1} -> #{indexes[1]+1}
       #{items[1].title}
     ```
@@ -63,7 +63,7 @@
     fstr = @parseFilters(item.filters)
     reply.description = "**Filters**: #{fstr}" if fstr
     reply
-
+
   removeItem: (item, removedBy)=>
     reply =
       url: item.sauce
@@ -80,7 +80,7 @@
     if removedBy
       reply.footer =
         icon_url: removedBy.avatarURL
-        text: "Removed by #{@getDisplayName removedBy}"
+        text: "Removed by #{@getDisplayName removedBy}"
     fstr = @parseFilters(item.filters)
     reply.description = "**Filters**: #{fstr}" if fstr
     reply
@@ -109,7 +109,7 @@
         icon_url: qI.requestedBy.avatarURL
       fields: [
         { inline: true, name: 'Length', value: @parseTime qI.duration }
-
+
       ]
     }
     if qI.filters and qI.filters.length
@@ -119,6 +119,10 @@
   queue: (q, page=1)=>

     return { description: 'Nothing currently on queue.' } if not q.items.length
+
+    # Calculate total time
+    totalTime = 0
+    totalTime += el.duration for el in q.items

     itemsPerPage = 10
     pages = Math.ceil(q.items.length / itemsPerPage)
@@ -130,7 +134,7 @@
       title: "Up next"
       description: ''
       footer:
-        text: "#{q.items.length} total items. (Page #{page}/#{pages})"
+        text: "#{q.items.length} total items. [#{@parseTime totalTime}] (Page #{page}/#{pages})"
     }

     offset = (page-1) * itemsPerPage
@@ -139,10 +143,10 @@
     for qI, i in q.items.slice offset, max
       r.description += "**#{offset+i+1}.** [#{qI.title}](#{qI.sauce}) #{@parseFilters qI.filters}" +
                         "(#{@parseTime qI.duration}) Requested By #{@getDisplayName qI.requestedBy}\n"
-
+
     r.description += "Use #{@prefix}queue #{page+1} to see the next page." if page < pages
     r
-
+
   ###
   # Functions
   ###


we only need the setTimeout on the first window load. must be some magic caching going on somewhere.

--- src/editor.coffee
+++ src/editor.coffee
@@ -49,12 +49,13 @@
       win: null
       mac: shortcut
       sender: 'editor'
-exports.resize = (e) ->
+exports.resize = (timeout=1) ->
   setTimeout ->
     editor.focus()
     editor.resize()
-  , 1
+  , timeout

+exports.resize(200)

 bindKey 'open', 'Command-O', (env, args, request) ->
   if file = Chrome.openPanel()


separate tracking deltas and silent changes

--- src/editor.coffee
+++ src/editor.coffee
@@ -56,19 +56,15 @@
     TandemEditor.editors.push(this)

   disable: ->
-    this.trackDelta( =>
+    this.doSilently( =>
       @doc.root.setAttribute('contenteditable', false)
-    , false)
+    )

   enable: ->
     if !@doc.root.getAttribute('contenteditable')
-      this.trackDelta( =>
+      this.doSilently( =>
         @doc.root.setAttribute('contenteditable', true)
-      , false)
-      #@doc.root.focus()
-      #position = Tandem.Position.makePosition(this, @options.cursor)
-      #start = new Tandem.Range(this, position, position)
-      #this.setSelection(start)
+      )

   initListeners: ->
     modified = false
@@ -103,13 +99,15 @@
   # applyAttribute: (TandemRange range, String attr, Mixed value) ->
   # applyAttribute: (Number index, Number length, String attr, Mixed value) ->
   applyAttribute: (index, length, attr, value, emitEvent = true) ->
-    delta = this.trackDelta( =>
-      @selection.preserve( =>
-        this.keepNormalized( =>
-          @doc.applyAttribute(index, length, attr, value)
-        )
-      )
-    , emitEvent)
+    this.doSilently( =>
+      delta = this.trackDelta( =>
+        @selection.preserve( =>
+          this.keepNormalized( =>
+            @doc.applyAttribute(index, length, attr, value)
+          )
+        )
+      , emitEvent)
+    )
     this.emit(TandemEditor.events.TEXT_CHANGE, delta) if emitEvent

   applyDelta: (delta) ->
@@ -152,14 +150,22 @@
     console.assert(_.isEqual(composed, newDelta), oldDelta, delta, composed, newDelta)

   deleteAt: (index, length, emitEvent = true) ->
-    delta = this.trackDelta( =>
-      @selection.preserve( =>
-        this.keepNormalized( =>
-          @doc.deleteText(index, length)
-        )
-      )
-    , emitEvent)
+    this.doSilently( =>
+      delta = this.trackDelta( =>
+        @selection.preserve( =>
+          this.keepNormalized( =>
+            @doc.deleteText(index, length)
+          )
+        )
+      , emitEvent)
+    )
     this.emit(TandemEditor.events.TEXT_CHANGE, delta) if emitEvent
+
+  doSilently: (fn) ->
+    oldIgnoreDomChange = @ignoreDomChanges
+    @ignoreDomChanges = true
+    fn()
+    @ignoreDomChanges = oldIgnoreDomChange

   getAt: (index, length) ->
     # - Returns array of {text: "", attr: {}}
@@ -176,58 +182,54 @@
     return @selection.getRange()

   insertAt: (index, text, emitEvent = true) ->
-    delta = this.trackDelta( =>
-      @selection.preserve( =>
-        this.keepNormalized( =>
-          @doc.insertText(index, text)
-        )
-      )
-    , emitEvent)
+    this.doSilently( =>
+      delta = this.trackDelta( =>
+        @selection.preserve( =>
+          this.keepNormalized( =>
+            @doc.insertText(index, text)
+          )
+        )
+      , emitEvent)
+    )
     this.emit(TandemEditor.events.TEXT_CHANGE, delta) if emitEvent

   setSelection: (range) ->
     @selection.setRange(range)

-  trackDelta: (fn, track = true) ->
-    oldIgnoreDomChange = @ignoreDomChanges
-    @ignoreDomChanges = true
-    delta = null
-    if track
-      oldDelta = @doc.toDelta()
-      fn()
-      newDelta = @doc.toDelta()
-      decompose = JetSync.decompose(oldDelta, newDelta)
-      compose = JetSync.compose(oldDelta, decompose)
-      console.assert(_.isEqual(compose, newDelta), oldDelta, newDelta, decompose, compose)
-      delta = decompose
-    else
-      fn()
-    @ignoreDomChanges = oldIgnoreDomChange
+  trackDelta: (fn) ->
+    oldDelta = @doc.toDelta()
+    fn()
+    newDelta = @doc.toDelta()
+    decompose = JetSync.decompose(oldDelta, newDelta)
+    compose = JetSync.compose(oldDelta, decompose)
+    console.assert(_.isEqual(compose, newDelta), oldDelta, newDelta, decompose, compose)
     return delta

   update: ->
-    delta = this.trackDelta( =>
-      @selection.preserve( =>
-        Tandem.Document.normalizeHtml(@doc.root)
-        lines = @doc.lines.toArray()
-        lineNode = @doc.root.firstChild
-        _.each(lines, (line, index) =>
-          while line.node != lineNode
-            if line.node.parentNode == @doc.root
-              newLine = @doc.insertLineBefore(lineNode, line)
-              lineNode = lineNode.nextSibling
-            else
-              @doc.removeLine(line)
-              return
-          @doc.updateLine(line)
-          lineNode = lineNode.nextSibling
-        )
-        while lineNode != null
-          newLine = @doc.appendLine(lineNode)
-          lineNode = lineNode.nextSibling
-      )
-      @selection.update(true)
-    , true)
+    this.doSilently( =>
+      delta = this.trackDelta( =>
+        @selection.preserve( =>
+          Tandem.Document.normalizeHtml(@doc.root)
+          lines = @doc.lines.toArray()
+          lineNode = @doc.root.firstChild
+          _.each(lines, (line, index) =>
+            while line.node != lineNode
+              if line.node.parentNode == @doc.root
+                newLine = @doc.insertLineBefore(lineNode, line)
+                lineNode = lineNode.nextSibling
+              else
+                @doc.removeLine(line)
+                return
+            @doc.updateLine(line)
+            lineNode = lineNode.nextSibling
+          )
+          while lineNode != null
+            newLine = @doc.appendLine(lineNode)
+            lineNode = lineNode.nextSibling
+        )
+        @selection.update(true)
+      , true)
+    )
     return delta


add cursor interface

add cursor interface

--- src/editor.coffee
+++ src/editor.coffee
@@ -13,6 +13,7 @@

   @CONTAINER_ID: 'tandem-container'
   @ID_PREFIX: 'editor-'
+  @CURSOR_PREFIX: 'cursor-'
   @DEFAULTS:
     cursor: 0
     enabled: true
@@ -30,6 +31,19 @@
     @destructors = []
     this.reset(true)
     this.enable() if @options.enabled
+
+  makeCursor: ->
+    span = @doc.root.ownerDocument.createElement('span')
+    span.classList.add('cursor')
+    span.classList.add(Tandem.Constants.SPECIAL_CLASSES.EXTERNAL)
+
+  addCursor: (index) ->
+    cursor = this.makeCursor()
+    cursor.id = _.uniqueId(TandemEditor.CURSOR_PREFIX)
+    return cursor.id
+
+  moveCursor: (id) ->
+    cursor = @doc.root.ownerDocument.getElementById(id)

   destroy: ->
     this.disable()


Make hiddentInput bigger when inputing with IME.

Some IME's input bubble would have strange behavior when they thought
the target input box is small.

--- src/editor.coffee
+++ src/editor.coffee
@@ -705,11 +705,13 @@
     startScreenPosition = null
     @hiddenInput.on 'compositionstart', =>
       startScreenPosition = @getCursorScreenPosition()
+      @hiddenInput.css('width', '100%')
     @hiddenInput.on 'compositionupdate', (e) =>
       @insertText(e.originalEvent.data)
       @selectToScreenPosition(startScreenPosition)
     @hiddenInput.on 'compositionend', =>
       @delete()
+      @hiddenInput.css('width', '1px')
       startScreenPosition = null

   selectOnMousemoveUntilMouseup: ->


fix filesystem tests

fix filesystem tests

--- tests/filesystem.coffee
+++ tests/filesystem.coffee
@@ -112,8 +112,8 @@
                         fs.existsSync(folderPath).should.be.true

                         pouch.folders.all (err, docs) ->
-                            docs.rows.length.should.equal 3
-                        done()
+                            docs.rows.length.should.equal 5
+                            done()

     describe "applyFileDBChanges", ->
@@ -167,7 +167,7 @@
                 createFileDocument (err, res) ->
                     should.not.exist err

-                    filesystem.applyFileDBChanges false, (err, res) ->
+                    filesystem.applyFileDBChanges (err, res) ->
                         should.not.exist err
                         fs.existsSync(filePath).should.be.true
                         done()
@@ -206,7 +206,7 @@
         it "creates a DB document from a local folder information", (done) ->
             fs.mkdir dirPath, (err) ->
                 should.not.exist err
-                filesystem.createDirectoryDoc dirPath, false, (err, res) ->
+                filesystem.createDirectoryDoc dirPath, (err, res) ->
                     should.not.exist err
                     should.exist res._id
                     pouch.db.query 'folder/byFullPath', key: "/#{dirName}", (err, res) ->
@@ -219,7 +219,7 @@
         it "creates parent directory DB doc", (done) ->
             mkdirp dirPath2, (err) ->
                 should.not.exist err
-                filesystem.createDirectoryDoc dirPath2, false, (err, res) ->
+                filesystem.createDirectoryDoc dirPath2, (err, res) ->
                     should.not.exist err
                     should.exist res._id
                     pouch.db.query 'folder/byFullPath', key: "/#{parentDirName}", (err, res) ->
@@ -236,7 +236,7 @@

         it "does not update DB document when folder exists", (done) =>
-            filesystem.createDirectoryDoc dirPath, true, (err, res) =>
+            filesystem.createDirectoryDoc dirPath, (err, res) =>
                 pouch.db.query 'folder/byFullPath', key: "/#{dirName}", (err, res) =>
                     should.not.exist err
                     res.rows.length.should.not.equal 0
@@ -255,7 +255,7 @@
         it "creates a DB document from a local file's information", (done) =>
             fs.writeFile filePath, 'hello', (err) =>
                 should.not.exist err
-                filesystem.createFileDoc filePath, false, (err, res) =>
+                filesystem.createFileDoc filePath, (err, res) =>
                     should.not.exist err
                     should.exist res.id
                     pouch.db.query 'file/byFullPath', key: "/#{fileName}", (err, res) =>
@@ -271,7 +271,7 @@
             mkdirp '/tmp/cozy/test_parent_dir2', (err) ->
                 fs.writeFile filePath2, 'hello', (err) ->
                     should.not.exist err
-                    filesystem.createFileDoc filePath2, false, (err, res) ->
+                    filesystem.createFileDoc filePath2, (err, res) ->
                         should.not.exist err
                         should.exist res.id
                         pouch.db.query 'folder/byFullPath', key: "/#{parentDirName}", (err, res) ->
@@ -290,7 +290,7 @@

         it "does not update DB document when file exists", (done) =>
-            filesystem.createFileDoc filePath, true, (err, res) =>
+            filesystem.createFileDoc filePath, (err, res) =>
                 pouch.db.query 'file/byFullPath', key: "/#{fileName}", (err, res) =>
                     should.not.exist err
                     for key, value in res.rows[0].value
@@ -300,7 +300,7 @@
         it "updates DB documents when file has changed", (done) =>
             setTimeout =>
                 fs.writeFile filePath, 'hello2', (err) =>
-                    filesystem.createFileDoc filePath, false, (err, res) =>
+                    filesystem.createFileDoc filePath, (err, res) =>
                         pouch.db.query 'file/byFullPath', key: "/#{fileName}", (err, res) =>
                             should.not.exist err
                             should.exist res.rows[0].value


rename preDraw to beforeDraw to follow naming conventions

--- src/engine.coffee
+++ src/engine.coffee
@@ -76,7 +76,7 @@

   The current camera transform <b>is</b> applied.

-  @name preDraw
+  @name beforeDraw
   @methodOf Engine#
   @event
   ###
@@ -142,7 +142,7 @@
         else if I.backgroundColor
           canvas.fill(I.backgroundColor)

-        self.trigger "preDraw", canvas
+        self.trigger "beforeDraw", canvas

         if I.zSort
           drawObjects = I.objects.copy().sort (a, b) ->


namespaced Movable

namespaced Movable

--- src/engine.coffee
+++ src/engine.coffee
@@ -50,7 +50,7 @@
   @name update
   @methodOf Engine#
   @event
-  @param {Number} elapsed
+  @param {Number} elapsedTime The time in seconds
   ###

   ###*


Removing dev comment

Removing dev comment

--- src/entity.coffee
+++ src/entity.coffee
@@ -66,17 +66,6 @@
   # ---------------------
   # Collision detection
   # ---------------------
-
-
-  # Collisions need to be detected and resolved AFTER you retrieve the user input and apply all the transformations to the object.
-  # So, for instance, your board would have a bounding box equal to the board width and height.
-  # To check for a collision with the outer bounds you would ask if your player object is outside the board's bounding box.
-  # If so, then you need to apply transformations to the object's x and y coordinates so that it is inside the box.
-  # The main conceptual point, I suppose, is that you shouldn't be checking for collisions as a precondition for user input.
-  # Instead, you should be checking for collisions as a result of user input, and then applying transformations to resolve it.
-
-
-
   inRowSpace: (e) ->
     # To collide with a tile from the left or right, you must be in its row
     @futureBottom() > e.top() and @futureTop() < e.bottom()


Atom: update linter paths

Atom: update linter paths

--- atom/config.cson
+++ atom/config.cson
@@ -61,16 +61,19 @@
     lintOnEditorFocus: false
   "linter-eslint":
     disableWhenNoEslintConfig: false
-  "linter-flake8": {}
-  "linter-pylint": {}
+  "linter-flake8":
+    executablePath: "/Users/jonklo/.pyenv/versions/tienda-3.6.7/bin/flake8"
+  "linter-pylint":
+    executablePath: "/Users/jonklo/.pyenv/versions/tienda-3.6.7/bin/pylint"
   "linter-stylelint":
     showIgnored: true
   "linter-ui-default":
     panelHeight: 236
     showPanel: true
-  "python-black": {}
+  "python-black":
+    binPath: "/Users/jonklo/.pyenv/versions/tienda-3.6.7/bin/black"
   "python-isort":
-    isortPath: "/Users/jonklo/miniconda3/envs/tienda/bin/isort"
+    isortPath: "/Users/jonklo/.pyenv/versions/tienda-3.6.7/bin/isort"
     sortOnSave: true
   "tree-view":
     hideIgnoredNames: true


Deactivate linter-flake8 package

Deactivate linter-flake8 package

It is currently broken.

--- atom/config.cson
+++ atom/config.cson
@@ -51,6 +51,7 @@
       "teletype"
       "spell-check"
       "linter-shellcheck"
+      "linter-flake8"
     ]
     followSymlinks: false
     ignoredNames: [


Rewrite Atom init script to auto-detect virtualenvs

--- atom/init.coffee
+++ atom/init.coffee
@@ -1,14 +1,38 @@
 # Your init script

-# If PATH does not already contain /usr/local/bin
-if process.env.PATH.indexOf('/usr/local/bin:') == -1
-
-  # Make environment variables available to Atom
-  process.env.PATH = ['/usr/local/bin', process.env.PATH].join(':')
+fs = require 'fs'
+path = require 'path'
+DEFAULT_WORKON_HOME = path.join process.env.HOME, '.virtualenvs'

+# Get path to directory containing all Python virtualenvs
+getVirtualenvHome = ->
+
+  if process.env.WORKON_HOME
+    return process.env.WORKON_HOME
+  else
+    return DEFAULT_WORKON_HOME
+
+
+# Activate Python virtualenv for this project directory if it exists
+activateVirtualenv = ->
+
+  project = atom.project.getPaths()[0]
+  if project
+    virtualenv = path.join getVirtualenvHome(), path.basename(project)
+    # Do not activate virtualenv if project is itself a virtualenv
+    if virtualenv != project
+      fs.lstat virtualenv, (err, stats) ->
+        if !err and stats.isDirectory()
+          process.env.PATH = [virtualenv, process.env.PATH].join ':'
+          process.env.VIRTUAL_ENV = virtualenv
+          console.log 'Activated virtualenv at', virtualenv
+
+
+# Explicitly define PATH and detect project virtualenvs
+process.env.PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
+activateVirtualenv()
 # Ensure that Python correctly outputs Unicode characters
 process.env.PYTHONIOENCODING = 'utf_8'
-
 # Prevent Python from generating bytecode (.pyc, .pyo) files
 process.env.PYTHONDONTWRITEBYTECODE = '1'


add direct testing of listener

add direct testing of listener

--- test/lib/test-transform.coffee
+++ test/lib/test-transform.coffee
@@ -2,6 +2,7 @@
 assert = require 'assert'
 thru = require 'through2'
 buildCio = require '../../lib'
+buildTransformerListener = require '../../lib/transformer'

 # no builder options yet...
 cio = buildCio()
@@ -21,6 +22,79 @@
     end: ->

 describe 'test transformer', ->
+
+  describe 'listener', ->
+
+    # use a fake transform object so we can verify it
+    fakeTransform = buildTransform()
+
+    # remember the options because they are passed to the builder too
+    # use a function so listener will call it to build the transform
+    builderOptions = transform:fakeTransform
+
+    # build the listener (which remembers these options due to scope)
+    listener = buildTransformerListener builderOptions
+
+    # pass a fake socket to the listener
+    fakeSocket =
+      pipe: (stream) ->
+        stream.pipedFrom = this
+        return stream
+      on: ->
+
+    # call the listener as if a new socket connection has been made
+    listener fakeSocket
+
+    it 'should return a listener function', ->
+      assert.equal (typeof listener), 'function'
+
+    it 'should call the builder to return the fakeTransform', ->
+      assert.strictEqual fakeSocket.transform, fakeTransform
+
+    it 'should pipe socket -> transform -> socket', ->
+      assert.equal fakeTransform.pipedFrom, fakeSocket
+      assert.equal fakeTransform.pipedTo, fakeSocket
+
+
+
+  describe 'listener with transform built from a function', ->
+
+    # use a fake transform object so we can verify it
+    fakeTransform = buildTransform()
+
+    # remember the options because they are passed to the builder too
+    # use a function so listener will call it to build the transform
+    builderOptions = transform: (optionsArg) ->
+      fakeTransform.options = optionsArg
+      return fakeTransform
+
+    # build the listener (which remembers these options due to scope)
+    listener = buildTransformerListener builderOptions
+
+    # pass a fake socket to the listener
+    fakeSocket =
+      pipe: (stream) ->
+        stream.pipedFrom = this
+        return stream
+      on: (event) ->
+
+    # call the listener as if a new socket connection has been made
+    listener fakeSocket
+
+    it 'should return a listener function', ->
+      assert.equal (typeof listener), 'function'
+
+    it 'should receive the builder options in the transform builder', ->
+      assert.strictEqual fakeTransform.options, builderOptions
+
+    it 'should build the fakeTransform and use it', ->
+      assert.strictEqual fakeSocket.transform, fakeTransform
+
+    it 'should pipe socket -> transform -> socket', ->
+      assert.equal fakeTransform.pipedFrom, fakeSocket
+      assert.equal fakeTransform.pipedTo, fakeSocket
+
+

   describe 'with client and server', ->


Attempt to pass second test

Attempt to pass second test

--- test/spec/test_reactive.coffee
+++ test/spec/test_reactive.coffee
@@ -96,10 +96,9 @@

     it 'should have the right tag', ->
       expect(elt).toBeDefined()
-      console.log(elt)
       expect(elt instanceof SVGRectElement).toBe(true)
     it 'should have the set attributes', ->
-      expect(elt.attr('x')).toBe('10')
+      expect(elt.getAttribute('x')).toBe('10')
     it 'should have the given child contents', ->
       cont = elt.contents()
       child = cont.last()


use forEach method

use forEach method

--- scripts/chat-logger.coffee
+++ scripts/chat-logger.coffee
@@ -204,7 +204,8 @@
       title: 'hubot log'
       description: "log of room: #{req.params.room}"
       link: "#{base_url}/#{req.params.room}/feed"
-    feed.item v for v in list_feed_item req.params.room if chat_data()[req.params.room]
+    if chat_data()[req.params.room]
+      list_feed_item(req.params.room).forEach (v) -> feed.item v
     res.type 'application/atom+xml'
     res.send feed.render 'atom-1.0'


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,6 +1,6 @@
 ###*
 The Follow module provides a simple method to set an object's
-velocityso that it will approach another object.
+velocity so that it will approach another object.

 # Health regeneration


added begins and ends

added begins and ends

--- tools/strings.coffee
+++ tools/strings.coffee
@@ -100,6 +100,14 @@
         while str.indexOf(regex) isnt -1
             str = str.replace regex, rep
         return str
+
+    ##
+    begins: (str, begin)->
+        str && begin && str.length > begin.length && str.substr(0, begin.length) is begin
+
+    ##
+    ends: (str, end)->
+        str && end && str.length > end.length && str.substr(end.length-1) is end

     ##
     ## simple string hashing function


Update gonews.coffee

Update gonews.coffee

--- src/gonews.coffee
+++ src/gonews.coffee
@@ -1,39 +1,45 @@
 # Description
-#   Hubot golang news extension
+#   Hubot golang news pulled from reddit
 #
 # Configuration:
-#   LIST_OF_ENV_VARS_TO_SET
+#   GONEWS_RSS_FEED_URL
 #
 # Commands:
-#   hubot hello - <what the respond trigger does>
-#   orly - <what the hear trigger does>
+#   gonews latest [\d+] - returns list of latest golang news. Default 5 items per response...
 #
 # Notes:
-#   <optional notes required for the script>
+#   GONEWS_RSS_FEED_URL is not needed. It will fall back to reddit/r/golang
 #
 # Author:
-#   0x19 <nevio.vesic@gmail.com>
+#   Nevio Vesic (0x19) <nevio.vesic@gmail.com>

 parser = require 'parse-rss'
-HtmlParser = require "htmlparser"
+
+reddit_url = process.env.GONEWS_RSS_FEED_URL || "https://www.reddit.com/r/golang/.rss"

 module.exports = (robot) ->

-  robot.respond /gonews/, (res) ->
-    url    = "http://golangweekly.com/rss/1b1gb6c4.rss"
+  robot.respond /gonews help/i, (msg) ->
+    cmds = robot.helpCommands()
+    cmds = (cmd for cmd in cmds when cmd.match(/(gonews)/))
+    msg.send cmds.join("\n")

-    parser url, (err,rss)->
-      console.log err if err
-      console.log rss
+  robot.respond /gonews latest ?(\d+)?/i, (msg) ->
+    random = Math.floor(Math.random() * (2000000 - 1000000) + 1000000)
+    count  = msg.match[1] || 5

-      handler = new HTMLParser.DefaultHandler((() ->),
-        ignoreWhitespace: true
-      )
-      parser  = new HTMLParser.Parser handler
-      parser.parseComplete rss[0].description
+    robot.logger.info "Fetching latest #{count} rss feed messages"
+    robot.logger.info "#{reddit_url}?time=#{random}"

-      #for feed in rss[0]
-      res.reply handler.dom
+    parser "#{reddit_url}?time=#{random}", (err,rss)->
+      if err
+        msg.send "Error happen while fetching golang news: #{err}"
+        return

-  #robot.respond /hello/, (res) ->
-  #  res.reply "hello!"
+      for feed in rss[0..count]
+        robot.emit 'slack.attachment',
+          message: msg.message
+          content:
+            title: feed.title,
+            title_link: feed.link
+            text: feed.description


Formatting / whitespace

Formatting / whitespace

--- src/listnr.coffee
+++ src/listnr.coffee
@@ -1,8 +1,10 @@
 addEvent = (el, type, handler) ->
   el.addEventListener(type, handler)

+
 removeEvent = (el, type, handler) ->
   el.removeEventListener(type, handler)
+

 class Context
   constructor: (@_listener) ->
@@ -35,6 +37,7 @@
   unmap: (combo) ->
     delete @_map[combo]
     this
+

 class @Listnr
   constructor: (options={}) ->


rely on request_render and fix up CS code (#6385)

--- bokehjs/src/coffee/models/plots/gmap_plot_canvas.coffee
+++ bokehjs/src/coffee/models/plots/gmap_plot_canvas.coffee
@@ -111,9 +111,7 @@
     @map = new maps.Map(@canvas_view.map_el, map_options)

     # update bokeh ranges whenever the map idles, which should be after most UI action
-    maps.event.addListener(@map, 'idle',
-      @_set_bokeh_ranges
-      @render())
+    maps.event.addListener(@map, 'idle', () => @_set_bokeh_ranges())

     # also need an event when bounds change so that map resizes trigger renders too
     maps.event.addListener(@map, 'bounds_changed', () => @_set_bokeh_ranges())


only render on inspections if there is a hover glyph

--- bokehjs/src/coffee/renderer/glyph/glyph_renderer.coffee
+++ bokehjs/src/coffee/renderer/glyph/glyph_renderer.coffee
@@ -47,7 +47,8 @@
     @listenTo(@model, 'change', @request_render)
     @listenTo(@mget('data_source'), 'change', @set_data)
     @listenTo(@mget('data_source'), 'select', @request_render)
-    @listenTo(@mget('data_source'), 'inspect', @request_render)
+    if @hover_glyph?
+      @listenTo(@mget('data_source'), 'inspect', @request_render)

     # TODO (bev) This is a quick change that  allows the plot to be
     # update/re-rendered when properties change on the JS side. It would


Fixing fromWhere routing on login

Fixing fromWhere routing on login

--- shared/router.coffee
+++ shared/router.coffee
@@ -97,9 +97,9 @@
 # Get all the accounts-entry routes one time
 exclusions = []
 _.each Router.routes, (route)->
-  exclusions.push route.name
+  exclusions.push route.getName()
 # Change the fromWhere session variable when you leave a path
 Router.onStop ->
   # If the route is an entry route, no need to save it
   if (!_.contains(exclusions, Router.current().route?.getName()))
-    Session.set('fromWhere', Router.current().path)
+    Session.set('fromWhere', window.location.pathname)


kite controller: use the new kite abstraction in tellKite

--- client/app/MainApp/kite/kitecontroller.coffee
+++ client/app/MainApp/kite/kitecontroller.coffee
@@ -83,8 +83,16 @@
       command = options
       options = {}

+    currentGroupName = (KD.getSingleton 'groupsController').getGroupSlug()
+
     options.kiteName or= "os"
     options.method   or= "exec"
+    # options.correlationName or= currentGroupName
+
+    kite = @getKite options.kiteName,
+      if not currentGroupName or currentGroupName is 'koding'
+      then KD.whoami().profile.nickname
+      else currentGroupName

     if command
       options.withArgs = command
@@ -94,7 +102,7 @@
     notify "Calling #{options.method} method, from #{options.kiteName} kite"
     log "Kite Request:", options

-    KD.whoami().tellKite? options, (err, response)=>
+    kite.tell options, (err, response)=>
       @parseKiteResponse {err, response}, options, callback

   setListeners:->


Fix 'edit' browser command to work with no args.

--- browser/frontend.coffee
+++ browser/frontend.coffee
@@ -193,15 +193,15 @@
   ls: (args) ->
     (node.fs.readdirSync '.').sort().join '\n'
   edit: (args) ->
-    data = DoppioFile.load(args[0])?.read() or defaultFile
+    data = if args[0]? then DoppioFile.load(args[0]).read() else defaultFile
     $('#console').fadeOut 'fast', ->
       $('#filename').val args[0]
       $('#ide').fadeIn('fast')
       # initialize the editor. technically we only need to do this once, but more
       # than once is fine too
       editor = ace.edit('source')
-      ext = args[0].split('.')[1]
-      if ext is 'java'
+      ext = args[0]?.split('.')[1]
+      if ext is 'java' or not args[0]?
         JavaMode = require("ace/mode/java").Mode
         editor.getSession().setMode(new JavaMode)
       else


collaboration: don't use defines

collaboration: don't use defines

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -347,12 +347,12 @@

     # for backwards compatibility.
     # TODO: keep this until CollaborationModel abstraction. ~Umut
-    @define 'participants',      -> refs.participants
-    @define 'changes',           -> refs.changes
-    @define 'permissions',       -> refs.permissions
-    @define 'broadcastMessages', -> refs.broadcastMessages
-    @define 'myWatchMap',        -> refs.watchMap
-    @define 'mySnapshot',        -> refs.snapshot
+    @participants      = refs.participants
+    @changes           = refs.changes
+    @permissions       = refs.permissions
+    @broadcastMessages = refs.broadcastMessages
+    @myWatchMap        = refs.watchMap
+    @mySnapshot        = refs.snapshot

   registerCollaborationSessionId: ->