Finish tests of ProjecAdminMilestonesController

--- test/unit/controllers/projectsControllerSpec.coffee
+++ test/unit/controllers/projectsControllerSpec.coffee
@@ -21,7 +21,18 @@
         default_question_status: 1,
         members: []
         tags: "",
-        list_of_milestones: [],
+        list_of_milestones: [
+            {
+                id: 1
+                name: "Sprint 1",
+                finish_date: "2014-02-13",
+                closed: true,
+                total_points: {},
+                closed_points: {},
+                client_increment_points: {},
+                team_increment_points: {}
+            }
+        ],
         roles: [],
         active_memberships: [],
         memberships: [],
@@ -329,14 +340,21 @@
         scope = null
         ctrl = null

-        beforeEach(inject(($rootScope, $controller, $httpBackend) ->
+        beforeEach(inject(($rootScope, $controller, $httpBackend, $q) ->
             scope = $rootScope.$new()
             routeParams = {
                 pslug: "test"
+            }
+            confirmMock = {
+                confirm: (text) ->
+                    defered = $q.defer()
+                    defered.resolve("test")
+                    return defered.promise
             }
             ctrl = $controller("ProjectAdminMilestonesController", {
                 $scope: scope,
                 $routeParams: routeParams,
+                $confirm: confirmMock
             })
             httpBackend = $httpBackend
             httpBackend.whenGET(APIURL+"/sites").respond(200, {test: "test"})
@@ -368,6 +386,20 @@

         it "should be actived", ->
             expect(ctrl.isActive("milestones")).to.be.true
+
+        it "should allow to delete a milestone", ->
+            milestone = ctrl.scope.project.list_of_milestones[0]
+            newProject = _.clone(FIXTURES.project, true)
+            newProject.list_of_milestones = []
+
+            httpBackend.expectDELETE("#{APIURL}/milestones/#{milestone.id}").respond(200)
+            httpBackend.expectGET("#{APIURL}/projects/1?").respond(200, newProject)
+
+            promise = ctrl.deleteMilestone(milestone)
+            httpBackend.flush()
+
+            promise.should.be.fulfilled.then ->
+                expect(ctrl.scope.project.list_of_milestones).to.be.lengthOf(0)

     describe "ProjecAdminMembershipsController", ->


Move misc & { transition } specs to up.change

--- spec_app/spec/javascripts/up/fragment_spec.js.coffee
+++ spec_app/spec/javascripts/up/fragment_spec.js.coffee
@@ -241,9 +241,37 @@

       describe 'with { document } option', ->

-        it 'replaces the given selector with a matching element that has the outer HTML from the given { content } string'
-
-        it 'replaces the given selector with a matching element that has the outer HTML from the given { content } element'
+        it 'replaces the given selector with a matching element that has the outer HTML from the given { document } string', asyncSpec (next) ->
+          $fixture('.before').text('old-before')
+          $fixture('.middle').text('old-middle')
+          $fixture('.after').text('old-after')
+
+          html =
+            """
+
new-before

+
new-middle

+
new-after

+            """
+
+          up.change('.middle', document: html)
+
+          next ->
+            expect($('.before')).toHaveText('old-before')
+            expect($('.middle')).toHaveText('new-middle')
+            expect($('.after')).toHaveText('old-after')
+
+        it 'replaces the given selector with a matching element that has the outer HTML from the given { document } element'
+
+        it "rejects if the selector can't be found in the given HTML string", (done) ->
+          $fixture('.foo-bar')
+          promise = up.change('.foo-bar', document: 'html without match')
+
+          u.task ->
+            promiseState(promise).then (result) =>
+              expect(result.state).toEqual('rejected')
+              expect(result.value).toMatch(/Could not match target in current page and response/i)
+              done()
+

       describe 'choice of target', ->

@@ -575,26 +603,261 @@

       describe 'with { transition } option', ->

-        it 'returns a promise that will be fulfilled once the server response was received and the swap transition has completed', asyncSpec (next) ->
-          fixture('.target', text: 'old-text')
+        it 'morphs between the old and new element', asyncSpec (next) ->
+          $fixture('.element.v1').text('version 1')
+          up.change('.element',
+            document: '
version 2
',
+            transition: 'cross-fade',
+            duration: 200,
+            easing: 'linear'
+          )
+
+          $old = undefined
+          $new = undefined
+
+          next =>
+            $old = $('.element.v1')
+            $new = $('.element.v2')
+
+            expect($old).toHaveLength(1)
+            expect($old).toHaveOpacity(1.0, 0.15)
+
+            expect($new).toHaveLength(1)
+            expect($new).toHaveOpacity(0.0, 0.15)
+
+          next.after 100, =>
+            expect($old).toHaveOpacity(0.5, 0.3)
+            expect($new).toHaveOpacity(0.5, 0.3)
+
+          next.after (100 + 70), =>
+            expect($new).toHaveOpacity(1.0, 0.1)
+            expect($old).toBeDetached()
+
+
+        it 'ignores a { transition } option when replacing a singleton element like ', asyncSpec (next) ->
+          # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
+          spyOn(up.element, 'isSingleton').and.callFake (element) -> e.matches(element, '.container')
+
+          $fixture('.container').text('old text')
+
+          extractDone = jasmine.createSpy()
+          promise = up.change('.container',
+            document: '
new text
',
+            transition: 'cross-fade',
+            duration: 200
+          )
+          promise.then(extractDone)
+
+          next =>
+            # See that we've already immediately swapped the element and ignored the duration of 200ms
+            expect(extractDone).toHaveBeenCalled()
+            expect($('.container').length).toEqual(1)
+            expect($('.container')).toHaveOpacity(1.0)
+
+        it 'marks the old fragment as .up-destroying during the transition', asyncSpec (next) ->
+          $fixture('.element').text('version 1')
+          up.change('.element',
+            document: '
version 2
',
+            transition: 'cross-fade',
+            duration: 200
+          )
+
+          next =>
+            $version1 = $('.element:contains("version 1")')
+            expect($version1).toHaveLength(1)
+            expect($version1).toHaveClass('up-destroying')
+
+            $version2 = $('.element:contains("version 2")')
+            expect($version2).toHaveLength(1)
+            expect($version2).not.toHaveClass('up-destroying')
+
+        # extract with { transition } option
+        it 'emits an up:fragment:destroyed event on the former parent element after the element has been removed from the DOM', (done) ->
+          $parent = $fixture('.parent')
+          $element = $parent.affix('.element.v1').text('v1')
+          expect($element).toBeAttached()
+
+          spy = jasmine.createSpy('event listener')
+          $parent[0].addEventListener 'up:fragment:destroyed', (event) ->
+            spy(event.target, event.fragment, up.specUtil.isDetached($element))
+
+          extractDone = up.change('.element',
+            document: '
v2
',
+            transition: 'cross-fade',
+            duration: 50
+          )
+
+          extractDone.then ->
+            expect(spy).toHaveBeenCalledWith($parent[0], $element[0], true)
+            done()
+
+
+        it 'cancels an existing transition by instantly jumping to the last frame', asyncSpec (next) ->
+          $fixture('.element.v1').text('version 1')
+
+          up.change('.element',
+            document: '
version 2
',
+            transition: 'cross-fade',
+            duration: 200
+          )
+
+          next =>
+            $ghost1 = $('.element:contains("version 1")')
+            expect($ghost1).toHaveLength(1)
+            expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
+
+            $ghost2 = $('.element:contains("version 2")')
+            expect($ghost2).toHaveLength(1)
+            expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
+
+          next =>
+            up.change('.element',
+              document: '
version 3
',
+              transition: 'cross-fade',
+              duration: 200
+            )
+
+          next =>
+            $ghost1 = $('.element:contains("version 1")')
+            expect($ghost1).toHaveLength(0)
+
+            $ghost2 = $('.element:contains("version 2")')
+            expect($ghost2).toHaveLength(1)
+            expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
+
+            $ghost3 = $('.element:contains("version 3")')
+            expect($ghost3).toHaveLength(1)
+            expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
+
+
+        it 'delays the resolution of the returned promise until the transition is over', (done) ->
+          $fixture('.element').text('version 1')
           resolution = jasmine.createSpy()
-          promise = up.change(target: '.target', url: '/path', transition: 'cross-fade', duration: 200)
+          promise = up.change('.element',
+            document: '
version 2
',
+            transition: 'cross-fade',
+            duration: 60
+          )
           promise.then(resolution)
-
-          next =>
+          expect(resolution).not.toHaveBeenCalled()
+
+          u.timer 20, ->
             expect(resolution).not.toHaveBeenCalled()
-            expect('.target').toHaveText('old-text')
-            @respondWith '
new-text
'
-
-          next.after 100, =>
-            target = e.first('.target')
-            opacity = e.styleNumber(target, 'opacity')
-            expect(opacity).toBeAround(0.5, 0.4)
-            expect(resolution).not.toHaveBeenCalled()
-
-          next.after 200, =>
+
+          u.timer 200, ->
             expect(resolution).toHaveBeenCalled()
-            expect('.target').toHaveText('new-text')
+            done()
+
+        it 'attaches the new element to the DOM before compilers are called, so they can see their parents and trigger bubbling events', asyncSpec (next)->
+          $parent = $fixture('.parent')
+          $element = $parent.affix('.element').text('old text')
+          spy = jasmine.createSpy('parent spy')
+          up.$compiler '.element', ($element) -> spy($element.text(), $element.parent())
+          up.change '.element',
+            document: '
new text
',
+            transition: 'cross-fade',
+            duration: 50
+
+          next =>
+            expect(spy).toHaveBeenCalledWith('new text', $parent)
+
+
+        describe 'when up.morph() is called from a transition function', ->
+
+          it "does not emit multiple replacement events (bugfix)", (done) ->
+            $element = $fixture('.element').text('old content')
+
+            transition = (oldElement, newElement, options) ->
+              up.morph(oldElement, newElement, 'cross-fade', options)
+
+            destroyedListener = jasmine.createSpy('listener to up:fragment:destroyed')
+            up.on 'up:fragment:destroyed', destroyedListener
+            insertedListener = jasmine.createSpy('listener to up:fragment:inserted')
+            up.on 'up:fragment:inserted', insertedListener
+
+            extractDone = up.change('.element',
+              document: '
new content
',
+              transition: transition,
+              duration: 50,
+              easing: 'linear'
+            )
+
+            extractDone.then ->
+              expect(destroyedListener.calls.count()).toBe(1)
+              expect(insertedListener.calls.count()).toBe(1)
+              done()
+
+          it "does not compile the element multiple times (bugfix)", (done) ->
+            $element = $fixture('.element').text('old content')
+
+            transition = (oldElement, newElement, options) ->
+              up.morph(oldElement, newElement, 'cross-fade', options)
+
+            compiler = jasmine.createSpy('compiler')
+            up.$compiler '.element', compiler
+
+            extractDone = up.change('.element',
+              document: '
new content
',
+              transition: transition,
+              duration: 50,
+              easing: 'linear'
+            )
+
+            extractDone.then ->
+              expect(compiler.calls.count()).toBe(1)
+              done()
+
+          it "does not call destructors multiple times (bugfix)", (done) ->
+            $element = $fixture('.element').text('old content')
+
+            transition = (oldElement, newElement, options) ->
+              up.morph(oldElement, newElement, 'cross-fade', options)
+
+            destructor = jasmine.createSpy('destructor')
+            up.$compiler '.element', (element) ->
+              return destructor
+
+            up.hello($element)
+
+            extractDone = up.change('.element',
+              document: '
new content
',
+              transition: transition,
+              duration: 50,
+              easing: 'linear'
+            )
+
+            extractDone.then ->
+              expect(destructor.calls.count()).toBe(1)
+              done()
+
+
+        describe 'when animation is disabled', ->
+
+          beforeEach ->
+            up.motion.config.enabled = false
+
+          it 'immediately swaps the old and new elements without creating unnecessary ghosts', asyncSpec (next) ->
+            $fixture('.element').text('version 1')
+            up.change('.element',
+              document: '
version 2
',
+              transition: 'cross-fade',
+              duration: 200
+            )
+            next =>
+              expect($('.element')).toHaveText('version 2')
+              expect($('.up-ghost')).toHaveLength(0)
+
+          it "replaces the elements directly, since first inserting and then removing would shift scroll positions", asyncSpec (next) ->
+            swapDirectlySpy = up.motion.knife.mock('swapElementsDirectly')
+            $fixture('.element').text('version 1')
+            up.change('.element',
+              document: '
version 2
',
+              transition: false
+            )
+
+            next =>
+              expect(swapDirectlySpy).toHaveBeenCalled()
+

       describe 'with { reveal } option', ->

@@ -1085,6 +1348,18 @@
             expect(spy).toHaveBeenCalledWith('old text', $parent)
             done()

+      it 'focuses an [autofocus] element in the new fragment', asyncSpec (next) ->
+        $fixture('.foo-bar')
+        up.change '.foo-bar', document: """
+          <form class='foo-bar'>
+            <input class="autofocused-input" autofocus>
+          </form>
+        """
+
+        next =>
+          input = $('.autofocused-input').get(0)
+          expect(input).toBeGiven()
+          expect(document.activeElement).toBe(input)

       describeCapability 'canCustomElements', ->

@@ -1811,26 +2086,6 @@

     describe 'up.extract', ->

-      it 'updates a selector on the current page with the same selector from the given HTML string', asyncSpec (next) ->
-
-        $fixture('.before').text('old-before')
-        $fixture('.middle').text('old-middle')
-        $fixture('.after').text('old-after')
-
-        html =
-          """
-
new-before

-
new-middle

-
new-after

-          """
-
-        up.extract('.middle', html)
-
-        next ->
-          expect($('.before')).toHaveText('old-before')
-          expect($('.middle')).toHaveText('new-middle')
-          expect($('.after')).toHaveText('old-after')
-
       it "rejects if the selector can't be found on the current page", (done) ->
         html = '
text
'
         promise = up.extract('.foo-bar', html)
@@ -1841,16 +2096,6 @@
             expect(result.value).toMatch(/Could not match target in current page/i)
             done()

-      it "rejects if the selector can't be found in the given HTML string", (done) ->
-        $fixture('.foo-bar')
-        promise = up.extract('.foo-bar', 'html without match')
-
-        u.task ->
-          promiseState(promise).then (result) =>
-            expect(result.state).toEqual('rejected')
-            expect(result.value).toMatch(/Could not match target in current page and response/i)
-            done()
-
       it "ignores an element that matches the selector but also matches .up-destroying", (done) ->
         html = '
text
'
         $fixture('.foo-bar.up-destroying')
@@ -1884,225 +2129,6 @@
           $elements = $('.foo-bar')
           expect($($elements.get(0)).text()).toEqual('text')
           expect($($elements.get(1)).text()).toEqual('')
-
-      it 'focuses an [autofocus] element in the new fragment', asyncSpec (next) ->
-        $fixture('.foo-bar')
-        up.extract '.foo-bar', """
-          <form class='foo-bar'>
-            <input class="autofocused-input" autofocus>
-          </form>
-        """
-
-        next =>
-          input = $('.autofocused-input').get(0)
-          expect(input).toBeGiven()
-          expect(document.activeElement).toBe(input)
-
-
-
-
-      describe 'with { transition } option', ->
-
-        it 'morphs between the old and new element', asyncSpec (next) ->
-          $fixture('.element.v1').text('version 1')
-          up.extract('.element', '
version 2
', transition: 'cross-fade', duration: 200, easing: 'linear')
-
-          $old = undefined
-          $new = undefined
-
-          next =>
-            $old = $('.element.v1')
-            $new = $('.element.v2')
-
-            expect($old).toHaveLength(1)
-            expect($old).toHaveOpacity(1.0, 0.15)
-
-            expect($new).toHaveLength(1)
-            expect($new).toHaveOpacity(0.0, 0.15)
-
-          next.after 100, =>
-            expect($old).toHaveOpacity(0.5, 0.3)
-            expect($new).toHaveOpacity(0.5, 0.3)
-
-          next.after (100 + 70), =>
-            expect($new).toHaveOpacity(1.0, 0.1)
-            expect($old).toBeDetached()
-
-
-        it 'ignores a { transition } option when replacing a singleton element like ', asyncSpec (next) ->
-          # shouldSwapElementsDirectly() is true for body, but can't have the example replace the Jasmine test runner UI
-          spyOn(up.element, 'isSingleton').and.callFake (element) -> e.matches(element, '.container')
-
-          $fixture('.container').text('old text')
-
-          extractDone = jasmine.createSpy()
-          promise = up.extract('.container', '
new text
', transition: 'cross-fade', duration: 200)
-          promise.then(extractDone)
-
-          next =>
-            # See that we've already immediately swapped the element and ignored the duration of 200ms
-            expect(extractDone).toHaveBeenCalled()
-            expect($('.container').length).toEqual(1)
-            expect($('.container')).toHaveOpacity(1.0)
-
-        it 'marks the old fragment as .up-destroying during the transition', asyncSpec (next) ->
-          $fixture('.element').text('version 1')
-          up.extract('.element', '
version 2
', transition: 'cross-fade', duration: 200)
-
-          next =>
-            $version1 = $('.element:contains("version 1")')
-            expect($version1).toHaveLength(1)
-            expect($version1).toHaveClass('up-destroying')
-
-            $version2 = $('.element:contains("version 2")')
-            expect($version2).toHaveLength(1)
-            expect($version2).not.toHaveClass('up-destroying')
-
-        # extract with { transition } option
-        it 'emits an up:fragment:destroyed event on the former parent element after the element has been removed from the DOM', (done) ->
-          $parent = $fixture('.parent')
-          $element = $parent.affix('.element.v1').text('v1')
-          expect($element).toBeAttached()
-
-          spy = jasmine.createSpy('event listener')
-          $parent[0].addEventListener 'up:fragment:destroyed', (event) ->
-            spy(event.target, event.fragment, up.specUtil.isDetached($element))
-
-          extractDone = up.extract('.element', '
v2
', transition: 'cross-fade', duration: 50)
-
-          extractDone.then ->
-            expect(spy).toHaveBeenCalledWith($parent[0], $element[0], true)
-            done()
-
-
-        it 'cancels an existing transition by instantly jumping to the last frame', asyncSpec (next) ->
-          $fixture('.element.v1').text('version 1')
-
-          up.extract('.element', '
version 2
', transition: 'cross-fade', duration: 200)
-
-          next =>
-            $ghost1 = $('.element:contains("version 1")')
-            expect($ghost1).toHaveLength(1)
-            expect($ghost1.css('opacity')).toBeAround(1.0, 0.1)
-
-            $ghost2 = $('.element:contains("version 2")')
-            expect($ghost2).toHaveLength(1)
-            expect($ghost2.css('opacity')).toBeAround(0.0, 0.1)
-
-          next =>
-            up.extract('.element', '
version 3
', transition: 'cross-fade', duration: 200)
-
-          next =>
-            $ghost1 = $('.element:contains("version 1")')
-            expect($ghost1).toHaveLength(0)
-
-            $ghost2 = $('.element:contains("version 2")')
-            expect($ghost2).toHaveLength(1)
-            expect($ghost2.css('opacity')).toBeAround(1.0, 0.1)
-
-            $ghost3 = $('.element:contains("version 3")')
-            expect($ghost3).toHaveLength(1)
-            expect($ghost3.css('opacity')).toBeAround(0.0, 0.1)
-
-
-        it 'delays the resolution of the returned promise until the transition is over', (done) ->
-          $fixture('.element').text('version 1')
-          resolution = jasmine.createSpy()
-          promise = up.extract('.element', '
version 2
', transition: 'cross-fade', duration: 60)
-          promise.then(resolution)
-          expect(resolution).not.toHaveBeenCalled()
-
-          u.timer 20, ->
-            expect(resolution).not.toHaveBeenCalled()
-
-          u.timer 200, ->
-            expect(resolution).toHaveBeenCalled()
-            done()
-
-        it 'attaches the new element to the DOM before compilers are called, so they can see their parents and trigger bubbling events', asyncSpec (next)->
-          $parent = $fixture('.parent')
-          $element = $parent.affix('.element').text('old text')
-          spy = jasmine.createSpy('parent spy')
-          up.$compiler '.element', ($element) -> spy($element.text(), $element.parent())
-          up.extract '.element', '
new text
', transition: 'cross-fade', duration: 50
-
-          next =>
-            expect(spy).toHaveBeenCalledWith('new text', $parent)
-
-
-        describe 'when up.morph() is called from a transition function', ->
-
-          it "does not emit multiple replacement events (bugfix)", (done) ->
-            $element = $fixture('.element').text('old content')
-
-            transition = (oldElement, newElement, options) ->
-              up.morph(oldElement, newElement, 'cross-fade', options)
-
-            destroyedListener = jasmine.createSpy('listener to up:fragment:destroyed')
-            up.on 'up:fragment:destroyed', destroyedListener
-            insertedListener = jasmine.createSpy('listener to up:fragment:inserted')
-            up.on 'up:fragment:inserted', insertedListener
-
-            extractDone = up.extract('.element', '
new content
', transition: transition, duration: 50, easing: 'linear')
-
-            extractDone.then ->
-              expect(destroyedListener.calls.count()).toBe(1)
-              expect(insertedListener.calls.count()).toBe(1)
-              done()
-
-          it "does not compile the element multiple times (bugfix)", (done) ->
-            $element = $fixture('.element').text('old content')
-
-            transition = (oldElement, newElement, options) ->
-              up.morph(oldElement, newElement, 'cross-fade', options)
-
-            compiler = jasmine.createSpy('compiler')
-            up.$compiler '.element', compiler
-
-            extractDone = up.extract('.element', '
new content
', transition: transition, duration: 50, easing: 'linear')
-
-            extractDone.then ->
-              expect(compiler.calls.count()).toBe(1)
-              done()
-
-          it "does not call destructors multiple times (bugfix)", (done) ->
-            $element = $fixture('.element').text('old content')
-
-            transition = (oldElement, newElement, options) ->
-              up.morph(oldElement, newElement, 'cross-fade', options)
-
-            destructor = jasmine.createSpy('destructor')
-            up.$compiler '.element', (element) ->
-              return destructor
-
-            up.hello($element)
-
-            extractDone = up.extract('.element', '
new content
', transition: transition, duration: 50, easing: 'linear')
-
-            extractDone.then ->
-              expect(destructor.calls.count()).toBe(1)
-              done()
-
-
-        describe 'when animation is disabled', ->
-
-          beforeEach ->
-            up.motion.config.enabled = false
-
-          it 'immediately swaps the old and new elements without creating unnecessary ghosts', asyncSpec (next) ->
-            $fixture('.element').text('version 1')
-            up.extract('.element', '
version 2
', transition: 'cross-fade', duration: 200)
-            next =>
-              expect($('.element')).toHaveText('version 2')
-              expect($('.up-ghost')).toHaveLength(0)
-
-          it "replaces the elements directly, since first inserting and then removing would shift scroll positions", asyncSpec (next) ->
-            swapDirectlySpy = up.motion.knife.mock('swapElementsDirectly')
-            $fixture('.element').text('version 1')
-            up.extract('.element', '
version 2
', transition: false)
-
-            next =>
-              expect(swapDirectlySpy).toHaveBeenCalled()

       describe 'with { scrollBehavior } option', ->


fixing map drawing -- yay hax

fixing map drawing -- yay hax

--- src/coffee/controllers/player/player-map-ctrl.coffee
+++ src/coffee/controllers/player/player-map-ctrl.coffee
@@ -64,6 +64,7 @@

     $scope.initializeMap = ->
       $scope.currentMap = CurrentMap.getMap()
+      _.each $scope.currentMap?.map.layers[2].objects, (object) -> object.properties = {}
       game?.state.restart()

     CurrentMap.observe().then null, null, ->


support untitled files

support untitled files

--- lib/eval.coffee
+++ lib/eval.coffee
@@ -13,7 +13,7 @@

     code: editor.getText()
     module: editor.juliaModule
-    path: editor.getPath()
+    path: editor.getPath() || 'untitled-' + editor.getBuffer().inkId
     start: @cursor start
     end: @cursor end


monkey patch editor with metadata

monkey patch editor with metadata

--- lib/init.coffee
+++ lib/init.coffee
@@ -330,6 +330,7 @@
                 data: importers #.map((f) => path_.basename(f.path))

             metadata = getMetadata(found[0]).concat(fixture)
+            editor.metadata = metadata

             if ext == '.html'
                 preview = getHtmlPreview(found[0])


remove unused code, use progress bar with onPractice

--- src/components/student-dashboard/progress-guide.cjsx
+++ src/components/student-dashboard/progress-guide.cjsx
@@ -10,55 +10,37 @@
 LearningGuideSection = require '../learning-guide/section'
 LearningGuideColorKey = require '../learning-guide/color-key'

+# Number of sections to display
+NUM_SECTIONS = 4
+
 ProgressGuide = React.createClass
   displayName: 'ProgressGuide'
+
+  contextTypes:
+    router: React.PropTypes.func

   propTypes:
     courseId: React.PropTypes.string.isRequired

-  mixins: [ChapterSectionMixin]
-
-  getRecentChapters: (chapter, i) ->
-#    sections = _.map(chapter.children, @renderSectionBars)
-
{for section, si in chapter.children
-      <LearningGuideSection key={"#{i}.#{si}"} section={section} courseId={@props.courseId} />}
-

-
-
-  _percent: (num, total) ->
-    Math.round((num / total) * 100)
-
-  renderSectionBars: (section) ->
-
-    chapterSection =
-      @sectionFormat(section.chapter_section, @state.sectionSeparator)
-    sectionPercent = @_percent(section.current_level, 1)
-    colorClass = @colorizeBar(sectionPercent)
-

-

-        {chapterSection}
-        {section.title}
-

-      <BS.ProgressBar className={colorClass} now={sectionPercent} />
-

-
-  colorizeBar: (percent) ->
-    if percent > 75
-      'high'
-    else if percent <= 75 and percent >= 50
-      'medium'
-    else if percent <= 49
-      'low'
+  onPractice: (section) ->
+    @context.router.transitionTo('viewPractice', {courseId: @props.courseId}, {page_ids: section.page_ids})

   render: ->
-    guide = LearningGuideStudentStore.get(@props.courseId)
-    recentTopics = _.map(guide.children[0..4], @getRecentChapters)
+    courseId = @props.courseId
+    guide = LearningGuideStudentStore.get(courseId)
+
+    sections = for section, i in _.first(LearningGuideStudentStore.getAllSections(courseId), NUM_SECTIONS)
+      <LearningGuideSection key={i}
+        section={section}
+        onPractice={@onPractice}
+        courseId={courseId} />
+

Recent Topics

Current Level of Understanding

-        {recentTopics}
+        {_.first(sections, 4)}

       <LearningGuideColorKey />


bug fix when the image with a upper-case extname

--- lib/main.coffee
+++ lib/main.coffee
@@ -12,6 +12,6 @@
 # Files with these extensions will be opened as images
 imageExtensions = ['.gif', '.ico', '.jpeg', '.jpg', '.png']
 openUri = (uriToOpen) ->
-  uriExtension = path.extname(uriToOpen)
+  uriExtension = path.extname(uriToOpen).toLowerCase()
   if _.include(imageExtensions, uriExtension)
     new ImageEditor(uriToOpen)


simplify since there is no problem

simplify since there is no problem

--- lib/main.coffee
+++ lib/main.coffee
@@ -25,11 +25,10 @@
       @history.destroy()
       [@pasteArea, @history] = []

-    paste = @paste.bind(this)
     @subscriptions.add atom.commands.add 'atom-text-editor',
-      'clip-history:paste': -> paste(@getModel(), 'older')
-      'clip-history:paste-newer': -> paste(@getModel(), 'newer')
-      'clip-history:paste-last': -> paste(@getModel(), 'lastPasted')
+      'clip-history:paste': => @paste('older')
+      'clip-history:paste-newer': => @paste('newer')
+      'clip-history:paste-last': => @paste('lastPasted')
       'clip-history:clear': => @history.reset()

   deactivate: ->
@@ -55,7 +54,8 @@
     finally
       @pasting = false

-  paste: (editor, which) ->
+  paste: (which) ->
+    editor = atom.workspace.getActiveTextEditor()
     if editor.hasMultipleCursors() and settings.get('doNormalPasteWhenMultipleCursors')
       editor.pasteText()
       return


only save if buffer was modified

only save if buffer was modified

--- lib/main.coffee
+++ lib/main.coffee
@@ -24,7 +24,7 @@
     scope  = editor.getGrammar().scopeName

     if script? and scope.startsWith 'source.inno'
-      editor.save()
+      editor.save() if editor.isModified()

       @getPath (stdout) ->
         isccBin  = atom.config.get('language-inno.pathToISCC')


Revert introduction of Hydrogen#language

--- lib/main.coffee
+++ lib/main.coffee
@@ -19,7 +19,6 @@
     statusBarElement: null
     statusBarTile: null
     editor: null
-    language: null
     markerBubbleMap: {}

     activate: (state) ->
@@ -75,12 +74,12 @@

         @editor = currentPaneItem

-        @language = @editor.getGrammar?()?.name.toLowerCase()
-        if @language?
-            kernel = KernelManager.getRunningKernelForLanguage(@language)
+        language = @editor.getGrammar?()?.name.toLowerCase()
+        if language?
+            kernel = KernelManager.getRunningKernelForLanguage language

         if kernel?
-            @setStatusBarElement(kernel.statusView.getElement())
+            @setStatusBarElement kernel.statusView.getElement()
         else
             @removeStatusBarElement()

@@ -117,7 +116,9 @@

     createResultBubble: (code, row) ->
-        KernelManager.startKernelIfNeeded @language, (kernel) =>
+        language = @editor.getGrammar().name.toLowerCase()
+
+        KernelManager.startKernelIfNeeded language, (kernel) =>
             unless @watchSidebar?
                 @setWatchSidebar kernel.watchSidebar
             else if @watchSidebar.element.contains document.activeElement
@@ -128,7 +129,7 @@

             @clearBubblesOnRow row
             view = @insertResultBubble row
-            KernelManager.execute @language, code, (result) =>
+            KernelManager.execute language, code, (result) =>
                 view.spin false
                 view.addResult result


Reduce color margin

Reduce color margin

--- lib/main.coffee
+++ lib/main.coffee
@@ -50,9 +50,9 @@
   # _scaleDuo = chroma.scale([_duoHigh,          _duoLow]).colors(2)
   # _scaleTri = chroma.scale([_triHigh,          _triLow]).colors(2)

-  root.style.setProperty('--uno-1', chroma.mix( uno, 'hsl(250, 100%, 96%)', .75))  # how much white
+  root.style.setProperty('--uno-1', chroma.mix( uno, 'hsl(250, 100%, 96%)', .33))  # how much white
   root.style.setProperty('--uno-2', uno)                                            # <- set by user
-  root.style.setProperty('--uno-3', chroma.mix( uno, 'hsl(250, 6%, 18%)', .5))    # how much desaturated background (@syntax-bg)
+  root.style.setProperty('--uno-3', chroma.mix( uno, 'hsl(250, 6%, 18%)', .33))    # how much desaturated background (@syntax-bg)

   root.style.setProperty('--duo-1', duo)                                         # <- set by user
   root.style.setProperty('--duo-2', chroma.mix( duo, 'hsl(250, 12%, 18%)', .33)) # how much background (@syntax-bg)


Fix config bug.

Fix config bug.

--- lib/main.coffee
+++ lib/main.coffee
@@ -7,7 +7,7 @@
   parser: require('./parser')

   activate: (state) ->
-    #attach statusbar view
+    #attach status bar
     atom.packages.once 'activated', @createStatusBarView

     #set default config options
@@ -15,6 +15,10 @@
       showScopeHighlights: true,
       es6Support: true
     }
+
+    #when es6 config option changes, invalidate cache
+    atom.workspaceView.subscribe atom.config.observe 'atom-symbol-navigation.es6Support', =>
+      @parser.invalidateScopesCache()

     #attach commands
     atom.workspaceView.command "atom-symbol-navigation:jump-to-next-id", =>
@@ -25,10 +29,6 @@

     atom.workspaceView.command "atom-symbol-navigation:select-all-id", =>
       @selectAllIdentifiers()
-
-    #when es6 config option changes, invalidate cache
-    @subscribe atom.config.observe 'atom-symbol-navigation.es6Support', =>
-      @parser.invalidateScopesCache()

     #when active panel changes, erase status text
     atom.workspace.onDidChangeActivePaneItem =>


code cleaning

code cleaning

--- lib/paas.coffee
+++ lib/paas.coffee
@@ -31,9 +31,7 @@

         # Send a stop request to haibu server
         @client.stopApp = (manifest, token, callback) ->
-            data =
-                stop:
-                    name: manifest
+            data = stop: manifest
             controllerClient.setToken token
             controllerClient.post "drones/#{manifest.name}/stop", data, callback


introduced: color.saturate() and color.desaturate()

--- src/color.coffee
+++ src/color.coffee
@@ -93,6 +93,13 @@

     hsi: ->
         rgb2hsi @_rgb
+
+    name: ->
+        h = @hex()
+        for k of chroma.colors
+            if h == chroma.colors[k]
+                return k
+        h

     interpolate: (f, col, m) ->
         ###
@@ -162,11 +169,24 @@
         else
             throw "color mode "+m+" is not supported"

-    darken: (amount=0.2) ->
+    darker: (amount=20) ->
         me = @
         lch = me.lch()
-        lch[2] -= amount
+        lch[0] -= amount
         chroma.lch lch
+
+    brighter: (amount=20) ->
+        @darker -amount
+
+    saturate: (amount=20) ->
+        me = @
+        lch = me.lch()
+        lch[1] += amount
+        chroma.lch lch
+
+    desaturate: (amount=20) ->
+        @saturate -amount
+

 hex2rgb = (hex) ->
@@ -174,7 +194,7 @@
         if chroma.colors? and chroma.colors[hex]
             hex = chroma.colors[hex]
         else
-            throw "unknown color format: "+hex
+            throw "unknown color: "+hex
     if hex.length == 4 or hex.length == 7
         hex = hex.substr(1)
     if hex.length == 3
@@ -190,7 +210,7 @@
 rgb2hex = () ->
     [r,g,b] = unpack arguments
     u = r << 16 | g << 8 | b
-    str = "000000" + u.toString(16).toUpperCase()
+    str = "000000" + u.toString(16) #.toUpperCase()
     "#" + str.substr(str.length - 6)


Fix indexes

Fix indexes

--- server/startup/indexes.coffee
+++ server/startup/indexes.coffee
@@ -3,6 +3,6 @@
 		ChatMessage._ensureIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 })
 		ChatMessage._ensureIndex({ 'rid': 1 })
 		ChatMessage._ensureIndex({ 'rid': 1, 'ts': 1 })
-		ChatSubscription._ensureIndex({ 'uid': 1 })
+		ChatSubscription._ensureIndex({ 'u._id': 1 })
 		ChatSubscription._ensureIndex({ 'ts': 1 })
-		ChatSubscription._ensureIndex({ 'rid': 1, 'uid': 1 }, {unique: true})
+		ChatSubscription._ensureIndex({ 'rid': 1, 'u._id': 1 }, {unique: true})


Remove logs of stream.permissions.read

Remove logs of stream.permissions.read

--- server/stream/messages.coffee
+++ server/stream/messages.coffee
@@ -6,7 +6,7 @@
 	return false

 msgStream.permissions.read (eventName) ->
-	console.log('stream.permissions.read', this.userId, eventName);
+	# console.log('stream.permissions.read', this.userId, eventName);
 	# return this.userId == eventName;

 	try


Add more descriptions of expected draft cloning behavior

--- server/test/units/page.coffee
+++ server/test/units/page.coffee
@@ -5,6 +5,7 @@
 Q = require('q')

 User = require('../../models/user.coffee').model
+Narrative = require('../../models/narrative.coffee').model

 suite('Page')
 test('.create', (done) ->
@@ -273,3 +274,86 @@
     throw err
   )
 )
+
+test('.createDraftClone clones a public page,
+  and duplicates child sections with new IDs', (done) ->
+  originalSection = null
+
+  helpers.createPage(
+    sections: [
+      title: "Lovely Section"
+    ]
+  ).then( (page) ->
+    originalSection = page.sections[0]
+
+    page.createDraftClone()
+  ).then( (clonedPage) ->
+
+    assert.lengthOf clonedPage.sections, 1,
+      "cloned page was expected to 1 cloned section, but has #{clonedPage.sections.length}"
+
+    clonedSection = clonedPage.sections[0]
+
+    assert.strictEqual clonedSection.title, originalSection.title,
+      "Expected clonedSection.title (#{clonedSection.title}) to equal
+        originalSection.title (#{originalSection.title})"
+
+    assert.notStrictEqual clonedSection.id, originalSection.id,
+      "Expected clonedSection (id: #{clonedSection.id}) to be a new record, but had same id
+        as originalSection (#{originalSection.id})"
+
+    done()
+  ).fail( (err) ->
+    console.error err
+    throw err
+  )
+)
+
+test('.createDraftClone clones a public page,
+  and duplicates child narratives', (done) ->
+  publicPage = originalNarrative = null
+
+  helpers.createPage(
+    sections: [
+      title: "Lovely Section"
+    ]
+  ).then( (page) ->
+    publicPage = page
+
+    section = publicPage.sections[0]
+    Q.nfcall(
+      helpers.createNarrative, {
+        content: "Nested Narrative"
+        section: section
+      }
+    )
+
+  ).then( (narrative) ->
+    originalNarrative = narrative
+
+    publicPage.createDraftClone()
+  ).then( (clonedPage) ->
+
+    clonedSection = clonedPage.sections[0]
+
+    Q.nsend(
+      Narrative.findOne(section: clonedSection), 'exec'
+    )
+
+  ).then((clonedNarrative) ->
+    assert.isDefined clonedNarrative, "Couldn't find a cloned narrative"
+
+    assert.strictEqual clonedNarrative.title, originalNarrative.title,
+      "Expected clonedNarrative.title (#{clonedNarrative.title}) to equal
+        originalNarrative.title (#{originalNarrative.title})"
+
+    assert.notStrictEqual clonedNarrative.id, originalNarrative.id,
+      "Expected clonedNarrative.id (#{clonedNarrative.id}) to be different to
+        originalNarrative.id (#{originalNarrative.id})"
+
+    done()
+  ).fail( (err) ->
+    console.error err
+    throw err
+  )
+)


put all shutdown logic in beginShutdown, force program to exit after 2 mins

--- services/filestore/app.coffee
+++ services/filestore/app.coffee
@@ -103,8 +103,6 @@
 			else
 				res.send(503)

-
-
 app.get '*', (req, res)->
 	res.send 404

@@ -115,12 +113,16 @@
 beginShutdown = () ->
 	if appIsOk
 		appIsOk = false
-		server.close()
+		# hard-terminate this process if graceful shutdown fails
+		killTimer = setTimeout () ->
+			process.exit 1
+		, 120*1000
+		killTimer.unref?() # prevent timer from keeping process alive
+		server.close () ->
+			logger.log "closed all connections"
+			Metrics.close()
+			process.disconnect?()
 		logger.log "server will stop accepting connections"
-
-server.on "close", () ->
-	logger.log "closed all connections"
-	process.exit 1

 server.listen port, host, ->
 	logger.log("filestore store listening on #{host}:#{port}")


fix sentry initialisation

fix sentry initialisation

--- services/real-time/app.coffee
+++ services/real-time/app.coffee
@@ -1,12 +1,12 @@
 logger = require "logger-sharelatex"
 logger.initialize("real-time-sharelatex")
-if Settings.sentry?.dsn?
-	logger.initializeErrorReporting(Settings.sentry.dsn)

 express = require("express")
 session = require("express-session")
 redis = require("redis-sharelatex")
 Settings = require "settings-sharelatex"
+if Settings.sentry?.dsn?
+	logger.initializeErrorReporting(Settings.sentry.dsn)

 sessionRedisClient = redis.createClient(Settings.redis.websessions)


Minify correct version of Ace

Minify correct version of Ace

--- services/web/Gruntfile.coffee
+++ services/web/Gruntfile.coffee
@@ -161,6 +161,7 @@
 						"moment": "libs/#{PackageVersions.lib('moment')}"
 						"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
 						"libs/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
+						"ace": "#{PackageVersions.lib('ace')}"
 					shim:
 						"libs/pdf":
 							deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]


Removed support for TeX style files

Removed support for TeX style files

--- src/docco.coffee
+++ src/docco.coffee
@@ -166,8 +166,6 @@
     name: 'tex', symbol: '%'
   '.latex':
     name: 'tex', symbol: '%'
-  '.sty':
-    name: 'tex', symbol: '%'

 # Build out the appropriate matchers and delimiters for each language.
 for ext, l of languages


minor stuff

minor stuff

--- src/chinchilla/lazy_association.coffee
+++ src/chinchilla/lazy_association.coffee
@@ -26,7 +26,7 @@
     # @return [promise] promise of association action operation
     load: ->
       @contextOperation ||= @$operation.$(@$name)
-      @actionOperation  ||= @contextOperation.$$('get', per: 100).$promise.then @_assign.bind(@)
+      @actionOperation  ||= @contextOperation.$$('get').$promise.then @_assign.bind(@)

     # fetch association for specific object.
     # triggers load if necessary.
@@ -92,6 +92,7 @@

             @cache[backReference].push(result)
       else
+        # HAS ONE / BELONGS TO
         sortedResults = {}
         _.each results, (result) -> sortedResults[result['@id']] = result
         _.each @$objects, (object) =>


hide text input buttons for java files

hide text input buttons for java files

--- src/smc-webapp/editor-html-md/editor-html-md.coffee
+++ src/smc-webapp/editor-html-md/editor-html-md.coffee
@@ -75,9 +75,11 @@
         @element = templates.find(".salvus-editor-html-md").clone()

         # create the textedit button bar.
-        if @ext != 'java'
-            @edit_buttons = templates.find(".salvus-editor-textedit-buttonbar").clone()
-            @element.find(".salvus-editor-html-md-textedit-buttonbar").append(@edit_buttons)
+        @edit_buttons = templates.find(".salvus-editor-textedit-buttonbar").clone()
+        @element.find(".salvus-editor-html-md-textedit-buttonbar").append(@edit_buttons)
+
+        if @ext == 'java'
+            @element.find(".salvus-editor-html-md-textedit-buttonbar").hide()

         @preview = @element.find(".salvus-editor-html-md-preview")
         @preview_content = @preview.find(".salvus-editor-html-md-preview-content")


update source with linux-arm

update source with linux-arm

--- src/index.coffee
+++ src/index.coffee
@@ -74,7 +74,8 @@
     'linux-ia32',
     'linux-x64',
     'win32-ia32',
-    'win32-x64']
+    'win32-x64',
+    'linux-arm']

     Promise.map options.platforms, (platform) ->
       platform = 'darwin' if platform is 'osx'


Fix bug caused by modularScale becoming object

--- src/index.coffee
+++ src/index.coffee
@@ -60,8 +60,13 @@
     createStyles: -> createStylesString(options)
     fontSizeToPx: vr.adjustFontSizeTo
     fontSizeToMS: (scaler) ->
+      # TODO detect which modular scale to use based on current screen width.
+      # or better, this should just generate styles and insert them in head
+      # with media queries? Perhaps do something similar to CSS Modules
+      # where this just returns a custom class name that you add to your
+      # component.
       baseFont = options.baseFontSize.slice(0, -2)
-      newFontSize = ms(scaler, options.modularScales[0]) * baseFont + "px"
+      newFontSize = ms(scaler, options.modularScales[0].scale) * baseFont + "px"
       vr.adjustFontSizeTo(newFontSize)
     injectStyles: ->
       if document?


Ok, really updated mongo db name

Ok, really updated mongo db name

--- src/index.coffee
+++ src/index.coffee
@@ -20,7 +20,7 @@
 # db_config = "mongodb://#{config.DB_USER}:#{config.DB_PASS}@#{config.DB_HOST}:#{config.DB_PORT}/#{config.DB_NAME}"
 # mongoose.connect db_config
 if app.settings.env != 'production'
-  mongoose.connect 'mongodb://localhost/example'
+  mongoose.connect 'mongodb://localhost/webHistorian'
 else
   console.log('If you are running in production, you may want to modify the mongoose connect path')


Remove CSON reference from coffee file

Remove CSON reference from coffee file

--- src/index.coffee
+++ src/index.coffee
@@ -1,6 +1,5 @@
 fs            = require 'fs'
 path          = require 'path'
-CSON          = require 'cson'
 postcss       = require 'postcss'

 namespace = postcss.plugin 'postcss-namespace', (opts) ->


Use async mapLimit for processing

Use async mapLimit for processing

--- src/index.coffee
+++ src/index.coffee
@@ -1,7 +1,7 @@
 S3 = require('aws-sdk').S3
 fs = require 'fs'
 gm = require('gm').subClass imageMagick: true
-async = require 'async'
+mapLimit = require('async').mapLimit

 hash = require('crypto').createHash
 rand = require('crypto').pseudoRandomBytes
@@ -163,8 +163,8 @@
   @getMeta (err) =>
     @makeMpc (err) =>
       return cb err if err
-      versions = JSON.parse(JSON.stringify(@config.versions))
-      async.mapSeries versions, @resizeAndUpload.bind(@), (err, versions) =>
+      versions = JSON.parse(JSON.stringify(@config.opts.versions))
+      mapLimit versions, @config.opts.workers, @resizeAndUpload.bind(@), (err, versions) =>
         return cb err if err
         return cb null, versions, @meta


Winning code ;)

Winning code ;)

--- src/index.coffee
+++ src/index.coffee
@@ -16,24 +16,25 @@
 #   1. If that number is contained in the mapping
 #     1. Loop through the mapped sequences and increment the value by 1 for each.
 #     2. If the value is equal to 5, trigger 'win'
-#     3. Remove that number from the mapping
+#   2. Remove that number from the mapping
 ###

 BOARD_WIDTH = 5
 BOARD_HEIGHT = 5
 TL_BR_DIAGONAL_INDEX = BOARD_WIDTH + BOARD_HEIGHT
 TR_BL_DIAGONAL_INDEX = BOARD_WIDTH + BOARD_HEIGHT + 1
+SOCKET_SERVER = 'ws://yahoobingo.herokuapp.com'

-io = Require('socket.io')
-_ = Require 'underscore'
+io = require 'socket.io-client'
+_ = require 'underscore'

-client = io.connect 'ws://yahoobingo.herokuapp.com'
+client = io.connect SOCKET_SERVER

 sequences = undefined # The possible bingo sequences
 numbers = undefined # A mapping of my card numbers to an array of which possible 'bingo' sequences
-tryAgain = false

 client.on 'connect', ->
+  console.log "Connected to ", SOCKET_SERVER

   client.emit 'register',
     name: "Jess Telford"
@@ -41,44 +42,46 @@
     url: "https://github.com/jesstelford/yahoo-bingo-node"

 client.on 'card', (payload) ->
-
+  console.log "Card received: ", payload
+
   sequences = (0 for i in [0..(BOARD_WIDTH + BOARD_HEIGHT + 1)])
   numbers = {}

   colIndex = 0
-  _(payload.slots).each (columnOfNumbers) ->
+  _(payload.slots).each (columnOfNumbers, columnLetter) ->

     rowIndex = 0
     _(columnOfNumbers).each (number) ->
-      numbers[number] ?= []
-      numbers[number].push colIndex
-      numbers[number].push rowIndex
-      numbers[number].push TL_BR_DIAGONAL_INDEX if colIndex is rowIndex # We're on the top-left to bottomr-right diagonal
-      numbers[number].push TR_BL_DIAGONAL_INDEX if colIndex is BOARD_WIDTH - rowIndex # We're on the top-left to bottomr-right diagonal
+      numberKey = "#{columnLetter}#{number}"
+      numbers[numberKey] ?= []
+      numbers[numberKey].push colIndex
+      numbers[numberKey].push BOARD_HEIGHT + rowIndex
+      numbers[numberKey].push TL_BR_DIAGONAL_INDEX if colIndex is rowIndex # We're on the top-left to bottomr-right diagonal
+      numbers[numberKey].push TR_BL_DIAGONAL_INDEX if colIndex + 1 is BOARD_WIDTH - rowIndex # We're on the top-left to bottomr-right diagonal

       rowIndex++

     colIndex++

 client.on 'number', (number) ->
+  hit = false
   _(numbers[number]).each (sequenceIndex) ->
     return unless sequences[sequenceIndex]?
+    hit = true
     sequences[sequenceIndex]++
     if sequences[sequenceIndex] >= BOARD_WIDTH
       client.emit 'bingo' # WIN condition
       sequences[sequenceIndex] = undefined
+  numbers[number] = undefined
+
+  console.log "Number received: ", number, "... ", (if hit then "HIT" else "MISS")

 client.on 'win', (message) ->
   console.log "WIN!", message
-  tryAgain = false # We won, no need to try again

 client.on 'lose', (message) ->
   console.log "LOSE!", message
-  tryAgain = true # We want to try win another one

 client.on 'disconnect', ->
-  if tryAgain
-    client = io.connect 'ws://yahoobingo.herokuapp.com'
-    tryAgain = false
-  else
-    # TODO: Shut down the server somehow
+  console.log "Bye!"
+  process.exit()


Fix source of effects

Fix source of effects

Signed-off-by: Ian Macalinao <08370e385f4778cad8fe504ec5445edd3d45bd9a@gmail.com>

--- src/items.coffee
+++ src/items.coffee
@@ -26,7 +26,7 @@
     @passives = params.passives
     @active = params.active

-    setSources = (effects) -> e.source = this for e in effects
+    setSources = (effects) => e.source = this for e in effects

     # Set sources of all effects
     [@auras, @passives].map (el) -> setSources el if el


lexer: made @countLines return back the passed string

--- src/lexer.coffee
+++ src/lexer.coffee
@@ -150,8 +150,7 @@
       else @token 'STRNUM', @escapeLines string
     default
       return 0
-    @countLines string
-    string.length
+    @countLines(string).length

   # Matches heredocs, adjusting indentation to the correct level, as heredocs
   # preserve whitespace, but ignore indentation to the left.
@@ -163,8 +162,7 @@
     if quote is '"' and 0 <= doc.indexOf '#{'
     then @interpolateString doc, heredoc: true
     else @token 'STRNUM', @makeString doc, quote, true
-    @countLines heredoc
-    heredoc.length
+    @countLines(heredoc).length

   # Matches and consumes comments.
   commentToken: ->
@@ -174,15 +172,13 @@
       @token 'HERECOMMENT', @sanitizeHeredoc here,
         herecomment: true, indent: Array(@indent + 1).join(' ')
       @token<[ TERMINATOR \n ]>
-    @countLines comment
-    comment.length
+    @countLines(comment).length

   # Matches JavaScript interpolated directly into the source via backticks.
   jsToken: ->
     return 0 unless @chunk.charAt(0) is '`' and js = JSTOKEN.exec @chunk
     @token 'LITERAL', (js[=0]).slice 1, -1
-    @countLines js
-    js.length
+    @countLines(js).length

   # Matches regular expression literals. Lexing regular expressions is difficult
   # to distinguish from division, so we borrow some basic heuristics from
@@ -199,8 +195,7 @@
     return 0 if @tag() of <[ STRNUM LITERAL ++ -- ]> or
                 not regex = REGEX.exec @chunk
     @token 'LITERAL', if regex[=0] is '//' then '/(?:)/' else regex
-    @countLines regex
-    regex.length
+    @countLines(regex).length

   # Matches experimental, multiline and extended regular expression literals.
   heregexToken: (match) ->
@@ -208,8 +203,7 @@
     if 0 > body.indexOf '#{'
       body .= replace(HEREGEX_OMIT, '').replace(/\//g, '\\/')
       @token 'LITERAL', "/#{ body or '(?:)' }/#{flags}"
-      @countLines heregex
-      return heregex.length
+      return @countLines(heregex).length
     @tokens.push ['IDENTIFIER', 'RegExp', @line], ['CALL_START', '(', @line]
     tokens = []
     for [tag, value] of @interpolateString(body, regex: true)
@@ -275,7 +269,7 @@

   # Record an outdent token or multiple tokens, if we happen to be moving back
   # inwards past several recorded indents.
-  outdentToken: (moveOut, noNewlines, close) ->
+  outdentToken: (moveOut, noNewlines) ->
     while moveOut > 0
       if (len = @indents.length - 1) < 0
         moveOut = 0
@@ -290,7 +284,7 @@
         @outdebt = 0
         @token 'OUTDENT', dent
     @outdebt -= moveOut if dent
-    @token 'TERMINATOR', '\n' unless noNewlines or @tag() is 'TERMINATOR'
+    @newlineToken() unless noNewlines
     this

   # Matches and consumes non-meaningful whitespace. Tag the previous token
@@ -503,9 +497,9 @@

   # Count the number of lines in a string and add it to `@line`.
   countLines: (str) ->
-    num = pos = 0
-    ++num while pos = 1 + str.indexOf '\n', pos
-    @line += num
+    pos = 0
+    ++@line while pos = 1 + str.indexOf '\n', pos
+    str

 # Constants
 # ---------


adding test re: re-assigning a variable that contains a no-args "function"

--- tests.coffee
+++ tests.coffee
@@ -3582,6 +3582,18 @@
   """
   " running without param!"

+  # you can assign then re-assign the variable
+  # without triggering the function
+  """
+  🚀 = to rocket:
+  ﹍console print " running without param!"
+
+  🚀 = 2
+  console print 🚀
+  """
+  "2"
+
+
   """
   to 🚀:
   ﹍console print "launch!"


Klient: add vmOn method temporarily (wip)

--- client/Main/kite/kites/klient.coffee
+++ client/Main/kite/kites/klient.coffee
@@ -23,3 +23,6 @@
     webtermKillSession : 'webterm.killSession'
     webtermPing        : 'webterm.ping'

+  vmOn: ->
+    warn "VM ON CALLED FROM KLIENT!!"
+    Promise.resolve()


Normalize not supported on older firefox

--- resource/translators/translator.coffee
+++ resource/translators/translator.coffee
@@ -349,10 +349,13 @@
         )
       value = "{#{value}}"

-  field.bibtex = "  #{field.name} = #{value}".normalize('NFKC')
+  field.bibtex = "  #{field.name} = #{value}"
+  field.bibtex = field.bibtex.normalize('NFKC') if @normalize
   @fields.push(field)
   @has[field.name] = true
   return
+
+Reference::normalize = (typeof(''.normalize) == 'function')

 Reference::complete = ->
   @add({name: 'type', value: @itemtype}) if @fields.length == 0


Bug fix : EdgeClick function

Bug fix : EdgeClick function

2 bugs appears with the conf.edgeClick function.

1) when defining a function in configuration, this function was never
called
2) when called (with bugfix), the clicked edge was not passed in
parameter.

I replace the code with a new one very similar to the nodeClick
function. This last one works fine so I use the same pattern and it
works fine as well for edge clicking now.

--- app/scripts/alchemy/core/interactions.coffee
+++ app/scripts/alchemy/core/interactions.coffee
@@ -27,8 +27,8 @@
                 'selected': true
                     )
         d3.event.stopPropagation()
-        if typeof alchemy.conf.edgeClick? is 'function'
-            alchemy.conf.edgeClick()
+        if typeof alchemy.conf.edgeClick == 'function'
+            alchemy.conf.edgeClick(d)

     nodeMouseOver: (n) ->
         if alchemy.conf.nodeMouseOver?


Update sealeve

Update sealeve

--- app/scripts/lib/script_modules/entity.coffee
+++ app/scripts/lib/script_modules/entity.coffee
@@ -202,7 +202,7 @@
     defer.promise

   _getSeaLevel: ->
-    350 + (70 * (@entity.scale ? 1.0))
+    220 + (220 * (@entity.scale ? 1.0))

   _moveAir: (settings) ->
     defaults =


location: filter states list

location: filter states list

--- workers/social/lib/social/models/location/index.coffee
+++ workers/social/lib/social/models/location/index.coffee
@@ -23,12 +23,14 @@
       if err
       then callback err
       else callback null, states
-        .filter(Boolean)
+        .filter( (state) ->
+          !!state?.state
+        )
         .map( (state) ->
           state     : state.state
           stateCode : state.stateCode
         )
-        .reduce( (memo, value)->
+        .reduce( (memo, value) ->
           memo[value.stateCode] =
             title: value.state
             value: value.stateCode


Referral: add 5GB referral status checker code

--- workers/social/lib/social/models/referral/index.coffee
+++ workers/social/lib/social/models/referral/index.coffee
@@ -227,6 +227,13 @@

     dash queue, kallback

+  @checkFor5GBStatus = (delegate, callback)->
+    delegate.fetchReferrers (err, referrers)=>
+      return callback err  if err
+      for ref in referrers
+        {type, unit, amount} = ref
+        return callback null, yes  if type is "disk" and unit is "MB" and amount is 5000
+      callback err, no

   @fetchUserVM = (client, vmName, callback)->
     account = client.connection.delegate


Referral: add migration script for changing baseVM type

--- workers/social/lib/social/models/referral/index.coffee
+++ workers/social/lib/social/models/referral/index.coffee
@@ -54,68 +54,6 @@
         as            : 'redeemedOn'

-  @checkFor1GBStatus = (account, callback)->
-    account.fetchReferrers (err, referrers)=>
-      return callback err  if err
-      for ref in referrers
-        {sourceCampaign} = ref
-        return callback null, yes  if sourceCampaign is "baseVMSizeDecrease"
-      callback err, no
-
-  @add1GBDisk = (delegate, callback)->
-
-    @checkFor1GBStatus delegate, (err, used) =>
-      return callback err if err
-      if used
-        console.info "#{delegate.profile.nickname} is already get baseVMSizeDecrease point"
-        return callback null, yes
-
-      referral = new JReferral {
-        type   : "disk"
-        unit   : "MB"
-        amount : 1024
-        sourceCampaign : "baseVMSizeDecrease"
-      }
-      referral.save (err) ->
-        return callback err if err
-        #add referrer as referrer to the referral system
-        delegate.addReferrer referral, (err)->
-          return callback err if err
-          return callback null, yes
-
-  @changeBaseVMForUsers = secure (client, callback)->
-    {delegate} = client.connection
-    unless delegate.profile.nickname is "cihangirsavas"
-      return callback new Error "youcannotcallthisfunction"
-
-    selector = {
-      diskSizeInMB : $gte : 4096
-      vmType       : "user"
-      webHome      : $not : ///guest-///
-    }
-
-    JVM.someData selector, {webHome:1}, {}, (err, cursor)=>
-      if err then callback err
-      else
-        cursor.each (err, vm)=>
-          if err then callback err
-          else if vm?
-            nickname = vm.webHome
-            JAccount.one {'profile.nickname':nickname}, (err, account)=>
-              if err console.error err
-              else if account?
-                @add1GBDisk account, (err, res)=>
-                  if err console.error err
-                  else if res console.info "decreaseVMsize point is added for", nickname
-                  else console.info "decreaseVMsize point is not added for", nickname
-              else
-                console.warn "couldnt find account", nickname
-          else
-            return callback null, "done"
-
-
-
-
   @getReferralEarningLimits = (type) ->
     switch type
       when "disk" then return 16000
@@ -398,3 +336,75 @@
             queue.next()
       ]
       daisy queue
+
+
+
+  # this part is only for one time migration,
+  # After 100Tb campaign we increased baseVm to 4Gb
+  # but now we decided to decrease it back to 3GB
+  # now we are gonna add 1Gb as a referral point into our users and
+  # when they re-create their VMs it will decrease down to 3G
+  # they can redeem their referral point from account page
+  # if you see this part after
+  # 22 Feb 2014 feel free to remove completely! ~C.S
+
+  @checkFor1GBStatus = (account, callback)->
+    account.fetchReferrers (err, referrers)=>
+      return callback err  if err
+      for ref in referrers
+        {sourceCampaign} = ref
+        return callback null, yes  if sourceCampaign is "baseVMSizeDecrease"
+      callback err, no
+
+  @add1GBDisk = (delegate, callback)->
+
+    @checkFor1GBStatus delegate, (err, used) =>
+      return callback err if err
+      if used
+        console.info "#{delegate.profile.nickname} has already get baseVMSizeDecrease point"
+        return callback null, no
+
+      referral = new JReferral {
+        type   : "disk"
+        unit   : "MB"
+        amount : 1024
+        sourceCampaign : "baseVMSizeDecrease"
+      }
+      referral.save (err) ->
+        return callback err if err
+        #add referrer as referrer to the referral system
+        delegate.addReferrer referral, (err)->
+          return callback err if err
+          return callback null, yes
+
+  @changeBaseVMForUsers = secure (client, callback)->
+    {delegate} = client.connection
+    unless delegate.profile.nickname is "cihangirsavas"
+      return callback {message: "youcannotcallthisfunction"}
+
+    selector = {
+      diskSizeInMB : $gte : 4096
+      vmType       : "user"
+      webHome      : "$not" : new RegExp "guest-"
+    }
+
+    JVM.someData selector, {webHome:1}, {}, (err, cursor)=>
+      if err then callback err
+      else
+        cursor.each (err, vm)=>
+          if err then callback err
+          else if vm?
+            nickname = vm.webHome
+            JAccount.one {'profile.nickname':nickname}, (err, account)=>
+              if err then console.error err
+              else if account?
+                @add1GBDisk account, (err, res)=>
+                  if err then console.error err
+                  else if res then console.info "decreaseVMsize point is added for", nickname
+                  else console.info "decreaseVMsize point is not added for", nickname
+              else
+                console.warn "couldnt find account", nickname
+          else
+            return callback null, "done"
+
+


JReferral: fetchClaimedAmount and its friends, a complete new logic for keeping track of referrals

--- workers/social/lib/social/models/referral/index.coffee
+++ workers/social/lib/social/models/referral/index.coffee
@@ -21,6 +21,8 @@

       static          :
         addExtraReferral:
+          (signature Object, Function)
+        fetchClaimedAmount:
           (signature Object, Function)
         fetchReferredAccounts:
           (signature Object, Object, Function)
@@ -58,7 +60,7 @@
     JAccount.some { referrerUsername : username }, options, callback

-  @addExtraReferral: secure (client, options, callback)->
+  @addExtraReferral = secure (client, options, callback)->

     {delegate} = client.connection
     unless delegate.can 'administer accounts'
@@ -83,6 +85,86 @@
           return callback null, referral

+  useDefault = (options)->
+
+    options.unit ?= "MB"
+    options.type ?= "disk"
+
+    return options
+
+
+  fetchClaimedReferral = (options, callback)->
+
+    { originId, unit, type } = useDefault options
+
+    JClaimedReferral = require './claimedreferral'
+    JClaimedReferral.one { originId, unit, type }, callback
+
+
+  @updateClaimedAmount = (options, callback)->
+
+    { originId, unit, type, amount } = useDefault options
+
+    JClaimedReferral = require './claimedreferral'
+    JClaimedReferral.update { originId, unit, type }
+    ,
+      $set: { amount }
+    ,
+      upsert: yes
+    ,
+      (err)-> callback err
+
+
+  aggregateAmount = (options, callback)->
+
+    { originId, unit, type } = useDefault options
+
+    JReferral.aggregate
+      $match     : { unit, type, originId }
+    ,
+      $group     :
+        _id      : "$originId"
+        total    :
+          $sum   : "$amount"
+
+    , (err, res)->
+
+      return callback err      if err?
+      return callback null, 0  unless res?
+
+      callback null, res[0]?.total ? 0
+
+
+  @calculateAndUpdateClaimedAmount = (options, callback)->
+
+    options = useDefault options
+
+    aggregateAmount options, (err, amount)->
+
+      return callback err  if err?
+
+      options.amount = amount
+
+      JReferral.updateClaimedAmount options, (err)->
+        return callback err  if err?
+
+        callback null, amount
+
+
+  @fetchClaimedAmount = secure (client, options, callback)->
+
+    options = useDefault options
+    options.originId = originId = client.connection.delegate.getId()
+
+    fetchClaimedReferral options, (err, claimedReferral)->
+      return callback err  if err?
+
+      if claimedReferral?
+        return callback null, claimedReferral.amount
+
+      JReferral.calculateAndUpdateClaimedAmount options, callback
+
+
   do ->

     JAccount.on 'AccountRegistered', (me, referrerCode)->
@@ -103,35 +185,43 @@
     persistReferrals = (campaign, source, target, callback)->

       referral = null
+
+      type     = campaign.campaignType
+      unit     = campaign.campaignUnit
+      originId = target.getId()
+
       queue = [
-        ->
-          referral = new JReferral
-            amount        : campaign.campaignPerEventAmount or 256
-            type          : campaign.campaignType
-            unit          : campaign.campaignUnit
-            sourceCampaign: campaign.name
+
+        ->
+
+          referral = new JReferral {
+            amount         : campaign.campaignPerEventAmount or 256
+            sourceCampaign : campaign.name
+            referredBy     : source.getId()
+            originId, type, unit
+          }

           referral.save (err) ->
             return callback err  if err
             queue.next()
-        ->
-          source.addReferrer referral, (err)->
-            if err
-              referral.remove()
-              return callback err
-            queue.next()
-        ->
-          target.addReferred referral, (err)->
-            if err
-              referral.remove()
-              return callback err
-            console.info "referal saved for #{target.profile.nickname} from #{source.profile.nickname}"
-            queue.next()
-        ->
+
+        ->
+
+          options = { originId, type, unit }
+
+          JReferral.calculateAndUpdateClaimedAmount options, (err)->
+
+            return callback err  if err
+            queue.next()
+
+        ->
+
           campaign.increaseGivenAmountSpace (err)->
             console.error "Couldnt decrease the left space", err  if err?
             callback null
+
       ]
+
       daisy queue

@@ -150,6 +240,7 @@
         ->

           JReferralCampaign.isCampaignValid (err, res)->
+
             return console.error err  if err
             return  unless res.isValid

@@ -162,6 +253,7 @@
             return console.error err  if err
             # if account not fonud then do nothing and return
             return console.error "Account couldn't found" unless myAccount
+
             me = myAccount
             queue.next()

@@ -174,16 +266,17 @@
             return console.info "User already get the referrer"

           # get referrer
-          JAccount.one {'profile.nickname': referrerUsername }, (err, referrer)->
+          JAccount.one 'profile.nickname': referrerUsername, (err, _referrer)->

             if err
               # if error occurred than do nothing and return
               return console.error "Error while fetching referrer", err

-            unless referrer
+            unless _referrer
               # if referrer not fonud then do nothing and return
               return console.error "Referrer couldnt found"

+            referrer = _referrer
             queue.next()

         ->


Add spec that up.animate returns a promise

--- spec_app/spec/javascripts/up/motion_spec.js.coffee
+++ spec_app/spec/javascripts/up/motion_spec.js.coffee
@@ -19,6 +19,19 @@
           u.setTimer 200, ->
             expect(u.opacity($element)).toBeAround(1.0, 0.25)
             done()
+
+        it 'returns a promise that is resolved when the animation completed', (done) ->
+          $element = affix('.element').text('content')
+          resolveSpy = jasmine.createSpy('resolve')
+
+          promise = up.animate($element, 'fade-in', duration: 100, easing: 'linear')
+          promise.then(resolveSpy)
+
+          u.setTimer 50, ->
+            expect(resolveSpy).not.toHaveBeenCalled()
+            u.setTimer 70, ->
+              expect(resolveSpy).toHaveBeenCalled()
+              done()

         it 'cancels an existing animation on the element by instantly jumping to the last frame', ->
           $element = affix('.element').text('content')


Implemented method call chaining

Implemented method call chaining

--- rewriter.coffee
+++ rewriter.coffee
@@ -131,7 +131,7 @@

     @scanTokens (token, i, tokens) ->
       [tag]     = token
-      [prevTag] = if i > 0 then tokens[i - 1] else []
+      [prevTag] = prevToken = if i > 0 then tokens[i - 1] else []
       [nextTag] = if i < tokens.length - 1 then tokens[i + 1] else []
       stackTop  = -> stack[stack.length - 1]
       startIdx  = i
@@ -275,7 +275,14 @@
       #       c
       #     .h a
       #
-      if prevTag is 'OUTDENT' and inImplicitCall() and tag in ['.', '?.', '::', '?::']
+      # and also
+      #
+      #     f a
+      #     .g b
+      #     .h a
+      #
+      if (prevTag is 'OUTDENT' or prevToken.newLine) and inImplicitCall() and
+          tag in ['.', '?.', '::', '?::']
         endImplicitCall()
         return forward(1)


removing a case from Rewriter#add_implicit_parentheses that apparently never happens...

--- rewriter.coffee
+++ rewriter.coffee
@@ -123,10 +123,6 @@
           last: stack.pop()
           stack[stack.length - 1]: + last
       open: stack[stack.length - 1] > 0
-      if tag is 'CALL_END' and calls < 0 and open
-        stack[stack.length - 1]: - 1
-        @tokens.splice(i, 0, ['CALL_END', ')', token[2]])
-        return 2
       if !post? or (parens is 0 and include IMPLICIT_END, tag)
         return 1 if tag is 'INDENT' and prev and include IMPLICIT_BLOCK, prev[0]
         return 1 if tag is 'OUTDENT' and token.generated


addIceCandidate() test for Remote.Peer

addIceCandidate() test for Remote.Peer

--- src/tests/spec/public/models/remote.peer.spec.coffee
+++ src/tests/spec/public/models/remote.peer.spec.coffee
@@ -300,3 +300,15 @@
 				expect(callArgs[0]).toBe('peer.addIceCandidate')
 				expect(callArgs[0]).toBe(fakeController.id)
 				expect(callArgs[0]).toBe(fakeCandidate)
+
+		describe 'when adding an ICE candidate', ->
+			it 'should relay the candidate to the RTC connection', ->
+				peer = new Peer(fakeController, '1', true, FakeRTCPeerConnection)
+				spyOn(peer._connection, 'addIceCandidate')
+
+				peer.addIceCandidate('a')
+
+				expect(peer._connection.addIceCandidate).toHaveBeenCalled()
+				callArgs = peer._connection.addIceCandidate.mostRecentCall.args
+				expect(callArgs.length).toBe(1)
+				expect(callArgs[0]).toBe('a')


Enable intra-search back button

Enable intra-search back button

--- web/site/search.coffee
+++ web/site/search.coffee
@@ -64,9 +64,9 @@

     $scope.search = (offset) ->
       if !$scope.query && !offset && $.isEmptyObject(fields) && $.isEmptyObject(metrics)
-        $location.replace().search({})
+        $location.search({})
         return
-      $location.replace()
+      $location
         .search('q', $scope.query)
         .search('volume', type?.volume)
         .search('offset', offset || undefined)


fix a regression so KDSelectBox values end up in the aggregated form data

--- components/inputs/KDSelectBox.coffee
+++ components/inputs/KDSelectBox.coffee
@@ -7,7 +7,7 @@
     super options

   setDomElement:(cssClass)->
-    @inputName  = @getOption name
+    @inputName  = @getOption "name"
     name        = "name='#{@options.name}'"
     @domElement = $ """
         <div class='kdselectbox #{cssClass}'>


make the filter input always tie up with the actual filter value (resolves #352)

--- modules/data-table/main/index.coffee
+++ modules/data-table/main/index.coffee
@@ -623,6 +623,7 @@

     selection.select('.hx-data-table-filter')
       .classed('hx-data-table-filter-visible', options.filterEnabled and not advancedSearchVisibleAndEnabled)
+      .value(@filter())

     @_.advancedSearchToggleButton.value(options.advancedSearchEnabled)


pipes could also be contained by type name


Minimise extent of deprecated macro highlighting

Trivial fix for themes that colour the background of deprecated function
calls. Having trailing whitespace and/or comments being marked in red is
slightly disconcerting.

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -665,12 +665,16 @@

 			# Deprecated macros
 			name: "invalid.deprecated.function.${2:/downcase}.ms.macro.roff"
+			contentName: "function-call.arguments.roff"
 			begin: "^([.'])[ \t]*(De|Ds)\\b"
-			end: "(?<!\\\\)(?=\n)"
+			end: "(?<!\\\\)$|(?=\\s*\\\\\")"
 			beginCaptures:
 				1: {name: "punctuation.definition.macro.roff"}
 				2: {name: "entity.function.name.roff"}
-			patterns: [{include: "#params"}]
+			patterns: [
+				{include: "#escapes"}
+				{include: "#string"}
+			]
 		},{

 			# Monospaced/constant-width text


Adding auditable.

Adding auditable.


activity: fix typo at EmojiSelector component

--- client/activity/lib/components/emojiselector/index.coffee
+++ client/activity/lib/components/emojiselector/index.coffee
@@ -7,7 +7,7 @@
 ActivityFlux      = require 'activity/flux'
 EmojiSelectorItem = require 'activity/components/emojiselectoritem'

-module.exports = class EmoiiSelector extends React.Component
+module.exports = class EmojiSelector extends React.Component

   DEFAULT_ITEMS_PER_ROW = 8
   ESC = 27
@@ -59,7 +59,7 @@
     return  unless @props.visible and event.which is ESC

     kd.utils.stopDOMEvent event
-    ActivityFlux.actions.emoji.resetCommonListFlags()
+    ActivityFlux.actions.emoji.resetCommonListFlags()

   renderList: ->
@@ -89,7 +89,7 @@
     className = classnames
       'EmojiSelector-container' : yes
       'hidden'                  : not visible
-
+

@@ -105,4 +105,4 @@

-
+


at least use showSearch boolean

at least use showSearch boolean

--- app/assets/javascripts/effective_panel_select/effective_panel_select.js.coffee
+++ app/assets/javascripts/effective_panel_select/effective_panel_select.js.coffee
@@ -55,13 +55,13 @@
         tab.attr('id', href.substring(1))

     initEvents: ->
-      @panel.on 'click', '.selection', (event) => @toggle()
+      @panel.on 'click', '.selection', (event) => @toggle() and true
       @panel.on 'click', '.selection-clear', (event) => @clear() and false
       @panel.on 'click', '[data-item-value]', (event) => @val($(event.currentTarget).data('item-value')) and false
       @panel.on 'click', '[data-fetch-item]', (event) => @fetch($(event.currentTarget).closest('[data-item-value]').data('item-value')) and false
       @panel.on 'click', '.fetched-clear', (event) => @reset() and false
       @panel.on 'click', '.fetched-select', (event) => @setVal($(event.currentTarget).data('item-value')) and false
-      @panel.on 'keyup', '.search-value', (event) => @search($(event.currentTarget).val())
+      @panel.on 'keyup', '.search-value', (event) => @search($(event.currentTarget).val()) and true
       @panel.on 'keydown', (event) => @searchVal.focus() and true

     initInvade: ->
@@ -176,14 +176,11 @@
         if tabPaneExcluded
           @tabList.find("a[href='##{$tabPane.attr('id')}']").parent('li').addClass('excluded')

-      # Activate the first tab if there are results but we can't see them
-      if results.length > 0 && @tabList.find('li.active:not(.excluded)').length == 0
-        @activateTab(results[0])
-
-      if results.length == 0
+      if results.length > 0 && @options.showSearch
+        @searchResults.html("#{results.length} result#{if results.length > 1 then 's' else ''} for '#{value}'")
+        @activateTab(results[0]) if @tabList.find('li.active:not(.excluded)').length == 0 # activate first tab if no tab is displayed
+      else
         @searchResults.html('')
-      else
-        @searchResults.html("#{results.length} result#{if results.length > 1 then 's' else ''} for '#{value}'")

       results


Post logs to /api/logging

Post logs to /api/logging

--- scripts/helpers/logging.coffee
+++ scripts/helpers/logging.coffee
@@ -6,7 +6,8 @@

   Backbone.Events.trigger = _.wrap Backbone.Events.trigger, (originalTrigger) ->
     args = _.toArray(arguments).slice(1)
-    console.log args
-    console.log console, _.flatten(['trigger', args])
+
+    log = {'msg': _.flatten(['trigger', args, console])}
+    $.post('/api/logging', log)

     originalTrigger.apply(@, args)


InputWidget : check for error

InputWidget : check for error

--- client/Social/Activity/views/inputwidget.coffee
+++ client/Social/Activity/views/inputwidget.coffee
@@ -105,12 +105,13 @@

       callback? err, activity

-      KD.showError err,
-        AccessDenied :
-          title      : "You are not allowed to #{action} activities"
-          content    : 'This activity will only be visible to you'
-          duration   : 5000
-        KodingError  : 'Something went wrong while creating activity'
+      if err
+        KD.showError err,
+          AccessDenied :
+            title      : "You are not allowed to post activity"
+            content    : 'This activity will only be visible to you'
+            duration   : 5000
+          KodingError  : 'Something went wrong while creating activity'

       KD.getSingleton("badgeController").checkBadge
         property : "statusUpdates", relType : "author", source : "JNewStatusUpdate", targetSelf : 1


Client: Activity: Tests: Updated generateDummyAccount method to set blank channel name and to be able to pass participants

--- client/app/lib/util/generateDummyChannel.coffee
+++ client/app/lib/util/generateDummyChannel.coffee
@@ -1,4 +1,7 @@
-module.exports = ({ channelId, channelName, typeConstant, privacyConstant, group, accountOldId, createdAt }) ->
+module.exports = ({ channelId, channelName, typeConstant, privacyConstant, group, accountOldId, createdAt, participantsPreview }) ->
+
+  channelName = if channelName or channelName is '' then channelName else 'dummy-channel'
+
   return {
     id                  : channelId or "6089984394406133765"
     _id                 : channelId or "6089984394406133765"
@@ -10,9 +13,9 @@
     groupName           : group or 'dummy-group'
     isParticipant       : true
     metaBits            : 0
-    name                : channelName or 'dummy-channel'
+    name                : channelName
     participantCount    : 2
-    participantsPreview : [
+    participantsPreview : participantsPreview or [
       {
         _id             : "568b34b24868cdaf029fcce4"
         constructorName : "JAccount"


Landing: handle register errors correctly

--- client/landing/site.landing/coffee/login/AppView.coffee
+++ client/landing/site.landing/coffee/login/AppView.coffee
@@ -91,7 +91,7 @@
         cssClass    : "github-login"
         partial     : "Sign in using GitHub"
         click       : -> KD.singletons.oauthController.openPopup "github"
-
+
     else
       @github = new KDCustomHTMLView
         tagName     : "a"
@@ -410,8 +410,11 @@

     if /duplicate key error/.test message
       form.emit 'EmailError'
-    else if message is 'Errors were encountered during validation'
-      form.emit 'UsernameError'
+    else if /^Errors were encountered during validation/.test message
+      if /email/.test message
+        form.emit 'EmailError'
+      else
+        form.emit 'UsernameError'
     else
       new KDNotificationView title: message


Better error reporting to browser

Better error reporting to browser

--- rest_controller.coffee
+++ rest_controller.coffee
@@ -4,6 +4,10 @@
 bbCallback = ORMUtils.bbCallback
 JSONUtils = require 'backbone-orm/lib/json_utils'
 JoinTableControllerSingleton = require './lib/join_table_controller_singleton'
+
+sendError = (res, err) ->
+  res.setHeader('Content-Type', 'text/plain')
+  return res.status(500).send(err.toString())

 module.exports = class RESTController

@@ -32,7 +36,7 @@
       cursor = @model_type.cursor(JSONUtils.parse(req.query))
       cursor = cursor.whiteList(@white_lists.index) if @white_lists.index
       cursor.toJSON (err, json) =>
-        return res.send(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         return res.json({result: json}) if cursor.hasCursorQuery('$count') or cursor.hasCursorQuery('$exists')
         unless json
           if cursor.hasCursorQuery('$one')
@@ -42,101 +46,101 @@

         if cursor.hasCursorQuery('$page')
           @render req, json.rows, (err, rendered_json) =>
-            return res.status(500).send(error: err.toString()) if err
+            return sendError(res, err) if err
             json.rows = rendered_json
             res.json(json)
         else
           @render req, json, (err, rendered_json) =>
-            return res.status(500).send(error: err.toString()) if err
+            return sendError(res, err) if err
             res.json(rendered_json)

     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   show: (req, res) =>
     try
       cursor = @model_type.cursor(req.params.id)
       cursor = cursor.whiteList(@white_lists.show) if @white_lists.show
       cursor.toJSON (err, json) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         return res.status(404).send() unless json
         json = _.pick(json, @white_lists.show) if @white_lists.show

         @render req, json, (err, json) =>
-          return res.status(500).send(error: err.toString()) if err
+          return sendError(res, err) if err
           res.json(json)

     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   create: (req, res) =>
     try
       json = JSONUtils.parse(if @white_lists.create then _.pick(req.body, @white_lists.create) else req.body)
       model = new @model_type(@model_type::parse(json))
       model.save {}, bbCallback (err) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err

         json = if @white_lists.create then _.pick(model.toJSON(), @white_lists.create) else model.toJSON()
         @render req, json, (err, json) =>
-          return res.status(500).send(error: err.toString()) if err
+          return sendError(res, err) if err
           res.json(json)

     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   update: (req, res) =>
     try
       json = JSONUtils.parse(if @white_lists.update then _.pick(req.body, @white_lists.update) else req.body)
       @model_type.find req.params.id, (err, model) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         return res.status(404).send() unless model
         model.save model.parse(json), bbCallback (err) =>
-          return res.status(500).send(error: err.toString()) if err
+          return sendError(res, err) if err

           json = if @white_lists.update then _.pick(model.toJSON(), @white_lists.update) else model.toJSON()
           @render req, json, (err, json) =>
-            return res.status(500).send(error: err.toString()) if err
+            return sendError(res, err) if err
             res.json(json)

     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   destroy: (req, res) =>
     try
       @model_type.exists req.params.id, (err, exists) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         return res.status(404).send() unless exists

         @model_type.destroy {id: req.params.id}, (err) ->
-          return res.status(500).send(error: err.toString()) if err
+          return sendError(res, err) if err
           res.status(200).send()

     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   destroyByQuery: (req, res) =>
     try
       @model_type.destroy JSONUtils.parse(req.query), (err) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         res.send(200)
     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   head: (req, res) =>
     try
       @model_type.exists req.params.id, (err, exists) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         res.send(if exists then 200 else 404)
     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   headByQuery: (req, res) =>
     try
       @model_type.exists JSONUtils.parse(req.query), (err, exists) =>
-        return res.status(500).send(error: err.toString()) if err
+        return sendError(res, err) if err
         res.send(if exists then 200 else 404)
     catch err
-      res.status(500).send(error: err.toString())
+      sendError(res, err)

   render: (req, json, callback) ->
     template_name = req.query.$render or req.query.$template or @default_template


feature: load basic parameters from uri

feature: load basic parameters from uri

--- application.coffee
+++ application.coffee
@@ -16,6 +16,7 @@
 {Animator} = require "./animator.coffee"
 {MouseToolCombo} = require "./mousetool.coffee"
 {GenericTransitionFunc, BinaryTransitionFunc,DayNightTransitionFunc,binaryTransitionFunc2GenericCode, dayNightBinaryTransitionFunc2GenericCode, parseGenericTransitionFunction, parseTransitionFunction} = require "./rule.coffee"
+{parseUri} = require "./parseuri.coffee"
 M = require "./matrix3.coffee"

 MIN_WIDTH = 100
@@ -24,6 +25,33 @@
 canvasSizeUpdateBlocked = false
 randomFillNum = 2000
 randomFillPercent = 0.4
+
+class DefaultConfig
+  getGrid: -> [7,3]
+  getCellData: -> ""
+  getFunctionCode: -> "B 3 S 2 3"
+
+class UriConfig
+  constructor: ->
+    @keys = parseUri(""+window.location).queryKey
+
+  getGrid: ->
+    if @keys.grid?
+      try
+        match = @keys.grid.match /{(\d+)[,;](\d+)}/
+        throw new Error("Syntax is bad: #{@keys.grid}") unless match
+        n = parseIntChecked match[1]
+        m = parseIntChecked match[2]
+        return [n,m]
+      catch e
+        alert "Bad grid paramters: #{@keys.grid}"
+    return [7,3]
+  getCellData: ->@keys.cells
+  getFunctionCode: ->
+    if @keys.rule?
+      @keys.rule.replace /_/g, ' '
+    else
+      "B 3 S 2 3"

 class Application
   constructor: ->
@@ -52,16 +80,22 @@
   getGroup: -> @tessellation.group
   getTransitionFunc: -> @transitionFunc

-  initialize: ->
-    @tessellation = new Tessellation 7,3
+  initialize: (config = new DefaultConfig)->
+    [n,m] = config.getGrid()
+    @tessellation = new Tessellation n, m
     console.log "Running knuth-bendix algorithm...."
     rewriteRuleset = knuthBendix vdRule @getGroup().n, @getGroup().m
     console.log "Finished"
     @appendRewrite = makeAppendRewrite rewriteRuleset
     @getNeighbors = mooreNeighborhood @getGroup().n, @getGroup().m, @appendRewrite

-    @cells = new NodeHashMap
-    @cells.put unity, 1
+    cellData = config.getCellData()
+    if cellData
+      console.log "import: #{cellData}"
+      @importData cellData
+    else
+      @cells = new NodeHashMap
+      @cells.put unity, 1

     @observer = new @ObserverClass @tessellation, @appendRewrite, minVisibleSize
     @observer.onFinish = -> redraw()
@@ -70,7 +104,7 @@
     @animator = new Animator this
     @paintStateSelector = new PaintStateSelector this, E("state-selector"), E("state-selector-buttons")

-    @transitionFunc = parseTransitionFunction "B 3 S 2 3", application.getGroup().n, application.getGroup().m
+    @transitionFunc = parseTransitionFunction config.getFunctionCode(), application.getGroup().n, application.getGroup().m
     @lastBinaryTransitionFunc = @transitionFunc

@@ -118,7 +152,16 @@
     updateCanvasSize()
     if found > 0
       @navigator.navigateToResult 0
-
+
+  importData: (data)->
+    try
+      console.log "importing #{data}"
+      @cells = importField parseFieldData data
+      console.log "Imported #{@cells.count} cells"
+    catch e
+      alert "Faield to import data: #{e}"
+      @cells = new NodeHashMap
+

 updateCanvasSize = ->
   return if canvasSizeUpdateBlocked
@@ -233,7 +276,7 @@
 context = canvas.getContext "2d"

 application = new Application
-application.initialize()
+application.initialize new UriConfig

 dragHandler = null

@@ -517,8 +560,7 @@

 doImport = ->
   try
-    data = parseFieldData E('import').value
-    application.cells = importField data
+    application.importData E('import').value
     updatePopulation()
     redraw()
     E('import-dialog').style.display = 'none'


 Merge branch 'master.groups.envUpdate' into 'master.groups'

Master.Groups.Env Update

--- client/Main/payment/subscriptionusageview.coffee
+++ client/Main/payment/subscriptionusageview.coffee
@@ -30,6 +30,10 @@

     items = @getGauges()
     controller.instantiateListItems items
+    KD.getSingleton("vmController").on 'VMListChanged', =>
+      items = @getGauges()
+      controller.removeAllItems()
+      controller.instantiateListItems items

     controller


client/app: remove sortEnvironmentStacks util

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/util/sortEnvironmentStacks.coffee
+++ client/app/lib/util/sortEnvironmentStacks.coffee
@@ -1,27 +0,0 @@
-isManagedVMStack = require './isManagedVMStack'
-
-module.exports = (stacks = []) ->
-
-
-  stacks.sort (a, b) ->
-
-    return  1  if isManagedVMStack b
-    return -1
-
-
-  stacks.sort (a, b) ->
-
-    return  1  if a.config?.oldOwner
-    return -1
-
-
-  stacks.sort (a, b) ->
-
-    return -1  if a.config?.groupStack
-    return  1
-
-
-  stacks.sort (a, b) ->
-
-    return -1  if new Date(a.meta.modifiedAt) > new Date(b.meta.modifiedAt)
-    return  0


Fix opening multiple Terminal instances from JS console

If we open the browser's console and do

appManager.openApplication("WebTerm");
appManager.openApplication("WebTerm");

It would crash because two requests to tellKite to
fetch the same exchange.

Put a delay to avoid refetching.

Also bind the kite queue to "disconnected" event.

--- client/landing/app/MainApp/account-mixins.coffee
+++ client/landing/app/MainApp/account-mixins.coffee
@@ -78,14 +78,37 @@

       getChannelName =(kiteName)-> "private-kite-#{kiteName}"

-      fetchChannel =(kiteName, callback)->
+      # A helper to delay a call until some condition is met
+      # Types:
+      #  condition = func() -> boolean
+      #  delay = integer (optional), default is 100ms.
+      #  callback = func()
+      waitUntil = (condition, delay, callback) ->
+        unless callback
+          callback = delay
+          delay = 100
+        g = ->
+          if condition()
+            callback()
+            clearInterval h
+        h = setInterval g, delay
+
+      fetchChannel =(kiteName, callback)->
         channelName = getChannelName(kiteName)
         unless channels[channelName]
+          # Use a cheap hack to ensure the next immediate call to
+          # this will not fetch channel again
+          channels[channelName] = true
           KD.remote.fetchChannel channelName, (channel) ->
             channels[channelName] = channel
             callback channel
         else
-          callback channels[channelName]
+          # Because we set channels[channelName] to true when there
+          # are consecutive calls to this, we want it to be a Channel
+          # instead, so we wait until the first call is finish.
+          condition = -> channels[channelName] instanceof Channel
+          waitUntil condition, ->
+            callback channels[channelName]

       (options, callback=->)->
         scrubber = new Scrubber localStore


test/myteamhelpers: add invite users function

--- client/test/lib/helpers/myteamhelpers.coffee
+++ client/test/lib/helpers/myteamhelpers.coffee
@@ -191,3 +191,15 @@
       .pause 5000, -> callback()

+  inviteUsers: (browser, invitations, callback) ->
+
+    fn = ( invitations, done ) ->
+      _remote.api.JInvitation.create {invitations: invitations}, (err) ->
+        done err
+
+    browser
+      .timeoutsAsyncScript 10000
+      .executeAsync  fn, [ invitations ], (result) ->
+        callback()
+
+


Toolbar: simplify selected credential check by using JStackTemplate helper

--- client/new-stack-editor/lib/views/toolbar.coffee
+++ client/new-stack-editor/lib/views/toolbar.coffee
@@ -46,13 +46,8 @@

     { _id, accessLevel, credentials, title } = data

-    if data.accessLevel is 'group'
-      accessLevel = 'team'
-
-    if credentials and (providers = Object.keys credentials).length
-      count = 0
-      for provider in providers when provider isnt 'custom'
-        count += credentials[provider].length
+    accessLevel = 'team'  if data.accessLevel is 'group'
+    count = data.getCredentialIdentifiers?().length ? 0

     credentials = if count
     then "#{count} credential#{if count > 1 then 's' else ''} is set"


fix for funky race condition (which you can reproduce by setting the timout to 100 for instance

--- core/app/backbone/collections/channel_activities.coffee
+++ core/app/backbone/collections/channel_activities.coffee
@@ -5,7 +5,8 @@
     @channel = opts.channel;
     @_count = new ChannelActivityCount {}, channel_activity_collection: @
     @_count.bind 'change', @update_count, @
-    @on 'reset add remove', @onReset, @
+    @on 'reset add remove', @setLatestTimestamp, @
+    @setLatestTimestamp()

   update_count: ->
     @trigger 'change_count'
@@ -19,8 +20,9 @@
   fetch_count: (args...)->
     @_count.fetch(args...)

-  onReset: ->
-    @_count.setTimestamp @latest_timestamp()
+  setLatestTimestamp: ->
+    timestamp = @latest_timestamp()
+    @_count.setTimestamp timestamp if timestamp

   url: -> '/' + this.channel.get('created_by').username + '/channels/' + this.channel.get('id') + '/activities';
   link: -> @url()
@@ -32,12 +34,24 @@
 class ChannelActivityCount extends Backbone.Model
   initialize: (attributes, options) ->
     @collection_url = options.channel_activity_collection.url()
+    @set timestamp: (@get('timestamp') || 0)

   url: -> @collection_url + "/count.json?timestamp=#{@get('timestamp')}"
+
+  fetch: (args...) ->
+    super(args...)
+
+  parse: (response)->
+    response = super(response)
+    if response.timestamp == @get('timestamp')
+      response
+    else
+      {count: 0}

   count: ->
     @get('count') || 0

   setTimestamp: (timestamp) ->
-    @set(timestamp: timestamp)
-    @set(count: null)
+    @set
+      timestamp: timestamp
+      count: 0


Removed unused method

Removed unused method

--- core/app/backbone/models/user.coffee
+++ core/app/backbone/models/user.coffee
@@ -55,9 +55,6 @@
       stream_path: "/#{username}/channels/#{@get('all_channel_id')}/activities"
       profile_path: "/#{username}"

-  followed_by_current_user: ->
-    @followers.contains window.currentUser
-
   follow: ->
     @followers.addFollower(window.currentUser)


Make size test a little fuzzier so it passes on chrome+FF+phantomjs.

--- core/spec/javascripts/factlink/text_area_view_spec.coffee
+++ core/spec/javascripts/factlink/text_area_view_spec.coffee
@@ -29,7 +29,7 @@
     expect(model.get('text')).to.equal 'bla - bla!'

   it "is initially small and scrollbarless", ->
-    expect(textarea().height()).to.be.below 32
+    expect(textarea().height()).to.be.below 45
     expect(textarea()[0].scrollHeight).to.equal textarea()[0].clientHeight

   it "remains small but gains a scrollbar when text doesn't fit", ->


Improve copy of students Permissions panel in onboarding

--- app/assets/javascripts/components/onboarding/index.cjsx
+++ app/assets/javascripts/components/onboarding/index.cjsx
@@ -158,7 +158,7 @@

Permissions

-            Once you´ve signed in, this website will make automatic edits using your Wikipedia account, reflecting actions you take here. Your account will be used to update wiki pages when:
+            Once you´ve signed in, this website will make automatic edits using your Wikipedia account, reflecting actions you take here. Your account will be used to update wiki pages to:

set up a sandbox page where you can practice editing


Use interval instead of recreating timers in live

--- app/assets/javascripts/uploadcare/widget/live.js.coffee
+++ app/assets/javascripts/uploadcare/widget/live.js.coffee
@@ -29,10 +29,8 @@
     if el.length && (!template || el[0] != template.content[0])
       el.remove()

+  live = -> uploadcare.initialize()
   if uploadcare.defaults.live
-    live = ->
-      uploadcare.initialize()
-      setTimeout(live, 100)
+    $ -> setInterval(live, 100)
+  else
     $(live)
-  else
-    $ -> uploadcare.initialize()


activity: set default wrapper and scroll view for topic pane

--- client/Social/Activity/views/topicmessagepane.coffee
+++ client/Social/Activity/views/topicmessagepane.coffee
@@ -1,6 +1,9 @@
 class TopicMessagePane extends MessagePane

   constructor: (options = {}, data) ->
+
+    options.wrapper    ?= no
+    options.scrollView ?= no

     super options, data

@@ -9,6 +12,7 @@
       .on 'MessageRemoved', @bound 'removeMessage'

     @filterLinks = null
+

   addMessage: (message) ->


social: fix location is deleted with message update bug

--- client/activity/lib/views/activityinputwidget.coffee
+++ client/activity/lib/views/activityinputwidget.coffee
@@ -1,3 +1,4 @@
+_                       = require 'lodash'
 kd                      = require 'kd'
 KDButtonView            = kd.ButtonView
 KDCustomHTMLView        = kd.CustomHTMLView
@@ -79,9 +80,11 @@
     return  if @locked
     return @reset yes  unless body = value.trim()

-    activity       = @getData()
-    {app, channel} = @getOptions()
-    payload        = @getPayload()
+    activity        = @getData()
+    {app, channel}  = @getOptions()
+    embedBoxPayload = @getEmbedBoxPayload()
+
+    payload = _.assign {}, activity?.payload, embedBoxPayload

     timestamp       = Date.now()
     clientRequestId = generateFakeIdentifier timestamp
@@ -169,7 +172,7 @@
     else kd.utils.wait 8000, @bound 'unlockSubmit'

-  getPayload: -> return @embedBox.getData()
+  getEmbedBoxPayload: -> return @embedBox.getData()

   showError: (err, options = {}) ->


update migrate from solo text to migrate

--- client/home/lib/flux/stores/welcomestepsstore.coffee
+++ client/home/lib/flux/stores/welcomestepsstore.coffee
@@ -114,7 +114,7 @@
     steps.setIn [ 'common', 'migrateFromKoding' ], toImmutable
       path: '/MigrateFromSolo'
       title: 'Migrate from Solo'
-      actionTitle: 'Migrate from Solo'
+      actionTitle: 'Migrate'
       videoLink: ''
       description: 'You can migrate your solo machines to team!'
       isDone: no