Add some complexities to the conditional stubbing

--- test/lib/when-test.coffee
+++ test/lib/when-test.coffee
@@ -15,9 +15,16 @@
     Given -> @testDouble = @create()
     Given -> @when(@testDouble(1)).thenReturn("foo")
     Given -> @when(@testDouble(2)).thenReturn("bar")
+    Given -> @when(@testDouble(lol: 'cheese')).thenReturn('nom')
+    Given -> @when(@testDouble(lol: 'fungus')).thenReturn('eww')
+    Given -> @when(@testDouble({lol: 'fungus'}, 2)).thenReturn('eww2')
     Then -> @testDouble() == undefined
-    Then -> @testDouble(1) == "foo"
-    Then -> @testDouble(2) == "bar"
+    And -> @testDouble(1) == "foo"
+    And -> @testDouble(2) == "bar"
+    And -> @testDouble(lol: 'cheese') == "nom"
+    And -> @testDouble(lol: 'fungus') == "eww"
+    And -> @testDouble({lol: 'fungus'}, 2) == "eww2"
+

   describe 'multiple test doubles', ->
     Given -> @td1 = @when(@create()()).thenReturn("lol1")


Fix crash condition with APNs feedback

Fix crash condition with APNs feedback

--- lib/pushservices/apns.coffee
+++ lib/pushservices/apns.coffee
@@ -14,8 +14,8 @@
         @payloadFilter = conf.payloadFilter

         # Handle Apple Feedbacks
-        conf.feedback = (time, apnsSubscriber) ->
-            tokenResolver 'apns', apnsSubscriber.hexToken(), (subscriber) =>
+        conf.feedback = (time, tokenBuffer) ->
+            tokenResolver 'apns', tokenBuffer.toString(), (subscriber) =>
                 subscriber?.get (info) ->
                     if info.updated < time
                         @logger.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")


Changed feedback handler to use batchFeedback option which is enabled by default.

--- lib/pushservices/apns.coffee
+++ lib/pushservices/apns.coffee
@@ -15,12 +15,13 @@

         @feedback = new apns.Feedback(conf)
         # Handle Apple Feedbacks
-        @feedback.on 'feedback', (time, tokenBuffer) =>
-            tokenResolver 'apns', tokenBuffer.toString(), (subscriber) =>
-                subscriber?.get (info) ->
-                    if info.updated < time
-                        @logger?.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")
-                        subscriber.delete()
+        @feedback.on 'feedback', (feedbackData) =>
+            feedbackData.forEach (item) =>
+                tokenResolver 'apns', item.device.toString(), (subscriber) =>
+                    subscriber?.get (info) ->
+                        if info.updated < item.time
+                            @logger?.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")
+                            subscriber.delete()

     push: (subscriber, subOptions, payload) ->


Improved "see more" feature.

Improved "see more" feature.

--- app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom/article_view.coffee
@@ -108,25 +108,28 @@
       return

     # prepare html body
-    signatureDetected = false
     if @article.content_type is 'text/html'
       @article['html'] = @article.body
+      @article['html'] = App.Utils.signatureIdentify( @article['html'] )
     else

-      # check if signature got detected in backend
-      body = @article.body
-      if @article.preferences && @article.preferences.signature_detection
-        signatureDetected = '########SIGNATURE########'
-        body = body.split("\n")
-        body.splice(@article.preferences.signature_detection, 0, signatureDetected)
-        body = body.join("\n")
-      body = App.Utils.textCleanup(body)
-      @article['html'] = App.Utils.text2html(body)
-
-    if signatureDetected
-      @article['html']  = @article['html'].replace(signatureDetected, '')
-    else
-      @article['html'] = App.Utils.signatureIdentify( @article['html'] )
+      # client signature detection
+      bodyHtml = App.Utils.text2html(@article.body)
+      @article['html'] = App.Utils.signatureIdentify(bodyHtml)
+
+      # if no signature detected or within frist 25 lines, check if signature got detected in backend
+      if @article['html'] is bodyHtml || (@article.preferences && @article.preferences.signature_detection < 25)
+        signatureDetected = false
+        body = @article.body
+        if @article.preferences && @article.preferences.signature_detection
+          signatureDetected = '########SIGNATURE########'
+          body = body.split("\n")
+          body.splice(@article.preferences.signature_detection, 0, signatureDetected)
+          body = body.join("\n")
+        if signatureDetected
+          body = App.Utils.textCleanup(body)
+          @article['html'] = App.Utils.text2html(body)
+          @article['html']  = @article['html'].replace(signatureDetected, '')

     @html App.view('ticket_zoom/article_view')(
       ticket:     @ticket


raise the damned max listeners on projects (I don't want to)

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -869,6 +869,12 @@
     public_project_titles : immutable.Map()

 store = redux.createStore('projects', ProjectsStore, init_store)
+
+# Every time a project actions gets created, there is a new listener
+# on the projects store, and there can be many projectActions, e.g.,
+# when working with a course with 200 students.
+# This is annoying and worrisome.
+store.setMaxListeners(1000)

 # Register projects actions
 actions = redux.createActions('projects', ProjectsActions)


fixed mistake in onEvent

fixed mistake in onEvent

--- demo.coffee
+++ demo.coffee
@@ -31,7 +31,7 @@
   old = e[name]
   if old
     f = (e) -> f(e); old(e)
-  e.name = f
+  e[name] = f
 trimNum = (n) -> parseFloat n.toPrecision(3)
 decamelize = (str) ->
   str = str.replace /([a-z])([A-Z])/g, (t) -> t.charAt(0) + ' ' + t.charAt(1).toLowerCase()


:arrow_up: add overwrites to info

:arrow_up: add overwrites to info

--- src/renderer/components/doc-detail-title-component.coffee
+++ src/renderer/components/doc-detail-title-component.coffee
@@ -6,7 +6,7 @@
 colors = require './colors/color-definition'

 ###
-@props.current [required]
+@props.current [required] local current which is child of current factor
 @props.style
 ###
 class DocDetailTitleComponent extends React.Component
@@ -23,6 +23,12 @@
         if @props.current.inheritedFrom?

             {"Inherited from #{@props.current.inheritedFrom.name.replace(/__constructor/, 'constructor')}"}
+

+      }
+      {
+        if @props.current.overwrites?
+

+            {"Overwrites #{@props.current.overwrites.name.replace(/__constructor/, 'constructor')}"}

       }
       <DocFlagtagsComponent flags={@props.current.flags} style={styles.tags} />


Added additonal comments about why test data is being used. This code needs to be updated once #15 is complete.

--- src/tlsAuthentication.coffee
+++ src/tlsAuthentication.coffee
@@ -1,7 +1,8 @@
 fs = require "fs"

 getTrustedApplicationCerts = ->
-	## FIXME: this should read from all saved applications, the following is done for test purposes
+	# FIXME: this should read from all saved applications, the following is done for test purposes
+	# once #15 is complete we should be able to update this
 	return fs.readFileSync "tls/cert.pem"

 ###
@@ -30,7 +31,8 @@
 			var subject = this.req.connection.getPeerCertificate().subject;

 			// lookup application by subject.cn (cn = domain) and set them as the authenticated user
-			// Add test data in the mean time
+			// FIXME: Add test data in the mean time
+			// once #15 is complete we should be able to update this
 			this.authenticated = {
 				"applicationID": "Musha_OpenMRS",
 				"domain": "him.jembi.org",


disable showing attachments on console

disable showing attachments on console

--- src/ui/views/messages.coffee
+++ src/ui/views/messages.coffee
@@ -377,7 +377,7 @@
     return cache

 formatAttachment = (att) ->
-    console.log 'attachment', att if att.length > 0
+    # console.log 'attachment', att if att.length > 0
     if att?[0]?.embed_item?.type_
         data = extractProtobufStyle(att)
         return if not data


Killing an already stopped project is not an error

Log a warning instead and continue.

--- app/coffee/LatexRunner.coffee
+++ app/coffee/LatexRunner.coffee
@@ -58,7 +58,8 @@
 		id = "#{project_id}"
 		logger.log {id:id}, "killing running compile"
 		if not ProcessTable[id]?
-			return callback new Error("no such project to kill")
+			logger.warn {id}, "no such project to kill"
+			return callback(null)
 		else
 			CommandRunner.kill ProcessTable[id], callback


fix /#! frontend route

fix /#! frontend route

--- www/src/routers/legacy.coffee
+++ www/src/routers/legacy.coffee
@@ -13,6 +13,7 @@

     class extends Backbone.Router
         routes:
+            "!": redirect("")
             "feed": redirect("")
             "perl": redirect("realm/perl")
             "perl/": redirect("realm/perl")


Playing with setTimeout

Playing with setTimeout

--- tests/youtube-info.coffee
+++ tests/youtube-info.coffee
@@ -11,8 +11,8 @@

   context 'youtube.com url', ->
     beforeEach (done) ->
+      room.user.say 'alice', 'https://www.youtube.com/watch?v=ePoi0_zSnYk'
       setTimeout ->
-        room.user.say 'alice', 'https://www.youtube.com/watch?v=ePoi0_zSnYk'
         done()
       , 10000

@@ -21,30 +21,30 @@

   context 'youtube.com url with additional values', ->
     beforeEach (done)->
+      room.user.say 'alice', 'https://www.youtube.com/watch?v=iq9DLJfpHd0&feature=youtu.be&ab_channel=CatRe-Tailer'
       setTimeout ->
-        room.user.say 'alice', 'https://www.youtube.com/watch?v=iq9DLJfpHd0&feature=youtu.be&ab_channel=CatRe-Tailer'
         done()
-      , 10000
+      , 2000

     it 'should be able to find title', ->
       expect(room.messages[1][1]).to.contain "Shia Surprise"

   context 'youtu.be url', ->
     beforeEach (done) ->
+      room.user.say 'alice', 'https://youtu.be/HP66dH1yEZo'
       setTimeout ->
-        room.user.say 'alice', 'https://youtu.be/HP66dH1yEZo'
         done()
-      , 10000
+      , 2000

     it 'should be able to find title', ->
       expect(room.messages[1][1]).to.contain "R2D2 learns a new trick"

   context 'youtu.be url with additional values', ->
     beforeEach (done) ->
+      room.user.say 'alice', 'https://youtu.be/oVnAVcbMoSM?t=39s'
       setTimeout ->
-        room.user.say 'alice', 'https://youtu.be/oVnAVcbMoSM?t=39s'
         done()
-      , 10000
+      , 2000

     it 'should be able to find title', ->
       expect(room.messages[1][1]).to.contain "GoPro fall at Garden of the Gods"


Fixed #385 caching selected inputs

Fixed #385 caching selected inputs

--- wzk/ui/grid/BulkChange.coffee
+++ wzk/ui/grid/BulkChange.coffee
@@ -114,6 +114,7 @@
     @param {goog.events.Event} e
   ###
   handleOpen: (e) =>
+    @selectedInputs = {}
     @selectedRows = @grid.getSelectedRows()

     content = e.target.getContentElement()


paste problem solved

paste problem solved

--- app/controllers/editor.coffee
+++ app/controllers/editor.coffee
@@ -23,7 +23,7 @@
     "click .headerwrap .edit": "toggleMode"
     "click .headerwrap .revert": "revert"
     "keydown #contentwrite > .inner": "keydown"
-    "paste #contentwrite > .inner": "paste"
+    #"paste #contentwrite > .inner": "paste"
     "dblclick #contentread": "toggleMode"
     "click header .right .delete": "deleteNote"
     "click header .star": "starNote"
@@ -211,21 +211,21 @@
       e.preventDefault()
       @toggleMode()

-  paste: (e) ->
-    # Keeps the range for later
-    @range = window.getSelection().getRangeAt(0)
-
-    # Paste it into a textarea (removes formatting)
-    #@psuedoinput.val("").focus()
-
-    # As the paste event isn't instant, put it back in a few secs.
-    setTimeout( =>
-      s = window.getSelection()
-      s.removeAllRanges()
-      s.addRange(@range)
-
-      @insertText @psuedoinput.val()
-    , 10)
+  #paste: (e) ->
+  #  # Keeps the range for later
+  #  @range = window.getSelection().getRangeAt(0)
+
+  #  # Paste it into a textarea (removes formatting)
+  #  #@psuedoinput.val("").focus()
+
+  #  # As the paste event isn't instant, put it back in a few secs.
+  #  setTimeout( =>
+  #    s = window.getSelection()
+  #    s.removeAllRanges()
+  #    s.addRange(@range)
+
+  #    @insertText @psuedoinput.val()
+  #  , 10)

   checkSelWrap: ->
     setTimeout =>


make checking for remember me token more robust.

--- src/smc-hub/user-remember-me.coffee
+++ src/smc-hub/user-remember-me.coffee
@@ -19,8 +19,13 @@
         cb(undefined, account_id)
         return
     x = remember_me.split('$')
+    try
+        hash = auth.generate_hash(x[0], x[1], x[2], x[3])
+    catch err
+        cb("not signed in")
+        return
     database.get_remember_me
-        hash : auth.generate_hash(x[0], x[1], x[2], x[3])
+        hash : hash
         cb   : (err, signed_in_mesg) ->
             if err
                 cb(err)


WIP: Tests for createOrUpdate method.

WIP: Tests for createOrUpdate method.

--- src/spec/product-import.spec.coffee
+++ src/spec/product-import.spec.coffee
@@ -39,6 +39,42 @@
   ]
 }

