blue!

blue!

--- app/assets/javascripts/jax/dev/controllers/jax.js.coffee
+++ app/assets/javascripts/jax/dev/controllers/jax.js.coffee
@@ -47,23 +47,35 @@

     @world.addObject new Jax.Framerate

-    jaxMesh = new Jax.Mesh.OBJ
-      path: "/assets/jack.obj"
-      material: new Jax.Material.Surface
-        intensity:
-          ambient: 1
-          diffuse: 1
-          specular: 1
-        color:
-          ambient: '#f40'
-          diffuse: '#f40'
-          specular: '#fff'
+    # FIXME I should be able to reuse a mesh with different materials,
+    # but the current API makes this either unintuitive or impossible.
+
     @jax1 = @world.addObject new Jax.Model
       position: [-0.062798, 0.580798,  0.486800]
-      mesh: jaxMesh
+      mesh: new Jax.Mesh.OBJ
+        path: "/assets/jack.obj"
+        material: new Jax.Material.Surface
+          intensity:
+            ambient: 1
+            diffuse: 1
+            specular: 1
+          color:
+            ambient: '#f40'
+            diffuse: '#f40'
+            specular: '#fff'
     @jax2 = @world.addObject new Jax.Model
       position: [-0.562798, 0.580798, -0.686800]
-      mesh: jaxMesh
+      mesh: new Jax.Mesh.OBJ
+        path: "/assets/jack.obj"
+        material: new Jax.Material.Surface
+          intensity:
+            ambient: 1
+            diffuse: 1
+            specular: 1
+          color:
+            ambient: '#04f'
+            diffuse: '#04f'
+            specular: '#fff'

     @jax1.camera.rotation = [0.215122, 0.907300, 0.328824, 0.149720]
     # HACK this shouldn't be necessary


Adding a default method 'fill' to all models that can be used to fill all the model's m.prop values with an object data so that we can avoid iterating over data in like a bazillion places

--- app/assets/javascripts/monster_mithril/core/model.coffee
+++ app/assets/javascripts/monster_mithril/core/model.coffee
@@ -18,6 +18,10 @@
           _kind: name
           params: @params
           reset : @reset
+          fill  : (data)=>
+            for k, v of data
+              if @$[k]
+                @$[k](v)

         @$.id = m.prop(attrs.id || null)


Update wiki page title to page

Update wiki page title to page

We removed 'wiki' from the modal but not
from the title. Sadlol.

Closes COMMS-1333

Test plan:
- create a page in a course
- delete the page
- view the deletion modal without wiki
in the title

Change-Id: Idc94b6e9ecfd57d6b9f06205f0cd75a54141229e
Reviewed-on: https://gerrit.instructure.com/157808
Tested-by: Jenkins
Reviewed-by: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>
Product-Review: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>
QA-Review: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>

--- app/coffeescripts/views/wiki/WikiPageDeleteDialog.coffee
+++ app/coffeescripts/views/wiki/WikiPageDeleteDialog.coffee
@@ -25,7 +25,7 @@

   dialogDefaults =
     fixDialogButtons: false
-    title: I18n.t 'delete_dialog_title', 'Delete Wiki Page'
+    title: I18n.t 'delete_dialog_title', 'Delete Page'
     width: 400
     height: 'auto'


added @roots method to traverse

added @roots method to traverse

--- src/data/util/traverse.coffee
+++ src/data/util/traverse.coffee
@@ -28,6 +28,11 @@
     path.pop()
     ret

+  @roots: (t) ->
+    _.compact @bfs t, (n) ->
+      n if n.children().length == 0
+
+

   @toString: (t, f=null) ->
     f ?= (n)->


Clean up submenu, remove commands that aren't bound to workspace

--- menus/git-plus.cson
+++ menus/git-plus.cson
@@ -11,25 +11,12 @@
       'submenu': [
         { 'label': 'Add', 'command': 'git-plus:add' }
         { 'label': 'Add All', 'command': 'git-plus:add-all' }
+        { 'label': 'Commit', 'command': 'git-plus:commit' }
+        { 'label': 'Diff', 'command': 'git-plus:diff' }
         { 'label': 'Diff All', 'command': 'git-plus:diff-all' }
         { 'label': 'Add & Commit', 'command': 'git-plus:add-and-commit'}
         { 'label': 'Add All & Commit', 'command': 'git-plus:add-all-and-commit'}
-        { 'label': 'Commit', 'command': 'git-plus:commit' }
-        { 'label': 'Commit Amend', 'command': 'git-plus:commit-amend' }
-        { 'label': 'Pull', 'command': 'git-plus:pull' }
-        { 'label': 'Push', 'command': 'git-plus:push' }
-        { 'label': 'Show', 'command': 'git-plus:show' }
         { 'label': 'Log', 'command': 'git-plus:log' }
-        { 'label': 'Log Current File', 'command': 'git-plus:log-current-file' }
-        { 'label': 'Tags', 'command': 'git-plus:tags' }
-        { 'label': 'Fetch', 'command': 'git-plus:fetch' }
-        { 'label': 'Remove', 'command': 'git-plus:remove' }
-        { 'label': 'Remove Current File', 'command': 'git-plus:remove-current-file' }
-        { 'label': 'Show', 'command': 'git-plus:show' }
-        { 'label': 'Log', 'command': 'git-plus:log' }
-        { 'label': 'Log Current File', 'command': 'git-plus:log-current-file' }
-        { 'label': 'Checkout all files', 'command': 'git-plus:checkout-all-files' }
-        { 'label': 'Tags', 'command': 'git-plus:tags' }
       ]
     ]
   }


Fix typo

Fix typo

--- src/definitions.coffee
+++ src/definitions.coffee
@@ -420,7 +420,7 @@
     @answer 'Next', nextAction, (state) -> !_.any state.options, (option) -> option.selected

   Copyleft: ->
-    @question 'Do you want others who modify your code to be forced to also release it under open source license?'
+    @question 'Do you want others who modify your code to be forced to also release it under an open source license?'
     @yes ->
       @include 'copyleft'
       if @has('weak') and @has('strong')


allow set fontFamily and fontSize

allow set fontFamily and fontSize

--- src/drawable/PointText.coffee
+++ src/drawable/PointText.coffee
@@ -13,34 +13,47 @@
 	----------------------
 	for example: text is in the right top of the point, then align = "+-"
 	###
-	constructor: (@text, @x, @y, options) ->
+	constructor: (@text, @x, @y) ->
 		super()
 		@type = 'PointText'
 		@strokeStyle = null # no stroke by default
 		@fillStyle = Bu.DEFAULT_TEXT_FILL_STYLE

-		@align = '00'
-		@fontFamily = 'SimSun'
-		@fontSize = 10
-		if options?
-			@align = options.align if options.align?
-			@fontFamily = options.fontFamily if options.fontFamily?
-			@fontSize = options.fontSize if options.fontSize?
-			@font = options.font if options.font?
-		@font = "#{ @fontSize }px #{ @fontFamily }" if @font?
+		options = Bu.combineOptions arguments,
+			align: '00'
+			fontFamily: 'Verdana'
+			fontSize: 11
+		@align = options.align
+		@_fontFamily = options.fontFamily
+		console.log @_fontFamily
+		@_fontSize = options.fontSize
+		@font = "#{ @_fontSize }px #{ @_fontFamily }" or options.font

-		setContextAlign @, @align
+		@setContextAlign @align

-	setContextAlign = (point, align) ->
+	@property 'fontFamily',
+		get: -> @_fontFamily
+		set: (val) ->
+			@_fontFamily = val
+			@font = "#{ @_fontSize }px #{ @_fontFamily }"
+
+	@property 'fontSize',
+		get: -> @_fontSize
+		set: (val) ->
+			@_fontSize = val
+			@font = "#{ @_fontSize }px #{ @_fontFamily }"
+			console.log @
+
+	setContextAlign: (align) =>
 		if align.length == 1
 			align = '' + align + align
 		alignX = align.substring(0, 1)
 		alignY = align.substring(1, 2)
-		point.textAlign = switch alignX
+		@textAlign = switch alignX
 			when '-' then 'right'
 			when '0' then 'center'
 			when '+' then 'left'
-		point.textBaseline = switch alignY
+		@textBaseline = switch alignY
 			when '-' then 'bottom'
 			when '0' then 'middle'
 			when '+' then 'top'


Tests for parsing atttrs into object/array added

--- spec/index.spec.coffee
+++ spec/index.spec.coffee
@@ -154,6 +154,24 @@

 		expect(view.render.bind(view)).not.toThrow()

+	it 'parses object in attrs into Object instance', ->
+
+		data = a: 1
+		json = JSON.stringify data
+		view = View.fromHTML uid(), ""
+		[elem] = view.node.children
+
+		expect(elem.attrs.get 'data').toEqual data
+
+	it 'parses array in attrs into Array instance', ->
+
+		data = [1, 2]
+		json = JSON.stringify data
+		view = View.fromHTML uid(), ""
+		[elem] = view.node.children
+
+		expect(elem.attrs.get 'data').toEqual data
+
 describe 'View Storage', ->

 	describe 'inputs are replaced', ->


Removes commented out lines

Removes commented out lines

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -2118,16 +2118,6 @@
       atom.config.set "tree-view.sortFoldersInline", false

     it "lists folders first if the option is not set", ->
-
-      # root = $(treeView.root)
-      # dirView = $(treeView.root.entries).find('.directory:contains(test-dir):first')
-      # dirView[0].expand()
-      # fileView = treeView.find('.file:contains(test-file.txt)')
-      # dirView2 = $(treeView.root.entries).find('.directory:contains(test-dir2):last')
-      # dirView2[0].expand()
-      # fileView2 = treeView.find('.file:contains(test-file2.txt)')
-      # fileView3 = treeView.find('.file:contains(test-file3.txt)')
-
       topLevelEntries = [].slice.call(treeView.root.entries.children).map (element) ->
         element.innerText

@@ -2147,7 +2137,6 @@

       expect(gammaEntries).toEqual(["theta", "delta.txt", "epsilon.txt"])

-
     it "sorts folders as files if the option is set", ->
       atom.config.set "tree-view.sortFoldersInline", true


Fixed: tests, somehow

Fixed: tests, somehow

--- tests/main.coffee
+++ tests/main.coffee
@@ -53,6 +53,10 @@
                 @stub.called.should.be.true

         describe "When Data System fails at sending doctypes list", (done) ->
+            before (done) -> fixtures.resetDatabase
+                    removeAllViews: true
+                    callback: done
+
             before ->
                 @sandbox = sinon.sandbox.create()
                 @stub = @sandbox.stub fixtures, "removeDocumentsOf"
@@ -81,6 +85,10 @@
     describe "Remove Every Views", ->

         describe "When removeEveryViews is called without parameters", ->
+
+            before (done) -> fixtures.resetDatabase
+                                removeAllViews: true
+                                callback: done

             before (done) ->
                 fixtures._createAllRequest 'alarm', ->
@@ -103,6 +111,10 @@
                     done()

         describe "When removeEveryViews is called with a list of design to remove", ->
+
+            before (done) -> fixtures.resetDatabase
+                                removeAllViews: true
+                                callback: done

             before (done) ->
                 fixtures._createAllRequest 'alarm', ->
@@ -130,6 +142,11 @@
     describe "#removeDocumentsOf", ->

         describe "When removeDocumentsOf is called with an object as parameter", ->
+
+            before (done) -> fixtures.resetDatabase
+                                removeAllViews: true
+                                callback: done
+
             before (done) -> fixtures.load doctypeTarget: 'Alarm', callback: done

             it "There should be an error", (done) ->
@@ -140,7 +157,12 @@
     describe "#_addDoc", ->

         describe "When  _addDoc is called with a document", ->
+            before (done) -> fixtures.resetDatabase
+                    removeAllViews: true
+                    callback: done
+
             before (done) -> fixtures.resetDatabase callback: done
+
             before (done) ->
                 @doc = require('./fixtures/fixtures.json')[0]
                 addDoc = fixtures._addDoc @doc
@@ -162,7 +184,10 @@
                     done()

         describe "When _addDoc is called with a document with a field _id", ->
-            before (done) -> fixtures.resetDatabase callback: done
+            before (done) -> fixtures.resetDatabase
+                                removeAllViews: true
+                                callback: done
+
             before (done) ->
                 @doc = require('./fixtures/fixtures-with-id.json')[0]
                 @doc.should.have.property '_id'
@@ -182,7 +207,9 @@
                     done()

         describe "When _addDoc is called with a document with a field _attachments", ->
