fixed typo

fixed typo

--- coffee/lib/abstract-chosen.coffee
+++ coffee/lib/abstract-chosen.coffee
@@ -10,7 +10,7 @@

     this.set_up_html()
     this.register_observers()
-    # instrantiation done, fire ready
+    # instantiation done, fire ready
     this.on_ready()

   set_default_values: ->


Added tests for $router.register().

Added tests for $router.register().

--- test/specs/router-spec.coffee
+++ test/specs/router-spec.coffee
@@ -10,6 +10,38 @@
         module 'fakeModule'

     describe '$router', ->
+        it '$router.register() will push the rule object into routerProvider.rules.', inject ($router) ->
+            $router.register 'web',
+                uri: '/'
+                templateUrl: '/template.html'
+                controller: 'HomeController'
+            routerProvider.rules.web.parents.pop()
+            delete routerProvider.rules.web.getCurrentParams
+            expect(routerProvider.rules).toEqual
+                web:
+                    uri: '/'
+                    templateUrl: '/template.html'
+                    controller: 'HomeController'
+                    namespace: 'web'
+                    uriParams: []
+                    matchPattern: '/'
+                    hrefTemplate: '/'
+                    parents: []
+                    matchReg: /^\/$/
+
+        it 'check getCurrentParams() of $router.register().', inject ($router, $location) ->
+            spyOn($location, 'path').and.returnValue '/AWFQ3MSHnmhfNRKX-yO9/?filter=root'
+            spyOn($location, 'search').and.returnValue
+                filter: 'root'
+            $router.register 'web',
+                uri: '/{userId:[\\w-]{20}}/?filter'
+                templateUrl: '/template.html'
+                controller: 'HomeController'
+            rule = routerProvider.rules.web
+            expect(rule.getCurrentParams()).toEqual
+                userId: 'AWFQ3MSHnmhfNRKX-yO9'
+                filter: 'root'
+
         it '$router.registerView() will push the view into views and call renderViews().', inject ($router) ->
             spyOn routerProvider, 'renderViews'
             $router.registerView 'view'


simplify logic and use 'dashboard' for link

--- tutor/src/components/task-step/step-footer-mixin.cjsx
+++ tutor/src/components/task-step/step-footer-mixin.cjsx
@@ -83,28 +83,23 @@
     @[renderDetailsForPanelMethod]?({stepId, taskId, courseId, review}) or @renderDefaultDetails({stepId, taskId, courseId, review})

   renderBackButton: ({taskId, courseId, review, panel}, custombuttonClasses) ->
-    defaultButtonClasses = 'btn btn-primary'
-
-    backButton = <TutorLink
-      to='viewStudentDashboard'
-      key='step-back-sd'
-      params={{courseId}}
-      className={custombuttonClasses or defaultButtonClasses}>
-        Back to Dashboard
-    </TutorLink>
-
-    if panel? and panel is 'teacher-read-only'
-      defaultButtonClasses = 'btn btn-default'
-
-      backButton = <TutorLink
+    if panel is 'teacher-read-only'
+      <TutorLink
         to='viewScores'
         key='step-back-scores'
         params={{courseId}}
-        className={custombuttonClasses or defaultButtonClasses}>
+        className={custombuttonClasses or 'btn btn-default'}>
           Back to Scores
       </TutorLink>
+    else
+      <TutorLink
+        to='dashboard'
+        key='step-back-sd'
+        params={{courseId}}
+        className={custombuttonClasses or 'btn btn-primary'}>
+          Back to Dashboard
+      </TutorLink>

-    backButton

   renderTeacherReadOnlyButtons: ({taskId, courseId, review, panel}) ->
     unless review?.length


Removed console.log statement.

Removed console.log statement.

--- src/middleware/messageStore.coffee
+++ src/middleware/messageStore.coffee
@@ -8,8 +8,6 @@

 exports.storeTransaction = (ctx, done) ->
 	logger.info 'Storing request metadata for inbound transaction'
-
-	console.log ctx.querystring

 	tx = new transactions.Transaction
 		status: transactionStatus.PROCESSING


Synced with upstream

Synced with upstream

--- tree.jquery.coffee
+++ tree.jquery.coffee
@@ -1262,7 +1262,7 @@
     selectNode: (node, must_toggle=false) ->
         canSelect = =>
             if not @tree_widget.options.onCanSelectNode
-                return true
+                return @tree_widget.options.selectable

             return @tree_widget.options.onCanSelectNode(node)


Fix typo

Fix typo

--- src/config.coffee
+++ src/config.coffee
@@ -14,7 +14,7 @@
   }
   grid_path: (world, args) ->
     size = world.get('size')
-    scale = world.get('scale ')
+    scale = world.get('scale')
     path = ""
     for n in [scale..size-1] by scale
       path  += "M#{n},1 V#{size - 1} M1,#{n} H#{size - 1} "


fixed test names

fixed test names

--- Src/coffeescript/cql-execution/test/elm/comparison/test.coffee
+++ Src/coffeescript/cql-execution/test/elm/comparison/test.coffee
@@ -93,13 +93,13 @@
   it 'should be false for 5 m < 5 km', ->
     @aLtB_Quantity_diff.exec(@ctx).should.be.true()

-  it 'should be false for 5 Cel < 4 m', ->
-    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel < 5 m', ->
-    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel < 40 m', ->
+  it 'should be null for 5 Cel < 4 m', ->
+    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel < 5 m', ->
+    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel < 40 m', ->
     should.not.exist(@aLtB_Quantity_incompatible.exec(@ctx))

 describe 'LessOrEqual', ->
@@ -133,13 +133,13 @@
   it 'should be false for 5 m <= 5 km', ->
     @aLtB_Quantity_diff.exec(@ctx).should.be.true()

-  it 'should be false for 5 Cel <= 4 m', ->
-    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel <= 5 m', ->
-    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel <= 40 m', ->
+  it 'should be null for 5 Cel <= 4 m', ->
+    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel <= 5 m', ->
+    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel <= 40 m', ->
     should.not.exist(@aLtB_Quantity_incompatible.exec(@ctx))

@@ -174,13 +174,13 @@
   it 'should be false for 5 m > 5 km', ->
     @aLtB_Quantity_diff.exec(@ctx).should.be.false()

-  it 'should be false for 5 Cel > 4 m', ->
-    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel > 5 m', ->
-    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel > 40 m', ->
+  it 'should be null for 5 Cel > 4 m', ->
+    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel > 5 m', ->
+    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel > 40 m', ->
     should.not.exist(@aLtB_Quantity_incompatible.exec(@ctx))

@@ -215,16 +215,16 @@
   it 'should be false for 5 m  >=5 km', ->
     @aLtB_Quantity_diff.exec(@ctx).should.be.false()

