Remove old code.g

Remove old code.g

--- source/javascripts/canvas.coffee
+++ source/javascripts/canvas.coffee
@@ -1,29 +0,0 @@
-Grasp.Canvas = Backbone.View.extend
-  initialize: (el) ->
-    @el = el
-    @canvas = new fabric.Canvas(@el.get(0), selection: false)
-    @objects = [ ]
-
-    @canvas.on "mouse:down", (info) =>
-      offset = @el.offset()
-      x = info.e.pageX - offset.left
-      y = info.e.pageY - offset.top
-
-      switch Grasp.options.currentElementType()
-        when "line"   then @startLine(info.target, x, y)
-        when "circle" then @startCircle(info.target, x ,y)
-        when "trash"  then @trashElement(info.target)
-
-  trashElement: (target) ->
-    return unless target?
-
-    _.each @objects, (obj) ->
-      obj.dispose() if obj.id == target.unique_id
-
-  startLine: (target, x, y) ->
-    return if target? # Only return if outside of an existing element.
-    @objects.push new Grasp.Line(canvas: @canvas, coords: [x, y, x, y])
-
-  startCircle: (target, x, y) ->
-    return if target? # Only return if outside of an existing element.
-    @objects.push new Grasp.Circle(canvas: @canvas, coords: [x, y])


clamps all axis scales (prevents bars outside of plane)

--- src/viz/types/helpers/graph/includes/axes.coffee
+++ src/viz/types/helpers/graph/includes/axes.coffee
@@ -212,7 +212,7 @@
   vars[axis].scale.ticks = t

   d3.scale[scaleType]()
-    .domain(range).range(rangeArray)
+    .domain(range).range(rangeArray).clamp(true)

 sizeScale = (vars, value) ->


Update tests

Update tests

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -1493,26 +1493,24 @@
         LocalStorage.clear()

       describe "when attempting to paste a directory into itself", ->
-        handlers = null
-
         describe "when copied", ->
           beforeEach ->
             LocalStorage['tree-view:copyPath'] = JSON.stringify([dirPath])
-            handlers = treeView.emitter.handlersByEventName
-
+
+          it "makes a copy inside itself", ->
+            newPath = path.join(dirPath, path.basename(dirPath))
             dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
-
-            newPath = path.join(dirPath, path.basename(dirPath))
             expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow()
             expect(fs.existsSync(newPath)).toBeTruthy()

           it "dispatches an event to the tree-view", ->
-            treeView.onEntryCopied ->
-            spyOn(handlers['entry-copied'], '0')
+            newPath = path.join(dirPath, path.basename(dirPath))
+            callback = jasmine.createSpy("onEntryCopied")
+            treeView.onEntryCopied(callback)

             dirView.click()
-            expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow()
-            expect(handlers['entry-copied'][0].callCount).toEqual(1)
+            atom.commands.dispatch(treeView.element, "tree-view:paste")
+            expect(callback).toHaveBeenCalledWith(initialPath: dirPath, newPath: newPath)

           it 'does not keep copying recursively', ->
             LocalStorage['tree-view:copyPath'] = JSON.stringify([dirPath])
@@ -1554,14 +1552,21 @@
             LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath])

           it "creates a copy of the original file in the selected file's parent directory", ->
-            treeView.onEntryCopied ->
-            spyOn(treeView.emitter.handlersByEventName['entry-copied'], '0')
             fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
             atom.commands.dispatch(treeView.element, "tree-view:paste")

-            expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy()
+            newPath = path.join(dirPath2, path.basename(filePath))
+            expect(fs.existsSync(newPath)).toBeTruthy()
             expect(fs.existsSync(filePath)).toBeTruthy()
-            expect(treeView.emitter.handlersByEventName['entry-copied'][0].callCount).toEqual(1)
+
+          it "emits an event", ->
+            callback = jasmine.createSpy("onEntryCopied")
+            treeView.onEntryCopied(callback)
+            fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
+            atom.commands.dispatch(treeView.element, "tree-view:paste")
+
+            newPath = path.join(dirPath2, path.basename(filePath))
+            expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath: newPath})

           describe "when the target already exists", ->
             it "appends a number to the destination name", ->
@@ -1713,27 +1718,33 @@
               expect(fs.existsSync(path.join(dirPath, "test-file30.txt"))).toBeTruthy()

       describe "when a file has been cut", ->
-        handlers = null
-
         beforeEach ->
           LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath])
-          handlers = treeView.emitter.handlersByEventName
-          treeView.onEntryMoved ->
-          spyOn(handlers['entry-moved'], '0')

         describe "when a file is selected", ->
           it "creates a copy of the original file in the selected file's parent directory and removes the original", ->
-            LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath])
-
             fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
             atom.commands.dispatch(treeView.element, "tree-view:paste")

-            expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy()
+            newPath = path.join(dirPath2, path.basename(filePath))
+            expect(fs.existsSync(newPath)).toBeTruthy()
             expect(fs.existsSync(filePath)).toBeFalsy()
-            expect(handlers['entry-moved'][0].callCount).toEqual(1)
+
+          it "emits an event", ->
+            callback = jasmine.createSpy("onEntryMoved")
+            treeView.onEntryMoved(callback)
+
+            fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
+            atom.commands.dispatch(treeView.element, "tree-view:paste")
+
+            newPath = path.join(dirPath2, path.basename(filePath))
+            expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath})

           describe 'when the target destination file exists', ->
             it 'does not move the cut file', ->
+              callback = jasmine.createSpy("onEntryMoved")
+              treeView.onEntryMoved(callback)
+
               filePath3 = path.join(dirPath2, "test-file.txt")
               fs.writeFileSync(filePath3, "doesn't matter")

@@ -1741,39 +1752,49 @@
               atom.commands.dispatch(treeView.element, "tree-view:paste")

               expect(fs.existsSync(filePath)).toBeTruthy()
-              expect(handlers['entry-moved'][0].callCount).toEqual(0)
+              expect(callback).not.toHaveBeenCalled()

         describe "when a directory is selected", ->
           it "creates a copy of the original file in the selected directory and removes the original", ->
             LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath])

+            callback = jasmine.createSpy("onEntryMoved")
+            treeView.onEntryMoved(callback)
             dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
             atom.commands.dispatch(treeView.element, "tree-view:paste")

-            expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy()
+            newPath = path.join(dirPath2, path.basename(filePath))
+            expect(fs.existsSync(newPath)).toBeTruthy()
             expect(fs.existsSync(filePath)).toBeFalsy()
-            expect(handlers['entry-moved'][0].callCount).toEqual(1)
+            expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath})

       describe "when multiple files have been cut", ->
         describe "when a file is selected", ->
-          handlers = null
-
           beforeEach ->
-            handlers = treeView.emitter.handlersByEventName
-            treeView.onEntryMoved ->
-            spyOn(handlers['entry-moved'], '0')
+            LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath2, filePath3])

           it "moves the selected files to the parent directory of the selected file", ->
-            LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath2, filePath3])
-
             fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
             atom.commands.dispatch(treeView.element, "tree-view:paste")

-            expect(fs.existsSync(path.join(dirPath, path.basename(filePath2)))).toBeTruthy()
-            expect(fs.existsSync(path.join(dirPath, path.basename(filePath3)))).toBeTruthy()
+            newPath2 = path.join(dirPath, path.basename(filePath2))
+            newPath3 = path.join(dirPath, path.basename(filePath3))
+            expect(fs.existsSync(newPath2)).toBeTruthy()
+            expect(fs.existsSync(newPath3)).toBeTruthy()
             expect(fs.existsSync(filePath2)).toBeFalsy()
             expect(fs.existsSync(filePath3)).toBeFalsy()
-            expect(handlers['entry-moved'][0].callCount).toEqual(2)
+
+          it "emits events", ->
+            callback = jasmine.createSpy("onEntryMoved")
+            treeView.onEntryMoved(callback)
+            fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
+            atom.commands.dispatch(treeView.element, "tree-view:paste")
+
+            newPath2 = path.join(dirPath, path.basename(filePath2))
+            newPath3 = path.join(dirPath, path.basename(filePath3))
+            expect(callback.callCount).toEqual(2)
+            expect(callback).toHaveBeenCalledWith({initialPath: filePath2, newPath: newPath2})
+            expect(callback).toHaveBeenCalledWith({initialPath: filePath3, newPath: newPath3})

           describe 'when the target destination file exists', ->
             it 'does not move the cut file', ->
@@ -1818,14 +1839,12 @@
           expect(atom.notifications.getNotifications()[0].getDetail()).toContain 'ENOENT: no such file or directory'

     describe "tree-view:add-file", ->
-      [addPanel, addDialog, handlers] = []
+      [addPanel, addDialog, callback] = []

       beforeEach ->
         jasmine.attachToDOM(workspaceElement)
-
-        handlers = treeView.emitter.handlersByEventName
-        treeView.onFileCreated ->
-        spyOn(handlers['file-created'], '0')
+        callback = jasmine.createSpy("onFileCreated")
+        treeView.onFileCreated(callback)

         waitsForFileToOpen ->
           fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
@@ -1851,7 +1870,7 @@

         describe "when the path without a trailing '#{path.sep}' is changed and confirmed", ->
           describe "when no file exists at that location", ->
-            it "adds a file, closes the dialog and selects the file in the tree-view", ->
+            it "adds a file, closes the dialog, selects the file in the tree-view, and emits an event", ->
               newPath = path.join(dirPath, "new-test-file.txt")

               waitsForFileToOpen ->
@@ -1867,8 +1886,8 @@
                 dirView.entries.querySelectorAll(".file").length > 1

               runs ->
-                expect(handlers['file-created'][0]).toHaveBeenCalled()
                 expect(treeView.element.querySelector('.selected').textContent).toBe path.basename(newPath)
+                expect(callback).toHaveBeenCalledWith({path: newPath})

             it "adds file in any project path", ->
               newPath = path.join(dirPath3, "new-test-file.txt")
@@ -1892,8 +1911,8 @@
                 dirView3.entries.querySelectorAll(".file").length > 1

               runs ->
-                expect(handlers['file-created'][0]).toHaveBeenCalled()
                 expect(treeView.element.querySelector('.file.selected').textContent).toBe path.basename(newPath)
+                expect(callback).toHaveBeenCalledWith({path: newPath})

           describe "when a file already exists at that location", ->
             it "shows an error message and does not close the dialog", ->
@@ -1905,7 +1924,7 @@
               expect(addDialog.errorMessage.textContent).toContain 'already exists'
               expect(addDialog.element).toHaveClass('error')
               expect(atom.workspace.getModalPanels()[0]).toBe addPanel
-              expect(handlers['file-created'][0]).not.toHaveBeenCalled()
+              expect(callback).not.toHaveBeenCalled()

           describe "when the project has no path", ->
             it "adds a file and closes the dialog", ->
@@ -1925,7 +1944,7 @@
                 expect(fs.isFileSync(newPath)).toBeTruthy()
                 expect(atom.workspace.getModalPanels().length).toBe 0
                 expect(atom.workspace.getActivePaneItem().getPath()).toBe(newPath)
-                expect(handlers['file-created'][0]).toHaveBeenCalled()
+                expect(callback).toHaveBeenCalledWith({path: newPath})

         describe "when the path with a trailing '#{path.sep}' is changed and confirmed", ->
           it "shows an error message and does not close the dialog", ->
@@ -1935,12 +1954,14 @@
             expect(addDialog.errorMessage.textContent).toContain 'names must not end with'
             expect(addDialog.element).toHaveClass('error')
             expect(atom.workspace.getModalPanels()[0]).toBe addPanel
+            expect(callback).not.toHaveBeenCalled()

         describe "when 'core:cancel' is triggered on the add dialog", ->
           it "removes the dialog and focuses the tree view", ->
             atom.commands.dispatch addDialog.element, 'core:cancel'
             expect(atom.workspace.getModalPanels().length).toBe 0
             expect(document.activeElement).toBe(treeView.element.querySelector(".tree-view"))
+            expect(callback).not.toHaveBeenCalled()

         describe "when the add dialog's editor loses focus", ->
           it "removes the dialog and focuses root view", ->