-            before (done) -> fixtures.resetDatabase callback: done
+            before (done) -> fixtures.resetDatabase
+                                removeAllViews: true
+                                callback: done
             before (done) ->
                 @doc = require('./fixtures/fixtures-with-attachment.json')[0]
                 @attachment = @doc._attachments


add spec with error

add spec with error

--- specs/access/merge.spec.coffee
+++ specs/access/merge.spec.coffee
@@ -4,7 +4,44 @@
 TableGateway = h.requireSrc('access/table-gateway')
 ActiveRecord = h.requireSrc('access/active-record')

-testData = h.testData
+data = [
+    {
+        firstName: 'Anderson',
+        lastName: 'Silva',
+        dOB: new Date('1975-04-13'),
+        country: 'Brazil',
+        heightInCm: 188,
+        reachInCm: 197,
+        weightInLb: 185
+    },
+    {
+        firstName: 'Wanderlei',
+        lastName: 'Silva',
+        dOB: new Date("1976-07-01"),
+        country: 'Brazil',
+        heightInCm: 180,
+        reachInCm: 188,
+        weightInLb: 204
+    },
+    {
+        firstName: 'Jon',
+        lastName: 'Jones',
+        country: 'USA',
+        heightInCm: 193,
+        reachInCm: 215,
+        weightInLb: 205 },
+    {
+        firstName: 'Cain',
+        lastName: 'Velasquez',
+        country: 'USA',
+        heightInCm: 185,
+        reachInCm: 196,
+        weightInLb: 240
+    }
+]
+
+shapes = [ [ data[0], data[1] ], [ data[2], data[3] ] ]
+
 db = schema = tables = null

 before () ->
@@ -13,15 +50,15 @@
     tables = schema.tablesByMany

 describe('Merge', () ->
-    it('merges an array of data', (done) ->
+    it('merges an array of data breaking in 2 shapes', (done) ->
         A.series([
-            (cb) -> db.fighters.deleteMany(id: ">": 0, cb)
-            (cb) -> db.fighters.merge(testData.fighters, cb)
+            (cb) -> db.fighters.merge(data, cb)
             (cb) -> db.fighters.all(cb)
         ], (err, results) ->
-            return done(err) if err?
-            fighters = results[2]
-            fighters.length.should.eql(4)
+            return done(err) if (err?)
+
+            console.log(results)
+
             done()
         )
     )


add select element to tabbable selectors

--- src/helpers/index.coffee
+++ src/helpers/index.coffee
@@ -1,7 +1,7 @@
 React = require 'react'
 _ = require 'underscore'

-TABBABLE_SELECTORS = ['a', 'button', 'input', 'form', 'textarea', '[tabindex]']
+TABBABLE_SELECTORS = ['a', 'button', 'input', 'form', 'textarea', 'select', '[tabindex]']
 TABBABLE_CLASS = 'openstax-tabbable'
 UNTABBABLE_CLASS = 'openstax-untabbable'


Listen to channel messages through HipChat adapter

--- src/hubot/hipchat.coffee
+++ src/hubot/hipchat.coffee
@@ -58,7 +58,7 @@
     bot.onMessage (channel, from, message)->
       author = { name: from, reply_to: channel }
       hubot_msg = message.replace(mention, "#{self.name}: ")
-      self.receive new Robot.Message(author, hubot_msg)
+      self.receive new Robot.TextMessage(author, hubot_msg)
     bot.onPrivateMessage (from, message)=>
       user = self.userForId(from.match(/_(\d+)@/)[1])
       author = { name: user.name, reply_to: from }


add attribute to keep scroll position after loading

--- src/infinite-scroll.coffee
+++ src/infinite-scroll.coffee
@@ -72,7 +72,7 @@
             container.scrollTop(container.scrollTop() - count * lineHeight)
           )
         else
-          scope.infiniteScrollReverse()
+          scope.infiniteScroll()

       if shouldScroll
         checkWhenEnabled = true


Query class: accepts columns from opts object

--- queries/query.coffee
+++ queries/query.coffee
@@ -7,6 +7,7 @@
     @connection = mysql.createConnection db
     @connection.connect()
     @query = options.body or "SELECT 'this is the default query. pass a query body' FROM dual"
+    @columns = options.columns

   run: (done) ->
     @connection.query @query, (err, rows, fields) =>


Bugfix and better fiber-safe reactive publishing

--- publish.coffee
+++ publish.coffee
@@ -29,6 +29,7 @@
                     methodFunc.apply @, args

     Meteor.methods wrappedMethods
+

 # JSON.stringify([modelName, instanceId, reactiveName]): true
@@ -274,37 +275,12 @@
                 if included and reactivesObj[reactiveName]?.val is undefined
                     reactivesObj[reactiveName] ?= {}

+
+
                     reactiveKey = "#{modelName}.#{reactiveName}"
                     future = J._reactiveCalcsInProgress[reactiveKey]
                     if future?
-                        # console.log "#{J.util.stringify querySpec} Recalc of #{reactiveKey} already in progress."
                     else
-                        # console.log "#{J.util.stringify querySpec} Fresh recalc of #{reactiveKey}."
-                        future = J._reactiveCalcsInProgress[reactiveKey] = Future.task ->
-                            if instanceDoc is undefined
-                                # Do a raw Mongo findOne which this includes the _reactives field.
-                                instanceDoc = modelClass.findOne id,
-                                    transform: false
-
-                            # instanceDoc might not exist because the instance might have been
-                            # deleted while we're still catching up publishing an @added or @changed
-                            # that includes a reactive.
-                            ret = if instanceDoc?
-                                if instanceDoc?._reactives?.reactiveName?.val is undefined
-                                    sanitizedInstanceDoc = _.clone instanceDoc
-                                    delete sanitizedInstanceDoc._reactives
-                                    instance = modelClass.fromDoc sanitizedInstanceDoc
-                                    J.denorm.recalc instance, reactiveName
-                                else
-                                    instanceDoc._reactives.reactiveName.val
-
-                            delete J._reactiveCalcsInProgress[reactiveKey]
-
-                            ret
-
-                    reactivesObj[reactiveName].val = future.wait()
-                    # console.log "#{J.util.stringify querySpec} returning value of #{reactiveKey}:
-                    #   {reactivesObj[reactiveName].val}"

         else
             fieldsByModelIdQuery[modelName][id][fieldName][qsString] = value
@@ -330,7 +306,6 @@

         observer = cursor.observeChanges
             added: (id, fields) =>
-                # log querySpec, "server says ADDED:", id, fields

                 fields = _.clone fields
                 fields._reactives ?= {}
@@ -364,7 +339,6 @@
                     @added modelClass.collection._name, id, changedFields

             changed: (id, fields) =>
-                # log querySpec, "server says CHANGED:", id, fields

                 fields = _.clone fields
                 fields._reactives ?= {}
@@ -379,11 +353,9 @@
                         changedFields[fieldName] = newValue

                 if not _.isEmpty changedFields
-                    # log querySpec, "sending CHANGED:", id, changedFields
                     @changed modelClass.collection._name, id, changedFields

             removed: (id) =>
-                # log querySpec, "server says REMOVED:", id

                 changedFields = {}

@@ -398,10 +370,8 @@

                 if _.isEmpty fieldsByModelIdQuery[querySpec.modelName][id]
                     delete fieldsByModelIdQuery[querySpec.modelName][id]
-                    # log querySpec, "sending REMOVED:", id
                     @removed modelClass.collection._name, id
                 else if not _.isEmpty changedFields
-                    # log querySpec, "sending CHANGED:", id
                     @changed modelClass.collection._name, id, changedFields

         session.observerByQsString().setOrAdd qsString, observer


offering defaults to "default"

offering defaults to "default"

--- tutor/specs/components/new-course/offering.spec.cjsx
+++ tutor/specs/components/new-course/offering.spec.cjsx
@@ -23,5 +23,5 @@
     OfferingsActions.loaded(burntOfferings)
     wrapper = shallow(<Offering {...@props} />)
     expect(wrapper.is('[data-appearance="firefirefire"]')).to.be.true
-    expect(wrapper.find('CourseChoiceContent[data-book-title=""]')).to.have.length(1)
+    expect(wrapper.find('CourseChoiceContent[data-book-title="default"]')).to.have.length(1)
     undefined


Fix: 適用失敗と成功が逆になっていたのを修正

Fix: 適用失敗と成功が逆になっていたのを修正

--- src/gui/js/repo.coffee
+++ src/gui/js/repo.coffee
@@ -289,7 +289,7 @@
               when "ru" then p.addLog("#{mod.name} - Не удалось удалить(#{err})")
       return
     ).then( ->
-      if errored
+      if !errored
         p.changePhase("done")
       else
         p.changePhase("failed")


more images

more images

--- src/ihavenoidea.coffee
+++ src/ihavenoidea.coffee
@@ -32,7 +32,16 @@
   'http://i3.kym-cdn.com/photos/images/original/000/234/739/fa5.jpg',
   'http://i1.kym-cdn.com/photos/images/original/000/234/765/b7e.jpg',
   'http://i0.kym-cdn.com/photos/images/original/000/234/142/196.jpg',
-  'http://i2.kym-cdn.com/photos/images/original/000/234/137/5c4.jpg'
+  'http://i2.kym-cdn.com/photos/images/original/000/234/137/5c4.jpg',
+  'http://i3.kym-cdn.com/photos/images/newsfeed/000/490/406/dd3.jpeg',
+  'http://i0.kym-cdn.com/photos/images/newsfeed/000/464/510/a5b.jpg',
+  'http://i1.kym-cdn.com/photos/images/original/000/310/183/6a0.jpg',
+  'http://i3.kym-cdn.com/photos/images/original/000/308/125/1ee.jpg',
+  'http://i0.kym-cdn.com/photos/images/original/000/305/236/ddc.jpg',
+  'http://i3.kym-cdn.com/photos/images/original/000/305/235/07a.jpg',
+  'http://i0.kym-cdn.com/photos/images/original/000/284/499/a69.jpg',
+  'http://i1.kym-cdn.com/photos/images/original/000/258/461/be0.jpg',
+  'http://i0.kym-cdn.com/photos/images/original/000/234/767/8d0.jpg'
 ]

 regex = new RegExp gaffes.join('|'), 'ig'


Remove unused code

Remove unused code

--- src/lines-component.coffee
+++ src/lines-component.coffee
@@ -103,7 +103,7 @@
     delete @oldState.lines[id]

   updateLineNodes: ->
-    {presenter, lineDecorations, mouseWheelScreenRow} = @props
+    {presenter, mouseWheelScreenRow} = @props

     for id of @oldState.lines
       unless @newState.lines.hasOwnProperty(id) or mouseWheelScreenRow is @screenRowsByLineId[id]
@@ -124,8 +124,6 @@
         @lineIdsByScreenRow[lineState.screenRow] = id
       @oldState.lines[id] = _.clone(lineState)

-      @renderedDecorationsByLineId[id] = lineDecorations[lineState.screenRow]
-
     return unless newLineIds?

     WrapperDiv.innerHTML = newLinesHTML
@@ -137,7 +135,7 @@
       node.appendChild(lineNode)

   buildLineHTML: (id) ->
-    {presenter, lineDecorations} = @props
+    {presenter} = @props
     {scrollWidth} = @newState
     {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]

@@ -234,10 +232,8 @@
     "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"

   updateLineNode: (id) ->
-    {lineDecorations} = @props
     {scrollWidth} = @newState
     {screenRow, top} = @newState.lines[id]
-

     lineNode = @lineNodesByLineId[id]

@@ -283,8 +279,7 @@
     @measureCharactersInNewLines()

   measureCharactersInNewLines: ->
-    {editor, tokenizedLines, renderedRowRange} = @props
-    [visibleStartRow] = renderedRowRange
+    {editor} = @props
     node = @getDOMNode()

     editor.batchCharacterMeasurement =>


Library updates

Library updates

--- src/instruments.coffee
+++ src/instruments.coffee
@@ -112,39 +112,45 @@
       # Using localTime makes it easier to anticipate the interference of
       # multiple oscillators
       @startTime = time if @startTime == -1
