Convert UTF8 to punycode until Recurly fixes their UTF-8 bug.

--- node_modules_koding/koding-payment/lib/main.coffee
+++ node_modules_koding/koding-payment/lib/main.coffee
@@ -1,10 +1,20 @@
-xml2js = require 'xml2js'
-https  = require 'https'
-S      = require 'string'
-
-unescapeHTML = (html)->
-  html
-  # S(html).decodeHTMLEntities().s
+xml2js   = require 'xml2js'
+https    = require 'https'
+
+S        = require 'string'
+punycode = require 'punycode'
+
+escape = (text)->
+  if typeof text is 'string'
+    punycode.encode S(text).escapeHTML().s
+  else
+    ''
+
+unescape = (text)->
+  if typeof text is 'string'
+    S(punycode.decode text).unescapeHTML().s
+  else
+    ''

 createId = require 'hat'
 getUniqueId = -> createId 64
@@ -58,11 +68,11 @@
     method   : method
     auth     : API_KEY + ':'
     headers  :
-      accept           : 'application/xml'
-      'content-length' : data.length
+      Accept           : 'application/xml'
+      'Content-Length' : data.length

   if data.length > 0
-    options.headers['content-type'] = 'application/xml; charset=utf-8'
+    options.headers['Content-Type'] = 'application/xml; charset=UTF-8'

   req = https.request options, (res) ->
     res.setEncoding 'utf8'
@@ -80,7 +90,7 @@
             errors = [Error('xml', 'Invalid XML', '')]
             callback errors, {}
           else if r.error
-            errors = [Error(r.error.symbol[0], unescapeHTML r.error.description[0]['_'], '')]
+            errors = [Error(r.error.symbol[0], r.error.description[0]['_'], '')]
             callback errors, {}
           else if r.errors
             # Build list of errors
@@ -98,9 +108,7 @@
     errors = [Error('connection', 'Unable to connect to payment backend.' , '')]
     callback errors, {}

-  if data.length > 0
-    req.write data
-  req.end()
+  req.end(data, 'utf8')

 # POST request
 recurlyPost = (operation, data, callback) ->
@@ -156,7 +164,7 @@
             plans.push
               title      : p.name[0]
               code       : p.plan_code[0]
-              desc       : if typeof p.description[0] is "string" then unescapeHTML p.description[0] else ""
+              desc       : if typeof p.description[0] is "string" then p.description[0] else ""
               feeMonthly : parseInt p.unit_amount_in_cents[0]['USD'][0]['_']
               feeInitial : parseInt p.setup_fee_in_cents[0]['USD'][0]['_']
       callback no, plans
@@ -173,7 +181,7 @@
           plan =
             code       : page.plan.plan_code[0]
             title      : page.plan.name[0]
-            desc       : if typeof page.plan.description[0] is "string" then unescapeHTML page.plan.description[0] else ""
+            desc       : if typeof page.plan.description[0] is "string" then page.plan.description[0] else ""
             feeMonthly : parseInt page.plan.unit_amount_in_cents[0]['USD'][0]['_']
             feeInitial : parseInt page.plan.setup_fee_in_cents[0]['USD'][0]['_']
           callback no, plan
@@ -206,7 +214,7 @@
   data.feeInitial ?= 0
   xml = """<plan>
              <plan_code>#{data.code}</plan_code>
-             <name>#{data.title}</name>
+             <name>#{escape data.title}</name>
              <setup_fee_in_cents>
                <USD>#{parseInt(data.feeInitial)}</USD>
              </setup_fee_in_cents>
@@ -224,7 +232,7 @@
       plan =
         code       : res.plan.plan_code[0]
         title      : res.plan.name[0]
-        desc       : if typeof res.plan.description[0] is "string" then unescapeHTML res.plan.description[0] else ""
+        desc       : if typeof res.plan.description[0] is "string" then res.plan.description[0] else ""
         feeMonthly : parseInt res.plan.unit_amount_in_cents[0]['USD'][0]['_']
         feeInitial : parseInt res.plan.setup_fee_in_cents[0]['USD'][0]['_']
       callback no, plan
@@ -319,8 +327,8 @@
                <account_code>#{data.accountCode}</account_code>
                <username>#{data.username}</username>
                <email>#{data.email}</email>
-               <first_name>#{data.firstName}</first_name>
-               <last_name>#{data.lastName}</last_name>
+               <first_name>#{escape data.firstName}</first_name>
+               <last_name>#{escape data.lastName}</last_name>

                <billing_info>
                  <number>#{data.cardNumber}</number>
@@ -328,14 +336,14 @@
                  <month>#{data.cardMonth}</month>
                  <year>#{data.cardYear}</year>

-                 <first_name>#{data.cardFirstName}</first_name>
-                 <last_name>#{data.cardLastName}</last_name>
-                 <address1>#{data.address1}</address1>
-                 <address2>#{data.address2}</address2>
-                 <city>#{data.city}</city>
-                 <state>#{data.state}</state>
+                 <first_name>#{escape data.cardFirstName}</first_name>
+                 <last_name>#{escape data.cardLastName}</last_name>
+                 <address1>#{escape data.address1}</address1>
+                 <address2>#{escape data.address2}</address2>
+                 <city>#{escape data.city}</city>
+                 <state>#{escape data.state}</state>
                  <zip>#{data.zipCode}</zip>
-                 <country>#{data.country}</country>
+                 <country>#{escape data.country}</country>

                  <ip_address>#{data.ipAddress}</ip_address>
                </billing_info>
@@ -435,7 +443,7 @@
               currency  : t.currency[0]
               datetime  : t.created_at[0]['_']
               refundable: t.refundable[0]['_']
-              owner     : "#{billing.first_name[0]} #{billing.last_name[0]}"
+              owner     : "#{unescape billing.first_name[0]} #{unescape billing.last_name[0]}"
               card      : "#{billing.card_type[0]} - #{billing.first_six.toString().substring(0,4)}-...-#{billing.last_four}"
               source    : t.source[0]
       callback no, transactions
@@ -506,7 +514,7 @@
               uuid        : t.uuid[0]
               amount      : t.total_in_cents[0]['_']
               currency    : t.currency[0]
-              description : if t.description[0].$ then "" else unescapeHTML t.description[0]
+              description : if t.description[0].$ then "" else unescape t.description[0]
               origin      : t.origin[0]
       callback no, adjustments

@@ -560,19 +568,19 @@
         if page.billing_info
           info = page.billing_info
           billing =
-            cardFirstName     : info.first_name[0]
-            cardLastName      : info.last_name[0]
+            cardFirstName     : unescape info.first_name[0]
+            cardLastName      : unescape info.last_name[0]
             cardNumber        : info.first_six.toString().substring(0,4) + '-...-' + info.last_four
             cardMonth         : info.month[0]['_']
             cardYear          : info.year[0]['_']
             cardCV            : 'XXX'
             cardType          : info.card_type[0]
-            address1          : info.address1[0]
-            address2          : if typeof info.address2[0] is "string" then info.address2[0] else ""
-            city              : info.city[0]
-            state             : info.state[0]
+            address1          : unescape info.address1[0]
+            address2          : if typeof info.address2[0] is "string" then unescape info.address2[0] else ""
+            city              : unescape info.city[0]
+            state             : unescape info.state[0]
             zip               : info.zip[0]
-            country           : info.country[0]
+            country           : unescape info.country[0]
         else
           billing = {}
       callback no, billing
@@ -586,8 +594,8 @@
         # See https://docs.recurly.com/api/account for XML format
         if page.account
           account =
-            firstName : page.account.first_name[0]
-            lastName  : page.account.last_name[0]
+            firstName : unescape page.account.first_name[0]
+            lastName  : unescape page.account.last_name[0]
             email     : page.account.email[0]
             username  : page.account.username[0]
             url       : 'https://koding.recurly.com/account/' + page.account.hosted_login_token[0]
@@ -606,6 +614,38 @@
     else
       callback no, res

+setBilling = (accountCode, data, callback) ->
+  # Remove credit card number if it's not updated
+  if data.cardNumber? and data.cardNumber.indexOf('...') > -1 or data.cardCV? and data.cardCV == 'XXX'
+    delete data.cardNumber
+    delete data.cardCV
+
+  # See https://docs.recurly.com/api/account for XML format
+  xml = """<?xml version="1.0" encoding="UTF-8"?>
+            <billing_info>
+             <number>#{data.cardNumber}</number>
+             <verification_value>#{data.cardCV}</verification_value>
+             <month>#{data.cardMonth}</month>
+             <year>#{data.cardYear}</year>
+
+             <first_name>#{escape data.cardFirstName}</first_name>
+             <last_name>#{escape data.cardLastName}</last_name>
+             <address1>#{escape data.address1}</address1>
+             <address2>#{escape data.address2}</address2>
+             <city>#{escape data.city}</city>
+             <state>#{escape data.state}</state>
+             <zip>#{data.zipCode}</zip>
+             <country>#{escape data.country}</country>
+
+             <ip_address>#{data.ipAddress}</ip_address>
+            </billing_info>"""
+
+  recurlyPut "accounts/#{accountCode}/billing_info", xml, (err, res) ->
+    if err
+      callback err, {}
+    else
+      callback no, res
+
 setAccountWithBilling = (accountCode, data, callback) ->
   # Remove credit card number if it's not updated
   if data.cardNumber? and data.cardNumber.indexOf('...') > -1 or data.cardCV? and data.cardCV == 'XXX'
@@ -618,8 +658,17 @@
              <account_code>#{accountCode}</account_code>
              <username>#{data.username}</username>
              <email>#{data.email}</email>
-             <first_name>#{data.firstName}</first_name>
-             <last_name>#{data.lastName}</last_name>
+             <first_name>#{escape data.firstName}</first_name>
+             <last_name>#{escape data.lastName}</last_name>
+           </account>"""
+
+  xmlWithBilling = """<?xml version="1.0" encoding="UTF-8"?>
+           <account>
+             <account_code>#{accountCode}</account_code>
+             <username>#{data.username}</username>
+             <email>#{data.email}</email>
+             <first_name>#{escape data.firstName}</first_name>
+             <last_name>#{escape data.lastName}</last_name>

              <billing_info>
                <number>#{data.cardNumber}</number>
@@ -627,19 +676,56 @@
                <month>#{data.cardMonth}</month>
                <year>#{data.cardYear}</year>

-               <first_name>#{data.cardFirstName}</first_name>
-               <last_name>#{data.cardLastName}</last_name>
-               <address1>#{data.address1}</address1>
-               <address2>#{data.address2}</address2>
-               <city>#{data.city}</city>
-               <state>#{data.state}</state>
+               <first_name>#{escape data.cardFirstName}</first_name>
+               <last_name>#{escape data.cardLastName}</last_name>
+               <address1>#{escape data.address1}</address1>
+               <address2>#{escape data.address2}</address2>
+               <city>#{escape data.city}</city>
+               <state>#{escape data.state}</state>
                <zip>#{data.zipCode}</zip>
-               <country>#{data.country}</country>
+               <country>#{escacpe data.country}</country>

                <ip_address>#{data.ipAddress}</ip_address>
              </billing_info>
            </account>"""

+
+  getAccount accountCode, (err, res) ->
+    if err
+      recurlyPost 'accounts', xmlWithBilling, (err, res) ->
+        if err
+          callback err, {}
+        else
+          callback no, res
+    else
+      if res.username
+        recurlyPut "accounts/#{accountCode}", xml, (err, res) ->
+          if err
+            callback err, {}
+          else
+            setBilling accountCode, data, (err, res) ->
+              if err
+                callback err, {}
+              else
+                callback no, res
+      else
+        recurlyPost 'accounts', xmlWithBilling, (err, res) ->
+          if err
+            callback err, {}
+          else
+            callback no, res
+
+setAccount = (accountCode, data, callback) ->
+  # See https://docs.recurly.com/api/account for XML format
+  xml = """<?xml version="1.0" encoding="UTF-8"?>
+           <account>
+             <account_code>#{accountCode}</account_code>
+             <username>#{data.username}</username>
+             <email>#{data.email}</email>
+             <first_name>#{escape data.firstName}</first_name>
+             <last_name>#{escape data.lastName}</last_name>
+           </account>
+  """
   getAccount accountCode, (err, res) ->
     if err
       recurlyPost 'accounts', xml, (err, res) ->
@@ -661,38 +747,6 @@
           else
             callback no, res