@@ -1959,6 +1980,7 @@
             runs ->
               expect(fs.isFileSync(newPath)).toBeTruthy()
               expect(atom.workspace.getActivePaneItem().getPath()).toBe newPath
+              expect(callback).toHaveBeenCalledWith({path: newPath})

       describe "when a directory is selected", ->
         it "opens an add dialog with the directory's path populated", ->
@@ -2006,14 +2028,12 @@
           expect(addDialog.element.textContent).toContain("You must open a directory to create a file with a relative path")

     describe "tree-view:add-folder", ->
-      [addPanel, addDialog, handlers] = []
+      [addPanel, addDialog, callback] = []

       beforeEach ->
         jasmine.attachToDOM(workspaceElement)
-
-        handlers = treeView.emitter.handlersByEventName
-        treeView.onDirectoryCreated ->
-        spyOn(handlers['directory-created'], '0')
+        callback = jasmine.createSpy("onDirectoryCreated")
+        treeView.onDirectoryCreated(callback)

         waitsForFileToOpen ->
           fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
@@ -2044,11 +2064,11 @@

               expect(document.activeElement).toBe(treeView.element.querySelector(".tree-view"))
               expect(dirView.querySelector('.directory.selected').textContent).toBe('new')
-              expect(handlers['directory-created'][0].callCount).toBe 1
+              expect(callback).toHaveBeenCalledWith({path: newPath})

         describe "when the path with a trailing '#{path.sep}' is changed and confirmed", ->
           describe "when no directory exists at the given path", ->
-            it "adds a directory and closes the dialog", ->
+            it "adds a directory, closes the dialog, and emits an event", ->
               newPath = path.join(dirPath, 'new', 'dir')
               addDialog.miniEditor.insertText("new#{path.sep}dir#{path.sep}")
               atom.commands.dispatch addDialog.element, 'core:confirm'
@@ -2058,7 +2078,7 @@

               expect(document.activeElement).toBe(treeView.element.querySelector(".tree-view"))
               expect(dirView.querySelector('.directory.selected').textContent).toBe('new')
-              expect(handlers['directory-created'][0].callCount).toBe(1)
+              expect(callback).toHaveBeenCalledWith({path: newPath + path.sep})

             it "selects the created directory and does not change the expansion state of existing directories", ->
               expandedPath = path.join(dirPath, 'expanded-dir')
@@ -2077,8 +2097,8 @@

               expect(document.activeElement).toBe(treeView.element.querySelector(".tree-view"))
               expect(dirView.querySelector('.directory.selected').textContent).toBe('new2')
-              expect(handlers['directory-created'][0].callCount).toBe(1)
               expect(treeView.entryForPath(expandedPath).isExpanded).toBeTruthy()
+              expect(callback).toHaveBeenCalledWith({path: newPath})

             describe "when the project has no path", ->
               it "adds a directory and closes the dialog", ->
@@ -2093,8 +2113,8 @@
                 addDialog.miniEditor.insertText(newPath)
                 atom.commands.dispatch addDialog.element, 'core:confirm'
                 expect(fs.isDirectorySync(newPath)).toBeTruthy()
-                expect(handlers['directory-created'][0].callCount).toBe 1
                 expect(atom.workspace.getModalPanels().length).toBe 0
+                expect(callback).toHaveBeenCalledWith({path: newPath})

           describe "when a directory already exists at the given path", ->
             it "shows an error message and does not close the dialog", ->
@@ -2106,18 +2126,16 @@
               expect(addDialog.errorMessage.textContent).toContain 'already exists'
               expect(addDialog.element).toHaveClass('error')
               expect(atom.workspace.getModalPanels()[0]).toBe addPanel
-              expect(handlers['directory-created'][0].callCount).toBe 0
+              expect(callback).not.toHaveBeenCalled()

     describe "tree-view:move", ->
       describe "when a file is selected", ->
-        [moveDialog, handlers] = []
+        [moveDialog, callback] = []

         beforeEach ->
           jasmine.attachToDOM(workspaceElement)
-
-          handlers = treeView.emitter.handlersByEventName
-          treeView.onEntryMoved ->
-          spyOn(handlers['entry-moved'], '0')
+          callback = jasmine.createSpy("onEntryMoved")
+          treeView.onEntryMoved(callback)

           waitsForFileToOpen ->
             fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))
@@ -2140,7 +2158,7 @@

         describe "when the path is changed and confirmed", ->
           describe "when all the directories along the new path exist", ->
-            it "moves the file, updates the tree view, and closes the dialog", ->
+            it "moves the file, updates the tree view, closes the dialog, and emits an event", ->
               newPath = path.join(rootDirPath, 'renamed-test-file.txt')
               moveDialog.miniEditor.setText(path.basename(newPath))

@@ -2158,7 +2176,7 @@
                 dirView = treeView.roots[0].querySelector('.directory')
                 dirView.expand()
                 expect(dirView.entries.children.length).toBe 0
-                expect(handlers['entry-moved'][0].callCount).toBe 1
+                expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath})

           describe "when the directories along the new path don't exist", ->
             it "creates the target directory before moving the file", ->
@@ -2174,7 +2192,7 @@
               runs ->
                 expect(fs.existsSync(newPath)).toBeTruthy()
                 expect(fs.existsSync(filePath)).toBeFalsy()
-                expect(handlers['entry-moved'][0].callCount).toBe 1
+                expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath})

           describe "when a file or directory already exists at the target path", ->
             it "shows an error message and does not close the dialog", ->
@@ -2188,7 +2206,7 @@
                 expect(moveDialog.errorMessage.textContent).toContain 'already exists'
                 expect(moveDialog.element).toHaveClass('error')
                 expect(moveDialog.element.parentElement).toBeTruthy()
-                expect(handlers['entry-moved'][0].callCount).toBe 0
+                expect(callback).not.toHaveBeenCalled()

         describe "when 'core:cancel' is triggered on the move dialog", ->
           it "removes the dialog and focuses the tree view", ->
@@ -2770,10 +2788,10 @@

       describe "when the file is deleted", ->
         it "updates the style of the directory", ->
-          handlers = treeView.emitter.handlersByEventName
-          treeView.onEntryDeleted ->
-          spyOn(handlers['entry-deleted'], '0')
-
+          callback = jasmine.createSpy("onEntryDeleted")
+          treeView.onEntryDeleted(callback)
+
+          deletedPath = treeView.selectedEntry().getPath()
           expect(treeView.selectedEntry().getPath()).toContain(path.join('dir2', 'new2'))
           dirView = findDirectoryContainingText(treeView.roots[0], 'dir2')
           expect(dirView).not.toBeNull()
@@ -2782,7 +2800,7 @@
             dialog.buttons["Move to Trash"]()
           atom.commands.dispatch(treeView.element, 'tree-view:remove')
           expect(dirView.directory.updateStatus).toHaveBeenCalled()
-          expect(handlers['entry-deleted'][0]).toHaveBeenCalled()
+          expect(callback).toHaveBeenCalledWith({path: deletedPath})

     describe "on #darwin, when the project is a symbolic link to the repository root", ->
       beforeEach ->


Fix indentation

Fix indentation

--- app/assets/javascripts/admin/taxon_concept.js.coffee
+++ app/assets/javascripts/admin/taxon_concept.js.coffee
@@ -8,7 +8,7 @@
   $('.select2').select2({
     placeholder: "Choose an option",
     allowClear: true
-    })
+  })

   $(".datepicker").datepicker(
     format: "dd/mm/yyyy",


fix week title formatting

fix week title formatting

--- app/assets/javascripts/components/timeline/week.cjsx
+++ app/assets/javascripts/components/timeline/week.cjsx
@@ -110,7 +110,7 @@

         {week_add_delete}
         {week_dates}
-
{'Week' + @props.index}

+
{'Week ' + @props.index}

       {week_content}


Cache shortened URLs

Cache shortened URLs

--- gitcommander.coffee
+++ gitcommander.coffee
@@ -83,17 +83,29 @@
   if nofEvents
     console.log "* Seen #{nofEvents} new events (#{events.length} total)."

+repoUrls = {}
 announceEvent = (event) ->
   weburl = "https://github.com/#{event.repo.name}"
-  gitio weburl, (err, res) ->
-    if err? or not res?
-      console.log "! Error when shortening URL #{weburl}."
-      event.repo.weburl = weburl
-    else
-      event.repo.weburl = res

+  # do we already have a shortened url for this one?
+  if event.repo.name of repoUrls
+    event.repo.weburl = repoUrls[event.repo.name]
+
+    # tell immediately
     for channel in CHANNELS
       eventTeller[event.type] channel, event
+  else
+    gitio weburl, (err, res) ->
+      if err? or not res?
+        console.log "! Error when shortening URL #{weburl}."
+        event.repo.weburl = weburl
+      else
+        # remember the url in case we need it again
+        repoUrls[event.repo.name] = res
+        event.repo.weburl = res
+
+      for channel in CHANNELS
+        eventTeller[event.type] channel, event

 eventTeller =
   "PushEvent": (channel, event) ->


routes for papers. Submitted only works just now but going to generalize soon

--- app/assets/javascripts/routes/papers_route.js.coffee
+++ app/assets/javascripts/routes/papers_route.js.coffee
@@ -7,9 +7,18 @@
     Thoj.Paper.asReviewer()

 Theoj.PapersSubmittedRoute = Ember.Route.extend
-  model : ->
+  controllerName: "paper_list"
+
+  model:->
     Theoj.Paper.asAuthor()

-Theoj.PapersIndexRoute   = Ember.Route.extend
-  model : (params) ->
+  setupController:(controller)->
+    controller.set("paperType", "Submitted")
+
+  renderTemplate: ->
+    @render('papers/papers_list')
+
+
+Theoj.PaperRoute = Ember.Route.extend
+  model :(params)->
     Theoj.Paper.get(params.id)


Added fixed version of cloud toggle

Added fixed version of cloud toggle

--- app/assets/javascripts/views/clouds/toggle.js.coffee
+++ app/assets/javascripts/views/clouds/toggle.js.coffee
@@ -7,7 +7,7 @@
   className: 'cloud-toggle'

   events: {
-    'click' : 'toggle'
+    'click' : 'activate'
   }

   initialize: ->
@@ -19,18 +19,15 @@
     this

   bind: ->
-    $(@el).bind 'clouds:show', (event,cloud_id) =>
-      $(@el).addClass('active') if cloud_id == @model.id
+    $(@el).bind 'page:show', (event,page_id) =>
+      if page_id == @model.id
+        $(@el).addClass('active')
+      else
+        $(@el).removeClass('active')

-    # nfc.on "#{@model.type}:#{@model.id}:chat", (payload) ->
-    #   console.log payload
+    nfc.on "#{@model.type}:#{@model.id}:chat", (payload) ->
+      console.log payload

-  toggle: ->
-    $('.cloud-toggle').removeClass('active')
-    $(@el).addClass('active')
+  activate: ->
     Backbone.history.navigate("/clouds/#{@model.id}",true)
-    false
-
-    # console.log Backbone.Router()
-    # if @model.active then  else
-      +    false


fix URI parser misinterpreting URIs with @ in the path

User and password fields can no longer contain / either. When is the auth
field even used anymore?


:green_heart: Revert changes in element specs

--- spec/minimap-element-spec.coffee
+++ spec/minimap-element-spec.coffee
@@ -244,8 +244,8 @@
           nextAnimationFrame()

           expect(minimapElement.drawLines).toHaveBeenCalled()
-          expect(minimapElement.drawLines.argsForCall[1][1]).toEqual(110)
-          expect(minimapElement.drawLines.argsForCall[1][2]).toEqual(110)
+          expect(minimapElement.drawLines.argsForCall[1][1]).toEqual(100)
+          expect(minimapElement.drawLines.argsForCall[1][2]).toEqual(101)

       describe 'when the editor visibility change', ->
         it 'does not modify the size of the canvas', ->


Remove some console.log calls

Remove some console.log calls

--- lib/script-options-view.coffee
+++ lib/script-options-view.coffee
@@ -57,15 +57,9 @@

   toggleScriptOptions: (command) ->
     switch command
-      when 'show'
-        console.log 'toggle show' # then
-        @scriptOptionsView.show()
-      when 'hide'
-        console.log 'toggle hide' # then
-        @scriptOptionsView.hide()
-      else
-        console.log 'toggle called'
-        @scriptOptionsView.toggle()
+      when 'show' then @scriptOptionsView.show()
+      when 'hide' then @scriptOptionsView.hide()
+      else @scriptOptionsView.toggle()

   saveOptions: ->
     splitArgs = (element) ->


Decode etag query param, in case it contains HTTP-encoded characters

--- proxy.coffee
+++ proxy.coffee
@@ -79,7 +79,7 @@
       # immediately give a 304 response. This prevents the overhead from piping
       # these common requests to the static daemon.
       if options.headers?['if-none-match'] && localDigestMatch = url.match(/etag=([^&]+)/)
-        if options.headers['if-none-match'] is localDigestMatch[1]
+        if options.headers['if-none-match'] is decodeURIComponent(localDigestMatch[1])
           res.writeHead(304, {
             server: 'vee'
           });


let the user know if he's adding a duplicate domain

--- client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -193,6 +193,8 @@
         createButton.hideLoader()
         if err
           warn err
+          if err.message?.indexOf("duplicate key error") isnt -1
+            return notifyUser "The domain #{domainName} already exists."
           return notifyUser "An error occured. Please try again later."
         else
           @showSuccess()
@@ -205,6 +207,8 @@
         createButton.hideLoader()
         if err
           warn err
+          if err.message?.indexOf("duplicate key error") isnt -1
+            return notifyUser "The domain #{domainName} already exists."
           return notifyUser "An error occured. Please try again later."
         else
           @showSuccess()


make scrollbar darker

make scrollbar darker

--- packages/rocketchat-theme/server/variables.coffee
+++ packages/rocketchat-theme/server/variables.coffee
@@ -11,8 +11,8 @@
 alphaColors=
   'transparent-darker': 'rgba(0,0,0,0.15)'
   'transparent-dark': 'rgba(0,0,0,0.03)'
-  'transparent-light': 'rgba(255,255,255,0.60)'
-  'transparent-lighter': 'rgba(255,255,255,0.25)'
+  'transparent-light': 'rgba(255,255,255,0.25)'
+  'transparent-lighter': 'rgba(255,255,255,0.50)'

 # Major colors form the core of the scheme
 # Names changed to reflect usage, comments show pre-refactor names
@@ -36,7 +36,7 @@
   'tertiary-font-color': '@transparent-light'
   'link-font-color': '@primary-action-color'
   'info-font-color': '@secondary-font-color'
-  'custom-scrollbar-color': '@transparent-dark'
+  'custom-scrollbar-color': '@transparent-darker'
   'status-online': '@success-color'
   'status-away': '@pending-color'
   'status-busy': '@error-color'


Highlight strings with spaces

Highlight strings with spaces

--- src/default.coffee
+++ src/default.coffee
@@ -126,7 +126,7 @@
   # @return [String] highlighted string.
   highlighter: (li, query) ->
     return li if not query
-    regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+","\\+") + ")(\\w*)\\s*<", 'ig')
+    regexp = new RegExp(">\\s*([^\<]*?)(" + query.replace("+","\\+") + ")([^\<]*)\\s*<", 'ig')
     li.replace regexp, (str, $1, $2, $3) -> '> '+$1+'<strong>' + $2 + '</strong>'+$3+' <'

   # What to do before inserting item's value into inputor.