-  it 'should be false for 5 Cel >= 4 m', ->
-    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel >= 5 m', ->
-    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 5 Cel >= 40 m', ->
-    should.not.exist(@aLtB_Quantity_incompatible.exec(@ctx))
-
-  it 'should be false for 100 [nmi_i] / 2 h > 49 mg/[lb_av]', ->
+  it 'should be null for 5 Cel >= 4 m', ->
+    should.not.exist(@aGtB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel >= 5 m', ->
+    should.not.exist(@aEqB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 5 Cel >= 40 m', ->
+    should.not.exist(@aLtB_Quantity_incompatible.exec(@ctx))
+
+  it 'should be null for 100 [nmi_i] / 2 h > 49 mg/[lb_av]', ->
     should.not.exist(@divideUcum_incompatible.exec(@ctx))

   it 'should be true for 100 mg / 2 [lb_av]  > 49 mg/[lb_av]', ->


Running bundle with tests bug fixed

Running bundle with tests bug fixed

--- cli/tasks/build/parse/index.coffee
+++ cli/tasks/build/parse/index.coffee
@@ -69,10 +69,8 @@
         file += 'window.app = module.exports;\n'
         file += 'Neft.unit = require(\'lib/unit\');\n'
         file += 'Neft.unit.runAutomatically = false;\n'
-        file += 'app.onReady(function() {\n'
         for testFile in testFiles
-            file += "    require('#{testFile}');\n"
-            file += '    Neft.unit.runTests();\n'
-        file += '});\n'
+            file += "require('#{testFile}');\n"
+            file += 'Neft.unit.runTests();\n'

     file


sitewide: markdown imageproxy uri fix

sitewide: markdown imageproxy uri fix

--- client/Framework/core/utils.coffee
+++ client/Framework/core/utils.coffee
@@ -111,7 +111,7 @@
     if url is ""
       "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
     else
-      KD.config.mainUri + '/-/imageProxy?url=' + encodeURIComponent(url)
+      '/-/imageProxy?url=' + encodeURIComponent(url)

   applyMarkdown: (text)->
     # problems with markdown so far:
@@ -119,14 +119,13 @@
     return null unless text

     marked.setOptions
-      gfm: true
-      pedantic: false
-      sanitize: true
-      highlight:(text,lang)->
+      gfm       : true
+      pedantic  : false
+      sanitize  : true
+      highlight :(text, lang)->
         if hljs.LANGUAGES[lang]?
-          hljs.highlight(lang,text).value
-        else
-          text
+        then hljs.highlight(lang,text).value
+        else text

     text = marked Encoder.htmlDecode text


Main: minor codestyle

Main: minor codestyle

--- client/Main/jcustomhtmlview.coffee
+++ client/Main/jcustomhtmlview.coffee
@@ -1,2 +1,3 @@
 class JCustomHTMLView extends KDCustomHTMLView
+
   JView.mixin @prototype


Fixed volume initialization value

Fixed volume initialization value

--- tp6/src/script/views/projector_pane.coffee
+++ tp6/src/script/views/projector_pane.coffee
@@ -45,16 +45,11 @@
     console.log("Setting up handlers")
     $('.power-button').click(@powerButtonClicked)
     $('.blank-button').click(@blankButtonClicked)
-    level = Tp.devices.volume.get('volume')
-    console.log('volume currently ' + level)
-    if level >= 0 and level <= 1
-      $('.volume-slider').slider("value", level * 100)
-    else
-      $('.volume-slider').slider()
-
-    $( ".volume-slider" ).on( "slide", @volumeSliderChanged);
     $('.mute-button').click(@muteButtonClicked)
     $('#auto-off .cancel-button').click(@autoOffClicked)
+    $('.volume-slider').slider()
+    $('.volume-slider').on( "slide", @volumeSliderChanged)
+    $('.volume-slider').slider( "option", "disabled", true ) #Disabled, then reenabled with initial volume in audioLevelChanged, because get volume here comes back undefined otherwise

   cancel: () ->
     console.log("This is a message for cancelling the autooff of projector.")
@@ -123,12 +118,9 @@

   audioLevelChanged: () ->
     level = Tp.devices.volume.get('volume')
-
-    # if level >= 0 and level <= 1
-    #   console.log('volume currently ' + level)
-      #$('.volume-slider').slider("value", level)
-    # if level >= 1 then $('.volume-plus.button').disable
-    # if level <= 0 then $('.volume-minus.button').disable
+    if $('.volume-slider').slider( "option", "disabled" ) #Part of the initialization of volume slider
+      $('.volume-slider').slider( "option", "disabled", false )
+      if level >= 0 and level <= 1 then $('.volume-slider').slider("value", level * 100)

   powerButtonClicked: () ->
     state = Tp.devices.projector.get 'state'


add unCheck during  loading

add unCheck during  loading

--- src/module/zk/zk_client.coffee
+++ src/module/zk/zk_client.coffee
@@ -14,7 +14,7 @@
 		_config = ProjectConfig.getZookeeperConf()
 		_clusterList = _config.clusterList
 		_retries = _config.retries || 3
-		_sessionTimeout = _config.sessionTimeout || 10000
+		_sessionTimeout = _config.sessionTimeout || 2000
 		_connectTimeout = _config.connectTimeout || 2000

 		@_client = ZK.createClient(_clusterList, {
@@ -38,21 +38,21 @@

 	_initEventListener : ->
 		@_client.on("disconnected", ()->
-			console.log("ZKClient receive event:disconnected")
+			Log.info("ZKClient receive event:disconnected")
 			@isConnected = false
 		)
 		@_client.on("connected", ()->
-			console.log("ZKClient receive event:connected")
+			Log.info("ZKClient receive event:connected")
 			@isConnected = true
 		)
 		@_client.on("connectedReadOnly", ()->
-			console.log("ZKClient receive event:connectedReadOnly")
+			Log.info("ZKClient receive event:connectedReadOnly")
 		)
 		@_client.on("expired", ()->
-			console.log("ZKClient receive event:expired")
+			Log.info("ZKClient receive event:expired")
 		)
 		@_client.on("authenticationFailed", ()->
-			console.log("ZKClient receive event:authenticationFailed")
+			Log.info("ZKClient receive event:authenticationFailed")
 		)

 	exists : (path, val, cb)->
@@ -157,6 +157,8 @@
 						if err.getCode() == ZK.Exception.NO_NODE
 							Log.error("NO_NODE found for path: #{path}")
 							return
+						else if err.getCode() == ZK.Exception.CONNECTION_LOSS
+							@_client.connect()
 						if cb
 							return cb(err, null, null)
 						else
@@ -183,6 +185,8 @@
 						if err.getCode() == ZK.Exception.NO_NODE
 							Log.error("NO_NODE found for path: #{path}")
 							return
+						else if err.getCode() == ZK.Exception.CONNECTION_LOSS
+							@_client.connect()
 						if cb
 							return cb(err, null, null)
 						else


Fix expected jasmine results

Fix expected jasmine results

--- js/tests/inner-tests.coffee
+++ js/tests/inner-tests.coffee
@@ -86,6 +86,6 @@

 j.describe "githubAuthURL", ->
   j.it "should be the expect values", ->
-    @expect(CI.github.authUrl()).toEqual "https://github.com/login/oauth/authorize?client_id=383faf01b98ac355f183&redirect_uri=http%3A%2F%2Fcirclehost%3A8080%2Fauth%2Fgithub%3Freturn-to%3D%252Ftests%252Finner&scope=user%2Crepo"
+    @expect(CI.github.authUrl()).toEqual "https://github.com/login/oauth/authorize?client_id=586bf699b48f69a09d8c&redirect_uri=http%3A%2F%2Fcirclehost%3A8081%2Fauth%2Fgithub%3Freturn-to%3D%252Ftests%252Finner&scope=user%2Crepo"

-    @expect(CI.github.authUrl([])).toEqual "https://github.com/login/oauth/authorize?client_id=383faf01b98ac355f183&redirect_uri=http%3A%2F%2Fcirclehost%3A8080%2Fauth%2Fgithub%3Freturn-to%3D%252Ftests%252Finner&scope="
+    @expect(CI.github.authUrl([])).toEqual "https://github.com/login/oauth/authorize?client_id=586bf699b48f69a09d8c&redirect_uri=http%3A%2F%2Fcirclehost%3A8081%2Fauth%2Fgithub%3Freturn-to%3D%252Ftests%252Finner&scope="


* minor changed

* minor changed

--- benchmark.coffee
+++ benchmark.coffee
@@ -1,4 +1,4 @@
-Cache = require './src/cache'
+Cache = require './lib/cache'

 maxCap = 10000
 lruCache = Cache(maxCap)


Using correct variable to display the packages path on "Install"

--- lib/install-panel.coffee
+++ lib/install-panel.coffee
@@ -24,7 +24,7 @@
             @span class: 'icon icon-question'
             @span outlet: 'publishedToText', 'Packages are published to '
             @a class: 'link', outlet: "openAtomIo", "atom.io"
-            @span " and are installed to #{path.join(fs.getHomeDirectory(), '.atom', 'packages')}"
+            @span " and are installed to #{path.join(process.env.ATOM_HOME, 'packages')}"

           @div class: 'search-container clearfix', =>
             @div class: 'editor-container', =>


util: quick idea how to document the message protocol via data

--- src/smc-util/message.coffee
+++ src/smc-util/message.coffee
@@ -31,6 +31,7 @@
 misc     = require('./misc')
 defaults = misc.defaults
 required = defaults.required
+_        = require('underscore')

 message = (obj) ->
@@ -40,8 +41,36 @@
         defaults(opts, obj)
     return obj

+# message2 for "version 2" of the message definitions
+# TODO document it, for now just search for "message2" to see examples
+message2 = (obj) ->
+
+    mk_descr = (val) ->
+        descr = val.descr
+        descr += ' (required)' if val.init == required
+        return descr
+
+    init   = _.mapObject(obj.fields, ((val) -> val.init))
+    fdescr = _.mapObject(obj.fields, mk_descr)
+    exports.documentation[obj.event] =
+                                descr : obj.descr ? ''
+                                fields: fdescr
+    exports.tests[obj.event] = obj.tests
+
+    exports[obj.event] = (opts={}) ->
+        if opts.event?
+            throw Error("ValueError: must not define 'event' when calling message creation function (opts=#{JSON.stringify(opts)}, obj=#{JSON.stringify(obj)})")
+        defaults(opts, init)
+    return obj
+
 # messages that can be used by the HTTP api.   {'event':true, ...}
 exports.api_messages = {}
+
+# this holds the documentation for the message protocol
+exports.documentation = {}
+
+# holds all the tests
+exports.tests = {}

 API = (obj) ->
     exports.api_messages[obj.event] = true
@@ -1034,13 +1063,29 @@
 Queries directly to the database (sort of like Facebook's GraphQL)
 ###

-API message
+API message2
     event          : 'query'
-    id             : undefined
-    query          : required
-    changes        : undefined
-    multi_response : false
-    options        : undefined
+    descr          : "This queries directly the database (sort of Facebook's GraphQL)"
+    fields:
+        id:
+             init  : undefined
+             descr : 'A unique UUID for the query'
+        query:
+             init  : required
+             descr : 'The actual query'
+        changes:
+             init  : undefined
+             descr : ''
+        multi_response:
+             init  : false
+             descr : ''
+        options:
+             init  : undefined
+             descr : ''
+    tests: [
+        [{query: 'test-query'}, "expected response"],
+        [{query: '2nd-query'}, "expected 2nd response"]
+    ]

 message
     event : 'query_cancel'


It's [ts] nick: line

It's [ts] nick: line

--- src/parsers/slackapp.coffee
+++ src/parsers/slackapp.coffee
@@ -48,5 +48,5 @@

     line = line.replace(/\s*\(edited\)$/, '')

-    result.push "#{nick} [#{ts}] #{line}"
+    result.push "[#{ts.format('h:mm A D MMM YYYY')}] #{nick}: #{line}"
   result


allow overriding context internals

allow overriding context internals

--- app/context.coffee
+++ app/context.coffee
@@ -327,7 +327,7 @@
   changes = new Bacon.Bus
   changes.plug result.component_list.model

-  _.extend result,
+  _.defaults result,
     current_options: {}
     changes: changes
     asyncs: asyncs


Don't watch files without compilers. Closes #292.

--- src/fs_utils/file_list.coffee
+++ src/fs_utils/file_list.coffee
@@ -84,7 +84,7 @@
     file

   _change: (path, compiler, isHelper) =>
-    return @_compileDependentFiles path if @_ignored path
+    return @_compileDependentFiles path if (@_ignored path) or not compiler
     if @_isAsset path
       @_copy (@_getAssetByPath(path) ? @_addAsset path)
     else


Fix variable names matching as numeric units

Stops "3section" being coloured like a contraction of "3s ection".


Colour definition terminators as control keywords

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -487,11 +487,11 @@
 				3: {name: "storage.type.function.roff"}
 				4: {name: "entity.name.function.roff"}
 				5: {name: "variable.parameter.roff"}
-				6: {name: "entity.terminator.roff", patterns: [{include: "#string"}]}
+				6: {name: "keyword.control.terminator.roff", patterns: [include: "#string"]}
 				7: {patterns: [include: "#param-group"]}
 			endCaptures:
 				1: {name: "punctuation.definition.request.roff"}
-				2: {name: "entity.terminator.roff"}
+				2: {name: "keyword.control.terminator.roff"}
 				3: {patterns: [{include: "#string"}]}
 			patterns: [{include: "$self"}]
 		}]


update post

update post

--- app.coffee
+++ app.coffee
@@ -8,8 +8,8 @@
   resp.end 'Welcome to Cloudq'

 app.post '/:queue', (req, resp) ->
-  console.log req.body
-  cloudq.queue req.params.queue, JSON.parse(req.body).job, (status) ->
+  console.log req.body.job
+  cloudq.queue req.params.queue, req.body.job, (status) ->
     resp.end JSON.stringify({ status: status })

 app.get '/:queue', (req, resp) ->


forget to add export

forget to add export

--- app.coffee
+++ app.coffee
@@ -13,3 +13,5 @@
 		res.send 'invalid'

 app.use(express.static(__dirname + '/wwwfiles'))
+
+exports.app = app


redirect for download

redirect for download

--- app.coffee
+++ app.coffee
@@ -99,7 +99,7 @@

     latest = versions[0]
     data = formatVersion latest, req.query.platform
-    res.json data
+    res.redirect data.url

 # catch 404 and forward to error handler
 app.use (req, res, next) ->


ammended format

ammended format

--- app.coffee
+++ app.coffee
@@ -7,11 +7,11 @@
 dbAuth = () -> client.auth('e212a2dcfe7eed6c849ddfce6c6c0e15')
 client.addListener('connected', dbAuth)
 client.addListener('reconnected', dbAuth)
-dbAuth()
-
 client.on("error", (err) ->
     console.log("Error " + err)
 )
+
+dbAuth()

 meryl.p(connect.static('public'))


Send HTTP 503 on failed scan instead of using the old packageList

--- app.coffee
+++ app.coffee
@@ -83,7 +83,7 @@
 app.set 'view engine', 'handlebars'
 # app.enable 'view cache'

-packageList = { }
+packageList = null
 updating = no
 updateTimeout = null
 lastUpdate = new Date
@@ -419,18 +419,20 @@
 							else
 								fileCallback err, data
 		, (err, data) ->
-			if err?
-				error "Error reading package list: #{err}"
-				callback? false
-				return
-
-			data = data.filter (item) -> item?
-
-			# overwrite packageList once everything succeeded
-			packageList = data
 			# update scan time and statistics
 			lastUpdate = new Date
 			updateTime = process.hrtime updateStart
+
+			if err?
+				error "Error reading package list: #{err}"
+				packageList = null
+				callback? false
+				return
+
+			data = data.filter (item) -> item?
+
+			# overwrite packageList once everything succeeded
+			packageList = data
 			debug "Finished update"

 			# and finally call the callback
@@ -451,6 +453,10 @@
 				res.redirect 301, "#{host}/#{req.query.packageName}/#{req.query.packageVersion.replace /[ ]/g, '_'}"
 			else
 				res.redirect 301, "#{host}/#{req.query.packageName}"
+			return
+
+		unless packageList?
+			res.sendStatus 503
 			return

 		unless req.accepts 'xml'


Minify & gzip socket.io files plus set etag headers

--- app.coffee
+++ app.coffee
@@ -14,6 +14,11 @@

 socketIoPort = 3005
 socketIoApp = io.listen socketIoPort
+socketIoApp.configure ->
+  socketIoApp.set 'browser client minification', true
+  socketIoApp.set 'browser client gzip', true
+  socketIoApp.set 'browser client etag', true
+
 socketIoRoutes = routes socketIoApp
 testClientRoute = testRoutes socketIoPort


Fix error message when filename is mismatched

--- app.coffee
+++ app.coffee
@@ -370,7 +370,7 @@
 										versionNumber = versionPackageXml.package.packageinformation[0].version[0]
 										# the tar file is incorrectly named -> abort
 										if (versionNumber.toLowerCase().replace (new RegExp ' ', 'g'), '_') isnt path.basename versionFile, '.tar'
-											fileCallback "version number does not match file (#{versionNumber.replace new RegExp(' ', 'g'), '_'} != #{path.basename versionFile, '.tar'})"
+											fileCallback "version number does not match file (#{versionNumber.toLowerCase().replace new RegExp(' ', 'g'), '_'} != #{path.basename versionFile, '.tar'})"
 											return

 										currentVersion = { }


Set default value for config.handlers

Set default value for config.handlers

--- app.coffee
+++ app.coffee
@@ -41,6 +41,7 @@

 config.host = config.host.replace /\/+$/, ''

+config.handlers ?= [ ]
 config.port ?= 9001
 config.ip ?= '0.0.0.0'
 config.enableFrontend ?= yes


Test if player exists in control-loop

Test if player exists in control-loop

--- src/public/scripts/app.game.coffee
+++ src/public/scripts/app.game.coffee
@@ -188,7 +188,7 @@
 		update: ( timestamp ) =>
 			dt = (timestamp - @lastUpdateTime) / 1000

-			if @allowInput
+			if @player and @allowInput
 				# If any keys are pressed, apply angular forces to the player
 				@player?.boost = @inputHandler.getBoost()


Remove commented out line and ;

Remove commented out line and ;

--- lib/linter-php.coffee
+++ lib/linter-php.coffee
@@ -28,8 +28,7 @@
       @executablePath = atom.config.get 'linter-php.phpExecutablePath'

   destroy: ->
-    # atom.config.unobserve 'linter-php.phpExecutablePath'
-    @disposables.dispose();
+    @disposables.dispose()

   createMessage: (match) ->
     # message might be empty, we have to supply a value


Fix side chat css

Fix side chat css

--- src/smc-webapp/side_chat.cjsx
+++ src/smc-webapp/side_chat.cjsx
@@ -540,6 +540,7 @@
                     border: "1px solid #ccc"
                     borderRadius: "4px"
                     boxShadow: "inset 0 1px 1px rgba(0,0,0,.075)",
+                    overflow: "auto",
                     padding: "5px 10px"

             suggestions:
@@ -548,6 +549,13 @@
                     border: "1px solid #ccc"
                     borderRadius: "4px"
                     fontSize: @props.font_size
+                    position: "absolute"
+                    bottom: "10px"
+                    overflow: "auto"
+                    height: "140px"
+                    width: "max-content"
+                    display: "flex"
+                    flexDirection: "column"

                 item:
                     padding: "5px 15px"


Update player_internal iOS bug workaround

--- src/player_internals.coffee
+++ src/player_internals.coffee
@@ -53,7 +53,17 @@
       @workerInitialized = true
       @requestFrame()
     else
-      console.log("Not producing audio fast enough.") if @workerInitialized
+      if @workerInitialized
+        console.log("Not producing audio fast enough.")
+        # If on iOS kill the audio context. There's a bug where returning a single
+        # frame of 0's puts the system audio in an unusable / muted state. I
+        # haven't looked into it thoroughly. This is a temporary workaround to
+        # reduce the severity of the bug.
+        # http://stackoverflow.com/questions/9038625/detect-if-device-is-ios
+        if navigator && window
+          if /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
+            window.player.pause()
+
       buffer = new Float64Array(AUDIO_BUFFER_SIZE * 2)
       buffer.fill 0


making Travis (hopefully) happy

making Travis (hopefully) happy

--- test/phantom-jasmine/run_jasmine_test.coffee
+++ test/phantom-jasmine/run_jasmine_test.coffee
@@ -9,7 +9,7 @@
     @tries = 0
     @max_tries = 10

-  get_status: -> @page.evaluate(-> console_reporter.status)
+  get_status: -> @page.evaluate(-> consoleReporter.status)

   terminate: ->
     switch @get_status()
@@ -44,4 +44,4 @@
     console.log "can't load the address!"
     phantom.exit 1

-  # Now we wait until onConsoleMessage reads the termination signal from the log.+  # Now we wait until onConsoleMessage reads the termination signal from the log.


remove expect from helpers, it's not exported

--- shared/specs/model/notifications.spec.coffee
+++ shared/specs/model/notifications.spec.coffee
@@ -1,4 +1,4 @@
-{Testing, expect, sinon, _} = require 'shared/specs/helpers'
+{Testing, sinon, _} = require 'shared/specs/helpers'
 moment = require 'moment'
 URLs = require 'model/urls'
 Notifications = require 'model/notifications'


Add test for token parsing of null datetime type.

--- test/unit/token/row-token-parser-test.coffee
+++ test/unit/token/row-token-parser-test.coffee
@@ -114,3 +114,23 @@
   test.strictEqual(token.columns[0].value.getTime(), new Date('January 3, 1900 00:00:45').getTime())

   test.done()
+
+module.exports.datetimeN = (test) ->
+  colMetaData = [type: dataTypeByName.DateTimeN]
+
+  buffer = new WritableBuffer(0)
+
+  buffer.writeUInt8(TYPE.ROW)
+  buffer.writeUInt8(0)
+  buffer = buffer.data
+  #console.log(buffer)
+
+  token = parser(buffer, 1, colMetaData)
+  #console.log(token)
+
+  test.strictEqual(token.length, buffer.length - 1)
+  test.strictEqual(token.columns.length, 1)
+  test.ok(!token.columns[0].value)
+  test.ok(token.columns[0].isNull)
+
+  test.done()


Hotkeys for tabs

Hotkeys for tabs

--- src/gui/msie/body.html.coffee
+++ src/gui/msie/body.html.coffee
@@ -20,6 +20,7 @@
           type: 'radio'
           name: '.'
           value: n
+          accesskey: k.slice 0, 1
         text ' ', k.replace /./, (s)-> s.toUpperCase()
         span()
     return


R E T I N A

R E T I N A

--- app/js/main.coffee
+++ app/js/main.coffee
@@ -1,6 +1,8 @@
 window.asteroids = ->
   window.stats = Utils.drawStats()
-  window.ctx = Sketch.create element: document.getElementById('asteroids')
+  window.ctx = Sketch.create
+    element: document.getElementById('asteroids')
+    retina: true
   window.ship = new Ship(ctx.width/2, ctx.height/2)
   controls = new KeyboardControls


Update device.coffee

Update device.coffee

create device.desktop()

--- src/device.coffee
+++ src/device.coffee
@@ -79,6 +79,9 @@

 device.tablet = ->
   device.ipad() or device.androidTablet() or device.blackberryTablet() or device.windowsTablet() or device.fxosTablet()
+
+device.desktop = ->
+  not device.tablet() and not device.mobile()

 device.portrait = ->
   (window.innerHeight/window.innerWidth) > 1


Added error callback to spawn in spawnServer

--- src/processes/server.coffee
+++ src/processes/server.coffee
@@ -146,6 +146,13 @@
     spawnOpts.cwd ?= process.cwd()

     child = spawn cmd, args, spawnOpts
+
+    child.on 'error', (err) ->
+      if err.errno is 'ENOENT'
+        console.error "Unable to find #{cmd}, ensure you have the required dependencies installed"
+
+      throw err
+
     child.baseUrl = "http://127.0.0.1:#{port}"
     child.logPath = logPath
     child.logHandle = logHandle


render root

render root

--- src/test/test_render.coffee
+++ src/test/test_render.coffee
@@ -14,13 +14,13 @@
 }

 exports.test_render = (test, rx) ->
+  world = god(rx_mock(rx), {})

   render_dict = (dict) ->
-    world = god(rx_mock(rx), dict)
-    render(world)
+    contents = god(rx_mock(rx), dict)
+    render(contents)

   test "rx_mock exists", (t) ->
-    world = god(rx_mock(rx), {})
     t.ok world.T, 'Has HTML Tags'
     t.ok world.T().div, 'Has HTML div'
     t.ok world.SVG, 'Has SVG Tags'


No need to use secure in internal method

--- workers/social/lib/social/models/account/index.coffee
+++ workers/social/lib/social/models/account/index.coffee
@@ -1042,7 +1042,7 @@
   fetchMyFollowersFromGraph: secure (client, options, callback)->
     @fetchFollowFromGraph "fetchFollowerMembers", client, options, callback

-  fetchFollowFromGraph: secure (followType, client, options, callback)->
+  fetchFollowFromGraph: (followType, client, options, callback)->
     graph = new Graph({config:KONFIG['neo4j']})
     userId = client.connection.delegate._id
     options.currentUserId = userId


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -4,7 +4,7 @@

   onData: (data) ->
     $(@node).fadeOut().fadeIn()
-    @render(data)
+    @render(data.data)

   renderX: (data) ->
     if !data
@@ -192,7 +192,7 @@
         if !data
           return

-        piedata = data[0].data.label
+        piedata = data.label
         console.log("update pie", piedata)

         $(@node).children(".title").text($(@node).attr("data-title"))


newpie experiments

newpie experiments

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -193,7 +193,7 @@
             #bbox.width and bbox.height will
             #describe the size of the label text
             #labelRadius = radius - 20
-            labelRadius = radiuso * 1.2
+            labelRadius = radiuso * 1.5
             d.x = Math.sin(a) * labelRadius
             d.l = d.x - bbox.width / 2 - 2
             d.r = d.x + bbox.width / 2 + 2


sometimes heuristic is wrong

sometimes heuristic is wrong

--- src/editor.coffee
+++ src/editor.coffee
@@ -127,9 +127,13 @@
       [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
       decomposeLeft = newLeftDelta.decompose(oldLeftDelta)
       decomposeRight = newRightDelta.decompose(oldRightDelta)
-      decompose = decomposeLeft.merge(decomposeRight)
+      decomposeA = decomposeLeft.merge(decomposeRight)
   catch ignored
-  decompose = newDelta.decompose(oldDelta) unless decompose?
+  decomposeB = newDelta.decompose(oldDelta)
+  if decomposeA and decomposeB
+    decompose = if decomposeA.ops.length < decomposeB.ops.length then decomposeA else decomposeB
+  else
+    decompose = decomposeA or decomposeB
   compose = oldDelta.compose(decompose)
   console.assert(compose.isEqual(newDelta), oldDelta, newDelta, decompose, compose)
   if !decompose.isIdentity() and options.internal


Create a saving section

Create a saving section

--- src/editor.coffee
+++ src/editor.coffee
@@ -524,22 +524,6 @@
   # Essential: Returns the {String} path of this editor's text buffer.
   getPath: -> @buffer.getPath()

-  # Essential: Saves the editor's text buffer.
-  #
-  # See {TextBuffer::save} for more details.
-  save: -> @buffer.save()
-
-  # Essential: Saves the editor's text buffer as the given path.
-  #
-  # See {TextBuffer::saveAs} for more details.
-  #
-  # * `filePath` A {String} path.
-  saveAs: (filePath) -> @buffer.saveAs(filePath)
-
-  # Extended: Determine whether the user should be prompted to save before closing
-  # this editor.
-  shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors()
-
   # Essential: Returns {Boolean} `true` if this editor has been modified.
   isModified: -> @buffer.isModified()

@@ -550,6 +534,26 @@
   copyPathToClipboard: ->
     if filePath = @getPath()
       atom.clipboard.write(filePath)
+
+  ###
+  Section: Saving
+  ###
+
+  # Essential: Saves the editor's text buffer.
+  #
+  # See {TextBuffer::save} for more details.
+  save: -> @buffer.save()
+
+  # Extended: Saves the editor's text buffer as the given path.
+  #
+  # See {TextBuffer::saveAs} for more details.
+  #
+  # * `filePath` A {String} path.
+  saveAs: (filePath) -> @buffer.saveAs(filePath)
+
+  # Extended: Determine whether the user should be prompted to save before closing
+  # this editor.
+  shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors()

   ###
   Section: Reading Text


Override isHidden for the Editor

Override isHidden for the Editor

We know it's visible if it is on the dom and doesn't have
display == none set. This is an order of magnitude faster than the
default implementation which calls getComputedStyle()

--- src/editor.coffee
+++ src/editor.coffee
@@ -1139,6 +1139,14 @@
       @layerMinWidth = minWidth
       @trigger 'editor:min-width-changed'

+  # Override for speed. The base function checks computedStyle, unnecessary here.
+  isHidden: ->
+    style = this[0].style
+    if style.display == 'none' or not @isOnDom()
+      true
+    else
+      false
+
   clearRenderedLines: ->
     @renderedLines.empty()
     @firstRenderedScreenRow = null
@@ -1440,11 +1448,11 @@
     new Array(div.children...)

   htmlForScreenRows: (startRow, endRow) ->
-    htmlLines = []
+    htmlLines = ''
     screenRow = startRow
     for line in @activeEditSession.linesForScreenRows(startRow, endRow)
-      htmlLines.push(@htmlForScreenLine(line, screenRow++))
-    htmlLines.join('\n\n')
+      htmlLines += @htmlForScreenLine(line, screenRow++)
+    htmlLines

   htmlForScreenLine: (screenLine, screenRow) ->
     { tokens, text, lineEnding, fold, isSoftWrapped } =  screenLine
@@ -1629,30 +1637,9 @@
     scopeStack = []
     line = []

-    updateScopeStack = (desiredScopes) ->
-      excessScopes = scopeStack.length - desiredScopes.length
-      _.times(excessScopes, popScope) if excessScopes > 0
-
-      # pop until common prefix
-      for i in [scopeStack.length..0]
-        break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
-        popScope()
-
-      # push on top of common prefix until scopeStack == desiredScopes
-      for j in [i...desiredScopes.length]
-        pushScope(desiredScopes[j])
-
-    pushScope = (scope) ->
-      scopeStack.push(scope)
-      line.push("")
-
-    popScope = ->
-      scopeStack.pop()
-      line.push("")
-
-    attributePairs = []
-    attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes
-    line.push("
")
+    attributePairs = ''
+    attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes
+    line.push("
")

     if text == ''
       html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini)
@@ -1663,19 +1650,43 @@
       lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
       position = 0
       for token in tokens
-        updateScopeStack(token.scopes)
+        @updateScopeStack(line, scopeStack, token.scopes)
         hasLeadingWhitespace =  position < firstNonWhitespacePosition
         hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition
         hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly)
         line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide}))
         position += token.value.length