-setAccount = (accountCode, data, callback) ->
-  # See https://docs.recurly.com/api/account for XML format
-  xml = """<?xml version="1.0" encoding="UTF-8"?>
-           <account>
-             <account_code>#{accountCode}</account_code>
-             <username>#{data.username}</username>
-             <email>#{data.email}</email>
-             <first_name>#{data.firstName}</first_name>
-             <last_name>#{data.lastName}</last_name>
-           </account>"""
-
-  getAccount accountCode, (err, res) ->
-    if err
-      recurlyPost 'accounts', xml, (err, res) ->
-        if err
-          callback err, {}
-        else
-          callback no, res
-    else
-      if res.username
-        recurlyPut "accounts/#{accountCode}", xml, (err, res) ->
-          if err
-            callback err, {}
-          else
-            callback no, res
-      else
-        recurlyPost 'accounts', xml, (err, res) ->
-          if err
-            callback err, {}
-          else
-            callback no, res
-
 module.exports =
   getPlans                    : getPlans
   getPlanInfo                 : getPlanInfo
@@ -704,6 +758,8 @@
   cancelUserSubscription      : cancelUserSubscription
   reactivateUserSubscription  : reactivateUserSubscription
   getAccount                  : getAccount
+  setBilling                  : setBilling
+  getBilling                  : getBilling
   setAccount                  : setAccount
   setAccountWithBilling       : setAccountWithBilling
   deleteAccount               : deleteAccount


More clear tests

More clear tests

--- test/src/vector-spec.coffee
+++ test/src/vector-spec.coffee
@@ -24,45 +24,54 @@
         expect(@sut.bearing()).to.equal -1 * Math.PI / 2

-  describe '->velocity', ->
+  describe '->magnitude', ->
     describe 'when x is 1 and y is 0', ->
       beforeEach ->
         @sut = new Vector 1, 0

       it 'should return 1', ->
-        expect(@sut.velocity()).to.equal 1
+        expect(@sut.magnitude()).to.equal 1

     describe 'when x is 2 and y is 0', ->
       beforeEach ->
         @sut = new Vector 2, 0

       it 'should return 2', ->
-        expect(@sut.velocity()).to.equal 2
+        expect(@sut.magnitude()).to.equal 2

     describe 'when x is 0 and y is 1', ->
       beforeEach ->
         @sut = new Vector 0, 1

       it 'should return 1', ->
-        expect(@sut.velocity()).to.equal 1
+        expect(@sut.magnitude()).to.equal 1

     describe 'when x is 0 and y is 2', ->
       beforeEach ->
         @sut = new Vector 0, 2

       it 'should return 2', ->
-        expect(@sut.velocity()).to.equal 2
+        expect(@sut.magnitude()).to.equal 2

     describe 'when x is 1 and y is 1', ->
       beforeEach ->
         @sut = new Vector 1, 1

       it 'should return the sqrt of 2', ->
-        expect(@sut.velocity()).to.equal Math.sqrt(2)
+        expect(@sut.magnitude()).to.equal Math.sqrt(2)

+    describe 'when x is 0 and y is -1', ->
+      beforeEach ->
+        @sut = new Vector 0, -1
+
+      it 'should return 1', ->
+        expect(@sut.magnitude()).to.equal 1
+
+  describe '->velocity', ->
     describe 'when x is 0 and y is -1', ->
       beforeEach ->
         @sut = new Vector 0, -1

       it 'should return -1', ->
         expect(@sut.velocity()).to.equal -1
+


Fix description of test

Fix description of test

--- test/testJsonpointer.coffee
+++ test/testJsonpointer.coffee
@@ -8,7 +8,7 @@

 describe "jsonpointer", () ->

-  describe "#get", () ->
+  describe ".get()", () ->

     it "should evaluate spec examples", () ->
       target =


fixed per-layout locales

fixed per-layout locales

it broke when starting from a clean config

--- client/keymap.coffee
+++ client/keymap.coffee
@@ -168,7 +168,7 @@

 bq = null # effective ` map as a dictionary, kept in sync with the prefs
 do updateBQ = ->
-  bq = {}; lc = prefs.kbdLocale(); l = layouts[lc]; n = l[0].length
+  bq = {}; lc = prefs.kbdLocale() || 'US'; l = layouts[lc]; n = l[0].length
   for i in [0..1] then for j in [0...n] then bq[l[i][j]] ?= l[2 + i][j]
   if s = prefs.prefixMaps()[lc] then for i in [0...s.length] by 2 then x = s[i]; y = s[i + 1]; bq[x] = y
   return


testfix: innerHTML needs to define a getter, else we get 'undefined' prepended to everything

--- test/fakeDocument.coffee
+++ test/fakeDocument.coffee
@@ -1,7 +1,7 @@
 HtmlParser = require './htmlparser'
 JadeRt = require '../lib/runtime'
 Jade = require '../lib/jade'
-
+Util = require 'util'

 class Document
   createElement: (tag) ->
@@ -69,7 +69,16 @@
     @attributes = []
     @classList = new ClassList()

+    @__defineGetter__ 'innerHTML', (html) => '__APPEND__'
     @__defineSetter__ 'innerHTML', (html) =>
+      # The 'value' of the innerHTML prop is '__APPEND__', so if we see that, we know it's a "+=" op
+      # Thus, when we do .innerHTML += "html", JS will prepend '__APPEND__' to "html" and call this setter method
+      if html.indexOf('__APPEND__') == 0
+        # Element.innerHTML += XXX
+        html = html.slice('__APPEND__'.length)
+      else
+        # Element.innerHTML = XXX
+        @childNodes = []
       stack = []
       parent = @
       HtmlParser.HTMLParser html,
@@ -90,6 +99,7 @@
           parent = stack.pop()

         chars: (text) ->
+          return if (typeof text != 'string' || text.length == 0)
           parent.appendChild(new TextNode(text))

         comment: (text) ->


don't forget to boot julia

