Add specs for before actions.

Add specs for before actions.

--- test/spec/dispatcher_spec.coffee
+++ test/spec/dispatcher_spec.coffee
@@ -22,56 +22,72 @@
     redirectToURLRoute = controller: 'test1', action: 'redirectToURL'
     redirectToControllerRoute = controller: 'test1', action: 'redirectToController'

-    # Define test controllers
-    class Test1Controller extends Controller
-
-      historyURL: (params) ->
-        #console.debug 'Test1Controller#historyURL'
-        'test1/' + (params.id or '')
-
-      initialize: (params, oldControllerName) ->
-        #console.debug 'Test1Controller#initialize', params.id, oldControllerName
-        super
-
-      show: (params, oldControllerName) ->
-        #console.debug 'Test1Controller#show', params, oldControllerName
-
-      redirectToURL: (params, oldControllerName) ->
-        @redirectTo '/test2/123'
-
-      redirectToController: (params, oldControllerName) ->
-        @redirectTo 'test2', 'show', params
-
-      dispose: (params, newControllerName) ->
-        #console.debug 'Test1Controller#dispose'
-        super
-
-    class Test2Controller extends Controller
-
-      historyURL: (params) ->
-        #console.debug 'Test2Controller#historyURL'
-        'test2/' + (params.id or '')
-
-      initialize: (params, oldControllerName) ->
-        #console.debug 'Test2Controller#initialize', params, oldControllerName
-        super
-
-      show: (params, oldControllerName) ->
-        #console.debug 'Test2Controller#show', params, oldControllerName
-
-      dispose: (params, newControllerName) ->
-        #console.debug 'Test2Controller#dispose'
-        super
-
-    # Define a test controller AMD modules
-    test1Module = 'controllers/test1_controller'
-    test2Module = 'controllers/test2_controller'
-    define test1Module, -> Test1Controller
-    define test2Module, -> Test2Controller
-
-    # Helpers for asynchronous tests
-    test1Loaded = (callback) -> require [test1Module], callback
-    test2Loaded = (callback) -> require [test2Module], callback
+    Test1Controller = null
+    Test2Controller = null
+
+    loadTest1ControllerAndExecute = null
+    loadTest2ControllerAndExecute = null
+
+    beforeEach ->
+      dispatcher = new Dispatcher()
+
+      # Define test controllers. The classes are redefined before each spec to
+      # ensure a spec fiddling around with their prototype can't break other specs.
+
+      Test1Controller = class Test1Controller extends Controller
+
+        historyURL: (params) ->
+          #console.debug 'Test1Controller#historyURL'
+          'test1/' + if params.id? then params.id else ''
+
+        initialize: (params, oldControllerName) ->
+          #console.debug 'Test1Controller#initialize', params.id, oldControllerName
+          super
+
+        show: (params, oldControllerName) ->
+          #console.debug 'Test1Controller#show', params, oldControllerName
+
+        redirectToURL: (params, oldControllerName) ->
+          @redirectTo '/test2/123'
+
+        redirectToController: (params, oldControllerName) ->
+          @redirectTo 'test2', 'show', params
+
+        dispose: (params, newControllerName) ->
+          #console.debug 'Test1Controller#dispose'
+          super
+
+      Test2Controller = class Test2Controller extends Controller
+
+        historyURL: (params) ->
+          #console.debug 'Test2Controller#historyURL'
+          'test2/' + if params.id? then params.id else ''
+
+        initialize: (params, oldControllerName) ->
+          #console.debug 'Test2Controller#initialize', params, oldControllerName
+          super
+
+        show: (params, oldControllerName) ->
+          #console.debug 'Test2Controller#show', params, oldControllerName
+
+        dispose: (params, newControllerName) ->
+          #console.debug 'Test2Controller#dispose'
+          super
+
+      # Define a test controller AMD modules
+      test1Module = 'controllers/test1_controller'
+      test2Module = 'controllers/test2_controller'
+      define test1Module, -> Test1Controller
+      define test2Module, -> Test2Controller
+
+      # Helpers for asynchronous tests
+      loadTest1ControllerAndExecute = (callback) -> require [test1Module], callback
+      loadTest2ControllerAndExecute = (callback) -> require [test2Module], callback
+
+    afterEach ->
+      dispatcher.dispose()
+      dispatcher = null
+

     # Reset helpers

@@ -81,9 +97,6 @@
       routeOptions = changeURL: false

     beforeEach refreshParams
-
-    it 'should initialize', ->
-      dispatcher = new Dispatcher()

     it 'should dispatch routes to controller actions', (done) ->
       proto = Test1Controller.prototype
@@ -93,7 +106,7 @@

       mediator.publish 'matchRoute', route1, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(initialize).was.calledWith params, null
         expect(action).was.calledWith params, null
         expect(historyURL).was.calledWith params
@@ -107,7 +120,7 @@
     it 'should not start the same controller if params match', (done)->
       mediator.publish 'matchRoute', route1, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         proto = Test1Controller.prototype
         initialize = sinon.spy proto, 'initialize'
         action     = sinon.spy proto, 'show'
@@ -115,7 +128,7 @@

         mediator.publish 'matchRoute', route1, params, routeOptions

-        test1Loaded ->
+        loadTest1ControllerAndExecute ->
           expect(initialize).was.notCalled()
           expect(action).was.notCalled()
           expect(historyURL).was.notCalled()
@@ -137,7 +150,7 @@
       refreshParams()
       mediator.publish 'matchRoute', route1, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(initialize).was.calledWith params, 'test1'
         expect(action).was.calledWith params, 'test1'
         expect(historyURL).was.calledWith params
@@ -159,7 +172,7 @@
       routeOptions.forceStartup = true
       mediator.publish 'matchRoute', route1, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(initialize).was.calledWith params, 'test1'
         expect(action).was.calledWith params, 'test1'
         expect(historyURL).was.calledWith params
@@ -171,10 +184,15 @@
         done()

     it 'should save the controller, action, params and url', (done) ->
-      # Now route to Test2Controller
+
+      # Call one route
+      mediator.publish 'matchRoute', route1, params, routeOptions
+
+      # Now open another route
       mediator.publish 'matchRoute', route2, params, routeOptions

-      test2Loaded ->
+      # Check that previous route is saved
+      loadTest2ControllerAndExecute ->
         d = dispatcher
         expect(d.previousControllerName).to.be 'test1'
         expect(d.currentControllerName).to.be 'test2'
@@ -186,12 +204,14 @@
         done()

     it 'should dispose inactive controllers and fire beforeControllerDispose events', (done) ->
+      mediator.publish 'matchRoute', route2, params, routeOptions
+
       dispose = sinon.spy Test2Controller.prototype, 'dispose'

       # Route back to Test1Controller
       mediator.publish 'matchRoute', route1, params, routeOptions

-      test2Loaded ->
+      loadTest2ControllerAndExecute ->
         expect(dispose).was.calledWith params, 'test1'

         dispose.restore()
@@ -199,13 +219,15 @@
         done()

     it 'should fire beforeControllerDispose events', (done) ->
+      mediator.publish 'matchRoute', route1, params, routeOptions
+
       beforeControllerDispose = sinon.spy()
       mediator.subscribe 'beforeControllerDispose', beforeControllerDispose

       # Now route to Test2Controller
       mediator.publish 'matchRoute', route2, params, routeOptions

-      test2Loaded ->
+      loadTest2ControllerAndExecute ->
         expect(beforeControllerDispose).was.called()
         passedController = beforeControllerDispose.lastCall.args[0]
         expect(passedController).to.be.a Test1Controller
@@ -216,13 +238,15 @@
         done()

     it 'should publish startupController events', (done) ->
+      mediator.publish 'matchRoute', route2, params, routeOptions
+
       startupController = sinon.spy()
       mediator.subscribe 'startupController', startupController

       # Route back to Test1Controller
       mediator.publish 'matchRoute', route1, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         passedEvent = startupController.lastCall.args[0]
         expect(passedEvent).to.be.an 'object'
         expect(passedEvent.controller).to.be.a Test1Controller
@@ -240,10 +264,10 @@
       action     = sinon.spy proto, 'show'
       historyURL = sinon.spy proto, 'historyURL'

-      mediator.publish '!startupController', 'test1', 'show', params,
-        routeOptions
-
-      test1Loaded ->
+      _(2).times ->
+        mediator.publish '!startupController', 'test1', 'show', params, routeOptions
+
+      loadTest1ControllerAndExecute ->
         expect(initialize).was.calledWith params, 'test1'
         expect(action).was.calledWith params, 'test1'
         expect(historyURL).was.calledWith params
@@ -270,7 +294,7 @@
       mediator.publish '!startupController', 'test1', 'show', params,
         routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(spy).was.calledWith "test1/#{params.id}", routeOptions

         mediator.unsubscribe '!router:changeURL', spy
@@ -285,7 +309,7 @@
       mediator.publish '!startupController', 'test1', 'show', params,
         routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(spy).was.calledWith routeOptions.path, routeOptions

         mediator.unsubscribe '!router:changeURL', spy
@@ -293,15 +317,23 @@
         done()

     it 'should support redirection to a URL', (done) ->
-      proto = Test1Controller.prototype
-      action = sinon.spy proto, 'redirectToURL'
+
+      # Open a route to check if previous controller info is correct after
+      # redirection
+
+      mediator.publish 'matchRoute', route1, params, routeOptions
+      refreshParams()
+
+      action = sinon.spy Test1Controller.prototype, 'redirectToURL'

       startupController = sinon.spy()
       mediator.subscribe 'startupController', startupController

+      # Open another route that redirects somewhere
+
       mediator.publish 'matchRoute', redirectToURLRoute, params, routeOptions

-      test1Loaded ->
+      loadTest1ControllerAndExecute ->
         expect(action).was.calledWith params, 'test1'

         # Don’t expect that the new controller was called
@@ -315,7 +347,7 @@
         expect(d.currentParams).not.to.be params
         expect(d.url).not.to.be "test1/#{params.id}"

-        expect(startupController).was.notCalled()
+        expect(startupController).was.calledOnce()

         mediator.unsubscribe 'startupController', startupController
         action.restore()
@@ -323,28 +355,24 @@
         done()

     it 'should support redirection to a controller action', (done) ->
-      proto = Test1Controller.prototype
-      redirectAction = sinon.spy proto, 'redirectToController'
-
-      proto = Test2Controller.prototype
-      targetAction = sinon.spy proto, 'show'
+      redirectAction = sinon.spy Test1Controller.prototype, 'redirectToController'
+      targetAction = sinon.spy Test2Controller.prototype, 'show'

       startupController = sinon.spy()
       mediator.subscribe 'startupController', startupController

       # Redirects from Test1Controller to Test2Controller
-      mediator.publish 'matchRoute', redirectToControllerRoute, params,
-        routeOptions
+      mediator.publish 'matchRoute', redirectToControllerRoute, params, routeOptions

       # Double async module loading to trick Require.js
-      test1Loaded -> test2Loaded ->
-        expect(redirectAction).was.calledWith params, 'test1'
-        expect(targetAction).was.calledWith params, 'test1'
+      loadTest1ControllerAndExecute -> loadTest2ControllerAndExecute ->
+        expect(redirectAction).was.calledWith params, null
+        expect(targetAction).was.calledWith params, null

         # Expect that the new controller was called because this does not require
         # the router but the controller to fire a !startupController event
         d = dispatcher
-        expect(d.previousControllerName).to.be 'test1'
+        expect(d.previousControllerName).to.be null
         expect(d.currentControllerName).to.be 'test2'
         expect(d.currentController).to.be.a Test2Controller
         expect(d.currentAction).to.be 'show'
@@ -364,11 +392,10 @@
       expect(dispatcher.dispose).to.be.a 'function'
       dispatcher.dispose()

-      proto = Test1Controller.prototype
-      initialize = sinon.spy proto, 'initialize'
-      mediator.publish 'matchRoute', route1, params, routeOptions
-
-      test1Loaded ->
+      initialize = sinon.spy Test1Controller.prototype, 'initialize'
+      mediator.publish 'matchRoute', route1, params, routeOptions
+
+      loadTest1ControllerAndExecute ->
         expect(initialize).was.notCalled()

         expect(dispatcher.disposed).to.be true