-    popScope() while scopeStack.length > 0
+    @popScope(line, scopeStack) while scopeStack.length > 0
     line.push(htmlEolInvisibles) unless text == ''
     line.push("") if fold

     line.push('
')
     line.join('')
+
+  @updateScopeStack: (line, scopeStack, desiredScopes) ->
+    excessScopes = scopeStack.length - desiredScopes.length
+    if excessScopes > 0
+      @popScope(line, scopeStack) while excessScopes--
+
+    # pop until common prefix
+    for i in [scopeStack.length..0]
+      break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
+      @popScope(line, scopeStack)
+
+    # push on top of common prefix until scopeStack == desiredScopes
+    for j in [i...desiredScopes.length]
+      @pushScope(line, scopeStack, desiredScopes[j])
+
+    null
+
+  @pushScope: (line, scopeStack, scope) ->
+    scopeStack.push(scope)
+    line.push("")
+
+  @popScope: (line, scopeStack)->
+    scopeStack.pop()
+    line.push("")

   @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) ->
     if not mini and showIndentGuide


Add Editor::onDidStopChanging which delegates to TextBuffer

--- src/editor.coffee
+++ src/editor.coffee
@@ -305,6 +305,9 @@

   onDidChangeGrammar: (callback)->
     @displayBuffer.onDidChangeGrammar(callback)