don't forget to boot julia

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -75,6 +75,7 @@
     {
       wordRegExp:  new RegExp(words.wordRegex, "g")
       getSuggestionForWord: (editor, text, range) =>
+        require('../connection').boot()
         {
           range: range
           callback: => @gotoSymbol(text, range)


Test Methods from Extra

Test Methods from Extra

--- test/unit/model_spec.coffee
+++ test/unit/model_spec.coffee
@@ -228,3 +228,14 @@

       tick()

+  # ## Extras
+  describe "Additional Methods", ->
+    it 'should be available as functions', inject (Collection) ->
+      Specials = Collection.new "Specials", {},
+        calculateStuff: (data) ->
+          if _.isArray(data)
+            _.reduce data, ((memo, val) -> memo + +val.count), 0
+          else 42
+
+      sum = Specials.calculateStuff [{count: 3}, {count: 2}]
+      expect(sum).to.eql 5


minor changes in the plumbing

minor changes in the plumbing

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -49,9 +49,9 @@
     level = nconf.get(levelKey) or "debug"
     filter = (string) -> string.replace "\n", ""
     writer = (data) -> logger.log level, filter data
-    options = stream: write: writer
-    options.format = format
-    connect.logger options
+    assert options = stream: write: writer
+    assert options.format = format
+    return connect.logger options

 # This middleware is really a wrapper around the `Connect` session
 # middleware. The reason it wraps it is to automatically configure


Add IE a bit more time

Add IE a bit more time

--- spec/Initialization.coffee
+++ spec/Initialization.coffee
@@ -11,7 +11,7 @@
       doc = iframe.contentDocument
       setTimeout ->
         done()
-      , 1000
+      , 3000
   it 'should start with the main screen', ->
     chai.expect(win.location.hash).to.equal ''
   it 'should have created the IndexedDB database', (done) ->


De-pending repeated typing test

De-pending repeated typing test

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -173,19 +173,25 @@
       expect(editorView).not.toHaveClass 'insert-mode'
       expect(editorView).not.toHaveClass 'visual-mode'

-    xit "allows undoing an entire batch of typing", ->
-      type('a')
-      type('b')
-      type('c')
-      editorView.trigger('vim-mode:activate-command-mode')
-      editorView.trigger('vim-mode:insert-after')
-      type('d')
-      type('e')
-      type('f')
-      editorView.trigger('vim-mode:activate-command-mode')
+    it "allows undoing an entire batch of typing", ->
+      keydown 'i'
+      editor.setText("abc")
+      keydown 'escape'
+      keydown 'a'
+      editor.setText("abcdef")
+      keydown 'escape'
       expect(editor.getText()).toBe "abcdef"
-      editorView.trigger('core:undo')
+      keydown 'u'
       expect(editor.getText()).toBe "abc"
+      keydown 'u'
+      expect(editor.getText()).toBe ""
+
+    it "allows repeating typing", ->
+      keydown 'i'
+      editor.setText("abc")
+      keydown 'escape'
+      keydown '.'
+      editor.setText("ababcc")

   describe "visual-mode", ->
     beforeEach -> keydown('v')


Added unit tests to check returns when find's no match

--- specs/somewhereSpec.coffee
+++ specs/somewhereSpec.coffee
@@ -29,6 +29,16 @@
     item = db.save 'users', user
     items = db.find 'users', { name: 'David' }
     expect(items.length).to.equal 2
+
+  it "should return an empty object if no match on find one", ->
+    item = db.findOne 'users', { name: 'Paul' }
+    expect(typeof item).to.equal 'object'
+    expect(Object.keys(item).length).to.equal 0
+
+  it "should return an empty array if no match on find all", ->
+    items = db.find 'users', { name: 'Paul' }
+    expect(items.push).to.be.a 'function'
+    expect(items.length).to.equal 0

   it "should return a pure object when find one", ->
     item = db.findOne 'users', { username: 'dreyacosta' }


envelope に user が含まれていないとき ( { room: roomId} での直接投稿)  などでエラーがでていたので修正。

--- src/adapters/direct.coffee
+++ src/adapters/direct.coffee
@@ -22,11 +22,13 @@
       else if typeof(string) == 'object'
         @bot.send envelope, JSON.stringify(string)
       else
-        @robot.logger.debug "Sending strings to user: " + envelope.user.name
+        if envelope.user?
+          @robot.logger.debug "Sending strings to user: " + envelope.user.name
         @bot.send envelope, string

   reply: (envelope, strings...) ->
-    @send envelope, strings.map((str) -> "@#{envelope.user.name} #{str}")...
+    if envelope.user?
+      @send envelope, strings.map((str) -> "@#{envelope.user.name} #{str}")...

   topic: (envelope, strings...) ->
     @bot.topic envelope, strings.join(',')


marker with githublink

marker with githublink

--- app/assets/javascripts/map.js.coffee
+++ app/assets/javascripts/map.js.coffee
@@ -24,7 +24,7 @@
 addMarker = (place, github_id) ->

   infowindow = new google.maps.InfoWindow
-    content: github_id
+    content: 'Github: ' + github_id + ''

   marker = new google.maps.Marker
     map: map,


Add filetype icons to ss assets

Add filetype icons to ss assets

--- app/assets/volume/spreadsheet.coffee
+++ app/assets/volume/spreadsheet.coffee
@@ -340,7 +340,10 @@
           else
             a = a[n]
             b += '_' + n
-          generateCell(row, 'asset', a.name, id + '-asset_' + b)
+          cell = generateCell(row, 'asset', a.name, id + '-asset_' + b)
+          icon = cell.insertBefore(document.createElement('img'), cell.firstChild)
+          icon.src = a.icon
+          icon.className = "hint-format-" + a.format.extension
           generateCell(row, 'classification', a.classification, id + '-class_' + b)
           generateCell(row, 'excerpt', a.excerpt, id + '-excerpt_' + b)
           return


Add more logging and don't delete temp files

--- app/coffee/S3PersistorManager.coffee
+++ app/coffee/S3PersistorManager.coffee
@@ -49,9 +49,9 @@
 			if res.statusCode != 200
 				logger.err bucketName:bucketName, key:key, fsPath:fsPath, "non 200 response from s3 putting file"
 				return callback("non 200 response from s3 on put file")
-			LocalFileWriter.deleteFile fsPath, (err)->
-				logger.log res:res,  bucketName:bucketName, key:key, fsPath:fsPath,"file uploaded to s3"
-				callback(err)
+			#LocalFileWriter.deleteFile fsPath, (err)->
+			logger.log res:res,  bucketName:bucketName, key:key, fsPath:fsPath,"file uploaded to s3"
+			callback(err)
 		putEventEmiter.on "error", (err)->
 			logger.err err:err,  bucketName:bucketName, key:key, fsPath:fsPath, "error emmited on put of file"
 			callback err
@@ -100,7 +100,12 @@
 				logger.err err:err, res:res, bucketName:bucketName, key:key, "something went wrong deleting file in aws"
 			callback(err)

-	deleteDirectory: (bucketName, key, callback)->
+	deleteDirectory: (bucketName, key, _callback)->
+		callback = (args...) ->
+			logger.log key: key, bucketName: bucketName, "calling delete callback"
+			_callback(args...)
+
+		logger.log key: key, bucketName: bucketName, "deleting directory"
 		s3Client = knox.createClient
 			key: settings.filestore.s3.key
 			secret: settings.filestore.s3.secret


highlight dates even if day is only one digit (no leading 0)

--- app/scripts/filters/highlight.coffee
+++ app/scripts/filters/highlight.coffee
@@ -39,16 +39,18 @@
                 matches = [ date.toString 'yyyy' ]

         _.map matches, (match) =>
-            match = do match.toLowerCase
-            i = 0
-            shift = 0
-            while i > -1
-                i = if i is 0 then lCased.indexOf match else lCased.indexOf match, i + 1
-                if i > 0
-                    tmp = str
-                    str = (tmp.substr 0, i + shift) + ""
-                    str += (tmp.substr i + shift, match.length) + ""
-                    str += tmp.substr (i + match.length + shift), tmp.length
-                    shift += 32
+            match = if match.startsWith '0' then [(match.substr 1, match.length), match] else [match]
+            _.map match, (match) =>
+                match = do match.toLowerCase
+                i = 0
+                shift = 0
+                while i > -1
+                    i = if i is 0 then lCased.indexOf match else lCased.indexOf match, i + 1
+                    if i > 0
+                        tmp = str
+                        str = (tmp.substr 0, i + shift) + ""
+                        str += (tmp.substr i + shift, match.length) + ""
+                        str += tmp.substr (i + match.length + shift), tmp.length
+                        shift += 32

         str


test for rendering auction results nav on artist page

--- apps/artist/test/client/index.coffee
+++ apps/artist/test/client/index.coffee
@@ -2,6 +2,7 @@
 benv          = require 'benv'
 Backbone      = require 'backbone'
 sinon         = require 'sinon'
+AuctionLots   = require '../../../../collections/auction_lots'
 Artist        = require '../../../../models/artist'
 _             = require 'underscore'
 { resolve }   = require 'path'
@@ -113,3 +114,15 @@
       @view.renderRelatedArtist new Artist(fabricate 'artist', name: 'Andy Foobar'), 0, 'Artists'
       _.last(Backbone.sync.args)[2].success [fabricate 'artwork', title: 'Andy Foobar Skull']
       _.last(@FillwidthView.args)[0].collection.first().get('title').should.equal 'Andy Foobar Skull'
+
+  describe "#setupAuctionResults", ->
+
+    it "hides artist sections if no auction results", ->
+      @view.setupAuctionResults()
+      _.last(Backbone.sync.args)[2].success()
+      @view.$el.html().should.not.include 'Auction'
+
+    it "keeps artist sections if auction results", ->
+      @view.setupAuctionResults()
+      _.last(Backbone.sync.args)[2].success new AuctionLots(_.times(10, (-> fabricate 'auction_result')), { state: { totalRecords: 10 }})
+      @view.$el.html().should.include 'Auction'


fix specs

fix specs

--- spec/notifications-count-spec.coffee
+++ spec/notifications-count-spec.coffee
@@ -51,7 +51,7 @@
       generateFakeFetchResponses()

     it "will add the new-notification class for as long as the animation", ->
-      notificationsCountNumber = notificationsCountContainer.firstChild
+      notificationsCountNumber = notificationsCountContainer.firstChild.firstChild
       expect(notificationsCountNumber).not.toHaveClass 'new-notification'
       atom.notifications.addInfo('A message')
       expect(notificationsCountNumber).toHaveClass 'new-notification'


fix test runner

fix test runner

--- kit/cake_builder.coffee
+++ kit/cake_builder.coffee
@@ -54,13 +54,17 @@
         'test'
         'Run test runner.'
         (opts) ->
-            args = opts.cli and [
-                '--single-run',
-                '--no-auto-watch',
-                # Travis supports running a real browser (Firefox) with a virtual screen.
-                '--browsers', 'Firefox'
-            ] or []
-            spawn 'karma', ['start', 'karma.conf.js'].concat(args)
+            if opts.cli
+                invoke 'build'
+                .then ->
+                    spawn 'karma', ['start', 'karma.conf.js'].concat([
+                        '--single-run',
+                        '--no-auto-watch',
+                        # Travis supports running a real browser (Firefox) with a virtual screen.
+                        '--browsers', 'Firefox'
+                    ])
+            else
+                spawn 'karma', ['start', 'karma.conf.js']
     ]
     [
         'coffeelint'


Add switch_url to Intercom

Add switch_url to Intercom

--- client/code/router/main.coffee
+++ client/code/router/main.coffee
@@ -139,6 +139,7 @@
               ts_datasets: 0
               tf_datasets: 0
               cb_datasets: 0
+              switch_url: "https://scraperwiki.com/switch/" + real.shortName

             _.each datasets, (dataset) ->
               date = moment(dataset.createdDate, 'YYYY-MM-DD HH:mm:ssZ').unix()


Don't set state

Don't set state

--- lib/BehaviorTree.coffee
+++ lib/BehaviorTree.coffee
@@ -261,8 +261,9 @@
       # Straight-up value returned
       return if choice.state is State.ABORTED
       return if typeof val?.isComplete is 'function' and not val.isComplete()
-      log.values "#{@name or @id} #{choice} resulted directly in #{typeof val} %s", val
-      choice.set 'data', val if isActive choice
+      unless typeof val?.isComplete is 'function'
+        log.values "#{@name or @id} #{choice} resulted directly in #{typeof val} %s", val
+        choice.set 'data', val if isActive choice
       choice.state = State.FULFILLED if isActive choice
       @resolve node.id, sourcePath
     catch e


modified file spinner

modified file spinner

--- src/FileManager/FileDirective.coffee
+++ src/FileManager/FileDirective.coffee
@@ -14,7 +14,7 @@
               draggable="true">

-
+

{{ file.metadata.name }}

{{ file.metadata.name |ellipsis:20 }}


Don't fetch missing log parts if test haven't started yet

--- assets/scripts/app/models/log.coffee
+++ assets/scripts/app/models/log.coffee
@@ -10,7 +10,8 @@
     @setParts()

   fetchMissingParts: (partNumbers, after) ->
-    # TODO: don't download if job is not running and log is finalized
+    return if @get('notStarted')
+
     data = {}
     data['part_numbers'] = partNumbers if partNumbers
     data['after'] = after if after


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/core_object.coffee
+++ test/core_object.coffee
@@ -42,7 +42,8 @@
   equals o.test(), "my_val"
   equals o.test2, "cool"

-test "#include multiple"
+test "#include multiple", ->
+

 module()


options

options

--- app/server.coffee
+++ app/server.coffee
@@ -329,18 +329,21 @@
     if settings.get('type') == 'lead'
       if response.exceptions.length > 0
         return Q.reject new ServerError(response.exceptions)
-      _.map _.flatten(_.values(response.results)), ({name, start, step, values}) ->
+      _.map _.flatten(_.values(response.results)), ({name, start, step, values, options}) ->
         if step?
           target: name
           datapoints: _.map values, (v, i) ->
             [start + step * i, v]
+          options: options
         else
           target: name
           datapoints: values
-    else
-      _.map response, ({target, datapoints}) ->
+          options: options
+    else
+      _.map response, ({target, datapoints, options}) ->
         target: target
         datapoints: _.map datapoints, ([v, ts]) -> [ts, v]
+        options: options

   # returns a promise
   get_data: (params) ->


fixed tests

fixed tests

--- test/engine.test.coffee
+++ test/engine.test.coffee
@@ -46,10 +46,10 @@
         do @engine?[ symbols.bDispose ]

     it 'instance passes the `instanceof`', ->
-        expect(Engine @validName).to.be.an.instanceof Engine
+        expect(Engine()).to.be.an.instanceof Engine

     it 'passes `Engine.prototype.isPrototypeOf` check', ->
-        expect(Engine.prototype.isPrototypeOf Engine @validName).to.be.true
+        expect(Engine.prototype.isPrototypeOf Engine()).to.be.true

     describe 'instance.getNodeType()', ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -36,5 +36,8 @@
     player.update()
     equals player.I.alpha, 0.2

+  2.times ->
+    player.
+

 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -42,8 +42,8 @@
     duration: 2

   equals boss.I.flickerAlpha, 0.5
-  equals player.I.flickerFrequency, 3
-  equals player.I.flickerDuration, 30
+  equals boss.I.flickerFrequency, 3
+  equals boss.I.flickerDuration, 2

 test 'it should change the alpha value when the flicker duration has elapsed', ->
   player = GameObject


remove step from game object default events


removed extra newline in "li sh"

removed extra newline in "li sh"

--- code/instance.coffee
+++ code/instance.coffee
@@ -105,8 +105,8 @@
         if tries
           console.log 'Trying ssh again'
         ssh = spawn 'ssh', args
-        ssh.stdout.on 'data', (data) -> console.log 'ssh: ' + data.toString('ascii')
-        ssh.stderr.on 'data', (data) -> console.log 'ssh: ' + data.toString('ascii')
+        ssh.stdout.on 'data', (data) -> process.stdout.write 'ssh: ' + data.toString('ascii')
+        ssh.stderr.on 'data', (data) -> process.stderr.write 'ssh: ' + data.toString('ascii')
         ssh.on 'exit', (rc) ->
           if rc == 255
             tries += 1


set identities only on ssh & scp - solves auth problems when you have a lot of keys

--- code/instance.coffee
+++ code/instance.coffee
@@ -19,8 +19,7 @@
     @state = state if state?
     @ip_address = ip if ip?

-  # Create a server ready to run. Somewhere on the internet.
-  # Servers are created in the "stopped" state.
+  # Create a server ready to run. Somewhere on the internet.  # Servers are created in the "stopped" state.
   @create: (config, callback) ->
     @config = new cf.Config config

@@ -98,7 +97,7 @@
   _ssh: (key, command, callback) ->
     @_wait_for_sshd =>
       args = [
-        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-i', key, "root@#{@ip_address}"]
+        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-i', key, "root@#{@ip_address}", '-o', 'IdentitiesOnly=yes' ]
       args = args.concat(command.split ' ')

       ssh = spawn 'ssh', args
@@ -111,7 +110,7 @@
   _scp: (key, files, callback) ->
     @_wait_for_sshd =>
       args = [
-        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-i', key ]
+        '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', 'o', 'IdentitiesOnly=yes', '-i', key ]
       args = args.concat files
       args.push "root@#{@ip_address}:/root"


Use the query schema directly, instead of reffing it

--- coffee/schema.coffee
+++ coffee/schema.coffee
@@ -1,16 +1,8 @@
 module.exports =
   id: "https://github.com/automathew/patchboard-spec"
   properties:
+
     schema: {$ref: "http://json-schema.org/draft-03/schema#"}
-    query:
-      id: "#query"
-      # Extend the full JSON schema, so we can constrain the legal
-      # values of "type".
-      extends: {$ref: "http://json-schema.org/draft-03/schema#"}
-      properties:
-        type:
-          enum:
-            ["string", "number", "integer", "boolean"]

     paths:
       type: "object"
@@ -46,7 +38,14 @@
                   required: true
                   type: "string"
                   enum: ["GET", "PUT", "POST", "PATCH", "DELETE"]
-                query: {$ref: "#query"}
+                query:
+                  # Extend the full JSON schema, so we can constrain the legal
+                  # values of "type".
+                  extends: {$ref: "http://json-schema.org/draft-03/schema#"}
+                  properties:
+                    type:
+                      enum:
+                        ["string", "number", "integer", "boolean"]
                 request_schema:
                   type: "string"
                   description: "The name of the schema describing the request body"


add async tests

add async tests

--- test/mocha-given.coffee
+++ test/mocha-given.coffee
@@ -129,3 +129,19 @@
 				context "Inner block B", ->
 					Given -> @units = "cm"
 					Then  -> @label == "3 cm"
+
+	describe 'async testing', ->
+		Given -> @t = Date.now()
+
+		describe 'deferred Given', ->
+			Given (done) -> setTimeout done, 30
+			Then -> Date.now() - @t >= 30
+
+		describe 'deferred Then', ->
+			Then (done) ->
+				setTimeout(=>
+					diff = Date.now() - @t
+					if diff >= 30
+						done()
+					else done new Error "expected '#{diff}' to be bigger than '30'"
+				, 30)


remove scrollBy

remove scrollBy

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -452,10 +452,6 @@
           window.location.hash = ''
           window.location.hash = hash

-          # scroll up by width of nav bar
-          # FIXME - make this dynamic
-          window.scrollBy(0, -145)
-
       $target = $(window.location.hash)
       if $target.prop('tagName')?.toLowerCase() is 'iframe'
         $target.on('load', jumpToHash)


test(test/stub/server.coffee): Add OPTIONS processing

--- test/stub/server.coffee
+++ test/stub/server.coffee
@@ -1,5 +1,8 @@
 ###*
-# @file Server stub
+# @file Express server stub
+#
+# Start:
+# $ ../../node_modules/coffee-script/bin/coffee server.coffee
 ###

 require 'coffee-script/register'
@@ -8,6 +11,13 @@

 app = express()
 app.set 'port', process.env.PORT || 3333
+
+app.options '/machines', (req, res) ->
+  'use strict'
+  allow = ['OPTIONS', 'HEAD', 'GET']
+  res.setHeader 'Allow', allow.join ', '
+  res.setHeader 'Cache-Control', 'no-cache, no-store, must-revalidate'
+  res.status(204).end()

 app.get '/machines', (req, res) ->
   'use strict'


removing cloning for assets

removing cloning for assets

--- platform/src/gamEvolve.coffee
+++ platform/src/gamEvolve.coffee
@@ -181,16 +181,8 @@
       paramValue = defaultValue

     compiledParams[paramName] = GE.compileParameter(paramValue, constants, bindings)
-    value = compiledParams[paramName].get()
+    evaluatedParams[paramName] = compiledParams[paramName].get()

-    # Let undefined or non-serializable values go through.
-    # Otherwise there is no way for missing parameters, or native components to be passed.
-    # TODO: Could be source of silent errors
-    try
-      evaluatedParams[paramName] = GE.cloneData(value)
-    catch e
-      evaluatedParams[paramName] = value
-
   locals =
     params: evaluatedParams
     children: childNames
@@ -364,7 +356,7 @@
   return {
     get: ->
       [parent, key] = GE.getParentAndKey(constants.modelData, name.split("."))
-      return parent[key]
+      return GE.cloneData(parent[key])

     set: (x) ->
       # TODO: create patch directly, rather than by comparison
@@ -395,6 +387,7 @@
   if not name? then throw new Error("Asset evaluator requires a name")

   return {
+    # The getter does not clone the asset. This could be a potential source of problems.
     get: -> return constants.assets[name]
     set: (x) -> return new GE.NodeVisitorResult() # Noop. Cannot set asset
   }


Test server: emit event for IQs with unknown ID

--- test/test_server.coffee
+++ test/test_server.coffee
@@ -294,6 +294,7 @@
             when "iq"
                 stanza.attrs.should.have.property "from"
                 stanza.attrs.should.have.property "to"
+                to = stanza.attrs.to
                 stanza.attrs.should.have.property "type"
                 type = stanza.attrs.type
                 [ "get", "set", "result", "error"].should.include type
@@ -301,13 +302,15 @@
                 id = stanza.attrs.id

                 @iqs[type][id] = stanza
-                event = "got-iq-#{type}-#{id}"
+                eventId = "got-iq-#{type}-#{id}"
+                eventTo = "got-iq-#{type}-to-#{to}"

                 # If it's an error, throw an exception unless it was expected
-                if type is "error" and @listeners(event).length == 0
+                if type is "error" and @listeners(eventId).length == 0
                     throw new ErrorStanza stanza

-                @emit event, stanza
+                @emit eventId, stanza
+                @emit eventTo, stanza

                 # Handle disco queries
                 if type is "get"


lpthread default on linux

lpthread default on linux

--- src/build/configure.coffee
+++ src/build/configure.coffee
@@ -103,6 +103,7 @@
     # static: true
     linux:
       "lstdc++": 1
+      "lpthread": 1
     mac:
       "lc++": 1


Fixed name generation.

Fixed name generation.

--- src/coffee/services.coffee
+++ src/coffee/services.coffee
@@ -53,7 +53,7 @@
       currentName = baseName
       suffix = 1
       while L.contains currentNames, currentName
-        currentName = "#{ currentName } (#{ suffix++ })"
+        currentName = "#{ baseName } (#{ suffix++ })"
       return currentName

   # Provide a function for connecting to a mine by URL.


Implement Requirement-Validation on Choices

--- src/coffee/storylet.coffee
+++ src/coffee/storylet.coffee
@@ -2,17 +2,25 @@
   constructor: (@id, @title, @text, @choices, @frontFacingChoice) ->
     Object.freeze @

-  isVisibleWith: (qualities) -> frontFacingChoice?.isVisibleWith qualities
+  isVisibleWith: (qualityLibrary) -> frontFacingChoice?.isVisibleWith qualityLibrary

 class Choice
   constructor: (@title, @text, @visibleReqs, @activeReqs, @next) ->

-  isVisibleWith: (qualities) ->
+  reqsAreMet = (reqs, qualityLibrary) ->
+    for qualityName, predicate of reqs
+      quality = qualityLibrary.resolve qualityName
+      return false unless predicate quality
+    return true

-  isActiveWith: (qualities) ->
+  isVisibleWith: (qualityLibrary) ->
+    reqsAreMet @visibleReqs, qualityLibrary

-angular.module 'qbn.storylet', []
-  .factory 'storyletLibrary', () ->
+  isActiveWith: (qualityLibrary) ->
+    reqsAreMet @activeReqs, qualityLibrary
+
+angular.module 'qbn.storylet', ['qbn.quality']
+  .factory 'storyletLibrary', (qualityLibrary) ->
     library = {}
     api =
       register: (args...) ->
@@ -24,8 +32,8 @@
           q
         else
           library[q.toString()]
-      filterVisible: (qualities) ->
-        storylet for _, storylet of library when storylet.isVisibleWith qualities
+      getAllFrontFacing: () ->
+        storylet for _, storylet of library when storylet.isVisibleWith qualityLibrary
     return Object.freeze api
   .factory 'choiceFactory', () ->
     (args...) -> new Choice args...


Marker type changes with selection. Fixes #2.

--- scripts/controllers/Classifier.coffee
+++ scripts/controllers/Classifier.coffee
@@ -94,8 +94,7 @@

 		selectedMarker = (m for m in @picker.markers when m.selected)[0]
 		if selectedMarker
-			@speciesButtons.removeClass 'active'
-			@speciesButtons.filter("[value='#{selectedMarker.marking.species}']").addClass 'active'
+			@speciesButtons.filter("[value='#{selectedMarker.marking.species}']").trigger 'click'

 		@speciesButtons.find('.count').html '0'
 		for marking in @classification.markings().all()


adding some more tests

adding some more tests

--- test/giphy.spec.coffee
+++ test/giphy.spec.coffee
@@ -79,12 +79,52 @@
       should.exist giphyPluginInstance
       should.exist giphyPluginInstance.api

+    it 'should be able to access the extend utility', ->
+      should.exist extend
+
+    it 'should be able to access the merge utility', ->
+      should.exist merge
+
     it 'can simulate environment variable values', ->
       process.env.TESTING = 'testing'
       process.env.TESTING.should.eql 'testing'

     it 'does not persist environment variable changes', ->
       should.not.exist process.env.TESTING
+
+  describe 'extend utility', ->
+    it 'creates an empty object for null input', ->
+      result = extend()
+      should.exist result
+      result.should.eql { }
+
+    it 'adds properties to an empty object', ->
+      result = extend null, { a: 1 }
+      should.exist result
+      result.should.eql { a: 1 }
+
+    it 'adds properties to a non-empty object', ->
+      result = extend { b: 2 }, { a: 1 }
+      should.exist result
+      result.should.eql { a: 1, b: 2 }
+
+    it 'overwrites properties on a non-empty object', ->
+      result = extend { a: 2 }, { a: 1 }
+      should.exist result
+      result.should.eql { a: 1 }
+
+    it 'retains empty string properties', ->
+      result = extend { }, { a: '' }
+      should.exist result
+      result.should.eql { a: '' }
+
+    it 'ignores null properties', ->
+      result = extend { a: 1 }, { b: null }
+      should.exist result
+      result.should.eql { a: 1 }
+
+  describe 'merge utility', ->
+    it 'has no tests yet'

   describe 'hubot script', ->
     giphyPluginInstance = null
@@ -578,6 +618,13 @@
         @giphy.getUriFromResultData.should.have.been.calledWith sampleData

     describe '.getRandomUri', ->
+      it 'gets a result without args', ->
+        state = { }
+        @fakes.stub @giphy.api, 'random'
+        @giphy.getRandomUri state
+        @giphy.api.random.should.have.been.called.once
+        @giphy.api.random.should.have.been.calledWith { }, sinon.match.func
+
       it 'gets a result using args', ->
         state = { args: 'testing' }
         @fakes.stub @giphy.api, 'random'


Tests: Added openTerminal test.

Tests: Added openTerminal test.

--- tests/src/ide/ide.coffee
+++ tests/src/ide/ide.coffee
@@ -214,3 +214,16 @@
         .waitForElementNotVisible  workspaceSelector, 20000
         .end()

+
+  openTerminal: (browser) ->
+
+    helpers.beginTest(browser)
+
+    browser
+      .waitForElementVisible     '.visible-tab-handle.plus', 20000
+      .click                     '.visible-tab-handle.plus span.icon'
+      .pause 2000
+      .waitForElementVisible     '.kdlistview-contextmenu li.new-terminal', 20000
+      .click                     '.kdlistview-contextmenu li.new-terminal'
+      .waitForElementVisible     '.terminal-pane .console span.outlined', 20000
+      .end()


* update test

* update test

--- test/index-test.coffee
+++ test/index-test.coffee
@@ -117,9 +117,9 @@
         describe '#' + k, ->
           it 'should log with level', ->
             log.level = v
-            log[k] '${status}: hiMsg'
+            log[k] 'hiMsg', 'ok'
             expect(writeFn).to.have.callCount 1
-            expect(writeFn.firstCall.args[0]).to.be.include 'hiMsg'
+            expect(writeFn.firstCall.args[0]).to.be.include 'hiMsg ok'
             expect(writeFn.firstCall.args[0]).to.be.include k
       )(k,v)


tests for remaining cipher functions assets

--- tests/test_assets.coffee
+++ tests/test_assets.coffee
@@ -99,26 +99,24 @@
         ])
         expect(decrypt(ct, "aaa111", params)).to.equals("This page is a demo of the Stanford Javascript Crypto Library.")

-    it "sjcl aes-gcm", ->
-        window = {}
-        window.window = window
+    window = {}
+    window.window = window

-        s =
-            ArrayBuffer: Object.freeze(ArrayBuffer)
-            Buffer: Object.freeze(Buffer)
-            Uint8Array: Object.freeze(Uint8Array)
-            console: Object.freeze(console)
-            window: Object.freeze(window)
-            Uint32Array: Object.freeze(Uint32Array)
+    esandbox =
+        ArrayBuffer: Object.freeze(ArrayBuffer)
+        Buffer: Object.freeze(Buffer)
+        Uint8Array: Object.freeze(Uint8Array)
+        console: Object.freeze(console)
+        window: Object.freeze(window)
+        Uint32Array: Object.freeze(Uint32Array)

-        decrypt = eval_file("../lib/assets/decode/aes-gcm.js", s)
-        encrypt = eval_file("../lib/assets/encode/aes-gcm.js", s)
+    it "sjcl aes-ccm", ->
+        decrypt = eval_file("../lib/assets/decode/aes-ccm.js", esandbox)
+        encrypt = eval_file("../lib/assets/encode/aes-ccm.js", esandbox)

         message = gen(5)

         params =
-            cipher: "aes"
-            mode: "gcm"
             iter: 1000
             ks: 128
             ts: 128
@@ -126,3 +124,36 @@

         data = encrypt(message, "aaa111", params)
         expect(decrypt(data, "aaa111", params)).to.equals(message)
+
+        data = encrypt(message, "aaa111", params)
+        expect(decrypt(data, "aaa111", params)).to.equals(message)
+
+    it "sjcl aes-gcm", ->
+        decrypt = eval_file("../lib/assets/decode/aes-gcm.js", esandbox)
+        encrypt = eval_file("../lib/assets/encode/aes-gcm.js", esandbox)
+
+        message = gen(5)
+
+        params =
+            iter: 1000
+            ks: 128
+            ts: 128
+            auth: "zzzzzz"
+
+        data = encrypt(message, "aaa111", params)
+        expect(decrypt(data, "aaa111", params)).to.equals(message)
+
+    it "sjcl aes-ocb2", ->
+        decrypt = eval_file("../lib/assets/decode/aes-ocb2.js", esandbox)
+        encrypt = eval_file("../lib/assets/encode/aes-ocb2.js", esandbox)
+
+        message = gen(5)
+
+        params =
+            iter: 1000
+            ks: 128
+            ts: 128
+            auth: "zzzzzz"
+
+        data = encrypt(message, "aaa111", params)
+        expect(decrypt(data, "aaa111", params)).to.equals(message)


Matches `\[` only it is at the end of line

Fixes #30

--- lib/latexer-hook.coffee
+++ lib/latexer-hook.coffee
@@ -49,7 +49,7 @@
       #If it does, try to find the closing item, and if that doesn't exist put it in.
       else if pos[0]>1
         previousLine = @editor.lineTextForBufferRow(pos[0]-1)
-        if (match = @beginRex.exec(previousLine)) or (match = /\\\[/.exec(previousLine))
+        if (match = @beginRex.exec(previousLine)) or (match = /\\\[\h*?$/.exec(previousLine))
           lineCount = @editor.getLineCount()
           remainingText = @editor.getTextInBufferRange([[pos[0],0],[lineCount+1,0]])
           if match[0] is "\\["


Fix multiple collections in single ShareJS doc

--- src/backbone.sharedcollection.coffee
+++ src/backbone.sharedcollection.coffee
@@ -103,13 +103,15 @@
     if @_syncDoc.type.name isnt "json"
       throw new Error "The ShareJS document type must be 'json', not '#{ @_syncDoc.type.name }'"

-
-    if @_syncDoc.created
-      @_initSyncDoc callbackWrapper
-    else
-      @_loadModelsFromSyncDoc callbackWrapper
-
     @_bindSendOperations()
+
+    # TODO: Clean up before this is the pyramid of doom
+    @_initSyncDoc (err) =>
+      return callbackWrapper err if err
+      @_initCollection (err) =>
+        return callbackWrapper err if err
+        @_loadModelsFromSyncDoc callbackWrapper
+

@@ -125,16 +127,30 @@

   _flushAddingQueue: ->
     while model = @_addingQueue.shift()
-      console.log "removing #{ model.get "name" } from queue"
       @add model

   _initSyncDoc: (cb) ->
-    log "Creating new sync doc with #{ @collectionId }"
-    ob = {}
-    ob[@collectionId] = {}
+
+    if not @_syncDoc.created
+      return cb()
+
+    if @_syncDoc.snapshot
+      return cb()
+
+    log "Creating new sync doc"
     @_syncDoc.submitOp [
       p: []
-      oi: ob
+      oi: {}
+    ], cb
+
+  _initCollection: (cb) ->
+    if @_syncDoc.snapshot[@collectionId]
+      return cb()
+
+    log "Adding new collection to syncdoc: #{ @collectionId }"
+    @_syncDoc.submitOp [
+      p: [@collectionId]
+      oi: {}
     ], cb

   _loadModelsFromSyncDoc: (cb=->) ->
@@ -186,8 +202,9 @@
       model.set id: SharedCollection.generateGUID()

     # Just ignore readds
-    if @_syncDoc.snapshot[this.collectionId][model.id]
+    if @_syncDoc.snapshot[this.collectionId]?[model.id]
       return
+

     log "SEND ADD #{ model.id }: #{ JSON.stringify model.toJSON() }"


better detection - unlock / unlocked / get

--- src/scripts/achievement_unlocked.coffee
+++ src/scripts/achievement_unlocked.coffee
@@ -2,9 +2,9 @@
 # achievement get <achievement> [achiever's gravatar email] - life goals are in reach.
 #
 module.exports = (robot) ->
-  robot.hear /achievement get (.+?)((?:\s*)[^@\s]+@[^@\s]+)?$/i, (msg) ->
-    caption = msg.match[1]
-    email = msg.match[2]
+  robot.hear /achievement (get|unlock(ed)?) (.+?)(\s*[^@\s]+@[^@\s]+)?\s*$/i, (msg) ->
+    caption = msg.match[3]
+    email = msg.match[4]
     url = "http://achievement-unlocked.heroku.com/xbox/#{escape(caption)}.png"
     if email
       url += "?email=#{escape(email.trim())}.png"


Specifically exclude EPUB

Specifically exclude EPUB

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -85,7 +85,7 @@
               dataType: 'json'
             .done (response) =>
               validDownloads = response.downloads.filter (info) ->
-                 info.size > 0
+                 info.size > 0 and info.format isnt 'EPUB'
               @set('downloads', validDownloads)
               @set('isLatest', response.isLatest)
               @set('canPublish', response.canPublish)


:bug: Dispose bottom views on deactivate

--- lib/linter-views.coffee
+++ lib/linter-views.coffee
@@ -72,13 +72,13 @@

   # This method is called when we get the status-bar service
   attachBottom: (statusBar) ->
-    statusBar.addLeftTile
+    @linter.subscriptions.add statusBar.addLeftTile
       item: @_bottomTabFile,
       priority: -1001
-    statusBar.addLeftTile
+    @linter.subscriptions.add statusBar.addLeftTile
       item: @_bottomTabProject,
       priority: -1000
-    statusBar.addLeftTile
+    @linter.subscriptions.add statusBar.addLeftTile
       item: @_bottomStatus,
       priority: -999


:bug: Invalidate markers when a range is deleted

--- lib/linter-views.coffee
+++ lib/linter-views.coffee
@@ -100,7 +100,7 @@
     return unless activeEditor
     @messages.forEach (message) =>
       return unless message.currentFile
-      @markers.push marker = activeEditor.markBufferRange message.range, {invalidate: 'never'}
+      @markers.push marker = activeEditor.markBufferRange message.range, {invalidate: 'inside'}
       activeEditor.decorateMarker(
         marker, type: 'line-number', class: "linter-highlight #{message.class}"
       )


Add filter query to view

Add filter query to view

--- view/src/coffee/user.coffee
+++ view/src/coffee/user.coffee
@@ -1,24 +1,36 @@
 $(document).ready ->
-  params = fromURLParameter(location.search.slice(1))
-  id = params.userId
-  fetch("/api/user/#{id}/focal")
-    .then (res) -> res.json()
-    .then (json) -> renderFocalGraph(json)
-  fetch("/api/user/#{id}/iso")
-    .then (res) -> res.json()
-    .then (json) -> renderISOGraph(json)
-  fetch("/api/user/#{id}/f_number")
-    .then (res) -> res.json()
-    .then (json) -> renderFNumberGraph(json)
-  fetch("/api/user/#{id}/exposure")
-    .then (res) -> res.json()
-    .then (json) -> renderExposureGraph(json)
-  fetch("/api/user/#{id}/camera")
-    .then (res) -> res.json()
-    .then (json) -> renderCameraGraph(json)
-  fetch("/api/user/#{id}/lens")
-    .then (res) -> res.json()
-    .then (json) -> renderLensGraph(json)
+  params = new URLSearchParams(location.search.slice(1))
+  id = params.get('userId')
+  fetch("/api/user/#{id}/focal?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderFocalGraph(json)
+      clickFocal(chart)
+  fetch("/api/user/#{id}/iso?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderISOGraph(json)
+      clickISO(chart)
+  fetch("/api/user/#{id}/f_number?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderFNumberGraph(json)
+      clickFNumber(chart)
+  fetch("/api/user/#{id}/exposure?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderExposureGraph(json)
+      clickExposure(chart)
+  fetch("/api/user/#{id}/camera?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderCameraGraph(json)
+      clickCamera(chart)
+  fetch("/api/user/#{id}/lens?#{params.toString()}")
+    .then (res) -> res.json()
+    .then (json) ->
+      chart = renderLensGraph(json)
+      clickLens(chart)

 focalGroup = [10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, 150, 200, 250, 300, 400, 500, 600, 800, 1000, 1500, 2000]
 renderFocalGraph = (elems) ->
@@ -48,6 +60,12 @@
     options: _.merge(barOptions('Focal count (35mm equivalent)'), xScaleLabel('mm'))
   }

+clickFocal = (chart) ->
+  document.getElementById('focalChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    focal = points[0]._model.label
+    addParams({focal: focal})
+
 renderISOGraph = (elems) ->
   ctx = getCtx('isoChart')
   elemGroup = _.groupBy elems, (elem) ->
@@ -70,6 +88,12 @@
       }]
     options: barOptions('ISO count')
   }
+
+clickISO = (chart) ->
+  document.getElementById('isoChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    iso = points[0]._model.label
+    addParams({iso: iso})

 fNumberGroup = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 30]
 renderFNumberGraph = (elems) ->
@@ -82,7 +106,6 @@
     .dropWhile (x) -> x['count'] == 0
     .dropRightWhile (x) -> x['count'] == 0
     .value()
-  console.log(_elems)
   labels = _elems.map (elem) -> elem['fNumber']
   values = _elems.map (elem) -> elem['count']
   color = labels.map (fNumber) ->
@@ -99,6 +122,13 @@
       }]
     options: barOptions('F-Number count')
   }
+
+clickFNumber = (chart) ->
+  document.getElementById('fNumberChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    fNumber = points[0]._model.label
+    addParams({fNumber: fNumber})
+

 exposureGroup = [-30, -15, -8, -4, -2, 1, 2, 4, 8, 15, 30, 60, 125, 250, 500, 1000, 2000, 4000, 8000, 16000].reverse()
 renderExposureGraph = (elems) ->
@@ -124,10 +154,18 @@
     options: barOptions('Shutter speed count')
   }

+clickExposure = (chart) ->
+  document.getElementById('exposureChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    exposure = points[0]._model.label
+    addParams({exposure: exposure})
+
+cameraIds = []
 renderCameraGraph = (elems) ->
   ctx = getCtx('cameraChart')
   values = elems.map (elem) -> elem['count']
   labels = elems.map (elem) -> elem['camera']
+  cameraIds = elems.map (elem) -> elem['id']
   new Chart ctx, {
     type: 'pie'
     data:
@@ -138,10 +176,18 @@
       }]
   }

+clickCamera = (chart) ->
+  document.getElementById('cameraChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    camera = cameraIds[points[0]._index]
+    addParams({camera: camera})
+
+lensIds = []
 renderLensGraph = (elems) ->
   ctx = getCtx('lensChart')
   values = elems.map (elem) -> elem['count']
   labels = elems.map (elem) -> elem['lens']
+  lensIds = elems.map (elem) -> elem['id']
   new Chart ctx, {
     type: 'pie'
     data:
@@ -151,6 +197,12 @@
         backgroundColor: colorSet(0.5)
       }]
   }