Pad gap timeframes.

Pad gap timeframes.

--- routes/room.coffee
+++ routes/room.coffee
@@ -38,6 +38,9 @@
   }

   gapClassForTimeframe = (startTime, endTime) ->
+    # Pad timestamps, if necessary, for consistency w/ API data
+    startTime[0] = if startTime[0] <= 9 then "0#{startTime[0]}" else startTime[0]
+    endTime[0] = if endTime[0] <= 9 then "0#{endTime[0]}" else endTime[0]
     startTime[1] = if startTime[1] < 9 then "#{startTime[1]}0" else startTime[1]
     endTime[1] = if endTime[1] < 9 then "#{endTime[1]}0" else endTime[1]


No crash if there's no target for Kicker personality

--- src/definitions/personalities.coffee
+++ src/definitions/personalities.coffee
@@ -132,11 +132,12 @@
 		target = creature.findNearest 30,
 			(e) -> e.type is 'creature'

-		if Math.abs(creature.x - target.x) <= 1 and
-				Math.abs(creature.y - target.y) <= 1
+		if target?
+			if Math.abs(creature.x - target.x) <= 1 and
+					Math.abs(creature.y - target.y) <= 1

-			creature.kick creature.directionTo target
+				creature.kick creature.directionTo target

-		else creature.moveTo target
+			else creature.moveTo target

 		12


Updates welcome image credit

Updates welcome image credit

--- src/desktop/apps/home/welcome.coffee
+++ src/desktop/apps/home/welcome.coffee
@@ -5,3 +5,4 @@
   href: '/sign_up?redirect-to=/personalize'
   link_text: 'Sign Up'
   background_image_url: '/images/welcome-hero.jpg'
+  credit_line: '© 2020 Calder Foundation, New York / Artists Rights Society (ARS), New York.'


uploadTo is not a function?

uploadTo is not a function?

--- src/grafana.coffee
+++ src/grafana.coffee
@@ -289,22 +289,7 @@
   uploadPath = () ->
     prefix = s3_prefix || 'grafana'
     "#{prefix}/#{crypto.randomBytes(20).toString('hex')}.png"
-
-  # Fetch an image from provided URL, upload it to S3, returning the resulting URL
-  uploadChart = (msg, title, url, link, site) ->
-    if grafana_api_key
-        requestHeaders =
-          encoding: null,
-          auth:
-            bearer: grafana_api_key
-      else
-        requestHeaders =
-          encoding: null
-
-    request url, requestHeaders, (err, res, body) ->
-      robot.logger.debug "Uploading file: #{body.length} bytes, content-type[#{res.headers['content-type']}]"
-      uploadTo[site()](msg, title, link, body, res)
-
+
   uploadTo =
     's3': (msg, title, link, body, res) ->
       client = knox.createClient {
@@ -360,3 +345,18 @@
               robot.logger.error err
               msg.send "#{title} - [Upload Error] - #{link}"
           )
+
+  # Fetch an image from provided URL, upload it to S3, returning the resulting URL
+  uploadChart = (msg, title, url, link, site) ->
+    if grafana_api_key
+        requestHeaders =
+          encoding: null,
+          auth:
+            bearer: grafana_api_key
+      else
+        requestHeaders =
+          encoding: null
+
+    request url, requestHeaders, (err, res, body) ->
+      robot.logger.debug "Uploading file: #{body.length} bytes, content-type[#{res.headers['content-type']}]"
+      uploadTo[site()](msg, title, link, body, res)


Slightly refactor helpers.

Slightly refactor helpers.

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -244,18 +244,17 @@
       error = err
     callback error, plugins

+getTestFiles = (config) ->
+  isTestFile = (generatedFile) ->
+    exports.startsWith generatedFile, sysPath.normalize('test/')
+  joinPublic = (generatedFile) ->
+    sysPath.join(config.paths.public, generatedFile)
+
+  joinTo = config.files.javascripts.joinTo
+  files = if typeof joinTo is 'string' then [joinTo] else Object.keys(joinTo)
+  files.filter(isTestFile).map(joinPublic)
+
 cachedTestFiles = null
+
 exports.findTestFiles = (config) ->
-  return cachedTestFiles if cachedTestFiles?
-
-  files = []
-  checkJoinToConfig = (generatedFile) ->
-    if exports.startsWith generatedFile, sysPath.normalize('test/')
-      files.push sysPath.join(config.paths.public, generatedFile)
-
-  if typeof config.files.javascripts.joinTo is 'string'
-    checkJoinToConfig config.files.javascripts.joinTo
-  else
-    Object.keys(config.files.javascripts.joinTo).forEach checkJoinToConfig
-
-  cachedTestFiles = files
+  cachedTestFiles ?= getTestFiles config


Removed redundant code

Removed redundant code

A line of code used for debugging was left in which returned a null
messaged at times

--- scripts/jira-issues.coffee
+++ scripts/jira-issues.coffee
@@ -62,11 +62,8 @@
               try
                 json = JSON.parse(body)
                 key = json.key
-
                 fields = []

-                msg.send JSON.stringify json.fields.assignee
-
                 # Ticket assignee
                 assignee = "unassigned"
                 if json.fields.assignee


Update modeline patterns to use github/linguist#5271


Do not reset tab index on modifiers.

Do not reset tab index on modifiers.

The prevents the active element from being reset when we rotate hint
markers.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -288,6 +288,7 @@
       HintCoordinator.sendMessage "rotateHints"

     else
+      @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey
       return

     # We've handled the event, so suppress it and update the mode indicator.


Active hint marker may be in another frame.

Do not set the style on the active hint marker if it's in another frame.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -527,7 +527,7 @@
     @activeHintMarker?.classList?.remove "vimiumActiveHintMarker"
     @activeHintMarker = linksMatched[tabCount]
     @activeHintMarker?.classList?.add "vimiumActiveHintMarker"
-    @activeHintMarker.style.zIndex = getNextZIndex()
+    @activeHintMarker?.style?.zIndex = getNextZIndex()

     linksMatched: linksMatched
     userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length


update comment

update comment

--- src/install.coffee
+++ src/install.coffee
@@ -356,7 +356,7 @@
         commands.push (packagePath, next) =>
           @installModule(options, pack, packagePath, next)
         if installGlobally and packageName isnt pack.name
-          commands.push (newPack, next) => # remove package with old name
+          commands.push (newPack, next) => # package was renamed; delete old package folder
             fs.removeSync(path.join(@atomPackagesDirectory, packageName))
             next(null, newPack)
         commands.push ({installPath}, next) ->


Use the env for the room if available

Use the env for the room if available

--- scripts/poo-tracker.coffee
+++ scripts/poo-tracker.coffee
@@ -14,7 +14,8 @@
       return console.error("Failed lindex with key '#{POO_TRACKER_KEY}' and index -1: #{err}") if err
       return if reply == null

-      robot.messageRoom "#arrakis", "#{reply}"
+      room = process.env.HUBOT_IRC_ROOMS or "#arrakis"
+      robot.messageRoom room, "#{reply}"

       # clean up
       redis_clent.lrem POO_TRACKER_KEY, 0


Until david gets more popular this graph is more interesting showing all the things

--- src/js/main.coffee
+++ src/js/main.coffee
@@ -56,7 +56,7 @@
 			.data(
 				bubble.nodes(transformData(data))
 					.filter((d) -> !d.children)
-					.filter((d) -> d.value > 1)
+					#.filter((d) -> d.value > 1)
 			)
 			.enter().append('g')
 			.attr('class', 'node')


Rename database table and permissions in quotes handler to reflect its name

--- handlers/quotes.coffee
+++ handlers/quotes.coffee
@@ -26,7 +26,7 @@
 api = require '../api'
 async = require 'async'
 winston = require 'winston'
-debug = (require 'debug')('Chatbot:handlers:joinMessages')
+debug = (require 'debug')('Chatbot:handlers:quotes')
 db = require '../db'
 { __, __n } = require '../i18n'

@@ -34,15 +34,15 @@

 onLoad = (callback) ->
 	db.serialize ->
-		db.run "CREATE TABLE IF NOT EXISTS joinMessages (
+		db.run "CREATE TABLE IF NOT EXISTS quotes (
 			userID INT(10),
 			value MEDIUMTEXT,
 			PRIMARY KEY(userID)
 		);"

-		getQuery = db.prepare "SELECT value FROM joinMessages WHERE userID = ?"
-		setQuery = db.prepare "INSERT OR REPLACE INTO joinMessages (userID, value) VALUES (?, ?)"
-		delQuery = db.prepare "DELETE FROM joinMessages WHERE userID = ?"
+		getQuery = db.prepare "SELECT value FROM quotes WHERE userID = ?"
+		setQuery = db.prepare "INSERT OR REPLACE INTO quotes (userID, value) VALUES (?, ?)"
+		delQuery = db.prepare "DELETE FROM quotes WHERE userID = ?"

 handleMessage = (message, callback) ->
 	# Don't match our own messages