+
+  onDidStopChanging: (callback) ->
+    @getBuffer().onDidStopChanging(callback)

   # Retrieves the current {TextBuffer}.
   getBuffer: -> @buffer


Reorg undo section

Reorg undo section

--- src/editor.coffee
+++ src/editor.coffee
@@ -1221,12 +1221,12 @@
   Section: Undo Operations
   ###

-  # Public: Undo the last change.
+  # Essential: Undo the last change.
   undo: ->
     @getLastCursor().needsAutoscroll = true
     @buffer.undo(this)

-  # Public: Redo the last change.
+  # Essential: Redo the last change.
   redo: ->
     @getLastCursor().needsAutoscroll = true
     @buffer.redo(this)


Remove Editor::isEqual

Remove Editor::isEqual

An editor is such a stateful concept that comparing different instances
for "equality" doesn't make much sense. This was interfering with the
ability to distinguish successive editors assigned as active pane items.

--- src/editor.coffee
+++ src/editor.coffee
@@ -267,22 +267,6 @@
     else
       'untitled'

-  # Compares two `Editor`s to determine equality.
-  #
-  # Equality is based on whether:
-  # * the two {TextBuffer}s are the same
-  # * the two `scrollTop` and `scrollLeft` property are the same
-  # * the most recent {Cursor}s are in the same position.
-  #
-  # Returns a {Boolean}.
-  isEqual: (other) ->
-    return false unless other instanceof Editor
-    @isAlive() == other.isAlive() and
-      @buffer.getPath() == other.buffer.getPath() and
-      @getScrollTop() == other.getScrollTop() and
-      @getScrollLeft() == other.getScrollLeft() and
-      @getCursorScreenPosition().isEqual(other.getCursorScreenPosition())
-
   # Controls visiblity based on the given {Boolean}.
   setVisible: (visible) -> @displayBuffer.setVisible(visible)


Add support for init.js; Fixes #331 (#339)

* Add support for init.js; Fixes #331

* Add init.js case to restore function

* move require into a separate line

--- lib/sync-settings.coffee
+++ lib/sync-settings.coffee
@@ -148,7 +148,9 @@
     if atom.config.get('sync-settings.syncStyles')
       files["styles.less"] = content: (@fileContent atom.styles.getUserStyleSheetPath()) ? "// styles file (not found)"
     if atom.config.get('sync-settings.syncInit')
-      files["init.coffee"] = content: (@fileContent atom.config.configDirPath + "/init.coffee") ? "# initialization file (not found)"
+      initPath = atom.getUserInitScriptPath()
+      path = require('path')
+      files[path.basename(initPath)] = content: (@fileContent initPath) ? "# initialization file (not found)"
     if atom.config.get('sync-settings.syncSnippets')
       files["snippets.cson"] = content: (@fileContent atom.config.configDirPath + "/snippets.cson") ? "# snippets file (not found)"

@@ -241,6 +243,9 @@

           when 'init.coffee'
             fs.writeFileSync atom.config.configDirPath + "/init.coffee", file.content if atom.config.get('sync-settings.syncInit')
+
+          when 'init.js'
+            fs.writeFileSync atom.config.configDirPath + "/init.js", file.content if atom.config.get('sync-settings.syncInit')

           when 'snippets.cson'
             fs.writeFileSync atom.config.configDirPath + "/snippets.cson", file.content if atom.config.get('sync-settings.syncSnippets')


Fix YARD

Fix YARD

--- src/fields.coffee
+++ src/fields.coffee
@@ -44,7 +44,7 @@
     !@changedFields().empty()

   # Returns all of the fields that have been changed since the last server response
-  # @return [Array<String>] the changed fields for the resource
+  # @return [Collection<String>] the changed fields for the resource
   @changedFields: () ->
     @klass().fields().select((field) =>
       oldField = @__fields[field]


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -1,6 +1,8 @@
 ###*
 The Follow module provides a simple method to set an object's
-velocity so that it will approach another object. This method
+velocity so that it will approach another object.
+
+This method
 relies on both objects having `position` methods. GameObject
 provides this method by default.


Add check if store is already synced with the channel in SyncToChannel

--- app/assets/javascripts/action_cable_notifications/store.coffee
+++ app/assets/javascripts/action_cable_notifications/store.coffee
@@ -37,15 +37,20 @@

     if !channelId
       console.warn "[syncToChannel]: Channel specified doesn't have an identifier"
+      false
     else
-      @channels[channelId] =
-        channel: channel
-        callbacks: {
-          received: channel.received
-        }
+      if @channels[channelId]
+        console.warn "[syncToChannel]: The store '#{@name}'' is already in sync with channel '#{channelId}'"
+        false
+      else
+        @channels[channelId] =
+          channel: channel
+          callbacks: {
+            received: channel.received
+          }

-      channel.received = packetReceived(@channels[channelId], collection).bind(this)
-    channel
+        channel.received = packetReceived(@channels[channelId], collection).bind(this)
+        true

   # Dispatch received packet to registered stores
   # collection overrides the collection name specified in the incoming packet


More sane fractions (e.g., 1 1/2 instead of 3/2)

--- src/scripts/ingredient.cjsx
+++ src/scripts/ingredient.cjsx
@@ -24,12 +24,17 @@

   float2rat: (x) ->
     # tolerance = 1.0e-6
+    if x > 1
+      w = parseInt x
+      f = x - w
+    else
+      f = x
     tolerance = 1.0e-1
     h1=1
     h2=0
     k1=0
     k2=1
-    b = x
+    b = f
     # TODO separate our whole numbers for fractions like 4/3 to 1 1/3
     while true
       a = Math.floor(b)
@@ -40,11 +45,17 @@
       k1 = a*k1+k2
       k2 = aux
       b = 1/(b-a)
-      break unless (Math.abs(x-h1/k1) > x*tolerance)
+      break unless (Math.abs(f-h1/k1) > f*tolerance)
     if k1 is 1
-      "#{h1}"
+      if w?
+        "#{w + h1}"
+      else
+        "#{h1}"
     else
-      h1+"/"+k1
+      if w?
+        "#{w} #{h1}/#{k1}"
+      else
+        "#{h1}/#{k1}"

   translateIngredients: ->
     string = @replaceUnicode()


webapp/pricing: some initial steps to add description text and FAQ

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -850,6 +850,7 @@
         else if ncols >= 7
             ncols = 12

+
TEST

             {@render_rows(live_subscriptions, ncols)}

@@ -862,7 +863,7 @@

Shared Resources

             <Row>
-                <Col sm=6>
+                <Col md=6 sm=12>

                     You may create many completely separate SageMathCloud projects.
@@ -875,7 +876,8 @@

-
When you purchase a subscription, you can upgrade the quotas on any projects
+

+                    When you purchase a subscription, you can upgrade the quotas on any projects
                     you use up to the amounts given by your subscription.  Multiple people can contribute
                     to increase the quotas on the same project, and may also remove their contributions
                     at any time.  You may also subscribe
@@ -884,18 +886,19 @@

+

                     Immediately email us at <HelpEmailLink/> if anything is unclear to you.

                 </Col>
-                <Col sm=6>
+                <Col md=6 sm=12>
                     <Row>
-                        <Col xs=6>
+                        <Col sm=6 xs=6>
                             <ProjectQuotaFreeTable/>
                         </Col>
-                        <Col xs=6>
+                        <Col sm=6 xs=6>
                             <ProjectQuotaBoundsTable/>
                         </Col>
                     </Row>
@@ -923,6 +926,28 @@
             else
                 throw Error("unknown type #{@props.type}")

+FAQS =
+    course120:
+        q: "I have a course of 120 students. Which plan should I purchase?",
+        a: "You can combine courses! Take two times the 'medium course plan' and you will end up with 140 upgrades, covering all your students."
+    test:
+        q: "q2"
+        a: "a2"
+
+FAQ = rclass
+    displayName : 'FAQ'
+
+    faq: ->
+        for qid, qa of FAQS
+
{qa.q} {qa.a}

+
+    render: ->
+

+
Frequently asked questions
,
+

+                {@faq()}
+

+

 Subscription = rclass
     displayName : 'Subscription'
@@ -1465,17 +1490,10 @@
 exports.render_static_pricing_page = () ->

         <ExplainResources type='shared'/>
-
-

         <SubscriptionGrid period='month'  is_static={true}/>
-
-

         <SubscriptionGrid period='year'  is_static={true}/>
-
-

-
         <SubscriptionGrid period='month4'  is_static={true}/>
-
+        <FAQ is_static={true}/>

 exports.visit_billing_page = ->


Mimic other coupon-promo adders

Mimic other coupon-promo adders

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -968,7 +968,11 @@
                     {<ConfirmPaymentMethod
                         is_recurring = {@is_recurring()}
                     /> if @props.selected_plan isnt ''}
-                    {<CouponAdder applied_coupons={@props.applied_coupons} coupon_error={@props.coupon_error} />}
+                    <Row>
+                        <Col sm={5} smOffset={7}>
+                            {<CouponAdder applied_coupons={@props.applied_coupons} coupon_error={@props.coupon_error} />}
+                        </Col>
+                    </Row>
                     {@render_create_subscription_buttons()}
                 </Well>
                 <ExplainResources type='shared'/>
@@ -1044,6 +1048,11 @@
             placeholder_text = 'Enter another code?'
         else
             placeholder_text = 'Enter your code here...'
+
+        if @state.coupon_id == ''
+            bsStyle = undefined
+        else
+            bsStyle = 'primary'

         <Well>

<Icon name='plus' /> Add a coupon?

@@ -1058,16 +1067,16 @@
                         placeholder = {placeholder_text}
                         onChange    = {(e) => @setState(coupon_id : e.target.value)}
                         onKeyDown   = {@key_down}
+                        onBlur      = {@submit}
                     />
                     <InputGroup.Button>
-                        <Button onClick={@submit} disabled={@state.coupon_id == ''}>
-                            <Icon name='check' />
+                        <Button onClick={@submit} disabled={@state.coupon_id == ''} bsStyle={bsStyle} >
+                            Apply
                         </Button>
                     </InputGroup.Button>
                 </InputGroup>
             </FormGroup> if @props.applied_coupons?.size == 0}
             {<SkinnyError error_text={@props.coupon_error} on_close={@actions('billing').clear_coupon_error} /> if @props.coupon_error}
-            {
No coupons applied
 if @props.applied_coupons?.size == 0}
         </Well>

 CouponList = rclass


Remove a console.log

Remove a console.log

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -813,7 +813,6 @@
         </div>

     render_renewal_info: ->
-        console.log("render_renewal_info", @props.selected_plan)
         if @props.selected_plan
             renews = not PROJECT_UPGRADES.membership[@props.selected_plan.split('-')[0]].cancel_at_period_end
             length = PROJECT_UPGRADES.period_names[@state.selected_button]


Fixed up rendering

Fixed up rendering

--- src/io/tty.coffee
+++ src/io/tty.coffee
@@ -127,8 +127,15 @@

 			program.write row.join ''

