Fix test

Fix test

--- test/daemon/app_manager.coffee
+++ test/daemon/app_manager.coffee
@@ -3,7 +3,7 @@
 _ = require 'lodash'
 manager = require '../../src/daemon/app_manager'

-describe.only 'appManager', ->
+describe 'appManager', ->

   describe 'getPort()', ->

@@ -43,9 +43,15 @@
     beforeEach ->
       manager.apps = []
       manager.katonPath = '/tmp/.katonPath'
-      manager.commander.getCommand = sinon.stub().returns 'command'
-      manager.spawner.spawn = sinon.stub().returns 'process'
+
+      sinon.stub(manager.commander, 'getCommand').returns 'command'
+      sinon.stub(manager.spawner, 'spawn').returns 'process'
+
       @app = manager.create 'foo'
+
+    afterEach ->
+      manager.commander.getCommand.restore()
+      manager.spawner.spawn.restore()

     it 'creates a new app in app list', ->
       assert.equal manager.apps.length, 1


fix(composer): get rid of extra blank lines on paste

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1357

--- internal_packages/composer/lib/contenteditable-component.cjsx
+++ internal_packages/composer/lib/contenteditable-component.cjsx
@@ -645,8 +645,14 @@
         blockquote: "p"
         table: "p"

-    cleanHtml.replace(/
/gi, "").replace(/<\/p>/gi, "

")
-
+    # We sanitized everything and convert all whitespace-inducing elements
+    # into
 tags. We want to de-wrap
 tags and replace with two line
+    # breaks instead.
+    cleanHTML = cleanHtml.replace(/
/gim, "").replace(/<\/p>/gi, "

")
+
+    # We never want more then 2 line breaks in a row.
+    # https://regex101.com/r/gF6bF4/2
+    cleanHTML.replace(/(
\s*){3,}/g, "

")


:lipstick:

:lipstick:

--- lib/cursor-position-view.coffee
+++ lib/cursor-position-view.coffee
@@ -6,8 +6,8 @@
     @div class: 'cursor-position inline-block'

   initialize: (@statusBar) ->
-    @subscribe @statusBar, 'active-buffer-changed', => @updateCursorPositionText()
-    @subscribe atom.rootView, 'cursor:moved', => @updateCursorPositionText()
+    @subscribe @statusBar, 'active-buffer-changed', @updateCursorPositionText
+    @subscribe atom.rootView, 'cursor:moved', @updateCursorPositionText

   afterAttach: ->
     @updateCursorPositionText()


seperated join team callback function

seperated join team callback function

--- servers/lib/server/handlers/jointeam.coffee
+++ servers/lib/server/handlers/jointeam.coffee
@@ -33,8 +33,6 @@
   return handleClientIdNotFound res, req  unless clientId

   client          = {}
-  # this function will be bound with necessary data in the queue later
-  kallback        = ->
   clientIPAddress = req.headers['x-forwarded-for'] || req.connection.remoteAddress

   queue = [
@@ -67,33 +65,34 @@
         queue.next()

     ->
-      kallback = joinTeamkallback.bind null, res, body
-      queue.next()
+      # generating callback function to be used in both login and convert
+      joinTeamKallback = generateJoinTeamKallback res, body

-    ->
       if alreadyMember
-      then JUser.login client.sessionToken, body, kallback
-      else JUser.convert client, body, kallback
+      then JUser.login client.sessionToken, body, joinTeamKallback
+      else JUser.convert client, body, joinTeamKallback

   ]

   daisy queue

-joinTeamkallback = (res, body, err, result) ->
+generateJoinTeamKallback = (res, body) ->

-  { redirect } = body
+  # returning a callback function
+  return (err, result) ->
+    { redirect } = body

-  # return if we got error from join/register
-  return res.status(400).send getErrorMessage err  if err?
-  # set clientId
-  res.cookie 'clientId', result.newToken, path : '/'
+    # return if we got error from join/register
+    return res.status(400).send getErrorMessage err  if err?
+    # set clientId
+    res.cookie 'clientId', result.newToken, path : '/'

-  # handle the request with an HTTP redirect:
-  return res.redirect 301, redirect if redirect
+    # handle the request with an HTTP redirect:
+    return res.redirect 301, redirect if redirect

-  # handle the request as an XHR response:
-  return res.status(200).end()
+    # handle the request as an XHR response:
+    return res.status(200).end()

 getErrorMessage = (err) ->


Fix move-to-top and move-to-bottom

Fix move-to-top and move-to-bottom

--- lib/project/results-view.coffee
+++ lib/project/results-view.coffee
@@ -36,6 +36,8 @@
         @selectPreviousResult()
       'core:move-left': => @collapseResult()
       'core:move-right': => @expandResult()
+      'core:move-to-top': =>
+        @selectFirstResult()
       'core:move-to-bottom': =>
         @renderResults(renderAll: true)
         @selectLastResult()
@@ -98,10 +100,26 @@
     @prop('scrollHeight') <= @height() + @pixelOverdraw or @prop('scrollHeight') <= @scrollBottom() + @pixelOverdraw

   selectFirstResult: ->
-    @find('.search-result:first').addClass('selected')
+    @find('.selected').removeClass('selected')
+
+    lastView = @find('.search-result:first')
+    parentView = lastView.closest('.path')
+    if parentView.hasClass('collapsed')
+      parentView.addClass('selected')
+    else
+      lastView.addClass('selected')
+
+    @scrollTop(0)

   selectLastResult: ->
-    @find('.search-result:last').addClass('selected')
+    @find('.selected').removeClass('selected')
+
+    lastView = @find('.search-result:last')
+    parentView = lastView.closest('.path')
+    if parentView.hasClass('collapsed')
+      @scrollTo(parentView.addClass('selected'))
+    else
+      @scrollTo(lastView.addClass('selected'))

   selectNextResult: ->
     selectedView = @find('.selected').view()


bad block

bad block

--- shared/ironTableCollection.coffee
+++ shared/ironTableCollection.coffee
@@ -23,12 +23,12 @@
                         limit: limit
                         skip: skip

-                meths = {}
-                meths["ironTable_" + @_name + "_recordCount"] = (select = {}) =>
-                    console.log("ironTable_" + @_name + "_recordCount called")
-                    @find?(select)?.count?()
-
-                Meteor.methods meths
+            meths = {}
+            meths["ironTable_" + @_name + "_recordCount"] = (select = {}) =>
+                console.log("ironTable_" + @_name + "_recordCount called")
+                @find?(select)?.count?()
+
+            Meteor.methods meths

     insertOk: ->
         false


Fixed treedropdownfield autocomplete not working

--- completions.cson
+++ completions.cson
@@ -1926,6 +1926,8 @@

 'TreeDropdownField':
   'prefix': 'treedropdownfield'
+  'module': 'silverstripe/framework'
+  'scope': '.text.html.php'
   'body': """
   TreeDropdownField::create(
   \t\'${1:name}ID\',


widget 提供addClass,removeClass,toggleClass方法

--- src/widget/widget.coffee
+++ src/widget/widget.coffee
@@ -100,10 +100,8 @@
 		@_classNamePool.add("ui")
 		className = @constructor.CLASS_NAME
 		if className
-#			@_classNamePool.add(className)
 			names = $.trim(className).split(" ")
-			for name in names
-				@_classNamePool.add(name)
+			@_classNamePool.add(name) for name in names

 		@_resetDimension()


Kite: add KitePlan model to hold subscriptions

--- workers/social/lib/social/models/kiteplan.coffee
+++ workers/social/lib/social/models/kiteplan.coffee
@@ -3,6 +3,8 @@
   {permit}            = require './group/permissionset'
   {secure, signature} = require 'bongo'
   KodingError         = require '../error'
+  JKite               = require './kite'
+  {Relationship}      = jraphical

   @share()
   @set
@@ -29,3 +31,60 @@
       instance :
         modify :
           (signature Object, Function)
+
+  @create: permit 'create kite plans',
+    success:(client, formData, callback)->
+      account  = client.connection.delegate
+      {kiteId} = formData
+
+      Relationship.one {
+        targetName  : "JKite"
+        targetId    : kiteId
+        sourceName  : "JAccount"
+        sourceId    : account.getId()
+        as          : "owner"
+      }, (err, rel) =>
+        return  callback new KodingError err if err or not rel
+        rel.fetchTarget (err, kite) =>
+          kitePlan = new JKitePlan formData
+          kitePlan.save (err)->
+            return  callback new KodingError err if err
+            kite.addPlan kitePlan, (err, res)->
+              return  callback new KodingError err if err
+              callback null, kitePlan
+
+  modify: permit 'edit kite plan',
+    success:(client, formData, callback)->
+      {kiteId} = formData
+      Relationship.one {
+        targetName  : "JKite"
+        targetId    : kiteId
+        sourceName  : "JAccount"
+        sourceId    : account.getId()
+        as          : "owner"
+      }, (err, rel) ->
+        return  callback new KodingError err if err or not rel
+        updatedFields =
+          name        : formData.name
+          price       : formData.price
+          description : formData.description
+          recurring   : formData.recurring
+
+        @update $set: updatedFields , callback
+
+  deletePlan: permit 'delete kite plan',
+    success:(client, callback)->
+      Relationship.one {
+        targeName   : "JKite"
+        targetId    : @getId()
+        sourceName  : "JAccount"
+        sourceId    : account.getId()
+        as          : "owner"
+      }, (err, rel) =>
+        return  callback new KodingError err if err
+        @remove (err, res)->
+          return  callback err, res
+
+
+
+


Use the array, Luke

Use the array, Luke

--- src/trix/views/image_attachment_view.coffee
+++ src/trix/views/image_attachment_view.coffee
@@ -24,7 +24,7 @@
     [image]

   getClassName: ->
-    [super, "image preview"].join(" ")
+    [super, "image", "preview"].join(" ")

   refresh: (image) ->
     image ?= @findElement()?.querySelector("img")


webapp errors: fix unhandledrejection error reporting

--- src/webapp-lib/webapp-error-reporter.coffee
+++ src/webapp-lib/webapp-error-reporter.coffee
@@ -295,7 +295,10 @@
     wrapFunction(console, "error", (-> sendLogLine("error", arguments)))

 if ENABLED
-    window.addEventListener("unhandledrejection",(e) -> reportException(e.reason,"unhandledrejection"))
+    window.addEventListener "unhandledrejection",(e) ->
+        # just to make sure there is a message
+        e.message ?= '<no message>'
+        reportException(e, "unhandledrejection")

 # public API


Map types support for Yandex.Map

Map types support for Yandex.Map

--- vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
+++ vendor/assets/javascripts/gmaps4rails/yandex/objects/map.coffee
@@ -9,7 +9,7 @@
   CONF:
     disableDefaultUI:       false
     disableDoubleClickZoom: false
-    type:                   "ROADMAP" # HYBRID, ROADMAP, SATELLITE, TERRAIN
+    type:                   "yandex#map"   # "yandex#satellite", "yandex#hybrid", "yandex#publicMap", "yandex#publicMapHybrid"
     mapTypeControl:         null

   constructor:(map_options, controller) ->
@@ -19,9 +19,10 @@
     @options  = @mergeObjects map_options, defaultOptions

     yandexOptions =
-      center: @createLatLng(@options.center_latitude, @options.center_longitude)
-      zoom:   @options.zoom
+      center:    @createLatLng(@options.center_latitude, @options.center_longitude)
+      zoom:      @options.zoom
       behaviors: @options.behaviors
+      type:      @options.type

     mergedYandexOptions = @mergeObjects map_options.raw, yandexOptions


Verify token screen and buffer delta

Verify token screen and buffer delta

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -355,29 +355,61 @@
         fullyTokenize(tokenizedBuffer)

         expect(tokenizedBuffer.lineForScreenRow(0).text).toBe "1   2   3   4"
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].screenDelta).toBe 3
+
         expect(tokenizedBuffer.lineForScreenRow(1).text).toBe "12  3   4   5"
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].screenDelta).toBe 2
+
         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "123 4       5   6"
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].screenDelta).toBe 1

         tokenizedBuffer.setTabLength(3)
         fullyTokenize(tokenizedBuffer)

         expect(tokenizedBuffer.lineForScreenRow(0).text).toBe "1  2  3  4"
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].screenDelta).toBe 2
+
         expect(tokenizedBuffer.lineForScreenRow(1).text).toBe "12 3     4  5"
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].screenDelta).toBe 1
+
         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "123   4     5  6"
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].screenDelta).toBe 3

         tokenizedBuffer.setTabLength(2)
         fullyTokenize(tokenizedBuffer)

         expect(tokenizedBuffer.lineForScreenRow(0).text).toBe "1 2   3 4"
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].screenDelta).toBe 1
+
         expect(tokenizedBuffer.lineForScreenRow(1).text).toBe "12  3   4 5"
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].screenDelta).toBe 2
+
         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "123 4     5 6"
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].screenDelta).toBe 1

         tokenizedBuffer.setTabLength(1)
         fullyTokenize(tokenizedBuffer)

         expect(tokenizedBuffer.lineForScreenRow(0).text).toBe "1 2  3 4"
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(0).tokens[1].screenDelta).toBe 1
+
         expect(tokenizedBuffer.lineForScreenRow(1).text).toBe "12 3   4 5"
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].screenDelta).toBe 1
+
         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "123 4    5 6"
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].bufferDelta).toBe 1
+        expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].screenDelta).toBe 1

   describe "when the buffer contains surrogate pairs", ->
     beforeEach ->


update crumbs to not require initial currentStep key

--- src/components/breadcrumb/breadcrumb-mixin.cjsx
+++ src/components/breadcrumb/breadcrumb-mixin.cjsx
@@ -4,7 +4,7 @@
 module.exports =
   propTypes:
     crumb: React.PropTypes.object.isRequired
-    currentStep: React.PropTypes.number.isRequired
+    currentStep: React.PropTypes.number
     goToStep: React.PropTypes.func.isRequired

   getInitialState: ->


improved oauth requesting

improved oauth requesting


a sprint has a team associated to it. If the team change during the project we can still the the old team

--- src/sprint/states/edit/controller.coffee
+++ src/sprint/states/edit/controller.coffee
@@ -17,7 +17,8 @@
   .then (response) ->
     $scope.boardColumns = response.data

-  $scope.devTeam = projectUtils.getDevTeam $scope.project.team
+  $scope.editedSprint.resources.team ?= $scope.project.team
+  $scope.devTeam = projectUtils.getDevTeam $scope.editedSprint.resources.team

   $scope.saveLabel = if $state.is 'tab.new-sprint' then 'Start the sprint' else 'Save'
   $scope.title = if $state.is 'tab.new-sprint' then 'NEW SPRINT' else 'EDIT SPRINT'


Cleaning up and testing --base option

Cleaning up and testing --base option

--- tests/functional/config.coffee
+++ tests/functional/config.coffee
@@ -79,7 +79,7 @@

   describe '[key:input]', ->
     it 'error should be shown when key is not set', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error You need at least one input dir in config file'

       global.__stdout = (data)-> out++
@@ -92,7 +92,7 @@
       config.parse()

     it 'error should be shown when input dir does not exist', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error Input dir does not exist ~> non/existent/folder'

       global.__stdout = (data)-> out++
@@ -108,7 +108,7 @@

   describe '[key:output]', ->
     it 'error should be shown when key is not set', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error You need at least one output in config file'

       global.__stdout = (data)-> out++
@@ -121,7 +121,7 @@
       config.parse()

     it 'error should be shown when js\'s output dir does not exist', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error JS\'s output dir does not exist ~> non/existent/folder'

       global.__stdout = (data)-> out++
@@ -134,7 +134,7 @@
       config.parse()

     it 'error should be shown when css\'s output dir does not exist', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error CSS\'s output dir does not exist ~> non/existent/folder'

       global.__stdout = (data)-> out++
@@ -151,7 +151,7 @@
   describe '[key:mappings]', ->

     it 'error should be shown when mapped folder doesn\'t exist', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error Mapping dir for \'mapped\' does not exist ~> '
       err_msg += 'non/existent/folder'

@@ -171,7 +171,7 @@

   describe '[key:server]', ->
     it 'error should be shown when server key is not set and -s is in use', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error Server\'s config not set in config file'

       global.global_options.server = true
@@ -188,7 +188,7 @@
       config.parse()

     it 'error should be shown when server root dir is not set', (done)->
-      out = err = 0
+      out = 0
       err_msg = 'error Server\'s root not set in in config file'

       global.global_options.server = true
@@ -209,7 +209,7 @@
       delete global.global_options

     it 'error should be shown when server root dir does not exist', (done)->
-      out = err = 0
+      out = 0
       root_dir = path.join base, 'not', 'existent', 'folder'
       err_msg = 'error Server\'s root dir does not exist ~> ' + root_dir

@@ -276,7 +276,7 @@
   describe '[option:config-file]', ->
     it 'error should be shown when informed config file does not exist', (done)->

-      out = err = 0
+      out = 0
       config_path = path.join base, 'not-existent.yml'
       err_msg = 'error Config file not found ~> ' + config_path

@@ -294,7 +294,7 @@

     it 'error should be shown when informed config file is a directory', (done)->

-      out = err = 0
+      out = 0
       config_path = path.join base, 'vendors'
       err_msg = 'error Config file\'s path is a directory  ~> ' + config_path

@@ -308,4 +308,22 @@
       config.parse()

       global.global_options['config-file'] = null
-      delete global.global_options+      delete global.global_options
+
+  describe '[option:base]', ->
+    it 'error should be shown when informed base dir does not exist', (done)->
+      out = 0
+      error_msgs = [
+        'error Dir informed with [--base] option doesn\'t exist ~> non/existent/folder'
+        'error Config file not found ~> '
+      ]
+
+      global.global_options.base = 'non/existent/folder'
+      global.__stdout = (data)-> out++
+      global.__stderr = (data)->
+        # console.log data
+        out.should.equal 0
+        data.should.equal error_msgs.shift()
+        done() unless error_msgs.length
+
+      config.parse()


Wrong indentation and avoid setting null beatmap

--- resources/assets/coffee/react/beatmap-discussions/main.coffee
+++ resources/assets/coffee/react/beatmap-discussions/main.coffee
@@ -120,6 +120,8 @@
       callback

   setCurrentBeatmapId: (_e, {id, callback}) ->
+    return callback?() if !id?
+
     osu.setHash "#:#{id}"

     return callback?() if id == @state.currentBeatmap.id
@@ -156,9 +158,9 @@
         callback: =>
           $.publish 'beatmapDiscussion:setHighlight', id: discussion.id

-        target = $(".js-beatmap-discussion-jump[data-id='#{id}']")
-        $(window).stop().scrollTo target, 500,
-          offset: modeSwitcher[0].getBoundingClientRect().height * -1
+          target = $(".js-beatmap-discussion-jump[data-id='#{id}']")
+          $(window).stop().scrollTo target, 500,
+            offset: modeSwitcher[0].getBoundingClientRect().height * -1

   setMode: (_e, mode, callback) ->


spec: remove spurious debugger statement

Change-Id: I251b8d899a27b95576d373d93f340a1a11647dc7
Reviewed-on: https://gerrit.instructure.com/82420
Reviewed-by: Joel Hough <4816dabf8db1bc6cac35b3a24cab2ff844b5b0c7@instructure.com>
Product-Review: Joel Hough <4816dabf8db1bc6cac35b3a24cab2ff844b5b0c7@instructure.com>
QA-Review: Joel Hough <4816dabf8db1bc6cac35b3a24cab2ff844b5b0c7@instructure.com>
Tested-by: Jenkins

--- spec/coffeescripts/views/DiscussionTopics/EditViewSpec.coffee
+++ spec/coffeescripts/views/DiscussionTopics/EditViewSpec.coffee
@@ -156,7 +156,6 @@
     ENV.CONDITIONAL_RELEASE_ENV = conditionalReleaseEnv
     view = @editView({ withAssignment: true })
     view.loadConditionalRelease()
-    debugger
     stub = @stub(view.conditionalReleaseEditor, 'setProps')
     view.onChange()
     view.onChange()


Fix cursor movement tests

Fix cursor movement tests

--- test/src/system/cursor_movement_test.coffee
+++ test/src/system/cursor_movement_test.coffee
@@ -1,7 +1,7 @@
 editorModule "Cursor movement", template: "editor_empty"

 editorTest "move cursor around attachment", (done) ->
-  getComposition().insertFile(createFile())
+  insertFile(createFile())
   assertLocationRange([0,1])
   moveCursor "left", ->
     assertLocationRange([0,0])
@@ -10,9 +10,9 @@
       done()

 editorTest "move cursor around attachment and text", (done) ->
-  getComposition().insertString("a")
-  getComposition().insertFile(createFile())
-  getComposition().insertString("b")
+  insertString("a")
+  insertFile(createFile())
+  insertString("b")
   assertLocationRange([0,3])
   moveCursor "left", ->
     assertLocationRange([0,2])
@@ -23,7 +23,7 @@
         done()

 editorTest "expand selection over attachment", (done) ->
-  getComposition().insertFile(createFile())
+  insertFile(createFile())
   assertLocationRange([0,1])
   expandSelection "left", ->
     assertLocationRange([0,0], [0,1])
@@ -34,9 +34,9 @@
         done()

 editorTest "expand selection over attachment and text", (done) ->
-  getComposition().insertString("a")
-  getComposition().insertFile(createFile())
-  getComposition().insertString("b")
+  insertString("a")
+  insertFile(createFile())
+  insertString("b")
   assertLocationRange([0,3])
   expandSelection "left", ->
     assertLocationRange([0,2], [0,3])
@@ -45,3 +45,14 @@
       expandSelection "left", ->
         assertLocationRange([0,0], [0,3])
         done()
+
+insertString = (string) ->
+  getComposition().insertString(string)
+  render()
+
+insertFile = (file) ->
+  getComposition().insertFile(file)
+  render()
+
+render = ->
+  getEditorController().render()


remove testing url + url of app changed?

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -487,8 +487,7 @@
           $("textarea[id='cartObject']").val(JSON.stringify(orders))
           formData = $(form).serialize()

-          testing_url = "https://script.google.com/macros/s/AKfycbyPEqIjIfyWR09vPhd5HcP7jB9KIjng0YzEwo2tjWOk8aEscM4/exec"
-          production_url = "https://script.google.com/macros/s/AKfycbz6VPh0yqNOmAChtPa9C1Ot9dk_JwHWj_vWPIZlPzr4YodmTvs/exec"
+          production_url = "https://script.google.com/macros/s/AKfycbx8LH0V-1gd_JSCbQItjtGlTQCNhNpWwFVd7IkW0E_uzmQj1pWP/exec"
           url = production_url

           $.ajax


missing twinkle

missing twinkle

--- assets/scripts/controllers/task.coffee
+++ assets/scripts/controllers/task.coffee
@@ -67,7 +67,7 @@
         lightDom = document.createElement "li"
         lightDom.id = wid
         _div = document.createElement 'div'
-        _div.setAttribute 'class','twinkle'
+        _div.setAttribute 'class',"twinkle twinkle-on"
         lightDom.appendChild _div
         _input = document.createElement 'input'
         _input.type = 'checkbox'


Increase timeout for cancel integration test

--- test/integration/cancel.coffee
+++ test/integration/cancel.coffee
@@ -54,18 +54,20 @@
         it 'moves the file', (done) ->
             setTimeout ->
                 Files.updateFile two, (err, updated) ->
+                    should.not.exist err
                     waitAppear twoPath, ->
                         fs.existsSync(onePath).should.be.false()
                         done()
-            , 500
+            , 800

         it 'moves back the file to its original path', (done) ->
             setTimeout ->
                 Files.updateFile one, (err, updated) ->
+                    should.not.exist err
                     waitAppear onePath, ->
                         fs.existsSync(twoPath).should.be.false()
                         done()
-            , 500
+            , 800

     describe 'Delete a file and recreate it', ->


hide on esc

hide on esc

--- resources/assets/lib/report-form.coffee
+++ resources/assets/lib/report-form.coffee
@@ -42,8 +42,21 @@
       showingModal: false

+  componentDidMount: =>
+    document.addEventListener 'keydown', @handleEsc
+
+
   componentDidUpdate: (_prevProps, prevState) =>
     Blackout.toggle(@state.showingModal, 0.5) unless prevState.showingModal == @state.showingModal
+
+
+  componentWillUnmount: =>
+    document.removeEventListener 'keydown', @handleEsc
+
+
+  handleEsc: (e) =>
+    if e.keyCode == 27
+      @setState showingModal: false

   hideModal: (e) =>


Fix protocol spec on Windows.

Fix protocol spec on Windows.

--- spec/api-protocol-spec.coffee
+++ spec/api-protocol-spec.coffee
@@ -116,7 +116,7 @@
       protocol.interceptProtocol targetScheme, (request) ->
         if process.platform is 'win32'
           pathInUrl = path.normalize request.url.substr(8)
-          assert.equal pathInUrl, __filename
+          assert.equal pathInUrl.toLowerCase(), __filename.toLowerCase()
         else
           assert.equal request.url, "#{targetScheme}://#{__filename}"


Use relative requires in specs

Use relative requires in specs

--- spec/fuzzy-finder-spec.coffee
+++ spec/fuzzy-finder-spec.coffee
@@ -1,6 +1,6 @@
 RootView = require 'root-view'
-FuzzyFinder = require 'fuzzy-finder/lib/fuzzy-finder-view'
-PathLoader = require 'fuzzy-finder/lib/path-loader'
+FuzzyFinder = require '../lib/fuzzy-finder-view'
+PathLoader = require '../lib/path-loader'
 _ = require 'underscore'
 $ = require 'jquery'
 {$$} = require 'space-pen'


Increase promise timeout to 30 seconds

Increase promise timeout to 30 seconds

CI is failing intermittently on this spec so increase the timeout
to see if it is a legitimate failure.

--- spec/preview-list-spec.coffee
+++ spec/preview-list-spec.coffee
@@ -15,7 +15,7 @@

   describe "when list is scrollable", ->
     it "adds more operations to the DOM when `scrollBottom` nears the `pixelOverdraw`", ->
-      waitsForPromise ->
+      waitsForPromise timeout: 30000, ->
         projectFindView.findEditor.setText(' ')
         projectFindView.confirm()


:white_check_mark: Add tests for opening search results in pending state

--- spec/results-view-spec.coffee
+++ spec/results-view-spec.coffee
@@ -421,6 +421,29 @@
       runs ->
         expect(atom.workspace.getActivePaneItem().getPath()).toContain('sample.')

+    it "opens the file containing the result in pending state when the search result is single-clicked", ->
+      pathNode = resultsView.find(".search-result")[0]
+      pathNode.dispatchEvent(buildMouseEvent('mousedown', target: pathNode, which: 1))
+      editor = null
+      waitsFor ->
+        editor = atom.workspace.getActiveTextEditor()
+
+      runs ->
+        expect(editor.isPending()).toBe true
+        expect(atom.views.getView(editor)).toHaveFocus()
+
+    it "opens the file containing the result in non-pending state when the search result is double-clicked", ->
+      pathNode = resultsView.find(".search-result")[0]
+      pathNode.dispatchEvent(buildMouseEvent('mousedown', target: pathNode, detail: 1))
+      pathNode.dispatchEvent(buildMouseEvent('mousedown', target: pathNode, detail: 2))
+      editor = null
+      waitsFor ->
+        editor = atom.workspace.getActiveTextEditor()
+
+      runs ->
+        expect(editor.isPending()).toBe false
+        expect(atom.views.getView(editor)).toHaveFocus()
+
     describe "when `openProjectFindResultsInRightPane` option is true", ->
       beforeEach ->
         atom.config.set('find-and-replace.openProjectFindResultsInRightPane', true)


在退订服务时清空服务数据

在退订服务时清空服务数据

--- core/plan.coffee
+++ core/plan.coffee
@@ -31,10 +31,14 @@
       if stillInService
         callback()
       else
-        mAccount.update _id: account._id,
+        modifier =
           $pull:
             'attribute.services': serviceName
-        , ->
+          $unset: {}
+
+        modifier['$unset']["attribute.plugin.#{plan}"] = ''
+
+        mAccount.update _id: account._id, modifier, ->
           (plugin.get serviceName).service.delete account, ->
             callback()


Moderation: Create synonym tags permission is added

--- workers/social/lib/social/models/tag.coffee
+++ workers/social/lib/social/models/tag.coffee
@@ -45,6 +45,7 @@
       'fetch system tag'      : ['moderator']
       'create system tag'     : ['moderator']
       'remove system tag'     : ['moderator']
+      'create synonym tags'    : ['moderator']
       # 'delete system tag'     : ['moderator']

     emitFollowingActivities : yes # create buckets for follower / followees
@@ -337,7 +338,7 @@
         return callback new KodingError "##{@title} have child topics! You must first delete them"
       callback null

-  createSynonym : permit ['create tags', 'read tags'],
+  createSynonym : permit ['create synonym tags'],
     success: (client, options, callback)->
       # whenever client wants to create a new tag for synonym title is used
       {title, id} = options


Fixed missing variable reference in test

--- src/mobile/apps/artwork/components/meta_data/test/view.coffee
+++ src/mobile/apps/artwork/components/meta_data/test/view.coffee
@@ -61,7 +61,6 @@
       createOrderStub = sinon.stub().returns(Promise.resolve(createOrderWithArtwork: orderOrError: order: id: "1234"))
       locationMock = assign: sinon.spy()
       @MetaDataView.__set__
-        errorModal: errorModalMock
         createOrder: createOrderStub
         location: locationMock
         CurrentUser:


Fixing typo

Fixing typo

--- src/quo.ajax.coffee
+++ src/quo.ajax.coffee
@@ -43,11 +43,7 @@
         xhr.open settings.type, settings.url, settings.async
         _xhrHeaders xhr, settings

-        console.error settings.timeout
         if settings.timeout > 0
-            # xhr.timeout = settings.timeout
-            # xhr.ontimeout = -> _xhrTimeout xhr, settings
-
             abortTimeout = setTimeout((-> _xhrTimeout xhr, settings ), settings.timeout)

         try


Fixing typo

Fixing typo


update contributors functionality in round model

--- src/node_modules/round-model/round-model.coffee
+++ src/node_modules/round-model/round-model.coffee
@@ -31,17 +31,29 @@
       else
         'warning'

-    getContributors: () ->
+    getAllocationsByUser: ->
+      # index allocations by user id
+      allocationsByUserId = {}
+      _.each @allocations, (allocation) ->
+        allocationsByUserId[allocation.user.id] = allocation
+      allocationsByUserId
+
+    getContributors: (memberships) ->
       contributors = {}

-      # start contributors with allocations
-      _.each @allocations, (allocation) ->
-        person = _.clone(allocation.user)
+      allocationsByUserId = @getAllocationsByUser()

-        person.allocationCents = allocation.amountCents
-        person.allocation = person.allocationCents / 100
+      # for each member
+      _.each memberships, (membership) ->
+        contributor = _.clone(membership.user)

-        contributors[person.id] = person
+        allocation = allocationsByUserId[contributor.id]
+
+        if (allocation)
+          contributor.allocationCents = allocation.amountCents
+          contributor.allocation = contributor.allocationCents / 100
+
+          contributors[contributor.id] = contributor

       # for each bucket
       _.each @buckets, (bucket) ->
@@ -50,17 +62,17 @@
         # add contributions to contributors
         _.each contributions, (contribution, userId) ->

-          person = contributors[userId]
+          contributor = contributors[userId]

-          if not person
+          if not contributor
             return

-          if not person.contributionCents
-            person.contributionCents = contribution.amountCents
+          if not contributor.contributionCents
+            contributor.contributionCents = contribution.amountCents
           else
-            person.contributionCents += contribution.amountCents
+            contributor.contributionCents += contribution.amountCents

-          person.contribution = person.contributionCents / 100
+          contributor.contribution = contributor.contributionCents / 100

       contributors


moved fullscreen button and markdown helper to the KDInputViewWithPreview

--- client/Framework/components/inputs/KDInputViewWithPreview.coffee
+++ client/Framework/components/inputs/KDInputViewWithPreview.coffee
@@ -7,6 +7,8 @@
     options.preview.language      or= "markdown"
     options.preview.showInitially ?= yes
     options.preview.mirrorScroll  ?= yes
+    options.allowMaximized        ?= yes
+    options.showHelperModal       ?= yes

     options.keyup ?= (event)=>
       if @options.preview.autoUpdate
@@ -25,8 +27,10 @@
     @showPreview = options.preview.showInitially or no

     @previewOnOffLabel = new KDLabelView
-      cssClass : "preview_switch_label"
-      title: "Preview"
+      cssClass      : "preview_switch_label"
+      title         : "Preview"
+      tooltip       :
+        title       : "Show/hide the Text Preview Box"
     @previewOnOffSwitch = new KDOnOffSwitch
       label         : @previewOnOffLabel
       size          : "tiny"
@@ -48,6 +52,103 @@
     @previewOnOffContainer = new KDView
       cssClass : "preview_switch"

+    if @options.showHelperModal
+
+      @markdownLink = new KDCustomHTMLView
+        tagName     : 'a'
+        name        : "markdownLink"
+        value       : "markdown is enabled"
+        attributes  :
+          href      : '#'
+          value     : "markdown syntax is enabled"
+        cssClass    : 'markdown-link'
+        partial     : "What is Markdown?"
+        tooltip     :
+          title     : "Show available Markdown formatting syntax"
+        click       : (event)=>
+          if $(event.target).is 'span'
+            link.hide()
+          else
+            markdownText = new KDMarkdownModalText
+            modal = new KDModalView
+              title       : "How to use the Markdown syntax."
+              cssClass    : "what-you-should-know-modal markdown-cheatsheet"
+              height      : "auto"
+              width       : 500
+              content     : markdownText.markdownText()
+              buttons     :
+                Close     :
+                  title   : 'Close'
+                  style   : 'modal-clean-gray'
+                  callback: -> modal.destroy()
+
+      @previewOnOffContainer.addSubView @markdownLink
+
+    if @options.allowMaximized
+      @fullScreenBtn = new KDButtonView
+        style           : "clean-gray small"
+        cssClass        : "fullscreen-button"
+        title           : "Fullscreen Edit"
+        icon            : no
+        tooltip         :
+          title         : "Open a Fullscreen Editor"
+        callback: =>
+          @textContainer = new KDView
+            cssClass:"modal-fullscreen-text"
+
+          @text = new KDInputViewWithPreview
+            type : "textarea"
+            cssClass : "fullscreen-data kdinput text"
+            allowMaximized : no
+            defaultValue : @getValue()
+
+          @textContainer.addSubView @text
+
+          modal = new KDModalView
+            title       : "Please enter your content here."
+            cssClass    : "modal-fullscreen"
+            width       : $(window).width()-110
+            height      : $(window).height()-120
+            view        : @textContainer
+            position:
+              top       : 55
+              left      : 55
+            overlay     : yes
+            buttons     :
+              Cancel    :
+                title   : "Discard changes"
+                style   : "modal-clean-gray"
+                callback:=>
+                  modal.destroy()
+              Apply     :
+                title   : "Apply changes"
+                style   : "modal-clean-gray"
+                callback:=>
+                  @setValue @text.getValue()
+                  @generatePreview()
+                  modal.destroy()
+
+          modal.$(".kdmodal-content").height modal.$(".kdmodal-inner").height()-modal.$(".kdmodal-buttons").height()-modal.$(".kdmodal-title").height() # minus the margin, border pixels too..
+          modal.$(".fullscreen-data").height modal.$(".kdmodal-content").height()-30-23+10
+          modal.$(".input_preview").height   modal.$(".kdmodal-content").height()-0-21+10
+          modal.$(".input_preview").css maxHeight:  modal.$(".kdmodal-content").height()-0-21+10
+          modal.$(".input_preview div.preview_content").css maxHeight:  modal.$(".kdmodal-content").height()-0-21
+          contentWidth = modal.$(".kdmodal-content").width()-40
+          halfWidth  = contentWidth / 2
+
+          @text.on "PreviewHidden", =>
+            modal.$(".fullscreen-data").width contentWidth #-(modal.$("div.preview_switch").width()+20)-10
+            # modal.$(".input_preview").width (modal.$("div.preview_switch").width()+20)
+
+          @text.on "PreviewShown", =>
+            modal.$(".fullscreen-data").width contentWidth-halfWidth-5
+            # modal.$(".input_preview").width halfWidth-5
+
+          modal.$(".fullscreen-data").width contentWidth-halfWidth-5
+          modal.$(".input_preview").width halfWidth-5
+
+      @previewOnOffContainer.addSubView @fullScreenBtn
+
     @previewOnOffContainer.addSubView @previewOnOffLabel
     @previewOnOffContainer.addSubView @previewOnOffSwitch


Ram usage bar: Minor fixes

Ram usage bar: Minor fixes

--- client/Framework/components/progressbar/KDProgressBarView.coffee
+++ client/Framework/components/progressbar/KDProgressBarView.coffee
@@ -1,6 +1,6 @@
 class KDProgressBarView extends KDCustomHTMLView
   constructor:(options = {})->
-    options.cssClass += " progressbar-container"
+    options.cssClass = KD.utils.curryCssClass "progressbar-container"

     super options

@@ -17,6 +17,5 @@

   updateBar:(value, unit, label)->
     @bar.setWidth value, unit
-    window.bar = @bar
     @darkLabel.updatePartial "#{label} "
     @lightLabel.updatePartial "#{label} "


Added safeguard when person is Null

Added safeguard when person is Null

--- client/person.coffee
+++ client/person.coffee
@@ -37,9 +37,10 @@
 Template.profile.authoredPublications = ->
   person = Persons.findOne
     slug: Session.get 'currentPersonSlug'
+
   Publications.find
     _id:
-      $in: _.pluck person.publications, '_id'
+      $in: _.pluck person?.publications, '_id'

 # Publications in logged user's library
 Template.profile.myPublications = ->


Passing 'firstRendr' to every 'action:end'

--- client/router.coffee
+++ client/router.coffee
@@ -59,10 +59,10 @@
     (paramsArray...) =>
       @trigger 'action:start', definition, firstRender
       if firstRender
-        firstRender = false
         views = BaseView.attach(@app)
         @currentView = @getMainView(views)
-        @trigger 'action:end', definition
+        @trigger 'action:end', definition, firstRender
+        firstRender = false
       else
         params = @getParamsHash(pattern, paramsArray)
         action = @getAction(definition)


recompact tf operation output, now that it works

--- src/tf.coffee
+++ src/tf.coffee
@@ -23,16 +23,11 @@
 tfName = tfRole = 'tf'

 sendqueue = []
+servicequeue = ->
+  obj = sendqueue.shift()
+  obj['msg'].send {room: msg.message.user.name}, obj['out']

 module.exports = (robot) ->
-
-  servicequeue = ->
-    obj = sendqueue.shift()
-    msg = obj['msg']
-    room = obj['room']
-    out = obj['out']
-    console.log JSON.stringify room
-    msg.send {room: room}, out

   robot.respond /tf help$/, (msg) ->
     cmds = []
@@ -150,12 +145,13 @@
     return exec "cd #{basepath}; rm -rf #{projname}", (error, stdout, stderr) ->
       msg.send {room: msg.message.user.name}, "Project deleted: #{projname}"

-  robot.respond /tf (plan|refresh|apply|destroy) ([^\s]+)$/i, (msg) ->
+  robot.respond /tf (plan|refresh|apply|destroy) ([^\s]+)( verbose)?$/i, (msg) ->
     unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)
       return msg.send {room: msg.message.user.name}, "Not authorized.  Missing #{tfRole} role."

     action = msg.match[1]
     projname = msg.match[2].replace /\//, "_"
+    verbose = true if msg.match[3]

     unless fs.existsSync("#{basepath}/#{projname}")
       return msg.send {room: msg.message.user.name}, "Invalid project name: `#{projname}`"
@@ -176,21 +172,21 @@
       if stdout
         if stdout.length < 1024
           return msg.send {room: msg.message.user.name}, "```\n#{stdout}\n```"
+        else unless verbose
+          return msg.send {room: msg.message.user.name}, "```\n#{line}\n```" if line.match /^Plan: / for line in stdout.split "\n"
         out = []
         waitms = 200
         textchunk = ''
         for line in stdout.split "\n"
-          if line.match /^\+\s/
+          if line.match /^(?:\+\s|Plan: )/
             textchunk = out.join "\n"
-            sendqueue.push { msg: msg, room: msg.message.user.id, out: "```\n#{textchunk}\n```" }
-            console.log textchunk
+            sendqueue.push { msg: msg, out: "```\n#{textchunk}\n```" }
             setTimeout servicequeue, waitms
             waitms = waitms + 200
             out = []
           out.push line
         textchunk = out.join "\n"
-        console.log textchunk
-        sendqueue.push { msg: msg, room: msg.message.user.id, out: "```\n#{textchunk}\n```" }
+        sendqueue.push { msg: msg, out: "```\n#{textchunk}\n```" }
         setTimeout servicequeue, waitms

   robot.respond /tf env ([^\s]+) set ([^\s]+)=(.+)$/i, (msg) ->


improve project user output

improve project user output

--- src/tf.coffee
+++ src/tf.coffee
@@ -124,7 +124,7 @@
       stat = fs.statSync("#{basepath}/#{fn}")
       dir.push fn if stat.isDirectory()

-    return msg.send {room: msg.message.user.name}, dir.join "\n"
+    return msg.send {room: msg.message.user.name}, "Projects: `"+ dir.join "`, `" +"`"

   robot.respond /tf remote ([^\s]+)$/i, (msg) ->
     projname = msg.match[1].replace /\//, "_"


Show loader unless `preview` subversion is created

--- demo/client/file/file.coffee
+++ demo/client/file/file.coffee
@@ -36,7 +36,15 @@
       img.onerror = ->
         self.showError.set true
         return
-      img.src = @data.file.link 'preview'
+      if @data.file.versions?.preview?.extension
+        img.src = @data.file.link 'preview'
+      else
+        handle = Collections.files.find(@data.file._id).observeChanges
+          changed: (_id, fileds) ->
+            if fileds?.versions?.preview?.extension
+              img.src = self.data.file.link 'preview'
+              handle.stop()
+            return
     else
       img  = new Image()
       img.onload = ->


Let View#delegate return the bound handler

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -150,6 +150,9 @@
         # Register handler
         @$el.on eventType, handler

+      # Return the bound handler
+      handler
+
     # Remove all handlers registered with @delegate

     undelegate: ->


Remove mongo port

Remove mongo port

--- src/config/environment.coffee
+++ src/config/environment.coffee
@@ -4,4 +4,4 @@
       name: 'Otagai.js'
     root: require('path').normalize(__dirname + '/..')
     db: process.env.MONGOLAB_URI || process.env.MONGOHQ_URL \
-        || 'mongodb://localhost:27020/dev'+        || 'mongodb://localhost/dev'


Added alpha blending to render method.

Added alpha blending to render method.

TODO:
  * Make the blending optional.

--- coffee/figure.coffee
+++ coffee/figure.coffee
@@ -84,6 +84,13 @@
     # notice the use of fat arrow!
     render: () =>

+
+        # enable alpha blending
+        # TODO: make optional
+        GL = @renderer.context
+        GL.enable(GL.BLEND)
+        GL.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA)
+
         if @needsUpdate

             console.log('calling update')
@@ -94,7 +101,6 @@

             @needsUpdate = false

-
         @renderer.render(@scene, @camera)


Fix authentication error for static files

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -35,7 +35,7 @@
 server.route [
   { method: 'GET', path: '/'      , config: { handler: site.getIndex  , auth: { mode: 'try' }}}
   #{ method: 'GET', path: '/static/images/cabin/{id}', handler: cabin.getImage }
-  { method: 'GET', path: '/static/{path*}'  , handler: site.getStatic }
+  { method: 'GET', path: '/static/{path*}'  , config: { handler: site.getStatic , auth: false }}
   { method: '*'  , path: '/login' , config: { handler: auth.allLogin  , auth: { mode: 'try' }}}
   { method: 'GET', path: '/logout', config: { handler: auth.getLogout , auth: true }}
   { method: 'GET', path: '/liste'  , config: { handler: cabin.getList  , auth: true }}


Typo in upload.coffee

Typo in upload.coffee

--- coffee/upload.coffee
+++ coffee/upload.coffee
@@ -35,9 +35,9 @@

   constructor: (uploadSettings = null) ->
     if uploadSettings != null and
-       typeof uploadSettings.uploadScript isnt undefined and
-       typeof uploadSettings.uploadFolder isnt undefined and
-       typeof uploadSettings.tmpFolder isnt undefined
+       typeof uploadSettings.uploadScript isnt 'undefined' and
+       typeof uploadSettings.uploadFolder isnt 'undefined' and
+       typeof uploadSettings.tmpFolder isnt 'undefined'

       @uploadSettings = uploadSettings


Team Preview: Bug, didn't show final teams.

--- server/conditions.coffee
+++ server/conditions.coffee
@@ -188,12 +188,8 @@
       @arranged = {}

     start: ->
-      if @arranging
-        @tell(Protocol.TEAM_PREVIEW)
-        @shouldStart = false
-      else
-        @tell(Protocol.REARRANGE_TEAMS, @getArrangements()...)
-        @shouldStart = true
+      @tell(Protocol.TEAM_PREVIEW)
+      @shouldStart = false

   extendFacade:
     arrangeTeam: (playerId, arrangement) ->
@@ -207,6 +203,8 @@
         return false  if !team.pokemon[index]
         return false  if arrangement.indexOf(index, i + 1) != -1
       if @battle.arrangeTeam(playerId, arrangement)
+        @arranging = false
+        @battle.tell(Protocol.REARRANGE_TEAMS, @battle.getArrangements()...)
         @battle.startBattle()
         @battle.sendUpdates()
       return true
@@ -221,5 +219,4 @@

     getArrangements: ->
       for playerId in @playerIds
-        console.log @arranging, @arranged
         @arranged[playerId] || [0...@getTeam(playerId).length]


Implemented test for wildcard subscriptions.

--- test/EventBusTest.coffee
+++ test/EventBusTest.coffee
@@ -48,4 +48,6 @@

 	# Tests wildcard subscriptions.
 	"wildcard subscription": (test) ->
-		test.done()
+		bus.subscribe "*", ->
+			test.done()
+		bus.publish "something"


Minor fix in demo

Minor fix in demo

--- demo/demo.coffee
+++ demo/demo.coffee
@@ -14,7 +14,8 @@
   Template.demo.events
     'click .switch-language': (e, template) ->
       e.preventDefault()
-      i18n.setLocale e.currentTarget.dataset.code
+      if e.currentTarget.dataset?.code
+        i18n.setLocale e.currentTarget.dataset.code
       false

 Meteor.startup ->


Skip intermittently failing test due to a timing issue in Replicant

--- test/src/modules/navigation_tests.coffee
+++ test/src/modules/navigation_tests.coffee
@@ -21,7 +21,7 @@
       assert.equal(navigation.action, "replace")
       done()

-navigationTest "following a same-origin data-turbolinks=false link", (assert, session, done) ->
+QUnit.skip "following a same-origin data-turbolinks=false link", (assert, session, done) ->
   session.clickSelector "#same-origin-false-link", (navigation) ->
     assert.equal(navigation.location.pathname, "/fixtures/one.html")
     assert.equal(navigation.action, "load")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -39,7 +39,7 @@

   equals player.I.x, 12

-    window.keydown.left = false
+  window.keydown.left = false

 test "should move player up while key is pressed", ->
   ;


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -9,7 +9,7 @@

   player.include(Controllable)

-  keydown.left = true
+  window.keydown.left = true
   player.update()

   equals player.I.x, 4


signin RU

signin RU

--- t9n/ru.coffee
+++ t9n/ru.coffee
@@ -30,7 +30,7 @@
   setPassword: "Установить пароль"
   sign: "Подпись"
   signIn: "Войти"
-  signin: "bойти"
+  signin: "войти"
   signOut: "Выйти"
   signUp: "Регистрация"
   signupCode: "Регистрационный код"


instances crud operations

instances crud operations

--- routes/api/v2/instances.coffee
+++ routes/api/v2/instances.coffee
@@ -1,7 +1,7 @@
 Meteor.startup ->

   if Meteor.isServer
-
+    _   = require 'lodash'
     lib = require './lib.coffee'

     formatInstanceForOverview = (i) ->
@@ -10,6 +10,14 @@
       state:
         current: i.state
         desired: i.desiredState
+    formatInstance = (i) ->
+      _.extend formatInstanceForOverview(i),
+        app:
+          name: i.app?.name
+          version: i.app?.version
+        services: _.mapValues i.services, (s, name) ->
+          state: s.state
+

     Router.map ->
@@ -17,10 +25,34 @@
         where: 'server'
         path: '/api/v2/instances'
       .get ->
-        lib.foundJson @response, Instances.find().map formatInstanceForOverview
+        lib.foundJson @response, 200, Instances.find().map formatInstanceForOverview

       @route 'api/v2/instances/single',
         where: 'server'
         path: '/api/v2/instances/:name'
       .get ->
-        lib.foundJson @response, Instances.findOne name: @params.name
+        check([@params.name], [String])
+        if (instance = Instances.findOne name: @params.name)
+          lib.foundJson @response, 200, formatInstance instance
+        else lib.notFound @response
+      .delete ->
+        try
+          check([name = @params.name], [String])
+          instance = Agent.stopInstance name
+          lib.foundJson @response, 200, formatInstanceForOverview instance
+        catch e
+          lib.endWithError @response, 400, (e.message or e.error)
+
+      .put ->
+        args = [
+          name = @params.name
+          app = @request.body.app
+          version = @request.body.version
+        ]
+        try
+          check(args, [String])
+          check([parameters = @request.body.parameters, options = @request.body.options], [Object])
+          instance = Agent.startApp app, version, name, parameters, options
+          lib.foundJson @response, 201, formatInstanceForOverview instance
+        catch e
+          lib.endWithError @response, 400, (e.message or e.error)


Fix test on Windows

Fix test on Windows

Fix a hardcoded *nix style path separator in the test to use the os
specific version from path.sep.

--- test/notification.coffee
+++ test/notification.coffee
@@ -1,3 +1,4 @@
+path = require("path")
 should = require("chai").should()
 sinon = require("sinon")

@@ -130,7 +131,7 @@
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace.should.be.an("array")
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace.should.have.length.of.at.least 2
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].should.have.keys("file", "lineNumber", "columnNumber", "method")
-      deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].file.should.contain "/error.js"
+      deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].file.should.contain path.sep + "error.js"
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].lineNumber.should.be.an "number"
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].columnNumber.should.be.an "number"
       deliverStub.firstCall.thisValue.events[0].exceptions[0].stacktrace[0].method.should.equal "Error"


make all tests run

make all tests run

--- test/roaster-spec.coffee
+++ test/roaster-spec.coffee
@@ -9,7 +9,14 @@
   describe "options", ->
     describe "passing options.isFile = true", ->
       it "returns a rendered file", ->
-        roaster Path.join(fixtures_dir, "markdown.md"), {isFile: true}, (err, contents) ->
+        callback = jasmine.createSpy()
+        roaster Path.join(fixtures_dir, "markdown.md"), {isFile: true}, callback
+
+        waitsFor ->
+          callback.callCount > 0
+
+        runs ->
+          [err, contents] = callback.mostRecentCall.args
           expect(err).toBeNull()
           expect(contents).toContain ''
     describe "not passing options.isFile", ->
@@ -21,11 +28,27 @@

   describe "emoji", ->
     it "returns emoji it knows", ->
-      roaster Path.join(fixtures_dir, "emoji.md"), {isFile: true}, (err, contents) ->
+      callback = jasmine.createSpy()
+      roaster Path.join(fixtures_dir, "emoji.md"), {isFile: true}, callback
+
+      waitsFor ->
+        callback.callCount > 0
+
+      runs ->
+        [err, contents] = callback.mostRecentCall.args
+
         expect(err).toBeNull()
         expect(contents).toEqual '

\n

\n

'
     it "can sanitize and return", ->
-      roaster Path.join(fixtures_dir, "emoji.md"), {isFile: true, sanitize:true}, (err, contents) ->
+      callback = jasmine.createSpy()
+      roaster Path.join(fixtures_dir, "emoji.md"), {isFile: true, sanitize:true}, callback
+
+      waitsFor ->
+        callback.callCount > 0
+
+      runs ->
+        [err, contents] = callback.mostRecentCall.args
+
         expect(err).toBeNull()
         expect(contents).toEqual '

\n

\n

'
     it "does nothing to unknown emoji", ->
@@ -33,10 +56,17 @@
         expect(err).toBeNull()
         expect(contents).toEqual '
:lala:
\n'
     it "does not mess up coded emoji", ->
-      roaster Path.join(fixtures_dir, "emoji_bad.md"), {isFile: true}, (err, contents) ->
+      callback = jasmine.createSpy()
+      roaster Path.join(fixtures_dir, "emoji_bad.md"), {isFile: true}, callback
+
+      waitsFor ->
+        callback.callCount > 0
+
+      runs ->
+        [err, contents] = callback.mostRecentCall.args
+
         expect(err).toBeNull()
         expect(contents).toEqual '
not :trollface:\n
\n
wow that is nice :smiley:
'
-
   # describe "headers", ->
   #   [toc, result, resultShort] = []


Update sk.coffee

Update sk.coffee

legal

--- app/locale/sk.coffee
+++ app/locale/sk.coffee
@@ -2051,39 +2051,39 @@
     advanced_web_scripting: "Pokročilé webové skriptovanie"
     input_handling: "Práca so vstupom"
     while_loops: "Cyklus while"
-#    place_game_objects: "Place game objects"
-#    construct_mazes: "Construct mazes"
-#    create_playable_game: "Create a playable, sharable game project"
-#    alter_existing_web_pages: "Alter existing web pages"
-#    create_sharable_web_page: "Create a sharable web page"
-#    basic_input_handling: "Basic Input Handling"
-#    basic_game_ai: "Basic Game AI"
-#    basic_javascript: "Basic JavaScript"
-#    basic_event_handling: "Basic Event Handling"
-#    create_sharable_interactive_web_page: "Create a sharable interactive web page"
-
-#  delta:
-#    added: "Added"
-#    modified: "Modified"
-#    not_modified: "Not Modified"
-#    deleted: "Deleted"
-#    moved_index: "Moved Index"
-#    text_diff: "Text Diff"
-#    merge_conflict_with: "MERGE CONFLICT WITH"
-#    no_changes: "No Changes"
-
-#  legal:
-#    page_title: "Legal"
-#    opensource_intro: "CodeCombat is completely open source."
-#    opensource_description_prefix: "Check out "
-#    github_url: "our GitHub"
-#    opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See "
-#    archmage_wiki_url: "our Archmage wiki"
-#    opensource_description_suffix: "for a list of the software that makes this game possible."
-#    practices_title: "Respectful Best Practices"
-#    practices_description: "These are our promises to you, the player, in slightly less legalese."
-#    privacy_title: "Privacy"
-#    privacy_description: "We will not sell any of your personal information."
+    place_game_objects: "Umiestni herné objekty"
+    construct_mazes: "Vytvor bludiská"
+    create_playable_game: "Vytvor hrateľný a zdielateľný herný projekt"
+    alter_existing_web_pages: "Zmeň existujúce webové stránky"
+    create_sharable_web_page: "Vytvor zdielateľnú webovú stránku"
+    basic_input_handling: "Práca so základným vstupom"
+    basic_game_ai: "Základná herná AI"
+    basic_javascript: "Základy JavaScriptu"
+    basic_event_handling: "Základná práca s udalosťami"
+    create_sharable_interactive_web_page: "Vytvor zdielateľnú, interaktívnu webovú stránku"
+
+  delta:
+    added: "Pridané"
+    modified: "Zmenené"
+    not_modified: "Nezmenené"
+    deleted: "Vymazané"
+    moved_index: "Zmenený Index"
+    text_diff: "Text Diff"
+    merge_conflict_with: " Konflikt pre MERGE s "
+    no_changes: "Bez zmien"
+
+  legal:
+    page_title: "Právne veci"
+    opensource_intro: "CodeCombat je open source."
+    opensource_description_prefix: "Pozri si "
+    github_url: "náš GitHub"
+    opensource_description_center: "a pomôž nám, ak máš chuť! CodeCombat je založený na tucte projektov open source, ktoré milujeme. Pozri sa "
+    archmage_wiki_url: " na našu  wikistránku pre Arcimágov,"
+    opensource_description_suffix: "kde nájdeš zoznam softvéru, ktorý umožnil vytvoriť našu hru."
+    practices_title: "Rešpektované zvyky"
+    practices_description: "Toto sľubujeme našim hráčom:"
+    privacy_title: "Súkromie"
+    privacy_description: "Nebudeme obchodovať so žiadnymi osobnými informáciami."
 #    security_title: "Security"
 #    security_description: "We strive to keep your personal information safe. As an open source project, our site is freely open to anyone to review and improve our security systems."
 #    email_title: "Email"
@@ -2148,26 +2148,26 @@
 #    license: "license"
 #    oreilly: "ebook of your choice"

-#  calendar:
-#    year: "Year"
-#    day: "Day"
-#    month: "Month"
-#    january: "January"
-#    february: "February"
-#    march: "March"
-#    april: "April"
-#    may: "May"
-#    june: "June"
-#    july: "July"
-#    august: "August"
-#    september: "September"
-#    october: "October"
-#    november: "November"
-#    december: "December"
-
-#  code_play_create_account_modal:
-#    title: "You did it!" # This section is only needed in US, UK, Mexico, India, and Germany
-#    body: "You are now on your way to becoming a master coder. Sign up to receive an extra 100 Gems & you will also be entered for a chance to win $2,500 & other Lenovo Prizes."
-#    sign_up: "Sign up & keep coding ▶"
-#    victory_sign_up_poke: "Create a free account to save your code & be entered for a chance to win prizes!"
-#    victory_sign_up: "Sign up & be entered to win $2,500"
+  calendar:
+    year: "Rok"
+    day: "Deň"
+    month: "Mesiac"
+    january: "Január"
+    february: "Február"
+    march: "Marec"
+    april: "Apríl"
+    may: "Máj"
+    june: "Jún"
+    july: "Júl"
+    august: "August"
+    september: "September"
+    october: "Október"
+    november: "November"
+    december: "December"
+
+  code_play_create_account_modal:
+    title: "Dokázal si to!" # This section is only needed in US, UK, Mexico, India, and Germany
+    body: "Si na ceste, kde sa z teba stane majster kódu. Zaregistruj sa  a získaš  100 diamantov & možnosť vyhrať $2,500 & vecné ceny v súťaži s  Lenovom.."
+    sign_up: "Zaregistruj sa &pokračuj v kódovaní ▶"
+    victory_sign_up_poke: "Vytvor si zdarma účet, aby si si uložil svoj program & získal možnosť vyhrať ceny!"
+    victory_sign_up: "Zaregistruj sa & bojuj o  $2,500"


course: while doing any of three steps of workflow, set something in database and store, which locks things (with a minute limit and spinner) to prevent double click -- this also nicely gives a graphical illustration of what is going on!

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -24,9 +24,9 @@
 ###
 TODO:

-- [ ] (0:45?)  #now -- make confirm copies have nice text and look nice
+- [ ] (0:45?) (0:55) while doing any of three steps of workflow, set something in database and store, which locks things (with a minute limit and spinner) to prevent double click.
 - [ ] (0:45?) re-assign/re-copy/re-collect need to have ... and confirm, since they are dangerous.
-- [ ] (0:45?) while doing any of three steps of workflow, set something in database and store, which locks things (with a minute limit and spinner) to prevent double click.
+- [ ] (0:30?) optimization -- get rid of the open/close/stop closures.

 NEXT VERSION (after a release):
@@ -41,6 +41,7 @@
 - [ ] (8:00?) #unclear way to show other viewers that a field is being actively edited by a user (no idea how to do this in react)

 DONE:
+- [x] (0:45?) (2:30) make confirm copies have nice text and look nice
 - [x] (0:45?) (0:37) xs mobile assignment looks bad -- need a fullscreen toggle thing.
 - [x] (1:00?) (1:17) make it so creating a project does not open it until user explicitly opens it.
 - [x] (1:30?) #now make the assign/collect/return all buttons have a confirmation and an option to only collect from students not already collected from already; this will clarify what happens on re-assign, etc.
@@ -139,6 +140,8 @@

 {User} = require('users')

+PARALLEL_LIMIT = 3  # number of async things to do in parallel
+
 flux_name = (project_id, course_filename) ->
     return "editor-#{project_id}-#{course_filename}"

@@ -333,7 +336,7 @@
                             cb      : cb
                 ], cb)
             id = @set_activity(desc:"Creating #{students.length} student projects")
-            async.mapLimit student_ids, 5, f, (err) =>
+            async.mapLimit student_ids, PARALLEL_LIMIT, f, (err) =>
                 @set_activity(id:id)
                 if err
                     @set_error("error creating student projects -- #{err}")
@@ -518,6 +521,8 @@
         #
         # where time >= now is the current time in milliseconds.
         copy_assignment_from_student: (assignment, student) =>
+            if @_start_copy(assignment, student, 'last_collect')
+                return
             id = @set_activity(desc:"Copying assignment from a student")
             finish = (err) =>
                 @clear_activity(id)
@@ -572,7 +577,7 @@
                         if err
                             errors += "\n #{err}"
                         cb()
-            async.mapLimit store.get_student_ids(deleted:false), 10, f, (err) =>
+            async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
                 if errors
                     error(errors)
                 else
@@ -587,6 +592,8 @@
         # where time >= now is the current time in milliseconds.

         return_assignment_to_student: (assignment, student) =>
+            if @_start_copy(assignment, student, 'last_return_graded')
+                return
             id = @set_activity(desc:"Returning assignment to a student")
             finish = (err) =>
                 @clear_activity(id)
@@ -642,7 +649,7 @@
                         if err
                             errors += "\n #{err}"
                         cb()
-            async.mapLimit store.get_student_ids(deleted:false), 10, f, (err) =>
+            async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
                 if errors
                     error(errors)
                 else
@@ -650,10 +657,42 @@

         _finish_copy: (assignment, student, type, err) =>
             if student? and assignment?
+                store = get_store(); student = store.get_student(student); assignment = store.get_assignment(assignment)
                 where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
                 x = syncdb.select_one(where:where)?[type] ? {}
                 x[student.get('student_id')] = {time: misc.mswalltime(), error:err}
                 @_update(set:{"#{type}":x}, where:where)
+
+        _start_copy: (assignment, student, type) =>
+            console.log("_start_copy")
+            if student? and assignment?
+                store = get_store(); student = store.get_student(student); assignment = store.get_assignment(assignment)
+                where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
+                x = syncdb.select_one(where:where)?[type] ? {}
+                y = (x[student.get('student_id')]) ? {}
+                if y.start? and new Date() - y.start <= 15000
+                    console.log("_start_copy: bail")
+                    return true  # never retry a copy until at least 15 seconds later.
+                y.start = misc.mswalltime()
+                x[student.get('student_id')] = y
+                console.log("_start_copy: do it")
+                @_update(set:{"#{type}":x}, where:where)
+            return false
+
+        _stop_copy: (assignment, student, type) =>
+            if student? and assignment?
+                store = get_store(); student = store.get_student(student); assignment = store.get_assignment(assignment)
+                where = {table:'assignments', assignment_id:assignment.get('assignment_id')}
+                x = syncdb.select_one(where:where)?[type]
+                if not x?
+                    return
+                y = (x[student.get('student_id')])
+                if not y?
+                    return
+                if y.start?
+                    delete y.start
+                    x[student.get('student_id')] = y
+                    @_update(set:{"#{type}":x}, where:where)

         # Copy the files for the given assignment to the given student. If
         # the student project doesn't exist yet, it will be created.
@@ -665,6 +704,8 @@
         #
         # where time >= now is the current time in milliseconds.
         copy_assignment_to_student: (assignment, student) =>
+            if @_start_copy(assignment, student, 'last_assignment')
+                return
             id = @set_activity(desc:"Copying assignment to a student")
             finish = (err) =>
                 @clear_activity(id)
@@ -735,7 +776,7 @@
                             errors += "\n #{err}"
                         cb()

-            async.mapLimit store.get_student_ids(deleted:false), 5, f, (err) =>
+            async.mapLimit store.get_student_ids(deleted:false), PARALLEL_LIMIT, f, (err) =>
                 if errors
                     error(errors)
                 else
@@ -752,6 +793,18 @@
                     @return_assignment_to_student(assignment_id, student_id)
                 else
                     @set_error("copy_assignment -- unknown type: #{type}")
+
+        # This doesn't really stop it yet, since that's not supported by the backend.
+        # It does stop the spinner and let the user try to restart the copy.
+        stop_copying_assignment: (type, assignment_id, student_id) =>
+            switch type
+                when 'assigned'
+                    type = 'last_assignment'
+                when 'collected'
+                    type = 'last_collect'
+                when 'graded'
+                    type = 'last_return_graded'
+            @_stop_copy(assignment_id, student_id, type)

         open_assignment: (type, assignment_id, student_id) =>
             # type = assigned, collected, graded
@@ -1011,10 +1064,11 @@
         student     : rtypes.object.isRequired
         user_map    : rtypes.object.isRequired
         project_map : rtypes.object.isRequired  # here entirely to cause an update when project activity happens
+        assignments : rtypes.object.isRequired  # here entirely to cause an update when project activity happens
         background  : rtypes.string

     shouldComponentUpdate: (nextProps, nextState) ->
-        return @state != nextState or @props.student != nextProps.student or @props.project_map != nextProps.project_map or @props.user_map != nextProps.user_map or @props.background != nextProps.background
+        return @state != nextState or @props.student != nextProps.student or @props.assignments != nextProps.assignments  or @props.project_map != nextProps.project_map or @props.user_map != nextProps.user_map or @props.background != nextProps.background

     displayName : "CourseEditorStudent"

@@ -1211,12 +1265,13 @@

 Students = rclass
     propTypes:
-        name         : rtypes.string.isRequired
-        flux         : rtypes.object.isRequired
-        project_id   : rtypes.string.isRequired
-        students     : rtypes.object.isRequired
-        user_map     : rtypes.object.isRequired
-        project_map  : rtypes.object.isRequired
+        name        : rtypes.string.isRequired
+        flux        : rtypes.object.isRequired
+        project_id  : rtypes.string.isRequired
+        students    : rtypes.object.isRequired
+        user_map    : rtypes.object.isRequired
+        project_map : rtypes.object.isRequired
+        assignments : rtypes.object.isRequired

     displayName : "CourseEditorStudents"

@@ -1378,7 +1433,9 @@
             <Student background={if i%2==0 then "#eee"} key={x.student_id}
                      student_id={x.student_id} student={@props.students.get(x.student_id)}
                      user_map={@props.user_map} flux={@props.flux} name={@props.name}
-                     project_map={@props.project_map} />
+                     project_map={@props.project_map}
+                     assignments={@props.assignments}
+                     />

     render_show_deleted: (num_deleted) ->
         if @state.show_deleted
@@ -1482,6 +1539,9 @@
     copy: (type, assignment_id, student_id) ->
         @props.flux.getActions(@props.name).copy_assignment(type, assignment_id, student_id)

+    stop: (type, assignment_id, student_id) ->
+        @props.flux.getActions(@props.name).stop_copying_assignment(type, assignment_id, student_id)
+
     save_grade: (e) ->
         e?.preventDefault()
         @props.flux.getActions(@props.name).set_grade(@props.assignment, @props.student, @state.grade)
@@ -1533,7 +1593,7 @@
         <ButtonGroup key='open_recopy'>
             <Button key="copy" bsStyle='warning' onClick={copy}>
                 <Tip title={name} placement={placement}
-                    tip={{copy_tip}
You have already copied these files so take extra care.}>
+                    tip={{copy_tip}}>
                     <Icon name='share-square-o' rotate={"180" if name=='Collect'}/> Re-{name.toLowerCase()}
                 </Tip>
             </Button>
@@ -1544,6 +1604,21 @@
             </Button>
         </ButtonGroup>

+    render_open_copying: (name, open, stop) ->
+        if name == "Return"
+            placement = 'left'
+        <ButtonGroup key='open_copying'>
+            <Button key="copy" bsStyle='success' disabled={true}>
+                <Icon name="circle-o-notch" spin /> {name}ing
+            </Button>
+            <Button key="stop" bsStyle='danger' onClick={stop}>
+                <Icon name="times" />
+            </Button>
+            <Button key='open'  onClick={open}>
+                <Icon name="folder-open-o" /> Open
+            </Button>
+        </ButtonGroup>
+
     render_copy: (name, copy, copy_tip) ->
         if name == "Return"
             placement = 'left'
@@ -1563,10 +1638,13 @@
     render_last: (name, obj, type, info, enable_copy, copy_tip, open_tip) ->
         open = => @open(type, info.assignment_id, info.student_id)
         copy = => @copy(type, info.assignment_id, info.student_id)
+        stop = => @stop(type, info.assignment_id, info.student_id)
         obj ?= {}
         v = []
         if enable_copy
-            if obj.time
+            if obj.start
+                v.push(@render_open_copying(name, open, stop))
+            else if obj.time
                 v.push(@render_open_recopy(name, open, copy, copy_tip, open_tip))
             else
                 v.push(@render_copy(name, copy, copy_tip))
@@ -2349,7 +2427,9 @@
         if @props.flux? and @props.students? and @props.user_map? and @props.project_map?
             <Students flux={@props.flux} students={@props.students}
                       name={@props.name} project_id={@props.project_id}
-                      user_map={@props.user_map} project_map={@props.project_map} />
+                      user_map={@props.user_map} project_map={@props.project_map}
+                      assignments={@props.assignments}
+                      />
         else
             return <Loading />


fix smtp mail content

fix smtp mail content


fix syntax error

fix syntax error

--- components/page.cjsx
+++ components/page.cjsx
@@ -75,10 +75,10 @@

           {@state.title}

-    return
-

-        {pageTitleElem}
-

+
+

+      {pageTitleElem}
+

 PageContent = React.createClass
   propTypes:
@@ -96,8 +96,7 @@
     style =
       whiteSpace: 'pre'
       marginBottom: '20px'
-    return
-
{this.state.content}

+
{this.state.content}

 PageForm = React.createClass
   propTypes:


Typo (faltou um @)

Typo (faltou um @)

--- src/coffee/portal-minicart.coffee
+++ src/coffee/portal-minicart.coffee
@@ -8,7 +8,7 @@
 		@options = $.extend {}, $.fn.vtexMinicart.defaults, options
 		@context = context
 		@hoverContext = @context.add('.show-minicart-on-hover')
-		@cartData = options.cartData	# default {}
+		@cartData = @options.cartData	# default {}
 		@init()

 	getOrderFormURL: =>


Fix compatiblity with new YouTube interface

--- src/popout_for_youtube.coffee
+++ src/popout_for_youtube.coffee
@@ -62,9 +62,10 @@
 class Button extends Node

   constructor: (@video)->
-    button           = document.createElement('button')
-    button.title     = 'Pop out'
-    button.className = 'popout-for-youtube__button'
+    @alignmentInterval = null
+    button             = document.createElement('button')
+    button.title       = 'Pop out'
+    button.className   = 'popout-for-youtube__button'
     super button
     @setClickBehavior()
     @maintainAlignment()
@@ -82,22 +83,27 @@
     @node.style.left = "#{point.x}px"

   remove: ->
+    clearInterval @alignmentInterval
     @node.parentNode.removeChild @node

   maintainAlignment: ->
-    setInterval =>
-      @align()
+    @alignmentInterval = setInterval =>
+      @align() # Can fail if the video node changes. Ignore.
     , 100

 class YouTubeVideoPage

   constructor: ->

+    @previousVideoId = null
+
     @whenVideoChanges =>
-      try @button.remove()
-      @videoId = @getVideoId()
-      @video   = new Video @videoId
-      @button  = new Button @video
+      try @button.remove() # Can fail if the button isn't arleady there. Ignore.
+      @previousVideoId = @newVideoId
+      @newVideoId      = @getVideoId()
+      @video           = new Video @newVideoId
+      @button          = new Button @video
+
       document.body.appendChild @button.node

       Extension.notifyVideoViewed()
@@ -105,10 +111,11 @@
   whenVideoChanges: (callback)->
     setInterval =>
       callback() if @videoChanged()
-    , 250
+    , 100

   videoChanged: ->
-    @videoId != @getVideoId()
+    newId = @getVideoId()
+    !@video || !@video.node || !@video.node.src || newId != @previousVideoId

   getVideoId: ->
     new URLSearchParams(document.location.search.substring(1)).get('v')


Removing unrequired StatusMessage injection

--- app/assets/javascripts/admin/standing_orders/controllers/standing_order_controller.js.coffee
+++ app/assets/javascripts/admin/standing_orders/controllers/standing_order_controller.js.coffee
@@ -1,4 +1,4 @@
-angular.module("admin.standingOrders").controller "StandingOrderController", ($scope, StatusMessage, StandingOrder, customers, schedules, paymentMethods, shippingMethods) ->
+angular.module("admin.standingOrders").controller "StandingOrderController", ($scope, StandingOrder, customers, schedules, paymentMethods, shippingMethods) ->
   $scope.standingOrder = StandingOrder.standingOrder
   $scope.customers = customers
   $scope.schedules = schedules


Increase the timeout to 10 seconds, because slow loading projects benefit most from the preloader.

--- framer/Extras/Preloader.coffee
+++ framer/Extras/Preloader.coffee
@@ -9,7 +9,7 @@
 		@_mediaLoaded = []
 		@_isLoading = false

-		@timeout = 5
+		@timeout = 10

 		@context = new Context({parent: Framer.CurrentContext, name: "Preloader"})


Social: move mapper functions to top of file

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -2,6 +2,43 @@

   constructor: (options = {}, data) ->
     super options, data
+
+  mapActivities = (messages)->
+    # if no result, no need to do something
+    return messages unless messages
+    # get messagees from result set if they are not at the first level
+    messages = messages.messageList if messages.messageList
+    messages = [].concat(messages)
+    revivedMessages = []
+    {SocialMessage} = KD.remote.api
+    for message in messages
+      m = new SocialMessage message.message
+      m._id = message.message.id
+      m.account = {}
+      m.account.constructorName = "JAccount"
+      m.account._id = message.accountOldId
+      m.meta = {}
+      m.meta.likes = message.interactions?.like?.length or 0
+      m.meta.createdAt = message.message.createdAt
+      m.replies = message.replies
+      m.repliesCount = message.replies.length or 0
+      m.interactions = message.interactions
+
+      revivedMessages.push m
+
+    return revivedMessages
+
+  mapActivity = (message)->
+    # if no result, no need to do something
+    return message unless message
+
+    {SocialMessage} = KD.remote.api
+    m = new SocialMessage message
+    m._id = message.id
+    m.meta = {}
+    m.meta.createdAt = message.createdAt
+
+    return m

   getCurrentGroup = (callback)->
     groupsController = KD.getSingleton "groupsController"
@@ -66,47 +103,6 @@

     return mappedMessages

-  mapActivities = (messages)->
-    # if no result, no need to do something
-    return messages unless messages
-    # get messagees from result set if they are not at the first level
-    messages = messages.messageList if messages.messageList
-    messages = [].concat(messages)
-    revivedMessages = []
-    {SocialMessage} = KD.remote.api
-    for message in messages
-      m = new SocialMessage message.message
-      m._id = message.message.id
-      m.account = {}
-      m.account.constructorName = "JAccount"
-      m.account._id = message.accountOldId
-      m.meta = {}
-      m.meta.likes = message.interactions?.like?.length or 0
-      m.meta.createdAt = message.message.createdAt
-      m.replies = message.replies
-      m.repliesCount = message.replies.length or 0
-      m.interactions = message.interactions
-
-      # todo- remove those event bindings
-      m.on "MessageReplySaved", log
-      m.on "update", log
-
-      revivedMessages.push m
-
-    return revivedMessages
-
-  mapActivity = (message)->
-    # if no result, no need to do something
-    return message unless message
-
-    {SocialMessage} = KD.remote.api
-    m = new SocialMessage message
-    m._id = message.id
-    m.meta = {}
-    m.meta.createdAt = message.createdAt
-
-    return m
-
   mapChannels = (channels)->
     return channels unless channels
     revivedChannels = []
@@ -128,6 +124,8 @@
       # bind all events
       registerAndOpenChannel c

+      c.on "*", -> console.log arguments
+      c.on "MessageAdded", -> console.log arguments, "messageadded"
       # push channel into stack
       revivedChannels.push c

@@ -166,6 +164,7 @@
       sendPrivateMessageRequest 'sendPrivateMessage', rest...
     fetchPrivateMessages :(rest...)->
       sendPrivateMessageRequest 'fetchPrivateMessages', rest...
+    revive : mapActivity

   channel:
     list                 : fetchChannels


Fix huge regression when not displaying conversations

--- client/app/stores/message_store.coffee
+++ client/app/stores/message_store.coffee
@@ -250,12 +250,12 @@
     getMessagesByMailbox: (mailboxID, useConversations) ->
         conversationIDs = []

-        if useConversations
-            sequence = _messages.filter (message) ->
-                mailboxes = Object.keys message.get 'mailboxIDs'
-                if mailboxID not in mailboxes
-                    return false
-
+        sequence = _messages.filter (message) ->
+            mailboxes = Object.keys message.get 'mailboxIDs'
+            if mailboxID not in mailboxes
+                return false
+
+            if useConversations
                 # one message of each conversation
                 conversationID = message.get 'conversationID'
                 if conversationID in conversationIDs
@@ -263,9 +263,9 @@
                 else
                     conversationIDs.push conversationID
                     return true
-            .sort(__getSortFunction _sortField, _sortOrder)
-        else
-            sequence = _messages.sort(__getSortFunction _sortField, _sortOrder)
+            else
+                return true
+        .sort(__getSortFunction _sortField, _sortOrder)

         # sequences are lazy so we need .toOrderedMap() to actually execute it
         _currentMessages = sequence.toOrderedMap()


Making Nitin dangerous

Making Nitin dangerous

--- client/landing/Business/AppView.coffee
+++ client/landing/Business/AppView.coffee
@@ -64,7 +64,7 @@

Scale as you grow

-              Fully scalable environments with customizable stacks gives you
+              Fully scalable environments with customizable stacks give you
               the ability to scale as you get bigger in size. Size matters.


$postLikes.find

$postLikes.find

--- vendor/assets/javascripts/jquery.socialShareButtons.js.coffee
+++ vendor/assets/javascripts/jquery.socialShareButtons.js.coffee
@@ -18,9 +18,9 @@
         $socials = $ @
         $postLikes = $socials.find(settings.containerSelector)
         if $postLikes.length
-          new SocialTw $(settings.twSelector), settings.url
-          new SocialFb $(settings.fbSelector), settings.url
-          new SocialVk $(settings.vkSelector), settings.url
+          new SocialTw $postLikes.find(settings.twSelector), settings.url
+          new SocialFb $postLikes.find(settings.fbSelector), settings.url
+          new SocialVk $postLikes.find(settings.vkSelector), settings.url

           if settings.isInitScroller
             initScroller $socials, $postLikes
@@ -40,8 +40,9 @@

   class SocialBase
     constructor: (@$selector, @url) ->
-      @getCount()
-      @initClick()
+      if @$selector.length
+        @getCount()
+        @initClick()

     getCount: -> throw Error 'unimplemented method'


Fix bug to display unit name correctly on storing creation

--- app/assets/javascripts/cocoon/iceberg.js.coffee
+++ app/assets/javascripts/cocoon/iceberg.js.coffee
@@ -140,7 +140,7 @@
       new Iceberg($(this))

     $('table.list').on 'cocoon:after-insert', (event, inserted) ->
-        iceberg = new Iceberg($(inserted), "add") if inserted?
+      iceberg = new Iceberg($(inserted), "add") if inserted?
       $('*[data-unit-name]').each ->
         $(this).find('.item-population-unit-name').html($(this).attr('data-unit-name'))


This fix for the 1 cent price decrease bug should get the last floating point bug out

--- app/assets/javascripts/realtime_price.js.coffee
+++ app/assets/javascripts/realtime_price.js.coffee
@@ -111,7 +111,9 @@
       thousand_separator = " "
       cent_separator = ","
     input_dollars = parseInt(input_price) + ""
-    input_cents = parseInt(100 * (input_price - parseInt(input_price)))
+    # Using Math.round in the next line corrects floating point error
+    # 17.99 - 17 = 0.9899999999999984 according to Javascript
+    input_cents = Math.round(100 * (input_price - parseInt(input_price)))
     for char, i in input_dollars.split('').reverse()
       formatted_price = char + formatted_price
       formatted_price = thousand_separator + formatted_price if ((i+1) % 3 == 0 && (input_dollars.length-1) != i)


Now, we should be good with the Env injection... :)

--- app/assets/javascripts/sprangular/module.coffee
+++ app/assets/javascripts/sprangular/module.coffee
@@ -24,7 +24,7 @@
       newInstance

 # Default Headers
-Sprangular.config ["$httpProvider", "$locationProvider", "$logProvider", ($httpProvider, $locationProvider, $logProvider) ->
+Sprangular.config ["$httpProvider", "$locationProvider", "$logProvider", "Env", ($httpProvider, $locationProvider, $logProvider, Env) ->
   $httpProvider.defaults.headers.common['Accept'] = 'application/json'
   $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
   $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded'


add routes for password reset

add routes for password reset

--- config/routes.coffee
+++ config/routes.coffee
@@ -5,6 +5,9 @@
     map.get('/authenticated', 'passport#isAuthenticated')
     map.post('/register', 'passport#register')
     map.post('/login', 'passport#login')
+    map.post("/login/forgot", "passport#forgotPassword")
+    map.get("/password/reset/:key", "passport#resetForm")
+    map.post("/password/reset/:key", "passport#resetPassword")
     map.get('/logout', 'passport#logout')

     map.get('/api/users', 'users#users')


Data files: improve script; fix for failed content.

--- scripts/normalize-data._coffee
+++ scripts/normalize-data._coffee
@@ -7,7 +7,8 @@
 # - Adds our `ready`, `failed`, and `progress` properties.
 #   Assumes all content we're processing finished successfully.
 #
-# - Moves the image info into a `dzi` container, if it isn't already.
+# - Moves the image info into a `dzi` container, if it exists.
+#   Update: we had a bug here, so also fixes that bug.
 #
 # - Fixes the `tileSize` and `tileOverlap` properties from strings to ints.
 #
@@ -16,6 +17,7 @@

 config = require '../config'
 crypto = require 'crypto'
+echo = console.log
 FS = require 'fs'
 Path = require 'path'

@@ -42,8 +44,17 @@
     path = path.replace /_([A-Z])/g, '$1'
     Path.basename path, Path.extname path

-# Now go through each existing content by ID, and process it:
-for idFileName in FS.readdir DIR_BY_ID_PATH, _
+# Now query all the IDs we have:
+echo 'Querying content files...'
+idFileNames = FS.readdir DIR_BY_ID_PATH, _
+totalNumIds = idFileNames.length
+echo "#{totalNumIds} files found."
+
+# Now go through each ID,and process it, outputting percentage every minute:
+echo 'Processing...'
+nextLogTime = Date.now() + 1000 * 60
+
+for idFileName, i in idFileNames
     # Skip non-content files:
     continue if (Path.extname idFileName) isnt '.json'

@@ -53,12 +64,15 @@
     # Massage the data by reading it, modifying it, then writing it back.
     # NOTE: This assumes full knowledge of the starting data.
     data = require idPath   # require() auto-parses JSON!
+    ready = (!!data.width and !!data.height) or
+        # if we're re-running this after our conversion:
+        (!!data.dzi?.width and !!data.dzi?.height)
     data =
         id: id
         url: data.url
-        ready: true
-        failed: false
-        progress: 1
+        ready: ready
+        failed: not ready
+        progress: if ready then 1 else 0
         mime: data.mime
         size: data.size
         dzi: data.dzi or {  # braces needed otherwise CS syntax error
@@ -68,6 +82,7 @@
             tileOverlap: parseInt data.tileOverlap, 10
             tileFormat: data.tileFormat
         }
+    delete data.dzi if not ready    # fix bug we made
     FS.writeFile idPath, (JSON.stringify data, null, 4), _

     # Generate a content-by-url symlink.
@@ -77,3 +92,10 @@
         FS.symlink urlToIdPath, urlPath, _
     catch err
         throw err unless err.code is 'EEXIST'
+
+    if Date.now() >= nextLogTime
+        percent = 100 * (i + 1) / totalNumIds
+        remaining = totalNumIds - (i + 1)
+        echo "#{percent.toFixed 2}% processed; #{remaining} files remaining."
+
+echo 'All done!'


Auto-submit to ladder on run with no errors for all course-ladder levels

--- app/views/play/level/tome/CastButtonView.coffee
+++ app/views/play/level/tome/CastButtonView.coffee
@@ -35,7 +35,7 @@
     # mirror level is added to
     @loadMirrorSession() if @options.level.get('slug') in ['ace-of-coders', 'elemental-wars', 'the-battle-of-sky-span', 'tesla-tesoro', 'escort-duty']
     @mirror = @mirrorSession?
-    @autoSubmitsToLadder = @options.level.get('slug') in ['wakka-maul']
+    @autoSubmitsToLadder = @options.level.isType('course-ladder')
     # Show publish CourseVictoryModal if they've already published
     if options.session.get('published')
       Backbone.Mediator.publish 'level:show-victory', { showModal: true, manual: false }


keyboard: api improvements

keyboard: api improvements

--- src/core/keyboard/listener.coffee
+++ src/core/keyboard/listener.coffee
@@ -27,12 +27,11 @@
     KDKeyboardListener.currentListener?.reset()

     seen = {}
-    @triageComboMaps().forEach (m) ->
-      m.eachCombo (combo, options = { global: yes }, listener) ->
-        return  if seen[combo]
-        seen[combo] = yes
-        method = if options.global then 'bindGlobal' else 'bind'
-        Mousetrap[method] combo, listener
+    @combos (combo, options = { global: yes }, listener) ->
+      return  if seen[combo]
+      seen[combo] = yes
+      method = if options.global then 'bindGlobal' else 'bind'
+      Mousetrap[method] combo, listener

     KDKeyboardListener.currentListener = this
     @isListening = yes
@@ -46,7 +45,7 @@
     KDKeyboardListener.currentListener = null
     return this

-  triageComboMaps: ->
+  getCombos: ->
     Object.keys @maps
       # prioritize by key:
       .sort (a, b) -> b - a # descending priority
@@ -57,6 +56,10 @@
         a.concat b
       , []

+  combos: (fn) ->
+    @getCombos().forEach (m) -> m.eachCombo fn
+    return this
+
   isActive: ->
     @isListening and this is KDKeyboardListener.currentListener


kdbuttonview: lazy loading for buttons

kdbuttonview: lazy loading for buttons

--- components/buttons/KDButtonView.coffee
+++ components/buttons/KDButtonView.coffee
@@ -34,12 +34,25 @@
   setFocus:-> @$().trigger 'focus'

   setDomElement:(cssClass)->
-    @domElement = $ """
+    {lazyDomId, tagName} = @getOptions()
+
+    if lazyDomId
+      el = document.getElementById lazyDomId
+      for klass in "kdview #{cssClass}".split ' ' when klass.length
+        el.classList.add klass
+
+    unless el?
+      warn "No lazy DOM Element found with given id #{lazyDomId}."  if lazyDomId
+      el =
+      """
       <button type='#{@getOptions().type}' class='kdbutton #{cssClass}' id='#{@getId()}'>

         Title
       </button>
       """
+
+    @domElement = $ el
+

   setTitle:(title)->
     @$('.button-title').html title


Fix href in case of image embedding

Fix href in case of image embedding

* Previously broken by `Drawing.href` feature
* Also use `href` if requested when embedding images

--- src/svgtiler.coffee
+++ src/svgtiler.coffee
@@ -229,7 +229,7 @@
               when '.png', '.jpg', '.jpeg', '.gif'
                 dirname: dirname
                 svg: """
-                  <image xlink:href="#{encodeURI data}"/>
+                  <image #{Drawing.hrefAttr()}="#{encodeURI data}"/>
                 """
               when '.svg'
                 dirname: path.dirname filename
@@ -676,6 +676,7 @@

 class Drawing extends Input
   @inlineImages: not window?
+  @hrefAttr: -> if @useHref then 'href' else 'xlink:href'
   load: (data) ->
     ## Turn strings into arrays
     data = for row in data
@@ -724,7 +725,6 @@
     doc = domImplementation.createDocument SVGNS, 'svg'
     svg = doc.documentElement
     svg.setAttribute 'xmlns:xlink', XLINKNS unless Drawing.useHref
-    href = if Drawing.useHref then 'href' else 'xlink:href'
     svg.setAttribute 'version', '1.1'
     #svg.appendChild defs = doc.createElementNS SVGNS, 'defs'
     ## <style> tags for CSS
@@ -802,7 +802,7 @@
         node.setAttribute 'height', height or use.getAttribute 'height'
         symbol.setAttribute 'viewBox', "0 0 #{width} #{height}"
         symbol.appendChild node
-      use.setAttribute href, '#' + inlineImages[attributes]
+      use.setAttribute Drawing.hrefAttr(), '#' + inlineImages[attributes]
       false
     ## Lay out the symbols in the drawing via SVG <use>.
     viewBox = [0, 0, 0, 0]  ## initially x-min, y-min, x-max, y-max
@@ -822,7 +822,7 @@
         continue unless symbol?
         levels[symbol.zIndex] ?= []
         levels[symbol.zIndex].push use = doc.createElementNS SVGNS, 'use'
-        use.setAttribute href, '#' + symbol.id
+        use.setAttribute Drawing.hrefAttr(), '#' + symbol.id
         use.setAttribute 'x', x
         use.setAttribute 'y', y
         scaleX = scaleY = 1


Avoid calling scrollToTarget twice when rendering entry pages

--- assets/javascripts/views/content/content.coffee
+++ assets/javascripts/views/content/content.coffee
@@ -51,6 +51,9 @@
     @addClass @constructor.loadingClass
     return

+  isLoading: ->
+    @el.classList.contains @constructor.loadingClass
+
   hideLoading: ->
     @removeClass @constructor.loadingClass
     return
@@ -92,12 +95,14 @@
     return

   scrollToTarget: ->
+    return if @isLoading()
     if @routeCtx.hash and el = @findTargetByHash @routeCtx.hash
       $.scrollToWithImageLock el, @scrollEl, 'top',
         margin: 20 + if @scrollEl is @el then 0 else $.offset(@el).top
       $.highlight el, className: '_highlight'
     else
       @scrollTo @scrollMap[@routeCtx.state.id]
+    clearTimeout @scrollTimeout
     return

   onReady: =>
@@ -121,7 +126,7 @@
   beforeRoute: (context) =>
     @cacheScrollPosition()
     @routeCtx = context
-    @delay @scrollToTarget
+    @scrollTimeout = @delay @scrollToTarget
     return

   cacheScrollPosition: ->


test against rimraf module when deleting directory recursively

--- test/unit/coffee/FSPersistorManagerTests.coffee
+++ test/unit/coffee/FSPersistorManagerTests.coffee
@@ -6,6 +6,7 @@
 modulePath = "../../../app/js/FSPersistorManager.js"
 SandboxedModule = require('sandboxed-module')
 fs = require("fs")
+response = require("response")

 describe "FSPersistorManagerTests", ->

@@ -17,6 +18,7 @@
       unlink:sinon.stub()
       rmdir:sinon.stub()
       exists:sinon.stub()
+    @Rimraf = sinon.stub()
     @LocalFileWriter =
       writeStream: sinon.stub()
     @requires =
@@ -25,6 +27,8 @@
       "logger-sharelatex":
         log:->
         err:->
+      "response":response
+      "rimraf":@Rimraf
     @location = "/tmp"
     @name1 = "530f2407e7ef165704000007/530f838b46d9a9e859000008"
     @name1Filtered ="530f2407e7ef165704000007_530f838b46d9a9e859000008"
@@ -115,11 +119,11 @@

   describe "deleteDirectory", ->
     beforeEach ->
-      @Fs.rmdir.callsArgWith(1,@error)
+      @Rimraf.callsArgWith(1,@error)

-    it "Should call rmdir with correct options", (done) ->
+    it "Should call rmdir(rimraf) with correct options", (done) ->
       @FSPersistorManager.deleteDirectory @location, @name1, (err) =>
-        @Fs.rmdir.calledWith("#{@location}/#{@name1}").should.equal.true
+        @Rimraf.calledWith("#{@location}/#{@name1}").should.equal.true
         done()

     it "Should propogate the error", (done) ->


touching session after reading from file

--- modules/http/platforms/node/lib/sessions/PersisedRegistry.coffee
+++ modules/http/platforms/node/lib/sessions/PersisedRegistry.coffee
@@ -22,8 +22,10 @@
                 each: (id, data, next)=>

                     sess = new (floyd.tools.objects.resolve @_config.sessions.type) id, @_config.sessions
+
+                    floyd.tools.objects.extend sess, data

-                    floyd.tools.objects.extend sess, data
+                    sess.touch()

                     @add sess


fixed dupbuilds (fixes #11)

fixed dupbuilds (fixes #11)

--- src/lib/renderer/ninja.coffee
+++ src/lib/renderer/ninja.coffee
@@ -63,7 +63,7 @@

   rendered = module.exports.render resolver

-  args = ['-v', '-C', resolver.rootDir]
+  args = ['-v', '-w', 'dupbuild=err', '-C', resolver.rootDir]

   switch os.platform()
     when 'darwin' or 'linux'


make deployment task quit on ended deployment

--- server/lib/tasks/deploy.coffee
+++ server/lib/tasks/deploy.coffee
@@ -31,4 +31,9 @@
     DeployClient.start(target, description)
   )

-startDeployTask()
+startDeployTask().then( ->
+  process.exit(0)
+).catch( (err) ->
+  console.error(err)
+  process.exit(1)
+)