+sampleMasterVariant = {
+  sku: "12345",
+  id: 1,
+  attributes: [
+    {
+      name: "attribute1",
+      value: "attribute1_value1"
+    }
+  ],
+  images: []
+}
+
+sampleVariant1 = {
+  id: "2",
+  sku: "12345_2",
+  attributes: [
+    {
+      name: "attribute1",
+      value: "attribute1_value2"
+    },
+    images: []
+  ]
+}
+
+sampleVariant2 = {
+  id: "7",
+  sku: "12345_7",
+  attributes: [
+    {
+      name: "attribute1",
+      value: "attribute1_value3"
+    },
+    images: []
+  ]
+}
+
 sampleNewPreparedProduct = {
   productType:
     id: "product_type_internal_id"
@@ -339,4 +375,51 @@
         expect(@import._resolveReference.calls.length).toBe 4
         expect(@import._resolveReference.calls[3].args[2]).toBe undefined
         done()
-      .catch done+      .catch done
+
+
+  describe '::_createOrUpdate', ->
+
+    it 'should create a new product', (done) ->
+      newProduct = _.deepClone(sampleNewPreparedProduct)
+      newProduct.name = { en: "My new product" }
+      newProduct.masterVariant = sampleMasterVariant
+      newProduct.variants = [ sampleVariant1, sampleVariant2 ]
+
+      existingProduct1 = _.deepClone(newProduct)
+      existingProduct1.id = "existing_id1"
+      existingProduct1.masterVariant.sku = "9876"
+      existingProduct1.variants[0].sku = "9876_1"
+      existingProduct1.variants[1].sku = "9876_2"
+
+      existingProduct2 = _.deepClone(newProduct)
+      existingProduct2.id = "existing_id2"
+      existingProduct2.masterVariant.sku = "9876_1"
+      existingProduct2.variants[0].sku = "9876_1_1"
+      existingProduct2.variants[1].sku = "9876_1_2"
+      existingProduct2.version = 1
+
+      updateProduct = _.deepClone(existingProduct2)
+      sampleVariant3 = _.deepClone(sampleVariant2)
+      sampleVariant3.sku = "9876_1_3"
+      sampleVariant3.id = "9"
+      #updateProduct.variants = []
+      updateProduct.variants[2] = sampleVariant3
+
+
+
+      existingProducts = [existingProduct1,existingProduct2]
+
+      spyOn(@import, "_prepareNewProduct").andCallFake (prepareProduct) => Promise.resolve(prepareProduct)
+      spyOn(@import.client._rest, 'POST').andCallFake (endpoint, payload, callback) ->
+        callback(null, {statusCode: 200}, {})
+      console.log(existingProducts[1].masterVariant)
+      console.log(updateProduct.masterVariant)
+      @import._createOrUpdate([newProduct,updateProduct],existingProducts)
+      .then =>
+        expect(@import._prepareNewProduct).toHaveBeenCalled()
+        expect(@import.client._rest.POST.calls[0].args[1]._settledValue).toEqual newProduct
+        console.log(@import.client._rest.POST.calls[1].args[1])
+        done()
+      .catch done
+


better scroll sync.

better scroll sync.

--- lib/markdown-preview-enhanced-view.coffee
+++ lib/markdown-preview-enhanced-view.coffee
@@ -252,6 +252,9 @@
             j = mid

         count++
+
+      if screenRow == -1
+        screenRow = mid

       if (screenRow >= 0)
         @scrollToPos(screenRow * @editor.getLineHeightInPixels() - @element.offsetHeight / 2, @editor.getElement())


getting rid of some debug printing

getting rid of some debug printing

--- app/js/services/wallet.coffee
+++ app/js/services/wallet.coffee
@@ -246,7 +246,7 @@
             t.status = if not val.is_confirmed and not val.is_virtual then "pending" else "-"

     process_transaction: (val) ->
-        console.log "------ process_transaction ------>", val
+        #console.log "------ process_transaction ------>", val
         existing_transaction = @transactions_all_by_id[val.trx_id]
         if existing_transaction and existing_transaction.id
             @update_transaction(existing_transaction, val)
@@ -263,7 +263,7 @@
                 account_name = acct[0]
                 balances = acct[1]
                 continue unless involved_accounts[account_name]
-                console.log "------ running_balances item ------>",account_name, balances
+                #console.log "------ running_balances item ------>",account_name, balances
                 running_balances[account_name] = []
                 for item in balances
                     asset_record = @blockchain.asset_records[item[1].asset_id]


Check mark centers before progressing for #24

--- app/lib/tutorial-steps.coffee
+++ app/lib/tutorial-steps.coffee
@@ -1,6 +1,9 @@
 translate = require 't7e'
 GhostMouse = require 'ghost-mouse'
 User = require 'zooniverse/models/user'
+
+isAbout = ([x, y], [idealX, idealY], give = 50) ->
+  idealX - give < x < idealX + give and idealY - give < y < idealY + give

 guideStyle =
   fill: 'transparent'
@@ -42,14 +45,24 @@
         transform: 'translate(210, 290), rotate(40)'
       @guide.attr guideStyle

+      @extantMarks = (mark for mark in @classifier.surface.marks)
+
     demo: ->
       ghostMouse.run ->
         @move '.marking-surface', (210 / 800), (290 / 400)
         @drag '.marking-surface', ((210 + 90) / 800), ((290 - 50) / 400)

     next:
-      'mouseup .marking-surface': 'adjustBubbleMarking'
-      'touchend .marking-surface': 'adjustBubbleMarking'
+      'mouseup .marking-surface': ->
+        newMarks = (mark for mark in @classifier.surface.marks when mark not in @extantMarks)
+
+        if newMarks.some((mark) -> isAbout mark.center, [210, 290])
+          'adjustBubbleMarking'
+        else
+          false
+
+      'touchend .marking-surface': ->
+        @._current.next['mouseup .marking-surface'].apply @, arguments

   adjustBubbleMarking:
     content: translate 'span', 'tutorial.adjustBubbleMarking.content'
@@ -90,12 +103,22 @@
         transform: 'translate(550, 120)'
       @guide.attr guideStyle

+      @extantMarks = (mark for mark in @classifier.surface.marks)
+
     onUnload: ->
       @guide.remove()

     next:
-      'mouseup .marking-surface': 'selectEgoTool'
-      'touchend .marking-surface': 'selectEgoTool'
+      'mouseup .marking-surface': ->
+        newMarks = (mark for mark in @classifier.surface.marks when mark not in @extantMarks)
+
+        if newMarks.some((mark) -> isAbout mark.center, [560, 120])
+          'selectEgoTool'
+        else
+          false
+
+      'touchend .marking-surface': ->
+        @._current.next['mouseup .marking-surface'].apply @, arguments

   selectEgoTool:
     content: translate 'span', 'tutorial.selectEgoTool.content'
@@ -123,12 +146,22 @@
         transform: 'translate(275, 30)'
       @guide.attr guideStyle

+      @extantMarks = (mark for mark in @classifier.surface.marks)
+
     onUnload: ->
       @guide.remove()

     next:
-      'mouseup .marking-surface': 'callOutTalk'
-      'touchend .marking-surface': 'callOutTalk'
+      'mouseup .marking-surface': ->
+        newMarks = (mark for mark in @classifier.surface.marks when mark not in @extantMarks)
+
+        if newMarks.some((mark) -> isAbout mark.center, [275, 30], 25)
+          'callOutTalk'
+        else
+          false
+
+      'touchend .marking-surface': ->
+        @._current.next['mouseup .marking-surface'].apply @, arguments

   callOutTalk:
     content: translate 'span', 'tutorial.callOutTalk.content'


Add a better description of "live"

Add a better description of "live"

--- app/pages/lab/visibility.cjsx
+++ app/pages/lab/visibility.cjsx
@@ -50,7 +50,7 @@
         Live

-
In a live project, workflows are locked and can no longer be edited. Classifications count toward subject retirement.

+
Workflows can be edited during development, and subjects will never retire. In a live project, workflows are locked and can no longer be edited, and classifications count toward subject retirement.


Workspaces: Removed log.

Workspaces: Removed log.

--- client/Main/addworkspaceview.coffee
+++ client/Main/addworkspaceview.coffee
@@ -26,7 +26,6 @@

       if @hasPendingRequest
         KD.utils.stopDOMEvent event
-        log 'pending request going on'
         return no

       data           = @getData()


verifcation: rename pin to code

verifcation: rename pin to code

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -311,7 +311,7 @@
         KD.getSingleton('router').handleRoute '/Login', {entryPoint}
         new KDNotificationView
           title     : "Check your email"
-          content   : "We've sent you a password recovery token."
+          content   : "We've sent you a password recovery code."
           duration  : 4500

         KD.mixpanel "Recover password, success"


Verification: change err.message to err.code

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -554,7 +554,8 @@

   showError = (err)->

-    if err.message is "CONFIRMATION_WAITING"
+
+    if err.code and err.code is 403
       {name, nickname}  = err.data
       KD.getSingleton('appManager').tell 'Account', 'displayConfirmEmailModal', name, nickname


handle null mention

handle null mention

--- client/shared/helpers/listen.coffee
+++ client/shared/helpers/listen.coffee
@@ -28,7 +28,7 @@

     $scope.fetchByNameFragment = (q) ->
-      $scope.q = q.toLowerCase()
+      $scope.q = [q].join().toLowerCase() # handle nulls
       if $scope.q.length > 0
         fetchThenUpdate()
         updateMentionables()


Change Model#findById implementation

Change Model#findById implementation

--- app/src/database/model.coffee
+++ app/src/database/model.coffee
@@ -210,7 +210,7 @@
     if @getBestIdentifierName() is '$loki'
       context.getCollection(@getCollectionName()).get(id)
     else
-      @find().data()[0]
+      @find(_.object([@getBestIdentifierName()], [id]), context).data()[0]

   @findOrCreate: (query, base, context = ledger.database.contexts.main) ->
     if _.isKindOf(base, ledger.database.contexts.Context)


Updated message per #114 : add Chimp & See, and make the site links more obvious.

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -23,7 +23,7 @@
         With your help, we've classified all the data we have so far. Great work!

         If you’d like to try a similar project, how about:
-

Chicago Wildlife Watch - this works much like Snapshot Serengeti (though with fewer lions), and has plenty of images awaiting volunteers. Try it now!

+

Chicago Wildlife Watch - this works much like Snapshot Serengeti (though with fewer lions), and has plenty of images awaiting volunteers. Try it now!

Chimp & See - explore wildlife in the African jungle with photos and video. Try it now!


Add test for IndicatorData.dataToSeedJSON

--- server/test/units/indicator_data.coffee
+++ server/test/units/indicator_data.coffee
@@ -71,3 +71,53 @@
   )

 )
+
+test("#dataToSeedJSON returns indicator data as JSON with the indicator field
+denormalised to the indicator name", (done) ->
+  indicator = new Indicator(
+    short_name: 'The indicator'
+  )
+  indicatorData = new IndicatorData(
+    data: [
+      date: 1357002000000,
+      value: "-",
+      text: "Great",
+    ]
+  )
+
+  expectedData = [{
+    indicator: indicator.short_name
+    data: indicatorData.data
+  }]
+
+  Q.nsend(
+    indicator, 'save'
+  ).then(->
+    indicatorData.indicator = indicator
+
+    Q.nsend(
+      indicatorData, 'save'
+    )
+  ).then(->
+    IndicatorData.dataToSeedJSON()
+  ).then((json)->
+
+    try
+      seedData = JSON.parse(json)
+
+      assert.lengthOf seedData, 1,
+        "Expected the seed data to have only 1 entry"
+
+      seedDataRow = seedData[0]
+      assert.strictEqual seedDataRow.indicator, indicator.short_name,
+        "Expected the seed data 'indicator' attribute to be the indicator short name"
+
+      assert.deepEqual seedDataRow.data, indicatorData.data,
+        "Expected the seed data 'data' to be the same as the indicator"
+
+      done()
+    catch e
+      done(e)
+
+  ).fail(done)
+)


Further documented the Readmill class

Further documented the Readmill class


Add Date of Adoption when marking a greyhound as adopted

If no date is provided, then use the current date.

--- scripts/adopt.coffee
+++ scripts/adopt.coffee
@@ -12,7 +12,7 @@
 git = require '../lib/git'
 site = require '../lib/site'

-adopt = (greyhound, callback) ->
+adopt = (greyhound, doa, callback) ->
   site.loadGreyhound greyhound, (info, bio) ->
     if not info?
       return callback "Sorry, couldn't find #{greyhound} 😕"