-		@renderEntities x, y, (map.listEntities 'item')
-		@renderEntities x, y, (map.listEntities 'creature')
+		entityLayer =
+			'creature': 3
+			'item': 2
+			'stairs': 1
+
+		entities = map.entities[..].sort (a, b) ->
+			entityLayer[a.type] - entityLayer[b.type]
+
+		@renderEntities x, y, entities

 	renderEntities: (x, y, entities) ->
 		c = @game.camera


Fix bug that removed text when inserting image

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -210,15 +210,20 @@

       if delta.insert?
         insertHelper thisObj, position, delta.insert
+        length =
+          if typeof delta.insert == "string"
+            delta.insert.length
+          else if typeof delta.insert == "number"
+            1
         from = thisObj._model.getContent("characters").ref position
         to = thisObj._model.getContent("characters").ref(
-          position+delta.insert.length)
+          position+length)
         thisObj._model.getContent("selections").select(
           from, to, delta_selections)
         thisObj._model.getContent("selections").unselect(
           from, to, delta_unselections)

-        return position + delta.insert.length
+        return position + length

       else if delta.delete?
         deleteHelper thisObj, position, delta.delete


fix merge issues

fix merge issues

--- node_modules_koding/domainer/lib/reseller/DNSManager.coffee
+++ node_modules_koding/domainer/lib/reseller/DNSManager.coffee
@@ -10,13 +10,9 @@

     options =
       method  : 'POST'
-<<<<<<< HEAD
       uri     : "http://kontrol.in.koding.com/domains/#{newRecord.domainName}"
       body    : JSON.stringify postData
-=======
-      uri     : "http://kontrol.in.koding.com/domains/#{newRecord.domainAddress}"
-      body    : JSON.stringify postData
->>>>>>> d759af228d8303e76de50936b023808ec9be8df9
+
       headers : {'content-type': 'application/json'}

     request.post options, (error, response, body) =>
@@ -33,16 +29,11 @@
       username : record.username

     options =
-<<<<<<< HEAD
+
       method : 'DELETE'
       uri     : "http://kontrol.in.koding.com/domains/#{record.domainName}"
       body    : JSON.stringify postData
       headers : {'content-type': 'application/json'}
-=======
-      method  : 'DELETE'
-      uri     : "http://kontrol.in.koding.com/domains/#{record.domainAddress}"
-      headers : {'content-type': 'application/json'}
->>>>>>> d759af228d8303e76de50936b023808ec9be8df9

     request.del options, (error, response, body) =>
       console.log body


correct path, with updated module.push

correct path, with updated module.push

--- src/test/spec/rcraft.coffee
+++ src/test/spec/rcraft.coffee
@@ -48,7 +48,7 @@

 ###

-Rcraft = require '../../lib/rcraft'
+Rcraft = require 'rcraft'

 describe "awesome", ->
   it "should be awesome.", ->


use related content link component

use related content link component

--- tutor/src/components/task-step/exercise.cjsx
+++ tutor/src/components/task-step/exercise.cjsx
@@ -7,7 +7,7 @@
 {StepPanel} = require '../../helpers/policies'

 BrowseTheBook = require '../buttons/browse-the-book'
-
+RelatedContentLink = require '../related-content-link'
 {ChapterSectionMixin, CardBody, ExerciseWithScroll, ExControlButtons} = require 'shared'

 StepFooter = require './step-footer'
@@ -86,30 +86,30 @@
     else
       @setState(@getPartsInfo(nextProps))

-  renderHelpLink: (sections) ->
-    return null unless sections? and not _.isEmpty(sections)
-    {courseId} = @props
-
-    sectionsLinks = _.chain sections
-      .map (section) =>
-        combined = @sectionFormat(section.chapter_section)
-        <BrowseTheBook
-          unstyled
-          key={combined}
-          section={combined}
-          courseId={courseId}
-          onlyShowBrowsable={false}>
-          {combined} {section.title}
-        </BrowseTheBook>
-      .compact()
-      .value()
-
-    helpLink =
-

-        Comes from&nbsp&nbsp{sectionsLinks}
-

-
-    if sectionsLinks.length > 0 then helpLink
+  # renderHelpLink: (sections) ->
+  #   return null unless sections? and not _.isEmpty(sections)
+  #   {courseId} = @props
+
+  #   sectionsLinks = _.chain sections
+  #     .map (section) =>
+  #       combined = @sectionFormat(section.chapter_section)
+  #       <BrowseTheBook
+  #         unstyled
+  #         key={combined}
+  #         section={combined}
+  #         courseId={courseId}
+  #         onlyShowBrowsable={false}>
+  #         {combined} {section.title}
+  #       </BrowseTheBook>
+  #     .compact()
+  #     .value()
+
+  #   helpLink =
+  #

+  #       Comes from&nbsp&nbsp{sectionsLinks}
+  #

+
+  #   if sectionsLinks.length > 0 then helpLink

   stopListeningForProgress: (props) ->
     props ?= @props
@@ -226,8 +226,9 @@

       task={task}
       parts={parts}
-      helpLink={@renderHelpLink(part.related_content)}
-
+      helpLink={
+        <RelatedContentLink courseId={courseId} content={part.related_content} />
+      }
       onStepCompleted={onStepCompleted}
       controlButtons={controlButtons}


Remove unused method

Remove unused method

--- src/tokenized-buffer.coffee
+++ src/tokenized-buffer.coffee
@@ -224,10 +224,6 @@
   tokenizedLineForRow: (bufferRow) ->
     @tokenizedLines[bufferRow]

-  # FIXME: benogle says: These are actually buffer rows as all buffer rows are
-  # accounted for in @tokenizedLines
-  linesForScreenRows: (startRow, endRow) ->
-    @tokenizedLines[startRow..endRow]

   stackForRow: (row) ->
     @tokenizedLines[row]?.ruleStack


incomplete: touch support

incomplete: touch support

--- scripts-src/application.coffee
+++ scripts-src/application.coffee
@@ -303,25 +303,16 @@

       attach_listeners: ->
           canvas = @canvas_container
-          canvas.addEventListener "mousedown", ((e) =>
-            t = @mouse_tool
-            t.on_mouse_down e if t
-            e.preventDefault()
-          ), false
-          canvas.addEventListener "mouseup", ((e) =>
-            t = @mouse_tool
-            t.on_mouse_up e  if t
-            e.preventDefault()
-          ), false
-          canvas.addEventListener "mousemove", ((e) =>
-              t = @mouse_tool
-              t.on_mouse_move e if t
-              e.preventDefault()
-          ), false
-          canvas.addEventListener "mouseout", ((e) =>
-            t = @mouse_tool
-            t.on_mouse_out e if t
-          )
+          self = this
+          canvas.addEventListener "mousedown", nodefault ((e) -> self.mouse_tool.on_mouse_down e ), false
+          canvas.addEventListener "mouseup", nodefault((e) -> self.mouse_tool.on_mouse_up e), false
+          canvas.addEventListener "mousemove", nodefault((e) -> self.mouse_tool.on_mouse_move e), false
+          canvas.addEventListener "mouseout", nodefault((e) -> self.mouse_tool.on_mouse_out e), false
+
+          canvas.addEventListener "touchstart", nodefault ((e) -> self.mouse_tool.on_touch_start e ), false
+          canvas.addEventListener "touchend", nodefault ((e) -> self.mouse_tool.on_touch_end e ), false
+          canvas.addEventListener "touchmove", nodefault ((e) -> self.mouse_tool.on_touch_move e ), false
+          canvas.addEventListener "touchleave", nodefault ((e) -> self.mouse_tool.on_touch_leave e ), false

       ###
       Load initial state from the URL parameters
@@ -591,7 +582,10 @@
     on_cell_change: ->
     on_click_cell: ->
     on_mouse_out: ->
-
+    on_touch_start: (e)->
+    on_touch_leave: (e)->
+    on_touch_end: (e)->
+    on_touch_move: (e)->
   ###
   # Mouse tool for erasing
   ###


fix gulp watch for coffeescript

fix gulp watch for coffeescript

--- tasks/dev.coffee
+++ tasks/dev.coffee
@@ -19,7 +19,7 @@
 gulp.task 'run-watch', (cb) ->
   fs.writeFileSync('./dev/dev_enabled', 'dev enabled', 'utf8')
   gulp.watch './stylesheets/*.styl', ['stylus']
-  gulp.watch './lib/*.coffee', ['coffee']
+  gulp.watch './lib/**/*.coffee', ['coffee']
   if process.platform == 'win32'
     gulp.watch './app/**', ['copy:app']


Remove ~ from username in message origin regex. Fixes #15

--- lib/IrcMessageParser.coffee
+++ lib/IrcMessageParser.coffee
@@ -187,7 +187,7 @@
 getMessageOrigin = (prefix) ->
   messageOrigin = {}
   if /:.*!.*@.+.*/.exec(prefix)
-    originParts = /:(.*)!~([^@]*)@(.*)/.exec(prefix)
+    originParts = /:(.*)!([^@]*)@(.*)/.exec(prefix)
     messageOrigin.isServer = false
     messageOrigin.nick = originParts[1]
     messageOrigin.username = originParts[2]


remove unused props

remove unused props

--- src/user/login-gateway.cjsx
+++ src/user/login-gateway.cjsx
@@ -9,17 +9,11 @@
 LoginGateway = React.createClass

   propTypes:
-    wrapperElement: React.PropTypes.element
-    title: React.PropTypes.string
-
     window: React.PropTypes.shape(
       open: React.PropTypes.func
     )

   getDefaultProps: ->
-    wrapperElement: React.createElement('p')
-
-    title: 'You need to login or signup in order to use ConceptCoach™'
     window: window

   getInitialState: ->


added forceAntialiasing option for Firefox

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -91,6 +91,7 @@
   shadingIntensity: 1
   easingMethod: ''
   newClass: null
+  forceAntialiasing: false

 class root.OriDomi
@@ -192,6 +193,9 @@
     hPanel.style[css.transitionEasing] = @settings.easingMethod
     hPanel.style[css.transformOrigin] = 'top'
     hPanel.style[css.transformStyle] = 'preserve-3d'
+    if @settings.forceAntialiasing
+      hPanel.style.outline = '1px solid transparent'
+
     hPanel.appendChild hMask

     for anchor in ['top', 'bottom']


added function to detect transform property with hyphens

--- oriDomi.coffee
+++ oriDomi.coffee
@@ -36,6 +36,12 @@
   'linear-gradient'

+getTransformProp = ->
+  prefix = css.transform.match(/(\w+)Transform/i)[1]
+  if prefix
+    "-#{ prefix.toLowerCase() }-transform"
+  else
+    'transform'

 # one dimensional:
@@ -67,6 +73,8 @@
     oriDomiSupport = false
     console?.warn 'oriDomi: Browser does not support oriDomi'
 css.gradientProp = getGradientProp()
+css.transformProp = getTransformProp()
+

 defaults =
@@ -181,6 +189,7 @@
     hPanel.style[transitionEasingProp] = @settings.easingMethod
     hPanel.style[transformOriginProp] = 'top'
     hPanel.style[transformStyleProp] = 'preserve-3d'
+    hPanel.style[css.transitionProp] = css.transformProp
     hPanel.appendChild hMask

     for anchor in ['top', 'bottom']


fixed positioning bug in unfreeze

fixed positioning bug in unfreeze

--- oridomi.coffee
+++ oridomi.coffee
@@ -1011,7 +1011,7 @@
       @isFrozen = false
       # Swap the visibility of the elements.
       hideEl @_cloneEl
-      showEl @_stageHolder
+      @_stageHolder.style[css.transform] = 'translateY(-100%)'
       @_setCursor()
       # Set `lastAngle` to 0 so an immediately subsequent call to `freeze` triggers the callback.
       @_lastOp.angle = 0


added colon

added colon

--- src/model/auditRecords.coffee
+++ src/model/auditRecords.coffee
@@ -47,4 +47,4 @@
     "EventTypeCode": eventTypeCodeSchema,
   "ActiveParticipant":[ activeParticipantSchema ]
   "AuditSourceIdentification": AuditSourceIdentificationSchema
-  "ParticipantObjectIdentification"[ ParticipantObjectIdentificationSchema ]
+  "ParticipantObjectIdentification": [ ParticipantObjectIdentificationSchema ]


roatee

roatee

--- udo_framer/MODHome201805v2.framer/app.coffee
+++ udo_framer/MODHome201805v2.framer/app.coffee
@@ -237,10 +237,14 @@
 			if this.name.indexOf("First")>=0
 				ArrowBarBG.visible=true
 				TheHeader.visible=true
+				ArrowComponent.animate
+					rotation:0
 				scrollMainPage.scrollToPoint(x:0,y:0)
 			else
 				TheHeader.visible=false
 				ArrowBarBG.visible=false