-      localTime = time - @startTime
-
-      _phase = if @numericPhase? then @numericPhase else @phase(localTime)
-      _frequency = if @numericFrequency? then @numericFrequency else @frequency(localTime)
-      _amplitude = if @numericAmplitude? then @numericAmplitude else @amplitude(localTime)
-
-      _amplitude * @oscillatorFunction((_frequency * localTime) + _phase)
+      @localTime = time - @startTime
+
+      _phase = if @numericPhase? then @numericPhase else @phase(@localTime)
+      _frequency = if @numericFrequency? then @numericFrequency else @frequency(@localTime)
+      _amplitude = if @numericAmplitude? then @numericAmplitude else @amplitude(@localTime)
+
+      _amplitude * @oscillatorFunction((_frequency * @localTime) + _phase)

     generator.displayName = "Oscillator Sound Generator"

-    generator.getFrequency = => @frequency
+    generator.getFrequency = =>
+      if @numericFrequency? then @numericFrequency else @frequency(@localTime)
     generator.setFrequency = (frequency) =>
       @frequency = frequency
       if Function.isFunction(@frequency)
         @numericFrequency = undefined
       else
         @numericFrequency = @frequency
-
-    generator.getPhase = => @phase
+      frequency
+
+    generator.getPhase = =>
+      if @numericPhase? then @numericPhase else @phase(@localTime)
     generator.setPhase = (phase) =>
       @phase = phase
       if Function.isFunction(@phase)
         @numericPhase = undefined
       else
         @numericPhase = @phase
-
-    generator.getAmplitude = => @amplitude
+      phase
+
+    generator.getAmplitude = =>
+      if @numericAmplitude? then @numericAmplitude else @amplitude(@localTime)
     generator.setAmplitude = (amplitude) =>
       @amplitude = amplitude
       if Function.isFunction(@amplitude)
         @numericAmplitude = undefined
       else
         @numericAmplitude = @amplitude
+      amplitude

     # Explicit return necessary in constructor
     return generator
@@ -547,10 +553,8 @@
   @S = 3

   constructor: (options={}) ->
-    throw new Error "Must specify filter type." unless options.type?
-
     @Fs = SAMPLE_RATE
-    @type = options.type # type of the filter
+    @type = options.type || Filter.LOW_PASS # type of the filter
     @parameterType = Filter.Q # type of the parameter

     @x_1_l = 0
@@ -613,6 +617,7 @@
   ###
   setFrequency: (freq) ->
     @setF0(freq)
+    freq
   getFrequency: -> @f0

   ###
@@ -629,6 +634,7 @@
     @parameterType = Filter.Q
     @Q = Math.max(Math.min(q, 115.0), 0.001)
     @recalculateCoefficients()
+    q

   #
   # Advanced filter parameters
@@ -653,6 +659,7 @@
     @parameterType = Filter.BW
     @BW = bw
     @recalculateCoefficients()
+    bw

   setS: (s) ->
     @parameterType = Filter.S


Add pattern for LGPL in license field

Add pattern for LGPL in license field

Refs atom/atom#7366

--- src/legal-eagle.coffee
+++ src/legal-eagle.coffee
@@ -48,6 +48,7 @@
     unless typeof license is 'string'
       license = license.type ? 'UNKNOWN'
     license = 'BSD' if license.match /^BSD-.*/
+    license = 'LGPL' if license.match /^LGPL(-.+)*/
     license = 'MIT' if license.match(/^MIT\W/)
     license = 'Apache' if license.match /^Apache.*/
     license = 'WTF' if license is 'WTFPL'


Add helpers for start/end events

Add helpers for start/end events

--- src/lib/Network.coffee
+++ src/lib/Network.coffee
@@ -86,27 +86,14 @@
   increaseConnections: ->
     if @connectionCount is 0
       # First connection opened, execution has now started
-      return if @hasStarted
-      @emit 'start',
-        start: @startupDate
-      @hasStarted = true
-      @hasStopped = false
+      @setStarted true
     @connectionCount++
-  decreaseConnections: ->
+  decreaseConnections: _.debounce =>
     @connectionCount--
     # Last connection closed, execution has now ended
     if @connectionCount is 0
-      ender = _.debounce =>
-        return if @connectionCount
-        return if @hasStopped
-        @emit 'end',
-          start: @startupDate
-          end: new Date
-          uptime: @uptime()
-        @hasStarted = false
-        @hasStopped = true
-      , 100
-      do ender
+      @setStarted false
+  , 10

   # ## Loading components
   #
@@ -581,11 +568,7 @@
         @started = true
         @sendDefaults (err) =>
           return callback err if err
-          return callback null if @hasStarted
-          @emit 'start',
-            start: @startupDate
-          @hasStarted = true
-          @hasStopped = false
+          @setStarted true
           callback null

   stop: ->
@@ -596,16 +579,24 @@
     # Tell processes to shut down
     for id, process of @processes
       process.component.shutdown()
-    @started = false
-    setTimeout =>
-      return if @hasStopped
+    @setStarted false
+
+  setStarted: (started) ->
+    return if @started is started
+    unless started
+      # Ending the execution
+      @started = false
       @emit 'end',
         start: @startupDate
         end: new Date
         uptime: @uptime()
-      @hasStarted = false
-      @hasStopped = true
-    , 50
+      return
+
+    # Starting the execution
+    @startupDate = new Date unless @startupDate
+    @started = true
+    @emit 'start',
+      start: @startupDate

   getDebug: () ->
     @debug


FBP runtime protocol: Send payloads with started/stopped

--- lib/runtime.coffee
+++ lib/runtime.coffee
@@ -265,11 +265,17 @@
         m =
             protocol: "network"
             command: "stopped"
+            payload:
+                running: false
+                started: false
         send m
     else if args[0] is "NETSTART"
         m =
             protocol: "network"
             command: "started"
+            payload:
+                running: true
+                started: true
         send m
     else
         string = args.join(", ")


Generate download requests before reboot

--- lib/session.coffee
+++ lib/session.coffee
@@ -987,24 +987,6 @@
       parameterList: ([p[0].join('.'), p[1], p[2]] for p in parameterValues)
     }

-  # Reboot
-  if syncState.reboot?
-    p = sessionContext.deviceData.paths.get(['Reboot'])
-    if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.reboot)
-      delete syncState.reboot
-      return {
-        name: 'Reboot'
-      }
-
-  # Factory reset
-  if syncState.factoryReset?
-    p = sessionContext.deviceData.paths.get(['FactoryReset'])
-    if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.factoryReset)
-      delete syncState.factoryReset
-      return {
-        name: 'FactoryReset'
-      }
-
   # Download
   iter = syncState.downloadsDownload.entries()
   while pair = iter.next().value
@@ -1019,6 +1001,24 @@
         fileType: deviceData.attributes.get(fileTypePath)?.value?[1][0]
         fileName: deviceData.attributes.get(fileNamePath)?.value?[1][0]
         targetFileName: deviceData.attributes.get(targetFileNamePath)?.value?[1][0]
+      }
+
+  # Reboot
+  if syncState.reboot?
+    p = sessionContext.deviceData.paths.get(['Reboot'])
+    if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.reboot)
+      delete syncState.reboot
+      return {
+        name: 'Reboot'
+      }
+
+  # Factory reset
+  if syncState.factoryReset?
+    p = sessionContext.deviceData.paths.get(['FactoryReset'])
+    if not (p? and sessionContext.deviceData.attributes.get(p)?.value?[1][0] >= syncState.factoryReset)
+      delete syncState.factoryReset
+      return {
+        name: 'FactoryReset'
       }

   return null


Use different visitor per event

Use different visitor per event

Instantiate a 'visitor' on every API request, since GA only tracks 500 events per visitor/session per day.

--- lib/tracker.coffee
+++ lib/tracker.coffee
@@ -1,8 +1,8 @@
 ua = require('universal-analytics')

-visitor = ua('UA-49535937-3')
+module.exports = (req, res, next) ->
+  visitor = ua('UA-49535937-3')

-module.exports = (req, res, next) ->
   eventParams =
     ec: 'API Request'                        # category
     ea: req.get('Referrer') || 'no referrer' # action


Use colon in fork name as in GitHub PR page

--- lib/updater.coffee
+++ lib/updater.coffee
@@ -97,7 +97,7 @@

   # Get Can I Use features from another user fork
   fork: (fork, file, callback) ->
-    [user, branch] = fork.split('/')
+    [user, branch] = fork.split(':')
     branch ||= 'master'
     url = "#{user}/caniuse/#{branch}/features-json/#{file}.json"
     @github url, (data) => callback @parse(data)


GameObject.Effect

GameObject.Effect

--- src/game_object_effect.coffee
+++ src/game_object_effect.coffee
@@ -1,5 +1,5 @@
 ###*
-A collection of effects to make you game juicy.
+A collection of effects to make your game juicy.

 @name Effect
 @fieldOf GameObject


bump version to 1.0.7 in ezBreadcrumbs.coffee

--- src/coffee/ezBreadcrumbs.coffee
+++ src/coffee/ezBreadcrumbs.coffee
@@ -2,7 +2,7 @@
  * EzBreadcrumbs - jQuery plugin that transforms a breadcrumbs to a responsive one. Useful when making responsive websites.
  * Copyright(c) Exequiel Ceasar Navarrete <exequiel.navarrete@ninthdesign.com>
  * Licensed under MIT
- * Version 1.0.6
+ * Version 1.0.7
 ###
 (($, window, document, undefined_) ->
     "use strict"


check for teacher content after mounting

--- tutor/src/components/book-page.cjsx
+++ tutor/src/components/book-page.cjsx
@@ -36,6 +36,9 @@

   getSplashTitle: ->
     this.props.ux.page.title
+
+  componentDidMount: ->
+    this.props.ux.checkForTeacherContent()

   componentDidUpdate: ->
     this.props.ux.checkForTeacherContent()


Suppress EventEmitter warnings

Suppress EventEmitter warnings

--- atom/browser/lib/guest-view-manager.coffee
+++ atom/browser/lib/guest-view-manager.coffee
@@ -41,6 +41,7 @@
   destroy = ->
     destroyGuest id if guestInstances[id]?
     embedder.removeListener event, destroy for event in destroyEvents
+  embedder.setMaxListeners 100  # we can have lots of <webview> in one page.
   embedder.once event, destroy for event in destroyEvents

   # Init guest web view after attached.


Sanitize tags

Sanitize tags

--- modules/tags.coffee
+++ modules/tags.coffee
@@ -41,7 +41,7 @@
       await Core.data.set("Tag:#{args[0].toLowerCase()}", filtered)
       msg.reply l.gen(l.tags.deleted, tag.length - filtered.length)

-    @registerCommand '!', { argSeparator: ' ' }, ({ msg, args })=>
+    @registerCommand '!', { argSeparator: ' ' }, ({ msg, args, l })=>
       return if msg.author.bot or args.length < 1
       # Get the tag
       tag = await Core.data.get("Tag:#{args[0].toLowerCase()}")
@@ -49,7 +49,7 @@
       # Pick a random reply and send it
       chance = new Chance()
       res = chance.pickone tag
-      msg.channel.send res.reply
+      msg.channel.send l.gen res.reply

     @registerCommand 'taginfo', { ownerOnly: true, argSeparator: ' ' }, ({ msg, args, bot })=>
       # Get the tag


Added shell page to the navbar.

Added shell page to the navbar.

--- web/coffee/navbar.coffee
+++ web/coffee/navbar.coffee
@@ -5,6 +5,7 @@
 teacherLoggedIn =
   Game: "/game/"
   Problems: "/problems"
+  Shell: "/shell"
   Scoreboard: "/scoreboard"
   Classroom: "/classroom"
   About:
@@ -18,6 +19,7 @@
 userLoggedIn =
   Game: "/game/"
   Problems: "/problems"
+  Shell: "/shell"
   Team: "/team"
   Scoreboard: "/scoreboard"
   About:


adding a group of items is possible now

adding a group of items is possible now

--- src/mixins/list.coffee
+++ src/mixins/list.coffee
@@ -41,7 +41,6 @@
     #   console.log(" => #{event}")
     @listenTo @collection, "add", (m) =>
       @added(m)
-      @showAlerts()
     @listenTo @collection, 'update', =>
       @listMeta.set({state: "listing"})
       @updateFilters()
@@ -152,6 +151,7 @@
         else
           container.appendChild(rendered)
       @cachedCount += 1
+      @showAlerts()
       return view
     return null

@@ -168,14 +168,17 @@
     @listEl.empty()
     @views = {}
     if @collection.length > 0
+      @addGroup(@collection.models)
+
+  addGroup: (group) ->
+    if group.length > 0
+      console.log("---- grouping ----")
+
       container = document.createDocumentFragment()
-      @cachedCount = 0
-      for model in @collection.models
-        retval = @added model, container
-        if retval
-          @cachedCount += 1
-      if container
-        @getListElement().append(container)
+      group.forEach (instance) =>
+        this.added(instance, container)
+
+      this.getListElement().append(container)

   remove: () ->
     for cid, view of @views
@@ -197,8 +200,10 @@
         if view
           view.remove()
           delete @views[model.cid]
+
+    @cachedCount = count
     @showAlerts()
-    @cachedCount = count
+
     return count

   count: () ->
@@ -241,6 +246,8 @@
       # console.log("[alert on] #{selector}")
       @listMeta.set(selector, true)
       @$el.find(selector).removeClass @emptyToggleClass
+    # else
+    #   console.log("[alert already on] #{selector} #{status}")

   hideAlert: (selector) ->
     status = @listMeta.get(selector)
@@ -248,6 +255,8 @@
       # console.log("[alert off] #{selector}")
       @listMeta.set(selector, false)
       @$el.find(selector).addClass @emptyToggleClass
+    # else
+    #   console.log("[alert already off] #{selector} #{status}")

 module.exports =
   mixins:


making lists filterable

making lists filterable

--- src/mixins/list.coffee
+++ src/mixins/list.coffee
@@ -8,6 +8,13 @@
   emptySelector: ".empty"
   existsSelector: ".exists"
   emptyToggleClass: "hide"
+  filter: null
+  filterFunc: (model, filters) ->
+    if filters
+      for name, value of filters
+        if model.get(name) != value
+          return false
+    return true

   initialize: (options) ->
     @views = {}
@@ -16,6 +23,11 @@
     @listenTo @collection, "remove", @removed

     @listenTo @, "render:post", @addAll
+
+  setFilter: (filter) ->
+    @filter = filter
+    # TODO: remove models that no longer match
+    # TODO: add models that are now matching

   getItemView: (model) ->
     return new @itemViewClass
@@ -33,10 +45,11 @@
     return @listEl

   added: (model) ->
-    view = @getItemView model
-    @views[model.cid] = view
-    @getListElement().append view.render().el
-    @showEmpty()
+    if @filterFunc model, @filters
+      view = @getItemView model
+      @views[model.cid] = view
+      @getListElement().append view.render().el
+      @showEmpty()

   removed: (model) ->
     view = @views[model.cid]


Make primaryKey string parsing just a touch faster by caching the parsed value.

@loginx :)