+
+clickLens = (chart) ->
+  document.getElementById('lensChart').onclick = (evt) ->
+    points = chart.getElementsAtEvent(evt)
+    lens = lensIds[points[0]._index]
+    addParams({lens: lens})

 colorSet = (a) -> [
   "rgba(0, 65, 255, #{a})",
@@ -190,3 +242,11 @@
         display: true
         labelString: label
     }]
+
+addParams = (params) ->
+  p = new URLSearchParams(location.search.slice(1))
+  for k, v of params
+    if p.has(k)
+      p.delete(k)
+    p.append(k, v)
+  location.search = p.toString()


UI Update: Show anti-air cut-in outside stage2

--- views/battle-info-area.cjsx
+++ views/battle-info-area.cjsx
@@ -70,7 +70,7 @@

       </Tooltip>
     }>
-      {__ 'AA CI'}: {shipName}
+      {shipName}
     </OverlayTrigger>

 BattleDetailArea = React.createClass
@@ -103,7 +103,7 @@
         continue unless kouku?
         info.push

-          {__ "Aerial Combat"} {id + 1}
+          {__ "Aerial Combat"}

         # Stage 1
@@ -113,7 +113,7 @@
               <PlaneCount count={kouku.api_stage1.api_f_count}
                           lost={kouku.api_stage1.api_f_lostcount} />