@@ -386,3 +413,219 @@
       expect(derivedDispatcher).to.be.a Dispatcher

       derivedDispatcher.dispose()
+
+    describe 'Before actions', ->
+
+      describe "General behavior", ->
+
+        route = controller: 'test_before_actions', action: 'show'
+
+        class TestBeforeActionsController extends Controller
+
+          historyURL: (params) ->
+            'test_before_actions/' + (params.id or '')
+
+          beforeAction:
+            show: ->
+            index: ->
+
+          show: (params, oldControllerName) ->
+
+          index: (params, oldControllerName) ->
+
+
+        # Define a test controller AMD modules
+        testBeforeActionsModule = 'controllers/test_before_actions_controller'
+        define testBeforeActionsModule, -> TestBeforeActionsController
+
+        # Helpers for asynchronous tests
+        loadBeforeActionsAndExecute = (callback) -> require [testBeforeActionsModule], callback
+
+        it 'should not run executeAction directly if before actions are present', (done) ->
+          executeAction = sinon.spy dispatcher, 'executeAction'
+          executeBeforeActionChain = sinon.mock(dispatcher).expects 'executeBeforeActionChain'
+
+          mediator.publish 'matchRoute', route, params, routeOptions
+
+          loadBeforeActionsAndExecute ->
+            expect(executeAction.called).to.not.be.ok()
+            expect(executeBeforeActionChain.getCall(0).args[0]).to.be.a TestBeforeActionsController
+
+            executeAction.restore()
+            executeBeforeActionChain.verify()
+
+            done()
+
+        it 'should call executeAction after with exactly the same arguments', (done) ->
+          executeAction = sinon.mock(dispatcher).expects 'executeAction'
+
+          mediator.publish 'matchRoute', route, params, routeOptions
+
+          loadBeforeActionsAndExecute ->
+            args = executeAction.getCall(0).args
+
+            expect(args).to.have.length 5
+            expect(args[0]).to.be.a TestBeforeActionsController
+            expect(args[1]).to.be 'test_before_actions'
+            expect(args[2]).to.be 'show'
+            expect(args[3]).to.be.an 'object'
+            expect(args[4]).to.be.an 'object'
+
+            executeAction.verify()
+
+            done()
+
+      describe '#executeBeforeActionChain', ->
+
+        it "should run all defined before actions when running an action", ->
+          called = []
+
+          class TestController extends Controller
+
+            historyURL: (params) ->
+              'test_before_actions/' + (params.id or '')
+
+            beforeAction:
+              show: -> called.push 'showBeforeAction'
+              'show*': 'beforeShow'
+              create: -> called.push 'createBeforeAction'
+
+            show: ->
+
+            create: ->
+
+            beforeShow: ->
+              called.push 'showWildcardBeforeAction'
+
+          controller = new TestController()
+
+          dispatcher.executeBeforeActionChain controller, 'test', 'show', params, routeOptions
+
+          expect(called).to.have.length 2
+          expect(called).to.contain 'showBeforeAction'
+          expect(called).to.contain 'showWildcardBeforeAction'
+
+          called = []
+
+          dispatcher.executeBeforeActionChain controller, 'test', 'create', params, routeOptions
+
+          expect(called).to.have.length 1
+          expect(called).to.contain 'createBeforeAction'
+
+
+        it "should run all before actions of the whole prototype chain in correct order", ->
+          BaseController = Controller.extend
+            historyURL: -> 'foo'
+
+            beforeAction:
+              '.*': 'loadSession'
+
+            loadSession: ->
+              userModel = isAdmin: -> true
+
+          AdminController = BaseController.extend
+            beforeAction:
+              '.*': 'checkAdminPrivileges'
+
+            checkAdminPrivileges: (params, userModel) ->
+              unless userModel.isAdmin()
+                @redirectTo '500'
+
+          UserBanningController = AdminController.extend
+            beforeAction:
+              'index': 'loadUsers'
+
+            index: ->
+            loadUsers: ->
+
+          loadSession = sinon.spy BaseController.prototype, "loadSession"
+          checkAdminPrivileges = sinon.spy AdminController.prototype, "checkAdminPrivileges"
+          loadUsers = sinon.spy UserBanningController.prototype, "loadUsers"
+
+          controller = new UserBanningController()
+          dispatcher.executeBeforeActionChain controller, 'user_banning', 'index', params, routeOptions
+
+          expect(loadSession).was.called()
+          expect(checkAdminPrivileges).was.called()
+          expect(loadUsers).was.called()
+
+          expect(loadSession.calledBefore(checkAdminPrivileges)).to.be.ok
+          expect(checkAdminPrivileges.calledBefore(loadUsers)).to.be.ok
+
+        it "should throw an error if a before action method isn't a function or a string", ->
+
+          class BrokenBeforeActionController extends Controller
+            historyURL: -> 'foo'
+            beforeAction:
+              index: new Date()
+            index: ->
+
+          controller = new BrokenBeforeActionController()
+
+          failFn = -> dispatcher.executeBeforeActionChain controller, 'broken_before_action', 'index', params, routeOptions
+
+          expect(failFn).to.throwError()
+
+
+        it 'should handle sync. before actions then pass the params and the returned value', ->
+          previousBeforeActionReturnValueToCheck = null
+
+          class BeforeActionChainController extends Controller
+
+            historyURL: -> 'foo'
+
+            beforeAction:
+              '.*': (params) ->
+                params.bar = "qux"
+                'foo' # This return value should be passed to next before action in the chain
+              'show': (params, previousBeforeActionReturnValue) ->
+                previousBeforeActionReturnValueToCheck = previousBeforeActionReturnValue
+
+            show: ->
+
+           controller = new BeforeActionChainController()
+           dispatcher.executeBeforeActionChain controller, 'before_action_chain', 'show', params, routeOptions
+           expect(params.bar).to.be 'qux'
+
+           # This is done here to ensure the method before actions are actually run synchronous
+           # and not asynchronous.
+           expect(previousBeforeActionReturnValueToCheck).to.equal 'foo'
+
+
+        it 'should handle async. before actions, then pass the returned value', ->
+          promise =
+            done: (callback) ->
+              @callback = -> callback 'response'
+            then: ->
+            resolve: -> @callback()
+
+          class AsyncBeforeActionChainController extends Controller
+            historyURL: -> 'foo'
+            beforeAction:
+              '.*': (params) ->
+                # Returning a promise here triggers asynchronous behavior.
+                promise
+              'show': (params, previousBeforeActionReturnValue) ->
+                previousBeforeActionReturnValueToCheck = previousBeforeActionReturnValue
+
+            show: ->
+
+          controller = new AsyncBeforeActionChainController()
+
+          action = sinon.spy controller, 'show'
+          beforeAction = sinon.spy controller.beforeAction, 'show'
+
+          dispatcher.executeBeforeActionChain controller, 'async_before_action_chain', 'show', params, routeOptions
+
+          expect(beforeAction.callCount).to.be 0
+          expect(action.callCount).to.be 0
+
+          # Force promise to be resolved...
+
+          promise.resolve()
+
+          expect(beforeAction.calledWith(sinon.match.object, "response")).to.be.ok()
+
+          expect(beforeAction.calledOnce).to.be.ok()
+          expect(action.calledOnce).to.be.ok()
+


model 添加到 Model 上

model 添加到 Model 上

--- lib/model.coffee
+++ lib/model.coffee
@@ -217,7 +217,9 @@
   sequelize.query "SET time_zone='+0:00'"

   _.each(utils.getModules(path, ['coffee', 'js'], ['index', 'base']), (v, k) ->
-    Models[k] = v(sequelize)
+    Model = v(sequelize)
+    Model.model = model
+    Models[k] = Model
   )

   # model 之间关系的定义


fix draggable image disappearing when dragged into contenteditable

--- plugins/image.coffee
+++ plugins/image.coffee
@@ -281,7 +281,7 @@

                 # check if the element was dragged into or within a contenteditable
                 checkOrigin: (event) ->
-                    unless $(event.srcElement).parents("[contenteditable]").length is 0
+                    unless $(event.target).parents("[contenteditable]").length is 0
                         true
                     else
                         false


Proper cleanup for IE tests

Proper cleanup for IE tests