+				ArrowComponent.animate
+					rotation:180
 				scrollMainPage.scrollToPoint(x:0,y:500)
 		item.on "focus",->
 			if this.name.indexOf("First")>=0


fixed terminal-notifier.app integration

fixed terminal-notifier.app integration

--- src/logger.coffee
+++ src/logger.coffee
@@ -23,7 +23,7 @@
       args.title ?= args.name ? 'Terminal'
       args.groupId ?= counter += 1
       args.bundleId ?= 'com.apple.Terminal'
-      proc = spawn tnBin, [args.groupId, args.title, message, args.bundleId]
+      proc = spawn tnBin, ['-group', args.groupId, '-title', args.title, '-message', message, '-activate', args.bundleId]
       proc.on 'exit', ->
         callback()
       yes


return GeocacheTypeId as number

return GeocacheTypeId as number

--- storage/lib/geocache.coffee
+++ storage/lib/geocache.coffee
@@ -83,7 +83,7 @@
             Difficulty: parseFloat row.difficulty
             Archived: row.archived
             Available: row.available
-            CacheType: GeocacheTypeId: row.geocachetypeid
+            CacheType: GeocacheTypeId: parseInt row.geocachetypeid
             ContainerType: ContainerTypeName: row.containertypename
             EncodedHints: row.encodedhints
             meta: updated: row.updated


Recurse an arbitrary depth into shadow DOMs for insert mode element

--- content_scripts/mode_insert.coffee
+++ content_scripts/mode_insert.coffee
@@ -45,7 +45,10 @@
     DomUtils.isFocusable @getActiveElement()

   getActiveElement: ->
-    document.activeElement?.shadowRoot?.activeElement ? document.activeElement
+    activeElement = document.activeElement
+    while activeElement?.shadowRoot?.activeElement
+      activeElement = activeElement.shadowRoot.activeElement
+    activeElement

   # Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance.
   @suppressedEvent: null


fix(transaction-runner): use => in validateTransaction gavel callbacks

--- src/transaction-runner.coffee
+++ src/transaction-runner.coffee
@@ -612,7 +612,7 @@

     logger.verbose('Validating HTTP transaction by Gavel.js')
     logger.debug('Determining whether HTTP transaction is valid (getting boolean verdict)')
-    gavel.isValid transaction.real, transaction.expected, 'response', (isValidError, isValid) ->
+    gavel.isValid transaction.real, transaction.expected, 'response', (isValidError, isValid) =>
       if isValidError
         logger.debug('Gavel.js validation errored:', isValidError)
         @emitError(isValidError, test)
@@ -628,7 +628,7 @@
         test.status = 'fail'

       logger.debug('Validating HTTP transaction (getting verbose validation result)')
-      gavel.validate transaction.real, transaction.expected, 'response', (validateError, gavelResult) ->
+      gavel.validate transaction.real, transaction.expected, 'response', (validateError, gavelResult) =>
         if not isValidError and validateError
           logger.debug('Gavel.js validation errored:', validateError)
           @emitError(validateError, test)


fix searching for group set by id

fix searching for group set by id

--- src/app/common/services/group-service.coffee
+++ src/app/common/services/group-service.coffee
@@ -20,7 +20,7 @@
     task.definition.group_set?

   groupService.groupSetName = (id, unit) ->
-    gs = _.find(unit.group_sets, (gs) -> gs.id is id)
+    gs = _.find(unit.group_sets, (gs) -> gs.id is parseInt(id))
     if gs then gs.name else "Individual Work"

   groupService


dont nest qunit tests inside their module declaration

This causes each test to pickup the context of whatever module
was declared before.  H8u qunit.

TEST PLAN:
  JS specs should pass

Change-Id: Iad666ed45e3fb2d147d331b04290f491f42acb8c
Reviewed-on: https://gerrit.instructure.com/52318
Reviewed-by: Simon Williams <088e16a1019277b15d58faf0541e11910eb756f6@instructure.com>
Tested-by: Jenkins
Product-Review: Ethan Vizitei <73eeb0b2f65d05a906adf3b21ee1f9f5a2aa3c1c@instructure.com>
QA-Review: Ethan Vizitei <73eeb0b2f65d05a906adf3b21ee1f9f5a2aa3c1c@instructure.com>

--- spec/coffeescripts/views/courses/roster/InvitationsViewSpec.coffee
+++ spec/coffeescripts/views/courses/roster/InvitationsViewSpec.coffee
@@ -9,18 +9,20 @@
   server = null

   module 'InvitationsView',
+    setup: ->
+    teardown: ->

-    buildView = (enrollment)->
-      model = new RosterUser( enrollments: [enrollment] )
-      model.currentRole = 'student'
-      view = new InvitationsView(model: model)
+  buildView = (enrollment)->
+    model = new RosterUser( enrollments: [enrollment] )
+    model.currentRole = 'student'
+    view = new InvitationsView(model: model)

-    test 'knows when invitation is pending', ->
-      enrollment = {id: 1, role: 'student', enrollment_state: 'invited'}
-      buildView enrollment
-      equal view.invitationIsPending(), true
+  test 'knows when invitation is pending', ->
+    enrollment = {id: 1, role: 'student', enrollment_state: 'invited'}
+    buildView enrollment
+    equal view.invitationIsPending(), true

-    test 'knows when invitation is not pending', ->
-      enrollment = {id: 1, role: 'student', enrollment_state: 'accepted'}
-      buildView enrollment
-      equal view.invitationIsPending(), false
+  test 'knows when invitation is not pending', ->
+    enrollment = {id: 1, role: 'student', enrollment_state: 'accepted'}
+    buildView enrollment
+    equal view.invitationIsPending(), false


Updated comment.

Updated comment.

--- tunemachine/routes/api.coffee
+++ tunemachine/routes/api.coffee
@@ -67,7 +67,7 @@
           res.status = 404
           res.end()

-  # GET /api/playlist/:playlistId
+  # GET /api/playlist/:playlistId/:snapId
   app.get '/api/playlist/:playlistId/:snapId', requireLoggedIn, get

   post = (req, res) ->


replyTo is overridable when sending email

--- services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
+++ services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
@@ -34,7 +34,6 @@
 			to: "bob@bob.com"
 			subject: "new email"
 			html: "<hello></hello>"
-			replyTo: "sarah@bob.com"

 	describe "sendEmail", ->

@@ -71,3 +70,12 @@
 				args.replyTo.should.equal @settings.email.replyToAddress
 				done()

+
+		it "should use the reply to address in options as an override", (done)->
+			@sesClient.sendemail.callsArgWith(1)
+
+			@opts.replyTo = "someone@else.com"
+			@sender.sendEmail @opts, =>
+				args = @sesClient.sendemail.args[0][0]
+				args.replyTo.should.equal @opts.replyTo
+				done()


Moves AP from spec helpers to lib to make it globally available

--- support/spec_helpers.coffee
+++ support/spec_helpers.coffee
@@ -57,5 +57,3 @@
   )

-window.AP = (object)->
-  "#{object.constructor.name}(#{jasmine.pp object})"


tests: Better check against error

tests: Better check against error

--- spec/protocol.coffee
+++ spec/protocol.coffee
@@ -74,7 +74,7 @@
     fs.mkdirSync options.componentdir
     runtime = new Runtime options
     runtime.start (err, url) ->
-      chai.expect(err).to.be.a 'null'
+      chai.expect(err).to.not.exist
       ui.once 'connected', () ->
         done()
       ui.connect options.port


Look for error context on err object, not result

--- test/auth.coffee
+++ test/auth.coffee
@@ -71,7 +71,7 @@
         @services.getRole {}, (err, result) ->
           should.exist err?.message, 'expected error'
           err.message.should.eql "getRole requires 'sessionId' to be defined."
-          result.should.eql
+          err.should.include
             reason: 'requiredField'
             fieldName: 'sessionId'
             serviceName: 'getRole'


Validate existence of group and user membership before adding to classification metadata

--- app/pages/project/classify.cjsx
+++ app/pages/project/classify.cjsx
@@ -145,8 +145,8 @@
           workflow: workflow.id
           subjects: [subject.id]

-      if @props.location.query?.group?
-        classification.update({ 'metadata.user_group': @props.location.query.group })
+      # if @validateUserGroup()
+      #   classification.update({ 'metadata.user_group': @props.location.query.group })

       # If the user hasn't interacted with a classification resource before,
       # we won't know how to resolve its links, so attach these manually.
@@ -213,8 +213,7 @@
       {if @props.projectIsComplete
         <FinishedBanner project={@props.project} />}