--- src/model/model.coffee
+++ src/model/model.coffee
@@ -262,17 +262,17 @@
         core.get.apply(@, arguments)
       else
         @get(primaryKey)
-    set: (k, v) ->
+    set: (key, value) ->
       # naively coerce string ids into integers
-      if (typeof v is "string") and (v.match(/[^0-9]/) is null) and ("#{parseInt(v, 10)}" is v)
-        v = parseInt(v, 10)
+      if (typeof value is "string") and (value.match(/[^0-9]/) is null) and ("#{parsedValue = parseInt(value, 10)}" is value)
+        value = parsedValue

       primaryKey = @constructor.primaryKey
       if primaryKey == 'id'
-        @_willSet(k)
+        @_willSet(key)
         core.set.apply(@, arguments)
       else
-        @set(primaryKey, v)
+        @set(primaryKey, value)

   isNew: -> typeof @get('id') is 'undefined'


Updated user model to allow for settings

--- src/model/users.coffee
+++ src/model/users.coffee
@@ -1,5 +1,15 @@
 mongoose = require "mongoose"
 Schema = mongoose.Schema
+
+
+ConfigSchema = new Schema
+    "firstname":    { type: String, required: true }
+    "surname":      { type: String, required: true }
+    "email":      { type: String, required: true, unique: true }
+
+#compile the Config Schema into a Model
+exports.Config = mongoose.model 'Config', ConfigSchema
+

 UserSchema = new Schema
     "firstname":		{ type: String, required: true }
@@ -9,7 +19,8 @@
     "passwordHash": 	{ type: String, required: true }
     "passwordSalt": 	{ type: String, required: true }
     "groups": 			[ { type: String, required: false } ]
-    "msisdn": 		 	{ type: String, required: false }
+    "msisdn":       { type: String, required: false }
+    "settings": 		 	{ type: Object }

 #compile the User Schema into a Model
 exports.User = mongoose.model 'User', UserSchema


Changed textures smoothtep (cherry picked from commit 8a3a320)

--- src/modules/Objects.coffee
+++ src/modules/Objects.coffee
@@ -73,11 +73,11 @@

     void main()
     {
-      vec4 dirt = (smoothstep(0.01, 0.25, vAmount) - smoothstep(0.24, 0.26, vAmount)) * texture2D( dirtTexture, vUV * 10.0 );
-      vec4 sand = (smoothstep(0.24, 0.27, vAmount) - smoothstep(0.28, 0.31, vAmount)) * texture2D( sandyTexture, vUV * 10.0 );
-      vec4 grass = (smoothstep(0.28, 0.32, vAmount) - smoothstep(0.35, 0.40, vAmount)) * texture2D( grassTexture, vUV * 20.0 );
-      vec4 rock = (smoothstep(0.30, 0.50, vAmount) - smoothstep(0.40, 0.70, vAmount)) * texture2D( rockyTexture, vUV * 20.0 );
-      vec4 snow = (smoothstep(0.50, 0.65, vAmount))                                   * texture2D( snowyTexture, vUV * 10.0 );
+      vec4 dirt = (smoothstep(0.01, 0.05, vAmount) - smoothstep(0.02, 0.26, vAmount)) * texture2D( dirtTexture, vUV * 10.0 );
+      vec4 sand = (smoothstep(0.08, 0.27, vAmount) - smoothstep(0.30, 0.33, vAmount)) * texture2D( sandyTexture, vUV * 10.0 );
+      vec4 grass = (smoothstep(0.30, 0.33, vAmount) - smoothstep(0.34, 0.35, vAmount)) * texture2D( grassTexture, vUV * 20.0 );
+      vec4 rock = (smoothstep(0.30, 0.38, vAmount) - smoothstep(0.65, 0.75, vAmount)) * texture2D( rockyTexture, vUV * 20.0 );
+      vec4 snow = (smoothstep(0.65, 0.75, vAmount))                                   * texture2D( snowyTexture, vUV * 10.0 );

       gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0) + dirt + sand + grass + rock + snow;
     }


chore(Gruntfile): Clean up grunt file

chore(Gruntfile): Clean up grunt file

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,23 +1,4 @@
 module.exports = (grunt) ->
-  console.log """
-
-  f;:i     f;:l                       ,:ilttlf:
-  fffLf   Lffff                      ,iltLllCLf
-  tfLL;   itfLf     t       tltfL f ;itfffGtiiLfl;llt @,:;Cliii  li;f;lii;,tf::;litf
-  tfLLfL  ;LfLt    Lif    L;lLLLftf :Lfff    lit tLLf Ltff fLLf  lfLGffLCCCLfffLLLftL
-  itLLLt liLfLt    itt   C;tLtLfiLtllfLi      l  fLLfLtL;  ftLfLfftf tLf   l lft  fft
-  iliLLtC;LlfLt    ;ttC  lfffC  @lllffL;  ;i;;lt  fLL;fff   lLLttLf  iftli;l iLt tfLt
-  iiilLLfiLffLi@  ;ttfi  lfLL    @ltffLL fLLfltCf ttLtCf    lif;fft  lftt;:f ifli;Ct
-  iilfLLlLttffi@  ittLtfLlfLi      ltLLL it;:ffLf  fLLL      lLfLl   iL;,:ti iLlLL;
-  ;ilftLLCLtfLi@ ttfLLLlLtLLl     LfiLLf LLLtfLLt   lLt      fffLl   ;ff     ;LtlLl
-  litLtLLtCtLLlG ;ClllLLffLLf    ttLfCLLL   tlLLi   tLt      tiLtf   iLl   : :LtftLt
-  llfLffCi ;LLlGitfLLfLLtflLLfCL;tLLflCLftt:.tLLt   tLi       iLl    iLl:;:t :Lt lLLi
-  tfffLtCC iLLt@tCl  CiLLittCLLfCLf  tfLCLLLCCfl    LCi       ;ft    LCCCCCi fCL  lLL;
-  iLLf tf  lffffLft   fttt tltiltt    ftti;::lt    tfttf      Lf    tftllltf;ftit lfft
-        C                                l;;                   C                   ff
-
-  """
-
   require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks

   grunt.initConfig
@@ -70,7 +51,7 @@
   ]

   grunt.registerTask "dev", "Watch src and run test server", [
-    "compile:dev"
+    "compile"
     "copy:data"
     "copy:template"
     "karma:unit"


Replaced database recreation with migrations.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -140,7 +140,7 @@

   grunt.registerTask 'build', ['sass']

-  grunt.registerTask 'install', ['install-virtualenv', 'install-dependencies', 'build', 'init-db']
+  grunt.registerTask 'install', ['install-virtualenv', 'install-dependencies', 'build', 'migrate-db']

   grunt.registerTask 'pip-install', 'Get remaining dependencies', ->
@@ -181,27 +181,17 @@

-  grunt.registerTask 'clean-db', 'Clean the database', ->
-    done = this.async()
+  grunt.registerTask 'migrate-db', 'Migrate the database', ->

-    grunt.log.writeln 'deleting database...'
-    fs.unlink 'db.sqlite3', (err) ->
-      if not err == null
-        grunt.log.writeln err
-        done(false)
-      else
-        grunt.task.run('init-db')
-        done()
+    loaddata = not fs.existsSync 'db.sqlite3'
+
+    grunt.log.writeln 'migrating...'
+    pythonExecSync ['manage.py', 'makemigrations']
+    pythonExecSync ['manage.py', 'migrate']
+
+    if loaddata
+      grunt.log.writeln 'loading sample data...'
+      pythonExecSync ['manage.py', 'loaddata', 'courses']

-  grunt.registerTask 'init-db', 'Initialize the database', ->
-    done = this.async()
-
-    grunt.log.writeln 'migrating...'
-    pythonExecSync ['manage.py', 'migrate']
-    grunt.log.writeln 'loading sample data...'
-    pythonExecSync ['manage.py', 'loaddata', 'courses']
-
-    done()
-
   grunt.registerTask 'default', ['sass']


Added spatial search to physics system.

Added spatial search to physics system.

--- src/physics/physics.coffee
+++ src/physics/physics.coffee
@@ -22,16 +22,21 @@
   # The particle emitter
   emitter: null

+  # The spatial optimisation manager to use
+  space: null
+
+  # Settings
   springIterations: 1

   constructor: ->
+    @space = new Space()
+    @emitter = new Emitter(this)
     @clear()

   clear: ->
     @particles = []
     @behaviours = []
     @constraints = []
-    @emitter = new Emitter(this)

   # polymorphic add
   add: ->
@@ -69,6 +74,8 @@

   update: ->
     @emitter.update()
+
+    @space.update(this)

     particles = @particles

@@ -126,11 +133,32 @@
   size: -> @particles.length

-
-###
-
+###
+  Spatial Search
+
+  Simple brute force spatial searches.
+  Subclasses may override this to organise particles so they can be found quicker later
+###
+class Space
+  physics = null
+
+  constructor: ->
+
+  update: (physics_) -> physics = physics_
+
+  search: (center, radius) ->
+    result = []
+    radiusSq = radius * radius
+
+    for particle in physics.particles
+      if center.distanceSquared(particle.position) < radiusSq
+        result.push particle
+
+    result
+
+
+###
   Particle Emitter
-
 ###
 class Emitter
   rate: 1
@@ -199,6 +227,8 @@
     delta = @b.position.sub_ @a.position
     dist = delta.length()

+    return if dist == 0
+
     normDistStrength = (dist - @restLength) / (dist * @strength) * 0.5

     if not @a.isLocked
@@ -213,6 +243,7 @@
 module.exports =
   Physics: Physics
   Emitter: Emitter
+  Space: Space
   Behaviour: Behaviour
   Constraint: Constraint
   Spring: Spring


client: remove formatContent util

client: remove formatContent util

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/util/formatContent.coffee
+++ client/app/lib/util/formatContent.coffee
@@ -1,25 +0,0 @@
-transformTagTokens = require './transformTagTokens'
-transformEmails = require './transformEmails'
-transformTags = require './transformTags'
-formatQuotes = require './formatQuotes'
-formatBlockquotes = require './formatBlockquotes'
-applyMarkdown = require './applyMarkdown'
-expandUsernames = require './expandUsernames'
-markdownUrls = require './markdownUrls'
-
-module.exports = (body = '', markdownOptions = {}) ->
-
-  fns = [
-    transformTagTokens
-    transformTags
-    transformEmails
-    formatQuotes
-    formatBlockquotes
-    markdownUrls
-  ]
-
-  body = fn body for fn in fns
-  body = applyMarkdown body, markdownOptions
-  body = expandUsernames body, 'code, a'
-
-  return body


Clear errors on subsequent submission

Clear errors on subsequent submission

--- apps/tools/components/send_invitation/index.coffee
+++ apps/tools/components/send_invitation/index.coffee
@@ -45,6 +45,8 @@

     serializer = new Serializer $form

+    $errors.empty()
+
     $submit
       .prop 'disabled', true
       .text 'Sending...'


Revert "Prevent bokeh from eating scroll events if wheel tool isn't active"

This reverts commit 3d0402c705845dcf6daf6d42184dc3ce7f9a608b.

--- bokehjs/src/coffee/common/ui_events.coffee
+++ bokehjs/src/coffee/common/ui_events.coffee
@@ -97,9 +97,6 @@
     gestures = tm.gestures or tm.get('gestures')
     active = gestures[base_event_type].active
     if active?
-      if event_type == "scroll"
-        e.preventDefault()
-        e.stopPropagation()
       @trigger("#{event_type}:#{active.id}", e)

   _bokify_hammer: (e) ->
@@ -197,6 +194,8 @@
     @_bokify_jq(e)
     e.bokeh.delta = delta
     @_trigger('scroll', e)
+    e.preventDefault()
+    e.stopPropagation()

   _key_down: (e) ->
     # NOTE: keydown event triggered unconditionally


fixes #4

fixes #4

--- client/views/common/editShiftModal/editShiftModal.coffee
+++ client/views/common/editShiftModal/editShiftModal.coffee
@@ -195,4 +195,5 @@
 				wrs -> FlowRouter.setQueryParams editShift: shiftId

 	'click #switch': ->
-		wrs -> FlowRouter.setQueryParams showShift: FlowRouter.getQueryParam 'editShift'
+		shiftId = FlowRouter.getQueryParam 'editShift'
+		wrs -> FlowRouter.setQueryParams showShift: shiftId


Suppression de l'animation d'explosion

Suppression de l'animation d'explosion

--- js/game/config.coffee
+++ js/game/config.coffee
@@ -12,6 +12,5 @@

         { id: 'bulletFireShot', src: 'images/bulletFireShot.png' },
         { id: 'lum', src: 'images/lum.png' },
-        { id: 'explosion', src: 'images/explosion.png' },
         { id: 'smoke', src: 'images/smoke.png' },
     ]