@@ -23,11 +23,13 @@
       return callback "#{capitalize(greyhound)} has already been adopted 😝"

     info.category = 'adopted'
+    info.doa = if doa? then new Date(doa) else new Date()
     site.dumpGreyhound greyhound, info, bio, callback

 module.exports = (robot) ->
-  robot.respond /adopt (.*)/i, (res) ->
+  robot.respond /adopt (\w+)\s?(\d{4}-\d{1,2}-\d{1,2})?/i, (res) ->
     greyhound = res.match[1]?.toLowerCase()
+    doa = res.match[2]
     message = "#{capitalize(greyhound)} Adopted! 💗"
     branch = "adopt-#{greyhound}"
     user =
@@ -40,7 +42,7 @@
       return res.reply err if err?
       git.branch repo, branch, (err, ref) ->
         return res.reply err if err?
-        adopt greyhound, (err) ->
+        adopt greyhound, doa, (err) ->
           return res.reply err if err?
           git.commit repo, user, message, (err, oid) ->
             return res.reply err if err?


Use Require.JS in game.coffee

Use Require.JS in game.coffee

--- game.coffee
+++ game.coffee
@@ -1,56 +1,59 @@
-class Game
+define(['position', 'player', 'synchronizedtime', 'point'], (Position, Player, SynchronizedTime, Point) ->

-  @NORTH = 0
-  @SOUTH = 1
-  @EAST = 2
-  @WEST = 3
+  class Game

-  @WIDTH:  200
-  @HEIGHT:  300
+    @NORTH = 0
+    @SOUTH = 1
+    @EAST = 2
+    @WEST = 3

-  @VELOCITY = 0.1
+    @WIDTH:  200
+    @HEIGHT:  300

-  @listeners = []
-  @players = []
+    @VELOCITY = 0.1

-  start: ->
-    @player0 = new Player(new Position([Game.WIDTH/2, 0], Game.SOUTH, 0))
-    @player1 = new Player(new Position([Game.WIDTH, Game.HEIGHT/2], Game.WEST, 0))
-    @player2 = new Player(new Position([Game.WIDTH/2, Game.HEIGHT], Game.NORTH, 0))
-    @player3 = new Player(new Position([0, Game.HEIGHT/2], Game.EAST, 0))
+    @listeners = []
+    @players = []

-    @players = [@player0, @player1, @player2, @player3]
+    start: ->
+      @player0 = new Player(new Position([Game.WIDTH/2, 0], Game.SOUTH, 0))
+      @player1 = new Player(new Position([Game.WIDTH, Game.HEIGHT/2], Game.WEST, 0))
+      @player2 = new Player(new Position([Game.WIDTH/2, Game.HEIGHT], Game.NORTH, 0))
+      @player3 = new Player(new Position([0, Game.HEIGHT/2], Game.EAST, 0))

-    @old_time = SynchronizedTime.getTime()
+      @players = [@player0, @player1, @player2, @player3]
+
+      @old_time = SynchronizedTime.getTime()

-  key_down: (player, key, coord) ->
+    key_down: (player, key, coord) ->

-  getPositions: ->
-    p.lastPos() for p in @players
+    getPositions: ->
+      p.lastPos() for p in @players

-  getCurrentLines: ->
-    p.current_line for p in @players
+    getCurrentLines: ->
+      p.current_line for p in @players

-  timer_tick: ->
-    console.log "Tick!"
-    new_time = SynchronizedTime.getTime()
-    elapsed_time = new_time - @old_time
+    timer_tick: ->
+      console.log "Tick!"
+      new_time = SynchronizedTime.getTime()
+      elapsed_time = new_time - @old_time

-    for player in @players
-      lastpos = player.lastPos()
-      if lastpos.direction == Game.NORTH
-        player.addToLine(new paper.Point(lastpos.x - (Game.VELOCITY * elapsed_time), lastpos.y))
-      else if lastpos.direction == Game.SOUTH
-        player.addToLine(new paper.Point(lastpos.x + (Game.VELOCITY * elapsed_time), lastpos.y))
-      else if lastpos.direction == Game.EAST
-        player.addToLine(new paper.Point(lastpos.x, lastpos.y + (Game.VELOCITY * elapsed_time)))
-      else if lastpos.direction == Game.WEST
-        player.addToLine(new paper.Point(lastpos.x, lastpos.y - (Game.VELOCITY * elapsed_time)))
+      for player in @players
+        lastpos = player.lastPos()
+        if lastpos.direction == Game.NORTH
+          player.addToLine(new Point(lastpos.x - (Game.VELOCITY * elapsed_time), lastpos.y))
+        else if lastpos.direction == Game.SOUTH
+          player.addToLine(new Point(lastpos.x + (Game.VELOCITY * elapsed_time), lastpos.y))
+        else if lastpos.direction == Game.EAST
+          player.addToLine(new Point(lastpos.x, lastpos.y + (Game.VELOCITY * elapsed_time)))
+        else if lastpos.direction == Game.WEST
+          player.addToLine(new Point(lastpos.x, lastpos.y - (Game.VELOCITY * elapsed_time)))

-    @old_time = new_time
+      @old_time = new_time

-  add_listener: (listener) ->
-    @listeners.append(listener)
+    add_listener: (listener) ->
+      @listeners.append(listener)

-window.Game = Game
+  Game
+)


snow API changed, rich results now

snow API changed, rich results now

--- scripts/neige.coffee
+++ scripts/neige.coffee
@@ -9,7 +9,7 @@
 #   These are from the scripting documentation: https://github.com/github/hubot/blob/master/docs/scripting.md

-neigeUrl = "http://www.myweather2.com/developer/weather.ashx?uac=7fcYm4FvK/&output=json&uref=b10a3eb6-d28a-4194-b01a-36c07dbd5d2f"
+neigeUrl = "http://clientservice.onthesnow.com/externalservice/resort/2212/snowreport?token=a5bae8eea465aae810130937a29cebaa6cd92ce60336d8e3&language=fr&country=FR"

 module.exports = (robot) ->

@@ -26,13 +26,22 @@
         .header('Accept', 'application/json')
         .get() (err, res, body) ->
           data = JSON.parse(body)
+          var fall = data.report.snowfall.snow24h
+
+          if fall
+            msg.send "Elle a tombé : #{fall} cm de neige hier :-D B-)"
+
+          onSlope = data.report.snowQuality.onSlope
+          msg.send "En bas: #{onSlope.surfaceBottom}, #{onSlope.lowerDepth} cm"
+          msg.send "En haut: #{onSlope.surfaceTop}, #{onSlope.upperDepth} cm"
+
           msg.send "#{data.weather.snow_report[0].conditions}"
-     , 1000
+     , 10000

    robot.respond /stop neige/, (msg) ->
      if annoyIntervalId
        msg.send "OK, ça va..."
-       clearInterval(annoyIntervalId)
+       clearIntervamsg.send(annoyIntervalId)
        annoyIntervalId = null
      else
        msg.send "Impossible, les alertes ne sont pas activées"


Workspace: Fixed opening a url for preview pane.

--- client/app/CommonViews/workspace/panes/collaborativepreviewpane.coffee
+++ client/app/CommonViews/workspace/panes/collaborativepreviewpane.coffee
@@ -9,8 +9,7 @@
     {@previewer} = @previewPane

     if @isJoinedASession
-      @workspaceRef.once "value", (snapshot) =>
-        @previewer.openPath snapshot.val().url
+      @workspaceRef.once "value", (snapshot) => @openPath snapshot
     else
       @previewer.on "ready", => @saveUrl()

@@ -18,10 +17,13 @@

     @previewer.on "ViewerRefreshed",       => @saveUrl yes

-    @workspaceRef.on "value", (snapshot)   =>
-      @previewer.openPath snapshot.val().url
+    @workspaceRef.on "value", (snapshot)   => @openPath snapshot

     @workspaceRef.onDisconnect().remove() if @amIHost
+
+  openPath: (snapshot) ->
+    value = snapshot.val()
+    @previewer.openPath value.url  if value?.url

   saveUrl: (force) ->
     url = @previewer.path.replace /\?.*/, ""


client/landing: remove token from team create domain tab back link

--- client/landing/site.landing/coffee/team/forms/teamdomaintabform.coffee
+++ client/landing/site.landing/coffee/team/forms/teamdomaintabform.coffee
@@ -47,7 +47,7 @@
     @backLink = new kd.CustomHTMLView
       tagName      : 'span'
       cssClass     : 'TeamsModal-button-link back'
-      partial      : " Back"
+      partial      : " Back"

     @button = new kd.ButtonView
       title        : 'NEXT'


Fix the config

Fix the config

Add check for config[env] (since ENV=test was causing an error), move prod db to production config.

--- server/config.coffee
+++ server/config.coffee
@@ -11,13 +11,14 @@
       env: env
       templatePath: "#{__dirname}/../user/templates/"
       catchAll: yes
-    db: if env == "production" then process.env.MONGOHQ_URL else "mongodb://localhost/buckets_#{env}"
+    db: "mongodb://localhost/buckets_#{env}"
   production:
     smtp:
       service: 'Mandrill'
       auth:
         user: process.env.MANDRILL_APIKEY
         pass: process.env.MANDRILL_USERNAME
+    db: process.env.MONGOHQ_URL
   development:
     smtp:
       service: 'Gmail'
@@ -25,4 +26,7 @@
         user: 'your.email@gmail.com'
         pass: 'PASSWORD'

-module.exports = _.defaults(config[env], config.default)
+module.exports = if config[env]?
+  _.defaults(config[env], config.default)
+else
+  config.default


make hand work with sharing server

make hand work with sharing server

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -578,26 +578,25 @@
 	###########################################################################
 	# HANDS

-	supportsHandSwitching: ->
+	handSwitchingSupported: ->
 		return @_device.hands != undefined

 	nextHand: ->
-		if @supportsHandSwitching()
+		if @handSwitchingSupported()
 			hands = _.keys(@_device.hands)
 			if hands.length > 0
-				index = hands.indexOf(@selectedHand)
+				nextHandIndex = hands.indexOf(@selectedHand) + 1
 				nextHand = ""
-				nextHand = hands[index + 1] if index < (hands.length - 1)
+				nextHand = hands[nextHandIndex] if nextHandIndex < hands.length
 				return @setHand(nextHand)
 		return false

 	setHand: (hand) ->
 		@selectedHand = hand
-		return @handsImageLayer.image = "" if !hand or !@supportsHandSwitching()
+		return @handsImageLayer.image = "" if !hand or !@handSwitchingSupported()

 		handData = @_device.hands[hand]
 		if handData
-			resourceUrl = window.FramerStudioInfo.deviceImagesUrl
 			@hands.width = handData.width
 			@hands.height = handData.height
 			@hands.center()
@@ -605,8 +604,23 @@
 			@handsImageLayer.size = @hands.size
 			@handsImageLayer.y = 0
 			@handsImageLayer.y = handData.offset if handData.offset
-			@handsImageLayer.image = "#{resourceUrl}/#{hand}.png"
+			@handsImageLayer.image = @handImageUrl(hand)
 			return hand
+
+	handImageUrl: (hand) ->
+
+		# We want to get these image from our public resources server
+		resourceUrl = "//resources.framerjs.com/static/DeviceResources"
+
+		# If we're running Framer Studio and have local files, we'd like to use those
+		if Utils.isFramerStudio() and window.FramerStudioInfo
+			resourceUrl = window.FramerStudioInfo.deviceImagesUrl
+
+		# We'd like to use jp2 if possible, or check if we don't for this specific device
+		if Utils.isJP2Supported() and @_device.deviceImageJP2 is true
+			return "#{resourceUrl}/#{hand}.jp2"
+
+		return "#{resourceUrl}/#{hand}.png"

 ###########################################################################


Improved keeping track of previous background color

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -75,6 +75,7 @@
 		@background.clip = true
 		@background.backgroundColor = "transparent"
 		@background.classList.add("DeviceBackground")
+		@_previousBackgroundColor = @background.backgroundColor

 		@hands    = new Layer
 		@handsImageLayer = new Layer parent: @hands
@@ -370,7 +371,10 @@
 				@_previousBackgroundColor = @background.backgroundColor
 				@background.backgroundColor = @screen.backgroundColor
 				@screen.on "change:backgroundColor", (color) =>
+					# Hacky way to keep the prev backgroundColor
+					prev = @_previousBackgroundColor
 					@background.backgroundColor = color
+					@_previousBackgroundColor = prev
 				@background.on "change:backgroundColor", (color) =>
 					@_previousBackgroundColor = color
 					@background.backgroundColor = @screen.backgroundColor


Set default Slider Value to 0.5

Set default Slider Value to 0.5

--- framer/Components/SliderComponent.coffee
+++ framer/Components/SliderComponent.coffee
@@ -69,6 +69,9 @@
 			@fill.height = @height
 		else
 			@fill.width = @width
+
+		# Set initial value
+		if not @value then @value = 0.5

 		@fill.borderRadius = @sliderOverlay.borderRadius = @borderRadius


Implemented sequential file checksum/upload processing

