some linting around

some linting around

--- test/phabs_test.coffee
+++ test/phabs_test.coffee
@@ -17,11 +17,11 @@
   hubotHear = (message) ->
     beforeEach (done) ->
       room.messages = []
-      room.user.say "momo", message
+      room.user.say 'momo', message
       room.messages.shift()
       setTimeout (done), 50

-  setEmail = () ->
+  setEmail = ->
     beforeEach ->
       room.receive = (userName, message) ->
         new Promise (resolve) =>
@@ -29,7 +29,7 @@
           user = new Hubot.User(userName, { room: @name, email_address: 'momo@example.com' })
           @robot.receive(new Hubot.TextMessage(user, message), resolve)

-  setPhid = () ->
+  setPhid = ->
     beforeEach ->
       room.receive = (userName, message) ->
         new Promise (resolve) =>
@@ -40,18 +40,18 @@
   hubot = (message) ->
     hubotHear "@hubot #{message}"

-  hubotResponse = () ->
+  hubotResponse = ->
     room.messages[0][1]

-  hubotResponseCount = () ->
+  hubotResponseCount = ->
     room.messages.length

   beforeEach ->
-    process.env.PHABRICATOR_URL = "http://example.com"
-    process.env.PHABRICATOR_API_KEY = "xxx"
-    process.env.PHABRICATOR_BOT_PHID = "PHID-USER-xxx"
-    process.env.PHABRICATOR_PROJECTS = "PHID-PROJ-xxx:proj1,PHID-PROJ-yyy:proj2"
-    room = helper.createRoom(httpd: false)
+    process.env.PHABRICATOR_URL = 'http://example.com'
+    process.env.PHABRICATOR_API_KEY = 'xxx'
+    process.env.PHABRICATOR_BOT_PHID = 'PHID-USER-xxx'
+    process.env.PHABRICATOR_PROJECTS = 'PHID-PROJ-xxx:proj1,PHID-PROJ-yyy:proj2'
+    room = helper.createRoom { httpd: false }

   afterEach ->
     delete process.env.PHABRICATOR_URL
@@ -84,21 +84,26 @@
       do nock.disableNetConnect
       nock(process.env.PHABRICATOR_URL)
         .get('/api/maniphest.info')
-        .reply( 200, { result: { status: 'open', priority: 'Low', name: 'Test task', ownerPHID: 'PHID-USER-42' }})
+        .reply(200, { result: {
+          status: 'open',
+          priority: 'Low',
+          name: 'Test task',
+          ownerPHID: 'PHID-USER-42'
+          } })
         .get('/api/user.query')
-        .reply( 200, { result: [{ userName: 'toto' }]})
+        .reply(200, { result: [{ userName: 'toto' }]})

     afterEach ->
       nock.cleanAll()

     context 'phab T42', ->
       hubot 'phab T42'
-      it "gives information about the task Txxx", ->
+      it 'gives information about the task Txxx', ->
         expect(hubotResponse()).to.eql 'T42 has status open, priority Low, owner toto'

     context 'ph T42 # with an ending space', ->
       hubot 'ph T42 '
-      it "gives information about the task Txxx", ->
+      it 'gives information about the task Txxx', ->
         expect(hubotResponse()).to.eql 'T42 has status open, priority Low, owner toto'

@@ -107,32 +112,32 @@
       do nock.disableNetConnect
       nock(process.env.PHABRICATOR_URL)
         .get('/api/user.query')
-        .reply( 200, { result: [{ phid: 'PHID-USER-42' }]})
+        .reply(200, { result: [ { phid: 'PHID-USER-42' } ] })
         .get('/api/maniphest.edit')
-        .reply( 200, { result: { object: { id: 42 }}})
+        .reply(200, { result: { object: { id: 42 } } })

     afterEach ->
       nock.cleanAll()

     context 'phab new something blah blah', ->
       hubot 'phab new something blah blah'
-      it "fails to comply if the project is not registered by PHABRICATOR_PROJECTS", ->
+      it 'fails to comply if the project is not registered by PHABRICATOR_PROJECTS', ->
         expect(hubotResponse()).to.eql 'Command incomplete.'

     context 'phab new proj1 a task', ->
       context 'when user is doing it for the first time and has no email recorded', ->
         hubot 'phab new proj1 a task'
-        it "invites the user to set his email address", ->
+        it 'invites the user to set his email address', ->
           expect(hubotResponse()).to.eql 'Sorry, I can\'t figure out your email address :( Can you tell me with `.phab me as you@yourdomain.com`?'
       context 'when user is doing it for the first time and has set an email addresse', ->
         setEmail()
         hubot 'phab new proj1 a task'
-        it "invites the user to set his email address", ->
+        it 'invites the user to set his email address', ->
           expect(hubotResponse()).to.eql 'Task T42 created = http://example.com/T42'
       context 'when user is known and his phid is in the brain', ->
         setPhid()
         hubot 'phab new proj1 a task'
-        it "invites the user to set his email address", ->
+        it 'invites the user to set his email address', ->
           expect(hubotResponse()).to.eql 'Task T42 created = http://example.com/T42'

@@ -142,7 +147,7 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/maniphest.update')
-          .reply( 200, { result: { error_info: 'No such Maniphest task exists.' }})
+          .reply(200, { result: { error_info: 'No such Maniphest task exists.' } })

       afterEach ->
         nock.cleanAll()
@@ -159,7 +164,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Open' }})
+            .reply(200, { result: { statusName: 'Open' } })

         afterEach ->
           nock.cleanAll()
@@ -173,7 +178,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Open' }})
+            .reply(200, { result: { statusName: 'Open' } })

         afterEach ->
           nock.cleanAll()
@@ -187,7 +192,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Resolved' }})
+            .reply(200, { result: { statusName: 'Resolved' } })

         afterEach ->
           nock.cleanAll()
@@ -201,7 +206,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Wontfix' }})
+            .reply(200, { result: { statusName: 'Wontfix' } })

         afterEach ->
           nock.cleanAll()
@@ -215,7 +220,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Invalid' }})
+            .reply(200, { result: { statusName: 'Invalid' } })

         afterEach ->
           nock.cleanAll()
@@ -229,7 +234,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { statusName: 'Spite' }})
+            .reply(200, { result: { statusName: 'Spite' } })

         afterEach ->
           nock.cleanAll()
@@ -244,7 +249,7 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/maniphest.update')
-          .reply( 200, { result: { error_info: 'No such Maniphest task exists.' }})
+          .reply(200, { result: { error_info: 'No such Maniphest task exists.' } })

       afterEach ->
         nock.cleanAll()
@@ -261,7 +266,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { priority: 'Unbreak Now!' }})
+            .reply(200, { result: { priority: 'Unbreak Now!' } })

         afterEach ->
           nock.cleanAll()
@@ -285,7 +290,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { priority: 'Needs Triage' }})
+            .reply(200, { result: { priority: 'Needs Triage' } })

         afterEach ->
           nock.cleanAll()
@@ -304,7 +309,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { priority: 'High' }})
+            .reply(200, { result: { priority: 'High' } })

         afterEach ->
           nock.cleanAll()
@@ -323,7 +328,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { priority: 'Normal' }})
+            .reply(200, { result: { priority: 'Normal' } })

         afterEach ->
           nock.cleanAll()
@@ -338,7 +343,7 @@
           do nock.disableNetConnect
           nock(process.env.PHABRICATOR_URL)
             .get('/api/maniphest.update')
-            .reply( 200, { result: { priority: 'Low' }})
+            .reply(200, { result: { priority: 'Low' } })

         afterEach ->
           nock.cleanAll()
@@ -354,7 +359,7 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/maniphest.info')
-          .reply( 200, { result: { error_info: 'No such Maniphest task exists.' }})
+          .reply(200, { result: { error_info: 'No such Maniphest task exists.' } })

       afterEach ->
         nock.cleanAll()
@@ -369,13 +374,13 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/maniphest.info')
-          .reply( 200, { result: {
-            status: 'open',
-            isClosed: false,
-            title: 'some task',
-            priority: 'Low',
+          .reply(200, { result: {
+            status: 'open',
+            isClosed: false,
+            title: 'some task',
+            priority: 'Low',
             uri: 'http://example.com/T42'
-          }})
+          } })

       afterEach ->
         nock.cleanAll()
@@ -394,13 +399,13 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/maniphest.info')
-          .reply( 200, { result: {
-            status: 'resolved',
-            isClosed: true,
-            title: 'some task',
-            priority: 'Low',
+          .reply(200, { result: {
+            status: 'resolved',
+            isClosed: true,
+            title: 'some task',
+            priority: 'Low',
             uri: 'http://example.com/T42'
-          }})
+          } })

       afterEach ->
         nock.cleanAll()
@@ -421,7 +426,7 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/file.info')
-          .reply( 200, { result: { error_info: 'No such file exists.' }})
+          .reply(200, { result: { error_info: 'No such file exists.' } })

       afterEach ->
         nock.cleanAll()
@@ -436,23 +441,23 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/file.info')
-          .reply( 200, { result: {
-            name: "image.png",
-            mimeType: "image/png",
-            byteSize: "1409",
-            uri: "https://example.com/F42"
-          }})
+          .reply(200, { result: {
+            name: 'image.png',
+            mimeType: 'image/png',
+            byteSize: '1409',
+            uri: 'https://example.com/F42'
+          } })

       afterEach ->
         nock.cleanAll()

       context 'whatever about F42 or something', ->
         hubot 'whatever about F42 or something'
-        it "gives information about the File, including uri", ->
+        it 'gives information about the File, including uri', ->
           expect(hubotResponse()).to.eql 'https://example.com/F42 - image.png (image/png 1.38 kB)'
       context 'whatever about http://example.com/F42 or something', ->
         hubot 'whatever about http://example.com/F42 or something'
-        it "gives information about the File, without uri", ->
+        it 'gives information about the File, without uri', ->
           expect(hubotResponse()).to.eql 'F42 - image.png (image/png 1.38 kB)'

@@ -462,7 +467,7 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/paste.query')
-          .reply( 200, { result: { }})
+          .reply(200, { result: { } })

       afterEach ->
         nock.cleanAll()
@@ -477,22 +482,22 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/paste.query')
-          .reply( 200, { result: [{
-            title: "file.coffee",
-            language: "",
-            uri: "https://example.com/P42"
-          }]})
+          .reply(200, { result: [ {
+            title: 'file.coffee',
+            language: '',
+            uri: 'https://example.com/P42'
+          } ] })

       afterEach ->
         nock.cleanAll()

       context 'whatever about P42 or something', ->
         hubot 'whatever about P42 or something'
-        it "gives information about the Paste, including uri", ->
+        it 'gives information about the Paste, including uri', ->
           expect(hubotResponse()).to.eql 'https://example.com/P42 - file.coffee'
       context 'whatever about http://example.com/P42 or something', ->
         hubot 'whatever about http://example.com/P42 or something'
-        it "gives information about the Paste, without uri", ->
+        it 'gives information about the Paste, without uri', ->
           expect(hubotResponse()).to.eql 'P42 - file.coffee'

@@ -501,20 +506,20 @@
         do nock.disableNetConnect
         nock(process.env.PHABRICATOR_URL)
           .get('/api/paste.query')
-          .reply( 200, { result: [{
-            title: "file.coffee",
-            language: "coffee",
-            uri: "https://example.com/P42"
-          }]})
+          .reply(200, { result: [ {
+            title: 'file.coffee',
+            language: 'coffee',
+            uri: 'https://example.com/P42'
+          } ] })

       afterEach ->
         nock.cleanAll()

       context 'whatever about P42 or something', ->
         hubot 'whatever about P42 or something'
-        it "gives information about the Paste, including uri", ->
+        it 'gives information about the Paste, including uri', ->
           expect(hubotResponse()).to.eql 'https://example.com/P42 - file.coffee (coffee)'
       context 'whatever about http://example.com/P42 or something', ->
         hubot 'whatever about http://example.com/P42 or something'
-        it "gives information about the Paste, without uri", ->
+        it 'gives information about the Paste, without uri', ->
           expect(hubotResponse()).to.eql 'P42 - file.coffee (coffee)'


Fix: load page at hash scrolls too far

Fix: load page at hash scrolls too far

On page load, scrolling to hash runs before everything has loaded.
Experimentally, I found that about 1/8 second delay works.

--- src/scripts/modules/media/media.coffee
+++ src/scripts/modules/media/media.coffee
@@ -104,6 +104,12 @@
           obscured = $pinnable.height()
           top = $(window.location.hash).position().top
           $(window).scrollTop(top - obscured)
+          # On page load, might need to re-adjust
+          setTimeout(->
+            top2 = $(window.location.hash).position().top
+            if top2 != top
+              $(window).scrollTop(top2 - obscured)
+          , 125)
       Backbone.on('window:hashChange', adjustHashTop)

       # closing is triggered in 'onBeforeClose'