Fix height on resize

Fix height on resize

--- src/jquery.superslides.coffee
+++ src/jquery.superslides.coffee
@@ -52,11 +52,10 @@
     $('body').css
       margin: 0

-    $(el).css
+    @el.css
       position: 'relative'
       overflowX: 'hidden'
       width: '100%'
-      height: height

     $control.css
       position: 'relative'
@@ -80,6 +79,9 @@
       "z-index": '-1'

   setupContainers = =>
+    @el.css
+      height: height
+
     $control.css
       width: width * multiplier
       left: -width


Correctly handle output loading on a finished build with a failed action

--- js/inner/build.coffee
+++ js/inner/build.coffee
@@ -28,11 +28,17 @@

   constructor: (json) ->

-    json.steps = if json.steps? then (new CI.inner.Step(j, @) for j in json.steps) else []
+    steps = json.steps
+
+    json.steps = []

     super(json)

     CI.inner.VcsUrlMixin(@)
+
+    if steps?
+      for s in steps
+        @steps.push(new CI.inner.Step(s, @))

     @url = @komp =>
       @urlForBuildNum @build_num


replace dead image link

replace dead image link

--- src/scripts/geordie.coffee
+++ src/scripts/geordie.coffee
@@ -12,7 +12,7 @@
   "http://static.squarespace.com/static/5011abbee4b0253ab1204ac0/t/511b569ae4b0dcc6d89c9db5/1360746138704/GAzza.jpeg",
   "http://www.assetstorage.co.uk/AssetStorageService.svc/GetImageFriendly/721463860/339/422/0/0/1/80/ResizeBestFit/0/FRU/883FBEAF0426B94D8793618077E9D342/geoff-and-spuggy.jpg",
   "http://www.bbc.co.uk/tyne/content/images/2006/08/21/ant_and_dec_show_470x353.jpg",
-  "http://www.starstills.com/product_images/m/ss2237118_-_photograph_of_jimmy_nail_as_leonard_jeffrey_oz_osborne_from_auf_wiedersehen_pet_available_in_4_sizes_framed_or_unframed_buy_now_at_starstills__91430.jpg",
+  "http://i.telegraph.co.uk/multimedia/archive/00585/news-graphics-2004-_585763a.jpg",
   "http://i2.cdnds.net/13/31/618x579/uktv-auf-wiedersehen-pet.jpg",
   "http://eil.com/images/main/Lindisfarne+-+Fog+On+The+Tyne+%5BRevisited%5D+-+5%22+CD+SINGLE-494879.jpg",
   "https://s3.amazonaws.com/uploads.hipchat.com/64658/539392/B3WShhDa0HOiKZ3/upload.png",


fix tests

fix tests

--- components/marketing_signup_modal/test/middleware.coffee
+++ components/marketing_signup_modal/test/middleware.coffee
@@ -3,29 +3,20 @@

 describe 'showMarketingSignupModal', ->
   beforeEach ->
-    @req = {}
+    @req = query: { 'm-id': 'foo' }
     @res =
       locals:
         sd:
           APP_URL: 'http://www.artsy.net'
-          MARKETING_SIGNUP_MODAL_PATHS: '/foo,/bar'
+          MARKETING_SIGNUP_MODAL_SLUG: 'foo'
     @next = sinon.stub()

-  it 'shows the modal if coming from outside artsy', ->
-    @req.path = '/foo'
-    @req.get = sinon.stub().returns 'google.com'
+  it 'shows the modal if coming from a campaign', ->
     middleware @req, @res, @next
     @res.locals.showMarketingSignupModal.should.be.ok()

   it 'does not show the modal if coming from artsy', ->
-    @req.path = '/foo'
-    @req.get = sinon.stub().returns 'artsy.net'
-    middleware @req, @res, @next
-    (@res.locals.showMarketingSignupModal?).should.not.be.ok()
-
-  it 'does not show the modal if not in the right path', ->
-    @req.path = '/baz'
-    @req.get = sinon.stub().returns 'google.com'
+    @req.query = {}
     middleware @req, @res, @next
     (@res.locals.showMarketingSignupModal?).should.not.be.ok()


Attributes with override values should always show them

--- src/View/Expression.coffee
+++ src/View/Expression.coffee
@@ -22,7 +22,7 @@
     attribute: Model.Attribute
   render: ->
     attribute = @props.attribute
-    if attribute.isTrivial()
+    if attribute.isTrivial() and not attribute.hasOverrideValue()
       R.span {}
     else
       value = attribute.value()


Single listing for keeping track of brackets

--- src/lib/InternalSocket.coffee
+++ src/lib/InternalSocket.coffee
@@ -27,12 +27,10 @@
         metadata: @metadata

   constructor: (@metadata = {}) ->
-    @connected = false
-    @groups = []
+    @brackets = []
     @dataDelegate = null
     @debug = false
     @emitEvent = @regularEmitEvent
-    @braceCount = 0

   # ## Socket connections
   #
@@ -65,7 +63,7 @@
   disconnect: ->
     @handleSocketEvent 'disconnect', null

-  isConnected: -> @connected
+  isConnected: -> @brackets.length > 0

   # ## Sending information packets
   #
@@ -178,8 +176,7 @@
   ipToLegacy: (ip) ->
     switch ip.type
       when 'openBracket'
-        @braceCount++
-        if @braceCount is 1
+        if @brackets.length is 1
           return legacy =
             event: 'connect'
             payload: @
@@ -191,8 +188,7 @@
           event: 'data'
           payload: ip.data
       when 'closeBracket'
-        @braceCount--
-        if @braceCount is 0
+        if @brackets.length is 0
           return legacy =
             event: 'disconnect'
             payload: @
@@ -204,41 +200,36 @@
     ip = @legacyToIp event, payload

     # Handle state transitions
-    if ip.type is 'data' and not @connected and autoConnect
+    if ip.type is 'data' and @brackets.length is 0 and autoConnect
       # Connect before sending data
       @handleSocketEvent 'connect', null

-    if ip.type is 'openBracket' and ip.data
-      if not @connected and autoConnect
-        # Connect before sending bracket
-        @handleSocketEvent 'connect', null
-      # Append bracket to socket group list
-      @groups.push ip.data
-
-    if ip.type is 'openBracket' and ip.data is null
-      # If we're already connected, no need to connect again
-      return if @connected
-      @connected = true
+    if ip.type is 'openBracket'
+      if ip.data is null
+        # If we're already connected, no need to connect again
+        return if @brackets.length
+      if ip.data
+        if @brackets.length is 0 and autoConnect
+          # Connect before sending bracket
+          @handleSocketEvent 'connect', null
+      @brackets.push ip.data

     if ip.type is 'closeBracket'
-      if @groups.length
-        # Add group name to bracket
-        ip.data = @groups.pop()
-      else
+      unless @brackets.length
+        # Last bracket was closed, we're disconnected
         # If we were already disconnected, no need to disconnect again
-        return unless @connected
-        # Last bracket was closed, we're disconnected
-        @connected = false
-        ip.data = null
+        return if @brackets.length is 0
+      # Add group name to bracket
+      ip.data = @brackets.pop()

     # Emit the IP Object
     @emitEvent 'ip', ip

     # Emit the legacy event
     legacyEvent = @ipToLegacy ip
-    @emitEvent 'connect' if ip.type is 'data' and @braceCount is 0 and not @connected
+    @emitEvent 'connect', @ if ip.type is 'data' and @brackets.length is 0 and autoConnect
     @emitEvent legacyEvent.event, legacyEvent.payload
-    @emitEvent 'disconnect' if ip.type is 'data' and @braceCount is 0 and @connected
+    @emitEvent 'disconnect', @ if ip.type is 'data' and @brackets.length is 0 and autoConnect

 exports.InternalSocket = InternalSocket


Defined 4 puzzles

Defined 4 puzzles

--- web/scripts/codeland.coffee
+++ web/scripts/codeland.coffee
@@ -105,7 +105,6 @@
 _sequence4.dyk = []