-      {if @props.location.query?.group?
-
You are classifying as a student of your classroom.
}
+
       {if @state.classification?
         <Classifier
           {...@props}
@@ -310,6 +309,15 @@
               'preferences.selected_workflow': props.preferences.settings.workflow_id
             props.preferences.save()

+  validateUserGroup: ->
+    valid = false
+    console.log(@props.location.query.group)
+    if @props.location.query?.group? and @props.user?
+      apiClient.type('user_groups').get(@props.location.query.group).then (group) =>
+        console.log(group, group and group.links.users.includes(@props.user.id))
+        valid = group and group.links.users.includes(@props.user.id)
+    valid
+

 # For debugging:
 window.currentWorkflowForProject = currentWorkflowForProject


Moved skibba to retired list (on his request).

--- app/lib/en.coffee
+++ app/lib/en.coffee
@@ -1149,7 +1149,6 @@
       schawinski: 'Kevin Schawinski - Co-Founder: Astronomer, ETH Zurich
Works on the formation and evolution of galaxies and their supermassive black holes. Usually trying to get more sleep. And coffee.'
       edmondson: 'Edd Edmondson - Astronomer, University of Portsmouth
Zookeeper, aiding in accessing Sloan data, and general helper-outer.'
       keel: '''Bill Keel - Astronomer, University of Alabama
Author of 'The Road to Galaxy Formation' and 'The Sky at Einstein's Feet'. With research interests in galaxy formation and evolution, dust content of galaxies, and active galactic nuclei, he couldn't stay away when Galaxy Zoo got started.'''
-      skibba: 'Ramin Skibba - Science Writer, Former Astronomer, San Diego
Ramin lead the use of marked clustering statistics and dark matter halo models to analyse the environmental dependence of Galaxy Zoo morphologies. Always keenly interested in science communication and science policy, he's shifted from research astronomy to science writing.'
       cardamone: '''Carie Cardamone - Astronomer, Wheelock College
An educator and astronomer, studying the Zoo's green peas, interested in understanding why galaxies grow they way they do. She's very enthusiastic about engaging the broader public in scientific research and the nature of science.'''
       willett: 'Kyle Willett - Lead Data Scientist: Astronomer, University of Minnesota
  Studies the roles of both morphology and environment on galaxies and their active black holes. He lead the data reduction and catalog creation from Galaxy Zoo 2 classifications, has worked on building new tools to enable independent research by citizen science volunteers and now leads efforts to include new data sets in Galaxy Zoo. In his spare time, he runs probably more than is healthy.'      casteels: 'Kevin Casteels - Astronomer, University of Victoria
  Interested in galaxy evolution and morphologies, specifically how they are affected by mergers and interactions.  Kevin is currently working with the makers of the Starry Night astronomy software, helping to develop new astronomy apps and keep the systems running!  In his spare time he is also a SCUBA instructor and loves the great outdoors.'
       urry: 'Meg Urry - Physicist, Yale University
Works on supermassive black holes (growth history, unification, relativistic jets, blazars) and their interplay with galaxies. In her spare time, she writes for CNN.com and works to increase the participation of women and minorities in science.'
@@ -1171,7 +1170,6 @@
       hart: 'Ross Hart - Astronomer, University of Nottingham
 PhD student.'
       kruk: 'Sandor Kruk - Astronomer, University of Oxford
 PhD student.'
       newnham: 'Lucy Newnham - Astronomer, University of Portsmouth
 PhD student.'
-

     dev:
       title: 'Zooniverse Development Team'
@@ -1224,7 +1222,8 @@
       smith: 'Arfon Smith
As an undergraduate, Arfon studied Chemistry at the University of Sheffield before completing his Ph.D. in Astrochemistry at The University of Nottingham in 2006. He worked as a senior developer at the Wellcome Trust Sanger Institute (Human Genome Project) in Cambridge before joining the Galaxy Zoo team in Oxford. He was responsible for leading the development of a platform for citizen science called Zooniverse, and was Director of Citizen Science at the Adler Planetarium where he led the software and infrastructure development for the Zooniverse.'
       whyte: '''Laura Whyte
Having spent her PhD trying to get computers to classify galaxies, Laura knows only too well the true value of the contribution to science made by the Galaxy Zoo volunteers. Since leaving astronomy she retrained as a maths teacher and picked up a few programming skills. It was with great delight she joined the team at the Zooniverse, developing online education and research tools, allowing her to use bits and pieces of all the skills she's picked up over the last decade.'''
       hoyle: 'Ben Hoyle - Astronomer, Ludwig-Maximilians-Universität München, Physics
While a PhD student in Portsmouth worked on connections between Google Sky and Galaxy Zoo.'
-
+      skibba: 'Ramin Skibba - Science Writer, Former Astronomer, San Diego
Ramin lead the use of marked clustering statistics and dark matter halo models to analyse the environmental dependence of Galaxy Zoo morphologies. Always keenly interested in science communication and science policy, he's shifted from research astronomy to science writing.'
+
   examine:
     title: 'Examine'
     survey: 'Survey'


Typo fix

Typo fix

--- src/plugins/nofloGraph.coffee
+++ src/plugins/nofloGraph.coffee
@@ -152,7 +152,6 @@
     graph.on 'removeNode', (nfNode) =>
       if nfNode.dataflowNode?
         nfNode.dataflowNode.remove()
-      @dataflow.plugins.uog.add 'node removed: ' + nfNode.id
       runtime.sendGraph 'removenode',
         id: nfNode.id
     graph.on 'addEdge', (nfEdge) =>


url search should check for a url

url search should check for a url

--- src/scripts/virustotal.coffee
+++ src/scripts/virustotal.coffee
@@ -9,7 +9,7 @@
 #
 # Commands:
 #   hubot virustotal hash <hash> - Searches VirusTotal for a hash
-#   hubot virustotal url <hash> - Searches VirusTotal for a url
+#   hubot virustotal url <url> - Searches VirusTotal for a url
 #
 # Author:
 #   Scott J Roberts - @sroberts


terminal: add a stupid heuristic to force reconnect in case no data is coming back; I don't like this but I'm sick of terminal hangs.

--- src/smc-webapp/console.coffee
+++ src/smc-webapp/console.coffee
@@ -258,6 +258,12 @@
             catch e
                 console.log("issue parsing message -- ", e)

+    reconnect_if_no_recent_data: =>
+        #console.log 'check for recent data'
+        if not @_got_remote_data? or new Date() - @_got_remote_data >= 15000
+            #console.log 'reconnecting since no recent data'
+            @session?.reconnect()
+
     set_session: (session) =>
         if @session?
             # Don't allow set_session to be called multiple times, since both sessions could
@@ -275,6 +281,7 @@
         # but can also be the result of a device attributes request, etc.
         @terminal.on 'data',  (data) =>
             @session.write_data(data)
+            setTimeout(@reconnect_if_no_recent_data, 3000)

         # The terminal receives a 'set my title' message.
         @terminal.on 'title', (title) => @set_title(title)
@@ -285,7 +292,8 @@

             # The remote server sends data back to us to display:
             @session.on 'data',  (data) =>
-                #console.log("got #{data.length} data")
+                #console.log("terminal got #{data.length} characters")
+                @_got_remote_data = new Date()
                 if @_rendering_is_paused
                     @_render_buffer += data
                 else
@@ -297,6 +305,7 @@
                 @element.find("a[href=#refresh]").addClass('btn-success').find(".fa").addClass('fa-spin')

             @session.on 'reconnect', () =>
+                @_got_remote_data = new Date()
                 #console.log("terminal: reconnect")
                 @element.find(".salvus-console-terminal").css('opacity':'1')
                 @element.find("a[href=#refresh]").removeClass('btn-success').find(".fa").removeClass('fa-spin')


pdf workaround isn't needed anymore

pdf workaround isn't needed anymore

--- src/smc-webapp/project.coffee
+++ src/smc-webapp/project.coffee
@@ -591,10 +591,6 @@
             timeout : 45
             cb      : undefined   # cb(err) when file download from browser starts -- instant since we use raw path

-        #if misc.filename_extension(opts.path) == 'pdf'
-            # unfortunately, download_file doesn't work for pdf these days...
-        #    opts.auto = false
-
         url = @download_href(opts.path)
         if opts.auto
             download_file(url)


side chat was causing a stacktrace every single time it is used in some cases...

--- src/smc-webapp/side_chat.cjsx
+++ src/smc-webapp/side_chat.cjsx
@@ -112,7 +112,7 @@
             misc.original_path(@props.path)
         )
         @props.actions.send_chat(value)
-        if @input_ref.current?
+        if @input_ref.current?.focus?
             @input_ref.current.focus()

     on_input_change: (value, mentions, plain_text) ->


Fix deleted open files by other perosn

Fix deleted open files by other perosn

--- src/smc-webapp/syncdoc.coffee
+++ src/smc-webapp/syncdoc.coffee
@@ -263,7 +263,7 @@
             @emit('before-change')

         @_syncstring.on 'deleted', =>
-            redux.getProjectActions(@project_id).close_file(@filename)
+            redux.getProjectActions(@project_id).close_tab(@filename)

     live: (s) =>
         if s? and s != @_syncstring.get()
@@ -408,7 +408,7 @@
                     @_syncstring.set(@codemirror.getValue())

                 @_syncstring.on 'deleted', =>
-                    redux.getProjectActions(@editor.project_id).close_file(@filename)
+                    redux.getProjectActions(@editor.project_id).close_tab(@filename)

                 save_state = () => @_sync()
                 # We debounce instead of throttle, because we want a single "diff/commit" to correspond


Remove unused menus.

Remove unused menus.

--- menus/dart-tools.cson
+++ menus/dart-tools.cson
@@ -1,16 +0,0 @@
-# See https://atom.io/docs/latest/creating-a-package#menus for more details
-'context-menu':
-  '.overlayer':
-      'Enable dart-tools': 'dart-tools:toggle'
-
-'menu': [
-  {
-    'label': 'Packages'
-    'submenu': [
-      'label': 'dart-tools'
-      'submenu': [
-        { 'label': 'Toggle', 'command': 'dart-tools:toggle' }
-      ]
-    ]
-  }
-]


Change default discussion page to general all

--- resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
+++ resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
@@ -8,7 +8,7 @@

 class window.BeatmapDiscussionHelper
   @DEFAULT_BEATMAP_ID: '-'
-  @DEFAULT_MODE: 'timeline'
+  @DEFAULT_MODE: 'generalAll'
   @DEFAULT_FILTER: 'total'
   @MAX_MESSAGE_PREVIEW_LENGTH: 100
   @MAX_LENGTH_TIMELINE: 750


Conditionally include deprecations in Marker

--- src/marker.coffee
+++ src/marker.coffee
@@ -1,7 +1,5 @@
 {Range} = require 'text-buffer'
 _ = require 'underscore-plus'
-{Subscriber} = require 'emissary'
-EmitterMixin = require('emissary').Emitter
 {Emitter} = require 'event-kit'
 Grim = require 'grim'

@@ -45,9 +43,6 @@
 # See {TextEditor::markBufferRange} for usage.
 module.exports =
 class Marker
-  EmitterMixin.includeInto(this)
-  Subscriber.includeInto(this)
-
   bufferMarkerSubscription: null
   oldHeadBufferPosition: null
   oldHeadScreenPosition: null
@@ -118,17 +113,6 @@
   onDidDestroy: (callback) ->
     @emitter.on 'did-destroy', callback

-  on: (eventName) ->
-    switch eventName
-      when 'changed'
-        Grim.deprecate("Use Marker::onDidChange instead")
-      when 'destroyed'
-        Grim.deprecate("Use Marker::onDidDestroy instead")
-      else
-        Grim.deprecate("Marker::on is deprecated. Use documented event subscription methods instead.")
-
-    EmitterMixin::on.apply(this, arguments)
-
   ###
   Section: Marker Details
   ###
@@ -161,9 +145,6 @@
   # the marker.
   getProperties: ->
     @bufferMarker.getProperties()
-  getAttributes: ->
-    Grim.deprecate 'Use Marker::getProperties instead'
-    @getProperties()

   # Essential: Merges an {Object} containing new properties into the marker's
   # existing properties.
@@ -171,16 +152,10 @@
   # * `properties` {Object}
   setProperties: (properties) ->
     @bufferMarker.setProperties(properties)
-  setAttributes: (properties) ->
-    Grim.deprecate 'Use Marker::getProperties instead'
-    @setProperties(properties)

   matchesProperties: (attributes) ->
     attributes = @displayBuffer.translateToBufferMarkerParams(attributes)
     @bufferMarker.matchesParams(attributes)
-  matchesAttributes: (attributes) ->
-    Grim.deprecate 'Use Marker::matchesProperties instead'
-    @matchesProperties(attributes)

   ###
   Section: Comparing to other markers
@@ -344,7 +319,7 @@

   destroyed: ->
     delete @displayBuffer.markers[@id]
-    @emit 'destroyed'
+    @emit 'destroyed' if Grim.includeDeprecations
     @emitter.emit 'did-destroy'
     @emitter.dispose()

@@ -375,7 +350,7 @@
     if @deferredChangeEvents?
       @deferredChangeEvents.push(changeEvent)
     else
-      @emit 'changed', changeEvent
+      @emit 'changed', changeEvent if Grim.includeDeprecations
       @emitter.emit 'did-change', changeEvent

     @oldHeadBufferPosition = newHeadBufferPosition
@@ -392,9 +367,38 @@
       @deferredChangeEvents = null

       for event in deferredChangeEvents
-        @emit 'changed', event
+        @emit 'changed', event if Grim.includeDeprecations
         @emitter.emit 'did-change', event
     return

   getPixelRange: ->
     @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)
+
+if Grim.includeDeprecations
+  {Subscriber} = require 'emissary'
+  EmitterMixin = require('emissary').Emitter
+  EmitterMixin.includeInto(Marker)
+  Subscriber.includeInto(Marker)
+
+  Marker::on = (eventName) ->
+    switch eventName
+      when 'changed'
+        Grim.deprecate("Use Marker::onDidChange instead")
+      when 'destroyed'
+        Grim.deprecate("Use Marker::onDidDestroy instead")
+      else
+        Grim.deprecate("Marker::on is deprecated. Use documented event subscription methods instead.")
+
+    EmitterMixin::on.apply(this, arguments)
+
+  Marker::getAttributes = ->
+    Grim.deprecate 'Use Marker::getProperties instead'
+    @getProperties()
+
+  Marker::setAttributes = (properties) ->
+    Grim.deprecate 'Use Marker::setProperties instead'
+    @setProperties(properties)
+
+  Marker::matchesAttributes = (attributes) ->
+    Grim.deprecate 'Use Marker::matchesProperties instead'
+    @matchesProperties(attributes)


Correctly construct avatar URL in production.

Passing null is not OK because then ROOT_URL is ignored (because option
is passed, as null, instead of not being passed at all).

--- lib/documents/person.coffee
+++ lib/documents/person.coffee
@@ -135,7 +135,7 @@
     size = 24 unless _.isNumber size

     defaultAvatar = if @gravatarHash then 'identicon' else Meteor.absoluteUrl 'images/spacer.gif',
-      rootUrl: if Meteor.settings?.public?.production then null else 'https://peerlibrary.org'
+      if Meteor.settings?.public?.production then {} else rootUrl: 'https://peerlibrary.org'

     # TODO: We should specify default URL to the image of an avatar which is generated from name initials
     "https://secure.gravatar.com/avatar/#{ @gravatarHash }?s=#{ size }&d=#{ defaultAvatar }"


Leaving the game now updatest the game list for others

--- server/src/server.coffee
+++ server/src/server.coffee
@@ -23,17 +23,18 @@
     gameNumber = this.now.gameNumber
     {game, group} = getThisGameGroup(gameNumber)
     groupReference = group.now
-    console.log "leaveGame now method called."
     playerIndex = game.removeClient(clientId)
     game.nextTurn( ->
       groupReference.receiveMoveTimeUp()
       groupReference.receiveState(game.state))
     group.now.receivePlayerDisconnect(playerIndex)
     group.now.receiveState(game.state)
+  gamesManager.cleanGames()
+  everyone.now.getGames()
+

 everyone.on 'connect', ->
   console.log "CONNECT: #{this.user.clientId}"
-  gamesManager.cleanGames()

 gamesManager = new GamesManager()
 if Settings.DEBUG


update unit tests - FileHandler now calls deleteDirectory on cache directory

--- services/filestore/test/unit/coffee/FileHandlerTests.coffee
+++ services/filestore/test/unit/coffee/FileHandlerTests.coffee
@@ -69,6 +69,7 @@
 		beforeEach ->
 			@keyBuilder.getConvertedFolderKey.returns(@stubbedConvetedKey)
 			@PersistorManager.deleteFile.callsArgWith(2)