--- client/import.coffee
+++ client/import.coffee
@@ -16,101 +16,78 @@
 UPLOAD_CHUNK_SIZE = 128 * 1024 # bytes
 DRAGGING_OVER_DOM = false
 fileBuffer = []
-
-currentUploadDocument = null
-
-uploadFetchOne = ->
-  return currentUploadDocument if currentUploadDocument
+# current element of checksum and upload queue
+currentDocument =
+  checksum: null
+  upload: null
+
+# observes files that are processed and removes them from buffer
+processedFiles = ImportingFile.documents.find
+  $or: [
+    finished: true
+  ,
+    errored: true
+  ]
+processedFiles.observe
+  added: (document) ->
+    delete fileBuffer[document._id]
+
+# verify or upload next file from queue
+Deps.autorun ->
   document = ImportingFile.documents.findOne
     finished: false
     errored: false
     sha256:
       $exists: true
-  currentUploadDocument = document
-  return currentUploadDocument
-
-uploadMarkComplete = (params) ->
-  console.log "uploadMarkComplete called "
-  ImportingFile.documents.update currentUploadDocument._id,
-    $set: params
-  delete fileBuffer[currentUploadDocument._id]
-  currentUploadDocument = null
-
-# observing files with computed checksums
-Deps.autorun ->
-  return if currentUploadDocument
-  document = uploadFetchOne()
-  return unless document
-
-  console.log "Got into upload autorun " + document.name
-
-  id = document._id
-  file = fileBuffer[id].file
-  fileContent = fileBuffer[id].fileContent
-
-  console.log "Creating publication for file " + document.name
+
+  # document will be null the first time it runs
+  # also, autorun will get called when document status changes
+  # if document id is unchanged it can return
+  return if not document or currentDocument.upload and currentDocument.upload._id == document._id
+  currentDocument.upload = document
+
   Meteor.call 'create-publication', document.name, document.sha256, (error, result) ->
     if error
-      uploadMarkComplete
-        errored: true
-        status: error.toString()
+      ImportingFile.documents.update document._id,
+        $set:
+          errored: true
+          status: error.toString()
       return

     if result.already
-      uploadMarkComplete
-        finished: true
-        status: "File already imported"
-        publicationId: result.publicationId
+      ImportingFile.documents.update document._id,
+        $set:
+          finished: true
+          status: "File already imported"
+          publicationId: result.publicationId
       return

     if result.verify
-      console.log "Verifying file " + document.name
-      verifyFile file, fileContent, result.publicationId, result.samples
+      verifyFile fileBuffer[document._id].file, fileBuffer[document._id].fileContent, result.publicationId, result.samples
     else
-      console.log "Uploading file " + document.name
-      uploadFile file, result.publicationId
-
-
-currentChecksumDocument = null
-
-hashFetchOne = ->
+      uploadFile fileBuffer[document._id].file, result.publicationId
+
+# calculate checksum for next file in queue
+Deps.autorun ->
   document = ImportingFile.documents.findOne
     preprocessed: true
     sha256:
       $exists: false
-  console.log document
-  currentChecksumDocument = document
-  return currentChecksumDocument
-
-hashMarkComplete = (params) ->
-  console.log "Called hashMarkComplete"
-  # setting hash value in collection so that autorun can move on
-  ImportingFile.documents.update currentChecksumDocument._id,
-    $set: params
-  currentChecksumDocument = null
-
-# observing documents ready for checksum computation
-Deps.autorun ->
-  return if currentChecksumDocument
-  document = hashFetchOne()
-  return unless document
-
-  console.log "Got into checksum computation autorun " + document.name
-  console.log document
-
-  id = document._id
-  currentId = id
-  fileContent = fileBuffer[id].fileContent
-
-  console.log "Computing checksum for " + document.name
-  ImportingFile.documents.update id,
+
+  # document will be null the first time it runs
+  # also, autorun will get called when document status changes
+  # if document id is unchanged it can return
+  return if not document or currentDocument.checksum and currentDocument.checksum._id == document._id
+  currentDocument.checksum = document
+
+  ImportingFile.documents.update document._id,
     $set:
       status: "Computing checksum"

   hash = new Crypto.SHA256
     onProgress: (progress) ->
       #TODO: update progressbar
-  hash.update fileContent, (error, result) ->
+  hash.update fileBuffer[document._id].fileContent, (error, result) ->
     #TODO: handle errors
     if error
       console.log "Import error: " + error.message
@@ -119,22 +96,10 @@
     if error
       console.log "Import error: " + error.message

-    alreadyImporting = ImportingFile.documents.findOne
-      sha256: result
-
-    if alreadyImporting
-      hashMarkComplete
-        finished: true
-        status: "File is already importing"
-        # publicationId might not yet be available, but let's try
-        publicationId: alreadyImporting.publicationId
-      return
-
-    console.log "Setting sha256 value"
-    # so that observer can do it's work
-    hashMarkComplete
-      sha256: result
-      status: "Checksum computed"
+    ImportingFile.documents.update document._id,
+      $set:
+        sha256: result
+        status: "Checksum computed"

 verifyFile = (file, fileContent, publicationId, samples) ->
   ImportingFile.documents.update file._id,
@@ -145,14 +110,16 @@
     new Uint8Array fileContent.slice sample.offset, sample.offset + sample.size
   Meteor.call 'verify-publication', publicationId, samplesData, (error) ->
     if error
-      uploadMarkComplete
-        errored: true
-        status: error.toString()
+      ImportingFile.document.update file._id,
+        $set:
+          errored: true
+          status: error.toString()
       return

-    uploadMarkComplete
-      finished: true
-      publicationId: publicationId
+    ImportingFile.document.update file._id,
+      $set:
+        finished: true
+        publicationId: publicationId

 uploadFile = (file, publicationId) ->
   ImportingFile.documents.update file._id,
@@ -167,14 +134,16 @@
   ,
     (error) ->
       if error
-        uploadMarkComplete
-          errored: true
-          status: error.toString()
+        ImportingFile.documents.update file._id,
+          $set:
+            errored: true
+            status: error.toString()
         return

-      uploadMarkComplete
-        finished: true
-        publicationId: publicationId
+      ImportingFile.documents.update file._id,
+        $set:
+          finished: true
+          publicationId: publicationId

 testPDF = (file, fileContent, callback) ->
   PDFJS.getDocument(data: fileContent, password: '').then callback, (message, exception) ->
@@ -184,7 +153,6 @@
         status: "Invalid PDF file"

 importFile = (file) ->
-  console.log "Import file called"
   reader = new FileReader()
   reader.onload = ->
     fileContent = @result
@@ -198,7 +166,6 @@
           status: "In queue for checksum computation"
           preprocessed: true

-  console.log "Inserting file into collection " + file.name
   ImportingFile.documents.insert
     name: file.name
     status: "Preprocessing file"


after pressing ` pop up a list of completions

--- client/keymap.coffee
+++ client/keymap.coffee
@@ -1,7 +1,52 @@
 do ->
+  ctid = null # autocompletion timeout id
+
+  window.onhelp = -> false # prevent IE from being silly
+
+  completions = []
+  Dyalog.reverseKeyMap = {}
+  CodeMirror.keyMap.dyalogBackquote =
+    auto: 'dyalog', nofallthrough: true, disableInput: true
+
+  ks = ''' `1234567890-=  ~!@#$%^&*()_+
+           qwertyuiop[]   QWERTYUIOP{}
+           asdfghjkl;\'\\ ASDFGHJKL:"|
+           zxcvbnm,./     ZXCVBNM<>?    '''.replace(/^\s*|\s*$/g, '').split /\s*/g
+
+  vs = ''' `¨¯<≤=≥>≠∨∧×÷  ⋄⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⌹
+           ?⍵∊⍴~↑↓⍳○*←→   ?⍵⍷⍴⍨↑↓⍸⍥⍣⍞⍬
+           ⍺⌈⌊_∇∆∘\'⎕⍎⍕⊢  ⍺⌈⌊_∇∆⍤⌸⌷≡≢⊣
+           ⊂⊃∩∪⊥⊤|⍝⍀⌿     ⊂⊃∩∪⊥⊤|⍪⍙⍠    '''.replace(/^\s*|\s*$/g, '').split /\s*/g
+
+  if ks.length != vs.length then console.error? 'bad configuration of backquote keymap'
+
+  for k, i in ks when k != (v = vs[i]) || k == '`'
+    completions.push text: v, displayText: v + ' `' + k + '   '
+    Dyalog.reverseKeyMap[v] = k
+    CodeMirror.keyMap.dyalogBackquote["'#{k}'"] = do (v = v) ->
+      (cm) ->
+        clearTimeout ctid; ctid = null; cm.replaceSelection v, 'end'
+        cm.state.completionActive?.close?()
+
   CodeMirror.keyMap.dyalog =
     fallthrough: 'default'
-    "'`'": (cm) -> cm.setOption 'keyMap', 'dyalogBackquote'
+    "'`'": (cm) ->
+      cm.setOption 'keyMap', 'dyalogBackquote'
+      clearTimeout ctid
+      ctid = setTimeout(
+        ->
+          c = cm.getCursor()
+          cm.showHint
+            completeOnSingleClick: true
+            hint: ->
+              data = list: completions, from: c, to: c
+              CodeMirror.on data, 'close', -> cm.setOption 'keyMap', 'dyalog'
+              data
+          clearTimeout ctid; ctid = null
+          return
+        500
+      )
+
     F1: (cm) ->
       c = cm.getCursor(); s = cm.getLine(c.line).toLowerCase()
       u =
@@ -24,24 +69,4 @@
       popup.focus?()
       return

-  window.onhelp = -> false # prevent IE from being silly
-
-  Dyalog.reverseKeyMap = {}
-  CodeMirror.keyMap.dyalogBackquote =
-    auto: 'dyalog', nofallthrough: true, disableInput: true
-
-  ks = ''' `1234567890-=  ~!@#$%^&*()_+
-           qwertyuiop[]   QWERTYUIOP{}
-           asdfghjkl;\'\\ ASDFGHJKL:"|
-           zxcvbnm,./     ZXCVBNM<>?    '''.split /\ */g
-
-  vs = ''' `¨¯<≤=≥>≠∨∧×÷  ⋄⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⌹
-           ?⍵∊⍴~↑↓⍳○*←→   ?⍵⍷⍴⍨↑↓⍸⍥⍣⍞⍬
-           ⍺⌈⌊_∇∆∘\'⎕⍎⍕⊢  ⍺⌈⌊_∇∆⍤⌸⌷≡≢⊣
-           ⊂⊃∩∪⊥⊤|⍝⍀⌿     ⊂⊃∩∪⊥⊤|⍪⍙⍠    '''.split /\ */g
-
-  for i in [0...ks.length] by 1
-    Dyalog.reverseKeyMap[vs[i]] = ks[i]
-    CodeMirror.keyMap.dyalogBackquote["'#{ks[i]}'"] = do (v = vs[i]) ->
-      (cm) -> cm.replaceSelection v, 'end'
   return


Fix postmessage

Fix postmessage

--- client/remote.coffee
+++ client/remote.coffee
@@ -4,7 +4,8 @@

 class Remote

-  constructor: -> _.extend @, Backbone.Events
+  constructor: ->
+    _.extend @, Backbone.Events

   update: (data) ->
     @post "update", data
@@ -24,9 +25,9 @@
   post: (cmd, args...) ->
     w = @iframe.get(0).contentWindow
     console.log "posting to iframe", @iframe, w
-    w.postMessage JSON.stringify
+    w.postMessage JSON.stringify(
       cmd: cmd
-      args: args
+      args: args)
     , "*"

@@ -56,7 +57,7 @@
   listenParentWindow: ->
     console.log "listening window"
     $(window).bind "message", (e) =>
-      console.log "getting from parent", e
+      console.log "getting from parent window", e
       cmdOb = JSON.parse e.originalEvent.data
       @trigger "cmd", cmdOb


Add a spinner to the vote page as well

Add a spinner to the vote page as well

--- client/routes.coffee
+++ client/routes.coffee
@@ -19,7 +19,8 @@
   'speaker',
   'speakers',
   'sponsors',
-  'about'
+  'about',
+  'vote'
 ]})

 Router.after(filters.resetScroll, {except:['wishes', 'proposals', 'speakers']});


Added new steps

Added new steps

--- snippets/modify.cson
+++ snippets/modify.cson
@@ -29,3 +29,9 @@
    'modify9':
      'prefix': 'modify the element found by "identifier" by setting the attribute "attribute-name" to "new attribute value"'
      'body': 'modify the element found by "$1" by setting the attribute "$2" to "$3"'
+   'modify10':
+     'prefix': 'modify the alias "alias" by converting it to upper case'
+     'body': 'modify the alias "alias" by converting it to upper case'
+  'modify11':
+     'prefix': 'modify the alias "alias" by converting it to lower case'
+     'body': 'modify the alias "alias" by converting it to lower case'


enabled remote scripts in browsers