Ensure the editor closes when the view using it is closed

--- src/scripts/modules/media/media.coffee
+++ src/scripts/modules/media/media.coffee
@@ -94,3 +94,5 @@
       @regions.editbar.empty()
       $('body').css('padding-top', '0') # Remove added padding
       window.scrollBy(0, -height) # Prevent viewport from jumping
+
+    onBeforeClose: () -> @model.set('editable', false) if @model.get('editable')


removing comments

removing comments

--- src/view/bindings/class_binding.coffee
+++ src/view/bindings/class_binding.coffee
@@ -3,28 +3,18 @@
 class Batman.DOM.ClassBinding extends Batman.DOM.AbstractCollectionBinding
   onlyObserve: Batman.BindingDefinitionOnlyObserve.Data

-  # set the 'master list' of classes that exist on the node
   constructor: () ->
-    # using classList may be a better option in the future when it is more widely
-    # suppoerted, however for now we'll have to use className
-
     @existingClasses = arguments[0].node.className.split(' ')
     super

   dataChange: (value) ->
-
     if value?
       @unbindCollection()
       if typeof value is 'string'

-        # create a new list of classes that merges existingClasses, then push
-        # the newly added class, join the list and re-apply the classes
         newClasses = [].concat(@existingClasses)
         newClasses.push value

-        # NOTE: i considered adding something that would go through and .trim()
-        # each string in the array, but decided that should be up to the user to
-        # format their strings correctly
         @node.className = newClasses.join(' ').trim()
       else
         @bindCollection(value)
@@ -39,16 +29,10 @@

       array = array.toArray() if array.toArray?

-      # create a new list of classes that merges existingClasses, then filter
-      # the new classes to be applied, removing any that already exist, concat
-      # the arrays, join them, and re-apply the classes
       existingClasses = [].concat @existingClasses
       newClasses = array.filter (val) ->
         existingClasses.indexOf(val) == -1

-      # NOTE: i considered adding something that would go through and .trim()
-      # each string in the array, but decided that should be up to the user to
-      # format their strings correctly
       @node.className = existingClasses.concat(newClasses).join(' ').trim()

-  handleArrayChanged: => @updateFromCollection()
+  handleArrayChanged: => @updateFromCollection()


:lipstick:

:lipstick:

--- src/packages/tabs/spec/tabs-spec.coffee
+++ src/packages/tabs/spec/tabs-spec.coffee
@@ -5,7 +5,7 @@
 fs = require 'fs'

 describe "Tabs", ->
-  [rootView, editor, statusBar, buffer, tabs] = []
+  [rootView, editor, buffer, tabs] = []

   beforeEach ->
     rootView = new RootView(require.resolve('fixtures/sample.js'))


add support for meteor-roles groups

add support for meteor-roles groups

--- server/boot.coffee
+++ server/boot.coffee
@@ -13,6 +13,7 @@
   Voltage =
     settings:
       adminRole: null
+      adminGroup: null

     config: (appConfig) ->
       @settings = _.extend(@settings, appConfig)
@@ -28,7 +29,7 @@
       if not Meteor.user()
         return false

-      if Voltage.settings.adminRole and not Roles.userIsInRole(Meteor.user(), Voltage.settings.adminRole)
+      if Voltage.settings.adminRole and not Roles.userIsInRole(Meteor.user(), Voltage.settings.adminRole, Voltage.settings.adminGroup)
         return false

       true


Fixed path to GameModel module

Fixed path to GameModel module

--- src/repositories/gamesrepository.coffee
+++ src/repositories/gamesrepository.coffee
@@ -1,6 +1,6 @@
 azure = require "azure"

-GameModel = require "../models/GameModel"
+GameModel = require "../models/gamemodel"

 class GamesRepository
 	# constructor
@@ -52,4 +52,4 @@
 				console.log "Error occurred with code #{ error.code } and statusCode #{ error.statusCode }"
 				res.send 500

-module.exports = GamesRepository+module.exports = GamesRepository


Only create a productType if it doesn't exist

--- src/spec/integration/testhelpers.coffee
+++ src/spec/integration/testhelpers.coffee
@@ -1,12 +1,16 @@
 _ = require 'underscore'
 _.mixin require('underscore-mixins')
 Promise = require 'bluebird'
+
+exports.uniqueId = (prefix) ->
+  _.uniqueId "#{prefix}#{new Date().getTime()}_"

 ###
  * You may omit the product in this case it resolves the created product type.
  * Otherwise the created product is resolved.
 ###
 exports.setupProductType = (client, productType, product) ->
+  console.log 'About to cleanup products...'
   client.productProjections
   .sort('id')
   .where('published = "true"')
@@ -24,15 +28,22 @@
     client.products.perPage(30).process (payload) ->
       Promise.map payload.body.results, (existingProduct) ->
         client.products.byId(existingProduct.id).delete(existingProduct.version)
-  .then -> client.productTypes.all().fetch()
   .then (result) ->
-    Promise.map result.body.results, (productType) ->
-      client.productTypes.byId(productType.id).delete(productType.version)
-  .then -> client.productTypes.create(productType)
+    console.log "Deleted #{_.size result} products, about to ensure productType"
+    # ensure the productType exists, otherwise create it
+    client.productTypes.where("name = \"#{productType.name}\"").perPage(1).fetch()
   .then (result) ->
+    if _.size(result.body.results) > 0
+      console.log "ProductType #{productType.name} already exists"
+      Promise.resolve(_.first(result.body.results))
+    else
+      console.log "Ensuring productType #{productType.name}"
+      client.productTypes.create(productType)
+      .then (result) -> Promise.resolve(result.body)
+  .then (pt) ->
     if product?
-      product.productType.id = result.body.id
-      client.products.create(product).then (result) ->
-        Promise.resolve result.body
+      product.productType.id = pt.id
+      client.products.create(product)
+      .then (result) -> Promise.resolve result.body # returns product
     else
-      Promise.resolve result.body
+      Promise.resolve pt # returns productType


Correcting and cleaning up --join ... now it works in conjunction with --watch

--- command.coffee
+++ command.coffee
@@ -49,7 +49,7 @@
 # Top-level objects shared by all the functions.
 opts         = {}
 sources      = []
-contents     = []
+sourceCode   = []
 optionParser = null

 # Run `coffee` by parsing passed options and determining what action to take.
@@ -78,27 +78,17 @@
 # compile them. If a directory is passed, recursively compile all
 # '.coffee' extension source files in it and all subdirectories.
 compileScripts = ->
-  unprocessed = []
-  remainingFiles = ->
-    total = 0
-    total += x for x in unprocessed
-    total
-  trackUnprocessedFiles = (sourceIndex, fileCount) ->
-    unprocessed[sourceIndex] ?= 0
-    unprocessed[sourceIndex] += fileCount
-  trackCompleteFiles = (sourceIndex, fileCount) ->
-    unprocessed[sourceIndex] -= fileCount
-    if opts.join
-      if helpers.compact(contents).length > 0 and remainingFiles() is 0
-        compileJoin()
-  for source in sources
-    trackUnprocessedFiles sources.indexOf(source), 1
-  for source in sources
-    base = path.join(source)
-    compile = (source, sourceIndex, topLevel) ->
+
+  for source, index in sources
+    sourceCode[index] = null
+
+    base = path.join source
+
+    compile = (source, topLevel) ->
       path.exists source, (exists) ->
         if topLevel and not exists and source[-7..] isnt '.coffee'
-          return compile "#{source}.coffee", sourceIndex, topLevel
+          source = sources[sources.indexOf(source)] = "#{source}.coffee"
+          return compile source, topLevel
         if topLevel and not exists
           console.error "File not found: #{source}"
           process.exit 1
@@ -107,22 +97,22 @@
           if stats.isDirectory()
             fs.readdir source, (err, files) ->
               throw err if err
-              trackUnprocessedFiles sourceIndex, files.length
-              for file in files
-                compile path.join(source, file), sourceIndex
-              trackCompleteFiles sourceIndex, 1
+              files = files.map (file) -> path.join source, file
+              index = sources.indexOf source
+              sources[index..index] = files
+              sourceCode[index..index] = files.map -> null
+              compile file for file in files
           else if topLevel or path.extname(source) is '.coffee'
             fs.readFile source, (err, code) ->
               throw err if err
-              if opts.join
-                contents[sourceIndex] = helpers.compact([contents[sourceIndex], code.toString()]).join('\n')
-              else
-                compileScript(source, code.toString(), base)
-              trackCompleteFiles sourceIndex, 1
-            watch source, base if opts.watch and not opts.join
+              compileScript(source, code.toString(), base)
+            watch source, base if opts.watch
           else
-            trackCompleteFiles sourceIndex, 1
-    compile source, sources.indexOf(source), true
+            index = sources.indexOf source
+            sources.splice index, 1
+            sourceCode.splice index, 1
+
+    compile source, true

 # Compile a single source script, containing the given code, according to the
 # requested options. If evaluating the script directly sets `__filename`,
@@ -136,6 +126,8 @@
     if      o.tokens      then printTokens CoffeeScript.tokens t.input
     else if o.nodes       then printLine CoffeeScript.nodes(t.input).toString().trim()
     else if o.run         then CoffeeScript.run t.input, t.options
+    else if o.join and file isnt o.join
+      compileJoin t.file, t.input
     else
       t.output = CoffeeScript.compile t.input, t.options
       CoffeeScript.emit 'success', task
@@ -159,11 +151,12 @@
   stdin.on 'end', ->
     compileScript null, code

-# After all of the source files are done being read, concatenate and compile
+# If all of the source files are done being read, concatenate and compile
 # them together.
-compileJoin = ->
-  code = contents.join '\n'
-  compileScript opts.join, code, opts.join
+compileJoin = (file, code) ->
+  sourceCode[sources.indexOf(file)] = code
+  unless sourceCode.some((code) -> code is null)
+    compileScript opts.join, sourceCode.join('\n'), opts.join

 # Load files that are to-be-required before compilation occurs.
 loadRequires = ->


debug...

debug...

--- config/initializers/requests.coffee
+++ config/initializers/requests.coffee
@@ -37,11 +37,6 @@
     Task.defineRequest "todosTag", todosTag, requests.checkError
     tags =
         map: ->
-            if not doc.done
-    Task.defineRequest "todosTag", todosTag, requests.checkError
-
-    tags =
-        map: ->
             unless doc.done
                 for tag in doc.tags
                     emit tag, tag


Fixed winston typo

Fixed winston typo

--- source/Downloader.coffee
+++ source/Downloader.coffee
@@ -60,7 +60,7 @@
 scrape = (source) ->
   winston.info('Start scrape for source: ' + source)
   request source, (error, response, html) ->
-    winston('Received answer from server.')
+    winston.info('Received answer from server.')
     $ = cheerio.load(html)
     winston.info('Parsed page with $.')
     links = $('.item').find('.play')


:fire: Unused import

:fire: Unused import

--- src/main-process/atom-application.coffee
+++ src/main-process/atom-application.coffee
@@ -11,7 +11,6 @@
 os = require 'os'
 net = require 'net'
 url = require 'url'
-querystring = require 'querystring'
 {EventEmitter} = require 'events'
 _ = require 'underscore-plus'
 FindParentDir = null


Add a method to get the top of the prelude stack more safely

--- src/generator/compile.glsl.compiler.coffee
+++ src/generator/compile.glsl.compiler.coffee
@@ -32,9 +32,16 @@
 glslCompiler.preludePop = (prelude) ->
   return prelude.pop()[0]

+# Get the top variable name in the prelude (with safety checks)
+glslCompiler.preludeTop = (prelude) ->
+  if not Array.isArray prelude or prelude.length == 0
+    throw "Could not retrieve top value from prelude."
+  return prelude[prelude.length - 1][0]
+
 # Add a value to the prelude without pushing it onto the stack
 glslCompiler.preludeAdd = (prelude, value, valueType) ->
   name = 'r' + prelude.counter
   prelude.counter += 1
   prelude.code += "  #{if valueType? then valueType else 'vec3'} #{name} = #{value};\n"
   return name
+


Don't decrease indent for "case" and "default"

It is causing unwanted indent decrease for "inline cases" (i.e. `case cond: expr;` in the same line)


add function to clear results before new test

--- test/browserify-tests.coffee
+++ test/browserify-tests.coffee
@@ -1,3 +1,4 @@
+clearPreviousResults = -> delete Result[key] for key of Result

 # test the usual successful run
 Tinytest.add 'test processFile', (test) ->
@@ -48,6 +49,8 @@
 # test an error in browserify
 Tinytest.add 'test with browserify error', (test) ->

+  clearPreviousResults()
+
   # cause it to use the on error callback
   Result.errorWanted = true


fix tests of pr #501

fix tests of pr #501