-
 root.getGameDescriptions = ->
     @gameDescriptions ?= {
         sequence1 : _sequence1


Use path.basename() instead of fsUtils.base()

--- src/app/package.coffee
+++ src/app/package.coffee
@@ -1,4 +1,4 @@
-fsUtils = require 'fs-utils'
+{basename} = require 'path'

 ### Internal ###
 module.exports =
@@ -21,4 +21,4 @@
   path: null

   constructor: (@path) ->
-    @name = fsUtils.base(@path)
+    @name = basename(@path)


config: run user-importer with dev config

--- config/main.dev.coffee
+++ config/main.dev.coffee
@@ -1049,7 +1049,7 @@
       function importusers () {

         cd #{projectRoot}
-        node #{projectRoot}/scripts/user-importer
+        node #{projectRoot}/scripts/user-importer -c dev

         migrateusers


feat (Transitions) add removeStaticContainer API method

--- src/models/Transitions.coffee
+++ src/models/Transitions.coffee
@@ -20,3 +20,9 @@
       successCallbacks: [callbacks.onSuccess, callbacks.onAnimationStarted]
       recurringCallbacks: [callbacks.onAnimationEnded]
       failureCallbacks: [callbacks.onFailure]
+
+  removeStaticContainer: (options = {}, callbacks = {}) ->
+    steroids.nativeBridge.nativeCall
+        method: "removeStaticContainerFromLayer"
+        successCallbacks: [callbacks.onSuccess]
+        failureCallbacks: [callbacks.onFailure]


Fix VueJS component snippet

Fix VueJS component snippet

Signed-off-by: Caddy Dz <cbc6199cd1cb93e9d3589fb9aa7ecaece7b37833@gmail.com>

--- src/atom/snippets.cson
+++ src/atom/snippets.cson
@@ -10,9 +10,9 @@
     'prefix' : 'router'
     'body': '<router-link to="${1:url}" exact>${2:title}</router-link>\n\t$3'
 '.source.js':
-  'vue.component':
-    'prefix' : 'vue.component'
+  'VueJS component':
+    'prefix' : 'component'
     'body': 'Vue.component (\'${1:name}\', {\n\tprops: [\n\t\t\'${2:property}\',\n\t],\n\ttemplate: `\n\t\t${3:html}\n\t`,\n\tmethods: {\n\t\t${4:method_name}() {\n\t\t\t${5:// code}\n\t\t}\n\t}\n});\n$6'
   'new vue':
-  'prefix' : 'new vue'
-  'body': 'new Vue({\n\tel: \'${1:#app}\',\n\tdata: {\n\t\t${2:name} : \'${3:value}\',\n\t}\n});\n$7'+    'prefix' : 'new vue'
+    'body': 'new Vue({\n\tel: \'${1:#app}\',\n\tdata: {\n\t\t${2:name} : \'${3:value}\',\n\t}\n});\n$7'


Fix Stylus test

Fix Stylus test

--- tests.coffee
+++ tests.coffee
@@ -92,22 +92,25 @@
 "

 css = "
-.someclass {\n
-  color: #dc143c;\n
-  color: #dc143c;\n
-  color: #dc143c;\n
-  color: #dc143c;\n
-  color: #dc143c;\n
-}\n
+.someclass {
+  color: #dc143c;
+  color: #dc143c;
+  color: #dc143c;
+  color: #dc143c;
+  color: #dc143c;
+}
 "
+
+# Whitespace doesn't matter
+nows = (text) -> text.replace /\s+/g, ''

 try
   stylus = require 'stylus'
 if stylus?
   stylus(styl).use(colorspaces()).render (err, test_css) ->
     throw err if err
-    if test_css is css
+    if nows(test_css) is nows(css)
       console.log 'Stylus works'
     else
-      console.log 'STYLUS ERROR\n' + test_css
+      console.log 'STYLUS ERROR' + test_css


freq analysis for calls

freq analysis for calls

--- src/calls/index.coffee
+++ src/calls/index.coffee
@@ -11,4 +11,18 @@

 rows = parseCalls file

-console.log rows
+# console.log rows
+
+console.log _.chain(rows)
+    .groupBy('Number')
+    .each((calls, number) ->
+        times = _.chain(calls)
+            .pluck('Date')
+            .map((date) -> moment(date))
+            .sortBy((date) -> date.unix())
+            .value()
+        calls.splice.apply(calls, [0, calls.length].concat([number, times.length, _.last(times).fromNow()]))
+    )
+    .sortBy(1)
+    .reverse()
+    .value()


fix bugs

fix bugs

--- app/lib/mini-course.cjsx
+++ app/lib/mini-course.cjsx
@@ -7,7 +7,9 @@
 apiClient = require 'panoptes-client/lib/api-client'

 completedThisSession = {}
-window?.tutorialsCompletedThisSession = completedThisSession
+window?.minicoursesCompletedThisSession = completedThisSession
+
+MINI_COURSE_LAST_SLIDE_SEEN = 0

 module.exports = React.createClass
   displayName: 'MiniCourse'
@@ -34,21 +36,16 @@
           # There's no workflow tutorial and no project given.
           Promise.resolve()

-    checkIfCompleted: (projectPreferences) ->
-      getCompletedAt = if completedThisSession[project.id]?
-        Promise.resolve new Date completedThisSession[project.id]
-      else if projectPreferences?
-        new Date projectPreferences?.preferences?.mini_course_completed_at
+    checkIfCompleted: (minicourse, project, user) ->
+      if user?
+        user.get 'project_preferences', project_id: project.id
+          .catch =>
+            []
+          .then ([projectPreferences]) =>
+            window.prefs = projectPreferences
+            projectPreferences?.preferences?.minicourses?.completed_at?["id_#{minicourse.id}"]?
       else
-        Promise.resolve null
-
-      getCompletedAt.then (completedAt) =>
-        if isNaN completedAt?.valueOf()
-          false
-        else
-          # TODO: Check if the completion date is greater than the minicourse's modified_at date.
-          # Return `null` to mean "Completed, but not with the most recent version".
-          true
+        Promise.resolve completedThisSession[minicourse.id]?

     start: (minicourse, user) ->
       MiniCourseComponent = this
@@ -67,7 +64,7 @@

           awaitMiniCourseMedia.then (mediaByID) =>
             Dialog.alert(<MiniCourseComponent project={project} user={user} minicourse={minicourse} media={mediaByID} />, {
-              className: 'tutorial-dialog minicourse-dialog', #reusing tutorial styling, minicourse-dialog used for submit
+              className: 'tutorial-dialog', #reusing tutorial styling
               required: true,
               closeButton: true
               onSubmit: @handleOptOut.bind(null, project, user, minicourse.id)
@@ -78,14 +75,14 @@
     startIfNecessary: ({workflow, project, user}) ->
       @find({workflow, project}).then (minicourse) =>
         if minicourse?
-          # @checkIfCompleted(minicourse, user).then (completed) =>
-          #   console.log {completed}
-          #   unless completed
-          if user?
-            user.get('project_preferences', project_id: project.id).then ([projectPreferences]) =>
-              @start minicourse, user unless projectPreferences.preferences.minicourses.opt_out["id_#{minicourse.id}"]
-          else
-            @start minicourse, user
+          @checkIfCompleted(minicourse, project, user).then (completed) =>
+            console.log {completed}
+            unless completed
+              if user?
+                user.get('project_preferences', project_id: project.id).then ([projectPreferences]) =>
+                  @start minicourse, user unless projectPreferences.preferences.minicourses.opt_out["id_#{minicourse.id}"]
+              else
+                @start minicourse, user

     handleOptOut: (project, user, minicourseID) ->
       if user?
@@ -99,7 +96,6 @@
   #     content: React.PropTypes.string

   getDefaultProps: ->
-    # steps: []
     media: {}
     minicourse: {}
     project: {}
@@ -109,7 +105,7 @@
     slideToStart: 0
     projectPreferences: null

-  componentDidMount: ->
+  componentWillMount: ->
     defaultPreferences = { "preferences.minicourses.opt_out.id_#{@props.minicourse.id}": false, "preferences.minicourses.slide_last_seen.id_#{@props.minicourse.id}": 0 }

     if @props.user?
@@ -117,13 +113,18 @@
         # projectPreferences.update "preferences": {}
         # projectPreferences.save()
         if projectPreferences.preferences.minicourses?
+          console.log('projectPreferences')
           if projectPreferences.preferences.minicourses.opt_out["id_#{@props.minicourse.id}"] # user is restarting via mini-course button
+            console.log('defaultPreferences', defaultPreferences)
             projectPreferences.update defaultPreferences
             projectPreferences.save()
               .then =>
-                slideToStart = projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"]
+                slideToStart = 0
                 @setState {slideToStart: slideToStart, projectPreferences: projectPreferences}
-          else if projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"]?
+          else if projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"] >= @props.minicourse.steps.length
+            slideToStart = 0
+            @setState {slideToStart: slideToStart, projectPreferences: projectPreferences}
+          else
             slideToStart = projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"]
             @setState {slideToStart: slideToStart, projectPreferences: projectPreferences}
         else
@@ -136,20 +137,19 @@
             preferences: {}
           })
           projectPreferences.update defaultPreferences
-          # projectPreferences.update 'preferences.mini_course_completed_at': now
           projectPreferences.save()
             .then =>
               @setState {slideToStart: slideToStart, projectPreferences: projectPreferences}
     else
-      slideToStart = 0
+      slideToStart = sessionStorage.getItem('minicourse_slide_last_seen') if sessionStorage.getItem('minicourse_slide_last_seen')?
+
       @setState {slideToStart}

   componentWillUnmount: ->
     if @state.projectPreferences?
-      if @state.projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"] < @props.minicourse.steps.length
-        nextSlideIndex = @state.projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"] + 1
-        @state.projectPreferences.update "preferences.minicourses.slide_last_seen.id_#{@props.minicourse.id}": nextSlideIndex
-        @state.projectPreferences.save()
+      @handleProjectPreferencesOnUnmount()
+    else
+      sessionStorage.setItem('minicourse_slide_last_seen', @state.slideToStart + 1) if @state.slideToStart < @props.minicourse.steps.length

   render: ->
     <StepThrough ref="stepThrough" className="tutorial-steps" defaultStep={@state.slideToStart}>
@@ -164,3 +164,16 @@
             </p>}
         </MediaCard>}
     </StepThrough>
+
+  handleProjectPreferencesOnUnmount: ->
+    if @state.projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"] < @props.minicourse.steps.length
+      nextSlideIndex = @state.projectPreferences.preferences.minicourses.slide_last_seen["id_#{@props.minicourse.id}"] + 1
+      @state.projectPreferences.update "preferences.minicourses.slide_last_seen.id_#{@props.minicourse.id}": nextSlideIndex
+      @state.projectPreferences.save()
+
+    if @state.slideToStart is @props.minicourse.steps.length - 1
+      now = new Date().toISOString()
+      completedThisSession[@props.minicourse.id] = now
+
+      @state.projectPreferences.update "preferences.minicourses.completed_at.id_#{@props.minicourse.id}": now
+      @state.projectPreferences.save()


Remove bans related to the server on server.remove()

Signed-off-by: Ian Macalinao <08370e385f4778cad8fe504ec5445edd3d45bd9a@gmail.com>

--- app/models/server.coffee
+++ app/models/server.coffee
@@ -1,5 +1,7 @@
 crypto = require 'crypto'
 mongoose = require 'mongoose'
+
+Ban = require 'ban'

 schema = new mongoose.Schema
   ip: type: String, required: yes, unique: yes
@@ -34,6 +36,10 @@
   else
     next()

+schema.pre 'remove', (next) ->
+  Ban.find({server: @_id}).remove ->
+    next()
+
 schema.virtual('link').get -> "/servers/#{@ip}"

 schema.virtual('editLink').get -> "/servers/#{@ip}/edit"


shortcuts/item: handle events

shortcuts/item: handle events

--- client/app/lib/shortcuts/views/item.coffee
+++ client/app/lib/shortcuts/views/item.coffee
@@ -22,16 +22,10 @@

   viewAppended: ->

-    self = this
-
     toggle = new kd.View
       cssClass : 'col'
       tagName  : 'div'
       partial  : renderToggle enabled : @model.enabled
-      click    : (e) ->
-        enabled = if _.isBoolean e then e else not self._enabled
-        @updatePartial renderToggle enabled: (self._enabled = enabled)
-        self.emit 'StateChanged', @id

     description = new kd.View
       cssClass : 'col'
@@ -40,14 +34,35 @@
     binding = new kd.View
       cssClass : 'col'
       partial  : present _.first @model.binding
-      click    : => @emit 'BindingSelected', @id
+
+    toggleClickHandler = (e) =>
+      enabled = if _.isBoolean e then e else not @_enabled
+      toggle.updatePartial renderToggle enabled: (@_enabled = enabled)
+      @emit 'Toggled', @_enabled
+
+    bindingClickHandler = =>
+      @setClass 'active'
+      binding.updatePartial ''
+      @emit 'Selected',
+        (res) =>
+          partial = if res then present res else present _.first @model.binding
+          binding.updatePartial partial
+          @unsetClass 'active'
+
+    syncHandler = (model) ->
+      toggle.click model.enabled
+      #binding.updatePartial present _.first model.binding
+
+    destroyHandler = =>
+      @off        'Synced' , syncHandler
+      toggle.off  'click'  , toggleClickHandler
+      binding.off 'click'  , bindingClickHandler
+
+    toggle.on  'click'                   , toggleClickHandler
+    binding.on 'click'                   , bindingClickHandler
+    @on        'Synced'                  , syncHandler
+    @once      'KDObjectWillBeDestroyed' , destroyHandler

     @addSubView toggle
     @addSubView description
     @addSubView binding
-
-    @on 'Synced', (model) ->
-      toggle.click model.enabled
-      binding.updatePartial present _.first model.binding
-
-    @once 'KDObjectWillBeDestroyed', => @off 'Synced'


Added convertToSplitView method to tab view to make it split.

--- client/ide/views/tabview/idetabview.coffee
+++ client/ide/views/tabview/idetabview.coffee
@@ -62,3 +62,15 @@

   removeOpenDocument: ->
     # TODO: This method is legacy, should be reimplemented in ace bundle.
+
+  convertToSplitView: ->
+    {parent} = this
+    @detach()
+    @unsetParent()
+
+    newTabView = new IDETabView
+    splitView  = new KDSplitView
+      type     : "vertical" # TODO: Hardcoded, should be optional
+      views    : [ this, newTabView ]
+
+    parent.addSubView splitView