enabled remote scripts in browsers

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -71,14 +71,27 @@
   upcomingInput: -> ""

 # Activate CoffeeScript in the browser by having it compile and evaluate
-# all script tags with a content-type of `text/coffeescript`. This happens
-# on page load. Unfortunately, the text contents of remote scripts cannot be
-# accessed from the browser, so only inline script tags will work.
-if document? and document.getElementsByTagName
+# all script tags with a content-type of `text/coffeescript`.
+# This happens on page load.
+if document?.getElementsByTagName
+  grind = (coffee) ->
+    setTimeout exports.compile coffee, noWrap: true
+  grindRemote = (url) ->
+    xhr = new (window.ActiveXObject or XMLHttpRequest)('Microsoft.XMLHTTP')
+    xhr.open 'GET', url, true
+    xhr.overrideMimeType 'text/plain' if 'overrideMimeType' of xhr
+    xhr.onreadystatechange = ->
+      grind xhr.responseText if xhr.readyState is 4
+    xhr.send null
   processScripts = ->
-    for tag in document.getElementsByTagName('script') when tag.type is 'text/coffeescript'
-      eval exports.compile tag.innerHTML
+    for script in document.getElementsByTagName 'script'
+      if script.type is 'text/coffeescript'
+        if script.src
+          grindRemote script.src
+        else
+          grind script.innerHTML
+    null
   if window.addEventListener
-    window.addEventListener 'load', processScripts, false
-  else if window.attachEvent
-    window.attachEvent 'onload', processScripts
+    addEventListener 'DOMContentLoaded', processScripts, false
+  else
+    attachEvent 'onload', processScripts


code & html indentation

code & html indentation

--- coffee/parser.coffee
+++ coffee/parser.coffee
@@ -238,27 +238,37 @@
       if not @node?
        throw new Error 'Invalid indentation'

+     if @node.type is TYPES.list
+      if line.type isnt TYPES.list
+       @node = @node.parent()
+
      switch line.type
       when TYPES.codeBlock
        prev = @node
+       indent = line.indentation + 1
        @addNode new CodeBlock indentation: 0
        while true
         @reader.next()
         break unless @reader.has()
         line = @reader.get()
+        if not line.empty and line.indentation < indent
+         indent = line.indentation
         break if line.type is TYPES.codeBlock
-        @node.addText line.line
+        @node.addText line.line.substr indent
        @node = prev

       when TYPES.html
        prev = @node
+       indent = line.indentation + 1
        @addNode new Html indentation: 0
        while true
         @reader.next()
         break unless @reader.has()
         line = @reader.get()
+        if not line.empty and line.indentation < indent
+         indent = line.indentation
         break if line.type is TYPES.html
-        @node.addText line.line
+        @node.addText line.line.substr indent
        @node = prev


Add api option to invoke gc

Add api option to invoke gc

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -67,6 +67,10 @@
       totalmem: os.totalmem()
       freemem: os.freemem()

+app.get '/system/gc', (req, res, next) ->
+  global.gc() if typeof global.gc is 'function'
+  res.end()
+
 app.param 'objectid', document.param
 app.all '/:collection/:objectid', (req, res, next) ->
   switch req.method
@@ -90,6 +94,7 @@
 MongoClient.connect process.env.MONGO_URI, (err, db) ->
   return err if err
   app.set 'db', db
+
   if not module.parent
     app.listen app.get 'port'
     console.log "Server is listening on port #{app.get('port')}"


Fix connect 3 deprectated warning

Fix connect 3 deprectated warning

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -28,7 +28,7 @@
 app.set('json spaces', 0) if app.get('env') isnt 'testing'
 app.use(express.compress())
 app.use(express.methodOverride())
-app.use(express.bodyParser())
+app.use(express.json())
 app.disable('x-powered-by')
 app.enable('verbose errors')
 app.set 'port', process.env.PORT_WWW or 8080


Fixing issue #1046.

Fixing issue #1046.

--- lexer.coffee
+++ lexer.coffee
@@ -439,10 +439,11 @@
         nested = new Lexer().tokenize inner, line: @line, rewrite: off
         nested.pop()
         nested.shift() if nested[0]?[0] is 'TERMINATOR'
-        if nested.length > 1
-          nested.unshift ['(', '(']
-          nested.push    [')', ')']
-        tokens.push ['TOKENS', nested]
+        if len = nested.length
+          if len > 1
+            nested.unshift ['(', '(']
+            nested.push    [')', ')']
+          tokens.push ['TOKENS', nested]
       i += expr.length
       pi = i + 1
     tokens.push ['NEOSTRING', str.slice pi] if i > pi < str.length


minimal changes

minimal changes

--- source/parrot.coffee
+++ source/parrot.coffee
@@ -1,5 +1,5 @@
 'use strict'

 parrot = @parrot =
-  version  : '0.12.12'
-  $        : if $$? then $$ else $ # DOM Handler Facade
+  version : '0.12.12'
+  $       : if $$? then $$ else $ # DOM Handler Facade


typo2

--- routes/todoist.coffee
+++ routes/todoist.coffee
@@ -19,7 +19,7 @@
      # Add
       console.log "add"

-  getProjects req, (req, r)->
+  get_projects req, (req, r)->
     projects = ""
     first = true
     for item in r


fix use of filtering on server for getting public paths

--- salvus/rethink.coffee
+++ salvus/rethink.coffee
@@ -1158,18 +1158,18 @@
         if not @_validate_opts(opts) then return
         # TODO: filter disabled on server not on client!
         query = @table('public_paths').getAll(opts.project_id, index:'project_id')
-        query.filter(@r.row("disabled").eq(false), {default: false}).pluck('path').run (err, v) =>
+        query.filter(@r.row("disabled").eq(false).default(true)).pluck('path').run (err, v) =>
             if err
                 opts.cb(err)
             else
-                opts.cb(undefined, (x.path for x in v when not x.disabled))
+                opts.cb(undefined, (x.path for x in v))

     has_public_path: (opts) =>
         opts = defaults opts,
             project_id  : required
             cb          : required    # cb(err, has_public_path)
         query = @table('public_paths').getAll(opts.project_id, index:'project_id')
-        query.filter(@r.row("disabled").eq(false), {default: false}).count().run (err, n) =>
+        query.filter(@r.row("disabled").eq(false).default(true)).count().run (err, n) =>
             opts.cb(err, n>0)

     path_is_public: (opts) =>


Tweaked regex.

Tweaked regex.

--- scripts/shipitharder.coffee
+++ scripts/shipitharder.coffee
@@ -123,7 +123,7 @@

   # Enable a looser regex if environment variable is set
   if process.env.HUBOT_SHIP_EXTRA_CAPYBARAS
-    regex = /ship(ping|z|s|ped)?\s*it\s*harder/i
+    regex = /ship(ping|z|s|ped)?\s*it\s*(the)?\s*hard(er|est)?/i
   else
     regex = /ship\s*it\s*harder/i


Inline emoji instead of String.fromCodePoint()

The latter is not available on Node.js 0.x.

--- spec/e2e_spec.coffee
+++ spec/e2e_spec.coffee
@@ -179,12 +179,11 @@

   it 'should builder supports emojis in cdata tags', ->
-    emoji = String.fromCodePoint(0x1F926)
-    builder.testCase().standardOutput('Emoji: ' + emoji)
+    builder.testCase().standardOutput('Emoji: 🤦')

     expect(builder.build()).toBe reportWith(
       '  <testcase>\n' +
       '    <system-out>\n' +
-      '      <![CDATA[Emoji: ' + emoji + ']]>\n' +
+      '      <![CDATA[Emoji: 🤦]]>\n' +
       '    </system-out>\n' +
       '  </testcase>')


Fixed gamespec

Fixed gamespec

--- spec/gamespec.coffee
+++ spec/gamespec.coffee
@@ -1,5 +1,5 @@
 requirejs.config({
-    baseUrl: '../',
+    #baseUrl: 'beamwars/',
     #paths: {
     #  'paper': 'lib/paper'
     #}
@@ -10,7 +10,7 @@
     #}
 })