@@ -97,7 +97,7 @@
 					else
 						api.replyTo message, __("Your quote has been deleted."), yes, callback
 			when 'forcequote'
-				db.checkPermissionByMessage message, 'joinMessages.forcequote', (hasPermission) ->
+				db.checkPermissionByMessage message, 'quotes.forcequote', (hasPermission) ->
 					if hasPermission
 						[ username, quote... ] = parameters.split /,/
 						username = username.trim()
@@ -116,7 +116,7 @@
 					else
 						callback?()
 			when 'wipequote'
-				db.checkPermissionByMessage message, 'joinMessages.wipequote', (hasPermission) ->
+				db.checkPermissionByMessage message, 'quotes.wipequote', (hasPermission) ->
 					if hasPermission
 						[ username, quote... ] = parameters.split /,/
 						username = username.trim()
@@ -147,7 +147,7 @@
 	], callback

 purge = (callback) ->
-	db.runHuppedQuery "DROP TABLE joinMessages;", (err) ->
+	db.runHuppedQuery "DROP TABLE quotes;", (err) ->
 		if err?
 			debug err
 			callback? err


derp;

derp;

--- scripts/status-cats.coffee
+++ scripts/status-cats.coffee
@@ -5,5 +5,5 @@

 module.exports = (robot) ->
-  robot.reply /status cat (\d+)/i, (msg) ->
+  robot.respond /status cat (\d+)/i, (msg) ->
     msg.send "http://httpcats.herokuapp.com/#{msg.match[1]}.jpg"


Look inside the component definition to compare values

--- src/library.coffee
+++ src/library.coffee
@@ -130,17 +130,18 @@
         names.push name if names.indexOf(name) is -1
         continue
       existing = @getComponent name
-      unless existing
+      unless existing?.definition
         # added
         @components[name] = comp
         names.push name if names.indexOf(name) is -1
         continue
       # update
-      for k, v of comp
-        if existing[k]
+      for k, v of comp.definition
+        if existing.definition[k]
           # Check if value has changed
-          continue if JSON.stringify(existing[k]) is JSON.stringify(v)
-        existing[k] = v
+          continue if JSON.stringify(existing.definition[k]) is JSON.stringify(v)
+        existing.definition[k] = v
+        continue if k in ['extra', 'id']
         names.push name if names.indexOf(name) is -1

     # Skip sending components-changed if nothing changed


加入获取commit的路由

加入获取commit的路由