work on bounty 227

work on bounty 227

--- client/source/views/entries/browser.coffee
+++ client/source/views/entries/browser.coffee
@@ -65,8 +65,16 @@

   modelSaved: (entry, newData) =>
     if newData?.id
+      notificationMessage = "You saved “#{entry.get('title')}”"
+      if newData.publishDate? && newData.status == "live"
+        notificationMessage = "You published “#{entry.get('title')}”"
+      else if newData.status == "draft"
+        if entry.previousAttributes().lastModified != newData.lastModified
+          notificationMessage = "You updated “#{entry.get('title')}”"
+        else
+          notificationMessage = "You saved “#{entry.get('title')}” draft"
+      toastr.success notificationMessage
       @model.set newData
-      toastr.success "You saved “#{entry.get('title')}”"
       @collection.add @model
     else
       toastr.success "You deleted “#{entry.get('title')}”"


Disable use_bl for /top request

Disable use_bl for /top request

--- coffee/controllers/posts_controller.coffee
+++ coffee/controllers/posts_controller.coffee
@@ -28,6 +28,8 @@
       else if params.tag?
         query.tag = params.tag
         params.title = "*#{params.tag}"
+      else
+        query = {}

       @collection = new Posts [], id: params.id, query: query
       @view = new PostsView collection: @collection, pageble: params.pageble


Resize only visible elements for nice performance increase.

--- web/lib/coffeescripts/layout.coffee
+++ web/lib/coffeescripts/layout.coffee
@@ -10,7 +10,7 @@
     this.fn()

   fill: (selector, get, set) ->
-    $(selector).each (ix, node) =>
+    $(selector).filter(':visible').each (ix, node) =>
       node = $(node)
       getter = node[get]
       parent = getter.call node.parent(), true


Draw a fake line before asking for approval, revert the approval back.

--- client/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
+++ client/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
@@ -26,10 +26,10 @@

     items = parseItems source, target
     {domain, machine} = items
-    return unless domain
-
-    @askForApprove items, 'delete', (modal)=>
-      return unless machine? # Remove a domain from a machine
+    return unless domain or machine
+
+    modal = @createApproveModal items, 'delete'
+    modal.once 'Approved', =>
       jDomain = domain.dia.getData().domain # JDomain
       vmName  = machine.dia.getData().title # JVM.hostnameAlias
       jDomain.unbindVM hostnameAlias: vmName, (err)=>
@@ -45,17 +45,27 @@

     items = parseItems source, target
     {domain, machine} = items
-    return unless domain
-
-    if machine?
-      if domain.dia.getData().domain.hostnameAlias.length > 0
-        return new KDNotificationView
-          title : "A domain name can only be bound to one VM."
-
-    if machine? # Assign a domain to a machine
+    return unless domain or machine
+
+    if domain.dia.getData().domain.hostnameAlias.length > 0
+      return new KDNotificationView
+        title : "A domain name can only be bound to one VM."
+
+    @addFakeConnection {
+      source, target,
+      options : {
+        lineColor  : "#cdcdcd"
+        lineDashes : [5]
+      }
+    }
+
+    modal = @createApproveModal items, 'create'
+    modal.once "KDObjectWillBeDestroyed", @bound 'resetScene'
+    modal.once 'Approved', =>
       jDomain = domain.dia.getData().domain # JDomain
       vmName  = machine.dia.getData().title # JVM.hostnameAlias
       jDomain.bindVM hostnameAlias: vmName, (err)=>
+        modal.destroy()
         return KD.showError err  if err
         jDomain.hostnameAlias.push vmName
         createConnection()
@@ -67,13 +77,11 @@
           @connect {dia : domain , joint : 'right'}, \
                    {dia : machine, joint : 'left' }, yes

-  askForApprove:(items, action, callback)->
+  createApproveModal:(items, action)->
     return unless KD.isLoggedIn()
       new KDNotificationView
         title : "You need to login to change domain settings."
-
-    modal = new EnvironmentApprovalModal {action}, items
-    modal.once 'Approved', => callback modal
+    return new EnvironmentApprovalModal {action}, items

   whenItemsLoadedFor:do->
     # poor man's when/promise implementation ~ GG


'wrapper' is called 'data' now

'wrapper' is called 'data' now

--- spec/lanes/models/PubSubSpec.coffee
+++ spec/lanes/models/PubSubSpec.coffee
@@ -8,7 +8,7 @@
         }, { id: 1, foo: 'bar' })
         component = Lanes.Test.makeComponent({}, model: model)
         expect(Lanes.Models.PubSub.add).toHaveBeenCalledWith(model)
-        component.wrapper.destroy()
+        component.data.destroy()
         expect(Lanes.Models.PubSub.remove).toHaveBeenCalledWith(model)

     it "can retrieve a model after checkin", ->


Support dynamically reloading custom file .csscomb.json file changes

Instead of using require which caches the result, check if file exists and then use it if it does, otherwise fall back to bundled config.

--- src/beautifiers/csscomb.coffee
+++ src/beautifiers/csscomb.coffee
@@ -1,5 +1,6 @@
 "use strict"
 Beautifier = require('./beautifier')
+fs = require('fs')

 module.exports = class JSBeautify extends Beautifier
   name: "CSScomb"
@@ -23,7 +24,12 @@
       try
         # Load from Project's .csscomb.json
         projectConfigPath = atom.project.getDirectories()?[0]?.resolve('.csscomb.json')
-        config = require(projectConfigPath) # Will throw error if does not exist
+        #config = require(projectConfigPath) # Will throw error if does not exist
+
+        stats = fs.statSync(projectConfigPath)
+
+        config = Comb.getCustomConfig(projectConfigPath)
+
       catch e
         # Fallback to csscomb
         config = Comb.getConfig('csscomb')


changing things around

changing things around

--- src/browser/user-events.coffee
+++ src/browser/user-events.coffee
@@ -10,6 +10,7 @@
 _ = require 'underscore-plus'
 Filesys = require '../backside/filesystem_chizelin'
 electron = require 'electron'
+graph = require '../backside/updates_graph'
 app = electron.app
 ipcMain = electron.ipcMain

@@ -34,5 +35,6 @@
             onedriveFS = new Filesys('OneDrive')

             onedriveFS.searchFS('C:\\Users')
+            graph(onedriveFS)
             #console.log onedriveFS.tree
             event.sender.send 'actionReply', onedriveFS


Key not needed for topmost component

Key not needed for topmost component

--- resources/assets/coffee/react/profile-page/extra-header.coffee
+++ resources/assets/coffee/react/profile-page/extra-header.coffee
@@ -20,8 +20,7 @@
 el = React.createElement

 ProfilePage.ExtraHeader = (props) ->
-  div
-    key: 'header'
+  div null,
     h2 className: 'page-extra__title', osu.trans("users.show.extra.#{props.name}.title")
     if props.withEdit
       span className: 'page-extra__dragdrop-toggle js-profile-page-extra--sortable-handle',


Hide snooze popover if the plugin is disabled

Summary:
Snooze is back in the mainline but not yet ready for primetime. We need to not show the popover if it's not enabled, which is what this diff does. It should be pretty simple to revert it once snooze has officially shipped.

Fixes T7791.

Test Plan: Tested manually.

Reviewers: juan, evan

Reviewed By: evan

Maniphest Tasks: T7791

Differential Revision: https://phab.nylas.com/D3855

--- internal_packages/thread-list/lib/thread-list.cjsx
+++ internal_packages/thread-list/lib/thread-list.cjsx
@@ -163,6 +163,10 @@
       Actions.queueTasks(tasks)
       callback(true)

+    disabledPackages = NylasEnv.config.get('core.disabledPackages') ? []
+    if 'thread-snooze' in disabledPackages
+      return props
+
     if FocusedPerspectiveStore.current().isInbox()
       props.onSwipeLeftClass = 'swipe-snooze'
       props.onSwipeCenter = =>
@@ -248,6 +252,10 @@
     Actions.queueTask(task)

   _onSnoozeItem: =>
+    disabledPackages = NylasEnv.config.get('core.disabledPackages') ? []
+    if 'thread-snooze' in disabledPackages
+      return
+
     threads = @_threadsForKeyboardAction()
     return unless threads
     # TODO this should be grabbed from elsewhere


Don't expand on mobile and add other comment

--- resources/assets/coffee/react/_components/comment.coffee
+++ resources/assets/coffee/react/_components/comment.coffee
@@ -53,10 +53,14 @@
     @xhr = {}
     @loadMoreButtonRef = React.createRef()

-    if @isDeleted()
+    if osu.isMobile()
+      # There's no indentation on mobile so don't expand by default otherwise it will be confusing.
       expandReplies = false
+    else if @isDeleted()
+      expandReplies = false
     else
       @children = @props.commentsByParentId?[@props.comment.id] ? []
+      # Collapse if either no children is loaded or current level doesn't add indentation.
       expandReplies = @children.length > 0 && @props.depth < MAX_DEPTH

     @state =


Improved initial animation

Improved initial animation

--- resources/assets/coffee/react/profile-page/badges.coffee
+++ resources/assets/coffee/react/profile-page/badges.coffee
@@ -24,17 +24,21 @@
     super props

     @slideTimer = 5000
+    @initialSlide = Math.max(2000, @props.badges.length * 500)
     @timeouts = {}
     @intervals = {}

     @state =
       currentBadge: @props.badges.length
       looping: true
+      initial: true

   componentDidMount: =>
     @timeouts.first = Timeout.set 0, @nextBadge
-    @intervals.slider = setInterval @nextBadge, @slideTimer
+    @timeouts.slider = Timeout.set @initialSlide, =>
+      @setState initial: false
+      @intervals.slider = setInterval @nextBadge, @slideTimer

   componentWillUnmount: =>
@@ -63,7 +67,7 @@
           className: 'profile-badges__stripe'
           style:
             transform: @currentBadgeTransform()
-            transitionDuration: if @state.looping then '' else '250ms'
+            transitionDuration: @currentBadgeTransitionDuration()
           for badge in @props.badges
             div
               key: badge.image_url
@@ -80,10 +84,19 @@
     "translateX(#{-100 * @state.currentBadge / @props.badges.length}%)"

+  currentBadgeTransitionDuration: =>
+    if @state.initial
+      "#{@initialSlide}ms"
+    else if @state.looping
+      ''
+    else
+      '250ms'
+
   nextBadge: (callback) =>
     return if !@state.looping

-    @setState currentBadge: (@state.currentBadge + 1) % @props.badges.length, callback
+    @setState
+      currentBadge: (@state.currentBadge + 1) % @props.badges.length, callback

   pause: =>


Input: Added focus and blur listeners to input to add and remove focus class.

--- src/lib/components/Input.coffee
+++ src/lib/components/Input.coffee
@@ -36,6 +36,9 @@

     if options.name
       @setName options.name
+
+    @on 'focus', => @addClass 'focus'
+    @on 'blur',  => @removeClass 'focus'

   ###*
     Sets input value.


hide header only on scroll, as oppose to auto

--- src/components/task/index.cjsx
+++ src/components/task/index.cjsx
@@ -292,7 +292,6 @@
         'task-with-milestones': showMilestones

     <PinnedHeaderFooterCard
-      forceShy={true}
       className={taskClasses}
       fixedOffset={0}
       header={header}


Parallelized List filter

Parallelized List filter

--- list.coffee
+++ list.coffee
@@ -107,13 +107,16 @@

     filter: (f = _.identity) ->
-        J.List(
-            _.filter @map().getValues(), f
+        # Parallelize running the filter function
+        filterOutputs = @map f
+        filtered = J.List [],
             tag:
                 filteredFrom: @
                 filterFunc: f
                 tag: "filtering of #{@toString()}"
-        )
+        @forEach (v, i) ->
+            if filterOutputs.get i then filtered.push v
+        filtered

     forEach: (f) ->


added debug info hook

added debug info hook

--- lixian.iced
+++ lixian.iced
@@ -4,8 +4,11 @@
 path = require 'path'
 md5 = require 'MD5'
 jarson = require 'jarson'
-
-module.exports = class Lixian
+{
+  EventEmitter
+} = require 'events'
+
+module.exports = class Lixian extends EventEmitter
   headers:
     'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36'
     'Accept': 'text/html'
@@ -16,6 +19,15 @@
     @_jar = request.jar()
     if options.cookie
       @_jar._jar = jarson.fromJSON options.cookie