-require(['position', 'game', 'synchronizedtime', 'point'], (Position, Game, SynchronizedTime, Point) ->
+define(['position', 'game', 'synchronizedtime', 'point'], (Position, Game, SynchronizedTime, Point) ->
   describe "Game", ->
     game = null


trim spaces from input

trim spaces from input

--- scripts/bungie.coffee
+++ scripts/bungie.coffee
@@ -9,7 +9,11 @@

   # executes when any text is directed at the bot
   robot.respond /(.*)/i, (res) ->
-    input = res.match[1].split ' '
+    array = res.match[1].split ' '
+
+    # trims spaces and removes empty elements in array
+    input = []
+    input.push el.trim() for el in array when (el.trim() isnt "")

     unless input.length is 2 or input.length is 3
       message = "Something didn't look right... Read more about using the bot here:\nhttps://github.com/phillipspc/showoff/blob/master/README.md"


activity: transform tag tokens [fixes #80007018]

Discards spaces in token name.

--- client/Main/utils.coffee
+++ client/Main/utils.coffee
@@ -721,6 +721,7 @@
   formatContent: (body = '') ->

     fns = [
+      KD.utils.transformTagTokens
       KD.utils.transformTags
       KD.utils.formatBlockquotes
       KD.utils.applyMarkdown
@@ -730,6 +731,17 @@
     body = KD.utils.expandUsernames body, 'code'

     return body
+
+
+  transformTagTokens: (text = '') ->
+
+    tokenPattern = /\|#:JTag:.*?:(.*?)\|/g
+
+    return text  unless tokenPattern.test text
+
+    text.replace tokenPattern, (match, name) ->
+
+      return "##{name.replace ' ', ''}"

   transformTags: (text = '') ->


Lol, probably want to hide these when... hiding.

--- client/file-modal.coffee
+++ client/file-modal.coffee
@@ -112,10 +112,10 @@

 Template.currentFile.rendered = ->
   @$('#currentFile').on 'hidden.bs.modal', (event) ->
+    $('[data-toggle="popover"]').popover 'hide'
     Session.set 'creds', null
     Session.set 'copyCommand', null
     Session.set 'currentFile', null
   @$('#currentFile').on 'shown.bs.modal', (event) ->
     $('#table-name').focus()
-    $('[data-toggle="popover"]').popover 'hide'
     getCreds()


Router: Refactor collaboration route handling

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -146,13 +146,7 @@
       KD.getSingleton('router').handleRoute "/IDE/#{machineLabel}/#{workspaceSlug}"

-  routeToLatestWorkspace = (options = {}) ->
-
-    {params}       = options
-    {machineLabel} = params  if params
-
-    # we assume that if machineLabel is all numbers it is the channelId - SY
-    return loadCollaborativeIDE machineLabel  if machineLabel and /^[0-9]+$/.test machineLabel
+  routeToLatestWorkspace = ->

     machine = KD.userMachines.first
     return putVMInWorkspace machine  if machine
@@ -215,17 +209,25 @@

   KD.registerRoutes 'IDE',

-    '/:name?/IDE': (rest...) ->
-      refreshWorkspaces -> routeToLatestWorkspace rest...
-
-    '/:name?/IDE/:machineLabel': (rest...) ->
-      refreshWorkspaces -> routeToLatestWorkspace rest...
-
-    '/:name?/IDE/:machineLabel/:workspaceSlug': (routeInfo) ->
-
-      {params} = routeInfo
+    '/:name?/IDE': ->
+      refreshWorkspaces -> routeToLatestWorkspace()
+
+    '/:name?/IDE/:machineLabel': ({params}) ->
+
       {machineLabel} = params

+      # we assume that if machineLabel is all numbers it is the channelId - SY
+      if /^[0-9]+$/.test machineLabel
+        refreshWorkspaces -> loadCollaborativeIDE machineLabel
+      else if machine = getMachine machineLabel
+        putVMInWorkspace machine
+      else
+        routeToLatestWorkspace()
+
+    '/:name?/IDE/:machineLabel/:workspaceSlug': ({params}) ->
+
+      {machineLabel} = params
+
       refreshWorkspaces ->

         findWorkspace params, (workspace) ->
@@ -233,9 +235,13 @@
           params.username = KD.nick()

           if workspace
-          then loadWorkspace params, workspace
+            return loadWorkspace params, workspace
           else
             for machine in KD.userMachines when machine.label is machineLabel
-              return loadWorkspace params
-
-            routeToLatestWorkspace {params}
+              break
+
+            if machine
+            then putVMInWorkspace machine
+            else loadWorkspace params
+
+          routeToLatestWorkspace()


Router: Simplify `routeToLatestWorkspace` function

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -151,16 +151,26 @@

   routeToLatestWorkspace = ->

-    machine = KD.userMachines.first
-    return putVMInWorkspace machine  if machine
-
-    KD.singletons.computeController.fetchMachines (err, machines)->
-
-      if err or not machines.length
-        KD.getSingleton('router').handleRoute "/IDE/koding-vm-0/my-workspace"
-        return
-
-      putVMInWorkspace machines.first
+    router = KD.getSingleton 'router'
+
+    if latestWorkspace = getLatestWorkspace()
+      {machineLabel, workspaceSlug, channelId} = latestWorkspace
+    else
+      return routeToFallback()
+
+    if channelId
+      for workspace in KD.userWorkspaces when workspace.channelId is channelId
+        return router.handleRoute "/IDE/#{channelId}"
+      routeToFallback()
+
+    else if machineLabel and workspaceSlug
+      return routeToFallback()  unless machine = getMachine machineLabel
+
+      findWorkspace {workspaceSlug, machineLabel}, (workspace) ->
+
+        if workspace
+        then router.handleRoute "/IDE/#{machineLabel}/#{workspaceSlug}"
+        else routeToMachineWorkspace machine

   loadCollaborativeIDE = (id) ->


Finder: Handle route to mount VM specific filetree.

--- client/ide/routes.coffee
+++ client/ide/routes.coffee
@@ -3,14 +3,21 @@
   KD.registerRoutes 'IDE',

     '/:name?/IDE' : ({params:{name}, query})->
+
       router = KD.getSingleton 'router'
       router.openSection 'IDE', name, query

-    '/:name?/IDE/VM' : ({params:{name}, query})->
-      KD.singletons.router.handleRoute 'IDE'
+    '/:name?/IDE/VM/:slug' : ({params:{name, slug}, query})->

-    '/:name?/IDE/VM/:slug' : ({params:{name, slug}, query})->
-      {appManager} = KD.singletons
-      callback = (app) ->
-        log '@acet handle this VM please', slug
-      appManager.open 'IDE', callback
+      appManager  = KD.getSingleton 'appManager'
+      ideApps     = appManager.appControllers.IDE
+      fallback    = ->
+        appManager.open 'IDE', { forceNew: yes }, (app) ->
+          app.mountedMachineUId = slug
+
+      return fallback()  unless ideApps?.instances
+
+      for instance in ideApps.instances when instance.mountedMachineUId is slug
+          ideInstance = instance
+
+      if ideInstance then appManager.showInstance ideInstance else fallback()


Accept multi emoji passing on emoji action

--- scripts/github.coffee
+++ scripts/github.coffee
@@ -17,19 +17,26 @@
     clearEmojiCache msg

 emojiMe = (msg, cb) ->
-  emoji = msg.match[1].replace(/^[ ]*/, "").replace(/[ ]*$/, "")
+  emoji_arr = msg.match[1]
+    .replace(/^[ ]*/, "").replace(/[ ]*$/, "")
+    .split(/\s+/)
   if fs.existsSync(cachefile)
     body = fs.readFile cachefile, (err, data) ->
       emojis = JSON.parse(data)
-      emoji_url = emojis[emoji]
-      cb emoji_url || "ないよ"
+      emoji_url = []
+      for emoji in emoji_arr
+        emoji_url.push emojis[emoji]
+      cb emoji_url.join("\n") || "ないよ"
   else
     msg.http('https://api.github.com/emojis')
       .get() (err, res, body) ->
         fs.writeFile cachefile, body
         emojis = JSON.parse(body)
-        emoji_url = emojis[emoji]
-        cb emoji_url || "ないよ"
+        emoji_url = []
+        for emoji in emoji_arr
+          emoji_url.push emojis[emoji]
+        cb emoji_url.join("\n") || "ないよ"
+

 clearEmojiCache = (msg) ->
   fs.unlinkSync cachefile


Add more classy insults

Add more classy insults

--- scripts/insult.coffee
+++ scripts/insult.coffee
@@ -15,6 +15,14 @@

 insults = [
+  "May the chocolate chips in your cookies always turn out to be raisins.",
+  "May every sock you wear be slightly rotated, just enough for it to be uncomfortable.",
+  "May your mother come to talk to you, and then leave your door slightly ajar, so that you have to get up and close it.",
+  "May your article load that extra little bit as you're about to click a link so you click an ad instead.",
+  "May both sides of your pillow be warm.",
+  "May you forever feel your cellphone vibrating in the pocket it's not even in.",
+  "May you always get up from your computer with your headphones still attached.",
+  "May your chair produce a sound similar to a fart, but only once, such that you cannot reproduce it to prove that it was just the chair.",
   "I don’t believe in plastic surgery, But in your case, Go ahead.",
   "People like you are the reason we have middle fingers.",
   "Why Don’t You Slip Into Something More Comfortable. Like A Coma?",


Fix logout redirect

Fix logout redirect

--- routes/api.coffee
+++ routes/api.coffee
@@ -70,7 +70,7 @@

 router.get '/logout', (req, res) ->
   req.logout()
-  res.redirect '/login'
+  res.redirect '/admin'

 router.get "/getUser", (req, res) ->
   res.respond req.user


Pre-array check.

Pre-array check.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -108,7 +108,7 @@
     robot.respond /top (\d*)$/i, (msg) ->
         if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
             pointcount = msg.match[1]
-            leaders = pointcount([username + " with " + points[username] + " points."])
+            # leaders = pointcount([username + " with " + points[username] + " points."])

-            msg.send "The top " + pointcount " users with the most points are: " + leaders
+            msg.send "The top " + pointcount " users with the most points are: "
             return


testing live

testing live

--- scripts/trello.coffee
+++ scripts/trello.coffee
@@ -35,6 +35,7 @@
 module.exports = (robot) ->
   robot.respond /trello add (.*)/i, (msg) ->
     subject = msg.match[1]
+    msg.send subject
     unless process.env.TRELLO_API_KEY?
       msg.send "Please specify the Trello API key in TRELLO_API_KEY"
       return
@@ -49,6 +50,7 @@

   robot.hear /^we (sh|c)ould (.*)/i, (msg) ->
     subject = msg.match[2]
+    msg.send subject
     unless process.env.TRELLO_API_KEY?
       msg.send "Please specify the Trello API key in TRELLO_API_KEY"
       return


Remove landing canvas from DOM when destroyed. Fixes #211

--- client/index.coffee
+++ client/index.coffee
@@ -186,3 +186,4 @@
   triangles = []
   renderer = undefined
   $('body').removeClass 'landing'
+  $('body canvas').remove()


Removed test for deleted "Last updated by..." template

--- client/test/src/report_index.coffee
+++ client/test/src/report_index.coffee
@@ -27,23 +27,6 @@

 test("Shows message when no reports are available")
 test("Can see the formatted completed_at datetime")
-
-test("Can see creator for each report", ->
-  author = "Kanye Quest"
-
-  reports = [{
-    author: author
-  }]
-
-  view = createAndShowReportListView(reports)
-
-  assert.match(
-    $('#test-container').text(),
-    new RegExp(".*#{author}.*")
-  )
-
-  view.close()
-)

 test("Can see if report is bookmarked/favourited", ->
   reports = [{


Don't show slider label in semiQuant mode

[#112525649]

--- src/code/views/value-slider-view.coffee
+++ src/code/views/value-slider-view.coffee
@@ -107,10 +107,13 @@
       "font-size": "#{@props.handleSize / 2}px"
       "top": "#{top}px"
       "left": centerOfDiv # margin will take care of the rest?
+    if not @props.displaySemiQuant
+      label = @renderNumber()
+    else label = null
     (div {},
       (div {className: "value-slider-handle", style: style, ref: "handle"},
         (i {className: "icon-codap-smallSliderLines"})
-        @renderNumber()
+        { label }
       )
     )


remove dependancies and upgrade to handlers

--- src/coffee/services/registration.coffee
+++ src/coffee/services/registration.coffee
@@ -9,55 +9,48 @@
 angular.module('CoolFormServices').
   factory('registrationService', (validators) ->

-    return ()->
-
+    return ()->
+
       fields = {}
       errors ={}
       values = {}
-      errorHandlers = {}
-      okHandlers = {}
-      # dependancies = {}
+      handlers =
+        ok: {}
+        errors: {}
+        change: {}

-      # setDependancies = (field) ->
-      #   if field.dependancies?
-      #     for dep in field.dependancies
-      #       if !dependancies[dep]? then dependancies[dep] = []
-      #       dependancies[dep].push(field.name)
-
+      initValidation = (field) ->
+        if field.validation?
+          for rule in field.validation
+            if validators[rule.validator]? and validators[rule.validator].init?
+              validators[rule.validator].init(field.name, rule, services)
+
       setFields = (pages) ->
         p = 0
         for page in pages
           for line in page.lines
             for field in line.fields
               fields[field.name] = field
-              # setDependancies(field)
+              initValidation(fields[field.name])
           p += 1

-      sendError = (fieldName, error) ->
-        if !errorHandlers[fieldName]? then return
-        for handler in errorHandlers[fieldName]
-          handler(error)
-
-      sendOk = (fieldName) ->
-        if !okHandlers[fieldName]? then return
-        for handler in okHandlers[fieldName]
-          handler()
+      handle = (fieldName, h, data) ->
+        if !h[fieldName]? then return
+        for fn in h[fieldName]
+          if data? then fn(data) else fn()

       removeError = (fieldName) ->
         delete errors[fieldName]
-        # if dependancies[fieldName]?
-        #   for dep in dependancies[fieldName]
-        #     removeError(dep)
-        sendOk(fieldName)
+        handle(fieldName, handlers.ok)

       validateField = (fieldName) ->
         field = fields[fieldName]
         if field.validation?
           for rule in field.validation
-            if validators[rule.validator]?
-              if validators[rule.validator](fieldName, values, rule) is false
+            if validators[rule.validator] and validators[rule.validator].validator?
+              if validators[rule.validator].validator(fieldName, values, rule) is false
                 errors[fieldName] = rule.options.message
-                sendError(fieldName, rule.options.message)
+                handle(fieldName, handlers.errors,rule.options.message)
                 return false
         removeError(fieldName)
         return true
@@ -73,22 +66,24 @@
       valueChange = (fieldName, value) ->
         values[fieldName] = value
         removeError(fieldName)
+        handle(fieldName, handlers.change, value)

       addHandler = (field, fn, list) ->
         if !list[field]? then list[field] = []
         if fn? then list[field].push(fn)

+      watchField = (fieldName, onOk, onError, onChange) ->
+        if onOk? then addHandler(fieldName, onOk, handlers.ok)
+        if onError? then addHandler(fieldName, onError, handlers.errors)
+        if onChange? then addHandler(fieldName, onChange, handlers.change)
+
+      registerField = (fieldName, onOk, onError, onChange) ->
+        watchField(fieldName, onOk, onError, onChange)
+        return (value) ->
+          valueChange(fieldName, value)
+
       registerController = (controllerScope, form) ->
         setFields(form.pages)
-
-      watchFieldValidation = (fieldName, onOk, onError) ->
-        if onOk? then addHandler(fieldName, onOk, okHandlers)
-        if onError? then addHandler(fieldName, onError, errorHandlers)
-
-      registerField = (fieldName, onOk, onError) ->
-        watchFieldValidation(fieldName, onOk, onError)
-        return (value) ->
-          valueChange(fieldName, value)

       services =
         registerController: registerController
@@ -96,7 +91,7 @@
         validateField: validateField
         validateFields: validateFields
         validateAllFields: validateAllFields
-        watchFieldValidation: watchFieldValidation
+        watchField: watchField

       return services
   )


fix problem where some private discussions aren't purged from the homepage on logout

Former-commit-id: 9b2bcacb53a4bf8d850cde3950165d01d4e9c4e7 [formerly 37916f0ef9713ea00eff49aa44ca2d7f74606df8] [formerly 31c604b8eae34b453dee4d28305fdf3f94f79d0e [formerly c2826de7984b470ed7d552a85d5df412eebcd59f]] [formerly 44a80ce237ae7476598fa377d18fe562d1bdee0b [formerly 828a380af0757961b7071eb71b0c970d1de66746] [formerly 17554053fb9fa949f8cceda9315102d66caceeee [formerly 17554053fb9fa949f8cceda9315102d66caceeee [formerly da131d5abc9f8796df750f6067382fd16cf1ce52]]]]
Former-commit-id: 164825fd7e6edba9b0b12545d04286eabf9fdeca [formerly 419e133b00d375388f95e27cc86adb2ab2b67d08] [formerly a1141d1e30f575c13a93905a589e2dd6f753c112 [formerly 9c6b9659a555add4c384e82435f164c170fcb7fe]]
Former-commit-id: 6db687dc29205ee76edbef1c33d3f9a7839fab24 [formerly d0d6572ec01dcaa2b19ef705bcef039281e5bca5]
Former-commit-id: 12129263b1f91de1a33299049493a64ace47799a
Former-commit-id: 8ddb2b27173eb788f52cbf4de3ee637bc1f595f4

--- @client/models/proposal.coffee
+++ @client/models/proposal.coffee
@@ -174,7 +174,7 @@
     initialize : (options = {}) ->
       super options
       @listenTo App.vent, 'user:signout', =>
-        @purge_inaccessible()
+        @purge_inaccessible()

     url : ->
       Routes.proposals_path( )
@@ -195,6 +195,7 @@
     purge_inaccessible : ->
       to_remove = @filter (p) ->
         p.get('publicity') < 2 || !p.get('published')
+
       @remove to_remove

@@ -202,7 +203,10 @@
     state:
       currentPage: 1

-    initialize : ->
+    initialize : (options = {}) ->
+      @listenTo App.vent, 'user:signout', =>
+        @purge_inaccessible()
+
       @state.pageSize = App.request('tenant').get('num_proposals_per_page')

   API =


insights: Fix lint error

insights: Fix lint error

--- src/insights-download-timeperiod.coffee
+++ src/insights-download-timeperiod.coffee
@@ -20,7 +20,7 @@
     fs.stat file, (err, stat) ->
       return callback err if err
       hasData = stat.size > 1
-      return callback null, hasData
+      return callback null, hasData

 dateOnlyString = (date) ->
   return date.toISOString().split('T')[0]
@@ -86,4 +86,4 @@
       console.log failures
     process.exit exitCode

-main() if not module.parent
+main() if not module.parent


AceView: Defensive check for panes which has no data, like Terminal etc.

--- client/ace/lib/aceview.coffee
+++ client/ace/lib/aceview.coffee
@@ -74,7 +74,7 @@

     { handles, panes } = @getDelegate().tabView

-    for pane in panes when pane.getData().path is path
+    for pane in panes when pane.getData()?.path is path

       { tabHandle } = pane
       tabHandle.setClass 'saved'


Fix bug uploading pictures on new Picture page.

--- app/assets/javascripts/app/photos.js.coffee
+++ app/assets/javascripts/app/photos.js.coffee
@@ -11,7 +11,6 @@
     @input = @el.find('input[type="file"]').change @select
     @url = @el.data('upload-url')
     @id = @el.data('object-id')
-    @album_id = @el.parents('form').find('[name="album_id"]').val()

   browse: (e) =>
     e.preventDefault()
@@ -44,6 +43,7 @@

   upload: (e) =>
     e?.preventDefault()
+    @album_id = @el.parents('form').find('[name="album_id"]').val()
     @files = if @input.attr('multiple')
       @input[0].files
     else


remove coffee file

remove coffee file

--- app/assets/javascripts/autoselect.js.coffee
+++ app/assets/javascripts/autoselect.js.coffee
@@ -1,79 +0,0 @@
-{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170
-{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
-{\colortbl;\red255\green255\blue255;}
-\margl1440\margr1440\vieww10800\viewh8400\viewkind0
-\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural
-
-\f0\fs24 \cf0 do ->\
-\
-  AutoSelect = (options) ->\
-    @options = if options != null then options else \{\}\
-    @$input = $(@options['input'])\
-    @width = @options['width'] or 240\
-    @placeholder = @options['placeholder']\
-    return\
-\
-  AutoSelect::init = ->\
-    scope = undefined\
-    url = undefined\
-    scope = @$input.attr('data-scope')\
-    url = @$input.attr('data-url')\
-    @$input.select2\
-      placeholder: @placeholder\
-      allowClear: true\
-      minimumInputLength: 1\
-      width: @width\
-      ajax:\
-        url: url\
-        quietMillis: 300\
-        data: (term, page) ->\
-          \{\
-            q: term\
-            page_limit: 10\
-            page: page\
-            scope: scope\
-          \}\
-        results: (data, page) ->\
-          more = undefined\
-          more = data.length >= 10\
-          \{\
-            results: data\
-            more: more\
-          \}\
-      formatResult: ((_this) ->\
-        (item, container, query, escapeFn) ->\
-          markup = undefined\
-          text = undefined\
-          markup = []\
-          window.Select2.util.markMatch _this.detailText(item), query.term, markup, escapeFn\
-          text = markup.join('')\
-      )(this)\
-      formatSelection: ((_this) ->\
-        (item, container, escapeFn) ->\
-          _this.detailText item\
-      )(this)\
-      initSelection: ((_this) ->\
-        (e, callback) ->\
-          id = undefined\
-          id = $(e).val()\
-          if id != ''\
-            return $.getJSON(url, id: id).done((data) ->\
-              callback data\
-              _this.$input.trigger 'autocomplete:ajax:success', data\
-            )\
-          return\
-      )(this)\
-    @$input.change 'select2-selecting', ->\
-      $(this).closest('.filter_form').submit()\
-\
-  AutoSelect::detailText = (item) ->\
-    _.reject(_.uniq(_.toArray(item)), (el) ->\
-      el == ''\
-    ).join ' - '\
-\
-  AutoSelect\
-# ---\
-# generated by coffee-script 1.9.2\
-\
-# ---\
-# generated by js2coffee 2.1.0}


take a stab at fixing notification bubbles

--- app/assets/javascripts/notifications.coffee
+++ app/assets/javascripts/notifications.coffee
@@ -27,9 +27,9 @@
 setInterval ->
   total = 0

-  $("#channel-list .unread_highlight").each (_, el) ->
+  $("#channel-list .unread_highlights").each (_, el) ->
     total += parseInt($(el).text())
-  $("#im-list .unread_highlight").each (_, el) ->
+  $("#im-list .unread_highlights").each (_, el) ->
     total += parseInt($(el).text())

   # This can cause weird re-bounces in situations like reading messages in


Listen to workflow changes before setting criteria

...variables so they always reflect the current workflow

Closes #1122

--- app/components/retirement-rules-editor.cjsx
+++ app/components/retirement-rules-editor.cjsx
@@ -1,4 +1,5 @@
 React = require 'react'
+ChangeListener = require './change-listener'

 module.exports = React.createClass
   displayName: 'RetirementRulesEditor'
@@ -16,19 +17,21 @@
     classification_count: count: 15

   render: ->
-    criteria = @props.workflow.retirement?.criteria ? @defaultCriteria
-    options = @props.workflow.retirement?.options ? @defaultOptions[criteria]
+    <ChangeListener target={@props.workflow}>{ =>
+      criteria = @props.workflow.retirement?.criteria ? @defaultCriteria
+      options = @props.workflow.retirement?.options ? @defaultOptions[criteria]

-
-      <select ref="criteriaSelect" value={criteria} disabled onChange={@handleChangeCriteria}>
-        <option value="classification_count">Classification count</option>
-      </select>{' '}
+
+        <select ref="criteriaSelect" value={criteria} disabled onChange={@handleChangeCriteria}>
+          <option value="classification_count">Classification count</option>
+        </select>{' '}

-      {switch criteria
-        when 'classification_count'
-          <input type="number" name="count" value={options.count} min="1" max="100" step="1" onChange={@handleChangeOption} />
-        else}
-
+        {switch criteria
+          when 'classification_count'
+            <input type="number" name="count" value={options.count} min="1" max="100" step="1" onChange={@handleChangeOption} />
+          else}
+
+    }</ChangeListener>

   handleChangeCriteria: (e) ->
     @props.workflow.update


switched off connection pooling for calls to the dashboard agent

--- server/cluster.coffee
+++ server/cluster.coffee
@@ -24,11 +24,16 @@
     options = _.extend {"dataDir": Settings.dataDir()}, options
     dir = "#{Meteor.settings.project}-#{instance}"
     console.log "Cluster.startApp #{app}, #{version}, #{instance}, #{EJSON.stringify options}, #{EJSON.stringify parameters} in project #{Meteor.settings.project}."
-    data =
-      dir: dir
-      startScript: Scripts.bash.start app, version, instance, options, parameters
-      stopScript: Scripts.bash.stop app, version, instance, options, parameters
-    HTTP.post "#{Settings.agentUrl()}/app/install-and-run", {data: data}, (err, result) ->
+
+    callOpts =
+      npmRequestOptions:
+        agent: false
+      data :
+        dir: dir
+        startScript: Scripts.bash.start app, version, instance, options, parameters
+        stopScript: Scripts.bash.stop app, version, instance, options, parameters
+
+    HTTP.post "#{Settings.agentUrl()}/app/install-and-run", callOpts, (err, result) ->
       console.log "Sent request to start instance. Response from the agent is", result
     ""


remove logs

remove logs

--- app/assets/javascripts/myrtc.js.coffee
+++ app/assets/javascripts/myrtc.js.coffee
@@ -34,7 +34,6 @@
   $("#file-progress")
     .append('

')
   counter++
-  console.log(counter)

 removeProgressBar = ->
   $(".progress-bar").remove()


Fixed #308 appearing a default error message

--- wzk/net/XhrFactory.coffee
+++ wzk/net/XhrFactory.coffee
@@ -60,7 +60,7 @@
       @flash.clearAll() if config.flash
       @applyJsonResponse response, config, xhr.getStatus()
     else
-      @flash.error() unless e.target.getConfig().flash
+      @flash.error() if e.target.getConfig().flash

   ###*
     @protected


OX-70

--- app/assets/javascripts/view/gui.coffee
+++ app/assets/javascripts/view/gui.coffee
@@ -28,14 +28,12 @@

                 resume : false
                 experiments : null
-                selectedExperimentIndex : null
+                selectedExperimentIndex : 0
                 changeExperiment : =>
-
+                  experiment = @settings.experiments[@settings.selectedExperimentIndex]
                   request(
                     method : "POST"
-                    url : "/experiment"
-                    contentType : "application/json"
-                    data : @settings.experiments[@settings.selectedExperimentIndex]
+                    url : "/experiment?id=#{experiment.id}&isNew=#{experiment.isNew}"
                   ).always -> window.location.reload()

                 position : initPos[0] + ", " + initPos[1] + ", " + initPos[2]


OX-70


fix move_to target

fix move_to target

--- app/assets/javascripts/views.js.coffee
+++ app/assets/javascripts/views.js.coffee
@@ -27,7 +27,7 @@

     member: (members)->
       member = (member for member in members when member.nickname == Blabbr.current_user.nickname)
-      member[0].hash = 'new_post' if member[0].unread == 0
+      member[0].hash = if member[0].unread == 0 then 'new_post' else "p#{member[0].hash}"
       member[0]

     events: ->


Reporter profile.

Reporter profile.

--- server/methods.coffee
+++ server/methods.coffee
@@ -33,6 +33,7 @@
     collection = Collections.get name
     console.log "after get collection: #{collection}"
     collection.update id, object.computeValues()  # returns the number of affected documents, in this case should be 1
+    console.log "formName: #{formName} object.forms[formName]?.afterUpdateServer: #{object.forms[formName]?.afterUpdateServer}"
     if formName
       if object.forms[formName]?.afterUpdateServer
         object.forms[formName].afterUpdateServer(object, @userId)


Support loading drafts from cnx-authoring

--- src/scripts/models/contents/node.coffee
+++ src/scripts/models/contents/node.coffee
@@ -13,12 +13,16 @@
   return class Node extends Backbone.AssociatedModel
     # url: () -> "#{SERVER}/contents/#{@id}"
     url: () ->
+      components = @id?.match(/([^:@]+)@?([^:]*):?([0-9]*)/) or []
+      id = components[1]
+      version = @get('version') or components[2]
+
       if @isNew()
         url = "#{AUTHORING}/contents"
-      else if @get('version') is 'draft'
-        url = "#{AUTHORING}/contents/#{@id}@draft.json" # FIX: Remove .json from URL
+      else if version is 'draft'
+        url = "#{AUTHORING}/contents/#{id}@draft.json" # FIX: Remove .json from URL
       else
-        url = "#{ARCHIVE}/contents/#{@id}"
+        url = "#{ARCHIVE}/contents/#{id}"

       return url


Add iteration tests to Set ut

Add iteration tests to Set ut

--- test/auto/unit/ut-set.coffee
+++ test/auto/unit/ut-set.coffee
@@ -109,4 +109,207 @@
       values.sort()
       expect(values).to.be.eql test_data

-# TODO add iteration tests
+  describe 'Iteration:', ->
+    describe 'Copy:', ->
+      describe 'foldl', ->
+        foldl = Iteration.Copy.foldl
+
+        it 'should return a0 iterating through empty set', ->
+          test_set = new Set
+          a = foldl test_set, 4, (a, {x}) ->
+            x - a
+
+          expect(a).to.be.equal 4
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          a = foldl test_set, 4, (a, {x}) ->
+            x - a
+
+          expect(a).to.be.equal -9
+
+      describe 'foldr', ->
+        foldr = Iteration.Copy.foldr
+
+        it 'should return a0 iterating through empty set', ->
+          test_set = new Set
+          a = foldr test_set, 4, ({x}, a) ->
+            x - a
+
+          expect(a).to.be.equal 4
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          a = foldr test_set, 4, ({x}, a) ->
+            x - a
+
+          expect(a).to.be.equal 17
+
+      describe 'map', ->
+        map = Iteration.Copy.map
+
+        it 'should return empty set iterating through empty set', ->
+          test_set = new Set
+          r = map test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql []
+          expect(r).to.be.not.equal test_set
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = map test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql [-10, 3]
+          expect(r).to.be.not.equal test_set
+
+      describe 'rmap', ->
+        rmap = Iteration.Copy.rmap
+
+        it 'should return empty set iterating through empty set', ->
+          test_set = new Set
+          r = rmap test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql []
+          expect(r).to.be.not.equal test_set
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = rmap test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql [3, -10]
+          expect(r).to.be.not.equal test_set
+
+      describe 'forEach', ->
+        forEach = Iteration.Copy.forEach
+
+        it 'should not perform callback iterating through empty set', ->
+          test_set = new Set
+          r = []
+          forEach test_set, ({x}) ->
+            r.push(x)
+
+          expect(r).to.be.eql []
+
+        it 'should perform callback iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = []
+          forEach test_set, ({x}) ->
+            r.push(x)
+
+          expect(r).to.be.eql [10, -3]
+
+      describe 'filter', ->
+        filter = Iteration.Copy.filter
+
+        it 'should return empty set iterating through empty set', ->
+          test_set = new Set
+          r = filter test_set, ({x}) ->
+            x > 0
+
+          expect(r.toArray()).to.be.eql []
+          expect(r).to.be.not.equal test_set
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = filter test_set, ({x}) ->
+            x > 0
+
+          expect(r.toArray()).to.be.eql [10]
+          expect(r).to.be.not.equal test_set
+
+      describe 'any', ->
+        any = Iteration.Copy.any
+
+        it 'should return false through empty set', ->
+          test_set = new Set
+          p = ({x}) ->
+            x > 0
+
+          expect(any test_set, p).to.be.false
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          p = ({x}) ->
+            x > 0
+
+          expect(any test_set, p).to.be.true
+
+        it 'should stop iterationg once result found', ->
+          test_set = new Set [10, -3, 5, -7]
+          count = 0
+          p = ({x}) ->
+            ++count
+            x < 0
+
+          expect(any test_set, p).to.be.true
+          expect(count).to.be.equal 2
+
+      describe 'all', ->
+        all = Iteration.Copy.all
+
+        it 'should return false through empty set', ->
+          test_set = new Set
+          p = ({x}) ->
+            x > 0
+
+          expect(all test_set, p).to.be.false
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          p = ({x}) ->
+            x > 0
+
+          expect(all test_set, p).to.be.false
+
+        it 'should stop iterationg once result found', ->
+          test_set = new Set [10, -3, 5, -7]
+          count = 0
+          p = ({x}) ->
+            ++count
+            x > 0
+
+          expect(all test_set, p).to.be.false
+          expect(count).to.be.equal 2
+
+    describe 'Replace:', ->
+      describe 'map', ->
+        map = Iteration.Replace.map
+
+        it 'should return empty set iterating through empty set', ->
+          test_set = new Set
+          r = map test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql []
+          expect(r).to.be.equal test_set
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = map test_set, ({x}) ->
+            -x
+
+          expect(r.toArray()).to.be.eql [-10, 3]
+          expect(r).to.be.equal test_set
+
+      describe 'filter', ->
+        filter = Iteration.Replace.filter
+
+        it 'should return empty set iterating through empty set', ->
+          test_set = new Set
+          r = filter test_set, ({x}) ->
+            x > 0
+
+          expect(r.toArray()).to.be.eql []
+          expect(r).to.be.equal test_set
+
+        it 'should return correct value iterating through non-empty set', ->
+          test_set = new Set [10, -3]
+          r = filter test_set, ({x}) ->
+            x > 0
+
+          expect(r.toArray()).to.be.eql [10]
+          expect(r).to.be.equal test_set


fix bug with root; fix bug with getting script attribute

--- earl.coffee
+++ earl.coffee
@@ -45,7 +45,11 @@

 window.get_script_attr ||= (script, attr) ->
   sc = document.querySelector("script[src*='#{script}'][src$='.coffee'], script[src*='#{script}'][src$='.js']")
-  sc.getAttribute(attr)
+  val = sc.getAttribute(attr)
+  if val == ''
+    val = true
+  val
+

 hist_aware = !!get_script_attr('earl', 'history-aware-links')

@@ -189,8 +193,7 @@

       # update browser history if it hasn't already been updated
       if url_from_browser_location() != new_location
-        h = (Earl.root + new_location)
-        history.pushState loc.query_params, title, h.replace(/(\/){2,}/, '/').replace(/(\/)$/, '')
+        history.pushState loc.query_params, title, new_location.replace(/(\/){2,}/, '/').replace(/(\/)$/, '')

       @last_location = new_location


Remove SESSION_OPENED subscriber for FieldCollection

The field data is not used directly by any UI elements and does not
need to be loaded. Furthermore, each Concept handles loading it's
specific set of fields so this collection would be redundant.

--- coffee/cilantro/models/field.coffee
+++ coffee/cilantro/models/field.coffee
@@ -55,13 +55,6 @@
         url: ->
             c.getSessionUrl('fields')

-        initialize: ->
-            super
-            # TODO change this to use Marionette's app initializer
-            c.subscribe c.SESSION_OPENED, => @fetch(reset: true)
-            c.subscribe c.SESSION_CLOSED, => @reset()
-            @on 'reset', @resolve
-
         search: (query, handler) ->
             c.Backbone.ajax
                 url: c._.result @, 'url'


stuff is more done, some tests

stuff is more done, some tests

--- coffeequate/tests/spec/parse.coffee
+++ coffeequate/tests/spec/parse.coffee
@@ -3,29 +3,29 @@
 	describe "Strings can be parsed into", ->

 		it "expressions", ->
-			expect(parse.stringToExpression("a + b").toLisp()).toBe("(a + b)")
-			expect(parse.stringToExpression("a + b + 3").toLisp()).toBe("(3 + a + b)")
-			expect(parse.stringToExpression("a * b").toLisp()).toBe("(a * b)")
-			expect(parse.stringToExpression("a * b * 3").toLisp()).toBe("(3 * a * b)")
-			expect(parse.stringToExpression("a ** b").toLisp()).toBe("(a ** b)")
-			expect(parse.stringToExpression("a ** b ** 3").toLisp()).toBe('(a ** (3 * b))')
-			expect(parse.stringToExpression("a * b + c * 4").toLisp()).toBe('((4 * c) + (a * b))')
-			expect(parse.stringToExpression("a * b ** 1/2 + c * 4").toLisp()).toBe('((4 * c) + (a * (b ** 1/2)))')
-			expect(parse.stringToExpression("a * b ** (5/6 + c) * 4").toLisp()).toBe('(4 * a * (b ** (5/6 + c)))')
-			expect(parse.stringToExpression("b+c*(-5)+(-a)").toLisp()).toBe('(b + (-5 * c) + (-1 * a))')
-			expect(parse.stringToExpression("((a))").toLisp()).toBe("a")
-			expect(parse.stringToExpression("((a + b))").toLisp()).toBe("(a + b)")
-			expect(parse.stringToExpression("((a + c) + b)").toLisp()).toBe("(a + b + c)")
-			expect(parse.stringToExpression("(((a + a) + b))**x").toLisp()).toBe('((b + (2 * a)) ** x)')
-			expect(parse.stringToExpression("(((x + 4) * b))**x").toLisp()).toBe('((b ** x) * ((4 + x) ** x))')
-			expect(parse.stringToExpression("(((x + 4) * b)**x)**x").toLisp()).toBe('((b ** (x ** 2)) * ((4 + x) ** (x ** 2)))')
-			expect(parse.stringToExpression("a ** (b + c)").toLisp()).toBe("(a ** (b + c))")
-			expect(parse.stringToExpression("(((x + 4) * b)**(x + - b) + - b)**x").toLisp()).toBe('(((-1 * b) + ((b ** (x + (-1 * b))) * ((4 + x) ** (x + (-1 * b))))) ** x)')
+			expect(parse.stringToExpression("a + b").toLisp()                                  ).toBe("(+ a b)")
+			expect(parse.stringToExpression("a + b + 3").toLisp()                              ).toBe("(+ 3 a b)")
+			expect(parse.stringToExpression("a * b").toLisp()                                  ).toBe("(* a b)")
+			expect(parse.stringToExpression("a * b * 3").toLisp()                              ).toBe("(* 3 a b)")
+			expect(parse.stringToExpression("a ** b").toLisp()                                 ).toBe("(** a b)")
+			expect(parse.stringToExpression("a ** b ** 3").toLisp()                            ).toBe("(** a (* 3 b))")
+			expect(parse.stringToExpression("a * b + c * 4").toLisp()                          ).toBe("(+ (* 4 c) (* a b))")
+			expect(parse.stringToExpression("a * b ** 1/2 + c * 4").toLisp()                   ).toBe("(+ (* 4 c) (* a (** b 1/2)))")
+			expect(parse.stringToExpression("a * b ** (5/6 + c) * 4").toLisp()                 ).toBe("(* 4 a (** b (+ 5/6 c)))")
+			expect(parse.stringToExpression("b+c*(-5)+(-a)").toLisp()                          ).toBe("(+ b (* -5 c) (* -1 a))")
+			expect(parse.stringToExpression("((a))").toString()                                ).toBe("a")
+			expect(parse.stringToExpression("((a + b))").toLisp()                              ).toBe("(+ a b)")
+			expect(parse.stringToExpression("((a + c) + b)").toLisp()                          ).toBe("(+ a b c)")
+			expect(parse.stringToExpression("(((a + a) + b))**x").toLisp()                     ).toBe("(** (+ b (* 2 a)) x)")
+			expect(parse.stringToExpression("(((x + 4) * b))**x").toLisp()                     ).toBe("(* (** b x) (** (+ 4 x) x))")
+			expect(parse.stringToExpression("(((x + 4) * b)**x)**x").toLisp()                  ).toBe("(* (** b (** x 2)) (** (+ 4 x) (** x 2)))")
+			expect(parse.stringToExpression("a ** (b + c)").toLisp()                           ).toBe("(** a (+ b c))")
+			expect(parse.stringToExpression("(((x + 4) * b)**(x + - b) + - b)**x").toLisp()    ).toBe("(** (+ (* -1 b) (* (** b (+ x (* -1 b))) (** (+ 4 x) (+ x (* -1 b))))) x)")

 			expect((x) -> parse.stringToExpression("a b c")).toThrow()

 		it "terms", ->
-			expect(parse.stringToTerminal("1/2").toLisp()).toEqual("1/2")
+			expect(parse.stringToTerminal("1/2").toString()).toEqual("1/2")
 			expect(parse.stringToTerminal("3/4")).toEqual(new terminals.Constant("3", "4"))

 		it "terminals with units", ->


Fixing minor bug with event being attached to nested elements

--- coffeescripts/jquery.flexnav.coffee
+++ coffeescripts/jquery.flexnav.coffee
@@ -62,8 +62,10 @@
 	).on('touchend mouseup', (e) ->
 		e.preventDefault()
 		e.stopPropagation()
+		bs         = settings['buttonSelector']
+		$btnParent = if ($(@).hasClass(bs)) then $(@) else $(@).parent(bs)
+		$thisNav   = $btnParent.data('navEl')
 		isDragging = false
-		$thisNav   = $(@).data('navEl')
 		if isDragging is false
 			console.log('clicked')
 			if nav_open is false


jupyter: unbreak kernels menu

jupyter: unbreak kernels menu

--- src/smc-webapp/jupyter/top-menubar.cjsx
+++ src/smc-webapp/jupyter/top-menubar.cjsx
@@ -152,9 +152,10 @@
                 @render_kernel_item(kernel)

     render_kernel: ->
+        items = @render_kernel_items()
         names = ['interrupt kernel', 'confirm restart kernel', 'confirm restart kernel and clear output', \
                  'confirm restart kernel and run all cells', '', \
-                 '<Change kernel...'].concat(@render_kernel_items())
+                 '<Change kernel...'].concat(items)

         @render_menu
             heading : 'Kernel'
@@ -176,6 +177,8 @@
     menu_item: (key, name) ->
         # TODO: this got complicated and should be its own component
         if name
+            if name.props?
+                return name  # it's already a MenuItem components
             if typeof(name) == 'object'
                 # use {name:'>nbconvert script', display:"Executable Script (.zzz)..."}, say, to be explicit about custom name to show
                 {name, display, style} = name
@@ -184,8 +187,6 @@
             else
                 display = undefined
             style ?= {}
-            if typeof(name) != 'string'
-                return name  # it's already a MenuItem
             if name[0] == '<'
                 disabled = true
                 name = name.slice(1)


不要なものを除去

不要なものを除去

--- coffee/index.coffee
+++ coffee/index.coffee
@@ -10,38 +10,6 @@
 md2react = require('md2react')

 $ = React.createElement
-
-defaultMarkdown = '''
-# Hello
-body
-1. 1
-2. 2
-------
-- [ ] unchecked
-- [x] checked
-- foo
-`a`
-------
-```
-bbb
-```
-**AA**
-*BB*
-[foo](/foo)
-![image](http://placehold.it/20x20/27709b/ffffff)
-> aaa
-> bbb
-|  TH  |  TH  |
-| ---- | ---- |
-|  TD  |  TD  |
-|  TD  |  TD  |
-'''
-
-defaultMarkdown = '''
-```js
-var x = 3;
-```
-'''

 Editor = React.createClass
   contentUpdateFromMarkdown: ->