--- test/unit/modules/toolbar.coffee
+++ test/unit/modules/toolbar.coffee
@@ -2,6 +2,7 @@

 describe('Toolbar', ->
   beforeEach( ->
+    resetContainer()
     @editorContainer = $('#editor-container').html('

@@ -15,6 +16,11 @@
     @toolbar = @quill.addModule('toolbar', { container: @toolbarContainer })
     @button = @toolbarContainer.querySelector('.ql-bold')
     @select = @toolbarContainer.querySelector('.ql-size')
+  )
+
+  afterEach((done) ->
+    clearInterval(@quill.editor.timer)
+    _.defer(done)
   )

   describe('format', ->


Added fabrication attributes

Added fabrication attributes

--- test/unit/rest_controller.coffee
+++ test/unit/rest_controller.coffee
@@ -10,7 +10,7 @@
   beforeEach: (callback) ->
     queue = new Queue(1)
     queue.defer (callback) -> MockServerModel.destroy {}, callback
-    queue.defer (callback) -> Fabricator.create(MockServerModel, 10, {id: Fabricator.uniqueId('id_'), name: Fabricator.uniqueId('mock_')}, callback)
+    queue.defer (callback) -> Fabricator.create(MockServerModel, 10, {id: Fabricator.uniqueId('id_'), name: Fabricator.uniqueId('album_'), created_at: Fabricator.dateString, updated_at: Fabricator.dateString}, callback)
     queue.await (err) -> callback(null, _.map(MockServerModel.MODELS = _.toArray(arguments).pop(), (test) -> test.attributes))

 require('../../lib/test_generators/backbone_rest')(test_parameters)


add comment

add comment

--- src/core/views/collection.coffee
+++ src/core/views/collection.coffee
@@ -98,7 +98,10 @@

       # no pending decorators? We're finished here
       unless @_pending
+
+        #load, loaded, display, displayed, etc.
         @set event, true
+
         @emit event
         callback()
         return


convenient option test

convenient option test

--- test/mocha/ephemeris.spec.coffee
+++ test/mocha/ephemeris.spec.coffee
@@ -13,6 +13,19 @@

     it "should fail, with some thrown error", ->
       ephemeris().should.throw()
+
+
+  describe "with an empty input, and convenient option", ->
+    output = {}
+    before (done) ->
+      child = ephemeris {}, convenient: true
+      child.stdout.on "data", (data) ->
+        output = json.parse data.toString()
+        done()
+
+    it "should produce some results", ->
+      output.should.have.keys '1'
+

   describe "with test json for a nativity", ->
     output = {}


Don't run video test in Safari

Don't run video test in Safari

--- test/tests/VideoLayerTest.coffee
+++ test/tests/VideoLayerTest.coffee
@@ -2,8 +2,9 @@

 	describe "Defaults", ->

-		it "should create video", ->
-
-			videoLayer = new VideoLayer video:"static/test.mp4"
-			# videoLayer._element.innerHTML.should.equal '<video src="static/test.mp4" style="width: 100%; height: 100%;"></video>'
-			videoLayer.player.src.should.equal "static/test.mp4"
+		# This allways errs in Safari
+		if not Utils.isSafari()
+			it "should create video", ->
+
+				videoLayer = new VideoLayer video:"static/test.mp4"
+				videoLayer.player.src.should.equal "static/test.mp4"


Update DockerRunner so that it can call texcount or other commands.

--- app/coffee/DockerRunner.coffee
+++ app/coffee/DockerRunner.coffee
@@ -10,11 +10,10 @@
   run: (project_id, command, directory, timeout, callback = (error) ->) ->
     logger.log project_id: project_id, command: command, directory: directory, "running docker command"

-    if command[0] != 'latexmk'
-      throw 'Invalid command'
-
     docker_cmd = (arg.replace('$COMPILE_DIR', directory).replace('$PROJECT_ID', project_id) \
       for arg in DockerRunner._baseCommand)
+    if command[0] != "latexmk"
+      docker_cmd.push "--entrypoint=#{command[0]}"
     docker_cmd.push Settings.clsi?.docker?.image or "texlive"
     docker_cmd = docker_cmd.concat (arg.replace('$COMPILE_DIR', '/source') for arg in command.slice(1))


Check collection owner for favourites (#3690)

--- app/collections/show-list.cjsx
+++ app/collections/show-list.cjsx
@@ -49,8 +49,8 @@
     return hasPermission

   isFavorite: (project) ->
-    if @props.collection.favorite
-      @setState isFavorite: @props.collection.favorite
+    if @props.collection.favorite and @props.collection.links.owner.id is @props.user.id
+      @setState isFavorite: true
     else if @props.user?
       query = {
         favorite: true


Specify mismatchTolerance: 0.005 of PhantomCSS

--- spec/diff/diff-runner.coffee
+++ spec/diff/diff-runner.coffee
@@ -3,6 +3,7 @@
   screenshotRoot: './spec/diff/screenshots'
   failedComparisonsRoot: './spec/diff/failures'
   libraryRoot: './node_modules/phantomcss'
+  mismatchTolerance: 0.005

 casper.start('./spec/diff/fixtures/defaults.html')
 casper.viewport(1200, 800)
@@ -14,4 +15,3 @@
 casper.run ->
   console.log("\nTHE END.")
   phantom.exit(phantomcss.getExitStatus())
-


:memo: Fix typo

:memo: Fix typo

--- spec/text-buffer-spec.coffee
+++ spec/text-buffer-spec.coffee
@@ -1391,7 +1391,7 @@
       runs ->
         expect(buffer.isModified()).toBeFalsy()

-    it "returns false when constructing an empty buffer with no path and loading it", ->
+    it "returns false when constructing an empty buffer with no path and not loading it", ->
       buffer.destroy()
       buffer = new TextBuffer()
       expect(buffer.isModified()).toBeFalsy()


:art: refactor spec as preperation

:art: refactor spec as preperation

--- spec/text-object-spec.coffee
+++ spec/text-object-spec.coffee
@@ -843,6 +843,9 @@
           selectedBufferRange: [[10, 0], [27, 0]]

   describe 'Fold', ->
+    rangeForRows = (startRow, endRow) ->
+      [[startRow, 0], [endRow + 1, 0]]
+
     beforeEach ->
       waitsForPromise ->
         atom.packages.activatePackage('language-coffee-script')
@@ -853,21 +856,23 @@
       atom.packages.deactivatePackage('language-coffee-script')

     describe 'inner-fold', ->
+
       it 'select fold row range except start row', ->
         set cursor: [13, 0]
-        ensure 'viz', selectedBufferRange: [[10, 0], [26, 0]]
+        ensure 'viz', selectedBufferRange: rangeForLine(10, 25)

       it 'select fold row range except start row', ->
         set cursor: [19, 0]
-        ensure 'viz', selectedBufferRange: [[19, 0], [24, 0]]
+        ensure 'viz', selectedBufferRange: rangeForLine(19, 23)
+
     describe 'a-fold', ->
       it 'select fold row range', ->
         set cursor: [13, 0]
-        ensure 'vaz', selectedBufferRange: [[9, 0], [26, 0]]
+        ensure 'vaz', selectedBufferRange: rangeForLine(9, 25)

       it 'select fold row range', ->
         set cursor: [19, 0]
-        ensure 'vaz', selectedBufferRange: [[18, 0], [24, 0]]
+        ensure 'vaz', selectedBufferRange: rangeForLine(18, 23)

   # Although following test picks specific language, other langauages are alsoe supported.
   describe 'Function', ->


Resource query now returns the promise.

Resource query now returns the promise.

--- app/javascripts/subject.coffee
+++ app/javascripts/subject.coffee
@@ -73,7 +73,7 @@
             get: ->
               # Create on demand.
               if not @_modelings?
-                @_modelings = Modelings.collect(this)
+                @_modelings = Modeling.collect(this)
               @_modelings

         # Add the isMultiSession method.
@@ -126,7 +126,7 @@
       #   extended subject REST objects array
       query: (condition) ->
         # Fetch the subjects which match the criteria.
-        Resources.Subject.query(select).$promise.then (result) ->
+        Resources.Subject.query(condition).then (result) ->
           # Extend the result objects.
           result.map(extend)
   ]


Fix window.URL.createObjectURL is returning undefined in Chrome

--- lib/shims.coffee
+++ lib/shims.coffee
@@ -5,7 +5,7 @@
 SessionDescription = window.mozRTCSessionDescription or window.RTCSessionDescription
 MediaStream = window.MediaStream or window.webkitMediaStream
 getUserMedia = navigator.mozGetUserMedia or navigator.getUserMedia or navigator.webkitGetUserMedia or navigator.msGetUserMedia
-URL = window.URL or window.webkitURL or window.msURL or window.oURL
+URL =  window.webkitURL or window.URL or window.msURL or window.oURL

 if getUserMedia?
   getUserMedia = getUserMedia.bind navigator
@@ -235,4 +235,4 @@
 this.frames.push({image:a,duration:b||this.duration})};i.prototype.compile=function(){return new g(this.frames.map(function(a){var b=l(h(atob(a.image.slice(23))));b.duration=a.duration;return b}))};return{Video:i,fromImageArray:function(a,b){return g(a.map(function(a){a=l(h(atob(a.slice(23))));a.duration=1E3/b;return a}))},toWebM:g}}();
 `

-module.exports = shim()+module.exports = shim()


Attempting to fix G+

Attempting to fix G+

--- app/lib/services/google.coffee
+++ app/lib/services/google.coffee
@@ -1,8 +1,8 @@
 module.exports = initializeGoogle = ->
-  onGPlusLoaded = ->
+  window.onGPlusLoaded = ->
     Backbone.Mediator.publish "gapi-loaded"
     return
-  signinCallback = (authResult) ->
+  window.signinCallback = (authResult) ->
     Backbone.Mediator.publish "gplus-logged-in", authResult  if authResult["access_token"]
     return
   (->


dashboard: newly added tab is deleted

dashboard: newly added tab is deleted

--- client/admin/lib/views/moderation/topicmoderationview.coffee
+++ client/admin/lib/views/moderation/topicmoderationview.coffee
@@ -25,12 +25,10 @@
     @addSubView @tabView = new KDTabView hideHandleCloseIcons: yes

     @tabView.addPane @allTopicsPane         = new KDTabPaneView name: 'All Topics'
-    @tabView.addPane @newlyTopicsPane       = new KDTabPaneView name: 'Newly Topics'
     @tabView.addPane @deletedTopicsPane     = new KDTabPaneView name: 'Deleted Topics'

     @tabView.showPaneByIndex 0

-    @allTopicsPane.addSubView       new TopicCommonView {}, @getData()
-    @newlyTopicsPane.addSubView     new TopicCommonView {}, @getData()
-    @deletedTopicsPane.addSubView   new TopicCommonView {}, @getData()
+    @allTopicsPane.addSubView       new TopicCommonView { typeConstant: "topic" }, @getData()
+    @deletedTopicsPane.addSubView   new TopicCommonView { typeConstant: "linkedtopic" }, @getData()


Hackathon: added additional prizes section

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -436,6 +436,22 @@

You will retain full ownership and rights to your creation. Be super awesome and have fun! Get
         creative, meet new people, make new friends and problem solve with them.

+

+
Additional Prizes

+

+

+

+
+
Teams with an account on oDesk.com or Elance.com get a chance to win an extra $2,500.

+

+

+

+

+
+
Paypal is sponsoring an additional $2,500 in prize money. Details on how to win this additional amount are coming soon.

+

+

+

What is the schedule?


hackathon: changed mashape link to include their tracking params

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -443,7 +443,7 @@

-
Mashape will reward an additional $1,000 to the team that makes the best use of their API.

+
Mashape will reward an additional $1,000 to the team that makes the best use of their API.


Fix a bug when a machine is down and is not a master

--- admin/static/coffee/dashboard.coffee
+++ admin/static/coffee/dashboard.coffee
@@ -87,11 +87,12 @@
                                 name: machines.get(issue.get('location')).get('name')
                             status.machines_with_disk_problems.push(new_machine)
                         when "MACHINE_DOWN"
-                            status.masters_offline.push
-                                machine_id: issue.get('victim')
-                                machine_name: machines.get(issue.get('victim')).get('name')
-                                namespace_name: masters[issue.get('victim')].name
-                                namespace_id: masters[issue.get('victim')].id
+                            if masters[issue.get('victim')]?
+                                status.masters_offline.push
+                                    machine_id: issue.get('victim')
+                                    machine_name: machines.get(issue.get('victim')).get('name')
+                                    namespace_name: masters[issue.get('victim')].name
+                                    namespace_id: masters[issue.get('victim')].id
                         when "NAME_CONFLICT_ISSUE"
                             status.num_conflicts_name++
                         when "VCLOCK_CONFLICT"


FIX: Correct error message on failed enrol student

--- src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee
+++ src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee
@@ -24,14 +24,13 @@
     # get tutorial_id from tutorial_name
     Project.create {unit_id: unit.id, student_num: student_id, tutorial_id: if tutorial then tutorial.id else null },
       (project) ->
-        if not unit.studentEnrolled project.project_id
+        if ! unit.studentEnrolled project.project_id
           unit.addStudent project
           alertService.add("success", "Student enrolled", 2000)
           $modalInstance.close()
         else
           alertService.add("danger", "Student is already enrolled", 2000)
           $modalInstance.close()
-
       , (response) ->
-        alertService.add("danger", "Unable to find student. Please check the student ID, and ensure the student has logged in at least once already to ensure they have an account.", 6000)
+        alertService.add("danger", "Error enrolling student: #{response.data.error}", 6000)
 )


Fixed Kitteh folder link

Fixed Kitteh folder link

--- app/pages/lab/how-to-page.cjsx
+++ app/pages/lab/how-to-page.cjsx
@@ -147,7 +147,7 @@
       You'll need to have all of your images in a single folder along with a manifest. The manifest file lists
       all the images we want to upload and links each image to additional information, also called metadata,
       such as date, time, and photographer. Read more about the manifest in the subsequent "Details" sections,
-      but for now we've done this for you in the ["Kitteh" folder]([https://drive.google.com/folderview?id=0B9JCiuF48lmgfndFZjdhTVRtcHEtQ3haY2ZYQjFmMW4wb0VWdnpiakZ4OVIyWlRjdXpiTVk&usp=sharing]).
+      but for now we've done this for you in the ["Kitteh" folder](https://drive.google.com/folderview?id=0B9JCiuF48lmgfndFZjdhTVRtcHEtQ3haY2ZYQjFmMW4wb0VWdnpiakZ4OVIyWlRjdXpiTVk&usp=sharing).

       Click on the "Upload Subjects" box and navigate to the "Kitteh" folder you downloaded. The easiest thing
       to do is simply hit _cmd + a_ (on Mac, or _ctrl + a_ on Windows) to select everything in that folder.


Bind callback to instance

Bind callback to instance

--- lib/observejs/app/assets/javascripts/observejs/creator.js.coffee
+++ lib/observejs/app/assets/javascripts/observejs/creator.js.coffee
@@ -22,7 +22,7 @@
       observer = new ObserveJS.Observer(el.instance)

       el.instance.observe = (observer, key, callback) ->
-        observer.bind(key, callback)
+        observer.bind(key, callback.bind(el.instance))

       el.instance.observe = el.instance.observe.bind(el.instance, observer)
       el.instance.observe.observer = observer


Update local setup to alert on command execution

--- app/scripts/local-setup.coffee
+++ app/scripts/local-setup.coffee
@@ -9,16 +9,17 @@
   class LocalSetup
     constructor: ->
       @loadCommands()
-      App.on "execute.commands", @executeCommand.bind(this)
+      App.on "fetch.commands", @fetchCommand.bind(this)

     loadCommands: ->
       $.getJSON("http://api.backtick.io/commands")
         .success((response) => App.trigger "load.commands", response)
         .error(console.log.bind(console, "Error fetching commands"))

-    executeCommand: (command) ->
-       $.getScript(command.src)
-        .success(App.trigger.bind(App, "executed.commands", command))
-        .error(App.trigger.bind(App, "executionError.commands", command))
+    fetchCommand: (command) ->
+      setTimeout ->
+        App.trigger "fetched.commands",
+          "alert('Commands can only be executed when running as an extension.')"
+      , 0

   new LocalSetup


more tests + testability improvements

more tests + testability improvements

--- tests/coffee/storage.test.coffee
+++ tests/coffee/storage.test.coffee
@@ -56,6 +56,20 @@
     captor.value()
     td.verify fs.unlink '/rootDir/myDomain/.bucket1.copy.lock', 'mycallback'

+  it 'should report the size of a storage bucket when /storage/size is invoked', ->
+    agent = td.object ['on']
+    cb = td.function()
+    captor = td.matchers.captor()
+    storage agent, null, dataDir: '/rootDir', domain: 'myDomain', remotefsUrl: 'remotefsUrl'
+    td.verify agent.on '/storage/size', captor.capture()
+    captor.value {name: 'bucket1'}, null, cb
+    td.verify fs.writeFile '/rootDir/myDomain/.bucket1.size.lock', td.matchers.anything(), captor.capture()
+    captor.value()
+    td.verify storageLib.remoteFs 'remotefsUrl', 'du', {dir: '/myDomain/bucket1'}, captor.capture()
+    captor.value null, {size: 1234}
+    td.verify fs.unlink '/rootDir/myDomain/.bucket1.size.lock', captor.capture()
+    captor.value()
+    td.verify cb null, {name: 'bucket1', size: 1234}

   it 'should periodically publish data storage statistics to mqtt', ->
     agent = td.object ['on']


Added sayLabelOptions groundLayer:boolean to put texts on the game layer (#6537)

--- app/lib/surface/Lank.coco.coffee
+++ app/lib/surface/Lank.coco.coffee
@@ -658,7 +658,8 @@
       d.updatePosition()

   addLabel: (name, style, labelOptions={}) ->
-    @labels[name] ?= new Label sprite: @, camera: @options.camera, layer: @options.textLayer, style: style, labelOptions: labelOptions
+    layer = if labelOptions.groundLayer then @options.groundLayer else @options.textLayer
+    @labels[name] ?= new Label sprite: @, camera: @options.camera, layer: layer, style: style, labelOptions: labelOptions
     @labels[name]

   addMark: (name, layer, thangType=null) ->


--- app/lib/surface/Lank.ozar.coffee
+++ app/lib/surface/Lank.ozar.coffee
@@ -654,7 +654,8 @@
       d.updatePosition()

   addLabel: (name, style, labelOptions={}) ->
-    @labels[name] ?= new Label sprite: @, camera: @options.camera, layer: @options.textLayer, style: style, labelOptions: labelOptions
+    layer = if labelOptions.groundLayer then @options.groundLayer else @options.textLayer
+    @labels[name] ?= new Label sprite: @, camera: @options.camera, layer: layer, style: style, labelOptions: labelOptions
     @labels[name]

   addMark: (name, layer, thangType=null) ->


Remove unused route

Remove unused route

--- app/assets/javascripts/rglossa/states/search/cwb/regex_route.coffee
+++ app/assets/javascripts/rglossa/states/search/cwb/regex_route.coffee
@@ -1,38 +0,0 @@
-App.CwbRegexRoute = Em.Route.extend
-
-  setupController: (controller, model) ->
-    # Now that we know we will use the regex interface, connect its
-    # controller to the controller handling CWB searches
-    @controllerFor('cwbSearches').set('searchTypeController', controller)
-
-  renderTemplate: ->
-    @render 'corpusInfo',
-      outlet: 'corpusInfo'
-      controller: 'corpusController'
-
-    @render 'cwbRegex',
-      outlet: 'search'
-
-  exit: ->
-    @render null,
-      outlet: 'corpusInfo'
-
-  events:
-    search: -> @transitionTo('searching')
-
-  #########################
-  # Non-routable substates
-  #########################
-
-  searching: Em.State.create
-
-    enter: ->
-      # TODO: Show spinner while we are searching
-      @controllerFor('cwbSearches').createCwbSearch()
-      App.store.commit()
-
-    #########
-    # Events
-    #########
-
-    showResult: -> @transitionTo('search.result')


fix events triggering on conversation box

--- app/elements/aozora-conversation-box/aozora-conversation-box.coffee
+++ app/elements/aozora-conversation-box/aozora-conversation-box.coffee
@@ -38,3 +38,6 @@

     option = e.model.option
     @app.story.jumpToNode getNodeById option.next
+
+    # prevent from triggering `_onTapOnBox` method
+    e.stopPropagation()


Add different curvature to relations which share source & target nodes

Based on https://github.com/zhanghuancs/D3.js-Node-MultiLinks-Node
Closes #116

--- app/frontend/javascripts/views/visualization-graph-canvas.js.coffee
+++ app/frontend/javascripts/views/visualization-graph-canvas.js.coffee
@@ -209,7 +209,10 @@
       # Add relation to data_relations_visibles array if both nodes exist and are visibles
       if d.source and d.target and d.source.visible and d.target.visible
         @data_relations_visibles.push d
-        @linkedByIndex[d.source_id+','+d.target_id] = true
+        @addRelationToLinkedByIndex d.source_id, d.target_id
+
+    # Add linkindex to relations
+    @setLinkIndex()

     console.log 'current nodes', @data_nodes
     console.log 'current relations', @data_relations_visibles
@@ -375,7 +378,8 @@
     if relation.source and relation.target and relation.source.visible and relation.target.visible
       console.log 'addRelationVisible'
       @data_relations_visibles.push relation
-      @linkedByIndex[relation.source_id+','+relation.target_id] = true
+      @addRelationToLinkedByIndex relation.source_id, relation.target_id
+      @setLinkIndex()

   # maybe we need to split removeVisibleRelationaData & removeRelationData
   removeRelationData: (relation) =>
@@ -393,6 +397,33 @@
     index = @data_relations_visibles.indexOf relation
     if index != -1
       @data_relations_visibles.splice index, 1
+
+  addRelationToLinkedByIndex: (source, target) ->
+    # count number of relations between 2 nodes
+    @linkedByIndex[source+','+target] = ++@linkedByIndex[source+','+target] || 1
+
+  # Add a linkindex property to relations
+  # Based on https://github.com/zhanghuancs/D3.js-Node-MultiLinks-Node
+  setLinkIndex: ->
+    # Sort relations
+    @data_relations_visibles.sort (a,b) ->
+      if a.source_id > b.source_id
+        return 1
+      else if a.source_id < b.source_id
+        return -1
+      else
+        if a.target_id > b.target_id
+          return 1
+        else if a.target_id < b.target_id
+          return -1
+        else
+          return 0
+    # set linkindex attr
+    @data_relations_visibles.forEach (relation, i) =>
+      if i != 0 && @data_relations_visibles[i].source_id == @data_relations_visibles[i-1].source_id && @data_relations_visibles[i].target_id == @data_relations_visibles[i-1].target_id
+        @data_relations_visibles[i].linkindex = @data_relations_visibles[i-1].linkindex + 1
+      else
+        @data_relations_visibles[i].linkindex = 1

   addNode: (node) ->
     console.log 'addNode', node
@@ -665,7 +696,12 @@

     dx = d.target.x - d.source.x
     dy = d.target.y - d.source.y
-    dist = @parameters.relationsCurvature * Math.sqrt dx*dx + dy*dy  # Calculate distance between source & target positions
+
+    # Calculate distance between source & target positions
+    dist = @parameters.relationsCurvature * Math.sqrt dx*dx + dy*dy
+    # if there are multiple links between these two nodes, we need generate different dr for each path
+    if @linkedByIndex[d.source_id+','+d.target_id] > 1
+      dist = dist / (1 + (1/@linkedByIndex[d.source_id+','+d.target_id]) * (d.linkindex - 1))

     # Calculate relation angle in order to draw always convex arcs & avoid relation labels facing down
     d.angle = Math.atan2(dx,dy) # *(180/Math.PI) to convert to degrees


Console: HTTPS support.

Console: HTTPS support.

--- app/modules/console/index.coffee
+++ app/modules/console/index.coffee
@@ -134,10 +134,15 @@
           # If the URL is empty, nothing to do.
           return unless url

+          # Parse the URL.
+          url = parseUrl url
+
+          isSecure = url.protocol is 'https:' or url.protocol is 'wss:'
+
           # Creates the new RFB object.
           rfb = new $window.RFB {
             # Options.
-            encrypt: false
+            encrypt: isSecure
             # local_cursor: true
             target: $element[0]
             wsProtocols: ['chat']
@@ -150,9 +155,6 @@
             onUpdateState: (args...) -> console.log args
           }

-          # Parse the URL.
-          url = parseUrl url
-
           path = url.path
           # Leading '/' is added by noVNC.
           if path[0] is '/'
@@ -161,7 +163,7 @@
           # Connects.
           rfb.connect(
             url.hostname
-            url.port || (if url.protocol is 'https:' then 443 else 80)
+            url.port || (if isSecure then 443 else 80)
             '' # TODO: comment.
             path
           )


#233 fix

#233 fix

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -335,7 +335,7 @@

 getFirstCharacterPositionForBufferRow = (editor, row) ->
   from = [row, 0]
-  getEndPositionForPattern(editor, from, /\s*/, containedOnly: true)
+  getEndPositionForPattern(editor, from, /\s*/, containedOnly: true) or from

 # [FIXME] Fix naming, this return bufferPosition
 getFirstCharacterBufferPositionForScreenRow = (editor, screenRow) ->


Hide achievements popup (#29)

Hide achievements popup (#29)

--- app/views/core/RootView.coffee
+++ app/views/core/RootView.coffee
@@ -58,6 +58,8 @@
     #return if @isIE()  # Some bugs in IE right now, TODO fix soon!  # Maybe working now with not caching achievement fetches in CocoModel?
     return if window.serverConfig.picoCTF
     return if achievement.get('hidden')
+
+    return # Hiding legacy achievement popups in Ozaria
     new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement

   handleNewAchievements: (e) ->


Added comments.

Added comments.

--- server/publication.coffee
+++ server/publication.coffee
@@ -31,6 +31,8 @@
     @processed = true
     Publications.update @_id, $set: processed: @processed

+  # A subset of public fields used for search results to optimize transmission to a client
+  # This list is applied to PUBLIC_FIELDS to get a subset
   @PUBLIC_SEARCH_RESULTS_FIELDS: ->
     [
       'slug'
@@ -41,6 +43,7 @@
       'numberOfPages'
     ]

+  # A set of fields which are public and can be published to the client
   @PUBLIC_FIELDS: ->
     fields:
       slug: 1


merging

merging

--- server/search_core.coffee
+++ server/search_core.coffee
@@ -39,7 +39,7 @@
     return console.error("could not connect to postgres", err)  if err

     # should give more weight to the occurrrence
-    statement = "SELECT word, similarity(word, $1)  AS similarity FROM word_table WHERE word % $1 ORDER BY similarity DESC LIMIT 3"
+    statement = "SELECT word, similarity(word, $1) AS similarity FROM word_table WHERE word % $1 ORDER BY similarity DESC LIMIT 3"

     client.query statement, [req.params.query], (err, result) ->
       done()


Add bug

Add bug

--- src/scripts/models/content.coffee
+++ src/scripts/models/content.coffee
@@ -104,6 +104,7 @@
       if @getPageNumber(node) < page
         @set('page', --page)

+      # BUG: Remove all nodes from the toc for a subcollection
       @get('toc').remove(node)
       node.get('parent').get('contents').remove(node)


sync validation mostly done

sync validation mostly done

--- src/shared/component/login.coffee
+++ src/shared/component/login.coffee
@@ -5,6 +5,7 @@
   ComponentNavigation
   login
   validateLogin
+  classNames
   # getCursorStates
 ) ->
   React.createClass
@@ -21,18 +22,31 @@
       updated = React.addons.update this.state, command
       errors = validateLogin updated.page.data
       console.log 'errors', errors
-      this.update command
+      this.update
+        page:
+          data: {$merge: delta}
+          errors: {$set: errors}
+
     handleClick: (event) ->
       event.preventDefault()
+      that = this
       console.log 'ComponentLogin', 'handleClick', this.state.page
-      login(this.state.page.data)
+      if that.hasErrors()
+        this.update
+          page:
+            hasBeenClicked: {$set: true}
+        return
+      login(that.state.page.data)
         .then (response) ->
           console.log 'success', response
         .fail (error) ->
           console.log 'error', error
     componentWillMount: ->
       console.log 'ComponentLogin', 'componentWillMount'
-      this.update {page: {data: {$set: {}}}}
+      this.update
+        page:
+          data: {$set: {}}
+          errors: {$set: validateLogin {}}
     componentWillReceiveProps: (nextProps) ->
       # after calling `this.update()` in `handleChange` there is no guarantee
       # that `this.state` already reflects our changes.
@@ -54,12 +68,23 @@
       # errors = validateLogin nextState.page.data
       # if errors
       # this.update {page: {errors: {$set: errors}}}
+    hasSuccess: (name) ->
+      this.state.page.data[name]? and not this.state.page.errors?[name]?
+    hasError: (name) ->
+      this.hasBeenClicked() and this.state.page.errors?[name]?
+    hasFeedback: (name) ->
+      this.hasSuccess(name) or this.hasError(name)
+    hasErrors: ->
+      this.state.page.errors?
+    hasBeenClicked: ->
+      this.state.page.hasBeenClicked is true
     render: ->
       that = this
       console.log 'ComponentLogin', 'render', 'this.state', this.state
+      page = that.state.page
       reactKup (k) ->
         k.div {className: 'ComponentLogin'}, ->
-          unless that.state.page.data?
+          unless page.data?
             return
           k.build ComponentNavigation
           k.div {className: 'container'}, ->
@@ -67,7 +92,14 @@
               k.div {className: 'col-md-4'}, ->
                 k.h1 'Login'
                 k.form ->
-                  k.div {className: 'form-group'}, ->
+                  k.div {
+                    className: classNames(
+                      'form-group'
+                      {'has-success': that.hasSuccess('identifier')}
+                      {'has-error': that.hasError('identifier')}
+                      {'has-feedback': that.hasFeedback('identifier')}
+                    )
+                  }, ->
                     k.label {htmlFor: 'inputIdentifier'}, 'Email or username'
                     k.input {
                       type: 'text',
@@ -75,10 +107,23 @@
                       id: 'inputIdentifier'
                       placeholder: 'Email or username'
                       name: 'identifier'
-                      value: that.state.page.data.identifier
+                      value: page.data.identifier
                       onChange: that.handleChange
                     }
-                  k.div {className: 'form-group'}, ->
+                    if that.hasSuccess('identifier')
+                      # TODO put this into its own component
+                      k.span {className: 'glyphicon glyphicon-ok form-control-feedback'}
+                    if that.hasError('identifier')
+                      # TODO put this into its own component
+                      k.span {className: 'glyphicon glyphicon-remove form-control-feedback'}
+                  k.div {
+                    className: classNames(
+                      'form-group'
+                      {'has-success': that.hasSuccess('password')}
+                      {'has-error': that.hasError('password')}
+                      {'has-feedback': that.hasFeedback('password')}
+                    )
+                  }, ->
                     k.label {htmlFor: 'inputPassword'}, 'Password'
                     k.input {
                       type: 'password',
@@ -86,11 +131,20 @@
                       id: 'inputPassword'
                       placeholder: 'Password'
                       name: 'password'
-                      value: that.state.page.data.password
+                      value: page.data.password
                       onChange: that.handleChange
                     }
-                  k.button {
-                    type: 'submit'
-                    className: 'btn btn-default'
+                    if that.hasSuccess('password')
+                      # TODO put this into its own component
+                      k.span {className: 'glyphicon glyphicon-ok form-control-feedback'}
+                    if that.hasError('password')
+                      # TODO put this into its own component
+                      k.span {className: 'glyphicon glyphicon-remove form-control-feedback'}
+                  k.a {
+                    className: classNames(
+                      'btn'
+                      'btn-default'
+                      {disabled: that.hasBeenClicked() and that.hasErrors()}
+                    )
                     onClick: that.handleClick
                   }, 'Login'


Restrict share to one item at a time

Restrict share to one item at a time

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -149,21 +149,15 @@

 ShareButton = rclass
     propTypes:
-        id     : rtypes.string
-        type   : rtypes.string # directory or file
-        on_click : rtypes.func
-
-    share_item: (e) ->
-        e.preventDefault()
-        e.stopPropagation()
-        @props.on_click()
+        id       : rtypes.string
+        on_click : rtypes.func.isRequired

     render: ->
         <Space/>
             <Button
                 bsStyle = 'default'
                 bsSize  = 'xsmall'
-                onClick = {@share_item}
+                onClick = {@props.on_click}
             >
                 <Icon name='share-square-o' /> Share...
             </Button>
@@ -171,33 +165,27 @@

 PublicButton = rclass
     propTypes:
-        id     : rtypes.string
-        data   : rtypes.object
-        type   : rtypes.string # directory or file
-
-    render_public_info_popover: ->
-        <Popover title={"This #{@props.type} is being shared publicly"} id={@props.id} >
-
-                Description: {@props.data.description}
-
-        </Popover>
+        id       : rtypes.string
+        on_click : rtypes.func.isRequired

     render: ->
         <Space/>
-            <OverlayTrigger
-                trigger   = 'click'
-                rootClose = {true}
-                overlay   = {@render_public_info_popover()}
+            <Button
+                bsStyle = 'info'
+                bsSize  = 'xsmall'
+                onClick = {@props.on_click}
             >
-                <Button
-                    bsStyle = 'info'
-                    bsSize  = 'xsmall'
-                    onClick = {(e)->e.stopPropagation()}
-                >
-                    <Icon name='bullhorn' /> Public...
-                </Button>
-            </OverlayTrigger>
+                <Icon name='bullhorn' /> Public...
+            </Button>

+
+generate_on_share_click = (name, actions) =>
+    (e) =>
+        e.preventDefault()
+        e.stopPropagation()
+        actions.set_all_files_unchecked()
+        actions.set_file_checked(name, true)
+        actions.set_file_action('share')

 FileRow = rclass
     displayName : 'ProjectFiles-FileRow'
@@ -272,15 +260,13 @@
     render_public_file_info: ->
         if @props.public_data? and @props.is_public
             <PublicButton
-                id     = {@props.name}
-                data   = {@props.public_data}
-                type   = 'file'
+                id       = {@props.name}
+                on_click = {generate_on_share_click(@props.name, @props.actions)}
             />
         else
             <ShareButton
                 id       = {@props.name}
-                on_click = {=> @props.actions.set_all_files_unchecked();@props.actions.set_file_checked(@props.name, true);@props.actions.set_file_action('share')}
-                type     = 'file'
+                on_click = {generate_on_share_click(@props.name, @props.actions)}
             />

     fullpath: ->
@@ -404,15 +390,14 @@
     render_public_directory_info: ->
         if @props.public_data? and @props.is_public
             <PublicButton
-                id     = {@props.name}
-                data   = {@props.public_data}
-                type   = 'folder'
+                id       = {@props.name}
+                on_click = {generate_on_share_click(@props.name, @props.actions)}
             />
         else
             <ShareButton
                 id       = {@props.name}
-                on_click = {=> @props.actions.set_all_files_unchecked();@props.actions.set_file_checked(@props.name, true);@props.actions.set_file_action('share')}
-                type     = 'folder'/>
+                on_click = {generate_on_share_click(@props.name, @props.actions)}
+            />

     render_time: ->
         if @props.time?


webapp/files: details around links + help for sharing files

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -1072,7 +1072,7 @@

-WIKI_SHARE_HELP_URL = 'https://doc.cocalc.com/share.html'
+WIKI_SHARE_HELP_URL = 'https://github.com/sagemathinc/cocalc/wiki/share'

 ProjectFilesActionBox = rclass
     displayName : 'ProjectFiles-ProjectFilesActionBox'
@@ -1557,7 +1557,7 @@

     render_share_defn: ->

-            Use sharing to make a file or directory visible to the world.   (If you would instead like to privately collaborate and chat with people in this project, go the Project Settings tab and "Add people to project".)
+            Use sharing to make a file or directory visible to the world.   (If you would instead like to privately collaborate and chat with people in this project, go the project Settings tab and "Add people to project".)

     set_public_file_unlisting_to: (new_value) ->
@@ -1702,10 +1702,10 @@
             <FormGroup>
             {if @props.get_total_project_quotas(@props.project_id)?.network then <Radio name="sharing_options" value="public_listed" checked={state == "public_listed"} onChange={handler} inline>
                     <Icon name='eye'/><Space/>
-                    Public (listed) - This will appear on the public share server.
+                    Public (listed) - This will appear on the public share server.
               </Radio> else <Radio disabled={true} name="sharing_options" value="public_listed" checked={state == "public_listed"} inline>
                     <Icon name='eye'/><Space/>
-                    Public (listed) - This will appear on the share server. Public (listed) is only available for projects with network enabled.
+                    Public (listed) - This will appear on the share server. Public (listed) is only available for projects with network enabled.
                 </Radio>}

               <Radio name="sharing_options" value="public_unlisted" checked={state == "public_unlisted"} onChange={handler} inline>


one green marker

one green marker

--- public/js/map.coffee
+++ public/js/map.coffee
@@ -12,6 +12,9 @@
 	coordMy = defaultCoord
 	coordFocus = coordMy

+	markerFocus = null
+	circleFocus = null
+
 	ads = []

@@ -22,28 +25,33 @@
 		.addTo map

-
-
 		map.on 'click', (e) =>
 			color = 'green'
-
+
 			coordFocus = e.latlng
 			ad =
 				location: coordinates: coordFocus
 				radius: 500
 				text: 'Novy Ad'
-			@displayAd ad, color

-			markerFocus = L.marker(ad.location.coordinates, {icon: icons.color(color)}).bindPopup(ad.text)
-			circleFocus = L.circle ad.location.coordinates, ad.radius,
-				color: "light#{color}",
-				fillColor: "light#{color}",
-				fillOpacity: 0.5
+			unless markerFocus
+				markerFocus = L.marker(ad.location.coordinates, {icon: icons.color(color)})
+				markerFocus.bindPopup(ad.text)
+				markerFocus.addTo map
+			else
+				markerFocus.setLatLng coordFocus
+				markerFocus.update

-			# circle.bindPopup "I am a circle."
+			unless circleFocus
+				circleFocus = L.circle ad.location.coordinates, ad.radius,
+					color: "light#{color}",
+					fillColor: "light#{color}",
+					fillOpacity: 0.5
+				circleFocus.addTo map
+			else
+				circleFocus.setLatLng coordFocus
+				circleFocus.update

-			markerFocus.addTo map
-			circleFocus.addTo map

 		# // var popup = L.popup();


+ free/freeSync methods

+ free/freeSync methods

--- src/abstract-iterator.coffee
+++ src/abstract-iterator.coffee
@@ -101,7 +101,7 @@
     @_indexOfKeys = -2
     @_ended = true

-  endSync: ->
+  freeSync: ->
     if @_indexOfKeys?
       return @_endKeys()
     else if @_endSync
@@ -109,6 +109,7 @@
       return @_endSync()
     else
       throw new NotImplementedError()
+  endSync: @::freeSync

   nextKeys: (callback) ->
     @_nexting = true
@@ -147,7 +148,7 @@
         callback.apply null, arguments
     @

-  end: (callback) ->
+  free: (callback) ->
     throw new InvalidArgumentError("end() requires a callback argument")  unless typeof callback is "function"
     return callback(new AlreadyEndError("end() already called on iterator"))  if @_ended
     if @_indexOfKeys?
@@ -156,3 +157,4 @@
     else
       @_ended = true
       @_end callback
+  end: @::free


add editing prop to image-set caption

add editing prop to image-set caption

--- client/apps/edit/components/content/sections/image_set/index.coffee
+++ client/apps/edit/components/content/sections/image_set/index.coffee
@@ -184,6 +184,7 @@
                         RichTextCaption {
                           item: item
                           key: 'caption-edit-' + i
+                          editing: @props.editing
                         }
                       ]
                     button {


Fix toggle/select/deselect buttons

Fix toggle/select/deselect buttons

--- app/assets/javascripts/lights.coffee
+++ app/assets/javascripts/lights.coffee
@@ -7,12 +7,12 @@
         $(this).parent().find('.update').val(this.value)

     $('#select_all_btn').on 'click', (e) ->
-        $('.toggl-switch').prop 'checked', true
+        $('.checkbox').prop 'checked', true
     $('#deselect_all_btn').on 'click', (e) ->
-        $('.toggl-switch').prop 'checked', false
+        $('.checkbox').prop 'checked', false

     $('#toggle_all_btn').on 'click', (e) ->
-        $('.toggl-switch').each () ->
+        $('.checkbox').each () ->
             $this = $(this)
             $this.prop 'checked', !$this.prop('checked')


Drop need for document ready Consistent quoting

--- app/assets/javascripts/orders.coffee
+++ app/assets/javascripts/orders.coffee
@@ -1,29 +1,28 @@
-$ ->
-  showOrderDialog = (orderId) ->
-    $.ajax
-      url: "/orders/#{orderId}/show_order_dialog"
-      type: 'POST'
-      dataType: 'json'
-      success: (response) ->
-        $("#order_id").text(response.order_id)
-        $("#date_received").text(response.order_date)
-        $("#organization_name").text(response.organization_name)
-        $("#status").text(response.status)
-        $("#edit_order_button").attr("href", "/orders/" + response.order_id + "/edit")
+showOrderDialog = (orderId) ->
+  $.ajax
+    url: "/orders/#{orderId}/show_order_dialog"
+    type: "POST"
+    dataType: "json"
+    success: (response) ->
+      $("#order_id").text(response.order_id)
+      $("#date_received").text(response.order_date)
+      $("#organization_name").text(response.organization_name)
+      $("#status").text(response.status)
+      $("#edit_order_button").attr("href", "/orders/" + response.order_id + "/edit")

-        orderDetails = JSON.parse(response.order_details)
-        html = []
-        html.push("
#{item.description}	#{item.quantity}
") for item in orderDetails
-        $("#order-details").html(html.join(""));
-        $("#order_details_modal").modal();
+      orderDetails = JSON.parse(response.order_details)
+      html = []
+      html.push("
#{item.description}	#{item.quantity}
") for item in orderDetails
+      $("#order-details").html(html.join(""));
+      $("#order_details_modal").modal();

-      error: (jqXHR, textStatus, errorThrown) ->
-        alert("Error occurred")
+    error: (jqXHR, textStatus, errorThrown) ->
+      alert("Error occurred")

-  $('.order').click (e) ->
-    e.stopPropagation()
-    orderId = $(e.target).parent().data('order-id')
-    showOrderDialog(orderId)
+$(document).on "click", ".order", (e) ->
+  e.stopPropagation()
+  orderId = $(e.target).parent().data("order-id")
+  showOrderDialog(orderId)

-  $(':input[name="status"]').change (e) ->
-    $(e.target).closest('form').submit()
+$(document).on "change", ':input[name="status"]', (e) ->
+  $(e.target).closest("form").submit()


WIP start refactoring spreadsheet for different views

sort of a mess but seems like the right approach

--- app/assets/volume/spreadsheet.coffee
+++ app/assets/volume/spreadsheet.coffee
@@ -62,6 +62,10 @@
       info

     pseudoCategory =
+      slot:
+        id: 'slot'
+        name: 'materials/session'
+        template: ['name', 'date', 'release']
       0:
         id: 0
         name: 'record'
@@ -71,18 +75,37 @@
         id: 'asset'
         name: 'file'
         not: 'No file'
+        template: ['name', 'classification', 'excerpt']
     constants.deepFreeze(pseudoCategory)
     getCategory = (c) ->
       pseudoCategory[c || 0] || constants.category[c]

     pseudoMetric =
-      id:
+      name: # slot, asset
+        id: 'name'
+        name: 'name'
+        display: ' '
+        type: 'text'
+      date: # slot
+        id: 'date'
+        name: 'test date'
+        type: 'date'
+      release: # slot
+        id: 'release'
+        name: 'release'
+      classification: # asset
+        id: 'classification'
+        name: 'classification'
+      excerpt: # asset
+        id: 'excerpt'
+        name: 'excerpt'
+      id: # record
         id: 'id'
         name: 'id'
         display: ' '
         type: 'number'
         release: constants.release.PUBLIC
-      age:
+      age: # record
         id: 'age'
         name: 'age'
         type: 'number'
@@ -102,7 +125,7 @@

         editing = $scope.editing = $attrs.edit != undefined
         top = $scope.top = 'top' of $attrs
-        assets = if $scope.assets = 'assets' of $attrs then []
+        assets = $scope.assets = 'assets' of $attrs
         id = $scope.id = $attrs.id ? if top then 'sst' else 'ss'
         limit = $attrs.limit

@@ -160,7 +183,17 @@
           populateMeasure = (m, v) ->
             arr(arr(r, m), n)[i] = v
             return
-          count = counts[i] = {}
+
+          count = counts[i] = {slot: 1}
+          c = 'slot'
+          r = records[c]
+          n = 0
+          populateMeasure('id', slot.id)
+          populateMeasure('name', slot.name)
+          if !slot.top || slot.date
+            populateMeasure('date', slot.date)
+          if !slot.top || slot.release
+            populateMeasure('release', slot.release)

           for rr in slot.records
             record = rr.record
@@ -191,10 +224,14 @@
             depends[record.id][i] = n

           if assets
-            ### jshint ignore:start #### fixed in jshint 2.5.7
-            assets[i] = (asset for assetId, asset of slot.assets)
-            ### jshint ignore:end ###
-            count.asset = assets[i].length
+            c = 'asset'
+            r = records[c]
+            for assetId, asset of slot.assets
+              n = inc(count, c)
+              populateMeasure('id', asset.id)
+              populateMeasure('name', asset.name)
+              populateMeasure('classification', asset.release)
+              populateMeasure('excerpt', asset.excerpt?)

           return

@@ -206,7 +243,7 @@
             if editing
               for m in category.template
                 arr(records[c], m)
-            metrics = _.map Object.keys(records[c]), maybeInt
+            metrics = Object.keys(records[c]).map(maybeInt)
                     .sort(byType)
             metrics.pop() # remove 'id' (necessarily last)
             metrics = _.map metrics, getMetric
@@ -226,7 +263,7 @@
               start: si
             }
           $scope.metricCols = metricCols
-          $scope.totalCols = 1 + 2*!top + metricCols.length + 3*!!assets
+          $scope.totalCols = 1 + 2*!top + metricCols.length + 3*assets
           if editing
             ### jshint ignore:start #### fixed in jshint 2.5.7
             $scope.categories = (c for ci, c of constants.category when ci not of records)
@@ -238,6 +275,8 @@
         # Call all populate functions
         populate = ->
           records = {}
+          records.slot = {id: []}
+          records.asset = {id: []} if assets
           depends = {}
           for s, i in slots
             populateSlot(i)
@@ -258,7 +297,12 @@

         # Add or replace the text contents of cell c for measure/type m with value v
         generateText = (c, m, v, assumed) ->
-          if m == 'release'
+          if m == 'name'
+            a = c.insertBefore(document.createElement('a'), c.firstChild)
+            #a.setAttribute('href', if editing then slot.editRoute() else slot.route())
+            a.className = "session icon hint-action-slot"
+            #v ?= constants.message('materials.top') if stop
+          else if m == 'release'
             cn = constants.release[v || 0]
             c.className = cn + ' release icon hint-release-' + cn
             v = ''
@@ -409,8 +453,8 @@
             generateCell(row, 'release', slot.release, id + '-release_' + i)
           for c in recordCols
             generateRecord(row, i, c)
-          if assets
-            generateAsset(row, i)
+          #if assets
+          #  generateAsset(row, i)
           return

         # Update all age displays.


Store text task value in component state (#4481)

* Store text task value in component state
Update text task value in local state while typing. Update the annotation on blur, or when the component unmounts.

* Update state when adding text tags

* Reset text task value when task changes

* Set initial text value from the annotation value

* Do a debounced annotation update onChange instead of onBlur

--- app/classifier/tasks/text/index.cjsx
+++ app/classifier/tasks/text/index.cjsx
@@ -1,5 +1,6 @@
 React = require 'react'
 createReactClass = require 'create-react-class'
+_ = require 'lodash'
 {Markdown} = require 'markdownz'
 GenericTask = require('../generic.jsx').default
 TextTaskEditor = require './editor'
@@ -58,6 +59,10 @@
   getInitialState: ->
     textareaHeight: undefined
     initOffsetHeight: undefined
+    value: @props.annotation.value
+
+  componentWillMount: ->
+    @debouncedUpdateAnnotation = _.debounce @updateAnnotation, 500

   componentDidMount: ->
     @setState initOffsetHeight: @refs.textInput.offsetHeight
@@ -65,12 +70,17 @@

   componentWillReceiveProps: (nextProps) ->
     if nextProps.task isnt @props.task
-      @setState textareaHeight: @state.initOffsetHeight, =>
+      @setState { value: '', textareaHeight: @state.initOffsetHeight }, =>
         @updateHeight()
+    if nextProps.annotation.value isnt @props.annotation.value
+      @setState value: nextProps.annotation.value

   componentDidUpdate: (prevProps) ->
     if prevProps.task isnt @props.task and @props.autoFocus is true
       @refs.textInput.focus()
+
+  componentWillUnMount: ->
+    @updateAnnotation()

   setTagSelection: (e) ->
     textTag = e.target.value
@@ -88,8 +98,7 @@
       textInBetween = textAreaValue.substring(selectionStart, selectionEnd)
       textAfter = textAreaValue.substring(selectionEnd, textAreaValue.length)
       value = textBefore + startTag + textInBetween + endTag + textAfter
-    newAnnotation = Object.assign @props.annotation, {value}
-    @props.onChange newAnnotation
+    @setState { value }

   updateHeight: ->
     @setState textareaHeight: @refs.textInput.scrollHeight + 2
@@ -101,7 +110,7 @@
           autoFocus={@props.autoFocus}
           className="standard-input full"
           ref="textInput"
-          value={@props.annotation.value}
+          value={@state.value}
           onChange={@handleChange}
           rows="1"
           style={height: @state.textareaHeight}
@@ -117,11 +126,16 @@
   handleChange: ->
     value = @refs.textInput.value

-    if @props.annotation.value?.length > value.length
+    if @state.value?.length > value.length
       @setState textareaHeight: @state.initOffsetHeight, =>
         @updateHeight()
     else
       @updateHeight()

+    @setState { value }
+    @debouncedUpdateAnnotation()
+
+  updateAnnotation: ->
+    value = @refs.textInput.value
     newAnnotation = Object.assign @props.annotation, {value}
     @props.onChange newAnnotation


Fix bug 193

Fix bug 193

--- app/coffee/controllers/issues.coffee
+++ app/coffee/controllers/issues.coffee
@@ -139,7 +139,7 @@

     regenerateTags = ->
         generateTagsFromList($scope.filtersData.statuses, $scope.constants.issueStatuses, "status", "statusTags")
-        generateTagsFromList($scope.filtersData.types, $scope.constants.types, "type", "$scope.selectedTagstypeTags")
+        generateTagsFromList($scope.filtersData.types, $scope.constants.types, "type", "typeTags")
         generateTagsFromList($scope.filtersData.severities, $scope.constants.severities, "severity", "severityTags")
         generateTagsFromList($scope.filtersData.priorities, $scope.constants.priorities, "priority", "priorityTags")
         generateTagsFromUsers($scope.filtersData.owners, "owner", "addedByTags")


don't delete opts.client_id too soon

don't delete opts.client_id too soon

--- src/smc-hub/postgres-user-query-queue.coffee
+++ src/smc-hub/postgres-user-query-queue.coffee
@@ -62,8 +62,7 @@
             changes    : undefined
             cb         : undefined
         client_id = opts.client_id
-        @_dbg("user_query(client_id='#{opts.client_id}')")
-        delete opts.client_id
+        @_dbg("user_query(client_id='#{client_id}')")
         state = @_state[client_id]
         if not state?
             state = @_state[client_id] =


Use editSession.lineForBufferRow()

Use editSession.lineForBufferRow()

Rather than the buffer directly.

--- src/app/language-mode.coffee
+++ src/app/language-mode.coffee
@@ -189,8 +189,7 @@
   #
   # Returns a {Number} of the indent level of the block of lines.
   minIndentLevelForRowRange: (startRow, endRow) ->
-    buffer = @editSession.buffer
-    indents = (@editSession.indentationForBufferRow(row) for row in [startRow..endRow] when buffer.lineForRow(row).trim())
+    indents = (@editSession.indentationForBufferRow(row) for row in [startRow..endRow] when @editSession.lineForBufferRow(row).trim())
     Math.min(indents...)

   # Indents all the rows between two buffer row numbers.


Remove unused code

Remove unused code

--- src/app/language-mode.coffee
+++ src/app/language-mode.coffee
@@ -93,22 +93,6 @@

     indentation

-  autoIndentTextAfterBufferPosition: (text, bufferPosition) ->
-    { row, column} = bufferPosition
-    stack = @tokenizedBuffer.stackForRow(row)
-    lineBeforeCursor = @buffer.lineForRow(row)[0...column]
-    if text[0] == "\n"
-      indent = @aceMode.getNextLineIndent(stack, lineBeforeCursor, @editSession.tabText)
-      text = text[0] + indent + text[1..]
-    else if @aceMode.checkOutdent(stack, lineBeforeCursor, text)
-      shouldOutdent = true
-
-    {text, shouldOutdent: false}
-
-  autoOutdentBufferRow: (bufferRow) ->
-    state = @tokenizedBuffer.stackForRow(bufferRow)
-    @aceMode.autoOutdent(state, @aceAdaptor, bufferRow)
-
   getLineTokens: (line, stack) ->
     {tokens, stack} = @grammar.getLineTokens(line, stack)


:lipstick

:lipstick

--- src/app/language-mode.coffee
+++ src/app/language-mode.coffee
@@ -22,7 +22,7 @@
       cursorBufferPosition = @editSession.getCursorBufferPosition()
       nextCharacter = @editSession.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.add([0, 1])])

-      if @isCloseBracket(text) and text == nextCharacter
+      if @isClosingBracket(text) and text == nextCharacter
         @editSession.moveCursorRight()
         false
       else if /^\s*$/.test(nextCharacter) and pairedCharacter = @pairedCharacters[text]
@@ -30,10 +30,10 @@
         @editSession.moveCursorLeft()
         false

-  isOpenBracket: (string) ->
+  isOpeningBracket: (string) ->
     @pairedCharacters[string]?

-  isCloseBracket: (string) ->
+  isClosingBracket: (string) ->
     @getInvertedPairedCharacters()[string]?

   getInvertedPairedCharacters: ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -81,7 +81,7 @@
   equals player.I.y, 21

   window.keydown.down = false
-  dinw
+  window.key

 test "should move player according to their speed", ->
   ;


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -33,5 +33,6 @@
 test "should move player back and forth when keys are changed", ->
   ;

+tes

 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

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

   window.keydown.up = false

-  equals player.I.x, 18
+  equals player.I., 18

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


Have a hash of all document root nodes.

Have a hash of all document root nodes.

--- app/clients.coffee
+++ app/clients.coffee
@@ -1,5 +1,10 @@
-($ ".current_xml").append CLIENTS = ($ "<clients>")
-($ ".current_xml").append GROUPS = ($ "<groups>")
+RECORDS = {}
+
+init_records = (name) ->
+  ($ ".current_xml").append RECORDS[name] = ($ "<#{name}>")
+
+init_records "clients"
+init_records "groups"

 @open_clients = ->
   menu_slide ->


Removendo código de teste que subiu erradamente

--- src/assets/coffee/app.coffee
+++ src/assets/coffee/app.coffee
@@ -9,7 +9,6 @@
   url_json_dados = 'https://raw.githubusercontent.com/' +
     'pedrorocha-net/eleicoes2016-rj-comparadoacoes' +
     '/master/data/candidatos_dados_processados.json'
-  url_json_dados = 'http://localhost/eleicoes2016-rj-comparadoacoes/data/candidatos_dados_processados.json'
   $http.get(url_json_dados).then (result) ->
     $scope.candidatos = result.data
     $scope.selecao1 = null


scrollToEl should include fixed navbar height

--- app/assets/javascripts/ab_admin/core/utils.js.coffee
+++ app/assets/javascripts/ab_admin/core/utils.js.coffee
@@ -76,7 +76,8 @@

 $.fn.scrollToEl = (speed='slow') ->
   return unless $(this)[0]
-  $('html, body').stop(true, true).animate({scrollTop: $(this).offset().top}, speed)
+  margin_top = $('.navbar-fixed-top').height() || 0
+  $('html, body').stop(true, true).animate({scrollTop: $(this).offset().top - margin_top}, speed)

 $.fn.toHref = ->
   $el = $(this)


Added european deadend-sign and parking signs with left/right-arrows

--- dev/europe.cson
+++ dev/europe.cson
@@ -195,6 +195,33 @@
   elements: [
     { type: 'square-rounded', color: 'blue' }
     { type: 'parking', color: 'white' }
+  ]
+
+information_parking_left:
+  category: 'information'
+  name: 'parking (left of this sign)'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'parking', color: 'white' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'translate(25%,30%) rotate(-90deg) scale(.33)'}
+  ]
+
+information_parking_right:
+  category: 'information'
+  name: 'parking (right of this sign)'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'parking', color: 'white' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'translate(25%,30%) rotate(90deg) scale(.33)'}
+  ]
+
+information_deadend:
+  category: 'information'
+  name: 'deadend'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'square-rounded', color: 'white', transform: 'translate(0,18%) scale(.15,.6)' }
+    { type: 'square-rounded', color: 'red', transform: 'translate(0,-15%) scale(.5,.15)' }
   ]

 mandatory_bicycle_pedestrian_dual_track:


Fixes swapped latlng value for map clustering

--- app/assets/javascripts/modules/maps/search.js.coffee
+++ app/assets/javascripts/modules/maps/search.js.coffee
@@ -19,7 +19,7 @@

         for pa in protected_areas
           marker = L.marker(
-            L.latLng(pa.coordinates?[0], pa.coordinates?[1]),
+            L.latLng(pa.coordinates?[1], pa.coordinates?[0]),
             { title: pa.name }
           )

@@ -34,8 +34,8 @@
     fitToResults: (points) ->
       return if points.length is 0

-      lats = points.map((p) -> parseFloat(p.coordinates[0]))
-      lons = points.map((p) -> parseFloat(p.coordinates[1]))
+      lats = points.map((p) -> parseFloat(p.coordinates[1]))
+      lons = points.map((p) -> parseFloat(p.coordinates[0]))

       maxLat = Math.max.apply(Math, lats)
       minLat = Math.min.apply(Math, lats)


more binding stuff

more binding stuff

--- app/assets/javascripts/views/comments/form.js.coffee
+++ app/assets/javascripts/views/comments/form.js.coffee
@@ -12,4 +12,8 @@

   renderAfterSubmit: ->
     console.log("submit")
+    comments = new Streamaweb.Collections.Comments()
+    comments.create({content: this.el.children[0].children[1].value})
+
+
     return false


Rebase.

Rebase.

--- tests/dom_tests/dom_tests.coffee
+++ tests/dom_tests/dom_tests.coffee
@@ -218,12 +218,6 @@
     focusInput 100
     assert.isTrue InsertMode.permanentInstance.isActive()

-  should "select the previously-focused input when count is 1", ->
-    document.getElementById("third").focus()
-    document.getElementById("third").blur()
-    focusInput 1
-    assert.equal "third", document.activeElement.id
-
   should "not trigger insert if there are no inputs", ->
     document.getElementById("test-div").innerHTML = ""
     focusInput 1


Start refactoring the save/restore test cases.

--- test/save-restore.coffee
+++ test/save-restore.coffee
@@ -13,8 +13,88 @@
     newPost: 'https://ask.metafilter.com/contribute/post.cfm'
     postPreview: 'post_preview.cfm'
     commentPreview: '/contribute/post_comment_preview.mefi#commentpreview'
-
-
+  fanFare:
+    hostname: 'fanfare.metafilter.com'
+    newPost: 'https://fanfare.metafilter.com/contribute/post.cfm'
+    postPreview:'post_preview.cfm'
+
+    # Path + hash
+    commentPreviewUrl: '/contribute/post_comment_preview.mefi#commentpreview'
+
+    commentPreviewPathname: '/contribute/post_comment_preview.mefi'
+    commentPreviewHash: '#commentpreview'
+
+    threadPathname: '/2212/Forever-Hitler-on-the-Half-Shell'
+    threadLinkId: 2122
+
+#simulateSave = (sessionStorage, site, for, mdCommentText, mdExtendedText) ->
+simulateSave = (options) ->
+  sessionStorage = options.sessionStorage ? throw new Error
+  site           = options.site           ? throw new Error
+  type           = options.type           ? throw new Error
+  mdCommentText  = options.mdCommentText  ? null
+  mdExtendedText = options.mdExtendedText ? null
+  if (type != 'comment' && type != 'post')
+    throw new Error("Bad argument 'type': #{type}")
+
+  sr = saveRestore.makeSaveRestore
+    sessionStorage: options.sessionStorage
+    location: makeLocation
+      # Location of the comment thread
+      hostname: postUrls[site].hostname
+      pathname: postUrls[site].threadPathname
+
+  # Simulate clicking the preview button
+  sr.saveMarkdownForPreview
+    mdCommentText: mdCommentText
+    mdExtendedText: mdExtendedText
+    linkId: if (type == 'comment') then postUrls[site].threadLinkId else undefined
+    # The URL we're going to, that previews the comment/post
+    formSubmitUrl:
+      if (type == 'comment') then postUrls[site].commentPreviewUrl else postUrls[site].postPreview
+
+simulateLoad = (options) ->
+  sessionStorage = options.sessionStorage ? throw new Error
+  site           = options.site           ? throw new Error
+  type           = options.type           ? throw new Error
+
+  # We have one location object for both this page and the page after submitting
+  # the preview form, since we're on a preview page. The preview page leads
+  # back to itself.
+  #
+  # TODO: But that won't be true for all the test cases
+  if (type == 'comment')
+    location = makeLocation
+      hostname: postUrls[site].hostname
+      pathname: postUrls[site].commentPreviewPathname
+      hash:     postUrls[site].commentPreviewHash
+
+    linkId = postUrls[site].threadLinkId
+  else if (type == 'post')
+    location = makeLocation
+      hostname: postUrls[site].hostName
+      pathname: postUrls[site].postPreview
+
+    linkId = undefined
+  else
+    throw new Error("Bad argument 'type': #{type}")
+
+  # Load the markdown on the preview page
+  sr = saveRestore.makeSaveRestore
+    sessionStorage: sessionStorage
+    location: location
+
+  formSubmitUrl = location.pathname + location.hash
+
+  savedDataJson = sr.restoreMarkdown
+    linkId: linkId
+    formSubmitUrl: formSubmitUrl
+
+  return {
+    storageKey: sr.getSessionStorageKey linkId, formSubmitUrl
+    savedDataJson: savedDataJson
+    sr: sr
+  }

 #doTestCase = require('../lib/test-utils').doTestCase
@@ -46,28 +126,28 @@
 # Create a mock window.location, with
 # sensible MeFi defaults.
 makeLocation = (location) ->
-  location ||= {}
+  location ?= {}

   # Abort if we accidentally used a camelcase name.
   ['hostName', 'pathName'].forEach (key) ->
     if location.hasOwnProperty(key)
       throw new Error("makeLocation: Invalid location key: #{key}")

-  location['protocol'] ||= "https:"
-  location['hostname'] ||= "www.metafilter.com"
-  location['pathname'] ||= "/"
+  location['protocol'] ?= "https:"
+  location['hostname'] ?= "www.metafilter.com"
+  location['pathname'] ?= "/"
   if !/^\//.test(location['pathname'])
     throw new Error("makeLocation: pathname needs to start with a slash: " +
       location['pathname'])
   if /#/.test(location['pathname'])
     throw new Error("makeLocation: Get that hash out of the pathname")

-  location['hash']     ||= ''
+  location['hash']     ?= ''
   if !/(^$)|^#/.test(location['hash'])
     throw new Error("makeLocation: Hash needs to be empty string or start with #: " +
       location['hash'])

-  location['href']     ||= location.protocol + location.hostname + location.pathname
+  location['href']     ?= location.protocol + location.hostname + location.pathname
   return location

 exports['test sessionStorage mock'] = (test) ->
@@ -92,6 +172,70 @@

   test.done()

+
+exports['categorize urls'] = (test) ->
+  testCases = [
+    # The new post page isn't a preview page (its preview is at a separate URL)
+    url: 'https://www.metafilter.com/contribute/post_good.mefi?pid=205463'
+    isPostPreview: false
+    isCommentPreview: false
+  ,
+    # form submit URL for the MeFi FPP page
+    url: 'post_preview.mefi'
+    isPostPreview: true
+    isCommentPreview: false
+  ,
+    # MeFi FPP preview with a hash. I haven't seen one of these
+    # in the wild, but we should be prepared for it.
+    url: 'post_preview.mefi#foobar'
+    isPostPreview: true
+    isCommentPreview: false
+  ,
+
+    # The FanFare new post page. Not a preview page.
+    url: 'https://fanfare.metafilter.com/contribute/post.cfm'
+    isPostPreview: false
+    isCommentPreview: false
+  ,
+
+    # FanFare post preview page. Its URL ends in .cfm,
+    # wereas the corresponding page on the blue ends in
+    # .mefi
+    url: 'post_preview.cfm'
+    isPostPreview: true
+    isCommentPreview: false
+  ,
+
+    # Again, match up the .cfm URL with a hash.
+    # Haven't seen one in the wild, but be prepared
+    # for one to exist.
+    url: 'post_preview.cfm#foobar'
+    isPostPreview: true
+    isCommentPreview: false
+  ,
+
+    # MeFi comment preview page
+    url: '/contribute/post_comment_preview.mefi#commentpreview'
+    isPostPreview: false
+    isCommentPreview: true
+  ,
+
+    # MeFi comment preview w/o the hash.
+    # Haven't seen one in the wild.
+    url: '/contribute/post_comment_preview.mefi'
+    isPostPreview: false
+    isCommentPreview: true
+  ]
+
+  testCases.forEach (testCase) ->
+    test.strictEqual(testCase.isPostPreview,
+                    saveRestore.isPostPreviewPage(testCase.url),
+                    testCase.url)
+    test.strictEqual(testCase.isCommentPreview,
+                    saveRestore.isCommentPreviewPage(testCase.url),
+                    testCase.url)
+
+  test.done()

 # Simulate doing one preview of a MeFi FPP
@@ -170,114 +314,32 @@

 # Simulate previewing a comment on the server side
-#
-# URL we're commenting on: https://fanfare.metafilter.com/2212/Forever-Hitler-on-the-Half-Shell
 exports['simulate server-side preview of a comment'] = (test) ->
   sessionStorage = makeSessionStorage()

-  sr = saveRestore.makeSaveRestore
-    sessionStorage: sessionStorage
-    location: makeLocation
-      hostname: 'fanfare.metafilter.com'
-      pathname: '/2212/Forever-Hitler-on-the-Half-Shell'
-
-  # Simulate clicking the preview button
   mdCommentText = "I really liked this show and/or movie!"
-  linkId = 2212
-  sr.saveMarkdownForPreview
+  simulateSave
+    sessionStorage: sessionStorage
+    site: 'fanFare'
+    type: 'comment'
     mdCommentText: mdCommentText
-    mdExtendedText: null
-    linkId: linkId
-    formSubmitUrl: '/contribute/post_comment_preview.mefi#commentpreview'

   test.strictEqual(sessionStorage.length, 1, "Comment was saved")

-
-  # Load the markdown on the preview page
-  sr = saveRestore.makeSaveRestore
-    sessionStorage: sessionStorage
-    location: makeLocation
-      hostname: 'fanfare.metafilter.com'
-      pathname: '/contribute/post_comment_preview.mefi'
-      hash:     '#commentpreview'
-
-
-  savedDataJson = sr.restoreMarkdown
-    linkId: linkId
-    formSubmitUrl: 'post_comment_preview.mefi#commentpreview'
-
+  loadResults = simulateLoad
+    sessionStorage: sessionStorage
+    site: 'fanFare'
+    type: 'comment'
+  savedDataJson = loadResults.savedDataJson
+  sr            = loadResults.sr
+  storageKey    = loadResults.storageKey

   test.strictEqual(mdCommentText, savedDataJson.comment, "comment was restored correctly")
   test.strictEqual(null,          savedDataJson.extended, "extended was restored correctly")

-  storageKey = sr.getSessionStorageKey linkId, 'post_comment_preview.mefi#commentpreview'
-  keyValue = sessionStorage.getItem(storageKey)
-  test.strictEqual(keyValue, null, "Data was removed from session storage")
+  storedItem = sessionStorage.getItem(storageKey)
+  test.strictEqual(storedItem, null, "Data was removed from session storage")
   test.strictEqual(sessionStorage.length, 0, "Nothing left in session storage")
-
-  test.done()
-
-exports['categorize urls'] = (test) ->
-  testCases = [
-    # The new post page isn't a preview page (its preview is at a separate URL)
-    url: 'https://www.metafilter.com/contribute/post_good.mefi?pid=205463'
-    isPostPreview: false
-    isCommentPreview: false
-  ,
-    # form submit URL for the MeFi FPP page
-    url: 'post_preview.mefi'
-    isPostPreview: true
-    isCommentPreview: false
-  ,
-    # MeFi FPP preview with a hash. I haven't seen one of these
-    # in the wild, but we should be prepared for it.
-    url: 'post_preview.mefi#foobar'
-    isPostPreview: true
-    isCommentPreview: false
-  ,
-
-    # The FanFare new post page. Not a preview page.
-    url: 'https://fanfare.metafilter.com/contribute/post.cfm'
-    isPostPreview: false
-    isCommentPreview: false
-  ,
-
-    # FanFare post preview page. Its URL ends in .cfm,
-    # wereas the corresponding page on the blue ends in
-    # .mefi
-    url: 'post_preview.cfm'
-    isPostPreview: true
-    isCommentPreview: false
-  ,
-
-    # Again, match up the .cfm URL with a hash.
-    # Haven't seen one in the wild, but be prepared
-    # for one to exist.
-    url: 'post_preview.cfm#foobar'
-    isPostPreview: true
-    isCommentPreview: false
-  ,
-
-    # MeFi comment preview page
-    url: '/contribute/post_comment_preview.mefi#commentpreview'
-    isPostPreview: false
-    isCommentPreview: true
-  ,
-
-    # MeFi comment preview w/o the hash.
-    # Haven't seen one in the wild.
-    url: '/contribute/post_comment_preview.mefi'
-    isPostPreview: false
-    isCommentPreview: true
-  ]
-
-  testCases.forEach (testCase) ->
-    test.strictEqual(testCase.isPostPreview,
-                    saveRestore.isPostPreviewPage(testCase.url),
-                    testCase.url)
-    test.strictEqual(testCase.isCommentPreview,
-                    saveRestore.isCommentPreviewPage(testCase.url),
-                    testCase.url)

   test.done()


Update error object returned by the API when an invalid token is supplied.

--- backend/server.coffee
+++ backend/server.coffee
@@ -23,6 +23,7 @@

 sendTokenError = (res) ->
   res.send {
+    result: 'error'
     error: 'Invalid token'
   }


(minor) whitespace.

(minor) whitespace.

--- js-library/app/js/jail_iframe/classes/performance_measurement.coffee
+++ js-library/app/js/jail_iframe/classes/performance_measurement.coffee
@@ -28,7 +28,6 @@

 FactlinkJailRoot.core_loaded_promise.then -> add_timing_event 'core_loaded'

-
 FactlinkJailRoot.core_loaded_promise
 .then( -> FactlinkJailRoot.host_loaded_promise)
 .then( -> FactlinkJailRoot.delay 1000)


Added a default route.

Added a default route.


collaborator search result fixed

collaborator search result fixed

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -66,7 +66,7 @@
       source: $('#collaborator_user_id').data('autocomplete-source')
       autoFocus: true
       messages:
-        noResults: null
+        noResults: ''
         results: ->


added jquery.trubolinks

added jquery.trubolinks

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -10,6 +10,7 @@
 # WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
 # GO AFTER THE REQUIRES BELOW.
 #
+#= require jquery.turbolinks
 # require jquery_ujs
 #= require bootstrap
 #= require pages


dont require_tree here

dont require_tree here

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -5,7 +5,6 @@
 #= require ember-data
 #= require_self
 #= require fortchan12
-#= require_tree .

 # for more details see: http://emberjs.com/guides/application/
 window.Fortchan12 = Ember.Application.create()


fix broken test

fix broken test

--- apps/search/test/client/index.coffee
+++ apps/search/test/client/index.coffee
@@ -21,7 +21,7 @@
         benv.expose $: benv.require 'jquery'
         Backbone.$ = $
         sinon.stub(Backbone, 'sync')
-        { SearchResultsView } = benv.requireWithJadeify '../../client/index.coffee', ['imageTemplate', 'resolvedImage']
+        { SearchResultsView } = benv.requireWithJadeify path.resolve(__dirname, '../../client/index.coffee'), ['imageTemplate', 'resolvedImage']
         @view = new SearchResultsView
           results: [fixture.searchResult]
         done()


Move the transition to the and set the tooltip data first

--- app/assets/javascripts/d3/stacked_bar.coffee
+++ app/assets/javascripts/d3/stacked_bar.coffee
@@ -143,12 +143,12 @@
       stacked_data = _.flatten @stack_method(@prepare_data())
       @svg.selectAll('rect.serie')
         .data(stacked_data, (s) -> s.id)
-        .transition()
         .attr('y', (d) => @series_height - @y(d.y0 + d.y))
         .attr('height', (d) => @y(d.y))
         .attr("data-tooltip-text", (d) =>
           Metric.autoscale_value d.y, @model.get('unit')
         )
+        .transition()

       # move the target lines
       @svg.selectAll('rect.target_line')


js fixes

js fixes

--- app/assets/javascripts/keg_engine2.js.coffee
+++ app/assets/javascripts/keg_engine2.js.coffee
@@ -94,7 +94,7 @@
       else
         $hidden.val("")

-      $("form").submit()
+      $("form").first().submit()

 $.fn.bind_ransack_btn = ->
   $(@).on
@@ -102,7 +102,8 @@
       $btn = $(@).data("ransack-btn")
       $el = $("[data-ransack-chk='#{$btn}']")
       $el.click()
-      $("form").submit()
+
+      $("form").first().submit()

 $.fn.bind_ransack_chk = ->
   $(@).on


Fix race condition on offline page

Fix race condition on offline page

--- assets/javascripts/views/content/offline_page.coffee
+++ assets/javascripts/views/content/offline_page.coffee
@@ -62,13 +62,17 @@
     return

   onInstallSuccess: (doc) ->
+    return unless @activated
     doc.getInstallStatus (status) =>
+      return unless @activated
       @docEl(doc).outerHTML = @renderDoc(doc, status)
       $.highlight @docEl(doc), className: '_highlight'
       @refreshLinks()
+      return
     return

   onInstallError: (doc) ->
+    return unless @activated
     el = @docEl(doc)
     el.lastElementChild.textContent = 'Error'
     return