+
+    if @_debug
+      @on 'debug', (prefix, data)=>
+        data = JSON.stringify data, null, ' ' unless data?.constructor is String
+        if (lines = data.split '\n').length > 30
+          console.log "#{prefix}> #{line}" for line in lines[..30]
+          console.log "#{prefix}>   ... total: #{lines.length} lines ..."
+        else
+          console.log data.replace /^/mg, "#{prefix}> "

     await @_reload defer e
     return cb e if e
@@ -42,13 +54,7 @@
     return cb null, body

   debug: (prefix, data)->
-    return unless @_debug
-    data = JSON.stringify data, null, ' ' unless data?.constructor is String
-    if (lines = data.split '\n').length > 30
-      console.log "#{prefix}> #{line}" for line in lines[..30]
-      console.log "#{prefix}>   ... total: #{lines.length} lines ..."
-    else
-      console.log data.replace /^/mg, "#{prefix}> "
+    @emit 'debug', prefix, data

   _post: (url, qs, data, cb)->
     url += "?"


Removed dead code (window.getTextView)

Removed dead code (window.getTextView)

--- core/app/backbone/views/text_view.coffee
+++ core/app/backbone/views/text_view.coffee
@@ -4,8 +4,3 @@

   initialize: ->
     this.bindTo(this.model, 'change:text', this.render)
-
-window.getTextView = (text) ->
-  tv = TextView.extend
-    initialize: -> @model = new Backbone.Model({text: text})
-  return new tv()


tableverse/avatar: Avatars are always visible to GM.

--- Oghma/tableverse/avatar.coffee
+++ Oghma/tableverse/avatar.coffee
@@ -83,7 +83,8 @@

       @is_visible = =>
         @__.visible_to.length == 0 ||
-        @__.visible_to.indexOf( O.me().gets( 'name' ) ) != -1
+        @__.visible_to.indexOf( O.me().gets( 'name' ) ) != -1 ||
+        O.isGM()

       @show_label = =>
         if @is_visible() && @__k?


woops!

woops!

That's what I get for rushing an update on Christmas Eve...

--- templates:forms.coffee
+++ templates:forms.coffee
@@ -1,8 +1,5 @@

-# Add the next upcoming version here to test!
-# Now working on: v1.1.0
-
-@ReactiveNext = do ->
+@ReactiveForms = ReactiveForms = do ->

   # Forms
   # ========================================================================================


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/collidable.coffee
+++ test/collidable.coffee
@@ -41,7 +41,7 @@
     height: 20
     mass: 1
     velocity: Point(0, 0)
-
+  .include "Collidable"

   Collidable.separate(leftObject, rightObject)


Improve coffee syntax

Improve coffee syntax

--- test/conditions.coffee
+++ test/conditions.coffee
@@ -10,176 +10,173 @@
   describe 'if', ->

     they 'should bypass if not present', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         () -> should.be.ok false
         next
-      )
+
     they 'should succeed if `true`', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         if: true
         () -> should.be.ok false
         next
-      )
+
     they 'should failed if `false`', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         if: false
         next
         () -> should.be.ok false
-      )
+
     they 'should succeed on `succeed` callback', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         if: (options, failed, succeed) -> succeed()
         () -> should.be.ok false
         next
-      )
+
     they 'should failed on `failed` callback', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         if: (options, failed, succeed) -> failed()
         next
         () -> should.be.ok false
-      )
+
     they 'should pass error object on `failed` callback', (ssh, next) ->
-      conditions.if(
+      conditions.if
         ssh: ssh
         if: (options, failed, succeed) -> failed new Error 'cool'
         (err) -> err.message is 'cool' and next()
         () -> should.be.ok false
-      )

   describe 'if_exists', ->

     they 'should pass if not present', (ssh, next) ->
-      conditions.if_exists(
+      conditions.if_exists
         ssh: ssh
         () -> should.be.ok false
         next
-      )
+
     they 'should succeed if dir exists', (ssh, next) ->
-      conditions.if_exists(
+      conditions.if_exists
         ssh: ssh
         if_exists: __dirname
         () -> should.be.ok false
         -> next()
-      )
+
     they 'should skip if file does not exists', (ssh, next) ->
-      conditions.if_exists(
+      conditions.if_exists
         ssh: ssh
         if_exists: './oh_no'
         next
         () -> should.be.ok false
-      )
+
     they 'should succeed if all files exists', (ssh, next) ->
-      conditions.if_exists(
+      conditions.if_exists
         ssh: ssh
         if_exists: [__dirname, __filename]
         () -> should.be.ok false
         -> next()
-      )
+
     they 'should skip if at least one file exists', (ssh, next) ->
-      conditions.if_exists(
+      conditions.if_exists
         ssh: ssh
         if_exists: [__dirname, './oh_no']
         next
         () -> should.be.ok false
-      )
+

   describe 'not_if_exists', ->

-    they 'should succeed if not present', (ssh, next) ->
-      conditions.not_if_exists(
+    they 'succeed if not present', (ssh, next) ->
+      conditions.not_if_exists
         ssh: ssh
         () -> should.be.ok false
         next
-      )
-    they 'should skip if dir exists', (ssh, next) ->
-      conditions.not_if_exists(
+
+    they 'skip if dir exists', (ssh, next) ->
+      conditions.not_if_exists
         ssh: ssh
         not_if_exists: __dirname
         next
         () -> should.be.ok false
-      )
-    they 'should succeed if dir does not exists', (ssh, next) ->
-      conditions.not_if_exists(
+
+    they 'succeed if dir does not exists', (ssh, next) ->
+      conditions.not_if_exists
         ssh: ssh
         not_if_exists: './oh_no'
         () -> should.be.ok false
         -> next()
-      )
-    they 'should succeed if no file exists', (ssh, next) ->
-      conditions.not_if_exists(
+
+    they 'succeed if no file exists', (ssh, next) ->
+      conditions.not_if_exists
         ssh: ssh
         not_if_exists: ['./oh_no', './eh_no']
         () -> should.be.ok false
         -> next()
-      )
-    they 'should skip if at least one file exists', (ssh, next) ->
-      conditions.not_if_exists(
+
+    they 'skip if at least one file exists', (ssh, next) ->
+      conditions.not_if_exists
         ssh: ssh
         not_if_exists: ['./oh_no', __filename]
         next
         () -> should.be.ok false
-      )

   describe 'should_exist', ->
     they 'should succeed if file exists', (ssh, next) ->
-      conditions.should_exist(
+      conditions.should_exist
         ssh: ssh
         should_exist: __filename
         () -> should.be.ok false
         -> next()
-      )
+
     they 'should failed if file does not exist', (ssh, next) ->
-      conditions.should_exist(
+      conditions.should_exist
         ssh: ssh
         should_exist: './oh_no'
         (err) ->
           err.should.be.a 'object'
           next()
         () -> should.be.ok false
-      )
+
     they 'should failed if at least one file does not exist', (ssh, next) ->
-      conditions.should_exist(
+      conditions.should_exist
         ssh: ssh
         should_exist: ['./oh_no', __filename]
         (err) ->
           err.should.be.a 'object'
           next()
         () -> should.be.ok false
-      )

   describe 'should_not_exist', ->
     they 'should succeed if file doesnt exist', (ssh, next) ->
-      conditions.should_not_exist(
+      conditions.should_not_exist
         ssh: ssh
         should_not_exist: './oh_no'
         () -> should.be.ok false
         next
-      )
+
     they 'should failed if file exists', (ssh, next) ->
-      conditions.should_not_exist(
+      conditions.should_not_exist
         ssh: ssh
         should_not_exist: __filename
         (err) ->
           err.should.be.a 'object'
           next()
         () -> should.be.ok false
-      )
+
     they 'should failed if at least one file exists', (ssh, next) ->
-      conditions.should_not_exist(
+      conditions.should_not_exist
         ssh: ssh
         should_not_exist: ['./oh_no', __filename]
         (err) ->
           err.should.be.a 'object'
           next()
         () -> should.be.ok false
-      )

+


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -39,6 +39,7 @@
       equals engine.closest(".enemy", player2.position()), enemy2

   @name closest
+
   @param {String} selector
   @param {Point} position
   ###


Namespace jQuery hooks to avoid conflicts with the rest of the app.

--- app/assets/javascripts/admin/suppliers_code.js.coffee
+++ app/assets/javascripts/admin/suppliers_code.js.coffee
@@ -5,18 +5,18 @@

   # Hooks used for changing existing records quantities
   $(document)
-    .on 'click', 'table.index td.quantity', ->
+    .on 'click', '#suppliers_report table.index td.quantity', ->
       if ($(this).children('input').size() == 0)
         field = input_field.clone()
         field.val($(this).html())
         $(this).html field
         $(this).find('input').focus()
-    .on 'blur', 'table.index td.quantity input', (el) ->
+    .on 'blur', '#suppliers_report table.index td.quantity input', (el) ->
       $(this).parent().html $(this).val()

   # Because we need to process returned data we use a custom hook
   # Since we only use it here it is not made as a jQuery plugin ($.fn.variantAutocomplete)
-  $(".variant_autocomplete").select2
+  $("#suppliers_report .variant_autocomplete").select2
     placeholder: "Select a variant"
     minimumInputLength: 4
     allowClear: true
@@ -40,7 +40,7 @@

   # Handle clicks on "Add button"
   # Add variant name, sku and qty to the table
-  $('[data-hook="add_button"]').on "click", ->
+  $('#suppliers_report [data-hook="add_button"]').on "click", ->
     $fieldset = $(this).parents("fieldset")
     sku = $fieldset.data("sku")
     name = $fieldset.data("name")
@@ -63,11 +63,11 @@

     return false

-  $('[data-hook="send_button"]').on "click", ->
+  $('#suppliers_report [data-hook="send_button"]').on "click", ->
     window.report = new SuppliersEmailReport $(this).parents('.supplier_products_table')
     return false

-  $('[data-hook="send_email_button"]').on "click", ->
+  $('#suppliers_report [data-hook="send_email_button"]').on "click", ->
     content = $(this).siblings('textarea').val()
     encoded_content = encodeURIComponent(content)


* Synched

* Synched

--- app/assets/javascripts/backbone.rails_store.js.coffee
+++ app/assets/javascripts/backbone.rails_store.js.coffee
@@ -1372,6 +1372,15 @@
       success(resp, status, xhr) if success
     super

+  add: (model) ->
+    if model instanceof Backbone.RailsModel
+      if model instanceof @model
+        return super
+    else
+      return super
+
+    throw "Invalid model type #{@store.getModelType(model)} for collection #{@}"
+
   _prepareModel: (attrs, options) ->
     if attrs instanceof Backbone.RailsModel
       unless attrs.collection


Create static `keyStringFromEvent` method so we can get our fancy strings elsewhere if needed

--- app/assets/javascripts/exo/keyboard_manager.js.coffee
+++ app/assets/javascripts/exo/keyboard_manager.js.coffee
@@ -37,6 +37,20 @@
       '⌘': 91, command: 91
     }

+    @keyStringFromEvent = (e) ->
+      keys = []
+      keys.push '⇧' if e.shiftKey
+      keys.push '⌥' if e.altKey
+      keys.push '⌃' if e.ctrlKey
+      keys.push '⌘' if e.metaKey
+
+      if _map[e.keyCode]
+        keys.push _map[e.keyCode]
+      else
+        keys.push String.fromCharCode e.keyCode
+
+      keys.join '+'
+
     constructor: (@el = null) ->
       @_responders = []
       @cid = _.uniqueId("k")
@@ -55,22 +69,14 @@
       # exit early if it's just a modifier key all by itself
       return if e.keyCode in [16, 18, 17, 91]

-      keys = []
-      keys.push '⇧' if e.shiftKey
-      keys.push '⌥' if e.altKey
-      keys.push '⌃' if e.ctrlKey
-      keys.push '⌘' if e.metaKey
-
-      keys.push _map[e.keyCode] || String.fromCharCode e.keyCode
-
-      str = keys.join '+'
+      str = KeyboardManager.keyStringFromEvent e

       if e.type == "keydown"
         @_handleKey("handleKeyDown", str, e)
       else if e.type == "keyup"
         @_handleKey("handleKeyUp", str, e)

-      return this
+      this

     _handleKey: (handlerMethod, key, e) ->
       for responder in @_responders
@@ -87,7 +93,7 @@
       idx = @_responders.indexOf responder
       @_responders.splice idx, 1 if idx > -1
       @_responders.unshift responder
-      return this
+      this

     revoke: (responder) ->
       idx = @_responders.indexOf responder