--- test/event_model_test.coffee
+++ test/event_model_test.coffee
@@ -10,6 +10,7 @@

     describe "load", ->

+        before helpers.before
         before helpers.cleanDb
         before (done) ->
             events = [
@@ -38,10 +39,12 @@
                 Event.create event, next
             , done

+        after helpers.after
+
         it "returns events for dates: 21030401 - 20130501", (done) ->
             start = moment('201304', 'YYYYMM')
             end = start.clone().add 'months', 1
-            Event.load start, end, (err, events) =>
+            Event.load start, end, (err, events) ->
                 events.length.should.equal 2
                 events[0].description.should.equal 'Title 1'
                 events[1].description.should.equal 'Title 3'
@@ -50,7 +53,7 @@
         it "returns events for dates: 21030401 - 20160401", (done) ->
             start = moment('201304', 'YYYYMM')
             end = start.clone().add 'years', 3
-            Event.load start, end, (err, events) =>
+            Event.load start, end, (err, events) ->
                 events.length.should.equal 3
                 events[0].description.should.equal 'Title 1'
                 events[1].description.should.equal 'Title 3'
@@ -60,7 +63,6 @@
         it "returns no events for dates: 21010401 - 20120401", (done) ->
             start = moment('201304', 'YYYYMM').subtract 'years', 2
             end = start.clone().add 'years', 1
-            Event.load start, end, (err, events) =>
+            Event.load start, end, (err, events) ->
                 events.length.should.equal 0
                 done()
-


run perf option

run perf option

--- run.coffee
+++ run.coffee
@@ -4,18 +4,19 @@
 ## TODO figure out better name & place & API for such script
 require 'fy'
 {yellow, green, cyan, magenta} = require "colors"
-{tokenize} = require './tokenizer'
-{parse   } = require './grammar'
-{type_inference} = require './type_inference'
-{translate} = require './translator'
+{tokenize, _tokenize} = require './tokenizer'
+{parse   , _parse   } = require './grammar'
+{type_inference, _type_inference} = require './type_inference'
+{translate, _translate} = require './translator'

 argv = require('minimist')(process.argv.slice(2))
 input = argv._[0].trim()
 if !input
   console.error """
-    -i    print input
-    -t    print tokens
-    -a    print ast
+    -i     print input
+    -t     print tokens
+    -a     print ast
+    --perf launch test specified times and hang

     Usage:    iced run.coffee input [-ita]
        or        ./run.coffee input [-ita]
@@ -25,28 +26,59 @@
 # some escape remove
 input = input.replace /\\n/g, "\n"

-if argv.i
-  p magenta "Input:", cyan.bold input
-await tokenize input, {}, defer err, tok_res
-### !pragma coverage-skip-block ###
-throw err if err
-if argv.t
-  p "Token list:"
-  pp tok_res
-await parse     tok_res, {}, defer err, ast
-throw err if err
-### !pragma coverage-skip-block ###
-await type_inference ast[0],  {}, defer err, res
-throw err if err
-### !pragma coverage-skip-block ###
+perform_test = (on_end=->)->
+  ### !pragma coverage-skip-block ###
+  if argv.i
+    p magenta "Input:", cyan.bold input
+  await tokenize input, {}, defer err, tok_res
+  ### !pragma coverage-skip-block ###
+  throw err if err
+  if argv.t
+    p "Token list:"
+    pp tok_res
+  await parse     tok_res, {}, defer err, ast
+  throw err if err
+  ### !pragma coverage-skip-block ###
+  await type_inference ast[0],  {}, defer err, res
+  throw err if err
+  ### !pragma coverage-skip-block ###

-if argv.a
-  p yellow.bold "AST[0]:"
-  p ast[0]
-  p yellow.bold "AST[0].value_array[0]:"
-  p ast[0].value_array[0]
+  if argv.a
+    p yellow.bold "AST[0]:"
+    p ast[0]
+    p yellow.bold "AST[0].value_array[0]:"
+    p ast[0].value_array[0]

-await translate ast[0],  {}, defer err, res
-### !pragma coverage-skip-block ###
-throw err if err
-p yellow "Output:", green.bold res+  await translate ast[0],  {}, defer err, res
+  ### !pragma coverage-skip-block ###
+  throw err if err
+  p yellow "Output:", green.bold res
+  on_end()
+
+perform_test_sync = ()->
+  ### !pragma coverage-skip-block ###
+  if argv.i
+    p magenta "Input:", cyan.bold input
+  tok_res = _tokenize input, {}
+  if argv.t
+    p "Token list:"
+    pp tok_res
+  ast = _parse     tok_res, {}
+  res = _type_inference ast[0],  {}
+  if argv.a
+    p yellow.bold "AST[0]:"
+    p ast[0]
+    p yellow.bold "AST[0].value_array[0]:"
+    p ast[0].value_array[0]
+
+  res = _translate ast[0],  {}
+  p yellow "Output:", green.bold res
+
+if argv.perf
+  global.perf_counter = 0
+  for i in [0 ... argv.perf]
+    perform_test_sync()
+  console.log "global.perf_counter=#{global.perf_counter}"
+  setTimeout (()->), 1e10
+else
+  perform_test() # async call is not necessary


Added map to dummy observable

Added map to dummy observable

--- source/observable.coffee
+++ source/observable.coffee
@@ -21,6 +21,7 @@
   Object.extend self,
     observe: (listener) ->
       listeners.push listener
+
     each: (args...) ->
       # Don't add null or undefined values to iteration
       if value?
@@ -77,9 +78,13 @@
     # No-op
     dummy.observe = ->

-    dummy.each = (args...) ->
-      if value?
-        [value].forEach(args...)
+    Object.extend dummy,
+      each: (args...) ->
+        if value?
+          [].concat(value).forEach(args...)
+      map: (args...) ->
+        if value?
+          [].concat(value).map(args...)

     return dummy


match current link checking logic

match current link checking logic

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -177,17 +177,16 @@

           # Convert links to maintain context in a book, if appropriate
           if @model.isBook()
-            $temp.find('a[href~="/contents/"]:not([data-type=footnote-number])').each (i, el) =>
+            $temp.find('a:not([data-type=footnote-number]):not(href^="#")').each (i, el) =>
               $el = $(el)
               href = $el.attr('href')

-              if href.substr(0, 1) isnt '#'
-                page = @model.getPage(href.substr(10))
-
-                if page
-                  pageNumber = page.getPageNumber()
-                  $el.attr('href', "/contents/#{@model.getVersionedId()}:#{pageNumber}")
-                  $el.attr('data-page', pageNumber)
+              page = @model.getPage(href.substr(10))
+
+              if page
+                pageNumber = page.getPageNumber()
+                $el.attr('href', "/contents/#{@model.getVersionedId()}:#{pageNumber}")
+                $el.attr('data-page', pageNumber)

           # Add nofollow to external user-generated links
           $temp.find('a[href^="http:"], a[href^="https:"], a[href^="//"]').attr('rel', 'nofollow')


Let developer optionally run a local dronehub server

If the option developer param of ?localServer=true is used it will
look for the server at localhost:8080

--- src/scripts/services/missionService.coffee
+++ src/scripts/services/missionService.coffee
@@ -1,26 +1,32 @@
 class Service
-  urlBase = 'http://localhost:8080/api/v1/mission'
-
-  constructor: (@$log, @$http) ->
+  constructor: (@$log, @$http, @$routeParams) ->
+    useLocalServer = $routeParams.localServer ? false
+    base = if useLocalServer
+      'http://localhost:8080'
+    else
+      'http://nestor.3dr.com'
+    path = '/api/v1/mission'
+    @urlBase = base + path
+    console.log("urlBase: " + @urlBase)

   get: ->
-    @$http.get(urlBase)
+    @$http.get(@urlBase)
     .then (results) ->
       results.data

   getMission: (id) ->
-    @$http.get("#{urlBase}/#{id}")
+    @$http.get("#{@urlBase}/#{id}")
     .then (results) ->
       results.data

   getGeoJSON: (id) ->
-    @$http.get("#{urlBase}/#{id}/messages.geo.json")
+    @$http.get("#{@urlBase}/#{id}/messages.geo.json")
     .then (results) ->
       results.data

   save: (mission) ->
-    @$http.post("#{urlBase}", mission)
+    @$http.post("#{@urlBase}", mission)
     .error (results, status) ->
       {results, status}

-angular.module('app').service 'missionService', ['$log', '$http', Service]
+angular.module('app').service 'missionService', ['$log', '$http', '$routeParams', Service]


Fix tests, retag

Fix tests, retag

--- test/lib/Connection-spec.coffee
+++ test/lib/Connection-spec.coffee
@@ -39,21 +39,23 @@

       describe 'when subscribe and the socket reconnects', ->
         beforeEach ->
+          @sut.bufferedSocketEmit = sinon.spy()
           @sut.subscribe uuid: 'this'
           sinon.spy @sut.socket, 'emit'
           @socket.emit 'ready', {uuid: 'cats', token: 'dogs'}

         it 'should re subscribe to the "this"', ->
-          expect(@socket.emit).to.have.been.calledWith 'subscribe', uuid: 'this'
+          expect(@sut.bufferedSocketEmit).to.have.been.calledWith 'subscribe', uuid: 'this'

       describe 'when subscribed to foo and the socket reconnects', ->
         beforeEach ->
+          @sut.bufferedSocketEmit = sinon.spy()
           @sut.subscribe uuid: 'foo'
           sinon.spy @sut.socket, 'emit'
           @socket.emit 'ready', {uuid: 'cats', token: 'dogs'}

         it 'should re subscribe to the "this"', ->
-          expect(@socket.emit).to.have.been.calledWith 'subscribe', uuid: 'foo'
+          expect(@sut.bufferedSocketEmit).to.have.been.calledWith 'subscribe', uuid: 'foo'

       describe 'when subscribed to foo, unsubscribed, and the socket reconnects', ->
         beforeEach ->


fix: broken mocha test due to missing model

Former-commit-id: 6c7ad08e2bf44129e8cdfcf3fb57d078d951b930 [formerly 1c130b60c2c855b82b1e28d4715f942242ff8f36]
Former-commit-id: 8178da10d766118ebcb59a5fc635fddf125fdeeb

--- test/mocha/canvas_drawer.coffee
+++ test/mocha/canvas_drawer.coffee
@@ -26,7 +26,7 @@
  a.g = {}
  a.ctx = sinon.stub()
  columnWidth = rowHeight = 10
- a.g.zoomer = new Zoomer {rowHeight: rowHeight, columnWidth: columnWidth}, g: {}
+ a.g.zoomer = new Zoomer {rowHeight: rowHeight, columnWidth: columnWidth}, g:{}, model: a.model
  a.d = new CanvasDrawSeq a.g, a.ctx, a.model,
    width: 200
    height: 200


touches: Add random circles, with interaction

--- touches/touches.coffee
+++ touches/touches.coffee
@@ -2,6 +2,8 @@

 width = 920
 height = 450
+ncircles = 20
+colors = ["orchid", "#4DF0A7"]
 svg = d3.select("div#chart")
         .append("svg")
         .attr({width: width, height: height})
@@ -10,6 +12,31 @@
    .attr({x:0, y:0, width:width, height:height})
    .attr({"stroke-width": 5, stroke:"black", fill:d3.rgb(200, 200, 200)})
    .style("pointer-events", "none")
+
+random_circles = (number, width, height) ->
+  dat = []
+  for i in [0...number]
+    x = Math.random()*width
+    y = Math.random()*height
+    dat.push([x,y])
+  dat
+
+dat = random_circles(ncircles, width, height)
+circolor = []
+for i in [0...dat.length]
+  circolor[i] = colors[0]
+
+circles = svg.selectAll("empty")
+   .data(dat)
+   .enter()
+     .append("circle")
+     .attr("cx", (d) -> d[0])
+     .attr("cy", (d) -> d[1])
+     .attr({r: 8})
+     .attr({stroke:"black", "stroke-width": 2, fill: colors[0]})
+   .on "click", (d,i) ->
+      circolor[i] = if circolor[i] == colors[0] then colors[1] else colors[0]
+      d3.select(this).transition().ease("linear").duration(500).attr("fill", circolor[i])

 ripples = (position) ->
    svg.selectAll("empty")
@@ -23,7 +50,7 @@
         .ease("linear")
         .duration(500)
         .delay((d) -> d*50)
-        .attr("r", 50)
+        .attr("r", 100)
         .style("opacity", 0)
         .remove()


Tweak terrain height with event volume. Close #4

--- layers/midground/terrain/main.coffee
+++ layers/midground/terrain/main.coffee
@@ -22,7 +22,8 @@

     @undulation    = THREE.Math.randFloat 0.5, 1.5
     @smoothness    = THREE.Math.randFloat 125, 300
-    @maxHeight     = THREE.Math.randFloat 75, 160
+    @realMaxHeight = THREE.Math.randFloat 75, 160
+    @maxHeight     = @realMaxHeight * 0.5
     @baseColor     = new THREE.Color().setHSV(
       Math.random()
       THREE.Math.randFloat 0.5, 1
@@ -60,7 +61,12 @@

     @travel.y -= @undulation * elapsed

-  onBeat: ->
+  onBeat: (beat) ->
+    new TWEEN.Tween(this)
+      .to({maxHeight: beat.volume * @realMaxHeight}, beat.duration * 1000)
+      .easing(TWEEN.Easing.Cubic.InOut)
+      .start()
+
     @beatValue = 1

   kill: ->


Add AWS.Kinesis browser integ tests

Add AWS.Kinesis browser integ tests

--- test/browser.spec.coffee
+++ test/browser.spec.coffee
@@ -194,6 +194,20 @@
         matchError(err, 'The specified job was not found')
         done()

+  describe 'AWS.Kinesis', ->
+    it 'makes a request', (done) ->
+      kinesis.listStreams (err, data) ->
+        noError(err)
+        expect(Array.isArray(data.StreamNames)).to.equal(true)
+        done()
+
+    it 'handles errors', (done) ->
+      kinesis.describeStream {StreamName: 'fake-stream'}, (err, data) ->
+        noData(data)
+        assertError(err, 'ResourceNotFoundException')
+        matchError(err, 'Stream fake-stream under account')
+        done()
+
   describe 'AWS.S3', ->
     testWrite = (done, body, compareFn, svc) ->
       svc = svc || s3


update tests

update tests

--- test/ingress_test.coffee
+++ test/ingress_test.coffee
@@ -46,7 +46,7 @@

     describe 'badges commands', ->
       it 'registers "have badge" listener', ->
-        expect(@robot.respond).to.have.been.calledWith(/(I|@?\w+) (?:have|has|got|earned)(?: the)? :?(\w+):? badge/i)
+        expect(@robot.respond).to.have.been.calledWith(/(I|@?\w+) (?:have|has|got|earned)(?: the)? :?([\w,\s]+):? badges?/i)

       it 'registers "what badges" listener', ->
         expect(@robot.respond).to.have.been.calledWith(/wh(?:at|ich) badges? do(?:es)? (I|@?\w+) have/i)
@@ -77,6 +77,16 @@
         expect(badges).not.to.include(':hacker1:')
         expect(badges).to.include(':hacker2:')

+      it '"I have" can handle multiple badge names', ->
+        @msg.match = [0, 'I', 'pioneer3, hacker4, builder1']
+        @robot.respond.args[2][1](@msg)
+        badges = @data.ingressBadges.U123
+        expect(@msg.reply).to.have.been.calledWith(sinon.match(/congrats on earning the .* badges!/))
+        expect(badges).to.be.a('array')
+        expect(badges).to.include(':pioneer3:')
+        expect(badges).to.include(':hacker4:')
+        expect(badges).to.include(':builder1:')
+
       it 'responds to "sinon2 has the verified badge"', ->
         @msg.match = [0, 'sinon2', 'verified']
         @robot.respond.args[2][1](@msg)


page entries by every 30

page entries by every 30

--- controllers/teams.coffee
+++ controllers/teams.coffee
@@ -40,7 +40,7 @@
   query = { 'entry.votable': true, lastDeploy: {$ne: null} }
   query.search = new RegExp(req.param('q'), 'i') if req.param('q')
   query.peopleIds = ($size: 1) if req.param('sort') == 'solo'
-  options = { sort: [["scores.#{sort}", -1]], limit: 50, skip: 50 * page }
+  options = { sort: [["scores.#{sort}", -1]], limit: 30, skip: 30 * page }
   Team.find query, {}, options, (err, teams) ->
     return next err if err
     Team.count query, (err, count) ->


#123 converter bugfix

#123 converter bugfix

--- converter/convert.coffee
+++ converter/convert.coffee
@@ -69,10 +69,9 @@

 convertTextures = (material, textures) ->
 	material.textures = []
-	for texture in textures
+	for texture in textures when texture.TextureSource?
 		newtex =
 			url: 'resources/'+texture.TextureSource
-
 		match = texture.TextureSource.match(/^[^\.]*/)
 		outputFile "textures/#{match[0]}.tex", newtex
 		material.textures.push "textures/#{match[0]}.tex"


incomplete

incomplete

--- hyperbolic_tessellation_test_app.coffee
+++ hyperbolic_tessellation_test_app.coffee
@@ -119,6 +119,7 @@
     for cell, i in @cells
       unless cells.get cell
         runCommands context, @cellShapes[i]
+        null
     context.stroke()

     #then cells
@@ -126,6 +127,7 @@
     for cell, i in @cells
       if cells.get cell
         runCommands context, @cellShapes[i]
+        null
     context.fill()
     return true

@@ -328,7 +330,9 @@

 drawEverything = ->
   s = Math.min( canvas.width, canvas.height ) / 2 #
-  context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillStyle="white"
+  #context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillRect 0, 0, canvas.width, canvas.height
   context.save()
   context.scale s, s
   context.translate 1, 1
@@ -337,13 +341,17 @@
   return rval

 lastTime = Date.now()
-fpsMax = 60
-dtMax = 1000.0/fpsMax #
+fpsDefault = 30
+dtMax = 1000.0/fpsDefault #
+
 redrawLoop = ->
   if dirty
     t = Date.now()
     if t - lastTime > dtMax
       if drawEverything()
+        tEnd = Date.now()
+        #adaptively update FPS
+        dtMax = dtMax*0.9 + (tEnd - t)*1.2*0.1
         dirty = false
       lastTime = t
   requestAnimationFrame redrawLoop


clean up console logs

clean up console logs

--- app/assets/javascripts/components/image-subject-viewer-mark.cjsx
+++ app/assets/javascripts/components/image-subject-viewer-mark.cjsx
@@ -157,14 +157,12 @@

   disableMarkButton: (key) ->
     marks = @state.marks
-    console.log 'DISABLING MARK: ', marks[key]
     marks[key].buttonDisabled = true
     @setState marks: marks
     @forceUpdate()

   enableMarkButton: (key) ->
     marks = @state.marks
-    console.log 'DISABLING MARK: ', marks[key]
     marks[key].buttonDisabled = false
     @setState marks: marks
     @forceUpdate()


CI: Focus window to improve intermittent test failures

--- test/src/modules/rendering_tests.coffee
+++ test/src/modules/rendering_tests.coffee
@@ -1,4 +1,4 @@
-QUnit.module "Rendering"
+QUnit.module "Rendering", beforeEach: -> window.focus()

 renderingTest = (name, callback) ->
   sessionTest name, (assert, session, done) ->


cleanup/docs

cleanup/docs

--- app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
+++ app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
@@ -73,6 +73,8 @@
     # draw
     el = $(div).find(".graph")
     g = new Dygraph(el[0], vals, opts)
+
+    # TODO move variables into Graph class?
     g._bixby_dragging = false # used to denote that we are in the middle of a click-drag operation
     g._bixby_metric = metric
     g._bixby_el = el[0]
@@ -217,13 +219,12 @@

     g = @dygraph
     metric = g._bixby_metric
-    query = metric.get("query")
     new_met = new Bixby.model.Metric({
       id: metric.id
       host_id: metric.get("metadata").host_id
       start: parseInt(startX / 1000)
       end: parseInt(endX / 1000)
-      downsample: query.downsample || "5m-avg"
+      downsample: metric.get("query").downsample || "5m-avg"
     })
     Backbone.multi_fetch [ new_met ], (err, results) =>
       # don't replace data... add on to existing data and sort by timestamp


Avoid incompatible selector syntax

Avoid incompatible selector syntax

--- test/src/modules/rendering_tests.coffee
+++ test/src/modules/rendering_tests.coffee
@@ -109,7 +109,15 @@
       done()

 getAssetElements = (document = document) ->
-  [document.querySelectorAll("head > script, head > style, head > link[rel=stylesheet]")...]
+  selectChildren document.head, (el) -> match(el, "script, style, link[rel=stylesheet]")

 getProvisionalElements = (document = document) ->
-  [document.querySelectorAll("head > :not(script):not(style):not(link[rel=stylesheet])")...]
+  selectChildren document.head, (el) -> not match(el, "script, style, link[rel=stylesheet]")
+
+selectChildren = (element, test) ->
+  node for node in element.childNodes when node.nodeType is Node.ELEMENT_NODE and test(node)
+
+match = (element, selector) ->
+  html = document.documentElement
+  fn = html.matchesSelector ? html.webkitMatchesSelector ? html.msMatchesSelector ? html.mozMatchesSelector
+  fn.call(element, selector)


Rm test users after user test run.

Rm test users after user test run.

Make sure to clone the options. Request module alters this option.

--- test/routes/users.coffee
+++ test/routes/users.coffee
@@ -25,8 +25,16 @@
   before (done) ->
     startApp -> done()

+  after (done) ->
+    request (base '/users'), _.clone(options), (e,r,b)->
+      testUsers = _.filter b.users, (u)->
+        return u.firstName is 'Test' and u.lastName is 'User'
+      async.each testUsers, (u, cb)->
+        request.del (base "/users/#{u.id}"), _.clone(options), cb
+      , done
+
   it 'Get all Users', (done)->
-    request (base '/users'), options, (e,r,b)->
+    request (base '/users'), _.clone(options), (e,r,b)->
       r.statusCode.should.be.equal 200
       b.should.have.property 'users'
       should.equal UserTestHelper.users.length <= b.users.length, yes
@@ -40,9 +48,9 @@
       done()

   it 'cant get each user individually', (done)->
-    request (base '/users'), options, (e,r,b)->
+    request (base '/users'), _.clone(options), (e,r,b)->
       iterator = (id, cb)->
-        request (base "/users/#{id}"), options, (e,r,b)->
+        request (base "/users/#{id}"), _.clone(options), (e,r,b)->
           r.statusCode.should.be.equal 200
           UserTestHelper.validate b
           cb()
@@ -58,9 +66,9 @@
   it 'can delete a user', (done)->
     t = "testUser#{uuid.v1()}@testuser.com"
     makeUser t, 200, (user)->
-      request.del (base "/users/#{user.id}"), options, (e,r,b)->
+      request.del (base "/users/#{user.id}"), _.clone(options), (e,r,b)->
         r.statusCode.should.be.equal 200
-        request (base "/users/#{user.id}"), options, (e,r,b)->
+        request (base "/users/#{user.id}"), _.clone(options), (e,r,b)->
           r.statusCode.should.be.equal 404
           done()


Fix heisenbug in storage tests.

Fix heisenbug in storage tests.

--- test/storage_test.coffee
+++ test/storage_test.coffee
@@ -5,6 +5,7 @@
 Repo        = require '../lib/argumenta/repo'
 Objects     = require '../lib/argumenta/objects'
 fixtures    = require '../test/fixtures'
+_           = require 'underscore'
 should      = require 'should'

 {Argument, Proposition, Commit, Tag} = Objects
@@ -443,8 +444,8 @@
             storage.getTagsFor targetHashes, (err, tags) ->
               should.not.exist err
               tags.length.should.equal 2
-              should.ok tag1.equals tags[0]
-              should.ok tag2.equals tags[1]
+              should.ok _.some tags, (t) -> t.equals tag1
+              should.ok _.some tags, (t) -> t.equals tag2
               done()

     describe 'getTagsPlusSources( targetHashes, callback )', ->
@@ -456,10 +457,10 @@
               tags.length.should.equal 2
               sources.length.should.equal 0
               commits.length.should.equal 2
-              should.ok tag1.equals tags[0]
-              should.ok tag2.equals tags[1]
-              should.ok tag1Commit.equals commits[0]
-              should.ok tag2Commit.equals commits[1]
+              should.ok _.some tags, (t) -> t.equals tag1
+              should.ok _.some tags, (t) -> t.equals tag2
+              should.ok _.some commits, (c) -> c.equals tag1Commit
+              should.ok _.some commits, (c) -> c.equals tag2Commit
               done()

       it 'should retrieve support tags, sources, and commits', (done) ->


:art:

:art:

--- spec/fs-plus-spec.coffee
+++ spec/fs-plus-spec.coffee
@@ -75,14 +75,14 @@
     tempDir = null

     beforeEach ->
-      tempDir = temp.mkdirSync('remove-directory')
+      tempDir = temp.mkdirSync('fs-plus-')

     it "removes an existing file", ->
       filePath = path.join(tempDir, 'existing-file')
       fs.writeFileSync(filePath, '')

       done = false
-      fs.remove filePath, (err, result) ->
+      fs.remove filePath, ->
         done = true

       waitsFor ->
@@ -95,7 +95,7 @@
       filePath = path.join(tempDir, 'non-existent-file')

       done = false
-      fs.remove filePath, (err, result) ->
+      fs.remove filePath, ->
         done = true

       waitsFor ->
@@ -109,7 +109,7 @@
       fs.makeTreeSync(path.join(directoryPath, 'subdir'))

       done = false
-      fs.remove directoryPath, (err, result) ->
+      fs.remove directoryPath, ->
         done = true

       waitsFor ->
@@ -117,7 +117,6 @@

       runs ->
         expect(fs.existsSync(directoryPath)).toBe false
-

   describe ".makeTreeSync(path)", ->
     aPath = path.join(temp.dir, 'a')


:bug: f motion not cancelled when not enough matches

--- spec/motions-spec.coffee
+++ spec/motions-spec.coffee
@@ -1413,6 +1413,12 @@
       keydown('f')
       commandModeInputKeydown('a')
       expect(editor.getCursorScreenPosition()).toEqual [0, 0]
+      # bug was making this behaviour depend on the number
+      keydown('1')
+      keydown('1')
+      keydown('f')
+      commandModeInputKeydown('a')
+      expect(editor.getCursorScreenPosition()).toEqual [0, 0]

     it "composes with d", ->
       editor.setCursorScreenPosition([0,3])
@@ -1459,6 +1465,11 @@
     it "doesn't move if there aren't the specified count of the specified character", ->
       keydown('1')
       keydown('0')
+      keydown('t')
+      commandModeInputKeydown('a')
+      expect(editor.getCursorScreenPosition()).toEqual [0, 0]
+      keydown('1')
+      keydown('1')
       keydown('t')
       commandModeInputKeydown('a')
       expect(editor.getCursorScreenPosition()).toEqual [0, 0]


Fixed tests

Fixed tests

--- spec/tubular.spec.coffee
+++ spec/tubular.spec.coffee
@@ -16,7 +16,7 @@
     it 'binds path to a variable', ->
       tubular ->
         @_ = { TEST_PROP: 'TEST_VALUE' }
-        @bind 'TEST_VAR', ['_', 'TEST_PROP'], ->
+        @bind 'TEST_VAR', (=> @_.TEST_PROP), ->
           expect(@TEST_VAR).toBe 'TEST_VALUE'

     it 'creates new view when model is updated', ->
@@ -25,7 +25,7 @@

       tubular ->
         @_ = { TEST_PROP: 'TEST_VALUE', f: (-> @TEST_PROP = 'TEST_CHANGED_VALUE') }
-        @bind 'TEST_VAR', ['_', 'TEST_PROP'], ->
+        @bind 'TEST_VAR', (=> @_.TEST_PROP), ->
           asyncViewGets.push (=> @TEST_VAR)

         modelInvoker = =>
@@ -44,7 +44,7 @@

       tubular ->
         @_ = { TEST_PROP: 'TEST_VALUE', f: (-> @TEST_PROP = 'TEST_VALUE') }
-        @bind 'TEST_VAR', ['_', 'TEST_PROP'], ->
+        @bind 'TEST_VAR', (=> @_.TEST_PROP), ->
           runCount += 1

         modelInvoker = =>
@@ -62,13 +62,14 @@
       binding = null

       tubular ->
-        @_ = { n: 0, f: (-> @n += 1) }
+        @n = 0
+        @f = (-> @n += 1)

         modelInvoker = =>
-          @_.f()
+          @f()
           @refresh()

-        binding = @bind 'TEST_VAR', ['_', 'n'], ->
+        binding = @bind 'TEST_VAR', (=> @n), ->
           runCount += 1

       # run after view init
@@ -85,8 +86,7 @@
       binding = null

       tubular ->
-        @_ = { n: 0 }
-        binding = @bind 'TEST_VAR', ['_', 'n'], ->
+        binding = @bind 'TEST_VAR', (=> 0), ->

       # run after view init
       binding.clear()


More reliable memory test results

More reliable memory test results

Ignore results from first iterations to ensure that at least some code
is properly compiled before running benchmarks.

--- performance/MemoryTest.coffee
+++ performance/MemoryTest.coffee
@@ -35,8 +35,8 @@
 processResults = (results, i) ->
   values = (x[i] for x in results)

-  mean: mean(values)
-  stddev: stddev(values)
+  mean: mean(values[2..])
+  stddev: stddev(values[2..])

 printResult = (label, result, forcePrefix = false) ->
   prefix = if prefix && result.mean > 0 then '+' else ''


Fix prefix in memory perf test

Fix prefix in memory perf test

--- performance/MemoryTest.coffee
+++ performance/MemoryTest.coffee
@@ -39,7 +39,7 @@
   stddev: stddev(values)

 printResult = (label, result, prefix = '') ->
-  console.log("  #{rpad(label, 20)}", prefix, lpad(byteFormat(result.mean), 12 - prefix.length), '\u00b1', byteFormat(result.stddev, result.mean))
+  console.log("  #{rpad(label, 20)}", lpad(prefix + byteFormat(result.mean), 12), '\u00b1', byteFormat(result.stddev, result.mean))
 createNObservable = (count, generator) ->
   n = Math.floor(count / 10)
   m = 10
@@ -69,8 +69,8 @@
   withSubscriber = processResults(results, 1)
   afterCleanup = processResults(results, 2)

-  plusSubscriber = if withSubscriber > 0 then '+' else ''
-  plusCleanup = if afterCleanup > 0 then '+' else ''
+  plusSubscriber = if withSubscriber.mean > 0 then '+' else ''
+  plusCleanup = if afterCleanup.mean > 0 then '+' else ''
   printResult('w/o subscription', withoutSubscriber)
   printResult('with subscription', withSubscriber, plusSubscriber)
   printResult('unsubscribe', afterCleanup, plusCleanup)


fix position unit test

fix position unit test

--- tests/unit/scripts/range.coffee
+++ tests/unit/scripts/range.coffee
@@ -7,11 +7,23 @@
 describe('Range', ->
   describe('Position', ->
     it('should correctly initialize position from index', ->
-      $('#editor-container').html('
1234

<s>56</s>78

')
+      $('#editor-container').html(Tandem.Utils.cleanHtml('
+

+          12
+          34
+

+

+          <s>56</s>
+          78
+

+

+

+
'))
       editor = new Tandem.Editor('editor-container')
-      numPositions = editor.doc.root.textContent.length + editor.doc.root.childNodes.length - 1
+      numPositions = 10
       positions = _.map([0..numPositions], (i) ->
-        return new Tandem.Position(editor, i)
+        position = new Tandem.Position(editor, i)
+        return position
       )

       expect(positions[0].leafNode.textContent).to.equal('12')


Change variable naming.

Change variable naming.

--- js/src/crypto/CryptoServiceLoader.coffee
+++ js/src/crypto/CryptoServiceLoader.coffee
@@ -44,13 +44,13 @@
       Promise.resolve(
         KeyStorageApi.getMasterAesCryptoService()
       )
-      .then (masterAesCryptoService_) ->
+      .then (_masterAesCryptoService) ->

-        if not masterAesCryptoService_
+        if not _masterAesCryptoService

-          masterAesCryptoService_ = new AesCryptoService(Cypher.AES_CTR_128)
+          _masterAesCryptoService = new AesCryptoService(Cypher.AES_CTR_128)
           cryptoServiceMarshaller = new CryptoServiceMarshaller()
-          marshalledCryptoService = cryptoServiceMarshaller.marshall(masterAesCryptoService_)
+          marshalledCryptoService = cryptoServiceMarshaller.marshall(_masterAesCryptoService)

           credentialLoader = new CredentialLoader()
           rsaCryptoService = new RsaCryptoService(credentialLoader.getCredentials().keypair)


Fix spec in IE10. Create links using innerHTML instead of setting the href attribute so that protocol is usable.

--- test/spec/layout_spec.coffee
+++ test/spec/layout_spec.coffee
@@ -1,16 +1,39 @@
 define [
   'jquery'
+  'underscore'
   'chaplin/mediator'
   'chaplin/lib/router'
   'chaplin/controllers/controller'
   'chaplin/views/layout'
   'chaplin/views/view'
-], ($, mediator, Router, Controller, Layout, View) ->
+], ($, _, mediator, Router, Controller, Layout, View) ->
   'use strict'

   describe 'Layout', ->
     # Initialize shared variables
     layout = testController = startupControllerContext = router = null
+
+    createLink = (attributes) ->
+      attributes = if _.isObject(attributes) then _.clone(attributes) else {}
+      # Yes, this is ugly. We’re doing it because IE8-10 reports an incorrect
+      # protocol if the href attribute is set programatically.
+      if attributes.href?
+        div = document.createElement 'div'
+        div.innerHTML = "Hello World"
+        link = div.firstChild
+        delete attributes.href
+        $link = $(link)
+      else
+        $link = $(document.createElement 'a')
+      $link.attr attributes
+
+    expectWasNotRouted = (linkAttributes) ->
+      spy = sinon.spy()
+      mediator.subscribe '!router:route', spy
+      $link = createLink linkAttributes
+      $link.appendTo(document.body).click().remove()
+      expect(spy).was.notCalled()
+      mediator.unsubscribe '!router:route', spy

     beforeEach ->
       # Create the layout
@@ -30,15 +53,6 @@

       # Create a fresh router
       router = new Router()
-
-      @expectWasNotRouted = (callback) ->
-        spy = sinon.spy()
-        mediator.subscribe '!router:route', spy
-        link = $('').text('Hello World')
-        callback(link)
-        link.appendTo(document.body).click().remove()
-        expect(spy).was.notCalled()
-        mediator.unsubscribe '!router:route', spy

     afterEach ->
       layout.dispose()
@@ -70,10 +84,7 @@
       spy = sinon.spy()
       mediator.subscribe '!router:route', spy
       path = '/an/internal/link'
-      a = $('').attr('href', path).text('Hello World')
-        .appendTo(document.body)
-        .click()
-        .remove()
+      createLink(href: path).appendTo(document.body).click().remove()
       expect(spy).was.called()
       args = spy.lastCall.args
       passedPath = args[0]
@@ -85,10 +96,7 @@
       spy = sinon.spy()
       mediator.subscribe '!router:route', spy
       path = '/another/link?foo=bar&baz=qux'
-      $('').attr('href', path).text('Hello World')
-        .appendTo(document.body)
-        .click()
-        .remove()
+      createLink(href: path).appendTo(document.body).click().remove()
       args = spy.lastCall.args
       passedPath = args[0]
       passedCallback = args[1]
@@ -97,31 +105,31 @@
       mediator.unsubscribe '!router:route', spy

     it 'should not route links without href attributes', ->
-      @expectWasNotRouted (link) -> link.attr('name', 'foo')
+      expectWasNotRouted name: 'foo'

     it 'should not route links with empty href', ->
-      @expectWasNotRouted (link) -> link.attr('href', '')
+      expectWasNotRouted href: ''

     it 'should not route links to document fragments', ->
-      @expectWasNotRouted (link) -> link.attr('href', '#foo')
+      expectWasNotRouted href: '#foo'

     it 'should not route links with a noscript class', ->
-      @expectWasNotRouted (link) -> link.attr('href', 'u').addClass('noscript')
+      expectWasNotRouted href: '/foo', class: 'noscript'

     it 'should not route rel=external links', ->
-      @expectWasNotRouted (link) -> link.attr('rel', 'external')
+      expectWasNotRouted href: '/foo', rel: 'external'

     it 'should not route target=blank links', ->
-      @expectWasNotRouted (link) -> link.attr('target', '_blank')
+      expectWasNotRouted href: '/foo', target: '_blank'

     it 'should not route non-http(s) links', ->
-      @expectWasNotRouted (link) -> link.attr('href', 'mailto:a@a.com')
-      @expectWasNotRouted (link) -> link.attr('href', 'javascript:1+1')
-      @expectWasNotRouted (link) -> link.attr('href', 'tel:1488')
+      expectWasNotRouted href: 'mailto:a@a.com'
+      expectWasNotRouted href: 'javascript:1+1'
+      expectWasNotRouted href: 'tel:1488'

     it 'should not route clicks on external links', ->
-      @expectWasNotRouted (link) -> link.attr('href', 'http://example.com/')
-      @expectWasNotRouted (link) -> link.attr('href', 'https://example.com/')
+      expectWasNotRouted href: 'http://example.com/'
+      expectWasNotRouted href: 'https://example.com/'

     it 'should register event handlers on the document declaratively', ->
       spy1 = sinon.spy()


remove todo flag

remove todo flag

--- scripts/pagerv2_commands.coffee
+++ scripts/pagerv2_commands.coffee
@@ -142,7 +142,6 @@
         res.send e
       res.finish()

-  # TODO
   #   hubot pd not me            - cancels an override if any
     robot.respond /pd not ([^ ]+)\s*$/, (res) ->
       [ _, who ] = res.match


implement update_project_storage_save -- somehow this got forgot

--- src/smc-hub/postgres-server-queries.coffee
+++ src/smc-hub/postgres-server-queries.coffee
@@ -1545,6 +1545,17 @@
             cb         : required
         @_get_project_column('storage', opts.project_id, opts.cb)

+    update_project_storage_save: (opts) =>
+        opts = defaults opts,
+            project_id : required
+            cb         : required
+        @_query
+            query : "UPDATE projects"
+            jsonb_merge :
+                storage : {saved:new Date()}
+            where : 'project_id :: UUID = $' : opts.project_id
+            cb    : opts.cb
+
     set_project_storage_request: (opts) =>
         opts = defaults opts,
             project_id : required


German translation

German translation


KD: Add terminal to allowed apps list

KD: Add terminal to allowed apps list

--- core/KD.coffee
+++ core/KD.coffee
@@ -225,5 +225,5 @@

 KD.enableLogs() if not KD.config?.suppressLogs

-KD.config.allowedApps = ['account']
+KD.config.allowedApps = ['account', 'terminal']
 prettyPrint = noop


remove unneeded getEnvifyOptions

remove unneeded getEnvifyOptions

--- plugin/browserify.coffee
+++ plugin/browserify.coffee
@@ -145,19 +145,6 @@

   return debug

-getEnvifyOptions = (debug, step) ->
-
-  envifyOptions =
-    NODE_ENV: if debug then 'development' else 'production'
-    # purge by default because we're running it this once
-    _: 'purge'
-
-  # TODO:
-  # use step to know which file we're processing so we can get config options
-  # for it and override env value, or purge value
-
-  return envifyOptions
-
 getReadable = (step) ->

   # Browserify accepts a Readable stream as input, so, we'll use a PassThrough


Removed comments in test file

Removed comments in test file

--- test/initialization.coffee
+++ test/initialization.coffee
@@ -58,125 +58,3 @@
       it "make requests with the expected path and headers", (done) ->
         d = new @clever.District {"name"}
         d.properties done
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-    # describe 'submits writeback requests', ->
-
-    # describe 'submits property requests', ->
-
-    #   assert_correct_district_save = (district, done) ->
-    #     district.save (err) ->
-    #       assert.ifError err
-    #       assert.equal district.get('name'), 'Test'
-    #       assert.equal district.get('location.address'), 'Tacos'
-    #       assert.equal district.get('location.city'), 'Burritos'
-    #       assert.deepEqual district.get('location'), {address: 'Tacos', city: 'Burritos'}
-    #       assert.equal district._uri, '/v1.1/districts/1235'
-    #       done()
-
-    #   it 'when created using the constructor', (done) ->
-    #     district = new @clever.District
-    #       name: 'Test'
-    #       location:
-    #         address: 'Tacos'
-    #         city: 'Burritos'
-    #     assert_correct_district_save district, done
-
-    #   it 'when created using .set()', (done) ->
-    #     district = new @clever.District({})
-    #     district.set("name", "Test")
-    #     district.set("location.address", "Tacos")
-    #     district.set("location.city", "Burritos")
-    #     assert_correct_district_save district, done
-
-    #   it 'when created using .set() and the constructor', (done) ->
-    #     district = new @clever.District
-    #       name: "Test"
-    #       location:
-    #         address: "Tacos"
-    #         city: "Tostada" #No one likes tostadas
-    #     district.set("location.city", "Burritos")
-    #     assert_correct_district_save district, done
-
-    # it 'successfully handles invalid post requests that return a json', (done) ->
-    #   @timeout 30000
-    #   scope = nock('http://fake_api.com')
-    #     .post('/v1.1/districts', {name: 'Test', location: address: 'Tacos'})
-    #     .reply(401, {error: 'unauthorized'})
-    #   district = new @clever.District
-    #     name: 'Test'
-    #     location:
-    #       address: 'Tacos'
-    #   district.save (err) ->
-    #     assert.equal err?.message, "received statusCode 401 instead of 200"
-    #     assert.deepEqual err.body, {error: 'unauthorized'}
-    #     scope.done()
-    #     done()
-
-    # it 'successfully handles invalid post requests that return a string', (done) ->
-    #   @timeout 30000
-    #   scope = nock('http://fake_api.com')
-    #     .post('/v1.1/districts', {name: 'Test', location: address: 'Tacos'})
-    #     .reply(401, 'unauthorized')
-    #   district = new @clever.District
-    #     name: 'Test'
-    #     location:
-    #       address: 'Tacos'
-    #   district.save (err) ->
-    #     assert.equal err?.message, "received statusCode 401 instead of 200"
-    #     assert.equal err.body, 'unauthorized'
-    #     scope.done()
-    #     done()


specs

specs

--- test/models/artwork.coffee
+++ test/models/artwork.coffee
@@ -283,6 +283,10 @@
       @artwork.unset 'collecting_institution'
       @artwork.partnerName().should.equal 'Gagosian Gallery'

+    it "partner name with collecting institution", ->
+      @artwork.set partner: fabricate 'partner'
+      @artwork.partnerName().should.equal 'MOMA'
+
   describe '#partnerLink', ->
     it "empty without partner", ->
       @artwork.unset 'partner'


Updating

Updating

--- keymap.cson
+++ keymap.cson
@@ -1,16 +1,17 @@
 ".platform-linux atom-text-editor:not([mini])":
-  "shift-ctrl-K": "json-writer:insert-link"
-  "shift-ctrl-I": "json-writer:insert-image"
-  "ctrl-i":       "json-writer:toggle-italic-text"
-  "ctrl-b":       "json-writer:toggle-bold-text"
-  "ctrl-'":       "json-writer:toggle-code-text"
-  "ctrl-h":       "json-writer:toggle-strikethrough-text"
-  "ctrl-1":       "json-writer:toggle-h1"
-  "ctrl-2":       "json-writer:toggle-h2"
-  "ctrl-3":       "json-writer:toggle-h3"
-  "ctrl-4":       "json-writer:toggle-h4"
-  "ctrl-5":       "json-writer:toggle-h5"
+  "ctrl-shift-B": "fuzzy-finder:toggle-buffer-finder"
+  "ctrl-shift-C": "terminal-plus:copy"
+  "shift-ctrl-K": "markdown-writer:insert-link"
+  "shift-ctrl-I": "markdown-writer:insert-image"
+  "ctrl-b":       "markdown-writer:toggle-bold-text"
+  "ctrl-'":       "markdown-writer:toggle-code-text"
+  "ctrl-h":       "markdown-writer:toggle-strikethrough-text"
+  "ctrl-i":       "markdown-writer:toggle-italic-text"
   "ctrl-r":       "find-and-replace:replace-all"
-  "ctrl-shift-c": "terminal-plus:copy"
+  "ctrl-1":       "markdown-writer:toggle-h1"
+  "ctrl-2":       "markdown-writer:toggle-h2"
+  "ctrl-3":       "markdown-writer:toggle-h3"
+  "ctrl-4":       "markdown-writer:toggle-h4"
+  "ctrl-5":       "markdown-writer:toggle-h5"
 '.editor':
   "ctrl-alt-s":   "editor:toggle-soft-wrap"


add line test case

add line test case

--- tests/mocha/unit/line.coffee
+++ tests/mocha/unit/line.coffee
@@ -10,13 +10,16 @@
         html: '

'
         format: { align: 'right' }
         leaves: []
-      'single leaf child':
+      'empty child':
+        html: '

'
+        leaves: [{ text: '', formats: { bold: true } }]
+      'leaf child':
         html: '
Bold
'
         leaves: [{ text: 'Bold', formats: { bold: true } }]
-      'single media child':
+      'media child':
         html: '

'
         leaves: [{ text: Quill.Format.MEDIA_TEXT, formats: { image: 'http://quilljs.com/images/icon.png' } }]
-      'single break child':
+      'break child':
         html: '

'
         leaves: [{ text: '', formats: {} }]
       'lots of children':
@@ -62,7 +65,7 @@
     # do not find node that is a child but not a leaf
     # find break leaf
     # no match
-    # line has no leaves... valid?
+    # line has no leaves
   )

   describe('findLeafAt', ->


Fix tests for new thumbifier

Fix tests for new thumbifier

--- tests/test_formatters.coffee
+++ tests/test_formatters.coffee
@@ -37,8 +37,8 @@
       f("img: ![](http://example.com/image_link)").should.equal('
img:
\n')

     it "should insert images previews", ->
-      f("img: ![test](http://example.com/1.jpg)").should.equal('
img:
\n')
-      f("img: ![Test][id]\n[id]: http://example.com/1.gif").should.equal('
img:
\n')
+      f("img: ![test](http://example.com/1.jpg)").should.equal('
img:
\n')
+      f("img: ![Test][id]\n[id]: http://example.com/1.gif").should.equal('
img:
\n')

     it "should allow whitelisted protocols", ->
       f("[nyan](xmpp:jid@jabber.org)").should.equal('
nyan
\n')


Log also data parameters in the event publisher

--- lib/eventpublisher.coffee
+++ lib/eventpublisher.coffee
@@ -33,6 +33,7 @@
                 return

             logger.verbose "Pushing message for event #{event.name}"
+            logger.silly "data = #{JSON.stringify data}"
             logger.silly 'Title: ' + payload.localizedTitle('en')
             logger.silly payload.localizedMessage('en')

@@ -62,4 +63,4 @@
                     event.delete =>
                         cb(0) if cb

-exports.EventPublisher = EventPublisher+exports.EventPublisher = EventPublisher


Translate event.code key representations to the corresponding event.key format.

--- lib/keyboard_utils.coffee
+++ lib/keyboard_utils.coffee
@@ -19,8 +19,15 @@
     unless Settings.get "ignoreKeyboardLayout"
       key = event.key
     else
+      # The logic here is from the vim-like-key-notation project (https://github.com/lydell/vim-like-key-notation).
       key = event.code
+      # Strip some standard prefixes.
       key = key[3..] if key[...3] == "Key"
+      key = key[5..] if key[...5] == "Digit"
+      key = key[6..] if key[...6] == "Numpad"
+      # Translate some special keys to event.key-like strings.
+      if @enUsTranslations[key]
+        key = if event.shift then @enUsTranslations[key][1] else @enUsTranslations[key][0]
       key = key.toLowerCase() unless event.shiftKey

     if key of @keyNames
@@ -62,6 +69,21 @@
   isPrintable: (event) ->
     @getKeyCharString(event)?.length == 1

+  enUsTranslations:
+    "Backquote":     ["`", "~"]
+    "Minus":         ["-", "_"]
+    "Equal":         ["=", "+"]
+    "Backslash":     ["\\","|"]
+    "IntlBackslash": ["\\","|"]
+    "BracketLeft":   ["[", "{"]
+    "BracketRight":  ["]", "}"]
+    "Semicolon":     [";", ":"]
+    "Quote":         ["'", '"']
+    "Comma":         [",", "<"]
+    "Period":        [".", ">"]
+    "Slash":         ["/", "?"]
+    "Space":         [" ", " "]
+
 KeyboardUtils.init()

 root = exports ? window


Collapse top-level configs

Collapse top-level configs

--- lib/package/config.coffee
+++ lib/package/config.coffee
@@ -9,6 +9,7 @@
   juliaOptions:
     type: 'object'
     order: 2
+    collapsed: true
     properties:
       bootMode:
         title: 'Boot Mode'
@@ -87,6 +88,7 @@
     title: 'UI Options'
     type: 'object'
     order: 3
+    collapsed: true
     properties:
       resultsDisplayMode:
         title: 'Result Display Mode'
@@ -375,6 +377,7 @@
   consoleOptions:
     type: 'object'
     order: 4
+    collapsed: true
     properties:
       consoleStyle:
         title: 'Style (requires restart)'
@@ -438,6 +441,7 @@
   remoteOptions:
     type: 'object'
     order: 5
+    collapsed: true
     properties:
       remoteJulia:
         title: 'Command to execute Julia on the remote server'


Fix problem with location sending

Fix problem with location sending

--- client.coffee
+++ client.coffee
@@ -31,8 +31,8 @@
 			return 0
 	if not (window.inRangeCheckinRunning?)
 		window.inRangeCheckinRunning = {}
-	if not (inRangeCheckinRunning[Plugin.groupId()]?)
-		inRangeCheckinRunning[Plugin.groupId()] = false
+	if not (inRangeCheckinRunning[Plugin.groupCode()]?)
+		inRangeCheckinRunning[Plugin.groupCode()] = false
 	window.restoreMapLocation = true
 	log 'restoreMapLocation='+restoreMapLocation
 	#Server.call 'log', Plugin.userId(), "FULL RENDER"
@@ -48,9 +48,9 @@
 			Db.local.set 'gameNumber', remote
 			# Do cleanup stuff
 			Db.local.remove 'currentSetupPage'
-			if inRangeCheckinRunning[Plugin.groupId()]
+			if inRangeCheckinRunning[Plugin.groupCode()]
 				clearInterval(checkinLocationFunction)
-				window.inRangeCheckinRunning[Plugin.groupId()] = false
+				window.inRangeCheckinRunning[Plugin.groupCode()] = false

 	# Ask for location
 	if !Geoloc.isSubscribed()
@@ -68,7 +68,7 @@
 			if checkinLocationFunction?
 				clearInterval(checkinLocationFunction)
 				checkinLocationFunction = undefined
-				window.inRangeCheckinRunning[Plugin.groupId()] = false
+				window.inRangeCheckinRunning[Plugin.groupCode()] = false
 		else if gameState is 1 # Game is running
 			# Set page title
 			page = Page.state.get(0)
@@ -121,7 +121,7 @@
 			if checkinLocationFunction?
 				clearInterval(checkinLocationFunction)
 				checkinLocationFunction = undefined
-				window.inRangeCheckinRunning[Plugin.groupId()] = false
+				window.inRangeCheckinRunning[Plugin.groupCode()] = false
 	Obs.observe ->
 		deviceId = Db.local.peek 'deviceId'
 		if not deviceId?
@@ -1403,13 +1403,13 @@
 											else
 												clearInterval(checkinLocationFunction)
 												checkinLocationFunction = undefined
-												window.inRangeCheckinRunning[Plugin.groupId()] = false
+												window.inRangeCheckinRunning[Plugin.groupCode()] = false
 										if within
 											log 'accuracy='+accuracy+', beaconRadius='+beaconRadius
 											if accuracy > beaconRadius # Deny capturing with low accuracy
 												if not inRangeValue?
 													#clearInterval(checinLocation) # TODO check if this is required or causes problems
-													#inRangeCheckinRunning[Plugin.groupId()] = false
+													#inRangeCheckinRunning[Plugin.groupCode()] = false
 													log 'Did not checkin location, accuracy too low: '+accuracy
 													Dom.div !->
 														Dom.cls 'infobar'
@@ -1428,24 +1428,24 @@
 															Dom.text 'Your accuracy of '+accuracy+' meter is higher than the maximum allowed '+beaconRadius+' meter.'
 											else
 												if inRangeValue?
-													if not (inRangeCheckinRunning[Plugin.groupId()]?) or not (inRangeCheckinRunning[Plugin.groupId()])
+													if not (inRangeCheckinRunning[Plugin.groupCode()]?) or not (inRangeCheckinRunning[Plugin.groupCode()])
 														checkinLocation()
 														setInterval(checkinLocation, 30*1000)
 														checkinLocationFunction = checkinLocation
-														window.inRangeCheckinRunning[Plugin.groupId()] = true
+														window.inRangeCheckinRunning[Plugin.groupCode()] = true
 												else
 													log 'Trying beacon takeover: userId='+Plugin.userId()+', location='+latLngObj+', deviceId='+deviceId
 													checkinLocation()
-													if not (inRangeCheckinRunning[Plugin.groupId()]?) or not (inRangeCheckinRunning[Plugin.groupId()])
+													if not (inRangeCheckinRunning[Plugin.groupCode()]?) or not (inRangeCheckinRunning[Plugin.groupCode()])
 														setInterval(checkinLocation, 30*1000)
 														checkinLocationFunction = checkinLocation
-														window.inRangeCheckinRunning[Plugin.groupId()] = true
+														window.inRangeCheckinRunning[Plugin.groupCode()] = true
 										else if (not within and inRangeValue? and (inRangeValue == deviceId || inRangeValue == 'true'))
 											log 'Trying stop of beacon takeover: userId='+Plugin.userId()+', location='+latLngObj+', deviceId='+deviceId
 											checkinLocation()
-											if inRangeCheckinRunning[Plugin.groupId()]
+											if inRangeCheckinRunning[Plugin.groupCode()]
 												clearInterval(checkinLocationFunction)
-												window.inRangeCheckinRunning[Plugin.groupId()] = false
+												window.inRangeCheckinRunning[Plugin.groupCode()] = false
 				else
 					log 'Location could not be found'
 		Obs.onClean ->


Fix number of team selection buttons

Fix number of team selection buttons

--- client.coffee
+++ client.coffee
@@ -367,7 +367,7 @@
 								Dom.onTap !->
 									numberOfTeams.set number
 					userCount = Math.min(Plugin.users.count().get(),6)
-					for number in [2..Math.max(userCount,4)]
+					for number in [2..Math.max(userCount,2)]
 						renderTeamButton number
 				# Duration input
 				Dom.h2 tr("Round time")


Possibly fix layout bug

Possibly fix layout bug

--- client.coffee
+++ client.coffee
@@ -75,6 +75,7 @@
 							Dom.style
 								marginLeft: '5px'
 								paddingBottom: '7px'
+								Flex: true
 							Dom.userText commit.get('message')
 		, (commit) ->
 			-(Date.parse(commit.get('date')))


Add basic overview of current and past rounds

--- client.coffee
+++ client.coffee
@@ -13,7 +13,12 @@
 exports.render = ->
 	if Page.state.get(0) is 'advanced'
 		renderAdvanded()
+	else if Page.state.get(0) is 'round'
+		renderRound()
+	else
+		renderRoundList()

+renderRound = ->
 	Dom.div !->
 		Dom.style fontSize: '150%', fontWeight: 'bold', textShadow: '0 1px 0 #fff', textAlign: 'center', padding: '4px 10px 10px 10px'
 		Dom.text Util.indexToQuestion(0)
@@ -25,6 +30,13 @@
 		{}.noSuchMethod()

 	Ui.bigButton 'Go to advanced...', -> Page.nav ['advanced']
+
+renderRoundList = ->
+	Ui.list ->
+		Ui.item ->
+			Dom.h2 "This is a list entry!"
+		Ui.item ->
+			Dom.h2 "This is also a list entry!"

 renderAdvanded = ->
 	Dom.h2 "Hello, World!"


Stack: Make delete button disabled for first filter and dry destroy method in firewall modal.

--- client/Environments/views/firewall/firewallfilterformview.coffee
+++ client/Environments/views/firewall/firewallfilterformview.coffee
@@ -44,18 +44,22 @@
             required   : yes
           messages     :
             required   : "Please select a action type"
+      remove           :
+        itemClass      : KDCustomHTMLView
+        cssClass       : "delete-button half"
+        click          : @bound "destroy"
       enabled          :
         label          : "Enabled"
         cssClass       : "half"
         itemClass      : KodingSwitch
         defaultValue   : yes
-      remove           :
-        itemClass      : KDCustomHTMLView
-        cssClass       : "delete-button half"
-        click          : =>
-          @emit "FirewallFilterRemoved"
-          @destroy()

-    delete options.fields.remove  unless options.removable
+    unless options.removable
+      options.cssClass = KD.utils.curry "undeletable", options.cssClass

     super options, data
+
+  destroy: ->
+    return no  unless @getOptions().removable
+    @emit "FirewallFilterRemoved"
+    super


code cleanup

code cleanup

--- client/Framework/components/inputs/KDInputViewWithPreview.coffee
+++ client/Framework/components/inputs/KDInputViewWithPreview.coffee
@@ -20,8 +20,6 @@
     @setClass "kdinputwithpreview"

     @showPreview = options.preview.showInitially or no
-
-    log @getId()

   setDomElement:(CssClass="")->
     super CssClass


Removed automated regeneration

Removed automated regeneration

--- docpad.coffee
+++ docpad.coffee
@@ -93,7 +93,7 @@
   }

   # Regenerate every hour
-  regenerateEvery: 3600000
+  #regenerateEvery: 3600000
 }

 # Export the DocPad Configuration


Stubbed out HTTP API methods for competition membership

--- http.coffee
+++ http.coffee
@@ -27,6 +27,9 @@
   ['/competitions/all', 'get', listCompetitions]
   ['/competitions/by-owner', 'get', listCompetitionsByOwner]
   ['/competitions/by-membership', 'get', listCompetitionsByMembership]
+  [/competition\/\w+/withdraw/, 'post', withdrawFromCompetition]
+  [/competition\/\w+/join/, 'post', joinCompetition]
+  [/competition\/\w+/, 'get', listCompetition]
 ]

 # Creates an object suitable for use with paged UIs
@@ -101,6 +104,9 @@
     )
     .done()

+listCompetition = (api) ->
+  (req, res) ->
+
 listCompetitions = (api) ->
   (req, res) ->
     api.listCompetitions()
@@ -126,5 +132,10 @@
 listCompetitionsByMembership = (api) ->
   (req, res) ->

+joinCompetition = (api) ->
+  (req, res) ->
+
+withdrawFromCompetition = (api) ->
+  (req, res) ->

 module.exports = {init}


rename "numerator denominator" to "fraction"

--- math/expression_manipulation_spec.coffee
+++ math/expression_manipulation_spec.coffee
@@ -482,16 +482,16 @@
       expect(new_exp.last(1)).toBeInstanceOf @components.classes.multiplication

-  describe "appending numerator and denominator fraction", ->
-    it_plays_manipulation_role
-      subject: ->
-        @manip.build_append_numerator_denominator()
+  describe "appending a fraction", ->
+    it_plays_manipulation_role
+      subject: ->
+        @manip.build_append_fraction()
       expression_for_performance: ->
         @exp_pos_builder()

     it "adds a numerator/denominator where we expect", ->
       exp = @exp_pos_builder(1)
-      new_exp = @manip.build_append_numerator_denominator().perform(exp).expression()
+      new_exp = @manip.build_append_fraction().perform(exp).expression()

       expect(new_exp.last()).toBeInstanceOf @components.classes.fraction
       expect(new_exp.last().numerator()).toBeInstanceOf @components.classes.expression


Rename `handleKeyboard` -> `handleKeypress`

--- lib/assets/javascripts/commandz.coffee
+++ lib/assets/javascripts/commandz.coffee
@@ -13,9 +13,9 @@

   keyboardShortcuts: (enable=true) ->
     addOrRemove = if enable then 'addEventListener' else 'removeEventListener'
-    document[addOrRemove]('keypress', this.handleKeyboard)
+    document[addOrRemove]('keypress', this.handleKeypress)

-  handleKeyboard: (e) =>
+  handleKeypress: (e) =>
     return if document.activeElement.nodeName is 'INPUT'
     return unless e.keyCode is 122 and e.metaKey is true


Don't want others removing data.

Don't want others removing data.

--- scripts/storage-delete.coffee
+++ scripts/storage-delete.coffee
@@ -6,6 +6,7 @@

 module.exports = (robot) ->
   robot.respond /storage delete (\w*)$/i, (msg) ->
-    if msg.match[1] isnt 'users'
-      delete robot.brain.data[msg.match[1]]
-      msg.send "#{msg.match[1]} deleted from storage"
+    if robot.auth.hasRole(msg.envelope.user, ['admin'])
+        if msg.match[1] isnt 'users'
+          delete robot.brain.data[msg.match[1]]
+          msg.send "#{msg.match[1]} deleted from storage"


Compute now has its own implementation of Observables, which will be used only if knockout is not present.

--- src/Compute.coffee
+++ src/Compute.coffee
@@ -1,30 +1,113 @@
 # If we are in a browser, then define Compute on the window.
 C = {}

-isValid = (observables, func)->
-  return false unless ko.isObservable o for o in observables
-  return false if ko.isObservable func
+# One shouldn't have to use knockout, awesome as it is, just for observables.
+# These will be used when there is no knockout. If knockout is present, we simply will
+# ko.observable (in C.o, C.oa)
+
+Observable = (val) ->
+  _value = val
+
+  subscriptions = []
+
+  callSubscribers = ()->
+    for s in subscriptions
+      s _value
+
+  o = (newVal)->
+    if arguments.length > 0
+      isValueChanged = _value isnt newVal
+      _value = newVal
+
+      if isValueChanged
+        callSubscribers()
+
+    else
+      _value
+
+  o.subscribe = (f)-> subscriptions.push f
+  o._isObservable = true
+
+  o
+
+
+ObservableArray = (arr)->
+  _value = arr or []
+  throw new Error('Argument to observable array must be null, undefined or Array') unless _value instanceof Array
+
+  subscriptions = []
+
+  callSubscribers = ()->
+    for s in subscriptions
+      s _value
+
+  o = (newArr)->
+    if arguments.length > 0
+      _value = newArr
+      callSubscribers()
+
+    else
+      _value
+
+  o.subscribe = (f)-> subscriptions.push f
+
+  o.push = (v)->
+    _value.push v
+    callSubscribers()
+
+  o.pop = ()->
+    v = _value.pop()
+    callSubscribers()
+    v
+
+  o.peek = ()->
+    _value[_value.length - 1]
+
+  o._isObservable = true
+
+  o
+
+
+_isObservable = (v)->
+  if typeof ko isnt 'undefined'
+    ko.isObservable v
+  else
+    v._isObservable
+
+
+_unwrap = (v)-> if _isObservable v then v() else v
+
+
+_isValid = (observables, func)->
+  return false unless _isObservable(o) for o in observables
+  return false if _isObservable(func)
   return false unless typeof func is 'function'
   true

-gather = (observables)->
+_gather = (observables)->
   values = []
-  values.push ko.unwrap(o) for o in observables
+  values.push _unwrap(o) for o in observables
   values

 C.o = (val)->
-  o = ko.observable val
-  o
+  if typeof ko isnt 'undefined'
+    ko.observable val
+  else
+    Observable val

 C.oa = (arr)->
-  ko.observableArray arr
+  if typeof ko isnt 'undefined'
+    ko.observableArray arr
+  else
+    ObservableArray arr
+

 C.on = (observables..., f)->
-  throw new Error 'Invalid arguments to C.on' unless isValid observables, f
+  throw new Error 'Invalid arguments to C.on' unless _isValid observables, f
   isStopped = false
   func = ()->
     return if isStopped
-    f.apply null, gather(observables)
+    f.apply null, _gather(observables)

   o.subscribe func for o in observables

@@ -34,12 +117,12 @@

 C.from = (observables..., f)->
-  throw new Error 'Invalid arguments to C.on' unless isValid observables, f
-  newOb = ko.observable()
+  throw new Error 'Invalid arguments to C.on' unless _isValid observables, f
+  newOb = C.o()
   isStopped = false
   func = ()->
     return if isStopped
-    val = f.apply null, gather(observables)
+    val = f.apply null, _gather(observables)
     newOb val
     newOb

@@ -49,8 +132,13 @@
   newOb.$resume = ()-> isStopped = false
   newOb

+C._gather         = _gather
+C._isValid        = _isValid
+C.Observable      = Observable
+C.ObservableArray = ObservableArray
+
 # If we are in node (which we will be, eventually)
-if window
+if typeof window isnt 'undefined'
   window.Compute = C
 else if module
-  module.exports = Compute
+  module.exports = C


Support tweaked layout for market insights on auction artwork pages

--- desktop/apps/artwork/routes.coffee
+++ desktop/apps/artwork/routes.coffee
@@ -94,7 +94,7 @@
       if saleId
         fetchMeData(meQuery, req.user, saleId)
           .then (meData) ->
-            res.render 'index', extend data, meData
+            res.render 'index', extend data, meData, { isAdmin: res.locals.sd.CURRENT_USER?.type is 'Admin' }
           .catch next
       else
         res.render 'index', extend data, { isAdmin: res.locals.sd.CURRENT_USER?.type is 'Admin' }


use view to have only image

use view to have only image

--- init.coffee
+++ init.coffee
@@ -20,7 +20,7 @@
         async.each(docs, convert, cb)

 createThumb = (cb) ->
-    File.all (err, files) ->
+    File.imageByDate (err, files) ->
         async.eachSeries files, thumb, cb

 # Create all requests and upload directory


add initial sprout template

add initial sprout template

Update license to carrot

update package description

--- init.coffee
+++ init.coffee
@@ -2,6 +2,14 @@
   {
     name: 'name'
     message: 'What is the name of your project?'
+  },
+  {
+    name: 'description'
+    message: 'Describe your project'
+  },
+  {
+    name: 'github_username'
+    message: 'What is your github username?'
   }
 ]


catch except when photo cannot convert image

--- init.coffee
+++ init.coffee
@@ -4,10 +4,14 @@
 convertImage = (cb) ->
     convert = (doc, callback) ->
         if doc._attachments?
-            console.log "Convert #{doc.title} ..."
-            doc.convertBinary (err, res, body) ->
-                console.log err if err?
-                callback err
+            try
+                console.log "Convert #{doc.title} ..."
+                doc.convertBinary (err, res, body) ->
+                    console.log err if err?
+                    callback err
+            catch
+                console.log "Cannot convert #{doc.title}"
+                callback()
         else
             callback()
     Photo.all (err, docs) ->


save every fields when connector object is created

--- server/init/konnectors.coffee
+++ server/init/konnectors.coffee
@@ -1,11 +1,11 @@
 path = require 'path'
 fs = require 'fs'
-
 log = require('printit')()

 Konnector = require '../models/konnector'

-modulesPath = path.join(path.dirname(fs.realpathSync(__filename)), '..', 'konnectors')
+currentPath = path.dirname fs.realpathSync __filename
+modulesPath = path.join currentPath, '..', 'konnectors'

 isCoffeeFile = (fileName) ->
     extension = fileName.split('.')[1]
@@ -38,12 +38,7 @@

             for name, konnectorModule of konnectorModules
                 unless konnectorHash[konnectorModule.name]?
-                    konnectorsToCreate.push
-                        name: konnectorModule.name
-                        description: konnectorModule.description
-                        fields: konnectorModule.fields
-                        init: konnectorModule.init
-
+                    konnectorsToCreate.push konnectorModule

             recCreate = ->
                 if konnectorsToCreate.length > 0


- remove unessary codes

- remove unessary codes

--- src/advance.coffee
+++ src/advance.coffee
@@ -10,13 +10,6 @@

 module.exports  = class AdvancePropertyManager
   gAttrsName = '$attributes'
-
-  getRealAttrName = (attrs, name)->
-    if not attrs.hasOwnProperty name
-      for k,v of attrs
-        return k if v.name is name
-      return
-    name

   @::[gAttrsName] = null
   @::SMART_ASSIGN = Properties.SMART_ASSIGN


Add package.json

Add package.json

--- test/lib/WorkflowExecutionHistoryGeneratorSpec.coffee
+++ test/lib/WorkflowExecutionHistoryGeneratorSpec.coffee
@@ -3,7 +3,7 @@

 WorkflowExecutionHistoryGenerator = require "../../lib/WorkflowExecutionHistoryGenerator"

-describe "Generate3DCartOrdersByFreshdeskUserIdCollection", ->
+describe "WorkflowExecutionHistoryGenerator", ->
   generator = null;

   before ->
@@ -166,14 +166,14 @@
           @ActivityTaskCompleted "3DCartDownloadOrders"
         ]
         decisions: [
-          @ScheduleActivityTask "BellefitGenerate3DCartOrdersByFreshdeskUserIdCollectionComplete",
+          @ScheduleActivityTask "BellefitGenerate3DCartOrdersByFreshdeskUserIdCollection",
             avatarIds:
               "Freshdesk": "D6vpAkoHyBXPadp4c"
               "3DCart": "T7JwArn9vCJLiKXbn"
         ]
         branches: [
           events: [
-            @ActivityTaskCompleted "BellefitGenerate3DCartOrdersByFreshdeskUserIdCollectionComplete"
+            @ActivityTaskCompleted "BellefitGenerate3DCartOrdersByFreshdeskUserIdCollection"
           ]
           decisions: [
             @CompleteWorkflowExecution()


Fills in closing an open expression feature

--- math/expression_manipulation_spec.coffee
+++ math/expression_manipulation_spec.coffee
@@ -34,7 +34,7 @@
         expect(@new_exp.first()).toBeInstanceOf @components.exponentiation

       it "provides the exponentiation its base", ->
-        expect(@new_exp.first().base()).toBeAnEqualExpressionTo @exp_builder(10)
+        expect(@new_exp.first().base()).toBeAnEqualExpressionTo @exp_builder(10).first()

     describe "on an expression that currently has an addition at the end", ->
       it "'drops' the addition", ->
@@ -84,12 +84,18 @@
       expect(exp).toBeInstanceOf @math.components.expression
       expect(exp.isOpen()).toEqual true

-  describe "closing a sub expression", ->
+  describe "closing a sub expression WIP", ->
     it "takes an open subexpression and closes it", ->
-      @exp = @exp_builder()
-      new_exp = @manip.open_sub_expression.build().invoke(@exp)
-      expected = @exp_builder({open_sub_expression: []})
+      exp = @exp_builder({open_expression: []})
+      new_exp = @manip.close_sub_expression.build().invoke(exp)
+      expect(new_exp).toBeAnEqualExpressionTo @exp_builder([])
+
+    it "correctly handles nested open subexpressions", ->
+      exp = @exp_builder({open_expression: [{open_expression: []}]})
+      new_exp = @manip.close_sub_expression.build().invoke(exp)
+      expected = @exp_builder([[1]])
       expect(new_exp).toBeAnEqualExpressionTo expected
+      #throw "should not accept"

   describe "(moved over from other test file)", ->
     describe "the square command", ->


save backgroundColor in device

save backgroundColor in device

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -250,10 +250,8 @@
 			# If we switch from fullscreen to a device, we should zoom to fit
 			shouldZoomToFit = @_deviceType is "fullscreen"

-			if deviceType is "fullscreen"
-				@screen.backgroundColor = "white"
-			else
-				@screen.backgroundColor = "black"
+			@screen.backgroundColor = "black"
+			@screen.backgroundColor = device.backgroundColor if device.backgroundColor?

 			@_device = _.clone(device)
 			@_deviceType = deviceType
@@ -1068,6 +1066,7 @@
 	"fullscreen":
 		name: "Fullscreen"
 		deviceType: "desktop"
+		backgroundColor: "white"

 	# iPad Air
 	"apple-ipad-air-2-silver": _.clone(iPadAir2BaseDevice)
@@ -1170,9 +1169,6 @@
 	"samsung-galaxy-note-5-silver-titanium": _.clone(SamsungGalaxyNote5BaseDevice)
 	"samsung-galaxy-note-5-white": _.clone(SamsungGalaxyNote5BaseDevice)

-
-	# OLD DEVICES
-
 	# Desktop Browser
 	"desktop-safari-1024-600":
 		deviceType: "browser"
@@ -1182,7 +1178,7 @@
 		deviceImageWidth: 1136
 		deviceImageHeight: 760
 		deviceImageJP2: true
-		maxStudioVersion: oldDeviceMaxVersion
+		backgroundColor: "white"
 	"desktop-safari-1280-800":
 		deviceType: "browser"
 		name: "Desktop Safari 1280 x 800"
@@ -1191,7 +1187,7 @@
 		deviceImageWidth: 1392
 		deviceImageHeight: 960
 		deviceImageJP2: true
-		maxStudioVersion: oldDeviceMaxVersion
+		backgroundColor: "white"
 	"desktop-safari-1440-900":
 		deviceType: "browser"
 		name: "Desktop Safari 1440 x 900"
@@ -1200,7 +1196,9 @@
 		deviceImageWidth: 1552
 		deviceImageHeight: 1060
 		deviceImageJP2: true
-		maxStudioVersion: oldDeviceMaxVersion
+		backgroundColor: "white"
+
+	# OLD DEVICES

 	# iPhone 6
 	"iphone-6-spacegray": _.clone(old_iPhone6BaseDevice)


fix for empty cache

fix for empty cache

--- src/builder/index.coffee
+++ src/builder/index.coffee
@@ -172,7 +172,7 @@
         for own name, value of options.assets
             @assets[name] = value
         @appcache = options.appcache
-        @cached = options.cached
+        @cached = options.cached ? []
         @hash_func = options.hash_func ? "md5"
         @randomize_urls = options.randomize_urls ? true
         @coding_func = options.coding_func


:bug: Fix missing change on marker deletion and update

--- lib/mixins/decoration-management.coffee
+++ lib/mixins/decoration-management.coffee
@@ -34,8 +34,8 @@
     marker = @getMarker(marker.id)

     @decorationMarkerDestroyedSubscriptions[marker.id] ?= @subscribe marker, 'destroyed', =>
-
       @removeAllDecorationsForMarker(marker)
+      @stackRangeChanges(marker.getScreenRange())

     @decorationMarkerChangedSubscriptions[marker.id] ?= @subscribe marker, 'changed', (event) =>
       decorations = @decorationsByMarkerId[marker.id]
@@ -45,6 +45,8 @@
       if decorations?
         for decoration in decorations
           @trigger 'minimap:decoration-changed', marker, decoration, event
+
+      @stackRangeChanges(marker.getScreenRange())

     decoration = new Decoration(marker, this, decorationParams)
     @decorationsByMarkerId[marker.id] ?= []
@@ -65,13 +67,13 @@
     range = decoration.marker.getScreenRange()
     return unless range?

+    @stackRangeChanges(range)
+
+  stackRangeChanges: (range) ->
     startScreenRow = range.start.row
     endScreenRow = range.end.row
     lastRenderedScreenRow  = @getLastVisibleScreenRow()
     firstRenderedScreenRow = @getFirstVisibleScreenRow()
-    console.log firstRenderedScreenRow, lastRenderedScreenRow
-    lastRenderedScreenRow = @editor.getLastScreenRow() if isNaN(lastRenderedScreenRow)
-    firstRenderedScreenRow = 0 if isNaN(firstRenderedScreenRow)
     screenDelta = (lastRenderedScreenRow - firstRenderedScreenRow) - (endScreenRow - startScreenRow)

     changeEvent =