+			@PersistorManager.deleteDirectory.callsArgWith(2)

 		it "should tell the filestore manager to delete the file", (done)->
 			@handler.deleteFile @bucket, @key, =>
@@ -77,7 +78,7 @@

 		it "should tell the filestore manager to delete the cached foler", (done)->
 			@handler.deleteFile @bucket, @key, =>
-				@PersistorManager.deleteFile.calledWith(@bucket, @stubbedConvetedKey).should.equal true
+				@PersistorManager.deleteDirectory.calledWith(@bucket, @stubbedConvetedKey).should.equal true
 				done()

 	describe "getFile", ->


use projectStateUnchanged instead of stateOk

--- services/web/app/coffee/Features/Compile/ClsiManager.coffee
+++ services/web/app/coffee/Features/Compile/ClsiManager.coffee
@@ -123,11 +123,11 @@
 			if project.compiler not in ClsiManager.VALID_COMPILERS
 				project.compiler = "pdflatex"

-			ClsiStateManager.checkProjectStateMatch project_id, project, (error, stateOk, projectStateHash) ->
+			ClsiStateManager.checkProjectStateMatch project_id, project, (error, projectStateUnchanged, projectStateHash) ->
 				return callback(error) if error?
-				logger.log project_id: project_id, checkState: stateOk, "checked project state"
+				logger.log project_id: project_id, projectStateUnchanged: projectStateUnchanged, "checked project state"
 				# see if we can send an incremental update to the CLSI
-				if stateOk and options.syncType isnt "full"
+				if projectStateUnchanged and options.syncType isnt "full"
 					ClsiManager._getContentFromDocUpdater project_id, (error, docUpdaterDocs) ->
 						return callback(error) if error?
 						ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) ->


add rootFolder to attributes in Clsi request

--- services/web/app/coffee/Features/Compile/ClsiManager.coffee
+++ services/web/app/coffee/Features/Compile/ClsiManager.coffee
@@ -120,7 +120,7 @@
 	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]

 	_buildRequest: (project_id, options={}, callback = (error, request) ->) ->
-		Project.findById project_id, {}, (error, project) ->
+		Project.findById project_id, {compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder:1}, (error, project) ->
 			return callback(error) if error?
 			return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
 			console.log "PROJECT", project, JSON.stringify(project.rootFolder,null,2)


allow user to select a given file to diff

--- src/smc-webapp/editor_git.cjsx
+++ src/smc-webapp/editor_git.cjsx
@@ -4,7 +4,7 @@
 ###

 {React, ReactDOM, rclass, rtypes, Redux, Actions, Store}  = require('./smc-react')
-{Button, Input, Form, Panel, Row, Col, Tabs, Tab} = require('react-bootstrap')
+{Button, Input, Form, Panel, Row, Col, Tabs, Tab, DropdownButton, MenuItem} = require('react-bootstrap')
 {Icon, Octicon, Space, Tip} = require('./r_misc')
 {salvus_client} = require('./salvus_client')
 misc = require('smc-util/misc')
@@ -183,11 +183,18 @@
                 else
                     @setState(git_status : output.stdout)

-    update_diff : () =>
+    update_diff : =>
+        store = @redux.getStore(@name)
+
+        if store
+            console.log('file to diff',store.get('file_to_diff'))
+            args = if store.get('file_to_diff') then ['diff', store.get('file_to_diff')] else ['diff']
+        else
+            args = ['diff']
         @setState(git_diff : 'updating...')
         @exec
             cmd  : "git"
-            args : ['diff']
+            args : args
             cb   : (err, output) =>
                 if err
                     @setState(git_diff : '')
@@ -332,6 +339,7 @@
             git_changed_tracked_files : rtypes.array
             git_changed_untracked_files : rtypes.array
             checked_files : rtypes.object
+            file_to_diff          : rtypes.string

     propTypes :
         actions : rtypes.object
@@ -513,10 +521,22 @@
             {
{@props.git_status}
 if @props.git_status}
         </Panel>

+    render_diff_files : ->
+        if @props.git_changed_tracked_files
+            for file, idx in @props.git_changed_tracked_files
+                console.log(idx)
+                <MenuItem key={idx} eventKey="{file}" onSelect={(e)=>console.log(JSON.stringify(e.target));@props.actions.setState(file_to_diff:e.target.text);@props.actions.update_diff()}>{file}</MenuItem>
+
     render_diff : ->
         head =

                 git diff
+                <Space/> <Space/>
+                <DropdownButton title='file' id='files_to_diff'
+
+                >
+                    {@render_diff_files()}
+                </DropdownButton>
                 <Space/> <Space/>
                 <Button onClick={=>@props.actions.update_diff()}>
                     Refresh


update embed for new handling of DOIs not in crossref but in oadoi

--- static/embed/oab_embed.coffee
+++ static/embed/oab_embed.coffee
@@ -410,9 +410,6 @@
   for m in ['title','year','journal','doi']
     if this.f?.metadata?[m]? or this.data[m]?
       _L.set '#_oab_'+m, (this.f.metadata ? this.data)[m] #.split('(')[0].trim()
-  #if this.f?.doi_not_in_crossref
-  #  _L.html '#_oab_bad_doi', this.f.doi_not_in_crossref
-  #  _L.show '#_oab_doi_not_in_crossref'
   _L.hide '._oab_panel'
   _L.show '#_oab_metadata'

@@ -458,7 +455,7 @@
             try
               v += ', ' if v.length
               v += if typeof author is 'string' then author else if typeof author is 'object' and author.family then (author.family + if author.given then ', ' + author.given else '') else JSON.stringify author
-      else if k in ['doi','pmid','pmc','pmcid','url','journal','title','year','issn','volume','issue','page','crossref_type','publisher','published','notes']
+      else if k in ['doi','pmid','pmc','pmcid','url','journal','title','year','issn','volume','issue','page','crossref_type','type','publisher','published','notes']
         v = this.f.metadata[k]
       url += (config[k] ? k) + '=' + encodeURIComponent(v) + '&' if v
     notes = if this.data.usermetadata then 'The user provided some metadata. ' else ''
@@ -583,11 +580,11 @@
       setTimeout (() => this.permissions()), 100
     else
       this.loading false
-      if this.f?.doi_not_in_crossref
+      if this.f?.doi_not_in_crossref and this.f?.doi_not_in_oadoi
         this.f = {}
         _L.show '#_oab_error', '
Double check your DOI, that doesn\'t look right to us.
'
         _L.gebi('_oab_input').focus()
-      else if this.f?.metadata?.crossref_type? and this.f.metadata.crossref_type not in ['journal-article', 'proceedings-article']
+      else if (this.f?.metadata?.crossref_type? and this.f.metadata.crossref_type not in ['journal-article', 'proceedings-article']) or (this.f?.metadata?.type? and this.f.metadata.type not in ['journal-article', 'proceedings-article'])
         _L.gebi('_oab_input').focus()
         nj = '
Sorry, right now this only works with academic journal articles.'
         if this.cml()
@@ -680,7 +677,7 @@
       setTimeout (() => this.findings()), 100
     else
       this.loading false
-      if ct = this.f.metadata?.crossref_type
+      if ct = this.f.metadata?.crossref_type ? this.f.metadata?.type
         if ct not in ['journal-article','proceedings-article','posted-content']
           if ct in ['book-section','book-part','book-chapter']
             err = '
Please make your request through our ' + (if this.config.book then 'book form' else 'book form')


Specs for the directJump setting

Specs for the directJump setting

--- test/test.coffee
+++ test/test.coffee
@@ -1,5 +1,7 @@
 chai = require('chai')
 sinon = require('sinon')
+spy = sinon.spy
+mock = sinon.mock
 sinonChai = require('sinon-chai')
 chai.use(sinonChai)
 jQuery = $ = require('jquery')
@@ -151,7 +153,7 @@
         expect(@$carousel.circular('settings').displayDuration).to.equal 4000

       it 'pausing on hover is disabled by default', (done) ->
-        _hover = sinon.spy $.fn, 'hover'
+        _hover = spy $.fn, 'hover'
         @$carousel.mouseenter =>
           expect(@$carousel.circular('isRunning')).to.be.true
           done()
@@ -164,26 +166,49 @@
         @$carousel.circular()

       it 'pausing on hover can be enabled', (done) ->
-        _hover = sinon.spy $.fn, 'hover'
+        _hover = spy $.fn, 'hover'
         @$carousel.on 'circular:init', =>
           @$carousel.on 'circular:paused', =>
             expect(@$carousel.circular('isRunning')).to.be.false
             done()
+          # FIXME: use .withArgs?
           expect(_hover).to.have.been.calledOnce
           expect(_hover).to.have.been.calledWith(@$carousel.circular('methods').pause, @$carousel.circular('methods').resume)
           @$carousel.hover.restore()
           @$carousel.mouseenter()
         @$carousel.circular({pauseOnHover: true})

+      it 'direct jump (transitionDelay enforced to 0) is disabled by default', (done) ->
+        _internals = null
+        @$carousel.on 'circular:jumped', ->
+          _internals.verify()
+          done()
+        @$carousel.on 'circular:init', =>
+          _internals = mock(@$carousel.circular('_internals'))
+          _internals.expects('transitionTo').once().withArgs(1, null)
+          $(@$carousel.circular('controls')[1]).click()
+        @$carousel.circular()
+
+      it 'direct jump can be enabled', (done) ->
+        _internals = null
+        @$carousel.on 'circular:jumped', ->
+          _internals.verify()
+          done()
+        @$carousel.on 'circular:init', =>
+          _internals = mock(@$carousel.circular('_internals'))
+          _internals.expects('transitionTo').once().withArgs(1, 0)
+          $(@$carousel.circular('controls')[1]).click()
+        @$carousel.circular({directJump: true})
+
       it 'autoStart is true by default', ->
-        _resume = sinon.spy @$carousel.circular('_internals'), 'resume'
+        _resume = spy @$carousel.circular('_internals'), 'resume'
         @$carousel.circular()
         expect(_resume).to.have.been.calledOnce
         expect(@$carousel.circular('isRunning')).to.be.true
         @$carousel.circular('_internals').resume.restore()

       it 'autoStart can be set to false', ->
-        _resume = sinon.spy @$carousel.circular('_internals'), 'resume'
+        _resume = spy @$carousel.circular('_internals'), 'resume'
         @$carousel.circular({autoStart: false})
         expect(_resume).to.not.have.been.called
         expect(@$carousel.circular('isRunning')).to.be.false


fixed test for 0.10

fixed test for 0.10

--- test/test.coffee
+++ test/test.coffee
@@ -65,16 +65,17 @@
 checkClean 'linux'
 checkClean 'win32'

-it 'should execute cat/type correctly (sync)', ->
-  platform = _platform()
-  pipe = _require '../'
-  inputSource = fs.readFileSync path.resolve path.join __dirname,
-    '../package.json'
-  res = pipe inputSource
-  output = execSync "#{res.command} #{typeCat} #{res.file}",
-    input: res.buffer
-  res.clean()
-  (JSON.parse output).name.should.equal 'smart-pipe'
+if execSync # node 0.10 doesn't have execSync for some reason.
+  it 'should execute cat/type correctly (sync)', ->
+    platform = _platform()
+    pipe = _require '../'
+    inputSource = fs.readFileSync path.resolve path.join __dirname,
+      '../package.json'
+    res = pipe inputSource
+    output = execSync "#{res.command} #{typeCat} #{res.file}",
+      input: res.buffer
+    res.clean()
+    (JSON.parse output).name.should.equal 'smart-pipe'

 it 'should execute cat/type correctly (async)', (done)->
   platform = _platform()


make test executable

make test executable

--- test/test.coffee
+++ test/test.coffee
@@ -1,3 +1,4 @@
+#!/usr/bin/env coffee
 js2c  = require('../lib/js2coffee')
 glob  = require('glob').globSync or require('glob').sync
 fs    = require('fs')


Fix indentation problem

Fix indentation problem

--- test/test.coffee
+++ test/test.coffee
@@ -87,12 +87,12 @@
       class TestCollection extends Backbone.TypeaheadCollection
         typeaheadAttributes: ['id']

-        collection = new TestCollection()
-
-        expected = []
-        actual = _.map collection.typeahead('a'), (m) -> m.id
-
-        actual.should.eql expected
+      collection = new TestCollection()
+
+      expected = []
+      actual = _.map collection.typeahead('a'), (m) -> m.id
+
+      actual.should.eql expected

     it 'should handle null/weird/missing attribute values', ->
       class BrokenCollection extends Backbone.TypeaheadCollection