-
+            {"Stage 1"}

               <PlaneCount count={kouku.api_stage1.api_e_count}
                           lost={kouku.api_stage1.api_e_lostcount} />
@@ -126,17 +126,24 @@
               <PlaneCount count={kouku.api_stage2.api_f_count}
                           lost={kouku.api_stage2.api_f_lostcount} />

-
-              <AntiAirCICell api={kouku.api_stage2.api_air_fire}
-                             sortieFleet={packet.poi_sortie_fleet}
-                             combinedFleet={packet.poi_combined_fleet}
-                             />
-
+            {"Stage 2"}

               <PlaneCount count={kouku.api_stage2.api_e_count}
                           lost={kouku.api_stage2.api_e_lostcount} />

+          # Anti air cut-in in stage 2
+          if kouku.api_stage2.api_air_fire?
+            info.push

+
+                <AntiAirCICell api={kouku.api_stage2.api_air_fire}
+                               sortieFleet={packet.poi_sortie_fleet}
+                               combinedFleet={packet.poi_combined_fleet}
+                               />
+
+              {"Anti-air Cutin"}
+
+

         info.push


Fix whitespace of webview when resizing

Fix whitespace of webview when resizing

--- views/layout.horizonal.cjsx
+++ views/layout.horizonal.cjsx
@@ -49,6 +49,7 @@
     }
   """
   $('kan-game #webview-wrapper')?.style?.height = $('kan-game webview')?.style?.height = $('kan-game webview /deep/ object[is=browserplugin]')?.style?.height = "#{Math.floor(480 * factor) - 1}px"
+  $('kan-game #webview-wrapper')?.style?.width = "#{Math.floor(800 * factor)}px"
   $('kan-game').style.marginTop = "#{Math.max(0, (window.innerHeight - Math.floor(480 * factor - 1) - 30)) / 2.0}px"
 if !window._delay
   adjustSize()


use fs.watch

use fs.watch

* renamed Tail.{watch,unwatch} -> Tail.{watch,unwatch}File
* implemented new Tail.{watch, unwatch} based on fs.watch
* introduced @watcher, @pos
* updated Tail.readBlock to increment pos as appropriate

--- tail.coffee
+++ tail.coffee
@@ -18,6 +18,7 @@
           @internalDispatcher.emit("next") if @queue.length >= 1
         stream.on 'data', (data) =>
           @buffer += data
+          (@pos += data.length) unless @pos is null
           parts = @buffer.split(@separator)
           @buffer = parts.pop()
           @emit("line", chunk) for chunk in parts
@@ -33,17 +34,38 @@

     @watch()

-  unwatch:->
-    fs.unwatchFile @filename
-    @isWatching = false
+
+  watch: ->
+    return if @isWatching
+    @pos = 0
+    @isWatching = true
+    @watcher = fs.watch @filename, @fsWatchOptions, (e) =>
+      if e is 'change'
+        fs.stat @filename, (e, stats) =>
+          if stats.size > @pos
+            @queue.push({start: @pos, end: stats.size})
+            @internalDispatcher.emit("next") if @queue.length is 1
+      else if e is 'rename'
+        @unwatch()
+        setTimeout(1000, => @watch())
+
+  unwatch: ->
+    @watcher.close()
+    @iswatching = false
     @queue = []
-
-  watch:->
+
+  watchFile:->
     return if @isWatching
     @isWatching = true
     fs.watchFile @filename, @fsWatchOptions, (curr, prev) =>
       if curr.size > prev.size
         @queue.push({start:prev.size, end:curr.size})
         @internalDispatcher.emit("next") if @queue.length is 1
+
+  unwatchFile:->
+    fs.unwatchFile @filename
+    @isWatching = false
+    @queue = []
+

 exports.Tail = Tail


Update menus

Update menus

- Add `iterrupt-julia`, `format-code` and `select-working-module`
command
- Move down Julia config stuff since it's relatively less important
runtime commands

--- lib/package/menu.coffee
+++ lib/package/menu.coffee
@@ -23,8 +23,11 @@
     label: 'Julia'
     submenu: [
       {label: 'Start Julia', command: 'julia-client:start-julia'}
+      {label: 'Interrupt Julia', command: 'julia-client:interrupt-julia'}
       {label: 'Stop Julia', command: 'julia-client:kill-julia'}
-      {label: 'Open Terminal', command: 'julia-client:open-a-repl'}
+      {label: 'Open Console', command: 'julia-client:open-console'}
+      {label: 'Clear Console', command: 'julia-client:clear-console'}
+      {label: 'Open REPL', command: 'julia-client:open-a-repl'}
       {
         label: 'Working Directory'
         submenu: [
@@ -34,13 +37,25 @@
           {label: 'Select...', command: 'julia-client:select-working-folder'}
         ]
       }
+      {
+        label: 'Working Module'
+        submenu: [
+          {label: 'Select...', command: 'julia-client:select-working-module'}
+        ]
+      }

       {type: 'separator'}

       {label: 'Run Block', command: 'julia-client:run-block'}
       {label: 'Run All', command: 'julia-client:run-all'}
-      {label: 'Open Console', command: 'julia-client:open-console'}
-      {label: 'Clear Console', command: 'julia-client:clear-console'}
+      {label: 'Format Code', command: 'julia-client:format-code'}
+
+      {type: 'separator'}
+
+      {label: 'Open Workspace', command: 'julia-client:open-workspace'}
+      {label: 'Open Documentation Browser', command: 'julia-client:open-documentation-browser'}
+      {label: 'Open Plot Pane', command: 'julia-client:open-plot-pane'}
+      {label: 'Open Debugger Pane', command: 'julia-client:open-debugger-pane'}

       {type: 'separator'}

@@ -51,13 +66,6 @@

       {type: 'separator'}

-      {label: 'Open Workspace', command: 'julia-client:open-workspace'}
-      {label: 'Open Documentation Browser', command: 'julia-client:open-documentation-browser'}
-      {label: 'Open Plot Pane', command: 'julia-client:open-plot-pane'}
-      {label: 'Open Debugger Pane', command: 'julia-client:open-debugger-pane'}
-
-      {type: 'separator'}
-
       {label: 'Help...', command: 'julia:get-help'}
       {label: 'Settings...', command: 'julia-client:settings'}
     ]


Dragging beyond graph's x-bounds should still draw

When the user drags above and below the y-bounds of the graph, we already
treat it as if they are dragging at the very top or bottom, to make it
easier to draw maxes and mins.

This does the same thing with the x-bounds. When a user drags across the
canvas, the last x coordinate at the axis is likely to get dropped, making
it so that the line does not end cleanly at the graphs bounds. Now we capture
x values beyond the bounds, so we can cleanly set the ends of the line.

[#156226989]

--- src/code/views/svg-graph-view.coffee
+++ src/code/views/svg-graph-view.coffee
@@ -203,6 +203,7 @@
       x = Math.round scaledCoords.x
       y = scaledCoords.y

+      # sanity check, but it shouldn't be possible to be out of bounds with our scaledCoords
       return if x < 0 || x > 100 || y < 0 || y > 100

       # our data is ordered in the format [[0, y], [1, y], [2, y], ...]
@@ -244,6 +245,7 @@
   pointToScaledCoords: (evt) ->
     rect = this.refs.graphBody?.getBoundingClientRect()
     coords = {x: rect.width - (rect.right-evt.clientX), y: rect.bottom - evt.clientY}
+    coords.x = Math.max(0, Math.min(coords.x, rect.width))
     coords.y = Math.max(0, Math.min(coords.y, rect.height))
     scaledCoords = {x: Math.round(coords.x / rect.width * 100), y: Math.round(coords.y / rect.height * 100)}
     scaledCoords


Make poll search case insensitive

Make poll search case insensitive

--- angular/core/components/previous_polls_page/previous_polls_page.coffee
+++ angular/core/components/previous_polls_page/previous_polls_page.coffee
@@ -4,9 +4,7 @@
   Records.groups.findOrFetchById($routeParams.key).then (group) =>
     @group = group
     @loadMore()
-  , throwPageError
-
-  throwPageError = (error) ->
+  , (error) ->
     $rootScope.$broadcast('pageError', error)

   from = 0
@@ -27,7 +25,7 @@
       return [] unless @group?
       _.sortBy(
         _.filter(@group.closedPolls(), (poll) =>
-          _.isEmpty(@fragment) or poll.title.match(@fragment)), '-closedAt')
+          _.isEmpty(@fragment) or poll.title.match(///#{@fragment}///i)), '-closedAt')

   @translations = TranslationService.eagerTranslate @,


Move test from @render to @shouldComponentUpdate

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -50,10 +50,10 @@
     getDefaultProps: ->
         layout: 'full'

+    shouldComponentUpdate: (nextProps, nextState) ->
+        !!nextProps.accounts

     render: ->
-        return unless @props.accounts
-
         closeUrl = @buildClosePanelUrl @props.layout

         classLabel = 'compose-label'


Remove superfluous semicolons

Remove superfluous semicolons

They were an artifact from when the class was ES6

--- client/lanes/screens/Instance.coffee
+++ client/lanes/screens/Instance.coffee
@@ -5,45 +5,45 @@
 class Lanes.Screens.Instance

     constructor: (selector, options)->
-        this.viewport = new Lanes.Views.Viewport({ selector: selector, instance: this });
-        Lanes.Models.Bootstrap.initialize(options);
+        this.viewport = new Lanes.Views.Viewport({ selector: selector, instance: this })
+        Lanes.Models.Bootstrap.initialize(options)
         Lanes.$(document).ready => @boot(options)

     boot: (options)->
-        this.root = Lanes.$( this.viewport.selector );
-        this.root.data().workspace = this;
-        this.viewport.root = this.root;
+        this.root = Lanes.$( this.viewport.selector )
+        this.root.data().workspace = this
+        this.viewport.root = this.root

         Lanes.lib.ResizeSensor(this.root[0],  _.bind( _.debounce( =>
-            @viewport.set({ width: this.root.width(), height: this.root.height() });
-        , 250 ), this) );
+            @viewport.set({ width: this.root.width(), height: this.root.height() })
+        , 250 ), this) )

-        this.root.addClass('lanes root');
+        this.root.addClass('lanes root')
         this.root.tooltip({
             viewport: '.lanes'
             selector: '[data-tooltip-message]'
             title: -> this.getAttribute('data-tooltip-message')
-        });
-        Lanes.Views.Keys.initialize();
-        Lanes.Models.PubSub.initialize() if options.pub_sub;
-
-        view = Lanes.getPath(options.root_view);
+        })
+        Lanes.Views.Keys.initialize()
+        Lanes.Models.PubSub.initialize() if options.pub_sub
+        cntrl = Lanes.Extensions.controlling()
+        view = Lanes.getPath(options.root_view)
         if view
-            this.displayInitialView(view);
+            this.displayInitialView(view)
         else
-            definition=Lanes.Models.Screens.all.findWhere({view: options.root_view});
+            definition=Lanes.Models.Screens.all.findWhere({view: options.root_view})
             if definition
                 definition.getScreen().then( (screen)=>
                     # break out of the promise so an errors during render get thrown properly
-                    _.defer( => @displayInitialView(screen); )
+                    _.defer( => @displayInitialView(screen) )
                 ,(msg)->
-                    Lanes.fatal("Unable to load initial screen ${options.root_view}", msg);
+                    Lanes.fatal("Unable to load initial screen ${options.root_view}", msg)
                 )
             else
-                Lanes.fatal(options.root_view + " doesn't exist!");
+                Lanes.fatal(options.root_view + " doesn't exist!")

     displayInitialView:(view)->
-        this.view = new view({parent: this, model: this.viewport}).render();
-        this.viewport.el = this.view.$el;
-        this.root.append( this.view.el );
-        Lanes.Extensions.fireOnAvailable(this);
+        this.view = new view({parent: this, model: this.viewport}).render()
+        this.viewport.el = this.view.$el
+        this.root.append( this.view.el )
+        Lanes.Extensions.fireOnAvailable(this)


Update ConnectedComponent.coffee

Update ConnectedComponent.coffee

--- client/lib/ConnectedComponent.coffee
+++ client/lib/ConnectedComponent.coffee
@@ -9,29 +9,26 @@

 import awaitAll from '../lib/awaitAll'

+class ErrorBoundary extends React.Component
+    constructor:(props) ->
+        super(props)
+        this.state = { error: null, errorInfo: null };
+
+
+    componentDidCatch:(error, errorInfo) ->
+        this.setState({
+            error: error,
+            errorInfo: errorInfo
+        })
+
+
+    render: () ->
+        if this.state.errorInfo
+            return
<FontAwesome name="exclamation-circle"/>

+
+        return this.props.children;
+
 export default ConnectedComponent = (Component, options) ->
-       `class ErrorBoundary extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = { error: null, errorInfo: null };
-  }
-
-  componentDidCatch(error, errorInfo) {
-    this.setState({
-      error: error,
-      errorInfo: errorInfo
-    })
-  }
-
-  render() {
-    if (this.state.errorInfo) {
-      return (
-
<FontAwesome name="exclamation-circle"/>

-      );
-    }
-    return this.props.children;
-  }
-}`
     class Result extends React.Component
         constructor: (props) ->
             super(props)


Fix javascript spec for movie#url is not defined

--- spec/javascripts/backbone/views/movies/movie_view_spec.js.coffee
+++ spec/javascripts/backbone/views/movies/movie_view_spec.js.coffee
@@ -24,8 +24,9 @@
     callback = sinon.spy
     movie = new Movierater.Models.Movie
       id: 1
+    movie.url = "movie"
     mock = sinon.mock(movie)
     mock.expects('save').once
     view = new Movierater.Views.Movies.MovieView({model : movie})
-    $(view.render().el).find("img")[1].click()
+    $(view.render().el).find("div.ratyrating").find("img")[1].click()
     mock.verify()


dont collide with allready included jquery module

--- src/dynamictemplate.coffee
+++ src/dynamictemplate.coffee
@@ -2,4 +2,14 @@
 { Tag, Builder } = require 'asyncxml'
 Template = require './template'

-@dynamictemplate = module.exports = { Tag, Builder, Template }
+module.exports = { Tag, Builder, Template }
+
+# browser support
+
+if @dynamictemplate?
+    @dynamictemplate.Template = Template
+    @dynamictemplate.Builder  = Builder
+    @dynamictemplate.Tag      = Tag
+else
+    @dynamictemplate = module.exports
+


Esconde o preço quando não determinado

Esconde o preço quando não determinado

--- src/coffee/portal-sku-selector.coffee
+++ src/coffee/portal-sku-selector.coffee
@@ -218,7 +218,7 @@
 			for dimension in undefinedDimensions
 				selectDimension(options.selectors.itemDimensionInput(dimension, context))

-			updatePrice(selectedSku, options, context)
+		updatePrice(selectedSku, options, context)

 	# Handles submission in the warn unavailable form
 	warnUnavailableSubmitHandler = (e) ->


Avoid creating setAttribute actions without name of attribute. Refs #9.

--- src/coffee/utils/product-utils.coffee
+++ src/coffee/utils/product-utils.coffee
@@ -199,6 +199,9 @@
             actions.push setAction if setAction
         else if REGEX_UNDERSCORE_NUMBER.test key
           if _.isArray value
+            # ignore pure array moves! TODO: remove when moving to new version of jsondiffpath (issue #9)
+            if _.size(value) is 3 and value[2] is 3
+              return
             v = helper.getDeltaValue(value)
             unless v
               v = value[0]
@@ -330,6 +333,7 @@

 buildSetAttributeAction = (diffed_value, variant, index, sameForAllAttributeNames) ->
   attribute = variant.attributes[index]
+  return unless attribute
   if attribute
     action =
       action: 'setAttribute'
@@ -376,6 +380,7 @@

 buildNewSetAttributeAction = (id, el, sameForAllAttributeNames) ->
   attributeName = el.name
+  return unless attributeName
   action =
     action: "setAttribute"
     variantId: id


Missed a Yoda

Missed a Yoda

--- lib/tin-foil-map.coffee
+++ lib/tin-foil-map.coffee
@@ -10,7 +10,9 @@

   get: (key) ->
     value = @map[key]
-    throw Error "#{key} not found in map" unless value
+
+    if !value then throw Error "#{key} not found in map"
+
     value

   extend: ->


delete unused my addition.

delete unused my addition.

--- lib/type-checker.coffee
+++ lib/type-checker.coffee
@@ -1,17 +0,0 @@
-{Operator}   = require './operators/index'
-{Repeat}    = require './prefixes'
-{Motion}     = require './motions/index'
-{TextObject} = require './text-objects'
-
-module.exports =
-  isOperator: ->
-    this instanceof Operators.Operator
-
-  isTextObject: ->
-    this instanceof TextObjects.TextObject
-
-  isMotion: ->
-    this instanceof Motions.Motion
-
-  isRepeat: ->
-    this instanceof Prefixes.Repeat


Todos and stuff

Todos and stuff

--- test.coffee
+++ test.coffee
@@ -7,7 +7,9 @@
 # yield
 #  test for conflicts when calling order many times
 #  test for calling too many times
-# yields void
+#  delay
+#  yields must have value or on_value
+# yields with null
 # Check for value on callback arguments
 # Give context to error messages
 # times called


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -22,7 +22,7 @@
   ###
   closest: (selector, position) ->
     self.find(selector).sort (a, b) ->
-      a.po
+      a.position()

   ###*
   Get a selection of GameObjects that match the specified selector criteria. The selector language


Update Atom config

Update Atom config

--- atom/atom/config.cson
+++ atom/atom/config.cson
@@ -11,7 +11,6 @@
     "atom-ide-code-format":
       formatOnSave: true
     "atom-ide-diagnostics-ui":
-      autoVisibility: true
       showDiagnosticTraces: true
       showDirectoryColumn: true
     "atom-ide-find-references": {}
@@ -27,8 +26,7 @@
   "atom-ternjs":
     excludeLowerPriorityProviders: true
     guess: false
-    inlineFnCompletion: false
-    lint: false
+    lint: true
     snippetsFirst: true
     useSnippets: true
   "auto-update-packages":
@@ -68,6 +66,7 @@
     preferredFormat: "HEX"
   core:
     audioBeep: false
+    debugLSP: true
     disabledPackages: [
       "feedback"
       "save-session"
@@ -83,6 +82,8 @@
     ]
     packagesWithKeymapsDisabled: [
       "dash"
+      "atom-ternjs"
+      "zentabs"
     ]
     projectHome: "/Users/Felix/Development"
     telemetryConsent: "limited"
@@ -99,7 +100,7 @@
   docblockr: {}
   editor:
     fontFamily: "FuraCode Nerd Font"
-    fontSize: 12
+    fontSize: 11
     invisibles:
       space: "•"
       tab: "›"
@@ -133,6 +134,8 @@
   "git-diff": {}
   "ide-flowtype":
     tryFlowBin: true
+  "ide-typescript":
+    javascriptSupport: false
   "imdone-atom":
     excludeVcsIgnoredPaths: true
     maxFilesPrompt: 1000
@@ -150,7 +153,7 @@
   "linter-tidy":
     tidyExecutablePath: "/usr/local/bin/tidy5"
   "linter-ui-default":
-    panelHeight: 99
+    panelHeight: 300
     showProviderName: true
     statusBarRepresents: "Current File"
   "markdown-preview":
@@ -180,9 +183,9 @@
   pigments: {}
   "prettier-atom":
     formatOnSaveOptions:
-      enabled: true
       isDisabledIfNoConfigFile: true
       isDisabledIfNotInPackageJson: true
+      showInStatusBar: true
     prettierEslintOptions:
       prettierLast: true
     prettierOptions:
@@ -190,14 +193,16 @@
     useEditorConfig: false
     useEslint: true
     useStylelint: true
-  "pristine-ui": {}
+  "pristine-ui":
+    fontSize: 10
+    hideDockButtons: true
   "repl-term":
     focusTerminal: true
   sassbeautify:
     indent: 2
     path: '''
       /usr/local/bin:/usr/local/sbin:/usr/local/git/bin:/usr/texbin:/Users/Felix/.rvm/gems/ruby-2.1.0/bin:/Users/Felix/.rvm/gems/ruby-2.1.0@global/bin:/Users/Felix/.rvm/rubies/ruby-2.1.0/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/Felix/.rvm/bin:/Users/Felix/.rvm/bin
-
+
     '''
   tabs:
     addNewTabsAtEnd: true
@@ -236,7 +241,6 @@
   welcome:
     showOnStartup: false
   zentabs:
-    maximumOpenedTabs: 1
     neverCloseUnsaved: true
 ".angular.html.text":
   editor:


Changed embedInScene for isosurfaces

Changed embedInScene for isosurfaces

Uses a callback to initialize scene, only after surface is constructed.

--- js-src/MathModel.coffee
+++ js-src/MathModel.coffee
@@ -246,10 +246,39 @@
     @march_async(false)
     @needsGui = true

-  embedObjects: ->
-    @march_async(false, @algorithm)
+  embedObjects: =>
+    @march_async(false, @algorithm, @embedCallback)
     null
     # @mathScene.scene.add(@surface)
+
+  # need to override embedInScene to handle asynchronous rendering so as not to
+  # make the snapshot vanish early
+
+  embedCallback: ->
+    model = @
+    old_calc = model.mathScene.calc
+    model.mathScene.calc = (t) ->
+      if model.calc?
+        model.calc()(t)
+      old_calc(t)
+      null
+
+    if model.needsGui
+    #   mathScene.activateGui()
+      model.addGui(model.mathScene.gui)
+
+    if not @mathScene.showingObjects
+      mathScene.create()
+
+    null
+
+
+  embedInScene: (mathScene) ->
+    model = @
+    model.mathScene = mathScene
+    model.embedObjects()
+    null
+

   rerender: ->
     if @mathScene?
@@ -285,7 +314,7 @@
     null

   # see http://stackoverflow.com/a/10372280 for starting Workers via blobs
-  march_async: (b, algorithm="marchingCubes") ->
+  march_async: (b, algorithm="marchingCubes", callback) ->
     that = @
     debug = @debug
     window.URL = window.URL || window.webkitURL
@@ -337,12 +366,14 @@
           console.log "surface constructed"
           that.mathScene.scene.add(that.surface)
           console.log "surface embedded"
+          if callback? then callback()
       else
         if that.mathScene?
           that.mathScene.scene.remove(that.surface)
           that.surface = new_surface
           that.mathScene.scene.add(that.surface)
           that.mathScene.render()
+          if callback? then callback()
       null

     worker.postMessage("Go!")


See if swapping this is better.

See if swapping this is better.

--- tests/loud_test.coffee
+++ tests/loud_test.coffee
@@ -52,7 +52,7 @@
   context 'louds in chat not from banned', ->
     it 'should be produced whenever someone louds anew', ->
       room.user.say 'alpha',   'FISH'
-      room.user.say 'bravo',   'CHICKEN'
-      room.user.say 'charlie', '@hubot loud ban FISH'
+      room.user.say 'bravo',   '@hubot loud ban FISH'
+      room.user.say 'charlie', 'CHICKEN'
       room.user.say 'delta',   'COW'
       expect(room.messages[2]).to.eql ['hubot', 'CHICKEN']


Don't list messages outside of group unless superuser

--- client/message.coffee
+++ client/message.coffee
@@ -1574,6 +1574,7 @@
     @messages = Messages.find {},
       fields:
         _id: true
+        group: true
         title: true
         file: true
         creator: true
@@ -1590,8 +1591,11 @@
       for child in msg.children
         recurse child
     recurse @data.child._id
+    ## Show (messages from) other groups if superuser for this group
+    acrossGroups = canSuper @data.child.group
     @messages =
-      for msg in @messages when msg._id of byId
+      for msg in @messages when msg._id of byId and (
+          acrossGroups or msg.group == @data.child.group)
         msg.text = "#{titleOrUntitled msg} by #{msg.creator} [#{msg._id}]"
         msg.html = "#{_.escape titleOrUntitled msg} by #{_.escape msg.creator} [#{_.escape msg._id}]"
         msg


Restore basic Edit button functionality

Restore basic Edit button functionality

--- client/message.coffee
+++ client/message.coffee
@@ -1,4 +1,4 @@
-import React, {useMemo, useState} from 'react'
+import React, {useEffect, useMemo, useState} from 'react'
 import {useTracker} from 'meteor/react-meteor-data'

 import {useRefTooltip} from './lib/tooltip'
@@ -355,13 +355,6 @@
 editingMessage = (message, user = Meteor.user()) ->
   user? and user.username in (message.editing ? [])

-safeToStopEditing = ->
-  data = Template.currentData()
-  instance = Template.instance()
-  #data.editing.length > 1 or (
-  data.title == instance.editTitle.get() and
-  data.body == instance.editBody.get()
-
 idle = 1000   ## one second

 messageClass = ->
@@ -423,7 +416,6 @@
 Template.submessage.onCreated ->
   @count = submessageCount++
   @editing = new ReactiveVar null
-  @editStopping = new ReactiveVar false
   @editTitle = new ReactiveVar null
   @editBody = new ReactiveVar null
   @lastTitle = null
@@ -810,7 +802,6 @@
   canReply: -> canPost @group, @_id
   editingRV: -> Template.instance().editing.get()
   editingNR: -> Tracker.nonreactive -> Template.instance().editing.get()
-  editStopping: -> Template.instance().editStopping.get()
   editTitle: -> Template.instance().editTitle.get()
   editData: ->
     #_.extend @,
@@ -819,7 +810,6 @@
     body: @body
     group: @group
     editing: @editing  ## to list other editors
-    editStopping: Template.instance().editStopping.get()
     editTitle: Template.instance().editTitle.get()
     editBody: Template.instance().editBody.get()
   hideIfEditing: ->
@@ -1312,24 +1302,6 @@

   'click .focusButton': (e) -> $(e.currentTarget).tooltip 'hide'

-  'click .editButton': (e, t) ->
-    e.preventDefault()
-    e.stopPropagation()
-    message = @_id  #e.target.getAttribute 'data-message'
-    if editing @
-      stop = -> Meteor.call 'messageEditStop', message
-      if safeToStopEditing()
-        stop()
-      else
-        t.editStopping.set true
-        t.autorun (computation) ->
-          if safeToStopEditing()
-            t.editStopping.set false
-            stop()
-            computation.stop()
-    else
-      Meteor.call 'messageEditStart', message
-
   'click .togglePreview': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
@@ -1981,6 +1953,18 @@
   , [message._id]
   historified = history ? message
   [editTitle, setEditTitle] = useState()
+  [editBody, setEditBody] = useState()
+  [editStopping, setEditStopping] = useState()
+  safeToStopEditing = ->
+    message.title == editTitle and message.body == editBody
+  useEffect ->
+    if editStopping and safeToStopEditing()
+      Meteor.call 'messageEditStop', message._id, (error, result) ->
+        if error?
+          console.error error
+        else
+          setEditStopping false
+    undefined
   preview = useTracker ->
     history? or
     messagePreview.get(message._id) ? messagePreviewDefault()
@@ -2023,6 +2007,16 @@
     e.preventDefault()
     e.stopPropagation()
     messageHistoryAll.set message._id, not messageHistoryAll.get message._id
+  onEdit = (e) ->
+    e.preventDefault()
+    e.stopPropagation()
+    if editing
+      if safeToStopEditing()
+        Meteor.call 'messageEditStop', message._id
+      else
+        setEditStopping true
+    else
+      Meteor.call 'messageEditStart', message._id

@@ -2143,73 +2137,81 @@
             +formatSelector format=format tabIndex=tabindex0+7
           }
           ###}
-          {###
-          unless history
-            if canAction
-              .btn-group
-                button.btn.btn-info.actionButton.dropdown-toggle(tabIndex={tabindex0+4}, type="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false")
-                  | Action
-                  span.caret
-                ul.dropdown-menu.dropdown-menu-right.actionMenu(role="menu")
-                  if minimized
-                    if canUnminimize
+          {unless history
+            <>
+              {###
+              if canAction
+                .btn-group
+                  button.btn.btn-info.actionButton.dropdown-toggle(tabIndex={tabindex0+4}, type="button", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false")
+                    | Action
+                    span.caret
+                  ul.dropdown-menu.dropdown-menu-right.actionMenu(role="menu")
+                    if minimized
+                      if canUnminimize
+                        li
+                          a.minimizeButton(href="#")
+                            button.btn.btn-success.btn-block(data-toggle="tooltip", data-container="body", data-placement="left", data-html="true", title="Open/unfold this message for all users. Use this if a discussion becomes relevant again. If you just want to open/unfold the message to see it yourself temporarily, use the [+] button on the left.") Unminimize
+                    else
+                      if canMinimize
+                        li
+                          a.minimizeButton(href="#")
+                            button.btn.btn-danger.btn-block(data-toggle="tooltip", data-container="body", data-placement="left", data-html="true", title="Close/fold this message for all users. Use this to clean up a thread when the discussion of this message (and all its replies) is resolved/no longer important. If you just want to close/fold the message yourself temporarily, use the [−] button on the left.") Minimize
+                    if deleted
+                      if canUndelete
+                        li
+                          a.deleteButton(href="#")
+                            button.btn.btn-success.btn-block Undelete
+                      if canSuperdelete
+                        li
+                          a.superdeleteButton(href="#")
+                            button.btn.btn-danger.btn-block Superdelete
+                    unless published
+                      if canPublish
+                        li
+                          a.publishButton(href="#")
+                            button.btn.btn-success.btn-block Publish
+                    unless deleted
+                      if canDelete
+                        li
+                          a.deleteButton(href="#")
+                            button.btn.btn-danger.btn-block Delete
+                    if published
+                      if canUnpublish
+                        li
+                          a.publishButton(href="#")
+                            button.btn.btn-danger.btn-block Unpublish
+                    if canPrivate
+                      if private
+                        li
+                          a.privateButton(href="#")
+                            button.btn.btn-success.btn-block Make Public
+                      else
+                        li
+                          a.privateButton(href="#")
+                            button.btn.btn-danger.btn-block Make Private
+                    if canParent
                       li
-                        a.minimizeButton(href="#")
-                          button.btn.btn-success.btn-block(data-toggle="tooltip", data-container="body", data-placement="left", data-html="true", title="Open/unfold this message for all users. Use this if a discussion becomes relevant again. If you just want to open/unfold the message to see it yourself temporarily, use the [+] button on the left.") Unminimize
-                  else
-                    if canMinimize
-                      li
-                        a.minimizeButton(href="#")
-                          button.btn.btn-danger.btn-block(data-toggle="tooltip", data-container="body", data-placement="left", data-html="true", title="Close/fold this message for all users. Use this to clean up a thread when the discussion of this message (and all its replies) is resolved/no longer important. If you just want to close/fold the message yourself temporarily, use the [−] button on the left.") Minimize
-                  if deleted
-                    if canUndelete
-                      li
-                        a.deleteButton(href="#")
-                          button.btn.btn-success.btn-block Undelete
-                    if canSuperdelete
-                      li
-                        a.superdeleteButton(href="#")
-                          button.btn.btn-danger.btn-block Superdelete
-                  unless published
-                    if canPublish
-                      li
-                        a.publishButton(href="#")
-                          button.btn.btn-success.btn-block Publish
-                  unless deleted
-                    if canDelete
-                      li
-                        a.deleteButton(href="#")
-                          button.btn.btn-danger.btn-block Delete
-                  if published
-                    if canUnpublish
-                      li
-                        a.publishButton(href="#")
-                          button.btn.btn-danger.btn-block Unpublish
-                  if canPrivate
-                    if private
-                      li
-                        a.privateButton(href="#")
-                          button.btn.btn-success.btn-block Make Public
-                    else
-                      li
-                        a.privateButton(href="#")
-                          button.btn.btn-danger.btn-block Make Private
-                  if canParent
-                    li
-                      a.parentButton(href="#")
-                        button.btn.btn-warning.btn-block Move
-            if editing
-              if editStopping
-                button.btn.btn-info.editButton.disabled(tabIndex={tabindex0+8}, title="Waiting for save to complete before stopping editing...") Stop Editing
+                        a.parentButton(href="#")
+                          button.btn.btn-warning.btn-block Move
+              ###}
+              {if editing
+                if editStopping
+                  <button className="btn btn-info editButton disabled" tabIndex={tabindex0+8} title="Waiting for save to complete before stopping editing...">Stop Editing</button>
+                else
+                  <button className="btn btn-info editButton" tabIndex={tabindex0+8} onClick={onEdit}>Stop Editing</button>
               else
-                button.btn.btn-info.editButton(tabIndex={tabindex0+8}) Stop Editing
-            else
-              unless folded
-                if canEdit
-                  if message.file
-                    +messageReplace _id=_id group=group tabIndex=tabindex0+7
-                  button.btn.btn-info.editButton(tabIndex={tabindex0+8}) Edit
-          ###}
+                if canEdit and not folded
+                  <>
+                    {###
+                    {if message.file
+                      +messageReplace _id=_id group=group tabIndex=tabindex0+7
+                    }
+                    ###}
+                    <button className="btn btn-info editButton" tabIndex={tabindex0+8} onClick={onEdit}>Edit</button>
+                  </>
+              }
+            </>
+          }

@@ -2277,7 +2279,7 @@
                     Stop Editing
                   </button>
                 else
-                  <button className="btn btn-info editButton">
+                  <button className="btn btn-info editButton" onClick={onEdit}>
                     Stop Editing
                   </button>
                 }


Fix key down on typeaheads

Fix key down on typeaheads

--- src/controls/TypeAhead/Matches.coffee
+++ src/controls/TypeAhead/Matches.coffee
@@ -18,7 +18,7 @@
       @ensureHighlightedVisible() if @props.highlightedIndex > -1

   ensureHighlightedVisible: ->
-    list = @refs.list?
+    list = @refs.list
     return unless list and @props.highlightedIndex >= 0
     return unless highlighted = list.childNodes[@props.highlightedIndex]
     list.scrollTop = list.scrollTop + highlighted.offsetTop - list.scrollHeight / 2 + highlighted.offsetHeight / 2


wait until after the plan has been loaded before navigating to the subpage

--- js/inner/billing.coffee
+++ js/inner/billing.coffee
@@ -174,8 +174,8 @@
         plan: plan_id
       success: (data) =>
         mixpanel.track('Paid')
+        @loadPlanData(data)
         VM.org().subpage('add-containers')
-        @loadPlanData(data)

   ajaxUpdatePlan: (changed_attributes, event) =>
     $.ajax


let users select parallelism higher than 24, but only if their plan supports it

--- js/inner/project.coffee
+++ js/inner/project.coffee
@@ -52,7 +52,6 @@

     CI.inner.VcsUrlMixin(@)

-    @parallelism_options = [1..24]
     # Make sure @parallel remains an integer
     @editParallel = @komp
       read: ->
@@ -76,6 +75,12 @@
     @plan_max_speed = @komp =>
       if @billing.chosenPlan() && @billing.chosenPlan().max_parallelism?
         @billing.chosenPlan().max_parallelism
+
+    @parallelism_options = @komp =>
+      if @plan_max_speed()
+        [1..Math.max(@plan_max_speed(), 24)]
+      else
+        [1..24]

     # Trial speed is counted as paid here
     @paid_speed = @komp =>


Workspace: Implemented host disconnected.

--- client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -23,6 +23,8 @@

       keys = snapshot.val()?.keys

+      @workspaceRef.onDisconnect().remove()  if @amIHost()
+
       if keys # if we have keys this means we're about to join an old session
         log "it's an old session, impressed!"
         @sessionData  = keys
@@ -36,6 +38,11 @@

     @workspaceRef.on "child_added", (snapshot) =>
       log "everything is something happened", "child_added", snapshot.val(), snapshot.name()
+
+    @workspaceRef.on "child_removed", (snapshot) =>
+      log "possible disconnection occured"
+
+      @showDisconnectedModal()  unless @disconnectedModal

     @on "NewPanelAdded", (panel) ->
       log "New panel created", panel
@@ -65,7 +72,10 @@

   ready: -> # have to override for collaborative workspace

-  showSessionModal: ->
+  amIHost: ->
+    [sessionOwner] = @sessionKey.split ":"
+    return sessionOwner == KD.nick()
+
   showNotActiveView: ->
     notValid = new KDView
       cssClass : "not-valid"


just in case

just in case

--- components/main_layout/client.coffee
+++ components/main_layout/client.coffee
@@ -46,7 +46,7 @@
     analytics(mixpanel: (mixpanel ? null), ga: ga)
     analytics.registerCurrentUser()
     setupSplitTests()
-    analytics.trackPageview()
+  analytics.trackPageview()
   # Log a visit once per session
   unless Cookies.get('active_session')?
     Cookies.set 'active_session', true