--- config.coffee
+++ config.coffee
@@ -48,12 +48,24 @@
       method: post: 'uploadFile', delete: false, put: false
     },
     {
+      #获取一个项目top N的commits
+      path: 'project/:project_id(\\d+)/commit'
+      biz: 'commit'
+      method: post: false, delete: false, put: false
+    },
+    {
+      #获取某个issue下的所有commit
+      path: 'issue/:issue_id(\\d+)/commit'
+      biz: 'commit'
+      method: post: false, delete: false, put: false
+    }
+    {
       ##查看素材
       paths:{
         get: '/asset/:project_id(\\d+)/:filename'
       },
       biz: 'asset'
-      method: get: 'readFile', put: false, post: false, delete: false
+      method: get: 'rea,dFile', put: false, post: false, delete: false
     },{
       #issue相关
       path: 'project/:project_id(\\d+)/issue',


better behaving quill styling

better behaving quill styling

--- @client/form.coffee
+++ @client/form.coffee
@@ -354,7 +354,6 @@
 .ql-editor {
   min-height: 120px;
   outline: none;
-  white-space: pre;
 }
 .ql-clipboard {
   display: none;
@@ -365,7 +364,367 @@
   position: absolute;
   color: rgba(0,0,0,.4);
   font-weight: 500;
-
+}
+
+
+
+.ql-container {
+  box-sizing: border-box;
+  height: 100%;
+  margin: 0px;
+  position: relative;
+}
+.ql-container.ql-disabled .ql-tooltip {
+  visibility: hidden;
+}
+.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
+  pointer-events: none;
+}
+
+.ql-editor {
+  box-sizing: border-box;
+  cursor: text;
+  line-height: 1.42;
+  height: 100%;
+  outline: none;
+  overflow-y: auto;
+  padding: 12px 15px;
+  tab-size: 4;
+  -moz-tab-size: 4;
+  text-align: left;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+.ql-editor p,
+.ql-editor ol,
+.ql-editor ul,
+.ql-editor pre,
+.ql-editor blockquote,
+.ql-editor h1,
+.ql-editor h2,
+.ql-editor h3,
+.ql-editor h4,
+.ql-editor h5,
+.ql-editor h6 {
+  margin: 0;
+  padding: 0;
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol,
+.ql-editor ul {
+  padding-left: 1.5em;
+}
+.ql-editor ol > li,
+.ql-editor ul > li {
+  list-style-type: none;
+}
+.ql-editor ul[data-checked=true],
+.ql-editor ul[data-checked=false] {
+  pointer-events: none;
+}
+.ql-editor ul[data-checked=true] > li *,
+.ql-editor ul[data-checked=false] > li * {
+  pointer-events: all;
+}
+.ql-editor ul[data-checked=true] > li::before,
+.ql-editor ul[data-checked=false] > li::before {
+  color: #777;
+  cursor: pointer;
+  pointer-events: all;
+}
+.ql-editor li::before {
+  display: inline-block;
+  margin-right: 0.3em;
+  text-align: right;
+  white-space: nowrap;
+  width: 1.2em;
+}
+.ql-editor li:not(.ql-direction-rtl)::before {
+  margin-left: -1.5em;
+}
+.ql-editor ol li,
+.ql-editor ul li {
+  padding-left: 1.5em;
+}
+.ql-editor ol li {
+  counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+  counter-increment: list-num;
+}
+.ql-editor ol li:before {
+  content: counter(list-num, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-increment: list-1;
+}
+.ql-editor ol li.ql-indent-1:before {
+  content: counter(list-1, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-1 {
+  counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-increment: list-2;
+}
+.ql-editor ol li.ql-indent-2:before {
+  content: counter(list-2, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-2 {
+  counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-increment: list-3;
+}
+.ql-editor ol li.ql-indent-3:before {
+  content: counter(list-3, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-3 {
+  counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-increment: list-4;
+}
+.ql-editor ol li.ql-indent-4:before {
+  content: counter(list-4, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-4 {
+  counter-reset: list-5 list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-increment: list-5;
+}
+.ql-editor ol li.ql-indent-5:before {
+  content: counter(list-5, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-5 {
+  counter-reset: list-6 list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-increment: list-6;
+}
+.ql-editor ol li.ql-indent-6:before {
+  content: counter(list-6, decimal) '. ';
+}
+.ql-editor ol li.ql-indent-6 {
+  counter-reset: list-7 list-8 list-9;
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-increment: list-7;
+}
+.ql-editor ol li.ql-indent-7:before {
+  content: counter(list-7, lower-alpha) '. ';
+}
+.ql-editor ol li.ql-indent-7 {
+  counter-reset: list-8 list-9;
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-increment: list-8;
+}
+.ql-editor ol li.ql-indent-8:before {
+  content: counter(list-8, lower-roman) '. ';
+}
+.ql-editor ol li.ql-indent-8 {
+  counter-reset: list-9;
+}
+.ql-editor ol li.ql-indent-9 {
+  counter-increment: list-9;
+}
+.ql-editor ol li.ql-indent-9:before {
+  content: counter(list-9, decimal) '. ';
+}
+.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 3em;
+}
+.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
+  padding-left: 4.5em;
+}
+.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 3em;
+}
+.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
+  padding-right: 4.5em;
+}
+.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 6em;
+}
+.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
+  padding-left: 7.5em;
+}
+.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 6em;
+}
+.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
+  padding-right: 7.5em;
+}
+.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 9em;
+}
+.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
+  padding-left: 10.5em;
+}
+.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 9em;
+}
+.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
+  padding-right: 10.5em;
+}
+.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 12em;
+}
+.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
+  padding-left: 13.5em;
+}
+.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 12em;
+}
+.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
+  padding-right: 13.5em;
+}
+.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 15em;
+}
+.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
+  padding-left: 16.5em;
+}
+.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 15em;
+}
+.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
+  padding-right: 16.5em;
+}
+.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 18em;
+}
+.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
+  padding-left: 19.5em;
+}
+.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 18em;
+}
+.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
+  padding-right: 19.5em;
+}
+.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 21em;
+}
+.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
+  padding-left: 22.5em;
+}
+.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 21em;
+}
+.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
+  padding-right: 22.5em;
+}
+.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 24em;
+}
+.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
+  padding-left: 25.5em;
+}
+.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 24em;
+}
+.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
+  padding-right: 25.5em;
+}
+.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 27em;
+}
+.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
+  padding-left: 28.5em;
+}
+.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 27em;
+}
+.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
+  padding-right: 28.5em;
+}
+.ql-editor .ql-video {
+  display: block;
+  max-width: 100%;
+}
+.ql-editor .ql-video.ql-align-center {
+  margin: 0 auto;
+}
+.ql-editor .ql-video.ql-align-right {
+  margin: 0 0 0 auto;
+}
+.ql-editor .ql-bg-black {
+  background-color: #000;
+}
+.ql-editor .ql-bg-red {
+  background-color: #e60000;
+}
+.ql-editor .ql-bg-orange {
+  background-color: #f90;
+}
+.ql-editor .ql-bg-yellow {
+  background-color: #ff0;
+}
+.ql-editor .ql-bg-green {
+  background-color: #008a00;
+}
+.ql-editor .ql-bg-blue {
+  background-color: #06c;
+}
+.ql-editor .ql-bg-purple {
+  background-color: #93f;
+}
+.ql-editor .ql-color-white {
+  color: #fff;
+}
+.ql-editor .ql-color-red {
+  color: #e60000;
+}
+.ql-editor .ql-color-orange {
+  color: #f90;
+}
+.ql-editor .ql-color-yellow {
+  color: #ff0;
+}
+.ql-editor .ql-color-green {
+  color: #008a00;
+}
+.ql-editor .ql-color-blue {
+  color: #06c;
+}
+.ql-editor .ql-color-purple {
+  color: #93f;
+}
+.ql-editor .ql-font-serif {
+  font-family: Georgia, Times New Roman, serif;
+}
+.ql-editor .ql-font-monospace {
+  font-family: Monaco, Courier New, monospace;
+}
+.ql-editor .ql-size-small {
+  font-size: 0.75em;
+}
+.ql-editor .ql-size-large {
+  font-size: 1.5em;
+}
+.ql-editor .ql-size-huge {
+  font-size: 2.5em;
+}
+.ql-editor .ql-direction-rtl {
+  direction: rtl;
+  text-align: inherit;
+}
+.ql-editor .ql-align-center {
+  text-align: center;
+}
+.ql-editor .ql-align-justify {
+  text-align: justify;
+}
+.ql-editor .ql-align-right {
+  text-align: right;
+}
+.ql-editor.ql-blank::before {
+  color: rgba(0,0,0,0.6);
+  content: attr(data-placeholder);
+  font-style: italic;
+  pointer-events: none;
+  position: absolute;
 }
 """


show proposal sort when there are at least two proposals in a list

--- @client/list.coffee
+++ @client/list.coffee
@@ -734,7 +734,7 @@
         list_actions
           list: @props.list
           add_new: !@props.combines_these_lists && customization('list_permit_new_items', list_key) && !is_collapsed && @props.proposals_count > 4
-          can_sort: customization('homepage_show_search_and_sort') && @props.proposals_count > 8
+          can_sort: customization('homepage_show_search_and_sort') && @props.proposals_count > 1


Update Atom config

Update Atom config

--- home/.atom/config.cson
+++ home/.atom/config.cson
@@ -24,6 +24,7 @@
       "atomatigit"
       "auto-update-packages"
       "deprecation-cop"
+      "vim-mode"
     ]
     themes: [
       "atom-dark-ui"


add the migrate task into grunt

add the migrate task into grunt

--- server-ce/Gruntfile.coffee
+++ server-ce/Gruntfile.coffee
@@ -165,7 +165,7 @@
 		Helpers.buildUpstartScripts()

-	#grunt.registerTask 'migrate', "compile migrations and run them", ['coffee:migrate', 'shell:migrate']
+	grunt.registerTask 'migrate', "compile migrations and run them", ['coffee:migrate', 'shell:migrate']

 	Helpers =


fieldsCheckbox should be unchecked when length is 0

--- app/assets/javascripts/incident_reports/edit.coffee
+++ app/assets/javascripts/incident_reports/edit.coffee
@@ -37,11 +37,12 @@
   event.preventDefault()
   addFields '.pax-fields'

-deleteFields = (fieldsSelector) ->
+deleteFields = (fieldsSelector, fieldsCheckbox) ->
   fields = $(fieldsSelector).last()
   fields.find('input').val('').prop 'checked', false
   if $(fieldsSelector).length > 1
     fields.remove()
+    $(fieldsCheckbox).prop 'checked', false

 deleteWitnessFields = (event) ->
   event.preventDefault()


Bug fix in NestedObjModel

Bug fix in NestedObjModel

--- app/assets/javascripts/libs/nested_obj_model.coffee
+++ app/assets/javascripts/libs/nested_obj_model.coffee
@@ -52,7 +52,7 @@
     if _.isPlainObject(newObj)
       # Recursively call triggerDeepChange for each key
       _.forOwn(newObj, (value, key) =>
-        @triggerDeepChange(oldObj[key], newObj[key], "#{deepKey}.#{key}")
+        @triggerDeepChange((if oldObj? then oldObj[key] else oldObj), newObj[key], "#{deepKey}.#{key}")
       )
     else if oldObj != newObj
       # Add the change to the changed object


Fix bug with missing locale for Rails I18n-js

--- app/assets/javascripts/plugins/vex_config.js.coffee
+++ app/assets/javascripts/plugins/vex_config.js.coffee
@@ -1,4 +1,4 @@
 $(document).on 'ready page:load page:restore', ->
   vex.defaultOptions.className = 'vex-theme-os'
-  vex.dialog.buttons.YES.text  = I18n.t('delete.yes')
-  vex.dialog.buttons.NO.text   = I18n.t('delete.no')+  vex.dialog.buttons.YES.text  = I18n.t('delete.yes', locale: gon.language)
+  vex.dialog.buttons.NO.text   = I18n.t('delete.no', locale: gon.language)


Show only accepted sessions on /proposals

--- server/publications.coffee
+++ server/publications.coffee
@@ -76,8 +76,8 @@
   options = {} if not options
   query = {} if not query
   query = _.extend(query, notDeletedPred)
-  # if not query.id
-  #   query = _.extend(query, {status: 'accepted'})
+  if not query.id
+    query = _.extend(query, {status: 'accepted'})
   proposals = Proposal.find(query, _.extend(options, {fields: proposalFields(@userId)}))
   userIds = _.flatten(proposals.map((p) -> p.speaker_ids))
   users = User.find({_id: $in: userIds}, {fields: userFields('profile.bio': 1)})


Enable bootstrap tooltips on menu items.

--- scripts/views/workspace/sidebar/toc-branch.coffee
+++ scripts/views/workspace/sidebar/toc-branch.coffee
@@ -136,6 +136,9 @@
       if @model.getParent?()
         EnableDnD.enableDropAfter(@model, @model.getParent(), @$el.find('> .editor-drop-zone-after'))

+      # Enable tooltips
+      @$el.find('button[title]').tooltip {container: '#workspace-container'}
+

     prettyName: () ->
       # Translate the mediaType attribute to something nice we can display.


Added a basic game loop and some even handling code

--- public/main.coffee
+++ public/main.coffee
@@ -0,0 +1,70 @@
+canvas = document.getElementById 'canvas'
+context = canvas.getContext '2d'
+
+blockStates = [ true, true, true, true, true, true, true, true ]
+blockColors = [ '#f00', '#0f0', '#00f', '#ccc', '#fcc', '#cfc', '#ccf', '#000' ]
+
+Game =
+  canvas:  document.getElementById 'canvas'
+  context: @canvas.getContext '2d'
+  fps: 60
+  stopped: false
+
+  #game loop stuff
+  loops: 0
+  skipTicks: 1000 / @fps
+  maxFrameSkip: 10
+  nextGameTick: (new Date).getTime()
+
+  paddleX: (640-80)/2
+
+  getCursorPosition: (e) =>
+    if e.pageX != undefined && e.pageY != undefined
+    	x = e.pageX
+    	y = e.pageY
+    else
+    	x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
+    	y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop
+
+    x -= Game.canvas.offsetLeft;
+    y -= Game.canvas.offsetTop;
+
+    point =
+      x: x
+      y: y
+
+    return point
+
+  onClick: (e) =>
+
+  onKeyDown: (e) =>
+    if(e.keyCode == 13)
+      Game.context.fillRect 0, 0, 640, 480
+
+  onKeyUp: (e) =>
+
+
+  update: () =>
+
+
+  draw: () =>
+    i = 0
+    #Game.canvas.width = Game.canvas.width
+
+    while i < 8
+      if blockStates[i]
+        Game.context.fillStyle = blockColors[i]
+        Game.context.fillRect i*80, 0, 80, 80
+
+      i++
+
+  run: () =>
+    @loops = 0
+
+    Game.update()
+    Game.draw()
+
+Game.canvas.addEventListener "click", Game.onClick, false
+document.addEventListener "keydown", Game.onKeyDown, false
+document.addEventListener "keyup", Game.onKeyUp, false
+Game._intervalId = setInterval Game.run, 0


Activity: Live status updates are added

Activity: Live status updates are added

--- client/Social/Activity/activitylistcontroller.coffee
+++ client/Social/Activity/activitylistcontroller.coffee
@@ -26,12 +26,19 @@
     @hiddenItems = []
     @_state      = 'public'

-    KD.getSingleton("groupsController").on "MemberJoinedGroup", (member) =>
+    groupController = KD.getSingleton("groupsController")
+    groupController.on "MemberJoinedGroup", (member) =>
       @updateNewMemberBucket member.member

-    KD.getSingleton("groupsController").on "FollowHappened", (info) =>
+    groupController.on "FollowHappened", (info) =>
       {follower, origin} = info
       @updateFollowerBucket follower, origin
+
+    groupController.on "PostIsCreated", (post) =>
+      {subject} = post
+      constructor = subject.bongo_.constructorName
+      subject = new KD.remote.api[constructor] subject
+      @addItem subject, 0

   resetList:->
     @newActivityArrivedList = {}


MemberItemView: Implemented role change actions.

--- client/admin/lib/views/members/memberitemview.coffee
+++ client/admin/lib/views/members/memberitemview.coffee
@@ -44,6 +44,8 @@

     super options, data

+    @actionButtons    = {}
+    @loggedInUserRole = @getRole data.loggedInUserRoles
     @memberRole       = @getRole data.roles

     @avatar = new AvatarView
@@ -57,6 +59,7 @@

     @createSettingsView()

+    @on 'UserRoleChangeRequested', @bound 'handleRoleChange'

   getRole: (userRoles) ->
@@ -89,6 +92,45 @@
       @settings.addSubView buttonView

+  handleRoleChange: (newRole) ->
+
+    { groupsController } = kd.singletons
+    button   = @actionButtons[newRole]
+    jAccount = @getData()
+
+    return button.hideLoader()  if @isInProgress
+
+    @isInProgress = yes
+
+    groupsController.ready =>
+      group    = groupsController.getCurrentGroup()
+      newRoles = [ newRole ]
+
+      newRoles.push 'admin'  if newRole is 'owner'
+
+      group.changeMemberRoles jAccount.getId(), newRoles, (err, response) =>
+        return @handleError button  if err
+
+        group.fetchUserRoles [ jAccount.getId() ], (err, roles) =>
+
+          return @handleError button  if err
+
+          data        = @getData()
+          data.roles  = (role.as for role in roles)
+          @memberRole = @getRole data.roles
+
+          @roleLabel.updatePartial "#{@memberRole.label} "
+          @settings.destroySubViews()
+          @createSettingsView()
+
+          @isInProgress = no
+
+
+  handleError: (button, message) ->
+
+    button.hideLoader()
+    message or= 'Failed to change user role. Please try again.'
+    return new KDNotificationView title: message, duration: 5000


MemberItemView: Some styling fixes.

MemberItemView: Some styling fixes.

--- client/admin/lib/views/members/memberitemview.coffee
+++ client/admin/lib/views/members/memberitemview.coffee
@@ -18,14 +18,14 @@
     super options, data

     @avatar = new AvatarView
-      size     : width: 40, height : 40
+      size  : width: 40, height : 40
     , @getData()

     @roleLabel = new KDCustomHTMLView
       cssClass : 'role'
       partial  : "Member "
       click    : =>
-        @settings.toggleClass 'hidden'
+        @settings.toggleClass  'hidden'
         @roleLabel.toggleClass 'active'

     @createSettingsView()


Account Settings: Credentials: Fill advanced inputs / fields with default values

--- client/app/lib/providers/computecontroller.ui.coffee
+++ client/app/lib/providers/computecontroller.ui.coffee
@@ -32,6 +32,8 @@
   @generateAddCredentialFormFor = (options) ->

     { provider, requiredFields, defaultTitle, defaultValues, callback } = options
+
+    defaultValues = []  unless defaultValues

     fields           =
       title          :
@@ -63,9 +65,7 @@
       _field = fields[field] = _.clone currentProvider.credentialFields[field]

       _field.required     = yes
-      _field.defaultValue = ''
-
-      _field.defaultValue = defaultValues[field]  if defaultValues and defaultValues[field]
+      _field.defaultValue = defaultValues[field] or ''

       if _field.type is 'selection'
@@ -93,9 +93,10 @@
     if advancedFields = currentProvider.advancedFields
       advancedFields.forEach (field) ->
         fields[field] =
-          label       : field.capitalize()
-          placeholder : field
-          cssClass    : 'advanced-field'
+          label         : field.capitalize()
+          placeholder   : field
+          cssClass      : 'advanced-field'
+          defaultValue  : defaultValues[field]

       buttons['Advanced Mode'] =
         style    : "solid medium"


ComputeController.UI: minor code improvement

--- client/app/lib/providers/computecontroller.ui.coffee
+++ client/app/lib/providers/computecontroller.ui.coffee
@@ -61,7 +61,7 @@
   hasStackRequiredField = (requiredFields, field) ->

     return yes  if field in requiredFields
-    return requiredFields.filter((item) -> item.name is field).length > 0
+    return (item for item in requiredFields when item.name is field).length > 0

   injectCustomActions = (requiredFields, buttons, callback) ->


Teams: Disable snapshots.

Teams: Disable snapshots.

--- client/app/lib/providers/machinesettingsmodal.coffee
+++ client/app/lib/providers/machinesettingsmodal.coffee
@@ -75,7 +75,7 @@
     isMachineRunning            = machine.status.state is Machine.State.Running
     disabledTabsForNotRunningVM = [ 'Disk Usage', 'Domains', 'VM Sharing', 'Snapshots' ]
     disabledTabsForManagedVM    = [ 'Specs', 'Domains', 'Snapshots' ]
-    disabledTabsForTeams        = [ 'Domains', 'Advanced' ]
+    disabledTabsForTeams        = [ 'Domains', 'Advanced', 'Snapshots' ]

     for item in PANE_CONFIG when item.title and item.viewClass


Read only guide link

Read only guide link

--- client/ide/lib/views/chat/idechatsettingspane.coffee
+++ client/ide/lib/views/chat/idechatsettingspane.coffee
@@ -154,6 +154,8 @@
     kd.singletons.appManager.tell 'IDE', 'setInitialSessionSetting', 'unwatch', state

+  READ_ONLY_GUIDE_LINK = 'http://learn.koding.com/guides/collaboration/#read_only'
+
   createReadOnlySettingElements: ->

     @readOnlyWrapper = new KDCustomHTMLView cssClass: 'wrapper read-only'
@@ -166,6 +168,13 @@
     @readOnlyWrapper.addSubView new KDLabelView
       title     : 'Read-only session'
       mousedown : toggle.bound 'mouseDown'
+
+    @readOnlyWrapper.addSubView new KDCustomHTMLView
+      tagName    : 'a'
+      attributes :
+        href     : READ_ONLY_GUIDE_LINK
+        target   : '_blank'
+      partial  : '(?)'

     @settings.addSubView @readOnlyWrapper


globalNotification: remove flash when the countdown is reset

--- client/landing/app/MainApp/globalnotification.coffee
+++ client/landing/app/MainApp/globalnotification.coffee
@@ -125,9 +125,7 @@

      {{> @timer}}

-
      {{> @content}}
-

         {{> @current}}
@@ -183,6 +181,7 @@
       if (@getCurrentTimePercentage() < @getOptions().flashThresholdPercentage) \
       or (@getCurrentTimeRemaining()/1000 < @getOptions().flashThresholdSeconds)
         @current.setClass 'flash'
+      else @current.unsetClass 'flash'
       currentTime = parseInt(@endTime - new Date(Date.now()),10)
       @timer.updatePartial timeText currentTime


fix(threadlist): can click already-selected thread to re-open

--- src/components/list-tabular.cjsx
+++ src/components/list-tabular.cjsx
@@ -35,8 +35,7 @@
       </div>

   _onClick: ->
-    if not @props.selected
-      @props.onSelect?(@props.item)
+    @props.onSelect?(@props.item)

     @props.onClick?(@props.item)
     if @_lastClickTime? and Date.now() - @_lastClickTime < 350


removed some repeated functions

removed some repeated functions

--- src/components/scores/index.cjsx
+++ src/components/scores/index.cjsx
@@ -1,20 +1,6 @@
 React = require 'react'
 BS = require 'react-bootstrap'
 _ = require 'underscore'
-
-Time   = require '../time'
-ReadingCell  = require './reading-cell'
-HomeworkCell = require './homework-cell'
-NameCell     = require './name-cell'
-AbsentCell   = require './absent-cell'
-ExternalCell = require './external-cell'
-SortingHeader = require './sorting-header'
-FixedDataTable = require 'fixed-data-table'
-ConceptCoachCell = require './concept-coach-cell'
-
-Table = FixedDataTable.Table
-Column = FixedDataTable.Column
-ColumnGroup = FixedDataTable.ColumnGroup

 HSTable = require './table-hs'
 CCTable = require './table-cc'
@@ -26,7 +12,6 @@
 {ScoresExportStore, ScoresExportActions} = require '../../flux/scores-export'
 LoadableItem = require '../loadable-item'
 ScoresExport = require './export'
-{QuickStatsShell} = require './quick-external-stats'
 {CoursePeriodsNavShell} = require '../course-periods-nav'
 ResizeListenerMixin = require '../resize-listener-mixin'

@@ -86,138 +71,6 @@
     Math.max(500, windowEl.height - table.offsetTop - bottomMargin)

-
-
-
-
-
-
-  renderHeadingCell: (heading, i) ->
-    i += FIRST_DATA_COLUMN # for the first/last name colums
-    if heading.type is 'external'
-      summary = <QuickStatsShell
-        className='summary'
-        id={"#{heading.plan_id}"}
-        periodId={@state.period_id}/>
-
-    else if heading.plan_id?
-      linkParams =
-        id: heading.plan_id
-        periodIndex: @state.periodIndex
-        courseId: @props.courseId
-
-      review =
-        <Router.Link
-          to='reviewTaskPeriod'
-          params={linkParams}
-          className='review-plan btn btn-default'>
-          Review
-        </Router.Link>
-
-    if heading.average
-      summary =
-        {(heading.average * 100).toFixed(0)}% avg
-
-
-    sortingHeader = <SortingHeader type={heading.type} sortKey={i}
-      sortState={@state.sort} onSort={@changeSortingOrder} isConceptCoach={@props.isConceptCoach}
-    >{heading.title}</SortingHeader>
-
-    dueDates =
<Time date={heading.due_at} format='shortest'/>

-    customHeader =

-      {dueDates unless @props.isConceptCoach}
-

-        {summary}
-        {review}
-

-

-
-    <ColumnGroup key={i} groupHeaderRenderer={-> sortingHeader} >
-
-      <Column
-        label={heading.title}
-        headerRenderer={-> customHeader}
-        cellRenderer={-> @cellData}
-        width={@state.colSetWidth}
-        flexGrow={1}
-        allowCellsRecycling={true}
-        isResizable=false
-        dataKey={i} />
-
-    </ColumnGroup>
-
-
-
-
-
-
-
-  renderAverageCell: (heading) ->
-    if heading.class_average
-      classAverage = Math.round(heading.class_average)
-    classAverage
-
-
-
-
-
-
-
-
-  renderStudentRow: (student_data) ->
-    props = {student:student_data, courseId: @props.courseId, roleId: student_data.role}
-    columns = [
-      <NameCell isConceptCoach={@props.isConceptCoach} key='name' {...props} />
-    ]
-
-    for task in student_data.data
-      props.task = task
-      columns.push switch task?.type or 'null'
-        when 'null'     then <AbsentCell   key='absent'   {...props} />
-        when 'reading'  then <ReadingCell  key='reading'  {...props} />
-        when 'homework' then <HomeworkCell key='homework' {...props} />
-        when 'external' then <ExternalCell key='extern'   {...props} />
-        when 'concept_coach' then <ConceptCoachCell  key='cc'  {...props} />
-    columns
-
-
-
-
-
-
-  renderNameHeader: ->
-    emptyCell =

-    studentIdHeader = Student ID
-    header =
-      <SortingHeader sortKey='name' sortState={@state.sort} onSort={@changeSortingOrder}>
-        Student Name{studentIdHeader if @props.isConceptCoach}
-      </SortingHeader>
-    dueDateHeading =
Due Date

-    customHeader =

-      {dueDateHeading unless @props.isConceptCoach}
-      {header}
-

-    # student name column width
-    if @props.isConceptCoach then nameColumns = 2 else nameColumns = 1
-    <ColumnGroup fixed={true} groupHeaderRenderer={-> emptyCell}>
-      <Column
-        width={@state.colSetWidth * nameColumns}
-        flexGrow={0}
-        allowCellsRecycling={true}
-        isResizable=false
-        dataKey='0'
-        fixed={true}
-        cellRenderer={-> @cellData}
-        headerRenderer={-> customHeader} />
-    </ColumnGroup>
-
-
-
-
-
-
   changeSortingOrder: (key) ->
     asc = if @state.sort.key is key then not @state.sort.asc else false
     @setState(sort: { key, asc})
@@ -232,12 +85,6 @@

   setPeriodIndex: (key) ->
     @setState({periodIndex: key + 1})
-
-
-
-
-
-

   getStudentRowData: ->
@@ -263,7 +110,6 @@
     { headings: scores.data_headings, rows: if sort.asc then sortData else sortData.reverse() }

-
   reviewInfoText: ->
     return null unless @props.isConceptCoach

@@ -272,16 +118,11 @@

-
-
-
   render: ->
     {courseId, isConceptCoach} = @props
     {period_id, tableWidth, tableHeight} = @state

     data = @getStudentRowData()
-
-

     periodNav =
       <CoursePeriodsNavShell
@@ -293,8 +134,6 @@

     scoresExport = <ScoresExport courseId={courseId} className='pull-right'/>

-
-
     if isConceptCoach
       scoresTable =
         <CCTable
@@ -323,11 +162,6 @@
           />

-
-
-
-
-
     noAssignments = No Assignments Yet

     scoresNote =
@@ -346,10 +180,6 @@
           {if students then scoresTable else noAssignments}

-
-
-
-


Cleaner syntax for setting default config paths in server/router.coffee

--- server/router.coffee
+++ server/router.coffee
@@ -14,11 +14,9 @@
 exports.init = (conf, callback) ->
   config = conf
   if (!config.paths || !config.paths.entryPath)
-    return callback("Missing entryPath")
-  if (!config.paths.routes)
-    config.paths.routes = config.paths.entryPath + '/routes'
-  if (!config.paths.controllerDir)
-    config.paths.controllerDir = config.paths.entryPath + '/controllers'
+    return callback(new Error("Missing entryPath"))
+  config.paths.routes ||= config.paths.entryPath + '/routes'
+  config.paths.controllerDir ||= config.paths.entryPath + '/controllers'
   callback()


Tests: Skip roundrobin tests on MQTT\direct

Not supported yet

--- spec/01transport.coffee
+++ spec/01transport.coffee
@@ -99,6 +99,8 @@
 transportTests = (type) ->
   address = transports[type]
   broker = null
+
+  describeIfRoundRobinSupport = if type == 'AMQP' then describe else describe.skip

   beforeEach (done) ->
     broker = transport.getBroker address
@@ -282,7 +284,7 @@
               clients.sender.sendToQueue outQueue2, {data: 'ident'}, (err) ->
                 chai.expect(err).to.not.exist

-  describe 'Roundrobin binding', ->
+  describeIfRoundRobinSupport 'Roundrobin binding', ->
     describe 'sending ACKed message, then NACKed message', ->
       received = null
       beforeEach (done) ->


i fail

i fail

--- server/server.coffee
+++ server/server.coffee
@@ -12,7 +12,7 @@
   return respond404 res unless model
   respondJSON res, 200, _.omit(model.toObject(), '_id')

-respondJSON = (res, code, model) ->
+respondJSON = (res, code, data) ->
   res.contentType 'application/json'
   res.send code, JSON.stringify data

@@ -46,7 +46,7 @@
   path = req.params[0]
   console.log 'album', path
   getAlbum(path).then (album) ->
-    respondJSON res, 200, album.toObject()
+    respondModel res, 200, album.toObject()
   .fail (e) ->
     console?.error e
     respond500 res


simplifying server/server module

simplifying server/server module

--- server/server.coffee
+++ server/server.coffee
@@ -28,14 +28,10 @@
 # options
 # - port
 #
-exports.start = (options, callback) ->
-  options ||= {}
+exports.start = (options = {}) ->
   port = options.port || 3030
-  app.listen port, undefined, (err) ->
-    console.log("server pid #{process.pid} listening on port #{port} in #{app.settings.env} mode")
-    if callback
-      callback(err, {port:port, env:app.settings.env})
-
+  app.listen(port)
+  console.log("server pid #{process.pid} listening on port #{port} in #{app.settings.env} mode")

 #
 # Initialize middleware stack


added subscription handler null check

added subscription handler null check

--- services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee
+++ services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee
@@ -70,7 +70,7 @@
 			return callback(error) if error?
 			User.findById recurlySubscription.account.account_code, (error, user) ->
 				return callback(error) if error?
-				SubscriptionUpdater.syncSubscription recurlySubscription, user._id, callback
+				SubscriptionUpdater.syncSubscription recurlySubscription, user?._id, callback


Cache/restore class tag contents, important for <text>

--- layout.coffee
+++ layout.coffee
@@ -579,12 +579,19 @@

         return if not (viewel = parent.sprite?.el)

+        # cache the old contents
+        oldbody = viewel.innerHTML
+
+        # unpack instance
         viewel.innerHTML = body
         children = (child for child in viewel.childNodes when child.nodeType == 1)
         for child in children
           child.$defer = null
           # console.log 'creating class child in parent', child, parent
           initFromElement(child, parent) unless child.localName in specialtags
+
+        # restore old contents
+        viewel.innerHTML = oldbody if (oldbody)
         return


check that services method exists

check that services method exists

Signed-off-by: Kevin Sawicki <ffb4761cba839470133bee36aeb139f58d7dbaa9@github.com>

--- src/package.coffee
+++ src/package.coffee
@@ -225,12 +225,14 @@
     for name, {versions} of @metadata.providedServices
       servicesByVersion = {}
       for version, methodName of versions
-        servicesByVersion[version] = @mainModule[methodName]()
+        if typeof @mainModule[methodName] is 'function'
+          servicesByVersion[version] = @mainModule[methodName]()
       @activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion)

     for name, {versions} of @metadata.consumedServices
       for version, methodName of versions
-        @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
+        if typeof @mainModule[methodName] is 'function'
+          @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
     return

   loadKeymaps: ->


Move Grim.deprecate call so that it can be correctly attributed

--- src/package.coffee
+++ src/package.coffee
@@ -191,7 +191,20 @@

     for [menuPath, map] in @menus when map['context-menu']?
       try
-        @activationDisposables.add(atom.contextMenu.add(map['context-menu']))
+        itemsBySelector = map['context-menu']
+
+        if includeDeprecatedAPIs
+          # Detect deprecated format for items object
+          for key, value of itemsBySelector
+            unless _.isArray(value)
+              deprecate("""
+                The context menu CSON format has changed. Please see
+                https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format
+                for more info.
+              """, {packageName: @name})
+              itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector)
+
+        @activationDisposables.add(atom.contextMenu.add(itemsBySelector))
       catch error
         if error.code is 'EBADSELECTOR'
           error.message += " in #{menuPath}"


reorder

reorder

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -319,6 +319,43 @@
   bindEnsureWaitOption: (optionsBase) =>
     @bindEnsureOption(optionsBase, true)

+  keystroke: (keys, options={}) =>
+    if options.waitsForFinish
+      finished = false
+      @vimState.onDidFinishOperation -> finished = true
+      delete options.waitsForFinish
+      @keystroke(keys, options)
+      waitsFor -> finished
+      return
+
+    target = @editorElement
+
+    for key in keys.split(/\s+/)
+      # [FIXME] Why can't I let atom.keymaps handle enter/escape by buildEvent and handleKeyboardEvent
+      if @vimState.__searchInput?.hasFocus() # to avoid auto populate
+        target = @vimState.searchInput.editorElement
+        switch key
+          when "enter" then atom.commands.dispatch(target, 'core:confirm')
+          when "escape" then atom.commands.dispatch(target, 'core:cancel')
+          else @vimState.searchInput.editor.insertText(key)
+
+      else if @vimState.inputEditor?
+        target = @vimState.inputEditor.element
+        switch key
+          when "enter" then atom.commands.dispatch(target, 'core:confirm')
+          when "escape" then atom.commands.dispatch(target, 'core:cancel')
+          else @vimState.inputEditor.insertText(key)
+
+      else
+        event = buildKeydownEventFromKeystroke(normalizeKeystrokes(key), target)
+        atom.keymaps.handleKeyboardEvent(event)
+
+    if options.partialMatchTimeout
+      advanceClock(atom.keymaps.getPartialMatchTimeout())
+
+  keystrokeWait: (keys, options={}) =>
+    @keystroke keys, Object.assign(waitsForFinish: true)
+
   # Ensure each options from here
   # -----------------------------
   ensureText: (text) ->
@@ -459,44 +496,4 @@
     for m in shouldNotContainClasses
       expect(@editorElement.classList.contains(m)).toBe(false)

-  # Public
-  # options
-  # - waitsForFinish
-  keystroke: (keys, options={}) =>
-    if options.waitsForFinish
-      finished = false
-      @vimState.onDidFinishOperation -> finished = true
-      delete options.waitsForFinish
-      @keystroke(keys, options)
-      waitsFor -> finished
-      return
-
-    target = @editorElement
-
-    for key in keys.split(/\s+/)
-      # [FIXME] Why can't I let atom.keymaps handle enter/escape by buildEvent and handleKeyboardEvent
-      if @vimState.__searchInput?.hasFocus() # to avoid auto populate
-        target = @vimState.searchInput.editorElement
-        switch key
-          when "enter" then atom.commands.dispatch(target, 'core:confirm')
-          when "escape" then atom.commands.dispatch(target, 'core:cancel')
-          else @vimState.searchInput.editor.insertText(key)
-
-      else if @vimState.inputEditor?
-        target = @vimState.inputEditor.element
-        switch key
-          when "enter" then atom.commands.dispatch(target, 'core:confirm')
-          when "escape" then atom.commands.dispatch(target, 'core:cancel')
-          else @vimState.inputEditor.insertText(key)
-
-      else
-        event = buildKeydownEventFromKeystroke(normalizeKeystrokes(key), target)
-        atom.keymaps.handleKeyboardEvent(event)
-
-    if options.partialMatchTimeout
-      advanceClock(atom.keymaps.getPartialMatchTimeout())
-
-  keystrokeWait: (keys, options={}) =>
-    @keystroke keys, Object.assign(waitsForFinish: true)
-
 module.exports = {getVimState, getView, dispatch, TextData, withMockPlatform}


Add ability to simulate DOM attachment to spec helper

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -131,3 +131,5 @@
     event = jQuery.event.fix(event)
     $(this).trigger(event)

+$.fn.simulateDomAttachment = ->
+  $('<html>').append(this)


Polish and document hash ID derivaton stuff

--- src/picksel.coffee
+++ src/picksel.coffee
@@ -1,6 +1,8 @@
 # Constants.
 USER_PATH = './.pickselacc' # Location of user file.
 PROJECT_PATH = './picksel.json' # Location of project file.
+AMBIG_DIR = './picksel_temp' # Location of ambiguous result resolution folder.
+AMBIG_BASE_PATH = './pickseltemp_base.temp' # Location of base image file.

 # Load dependencies.
 https = require 'https'
@@ -75,7 +77,7 @@
 #
 # @param [String] apiKey the API key to use for the request
 # @param [Number] id the ID of the image to get information for
-# @param [Number] resolution the resolution code of the image to get
+# @param [String] resolution the human-readable resolution required
 #
 buildUrl = (apiKey, id, resolution) ->
   code = humanResolutionToCode resolution
@@ -87,8 +89,8 @@
 # Builds a search URL for downloading image information.
 #
 # @param [String] apiKey the API key to use for the request
-# @param [Number] term the search term to use to query the API
-# @param [Number] resolution the resolution code to perform the search at
+# @param [String] term the search term to use to query the API
+# @param [String] resolution the human-readable resolution required
 #
 buildSearchUrl = (apiKey, term, resolution) ->
   code = humanResolutionToCode resolution
@@ -131,9 +133,9 @@

 # Checks whether or not a human-readable resolution name is valid.
 #
-# @param [Number] resolution the resolution code to check.
-#
-isValidResolutionCode = (resolution) -> humanResolutionToCode(resolution) > -1
+# @param [String] resolution the human-readable resolution name to check
+#
+isValidResolution = (resolution) -> humanResolutionToCode(resolution) > -1

 # Persists an object to a file as JSON.
@@ -201,18 +203,6 @@
   request options, callback

-# Returns the closest match from an array of matches (recursive).
-#
-# @param [Array] matches the array of matches to examine
-# @param [Object] closest the current closest match
-#
-closestMatch = (matches, closest) ->
-  if matches.length == 0 then return closest
-  next = matches.pop()
-  best = if closest && closest.distance <= next.distance then closest else next
-  closestMatch matches, best
-
-
 # Derives a search term to feed back in to the API from a hit.
 #
 # @param [Object] hit the hit to derive the search term for
@@ -236,50 +226,129 @@
   https.get url, (response) ->
     if response.statusCode == 200
       stream = response.pipe fs.createWriteStream(dest)
+      stream.on 'error', (error) ->
+        callback error
       stream.on 'close', () ->
-        callback null, url, dest # TODO: Gonna drop an error here.
-    else
-      callback true, url, dest # TODO: Error is true?
+        callback null
+    else
+      callback true # TODO: Error is true?

 # Downloads an image file from a URL and opens it.
 #
 # @param [String] url the URL to download the image from
-# @param [String] dest the destination file on disk
-# @param [Function] callback the function to call back on
-#
-requestImage = (url, dest, callback) ->
-  downloadToDisk url, dest, (error, url, dest) ->
-    jimp.read dest, callback
-
-
-downloadHits = (hits, callback, files) ->
+# @param [String] filename the destination file on disk
+# @param [Function] callback the function to call back on
+#
+# @example How to call this function
+#   requestImage url, filename, (error, img) ->
+#     console.log img
+#
+requestImage = (url, filename, callback) ->
+  downloadToDisk url, filename, (error) ->
+    jimp.read filename, callback
+
+
+# Caches the smallest resolution in a set of hits locally in a temporary folder.
+#
+# @param [Array] hits the hits to cache
+# @param [Function] callback the function to call back on
+# @param [Array] files used for head recursion
+#
+# @example How to call this function
+#   cacheHitsLocally hits, (error, files) ->
+#     console.log files
+#
+cacheHitsLocally = (hits, callback, files) ->
   if !files then files = [] # Initialize array for head recursion.
   if hits.length == 0
     callback null, files # Base case.
   else
-    item = hits.pop()
-    dest = "./picksel_temp/#{item.id_hash}.jpg"
-    files.push {filename:dest, hash:item.id_hash}
-    downloadToDisk item.previewURL, dest, (error, url, dest) ->
-      if !error
-        downloadHits hits, callback, files # Recursively process.
+    hit = hits.pop() # Take next hit.
+    filename = "#{AMBIG_DIR}/#{hit.id_hash}.temp"
+    file =
+      filename: filename
+      hash: hit.id_hash
+    files.push file # Push new file.
+    downloadToDisk hit.previewURL, filename, (error) ->
+      if error
+        callback error, null # Error during download.
       else
-        callback true, null
-
-
-matchImage = (base, files, callback, percs) ->
-  if !percs then percs = [] # Initialize array for head recursion.
+        cacheHitsLocally hits, callback, files # Recursively process.
+
+
+# Gets the perceptual distances for a set of image files from a base image.
+#
+# @param [Object] base the base image
+# @param [Array] files the files to compare to the base image
+# @param [Function] callback the function to call back on
+# @param [Array] distances used for head recursion
+#
+# @example How to call this function
+#   getDistances base, files, (error, distances) ->
+#     console.log distances
+#
+getDistances = (base, files, callback, distances) ->
+  if !distances then distances = [] # Initialize array for head recursion.
   if files.length == 0
-    callback null, percs # Base case.
+    callback null, distances # Base case.
   else
-    file = files.pop()
-    jimp.read file.filename, (error, img) ->
+    file = files.pop() # Take next file.
+    jimp.read file.filename, (error, img) -> # Load up next image.
       if error
         callback error, null
       else
-        percs.push {hash: file.hash, distance: jimp.distance(base, img)}
-        matchImage base, files, callback, percs
+        distance =
+          hash: file.hash
+          distance: jimp.distance base, img
+        distances.push distance # Push new distance.
+        getDistances base, files, callback, distances # Recurse.
+
+
+# Returns the closest match from an array of distances.
+#
+# @param [Array] matches the array of distances to examine
+# @param [Object] closest used for head recursion
+#
+getClosest = (matches, closest) ->
+  if matches.length == 0 then return closest # Base case.
+  next = matches.pop()
+  best = if closest && closest.distance <= next.distance then closest else next
+  getClosest matches, best
+
+
+# Reslolves an ambiguous set of image results encountered while attempting to
+# derive a hash ID from an ID.
+#
+# @param [Object] base the base image to compare
+# @param [Array] hits the ambiguous array of hits from the Pixabay API
+# @param [Function] callback the function to call back on
+#
+# @example How to call this function
+#   resolveAmbiguousResults base, hits, (error, hashId) ->
+#     console.log hashId
+#
+resolveAmbiguousResults = (base, hits, callback) ->
+  mkdir AMBIG_DIR, (error) -> # Create temporary folder.
+    if error
+      callback error, null # Couldn't create folder.
+    else
+      cacheHitsLocally hits, (error, files) ->
+        if error
+          log.error 'Couldn\'t download thumbnails for perceptual hashing.'
+          callback error, null # Failed, couldn't download files.
+        else
+          getDistances base, files, (error, distances) ->
+            rimraf AMBIG_DIR, fs, (error) -> # Delete temporary folder.
+              if error
+                log.warn "Couldn't clean up temporary folder at #{AMBIG_DIR}!"
+              else
+                rimraf AMBIG_BASE_PATH, fs, (error) -> # Delete temporary file.
+                  if error
+                    log.warn 'Couldn\'t clean up temporary file at' \
+                      + " #{AMBIG_BASE_PATH}!"
+                  else
+                    callback null, getClosest(distances).hash # Success!

 # Tries to derive the hash ID of the image with the given numeric ID.
@@ -287,12 +356,12 @@
 # @param [Number] id the ID of the image to get
 # @param [Function] callback the function to call back on
 #
-idToHashId = (id, callback) ->
+deriveHashId = (id, callback) ->
   url = buildUrl user.apiKey, id, 'tiny' # Initial query for image info.
   requestJson url, (error, response, body) ->
     if !error && response.statusCode == 200
       hit = body.hits[0]
-      requestImage hit.previewURL, "./pickseltemp_base.jpg", (err, base) ->
+      requestImage hit.previewURL, AMBIG_BASE_PATH, (err, base) ->
           search = deriveSearchTerm hit # Convert tags to search term.
           url = buildSearchUrl user.apiKey, search, 'full'
           requestJson url, (error, response, body) -> # Feed tags back in.
@@ -301,17 +370,7 @@
                 callback null, body.hits[0].id_hash # One result, simple!
               else
                 # Results are ambiguous, things get *way* more complicated.
-                mkdir './picksel_temp', (error) -> # Create temporary folder.
-                  if error
-                    callback error, null # Couldn't create folder.
-                  else
-                    downloadHits body.hits, (err, files) ->
-                      matchImage base, files, (err, percs) ->
-                        rimraf './picksel_temp', fs, () ->
-                          #WUT
-                        rimraf './pickseltemp_base.jpg', fs, () ->
-                          #WUT
-                        callback null, closestMatch(percs).hash
+                resolveAmbiguousResults base, body.hits, callback
             else
               callback error, null # Error during search.
     else
@@ -436,8 +495,9 @@
       log.warning 'A conflicting dependency is already present in your' \
         + ' project!' # No exact duplicates.
     else
-      if isValidResolutionCode resolution
+      if isValidResolution resolution
         validateId id, (success) ->
+          if
           if success
             image =
               id: id
@@ -668,7 +728,7 @@
           when 'install' then install() # Install assets.
           when 'add' then add process.argv # Add asset.
           when 'remove' then remove process.argv # Remove asset.
-          when 'test' then idToHashId '195893', (err, id) -> console.log id
+          when 'test' then deriveHashId '195893', (err, id) -> console.log id
       else
         # Workspace needs setting up first.
         log.error "Couldn't load workspace for above reason. Terminating."


KodingClient: Removed authCheckKey method

--- server/lib/server/helpers.coffee
+++ server/lib/server/helpers.coffee
@@ -27,20 +27,6 @@
   {authRegisterTemplate} = require './staticpages'
   {template}             = require 'underscore'
   template authRegisterTemplate, {msg}
-
-authCheckKey = (key, callback)->
-  if typeof key isnt "string"
-    return callback false, "Key is not of type string: '#{key}'"
-
-  if not key
-    return callback false, "Key is empty: '#{key}'"
-
-  key = decodeURIComponent key
-
-  if key.length isnt 64
-    return callback false, "Key does not have a len of 64 len: '#{key}'"
-
-  callback true, "key is valid"

 authenticationFailed = (res, err)->
   res.send "forbidden! (reason: #{err?.message or "no session!"})", 403
@@ -154,7 +140,6 @@
   error_404
   error_500
   authTemplate
-  authCheckKey
   authenticationFailed
   findUsernameFromKey
   findUsernameFromSession


add referrer to remote_url

add referrer to remote_url

--- src/api/xdm.coffee
+++ src/api/xdm.coffee
@@ -8,6 +8,7 @@
     remoteUrl += "&uid=#{config.uid}"   if config.uid
     remoteUrl += "&access_token=#{config.appSecret}" if config.appSecret
     remoteUrl += "&user_hash=#{config.userHash}" if config.userHash != undefined
+    remoteUrl += "&r=#{document.referrer}"
     remoteUrl

   (config, emitter)->


Use disposables in Atom

Use disposables in Atom

--- src/atom.coffee
+++ src/atom.coffee
@@ -7,7 +7,7 @@

 _ = require 'underscore-plus'
 {deprecate, includeDeprecations} = require 'grim'
-{Emitter} = require 'event-kit'
+{CompositeDisposable, Emitter} = require 'event-kit'
 {Model} = require 'theorist'
 fs = require 'fs-plus'
 {convertStackTrace, convertLine} = require 'coffeestack'
@@ -198,6 +198,7 @@
   # Call .loadOrCreate instead
   constructor: (@state) ->
     @emitter = new Emitter
+    @disposables = new CompositeDisposable
     {@mode} = @state
     DeserializerManager = require './deserializer-manager'
     @deserializers = new DeserializerManager()
@@ -232,7 +233,9 @@
       @emit 'uncaught-error', arguments...
       @emitter.emit 'did-throw-error', {message, url, line, column, originalError}

-    @unsubscribe()
+    @disposables?.dispose()
+    @disposables = new CompositeDisposable
+
     @setBodyPlatformClass()

     @loadTime = null
@@ -288,7 +291,7 @@
         deprecate "The atom.syntax global is deprecated. Use atom.grammars instead."
         @grammars

-    @subscribe @packages.onDidActivateInitialPackages => @watchThemes()
+    @disposables.add @packages.onDidActivateInitialPackages => @watchThemes()

     Project = require './project'
     TextBuffer = require 'text-buffer'
@@ -605,7 +608,7 @@
     @requireUserInitScript() unless safeMode

     @menu.update()
-    @subscribe @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
+    @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
       @setAutoHideMenuBar(newValue)
     @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar')

@@ -755,7 +758,7 @@

   # Notify the browser project of the window's current project path
   watchProjectPath: ->
-    @subscribe @project.onDidChangePaths =>
+    @disposables.add @project.onDidChangePaths =>
       @constructor.updateLoadSetting('initialPaths', @project.getPaths())

   exit: (status) ->


Simplify markdown escaping for warning

Simplify markdown escaping for warning

by using a regular expression instead of multiple replaces

--- src/atom.coffee
+++ src/atom.coffee
@@ -247,9 +247,7 @@
       responseChannel = "check-portable-home-writable-response"
       ipc.on responseChannel, (response) ->
         ipc.removeAllListeners(responseChannel)
-        escapeMarkdown = (inputMessage) ->
-          inputMessage.split('\\').join('\\\\').split('.').join('\\.').split('-').join('\\-').split('+').join('\\+').split('_').join('\\_').split('#').join('\\#').split('!').join('\\!')
-        atom.notifications.addWarning("#{escapeMarkdown response.message}") if not response.writable
+        atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
       ipc.send('check-portable-home-writable', responseChannel)

     checkPortableHomeWritable()


fix animation

fix animation

--- src/3d/BaseModel.coffee
+++ src/3d/BaseModel.coffee
@@ -114,7 +114,6 @@
     if options.reverse
       options.timeScale *= -1

-    @stopAnimations()
     anim = @animation(options.name)
     anim.setEffectiveTimeScale(options.timeScale)

@@ -126,6 +125,7 @@
         anim.clampWhenFinished = true
         anim.setLoop(THREE.LoopOnce, 0)

+    @stopAnimations()
     anim.play()

     if options.reverse && options.loop == false
@@ -142,7 +142,8 @@
   # stops all animations
   stopAnimations: ->
     for animation in @mesh.animations
-      animation.stop()
+      if animation.isRunning()
+        animation.stop()

   # checks if an animation is playing
   #


fixed grunt watch:js and removed watch:coffeescript

--- js/Gruntfile.coffee
+++ js/Gruntfile.coffee
@@ -105,11 +105,8 @@

 		watch:
-			coffeescript:
+			js:
 				files: ['app/**/*.coffee']
-				tasks: 'coffee'
-			js:
-				files: ['app/**/*.js']
 				tasks: 'js'
 			css:
 				files: ['../css/*.less']


Fix spec

Fix spec

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -684,7 +684,7 @@
         runs ->
           suggestionList = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list')
           wordContainer = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list .word-container')
-          expect(suggestionList.style['margin-left']).toBe "-#{wordContainer.offsetLeft}px"
+          expect(suggestionList.style['margin-left']).toBe "-#{wordContainer.offsetLeft - 1}px"

       it "keeps the suggestion list planted at the beginning of the prefix when typing", ->
         overlayElement = null


first iter refactoring boot.coffeee

first iter refactoring boot.coffeee

--- app/boot.coffee
+++ app/boot.coffee
@@ -16,24 +16,20 @@
     Records.memberships.fetchMyMemberships().then (data) ->
       membershipsLoadedDeferred.resolve()

-      # if user has no groups, log user out, display error dialog, and redirect home
-      if !data.groups
+      if CurrentUser().isActive() # redirect to group page with toast, set timezone, and confirm user if necessary
+        groupId = data.groups[0].id
+        $location.path("/groups/#{groupId}")
+        Toast.show('Welcome to Cobudget!')
+        if CurrentUser().utcOffset != moment().utcOffset()
+          Records.users.updateProfile(utc_offset: moment().utcOffset())
+        if !CurrentUser().isConfirmed
+          Records.users.updateProfile(confirmationToken: null)
+      else # if user is archived, log user out, display error dialog, and redirect home
         $auth.signOut().then ->
           global.cobudgetApp.currentUserId = null
           $location.path('/')
           Dialog.alert(title: 'error!', content: 'invalid credentials!')
           LoadBar.stop()
-
-      # if user has groups, and every group is initialized, redirect to group page
-      # with toast, set timezone, and confirm user if necessary
-      if data.groups && _.every(data.groups, {'initialized': true})
-        groupId = data.groups[0].id
-        $location.path("/groups/#{groupId}")
-        Toast.show('Welcome to Cobudget!')
-        if CurrentUser().utcOffset != moment().utcOffset()
-          Records.users.updateProfile(utc_offset: moment().utcOffset())
-        if CurrentUser().isPendingConfirmation
-          Records.users.updateProfile(confirmationToken: null)

   $rootScope.$on '$stateChangeError', (e, toState, toParams, fromState, fromParams, error) ->
     console.log('$stateChangeError signal fired!')


Link to existing implementations of the server protocol

--- lib/assets/javascripts/unpoly/protocol.coffee
+++ lib/assets/javascripts/unpoly/protocol.coffee
@@ -11,16 +11,22 @@
 exchange additional information when Unpoly is [updating fragments](/up.link).

 While the protocol can help you optimize performance and handle some
-edge cases, implementing it is entirely optional. For instance,
+edge cases, implementing it is **entirely optional**. For instance,
 `unpoly.com` itself is a static site that uses Unpoly on the frontend
 and doesn't even have a server component.

-If you have [installed Unpoly as a Rails gem](/install/rails), the protocol
-is already implemented and you will get some
-[Ruby bindings](https://github.com/unpoly/unpoly/blob/master/README_RAILS.md)
-in your controllers and views. If your server-side app uses another language
-or framework, you should be able to implement the protocol in a very short time.
-
+## Existing implementations
+
+You should be able to implement the protocol in a very short time.
+There are existing implementations for various web frameworks:
+
+- [Ruby on Rails](/install/rails)
+- [Roda](https://github.com/adam12/roda-unpoly)
+- [Rack](https://github.com/adam12/rack-unpoly) (Sinatra, Padrino)
+- [Phoenix](https://elixirforum.com/t/unpoly-a-framework-like-turbolinks/3614/15) (Elixir)
+
+
+## Protocol details

 \#\#\# Redirect detection for IE11


Show help not found as a warning

Show help not found as a warning

--- lib/atom-perl6-editor-tools-hyperclick.coffee
+++ lib/atom-perl6-editor-tools-hyperclick.coffee
@@ -48,7 +48,7 @@
     return unless @keywords?

     o = @keywords[word]
-    helpText = "Help not found for #{word}"
+    helpText = null
     if o?
       url = "http://doc.perl6.org#{o.url}"
       helpText = """
@@ -62,6 +62,9 @@
       callback: () ->
         options =
           dismissable: true
-        atom.notifications.addInfo(helpText, options)
+        if helpText?
+          atom.notifications.addInfo(helpText, options)
+        else
+          atom.notifications.addWarning("Help not found for #{word}")
         return
     }