Inject ModelingChart.

Inject ModelingChart.

--- app/javascripts/controllers.coffee
+++ app/javascripts/controllers.coffee
@@ -1,9 +1,9 @@
-define ['angular', 'lodash', 'ngsanitize', 'modeling', 'breast'],
+define ['angular', 'lodash', 'ngsanitize', 'resources', 'modelingchart', 'breast', 'slice'],
   (ng, _) ->
     ctlrs = ng.module(
       'qiprofile.controllers',
-      ['ngSanitize', 'ui.bootstrap', 'qiprofile.modelingchart',
-       'qiprofile.resources', 'qiprofile.breast']
+      ['ngSanitize', 'ui.bootstrap', 'qiprofile.resources',
+       'qiprofile.modelingchart', 'qiprofile.breast', 'qiprofile.slice']
     )

     # The local controller helper methods.
@@ -293,7 +293,7 @@
       ($scope, ModelingChart) ->
         # The d3 chart configuration.
         if $scope.selModeling?
-          $scope.config = ModelingChart.configure($scope.selModelingChart.results,
+          $scope.config = ModelingChart.configure($scope.selModeling.results,
                                                   $scope.dataSeriesConfig)
     ]


Lib updated

Lib updated

--- vendor/assets/javascripts/jquery.awesome-cropper.coffee
+++ vendor/assets/javascripts/jquery.awesome-cropper.coffee
@@ -130,11 +130,10 @@
     oWidth = img.attr('data-original-width')
     oHeight = img.attr('data-original-height')

-    console.log(width, height)
     if oWidth > oHeight
-      r = img.attr('data-original-height') / img.height()
+      r = oHeight / img.height()
     else
-      r = img.attr('data-original-width') / img.width()
+      r = oWidth / img.width()

     sourceX = Math.round(x * r)
     sourceY = Math.round(y * r)
@@ -145,9 +144,6 @@
     destWidth = settings.width
     destHeight = settings.height

-    console.log(sourceWidth, sourceHeight)
-    console.log(img.attr('data-original-height'), img.attr('data-original-width'))
-
     context = $cropSandbox.get(0).getContext('2d')
     context.drawImage(img.get(0), sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight)

@@ -159,7 +155,7 @@
       x2 = image.width()
       y2 = image.width()

-    drawImage($sourceIm, 0, 0, x2, y2)
+    drawImage($sourceIm, 0, 0, x2 - 1, y2 - 1)

     image.imgAreaSelect
       aspectRatio: '1:1'


Fix scroll.js.coffee

Fix scroll.js.coffee

--- vendor/assets/javascripts/oulu/helpers/scroll.js.coffee
+++ vendor/assets/javascripts/oulu/helpers/scroll.js.coffee
@@ -7,5 +7,3 @@
     $("html, body").animate
       scrollTop: position
     , speed, "swing"
-    false
-  return


Grouped activities! :D

Grouped activities! :D

--- core/app/backbone/views/activities/user_activities_view.coffee
+++ core/app/backbone/views/activities/user_activities_view.coffee
@@ -1,7 +1,4 @@
 class window.UserActivitiesGroupView extends Backbone.Marionette.CompositeView
-  @actions: []
-  actions: -> UserActivitiesGroupView.actions
-
   template:
     text:
       """
@@ -20,6 +17,9 @@
   itemView: Backbone.View
   itemViewContainer: ".js-region-activities"

+  @actions: ["added_first_factlink", "created_channel", "added_subchannel"]
+  actions: -> UserActivitiesGroupView.actions
+
   @new: (options)->
     new (@classForModel(options.model))(options)

@@ -28,12 +28,6 @@

     if action in UserFactActivitiesGroupView.actions
       UserFactActivitiesGroupView
-    else if action in UserChannelActivitiesGroupView.actions
-      UserChannelActivitiesGroupView
-    else if action in UserEvidenceActivitiesGroupView.actions
-      UserEvidenceActivitiesGroupView
-    else if action in UserOpinionActivitiesGroupView.actions
-      UserOpinionActivitiesGroupView
     else
       UserActivitiesGroupView

@@ -54,7 +48,7 @@
     @lastView = super(item, NewItemView, options)

 class UserFactActivitiesGroupView extends UserActivitiesGroupView
-  @actions: ["added_fact_to_channel"]#, "created_comment", "created_sub_comment"]
+  @actions: ["added_fact_to_channel", "created_comment", "created_sub_comment", "added_supporting_evidence", "added_weakening_evidence", "believes", "doubts", "disbelieves"]
   actions: -> UserFactActivitiesGroupView.actions

   template:
@@ -86,20 +80,3 @@

   appendable: (model) -> super(model) and @sameFact(model)

-class UserChannelActivitiesGroupView extends UserActivitiesGroupView
-  @actions: ["created_channel", "added_subchannel"]
-  actions: -> UserChannelActivitiesGroupView.actions
-
-class UserEvidenceActivitiesGroupView extends UserFactActivitiesGroupView
-  @actions: ["added_supporting_evidence", "added_weakening_evidence"]
-  actions: -> UserEvidenceActivitiesGroupView.actions
-
-class UserOpinionActivitiesGroupView extends UserFactActivitiesGroupView
-  @actions: ["believes", "doubts", "disbelieves"]
-  actions: -> UserOpinionActivitiesGroupView.actions
-
-  appendHtml: (collectionView, itemView, index)->
-    if !@has_view
-      collectionView.$(@itemViewContainer).append(itemView.el)
-      @has_view = true
-    # else: we do not care for old opinions


Account for Absence of researcherID

Account for Absence of researcherID

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -145,7 +145,7 @@
                 placeholder="Choose a Researcher"
                 onChange={@handleResearcherChange}
                 options={@researcherOptions()}
-                value={@props.project.configuration.researcherID} />
+                value={@props.project?.configuration?.researcherID} />
               <textarea className="standard-input full" name="researcher_quote" value={@props.project.researcher_quote} onChange={handleInputChange.bind @props.project} />
             </AutoSave>
             <small className="form-help">This text will appear on a project landing page alongside an avatar of the selected researcher. <CharLimit limit={255} string={@props.project.researcher_quote ? ''} /></small>


Remove promise renderer from project-details.cjsx (#3322)

* remove promise render for avatar

* remove promise renderer for background image

* conditional for handling the resource type returned

* consolidate promises and errors

* remove require for PromiseRenderer

consolidate error variables

remove unnecessary conditional

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -1,7 +1,6 @@
 React = require 'react'
 AutoSave = require '../../components/auto-save'
 handleInputChange = require '../../lib/handle-input-change'
-PromiseRenderer = require '../../components/promise-renderer'
 ImageSelector = require '../../components/image-selector'
 apiClient = require 'panoptes-client/lib/api-client'
 putFile = require '../../lib/put-file'
@@ -28,11 +27,12 @@

   getInitialState: ->
     {disciplineTagList, otherTagList} = @splitTags()
-    avatarError: null
-    backgroundError: null
     disciplineTagList: disciplineTagList
     otherTagList: otherTagList
     researchers: []
+    avatar: null
+    background: null
+    error: null

   componentWillMount: ->
     @props.project.get('project_roles', page_size: 100).then (roles) =>
@@ -40,6 +40,15 @@
         role.links.owner.id
       apiClient.type('users').get(scientists).then (researchers) =>
         @setState({ researchers })
+
+    avatar = @props.project.get('avatar')
+    background = @props.project.get('background')
+    Promise.all([avatar, background])
+      .then ([avatar, background]) =>
+        @setState {avatar, background}
+      .catch (error) =>
+        @setState {error}
+        console.log error

   splitTags: (kind) ->
     disciplineTagList = []
@@ -62,38 +71,26 @@
     # Failures on media GETs are acceptable here,
     # but the JSON-API lib doesn't cache failed requests,
     # so do it manually:
-
-    @avatarGet ?= @props.project.get 'avatar'
-      .catch ->
-        null
-
-    @backgroundGet ?= @props.project.get 'background'
-      .catch ->
-        null
+    avatarPlaceholder =
Drop an avatar image here

+    backgroundPlaceholder =
Drop a background image here

Input the basic information about your project, and set up its home page.

           Avatar

-          <PromiseRenderer promise={@avatarGet} then={(avatar) =>
-            placeholder =
Drop an avatar image here

-            <ImageSelector maxSize={MAX_AVATAR_SIZE} ratio={1} src={avatar?.src} placeholder={placeholder} onChange={@handleMediaChange.bind this, 'avatar'} />
-          } />
-          {if @state.avatarError
-
{@state.avatarError.toString()}
}
+          <ImageSelector maxSize={MAX_AVATAR_SIZE} ratio={1} src={@state.avatar?.src} placeholder={avatarPlaceholder} onChange={@handleMediaChange.bind this, 'avatar'} />
+          {if @state.error
+
{@state.error.toString()}
}

Pick a logo to represent your project. To add an image, either drag and drop or click to open your file viewer. For best results, use a square image of not more than 50 KB.

           Background image

-          <PromiseRenderer promise={@backgroundGet} then={(background) =>
-            placeholder =
Drop a background image here

-            <ImageSelector maxSize={MAX_BACKGROUND_SIZE} src={background?.src} placeholder={placeholder} onChange={@handleMediaChange.bind this, 'background'} />
-          } />
-          {if @state.backgroundError
-
{@state.backgroundError.toString()}
}
+          <ImageSelector maxSize={MAX_BACKGROUND_SIZE} src={@state.background?.src} placeholder={backgroundPlaceholder} onChange={@handleMediaChange.bind this, 'background'} />
+          {if @state.error
+
{@state.error.toString()}
}

This image will be the background for all of your project pages, including your project’s front page. To add an image, either drag and drop or left click to open your file viewer. For best results, use good quality images no more than 256 KB.


:art:

:art:

--- spec/results-view-spec.coffee
+++ spec/results-view-spec.coffee
@@ -314,22 +314,23 @@

       it "selects the first result on the next page when core:page-up is triggered", ->
         atom.commands.dispatch resultsView.element, 'core:move-to-bottom'
-        expect(resultsView.find("li:last")).toHaveClass 'selected'

         itemHeight = resultsView.find('.selected').outerHeight()
         pageHeight = Math.round(resultsView.innerHeight() / itemHeight) * itemHeight
         initialScrollTop = resultsView.scrollTop()
         itemsPerPage = Math.floor(pageHeight / itemHeight)
-        originalItemIndex = Math.floor(initialScrollTop / itemHeight) + itemsPerPage
+
+        initiallySelectedIndex = Math.floor(initialScrollTop / itemHeight) + itemsPerPage
+        expect(resultsView.find("li:eq(#{initiallySelectedIndex})")).toHaveClass 'selected'

         atom.commands.dispatch resultsView.element, 'core:page-up'
-        expect(resultsView.find("li:last")).not.toHaveClass 'selected'
-        expect(resultsView.find("li:eq(#{originalItemIndex - itemsPerPage})")).toHaveClass 'selected'
+        expect(resultsView.find("li:eq(#{initiallySelectedIndex})")).not.toHaveClass 'selected'
+        expect(resultsView.find("li:eq(#{initiallySelectedIndex - itemsPerPage})")).toHaveClass 'selected'
         expect(resultsView.prop('scrollTop')).toBe initialScrollTop - pageHeight

         atom.commands.dispatch resultsView.element, 'core:page-up'
-        expect(resultsView.find("li:eq(#{originalItemIndex - itemsPerPage})")).not.toHaveClass 'selected'
-        expect(resultsView.find("li:eq(#{originalItemIndex - itemsPerPage * 2})")).toHaveClass 'selected'
+        expect(resultsView.find("li:eq(#{initiallySelectedIndex - itemsPerPage})")).not.toHaveClass 'selected'
+        expect(resultsView.find("li:eq(#{initiallySelectedIndex - itemsPerPage * 2})")).toHaveClass 'selected'
         expect(resultsView.prop('scrollTop')).toBe initialScrollTop - pageHeight * 2

         _.times 60, ->


Dispatch command on correct elements in specs

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -8,12 +8,15 @@
 describe "SymbolsView", ->
   [symbolsView, activationPromise, editor] = []

+  getWorkspaceView = -> atom.views.getView(atom.workspace)
+  getEditorView = -> atom.views.getView(atom.workspace.getActiveTextEditor())
+
   beforeEach ->
     atom.project.setPath(temp.mkdirSync('atom-symbols-view-'))
     fs.copySync(path.join(__dirname, 'fixtures'), atom.project.getPath())

     activationPromise = atom.packages.activatePackage("symbols-view")
-    jasmine.attachToDOM(atom.views.getView(atom.workspace))
+    jasmine.attachToDOM(getWorkspaceView())

     waitsForPromise ->
       atom.packages.activatePackage('language-ruby')
@@ -24,13 +27,13 @@
         atom.workspace.open('sample.js')

       runs ->
-        atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), "symbols-view:toggle-file-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -38,7 +41,7 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
+        expect($(getWorkspaceView()).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'quicksort'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'Line 1'
@@ -52,13 +55,13 @@

       runs ->
         editor = atom.workspace.getActiveEditor()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -66,7 +69,7 @@
       runs ->
         symbolsView.cancel()
         spyOn(symbolsView, 'generateTags').andCallThrough()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -77,7 +80,7 @@
         expect(symbolsView.generateTags).not.toHaveBeenCalled()
         editor.getBuffer().emit 'saved'
         symbolsView.cancel()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -94,13 +97,13 @@
         atom.workspace.open('sample.js')

       runs ->
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -109,7 +112,7 @@
         symbolsView.filterEditorView.setText("nothing will match this")
         window.advanceClock(symbolsView.inputThrottle)

-        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
+        expect($(getWorkspaceView()).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 0
         expect(symbolsView.error).toBeVisible()
         expect(symbolsView.error.text().length).toBeGreaterThan 0
@@ -127,13 +130,13 @@
         atom.workspace.open('sample.txt')

       runs ->
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()

       waitsFor ->
         symbolsView.error.isVisible()
@@ -151,14 +154,14 @@

     runs ->
       expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [0,0]
-      expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).not.toExist()
-      atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+      expect($(getWorkspaceView()).find('.symbols-view')).not.toExist()
+      atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")

     waitsFor ->
-      $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li').length
+      $(getWorkspaceView()).find('.symbols-view').find('li').length

     runs ->
-      $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li:eq(1)').mousedown().mouseup()
+      $(getWorkspaceView()).find('.symbols-view').find('li:eq(1)').mousedown().mouseup()
       expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [1,2]

   describe "TagGenerator", ->
@@ -194,7 +197,7 @@
       runs ->
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([0,2])
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

       waitsForPromise ->
         activationPromise
@@ -210,7 +213,7 @@
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([6,24])
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -225,13 +228,13 @@
       runs ->
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([8,14])
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
-
-      waitsFor ->
-        $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li').length > 0
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')
+
+      waitsFor ->
+        $(getWorkspaceView()).find('.symbols-view').find('li').length > 0
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView).toBeVisible()
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
@@ -253,7 +256,7 @@
       runs ->
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
         atom.workspace.getActiveEditor().setCursorBufferPosition([13,4])
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -262,7 +265,7 @@
         expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [5,2]
         SymbolsView::moveToPosition.reset()
         atom.workspace.getActiveEditor().setCursorBufferPosition([14,2])
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -271,7 +274,7 @@
         expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [9,2]
         SymbolsView::moveToPosition.reset()
         atom.workspace.getActiveEditor().setCursorBufferPosition([15,5])
-        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+        atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -287,7 +290,7 @@
         runs ->
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([6,0])
-          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:return-from-declaration')
+          atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration')
           expect(editor.getCursorBufferPosition()).toEqual [6,0]

       it "returns to previous row and column", ->
@@ -298,14 +301,14 @@
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([6,24])
           spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+          atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 1

         runs ->
           expect(editor.getCursorBufferPosition()).toEqual [2,0]
-          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:return-from-declaration')
+          atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 2
@@ -324,7 +327,7 @@
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([8,14])
           spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+          atom.commands.dispatch(getWorkspaceView(), 'symbols-view:go-to-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 1
@@ -340,14 +343,14 @@
         atom.workspace.open("tagged.js")

       runs ->
-        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).not.toExist()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        expect($(getWorkspaceView()).find('.symbols-view')).not.toExist()
+        atom.commands.dispatch(getWorkspaceView(), "symbols-view:toggle-project-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -355,14 +358,14 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
+        expect($(getWorkspaceView()).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 4
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'callMeMaybe'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'tagged.js'
         expect(symbolsView.list.children('li:last').find('.primary-line')).toHaveText 'thisIsCrazy'
         expect(symbolsView.list.children('li:last').find('.secondary-line')).toHaveText 'tagged.js'
         expect(symbolsView.error).not.toBeVisible()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")
+        atom.commands.dispatch(getWorkspaceView(), "symbols-view:toggle-project-symbols")

         fs.removeSync(atom.project.resolve('tags'))

@@ -370,10 +373,10 @@
         symbolsView.reloadTags

       runs ->
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")
-
-      waitsFor ->
-        symbolsView.error.isVisible()
+        atom.commands.dispatch(getWorkspaceView(), "symbols-view:toggle-project-symbols")
+
+      waitsFor ->
+        symbolsView.error.text().length > 0

       runs ->
         expect(symbolsView.list.children('li').length).toBe 0
@@ -384,13 +387,13 @@
           fs.removeSync(atom.project.resolve("tagged.js"))

         it "doesn't open the editor", ->
-          atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")
+          atom.commands.dispatch(getWorkspaceView(), "symbols-view:toggle-project-symbols")

           waitsForPromise ->
             activationPromise

           runs ->
-            symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+            symbolsView = $(getWorkspaceView()).find('.symbols-view').view()

           waitsFor ->
             symbolsView.list.children('li').length > 0
@@ -414,19 +417,19 @@
       runs ->
         atom.workspace.getActiveEditor().setText("var test = function() {}")
         atom.workspace.getActiveEditor().save()
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
-
-      waitsForPromise ->
-        activationPromise
-
-      waitsFor ->
-        $(atom.views.getView(atom.workspace)).find('.symbols-view').view().error.isVisible()
-
-      runs ->
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      waitsFor ->
+        $(getWorkspaceView()).find('.symbols-view').view().error.isVisible()
+
+      runs ->
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
         atom.workspace.getActiveEditor().setGrammar(atom.syntax.grammarForScopeName('source.js'))
-        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
-        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
+        atom.commands.dispatch(getEditorView(), "symbols-view:toggle-file-symbols")
+        symbolsView = $(getWorkspaceView()).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -434,6 +437,6 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
+        expect($(getWorkspaceView()).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'test'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'Line 1'


Select venue on load.

Select venue on load.

--- app/scripts/controllers/map.coffee
+++ app/scripts/controllers/map.coffee
@@ -86,3 +86,7 @@
         group = L.featureGroup items.map((i) -> i.marker)
         map.fitBounds group.getBounds(), padding: [100, 100]
         group.addTo map
+
+        # Pre-select the venue
+        unless $scope.selected
+          $scope.selected = items.filter((i) -> i.properties.type is 'venue')[0]


array_extensions

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -489,14 +489,9 @@

 @name reject
 @methodOf Array#
-
-@param {Function} iterator The iterator receives each element in turn as
-the first agument.
-@param {Object} [context] Optional context parameter to be
-used as `this` when calling the iterator function.
-
-@type Array
-@returns An array containing the elements for which the iterator returned false.
+@param {Function} iterator The iterator receives each element in turn as the first agument.
+@param {Object} [context] Optional context parameter to be used as `this` when calling the iterator function.
+@returns {Array} An array containing the elements for which the iterator returned false.
 ###
 Array::reject = (iterator, context) ->
   this.partition(iterator, context)[1]


Fix render-issue when window dpr is the same as device dpr

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -174,7 +174,14 @@
 			centerLayer(@screen)

 			@setHand(@selectedHand) if @selectedHand and @_orientation is 0
-			@_context.devicePixelRatio = @_device.devicePixelRatio ? 1
+
+			pixelRatio = @_device.devicePixelRatio ? 1
+			if window.devicePixelRatio is pixelRatio and Utils.isDesktop()
+				# On desktop rendering natively without scaling looks better, so do that
+				@_context.devicePixelRatio = 1
+				@content.scale = pixelRatio
+			else
+				@_context.devicePixelRatio = pixelRatio

 	_shouldRenderFullScreen: ->


Render device fullscreen when Canvas size matches with device size.

This is great when prototyping for TV.

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -141,6 +141,7 @@
 			@background.width  = window.innerWidth  + (2 * backgroundOverlap)
 			@background.height = window.innerHeight + (2 * backgroundOverlap)

+			@_updateDeviceImage()
 			@hands.scale = @_calculatePhoneScale()
 			@hands.center()
 			@phone.center()
@@ -176,6 +177,9 @@
 			return true

 		if Utils.deviceType() is "phone" and @_device.deviceType is "tablet"
+			return true
+
+		if @_device.screenWidth is Canvas.width and @_device.screenHeight is Canvas.height
 			return true

 		return false


Allow 'enter' to submit class code on CampaignView (#5057)

In the class code entry text box in campaign view for anonymous players, allow 'enter' key to submit the class code.

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -556,6 +556,12 @@
   afterRender: ->
     super()
     @onWindowResize()
+
+    $('#anon-classroom-signup-code').keydown (event) ->
+      if (event.keyCode == 13)
+        # click join classroom button if enter is pressed in the text box
+        $("#anon-classroom-join-btn").click()
+
     unless application.isIPadApp
       _.defer => @$el?.find('.game-controls .btn:not(.poll)').addClass('has-tooltip').tooltip()  # Have to defer or i18n doesn't take effect.
       view = @


Further bugfixes in longest-simple-path-finder.

Forgot to add the + 2 to case 1 vs. case 2 comparison, and we need to account
for the fact that if there's only one child, it's + 1, not + 2.

--- maze.coffee
+++ maze.coffee
@@ -76,8 +76,9 @@
     else
       # Calculate heights, diameters and most distant nodes of children:
       results = (@maxpath(child, cell) for child in children)
-      # Add dummy result in case there's only one:
-      results.push({cell1: null, cell2: null, height: 0, diameter: 0})
+      # Add dummy result in case there's only one. The -1 is to compensate for
+      # the fact that we can only add one edge in the first case, not two.
+      results.push({cell1: cell, height: -1, diameter: 0})
       # Find the two highest subtrees:
       results.sort((a, b) -> b.height - a.height)
       highest = results[0]
@@ -85,7 +86,7 @@
       # Find the subtree with the greatest diameter:
       results.sort((a, b) -> b.diameter - a.diameter)
       amplest = results[0]
-      if highest.height + second_highest.height > amplest.diameter
+      if highest.height + second_highest.height + 2 > amplest.diameter
         # The longest simple path passes through cell
         {cell1: highest.cell1, cell2: second_highest.cell1, \
             height: highest.height + 1, \


Esc/backspace takes you back to level chooser

--- maze.coffee
+++ maze.coffee
@@ -326,7 +326,9 @@
     @messagebox.fadeIn(600)

   handleEvent: (event) =>
-    if @status == 'playing'
+    if event.which == 27 or event.which == 8
+      @playAgain(@maze.dimensions[0])
+    else if @status == 'playing'
       if event.which == 82 # R key
         @goUp()
       else if event.which == 70 # F key
@@ -339,9 +341,9 @@
         @goRight()
       else if event.which == 40 # Down key
         @goBackward()
-    if @status == 'frozen'
+    else if @status == 'frozen'
        if event.which == 80 # P key
-         @playAgain()
+         @playAgain(@maze.dimensions[0] + 1)
        else if event.which == 84 # T key
          @tweet()

@@ -446,7 +448,7 @@
     @messagebox.html('
' + @currentMessage() + '
')
     if @maze.isFinish([@x, @y, @z])
       @freeze()
-      @messagebox.append($('

').append(@makeActionLink('tweet', => @tweet())).append('      ').append(@makeActionLink('play again', => @playAgain())))
+      @messagebox.append($('

').append(@makeActionLink('tweet', => @tweet())).append('      ').append(@makeActionLink('play again', => @playAgain(@maze.dimensions[0] + 1))))
     if [@x, @y, @z] of @maze.portals
       level = @maze.portals[[@x, @y, @z]]
       @destroy( =>
@@ -462,10 +464,10 @@
             href: '#'
         }).append(text).click(callback)

-  playAgain: =>
+  playAgain: (nextLevel=3) =>
     @destroy( =>
-        new LevelChooserUI(new LevelChooser(@maze.dimensions[0] + 1), @frame,
-            @messagebox, @viewport)
+        new LevelChooserUI(new LevelChooser(nextLevel), @frame, @messagebox,
+                @viewport)
     )

   tweet: =>


Updated with SoundCloud regex

Updated with SoundCloud regex

*i've never done regex before

--- src/aotw.coffee
+++ src/aotw.coffee
@@ -35,7 +35,7 @@

         # Restrict commands denoted by a tilde to the following users.
         # If left empty, any user can issue restricted commands.
-        @admins = ["colt", "thomas", "Shell"]
+        @admins = ["colt", "thomas","stevendiaz", "Shell"] #hehe

         @robot.brain.on "loaded", storageLoaded
         storageLoaded()
@@ -87,7 +87,8 @@
             spotify = /https?:\/\/(open|play)\.spotify\.com\/(album|track|user\/[^\/]+\/playlist)\/([a-zA-Z0-9]+)/
             googlePlay = /https?:\/\/(music|play)\.google\.com\/music\/m\/([a-zA-Z0-9]+)/
             youtube = /https?:\/\/(?:www\.)?youtube.com\/watch\?(?=.*v=\w+)(?:\S+)?/
-            if url.match(spotify) or url.match(googlePlay) or url.match(youtube)
+            soundCloud = /^https?:\/\/(soundcloud.com|snd.sc)\/(.*)$/
+            if url.match(spotify) or url.match(googlePlay) or url.match(youtube) or url.match(soundCloud)
                 user = msg.message.user.name.toLowerCase()
                 try
                     @storage.nominations.push(user: user, url: msg.match[2])


Fix $.popup in some browsers

Fix $.popup in some browsers

--- assets/javascripts/lib/util.coffee
+++ assets/javascripts/lib/util.coffee
@@ -329,8 +329,11 @@

 $.popup = (value) ->
   win = window.open()
-  win.opener = null if win.opener
-  win.location = value.href or value
+  if win
+    win.opener = null if win.opener
+    win.location = value.href or value
+  else
+    window.open value.href or value, '_blank'
   return

 $.isTouchScreen = ->


Add env version and window dimensions to serialized state

--- src/atom-environment.coffee
+++ src/atom-environment.coffee
@@ -640,11 +640,13 @@
     @openInitialEmptyEditorIfNecessary()

   serialize: ->
+    version: @constructor.version
     project: @project.serialize()
     workspace: @workspace.serialize()
     packageStates: @packages.serialize()
     grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
     fullScreen: @isFullScreen()
+    windowDimensions: @windowDimensions

   unloadEditorWindow: ->
     return if not @project


fix validation of dataset

fix validation of dataset

--- client/code/model/dataset.coffee
+++ client/code/model/dataset.coffee
@@ -34,7 +34,7 @@
     $.ajax settings

   validate: (attrs) ->
-    return "Please enter a name" if 'displayName' of attrs and attrs.displayName.length < 1
+    return "Please enter a name" if 'displayName' of attrs and attrs.displayName?.length < 1

 class Cu.Collection.DatasetList extends Backbone.Collection
   model: Cu.Model.Dataset


added on select method for the select reps partial views

--- app/scripts/controllers/app.coffee
+++ app/scripts/controllers/app.coffee
@@ -36,6 +36,14 @@
       else
         console.warn "Error setting reps_list and overview data: ", error

+#####################
+# Define Non-API Methods
+#####################
+
+    # this is used to update a selected rep based on the input boxes
+    $scope.onSelect = ($item, $model, $label, rep)->
+      $scope.selected[rep] = $item
+
 ######################
 # Initial Method Calls
 ######################


Key bindings; reinstate keyToCommandRegistry.

... We need it for the help page.

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -120,7 +120,6 @@
           currentMapping[key] = registryEntry
         keys = rest
     chrome.storage.local.set normalModeKeyStateMapping: keyMapping
-    @keyToCommandRegistry = {} # We don't need this any more, so free up the memory.

   # An ordered listing of all available commands, grouped by type. This is the order they will
   # be shown in the help page.


make starting console sessions more robust

--- salvus/smc-project/local_hub.coffee
+++ salvus/smc-project/local_hub.coffee
@@ -316,7 +316,7 @@
     # Get or create session with given uuid.
     # Can be safely called several times at once without creating multiple sessions...
     get_session: (mesg, cb) =>
-        # TODO: must be robust against multiple clients opening same session_id at once, which
+        # NOTE: must be robust against multiple clients opening same session_id at once, which
         # would be likely to happen on network reconnect.
         winston.debug("get_session: console session #{mesg.session_uuid}")
         session = @_sessions[mesg.session_uuid]
@@ -361,7 +361,7 @@
                             winston.debug("restarted console server, then got port = #{port}")
                             cb()
             (cb) =>
-                # Got port -- now create the new session
+                winston.debug("console: Got port -- now create the new session")
                 @_new_session mesg, port, (err, _session) =>
                     if err
                         cb(err)
@@ -431,8 +431,23 @@
             # Request a Console session from console_server
             misc_node.enable_mesg(console_socket)
             console_socket.write_mesg('json', mesg)
+
+            # Below we wait for one message to come back from the console_socket.
+            # However, if 5s elapses with no response -- which could happen! --
+            # we give up, and return an error.  We then set cb undefined in case
+            # the session does actually work.
+            no_response = =>
+                if cb?
+                    cb("no response")
+                    console_socket.destroy()
+                    cb = undefined  # make sure doesn't get used below
+            no_response_timeout = setTimeout(no_response, 5000)
+
             # Read one JSON message back, which describes the session
             console_socket.once 'mesg', (type, desc) =>
+                clearTimeout(no_response_timeout)
+                if not cb?  # already failed
+                    return
                 if not history?
                     history = new Buffer(0)
                 # in future, history could be read from a file


* [bug] [autoUpdateData] can not import data if no model update

--- src/auto-update-data.coffee
+++ src/auto-update-data.coffee
@@ -1,7 +1,7 @@
-Promise     = require('bluebird')
-
-autoupdate = require('./auto-update')
-loadDataFrom= require('./load-data-from')
+Promise       = require 'bluebird'
+autoupdate    = require './auto-update'
+loadDataFrom  = require './load-data-from'
+modelNames    = require './model-names'

 # update database and import data(if aDataFolder exists)
 module.exports = (aApp, aOptions) ->
@@ -9,7 +9,8 @@
   result = autoupdate(aApp)
   if aDataFolder
     result = result.then (models)->
-      models = Promise.map models, (model, index)->
+      vModelNames = (aOptions and aOptions.models) || modelNames
+      models = Promise.map vModelNames, (model, index)->
         loadDataFrom(aApp, model, aDataFolder)
       models
   result


Add a test demonstrating a bracket forwarding issue

--- spec/Scoping.coffee
+++ spec/Scoping.coffee
@@ -282,3 +282,38 @@
       in1.endGroup()
       in1.endGroup()
       in1.disconnect()
+    it 'should forward new-style brackets as expected regardless of sending order', (done) ->
+      expected = [
+        'CONN'
+        '< 1'
+        '< a'
+        'DATA 1bazPc1:2fooPc2:PcMerge'
+        '>'
+        '>'
+        'DISC'
+      ]
+      received = []
+
+      out.on 'connect', ->
+        received.push 'CONN'
+      out.on 'begingroup', (group) ->
+        received.push "< #{group}"
+      out.on 'data', (data) ->
+        received.push "DATA #{data}"
+      out.on 'endgroup', ->
+        received.push '>'
+      out.on 'disconnect', ->
+        received.push 'DISC'
+        chai.expect(received).to.eql expected
+        done()
+
+      in1.connect()
+      in1.beginGroup 1
+      in1.beginGroup 'a'
+      in1.send 'baz'
+      in1.endGroup()
+      in1.endGroup()
+      in1.disconnect()
+      in2.connect()
+      in2.send 'foo'
+      in2.disconnect()


Use correct logging.

Use correct logging.

--- src/client/Gruntfile.coffee
+++ src/client/Gruntfile.coffee
@@ -130,9 +130,9 @@
         base.app.stassets.promise.then writeFiles
     catch e
       # Assume it's already running. TODO look for EACCESS
-      grunt.log 'Failed to start client'
-      grunt.log e
-      grunt.log e.stack
+      grunt.log.error 'Failed to start client'
+      grunt.log.writeln e
+      grunt.verbose.writeln e.stack
       process.env.URL = 'localhost:8080' # Next best guess
       writeFiles()


cassandra -- remove from parametrized types from CQL queries, since the upgraded driver is bad in some ways.

--- salvus/cassandra.coffee
+++ salvus/cassandra.coffee
@@ -532,8 +532,8 @@
             table : required
             delta : 1
             cb    : required
-        query = "update counts set count=count+? where table_name=?"
-        @cql query, [opts.delta, opts.table], opts.cb
+        query = "UPDATE counts SET count=count+? where table_name=?"
+        @cql query, [new cql.types.Long(opts.delta), opts.table], opts.cb

     # Get count of entries in a table for which we manually maintain the count.
     get_table_counter: (opts) =>
@@ -1372,8 +1372,8 @@
                 ttl = "USING ttl #{opts.ttl}"
             else
                 ttl = ""
-            query = "UPDATE account_creation_actions #{ttl} SET actions=actions+{?} WHERE email_address=?"
-            @cql(query, [misc.to_json(opts.action), opts.email_address], opts.cb)
+            query = "UPDATE account_creation_actions #{ttl} SET actions=actions+{'#{misc.to_json(opts.action)}'} WHERE email_address=?"
+            @cql(query, [opts.email_address], opts.cb)
         else
             @select
                 table     : 'account_creation_actions'
@@ -1845,11 +1845,11 @@
             cb         : undefined
         async.parallel([
             (cb) =>
-                query = "UPDATE projects SET hide_from_accounts=hide_from_accounts+{?} WHERE project_id=?"
-                @cql(query, [opts.account_id, opts.project_id], cb)
-            (cb) =>
-                query = "UPDATE accounts SET hidden_projects=hidden_projects+{?} WHERE account_id=?"
-                @cql(query, [opts.project_id, opts.account_id], cb)
+                query = "UPDATE projects SET hide_from_accounts=hide_from_accounts+{#{opts.account_id}} WHERE project_id=?"
+                @cql(query, [opts.project_id], cb)
+            (cb) =>
+                query = "UPDATE accounts SET hidden_projects=hidden_projects+{#{opts.project_id}} WHERE account_id=?"
+                @cql(query, [opts.account_id], cb)
         ], (err) => opts.cb?(err))

     unhide_project_from_user: (opts) =>
@@ -1859,11 +1859,11 @@
             cb         : undefined
         async.parallel([
             (cb) =>
-                query = "UPDATE projects SET hide_from_accounts=hide_from_accounts-{?} WHERE project_id=?"
-                @cql(query, [opts.account_id, opts.project_id], cb)
-            (cb) =>
-                query = "UPDATE accounts SET hidden_projects=hidden_projects-{?} WHERE account_id=?"
-                @cql(query, [opts.project_id, opts.account_id], cb)
+                query = "UPDATE projects SET hide_from_accounts=hide_from_accounts-{#{opts.account_id}} WHERE project_id=?"
+                @cql(query, [opts.project_id], cb)
+            (cb) =>
+                query = "UPDATE accounts SET hidden_projects=hidden_projects-{#{opts.project_id}} WHERE account_id=?"
+                @cql(query, [opts.account_id], cb)
         ], (err) => opts.cb?(err))

     # Make it so the user with given account id is listed as a(n invited) collaborator or viewer
@@ -1892,12 +1892,12 @@
         async.series([
             # add account_id to the project's set of users (for the given group)
             (cb) =>
-                query = "UPDATE projects SET #{opts.group}=#{opts.group}+{?} WHERE project_id=?"
-                @cql(query, [opts.account_id, opts.project_id], cb)
+                query = "UPDATE projects SET #{opts.group}=#{opts.group}+{#{opts.account_id}} WHERE project_id=?"
+                @cql(query, [opts.project_id], cb)
             # add project_id to the set of projects (for the given group) for the user's account
             (cb) =>
-                query = "UPDATE accounts SET #{opts.group}=#{opts.group}+{?} WHERE account_id=?"
-                @cql(query, [opts.project_id, opts.account_id], cb)
+                query = "UPDATE accounts SET #{opts.group}=#{opts.group}+{#{opts.project_id}} WHERE account_id=?"
+                @cql(query, [opts.account_id], cb)
         ], opts.cb)

     remove_user_from_project: (opts) =>
@@ -1912,12 +1912,12 @@
         async.series([
             # remove account_id from the project's set of users (for the given group)
             (cb) =>
-                query = "UPDATE projects SET #{opts.group}=#{opts.group}-{?} WHERE project_id=?"
-                @cql(query, [opts.account_id, opts.project_id], cb)
+                query = "UPDATE projects SET #{opts.group}=#{opts.group}-{#{opts.account_id}} WHERE project_id=?"
+                @cql(query, [opts.project_id], cb)
             # remove project_id from the set of projects (for the given group) for the user's account
             (cb) =>
-                query = "UPDATE accounts SET #{opts.group}=#{opts.group}-{?} WHERE account_id=?"
-                @cql(query, [opts.project_id, opts.account_id], cb)
+                query = "UPDATE accounts SET #{opts.group}=#{opts.group}-{#{opts.project_id}} WHERE account_id=?"
+                @cql(query, [opts.account_id], cb)
         ], opts.cb)

     # SINGLE USE ONLY:


feat(timer): cancel interval on destroy of element

--- src/common/countdown.coffee
+++ src/common/countdown.coffee
@@ -4,7 +4,7 @@
   restrict: "E"
   replace: true
   template: "
{{ countdown }}
"
-  controller: ['$scope', '$attrs', '$interval', '$rootScope', ($scope, $attrs, $interval, $rootScope) ->
+  controller: ['$scope', '$element', '$attrs', '$interval', '$rootScope', ($scope, $element, $attrs, $interval, $rootScope) ->
     start = $attrs.start
     interval = undefined

@@ -30,6 +30,11 @@
       $interval.cancel interval

     startCountdown()
+
+    # Intervals stay even if we change views, so we need to cancel it on
+    # destroy of the element
+    $element.bind '$destroy', ->
+      $interval.cancel interval
   ]

 ]


word speed now base on frame per sec

word speed now base on frame per sec

--- src/kick.coffee
+++ src/kick.coffee
@@ -9,6 +9,7 @@
 class ShootingWord
     constructor: (@full) ->
         @remain = @full
+        @speed = 80
         @top = (stage_height - font_height) * Math.random()
         @left = stage_width
         @repr = @make_repr()
@@ -28,7 +29,7 @@
         @update()

     move: ->
-        @left -= 1 # TODO render smoother w/ word movement speed
+        @left -= @speed / fps.rate()
         @repr.css('left', @left)

     update: ->
@@ -45,12 +46,15 @@
 fps = new class
     constructor: ->
         @frames = 20
+        @actual = 0
         @time = 0
         @date = new Date()

     loop: ->
+        if @actual != @frames
+            @actual += 1
         now = new Date()
-        @time += (now - @date - @time) / @frames
+        @time += (now - @date - @time) / @actual
         @date = now

     rate: ->
@@ -67,7 +71,6 @@
         @die = false
         @word = null
         @pair = null
-        @game = null
         @longest = []

     reset: ->
@@ -104,6 +107,11 @@

     attack: ->
         @words.some (word) -> word.left <= 0
+
+    easter_egg: ->
+        if animate.id == 1
+            @words = []
+            @add('kick')

 inventory = new class
@@ -149,6 +157,7 @@
         @id = null

     start: ->
+        fps.constructor()
         @id = setInterval(@loop, 12)
         $('#playground').css('background-color', 'lightblue')

@@ -178,9 +187,7 @@
     if event.keyCode == 13 # enter
         if not animate.id?
             animate.start()
-            if animate.id == 1
-                pool.words = []
-                pool.add('kick')
+            pool.easter_egg()
     if event.keyCode in [8, 27, 46] # backspace, escape, delete
         player.reset()


return empty string when no key given

return empty string when no key given

--- src/l10n.coffee
+++ src/l10n.coffee
@@ -30,6 +30,7 @@
 		@db.getLocale = => locale.id

 		@db.get = (key, substitutions...) ->
+			return '' unless key
 			originalKey = key
 			# protection against method redefine
 			key = '$' + key if angular.isFunction @[key]


Add promise method

Add promise method

--- index.coffee
+++ index.coffee
@@ -75,6 +75,10 @@
 		done null, Object.keys(parser.visited_sitemaps)
 	queue.push urls

+exports.parseSitemapsPromise = (urls, url_cb, sitemap_test) ->
+	new Promise (resolve) ->
+		exports.parseSitemaps(urls, url_cb, sitemap_test, resolve)
+
 exports.sitemapsInRobots = (url, cb) ->
 	request.get url, (err, res, body) ->
 		return cb err if err


Add command to get stash count

Add command to get stash count

--- index.coffee
+++ index.coffee
@@ -175,6 +175,12 @@
         Impromptu.exec 'git fetch --all', (err, results) ->
           done err, results

+  register 'stashCount',
+    update: (done) ->
+      command = 'git stash list | wc -l'
+      Impromptu.exec command, (err, count) ->
+        done err, parseInt count.trim(), 10
+
   # Register the git repository.
   @repository.register 'git',
     root: git.root


Remove Obsolete namespace

Remove Obsolete namespace

--- lib/written/app/assets/javascripts/written/core/content.coffee
+++ lib/written/app/assets/javascripts/written/core/content.coffee
@@ -137,5 +137,3 @@

     texts.join '\n'

-
-Written.Uploaders = {}


added domainr suggestion

added domainr suggestion

--- node_modules_koding/domainer/lib/reseller/DomainService.coffee
+++ node_modules_koding/domainer/lib/reseller/DomainService.coffee
@@ -50,13 +50,11 @@
       callback err, status

-  domainrApi: (domainName, params, callback) ->
-    options =
-      domain : domainName
-
-    domainr.search options, (responseFromDomainr) =>
-      console.log "====== RESPONSE ======"
-      console.log responseFromDomainr
+  domainrApi: (domainName, callback) ->
+
+    domainr.search domainName, (responseFromDomainr) =>
+      suggestions = (domain["domain"] for domain in responseFromDomainr.results when domain["availability"] isnt "taken")
+      callback suggestions

   getTldPrice: (tld, callback) ->


Fixed closing of new modal api.

Fixed closing of new modal api.

--- app/assets/javascripts/app/lib/app_post/websocket.coffee
+++ app/assets/javascripts/app/lib/app_post/websocket.coffee
@@ -203,7 +203,7 @@
       # close error message show up (because try so connect again) if exists
       App.Delay.clear('websocket-no-connection-try-reconnect-message', 'ws')
       if @error
-        @error.hide()
+        @error.close()
         @error        = false
         @tryToConnect = false


Use Angular 1.3.0-beta.13

Use Angular 1.3.0-beta.13

It has support for datetime inputs bound with ng-model.

--- app/assets/javascripts/hyper_admin/application.js.coffee
+++ app/assets/javascripts/hyper_admin/application.js.coffee
@@ -1,6 +1,6 @@
 #= require jquery
 #= require jquery_ujs
-#= require angular
+#= require unstable/angular
 #= require angular-ui-router
 #= require lodash
 #= require restangular


tweak jax scene

tweak jax scene

--- app/assets/javascripts/jax/dev/controllers/jax.js.coffee
+++ app/assets/javascripts/jax/dev/controllers/jax.js.coffee
@@ -15,24 +15,28 @@

   index: ->
     @timer = 0
-    @light = @world.addLight new Jax.Light.Directional
+    @light = @world.addLight new Jax.Light.Spot
       position: [3, 3, 3]
       direction: [1, -1, 1]
       attenuation:
-        linear: 1
+        constant: 1
+        linear: 0
+        quadratic: 0
+        # linear: 1
       shadows: true
       color:
-        ambient: '#222'
+        ambient: '#111'
         diffuse: '#eee'
         specular: '#fff'
-    @light.camera.rotation = [0.192552, -0.735685, -0.233828, -0.605820]
+    @light.camera.lookAt [0, 0, 0]
+    # @light.camera.rotation = [0.192552, -0.735685, -0.233828, -0.605820]
     # HACK this shouldn't be necessary
     @light.camera.fireEvent 'matrixUpdated'
     @update 0
     @world.addObject new Jax.Model
       castShadow: false
       mesh: new Jax.Mesh.Plane
-        size: 8
+        size: 32
         material: new Jax.Material.Surface
           intensity:
             ambient: 1
@@ -41,7 +45,8 @@
           color:
             ambient: '#0f0'
             diffuse: '#ccc'
-            specular: '#fff'
+            specular: '#555'
+          shininess: 24
       position: [0, 0, 0]
       direction: [0, 1, 0]


copy hidden files

copy hidden files

--- src/crystal/generate.coffee
+++ src/crystal/generate.coffee
@@ -251,7 +251,8 @@
 			if !fs.existsSync copy_dir
 				throw new Error "Directory (#{copy_dir}) does not exist in copy"

-			code_files = readdir copy_dir
+			code_files = readdir copy_dir, (x) ->
+				return true
 			for code_file in code_files
 				copy_filename = "#{output.path}/#{copy_dest}#{code_file}"
 				code_dir = copy_filename.substring 0, copy_filename.lastIndexOf('/')+1
@@ -311,6 +312,7 @@

 			# get content from output
 			template = generator.template
+
 			if template
 				template = template.replace /([  |\t]+)?>>>[a-z_]*<<<\n?/ig, (injector) ->
 					injector_tabs = injector.match /^[\s]+>/g


fix al-select

fix al-select

--- src/directive/select.coffee
+++ src/directive/select.coffee
@@ -134,8 +134,6 @@
             return

         mapper = select.mapper
-        select.change()
-
         id = null
         cd.watch key, (item) ->
             if id
@@ -143,11 +141,13 @@
                     mapper.release id
                     id = mapper.acquire item
                     element.value = id
+                    select.change()
                 else
                     mapper.replace id, item
             else
                 id = mapper.acquire item
                 element.value = id
+                select.change()
             return

         cd.watch '$destroy', ->


clean up remote-updater so it can be stopped and restarted easily

--- scripts/gh-book/remote-updater.coffee
+++ scripts/gh-book/remote-updater.coffee
@@ -14,6 +14,11 @@
     # This is updated every time the Remote Updater fetches
     lastSeenSha: null

+    initialize: () ->
+      # If the session config changes then clear the lastSeenSha because it may point to a different repo
+      session.on 'change', () =>
+        @lastSeenSha = null
+
     start: () ->
       # Get the current repo and last commit
       # Periodically check the commits list
@@ -25,24 +30,29 @@

       # Return a promise that is resolved once a start hash has been recorded
       # Note: This promise may still fail
-      return @pollUpdates()
+      @_runningPromise = @pollUpdates()
+      return @_runningPromise

-    stop: () -> @keepUpdating = false
+    stop: () ->
+      promise = @_runningPromise or (new $.Deferred()).resolve(@)
+
+      return promise.always () =>
+        @keepUpdating = false
+        @lastSeenSha = null

     # Returns a promise that is resolved when `@lastSeenSha` has been set.
     pollUpdates: () ->
       return if not @keepUpdating

-      return allContent.load().then () =>
+      throw new Error('BUG! remoteUpdater seems to be running twice. did you change repos?') if @_runningPromise
+      @_runningPromise = allContent.load().then () =>
         branch = session.getBranch()

         return branch.getCommits().then (commits) =>
           lastUpdatedSha = @lastSeenSha

           lastSeenSha = commits[0].sha
-
-          setTimeout (() => @pollUpdates()), UPDATE_TIMEOUT

           if not lastUpdatedSha
             @lastSeenSha = lastSeenSha
@@ -68,7 +78,7 @@
                   filePath = file.filename
                   model = allContent.get(filePath)

-                  if model
+                  if model?._loading
                     # TODO: Just invalidate the model by clearing `isLoaded`
                     return model.reload().then () =>
                       attributes =
@@ -80,3 +90,8 @@
                 return onceAll(promises)

             return onceAll(commitsPromises).then () => @lastSeenSha = lastSeenSha
+
+      return @_runningPromise.then () =>
+        @_runningPromise = false
+        setTimeout (() => @pollUpdates()), UPDATE_TIMEOUT if @keepUpdating
+


Account for error being null

Account for error being null

--- services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -96,7 +96,7 @@
 		EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
 			if error == "project_has_to_many_files"
 				res.status(400).json(req.i18n.translate("project_has_to_many_files"))
-			else if error.message == 'invalid element name'
+			else if error?.message == 'invalid element name'
 				res.status(400).json(req.i18n.translate('invalid_file_name'))
 			else if error?
 				res.status(500).json(req.i18n.translate('generic_something_went_wrong'))


Always keep doc_id update to date in track changes

--- services/web/public/coffee/track-changes/TrackChangesManager.coffee
+++ services/web/public/coffee/track-changes/TrackChangesManager.coffee
@@ -40,8 +40,8 @@

 		bindToFileTreeEvents: () ->
 			@ide.fileTreeManager.on "open:doc", (doc_id) =>
+				@doc_id = doc_id
 				if @enabled
-					@doc_id = doc_id
 					@updateDiff()

 		AB_BUCKETS: ["control", "one-week", "pop-up"]


Allow to have several identifiers for operation matching

--- server/lib/link_bank_operation.coffee
+++ server/lib/link_bank_operation.coffee
@@ -15,7 +15,10 @@
     constructor: (options) ->
         @log = options.log
         @model = options.model
-        @identifier = options.identifier.toLowerCase()
+        if typeof(options.identifier) is 'string'
+            @identifier = [options.identifier.toLowerCase()]
+        else
+            @identifier = options.identifier.map((id) -> id.toLowerCase())
         @amountDelta = options.amountDelta or 0.001
         @dateDelta = options.dateDelta or 15
         @minDateDelta = options.minDateDelta or @dateDelta
@@ -59,11 +62,16 @@
             opAmount = Math.abs operation.amount
             amountDelta = Math.abs opAmount - amount

-            if operation.title.toLowerCase().indexOf(@identifier) >= 0 and \
-            amountDelta <= @amountDelta and \
-            amountDelta <= minAmountDelta
-                operationToLink = operation
-                minAmountDelta = amountDelta
+            # Select the operation to link based on the minimal amount
+            # difference to the expected one and if the label matches one
+            # of the possible labels (identifier)
+            for identifier in @identifier
+                if operation.title.toLowerCase().indexOf(identifier) >= 0 and \
+                amountDelta <= @amountDelta and \
+                amountDelta <= minAmountDelta
+                    operationToLink = operation
+                    minAmountDelta = amountDelta
+                    break

         if not operationToLink?
             callback()


Parentheses are required to identify nodes in patterns

--- tests.coffee
+++ tests.coffee
@@ -62,7 +62,7 @@
   node = cursor.fetch()[0]
   __nodeCRC__ test, node.n, ["#{funcName}SyncReactiveTests"], {foo: "#{funcName}SyncReactiveTests"}

-  db[funcName] "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}
+  db[funcName] "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}

   node = cursor.fetch()[0]
   __nodeCRC__ test, node.n, ["#{funcName}SyncReactiveTests"], {foo: "#{funcName}SyncReactiveTests", newProp: 'rrrreactive!'}
@@ -567,7 +567,7 @@
         node = res[0].fetch()[0]
         __nodeCRC__ test, node.n, ['TransactionsCommitReactiveAsync'], {foo: 'TCRA'}

-        db.querySync "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}
+        db.querySync "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}

         node = res[0].fetch()[0]
         __nodeCRC__ test, node.n, ["TransactionsCommitReactiveAsync"], {foo: "TCRA", newProp: 'rrrreactive!'}
@@ -769,7 +769,7 @@
       node = res.fetch()[0]
       __nodeCRC__ test, node.n, ['BatchTestReactive'], {BatchTestReactive: true}

-      db.querySync "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}
+      db.querySync "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrreactive!'", {id: node.n.id}

       node = res.fetch()[0]
       __nodeCRC__ test, node.n, ["BatchTestReactive"], {BatchTestReactive: true, newProp: 'rrrreactive!'}
@@ -791,13 +791,13 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {}

   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -811,13 +811,13 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {testNodes: true}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {testNodes: true}

   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -831,21 +831,21 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {testNodes: true}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {testNodes: true}

   node.properties.set 'newProp', 'newPropValue'
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {testNodes: true, newProp: 'newPropValue'}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {testNodes: true, newProp: 'newPropValue'}

   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -859,7 +859,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {testNodes2: 'true'}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {testNodes2: 'true'}

   node.properties.set {newProp2: 'newPropValue2'}
@@ -867,13 +867,13 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {testNodes2: 'true', newProp2: 'newPropValue2'}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {testNodes2: 'true', newProp2: 'newPropValue2'}

   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -889,7 +889,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], initProps

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], initProps

   node.properties.update newProps
@@ -897,13 +897,13 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], newProps

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], newProps

   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -920,7 +920,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], initProps

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], initProps

   node.properties.update newProps
@@ -928,7 +928,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], newProps

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], newProps

   test.isTrue EJSON.equals node.properties.get(), newProps
@@ -936,7 +936,7 @@
   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -950,7 +950,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {one: 1}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {one: 1}

   node.properties.set {three: 3, four: 4}
@@ -958,7 +958,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {one: 1, three: 3, four: 4}

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {one: 1, three: 3, four: 4}

   test.isTrue EJSON.equals node.properties.get(), {one: 1, three: 3, four: 4}
@@ -966,7 +966,7 @@
   test.equal node.delete(), undefined
   test.equal node._node, undefined
   test.equal node.get(), undefined
-  test.equal db.queryOne("MATCH n WHERE id(n) = {id} RETURN n", {id: _id}) , undefined
+  test.equal db.queryOne("MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}) , undefined

 ###
 @test
@@ -983,7 +983,7 @@
   node.properties.delete 'one'
   test.isUndefined node.get().one

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}

   test.isUndefined _node.n.one

@@ -1004,7 +1004,7 @@
   node.properties.delete 'one'
   test.isUndefined node.get().one

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}

   test.isUndefined _node.n.one

@@ -1026,7 +1026,7 @@
   test.isUndefined node.get().one, '[one] removed from instance'
   test.isUndefined node.get().two, '[two] removed from instance'

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}

   test.isUndefined _node.n.one, '[one] removed from db'
   test.isUndefined _node.n.two, '[two] removed from db'
@@ -1051,7 +1051,7 @@
   test.isUndefined node.get().three
   test.isUndefined node.get().four

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}

   test.equal _node.n.one, 1
   test.equal _node.n.two, 2
@@ -1076,7 +1076,7 @@
   test.isUndefined node.get().one, '[one] removed from instance'
   test.isUndefined node.get().two, '[two] removed from instance'

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}

   test.isUndefined _node.n.one, '[one] removed from db'
   test.isUndefined _node.n.two, '[two] removed from db'
@@ -1106,7 +1106,7 @@
   _id = node.get().id
   test.equal node.property('three', 3).property('three'), 3

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {one: 1, two: 2, three: 3}
   test.equal node.delete(), undefined
   test.equal node.get(), undefined
@@ -1123,7 +1123,7 @@
   _id = node.get().id
   test.equal node.property('two', 3).property('two'), 3

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: _id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: _id}
   __nodeCRC__ test, _node.n, [], {one: 1, two: 3}
   test.equal node.delete(), undefined
   test.equal node.get(), undefined
@@ -1197,7 +1197,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {test: true}

-  db.querySync "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: _node.data[0][0].metadata.id}
+  db.querySync "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: _node.data[0][0].metadata.id}
   __nodeCRC__ test, node.get(), [], {test: true, newProp: 'rrrrreactive!'}
   test.equal node.delete(), undefined
   test.equal node.get(), undefined
@@ -1220,7 +1220,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), [], {test: true}

-  db.querySync "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: _node.data[0][0].metadata.id}
+  db.querySync "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: _node.data[0][0].metadata.id}
   __nodeCRC__ test, node.get(), [], {test: true, newProp: 'rrrrreactive!'}
   test.equal node.delete(), undefined
   test.equal node.get(), undefined
@@ -1317,7 +1317,7 @@
   __nodesInstanceCRC__ test, node
   __nodeCRC__ test, node.get(), ["NodesTestsReactive"], {testingReactive: 'some data'}

-  db.querySync "MATCH n WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: node.get().id}
+  db.querySync "MATCH (n) WHERE id(n) = {id} SET n.newProp = 'rrrrreactive!'", {id: node.get().id}

   __nodeCRC__ test, node.get(), ["NodesTestsReactive"], {testingReactive: 'some data', newProp: 'rrrrreactive!'}

@@ -1333,7 +1333,7 @@
   node = db.nodes().labels.set('MyLabel')
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel"], {}
   __nodeCRC__ test, _node.n, ["MyLabel"], {}

@@ -1348,7 +1348,7 @@
   node = db.nodes().labels.set(['MyLabel', 'MyLabel2'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel", "MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel", "MyLabel2"], {}

@@ -1363,7 +1363,7 @@
   node = db.nodes().labels.set(['MyLabel', 'MyLabel2']).labels.set('MyLabel3').labels.set(['MyLabel4', 'MyLabel5']).labels.set('MyLabel6')
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel", "MyLabel2", "MyLabel3", "MyLabel4", "MyLabel5", "MyLabel6"], {}
   __nodeCRC__ test, _node.n, ["MyLabel", "MyLabel2", "MyLabel3", "MyLabel4", "MyLabel5", "MyLabel6"], {}

@@ -1378,7 +1378,7 @@
   node = db.nodes().labels.set(['MyLabel', 'MyLabel2']).labels.set('MyLabel').labels.set(['MyLabel2', 'MyLabel5']).labels.set('MyLabel5')
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel", "MyLabel2", "MyLabel5"], {}
   __nodeCRC__ test, _node.n, ["MyLabel", "MyLabel2", "MyLabel5"], {}

@@ -1393,7 +1393,7 @@
   node = db.nodes().labels.set(['My Label', '']).labels.set('').labels.set('Label').labels.set(['', ''])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["My Label", "Label"], {}
   __nodeCRC__ test, _node.n, ["My Label", "Label"], {}

@@ -1408,13 +1408,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

   node.labels.replace(["MyLabel3"])

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel3"], {}

@@ -1429,13 +1429,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2', 'MyLabel1']).labels.set('MyLabel1')
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2",], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

   node.labels.set('MyLabel1').labels.replace(["MyLabel3", "MyLabel3"]).labels.set("MyLabel3")

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel3"], {}

@@ -1450,13 +1450,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2', '']).labels.set('').labels.replace(["", ""])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2",], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

   node.labels.replace(["", "MyLabel3"])

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel3"], {}

@@ -1471,13 +1471,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2",], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

   node.labels.delete('MyLabel1')

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel2"], {}

@@ -1492,13 +1492,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

   node.labels.delete('MyLabel5')

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2"], {}

@@ -1513,13 +1513,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2', 'MyLabel3'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2", "MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2", "MyLabel3"], {}

   node.labels.delete(['MyLabel1', 'MyLabel3'])

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel2"], {}
   __nodeCRC__ test, _node.n, ["MyLabel2"], {}

@@ -1534,13 +1534,13 @@
   node = db.nodes().labels.set(['MyLabel1', 'MyLabel2', 'MyLabel3'])
   __nodesInstanceCRC__ test, node

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2", "MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2", "MyLabel3"], {}

   node.labels.delete(['MyLabel5', 'MyLabel6'])

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2", "MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2", "MyLabel3"], {}

@@ -1565,7 +1565,7 @@
   node = db.nodes().label(['MyLabel1', 'MyLabel2', 'MyLabel3'])
   test.equal node.label(), ["MyLabel1", "MyLabel2", "MyLabel3"]

-  _node = db.queryOne "MATCH n WHERE id(n) = {id} RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} RETURN n", {id: node.get().id}
   __nodeCRC__ test, node.get(), ["MyLabel1", "MyLabel2", "MyLabel3"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1", "MyLabel2", "MyLabel3"], {}

@@ -1579,7 +1579,7 @@
 Tinytest.add 'Neo4jNode - label - ([String]).label() [SET / GET] [REACTIVE]', (test) ->
   node = db.nodes(null, true)
   test.equal node.label(), []
-  _node = db.queryOne "MATCH n WHERE id(n) = {id} SET n:MyLabel1 RETURN n", {id: node.get().id}
+  _node = db.queryOne "MATCH (n) WHERE id(n) = {id} SET n:MyLabel1 RETURN n", {id: node.get().id}
   test.equal node.label(), ["MyLabel1"]
   __nodeCRC__ test, node.get(), ["MyLabel1"], {}
   __nodeCRC__ test, _node.n, ["MyLabel1"], {}


delete blog.js.coffee

delete blog.js.coffee

--- app/assets/javascripts/blog.js.coffee
+++ app/assets/javascripts/blog.js.coffee
@@ -1,25 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
-
-$(document).ready ->
-  window.user_like = (blogId) ->
-    $.ajax
-      url: "/blogs/user_like"
-      data:
-        blog_id: blogId
-      dataType: 'json'
-      success: (data) ->
-        if data.type == 1
-          $("#like_icon").attr("class", "glyphicon glyphicon-heart")
-          updateLike(data.count)
-        else
-          $("#like_icon").attr("class", "glyphicon glyphicon-heart-empty")
-          updateLike(data.count)
-      error: () ->
-        alert("ajax请求不成功，请联系开发者")
-
-  window.updateLike = (count) ->
-    str = ''
-    str = '' + count
-    $("#count").html(str)


whoops

whoops

--- src/main.coffee
+++ src/main.coffee
@@ -60,11 +60,6 @@
     ret = client.getselfinfo()
     success = (userInfo) ->
       fs = require('fs')
-
-      #data = JSON.stringify(userInfo, null, '  ')
-      #fs.writeFile "getselfinfo_response.json", data, ->
-      #  console.log "DONEEEEEEEEEEEE"
-
       @model.self.username = userInfo.self_entity.properties.display_name
       @refresh()
     failure = (error) ->


Fix typo

Fix typo

--- src/main.coffee
+++ src/main.coffee
@@ -3,8 +3,9 @@
 async      = require('async')
 {inspect}  = require('util')
 Connection = require('./xmpp/connection')
-mpp        = require("node-xmpp")
+xmpp       = require('node-xmpp')
 NS         = require('./xmpp/ns')
+
 # Config
 config = require('jsconfig')
 version = require('./version')


make .coffee more like coffeescript

make .coffee more like coffeescript

--- app/assets/javascripts/file.js.coffee
+++ app/assets/javascripts/file.js.coffee
@@ -14,11 +14,11 @@
   update_current_path_and_title '/'

   window.error_msgs = [
-    "Fail to open this folder.",                # -1
-    "Fail to move this file to trash.",         # -2
-    "Fail to complete the undo request.",       # -3
-    "Fail to rename this file.",                # -4
-    "Fail to create new directory/file.",       # -5
+    "Fail to open this folder."                # -1
+    "Fail to move this file to trash."         # -2
+    "Fail to complete the undo request."       # -3
+    "Fail to rename this file."                # -4
+    "Fail to create new directory/file."       # -5
   ]

   update_footer = (number_of_files, number_of_dirs) ->
@@ -62,11 +62,10 @@
           dir_count += 1
         else
           type = "not_dir"
-        anchor = $('', {
-          class: 'file '+type,
-          href: '/open'+path+encodeURIComponent(b.name),
+        anchor = $ '',
+          class: 'file '+type
+          href: '/open'+path+encodeURIComponent(b.name)
           html: ''+b.name+''
-        })
         anchor.data('path', path+b.name)
         column_element.append($('
').append(anchor))
         file_count += 1
@@ -75,7 +74,9 @@
           anchor.addClass('active')
           if anchor.position().top > column_height - 20 # scroll to visible area
               should_scroll_to = anchor.position().top - column_height / 2
-      column_element.slimScroll({ height: window.column_height, scrollTo: should_scroll_to })
+      column_element.slimScroll
+        height: window.column_height
+        scrollTo: should_scroll_to
       update_footer file_count, dir_count
     .error -> # error getting ls json
       update_footer -1
@@ -99,18 +100,18 @@
       e.stopPropagation()
       hide_context_menu()
       $(this).nextAll('li').remove()
-    .selectable({
+    .selectable
       filter: 'li'
-      , selecting: (event, ui) ->
+      selecting: (event, ui) ->
         $(ui.selecting).find('a.file').addClass('active')
-      , unselecting: (event, ui) ->
+      unselecting: (event, ui) ->
         $(ui.unselecting).find('a.file').removeClass('active')
-      , start: (event, ui) ->
+      start: (event, ui) ->
         hide_context_menu()
         # pressing command or ctrl key to select multiple items
         if event.metaKey == false and event.ctrlKey == false
           $(this).find('a.file').removeClass('active')
-      , stop: (event, ui) ->
+      stop: (event, ui) ->
         column = $(this)
         selected = column.find('.ui-selectee.ui-selected')
         # clicking on one item
@@ -130,14 +131,12 @@
               show_renames()
           else
             window.last_clicked_list_item = { selected: selected, timeStamp: event.timeStamp }
-    })

   make_file_list_uploadable = (element) ->
-    dropzone = new Dropzone(element[0], {
-      url: "/upload" + element.data('ls-path'),
-      clickable: false,
+    dropzone = new Dropzone element[0],
+      url: "/upload" + element.data('ls-path')
+      clickable: false
       previewTemplate: '

'
-    })
     dropzone.on "sending", (file, xhr, formData) ->
       xhr.setRequestHeader 'X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')
     dropzone.on "success", ->
@@ -202,19 +201,19 @@
           post_path = window.routes.make_directory_path.replace('/:path', path + '/' + val)
         else
           post_path = window.routes.touch_file_path.replace('/:path', path + '/' + val)
-        $.post(post_path, {
+        $.post post_path,
           _method: 'post'
-        }).success (d) ->
+        .success (d) ->
           window.available_undos.unshift d
           load_file_list input.closest('ul.column')
         .error ->
           input.addClass('renaming')
           update_footer -5
       else
-        $.post(window.routes.rename_files_path.replace('/:path', path), {
-          _method: 'put',
+        $.post window.routes.rename_files_path.replace('/:path', path),
+          _method: 'put'
           to: val
-        }).success (d) ->
+        .success (d) ->
           window.available_undos.unshift d
           load_file_list input.closest('ul.column')
         .error ->
@@ -282,14 +281,14 @@
     if x + $('#context_menu').outerWidth() > $(window).width()
       x = $(window).width() - $('#context_menu').outerWidth()
     y = e.pageY - 10
-    $('#context_menu').show().css({
-      height: $('#menu').outerHeight() + 20,
-      top: e.pageY - 10,
-      left: x,
+    $('#context_menu').show().css
+      height: $('#menu').outerHeight() + 20
+      top: e.pageY - 10
+      left: x
       'z-index': 20
-    })
     if (y + $('#context_menu').height() > $(window).height())
-      $('#context_menu').css({ top: $(window).height() - $('#context_menu').height() })
+      $('#context_menu').css
+        top: $(window).height() - $('#context_menu').height()
     menu_items_before_clicked[type]()

   hide_context_menu = ->
@@ -314,9 +313,11 @@
         files = null
       else
         files = { files: files }
-      $.post(window.routes.remove_files_path.replace('/:path', path), $.extend({
-        _method: 'delete'
-      }, files)).success (d) ->
+      $.post window.routes.remove_files_path.replace('/:path', path),
+        $.extend
+          _method: 'delete'
+        , files
+      .success (d) ->
         window.available_undos.unshift d
         load_file_list first.closest('ul.column')
       .error ->
@@ -343,10 +344,9 @@
     mkfile true
   mkfile = (is_dir) ->
     current_list = window.selected_items.eq(0).find('ul.column')
-    anchor = $('', {
-      class: 'file '+(if is_dir==true then 'dir' else 'not_dir')+' active new',
+    anchor = $ '',
+      class: 'file '+(if is_dir==true then 'dir' else 'not_dir')+' active new'
       html: ''
-    })
     anchor.data('path', current_list.data('ls-path'))
     current_list.append($('
').append(anchor))

@@ -356,8 +356,20 @@
       load_file_list window.selected_items.eq(0).find('ul.column')

   menu_items =
-    for_files: ['Open--','Move to Trash--','Get Info','Rename'],
-    for_lists: ['Undo--','Make Directory','Create Empty File--','Refresh--','Get Info','Settings'],
+    for_files: [
+      'Open--'
+      'Move to Trash--'
+      'Get Info'
+      'Rename'
+    ]
+    for_lists: [
+      'Undo--'
+      'Make Directory'
+      'Create Empty File--'
+      'Refresh--'
+      'Get Info'
+      'Settings'
+    ]

   menu_items_before_clicked =
     for_files: ->
@@ -370,8 +382,20 @@
         $('#menu li:first').removeClass('disabled').find('a').text(text).attr('title', text)

   menu_items_clicked =
-    for_files: [open, move_to_trash, null, null],
-    for_lists: [undo, mkdir, mkfile, refresh, null, null],
+    for_files: [
+      open
+      move_to_trash
+      null
+      null
+    ]
+    for_lists: [
+      undo
+      mkdir
+      mkfile
+      refresh
+      null
+      null
+    ]

   menu_items_after_clicked =
     for_files: (index) ->
@@ -407,12 +431,11 @@
   resize_columns = ->
     parent = $('#columns_container')
     window.column_height = parent.height()
-    $('#columns').css({
-      top: parent.position().top,
-      left: parent.position().left,
-      width: parent.width(),
+    $('#columns').css
+      top: parent.position().top
+      left: parent.position().left
+      width: parent.width()
       height: window.column_height
-    })
     $('#columns ul.columns > li').height(window.column_height)
     $('#columns ul.column').slimScroll({ height: 'auto' })
     $('#columns ul.columns').slimScrollHorizontal({ width: 'auto' })


Fix #4868: Now can edit external user perms on private projects

--- app/coffee/modules/admin/roles.coffee
+++ app/coffee/modules/admin/roles.coffee
@@ -47,6 +47,7 @@
         "$q",
         "$tgLocation",
         "$tgNavUrls",
+        "$tgModel",
         "tgAppMetaService",
         "$translate",
         "tgErrorHandlingService",
@@ -54,7 +55,7 @@
     ]

     constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls,
-                  @appMetaService, @translate, @errorHandlingService, @projectService) ->
+                  @model, @appMetaService, @translate, @errorHandlingService, @projectService) ->
         bindMethods(@)

         @scope.sectionName = "ADMIN.MENU.PERMISSIONS"
@@ -72,6 +73,7 @@

     loadProject: ->
         project = @projectService.project.toJS()
+        project = @model.make_model("projects", project)

         if not project.i_am_admin
             @errorHandlingService.permissionDenied()


fix issue #4427 change default votes order

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -482,7 +482,10 @@
             currentOrder = $ctrl.getOrderBy()
             newOrder = target.data("fieldname")

-            finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
+            if newOrder == 'total_voters'
+                finalOrder = if currentOrder == newOrder then newOrder else "-#{newOrder}"
+            else
+                finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder

             $scope.$apply ->
                 $ctrl.replaceFilter("order_by", finalOrder)


Client: Activity: Deleted unnecesary isDeleting state and updated updateMessage method to show delete message modal

--- client/activity/lib/components/chatlistitem/index.coffee
+++ client/activity/lib/components/chatlistitem/index.coffee
@@ -69,10 +69,10 @@

   updateMessage: ->

-    return @deletePostButtonHandler()  if @state.isDeleting
+    value = @refs.editInput.getValue().trim()

-    value = @refs.editInput.getValue().trim()
-    return @setState isDeleting: yes  unless value
+    unless value
+      return @refs.MessageItemMenu.showDeletePostPromptModal()

     messageId = @props.message.get '_id'

@@ -81,8 +81,6 @@

   cancelEdit: ->
-
-    return @closeDeletePostModal()  if @state.isDeleting

     messageId = @props.message.get '_id'
     ActivityFlux.actions.message.unsetMessageEditMode messageId, @props.channelId, yes
@@ -143,7 +141,7 @@

     return null  unless @props.showItemMenu

-    <MessageItemMenu message={@props.message}/>
+    <MessageItemMenu ref='MessageItemMenu' message={@props.message}/>

   renderEmbedBox: ->


IMPROVED: reject invalid named size and location layouts

--- source/Art/Engine/Core/Element.coffee
+++ source/Art/Engine/Core/Element.coffee
@@ -208,7 +208,8 @@
   namedSizeLayouts = object namedSizeLayoutsRaw, (v) -> pointLayout v

   @getSizePointLayout: getSizePointLayout = (v, previousValue) ->
-    if (isString(v) && l = namedSizeLayouts[v])
+    if isString v
+      throw new Error "invalid named size layout: #{v}" unless l = namedSizeLayouts[v]
       if previousValue
         pointLayout namedSizeLayoutsRaw[v], previousValue
       else l
@@ -224,7 +225,8 @@
   namedLocationLayouts = object namedLocationLayoutsRaw, (v) -> pointLayout v

   @getLocationPointLayout: getLocationPointLayout = (v, previousValue) ->
-    if ((isNumber(v) || isString(v)) && l = namedLocationLayouts[v])
+    if v == 0 || isString v
+      throw new Error "invalid named location layout: #{v}" unless l = namedLocationLayouts[v]
       if previousValue
         pointLayout namedLocationLayoutsRaw[v], previousValue
       else l


IMPROVED - if scale is infinite, it screws up everything, so detect that, reset it to point1, and log.warn with details.

--- source/Art/Engine/Core/Element.coffee
+++ source/Art/Engine/Core/Element.coffee
@@ -1027,6 +1027,7 @@
     {
       _currentPadding, _currentSize, _axis, _scale, _angle, _elementToParentMatrix
     } = state = @getState pending
+    originalScale = _scale
     _scale = withScale if withScale?

     if isFunction _scale
@@ -1039,6 +1040,16 @@
       _scale = _scale parentSize, _currentSize

     _scale = point _scale
+    if _scale.isInfinite
+      log.warn "Art.Engine.Element":
+        message: "Scale is infinite. Replacing with point(1)."
+        element: @inspectedName
+        scale:
+          input:      originalScale
+          output:     _scale
+        parentSize:   parentSize
+        currentSize:  _currentSize
+      _scale = point1

     {left, top} = _currentPadding
     size  = _currentSize


Test window.Store persistence methods

Test window.Store persistence methods

--- spec/localstorage_store_spec.coffee
+++ spec/localstorage_store_spec.coffee
@@ -2,17 +2,53 @@

 describe 'window.Store', ->
   describe 'creation', ->
-    xit 'takes a name in its constructor', ->
+    it 'takes a name in its constructor', ->
+      store = new window.Store 'convenience store'
+      expect(store.name).toBe 'convenience store'
+
+  describe 'persistence', ->
+    {store, model} = {}
+    beforeEach ->
+      window.localStorage.clear()
+      window.localStorage.setItem 'cats', '3'
+      window.localStorage.setItem 'cats3', '{"id": "3", "color": "burgundy"}'
+      store = new window.Store 'cats'
+
+    it 'fetches records by id with find', ->
+      expect(store.find(id: 3)).toEqual id: '3', color: 'burgundy'
+
+    it 'fetches all records with findAll', ->
+      expect(store.findAll()).toEqual [id: '3', color: 'burgundy']
+
+    it 'clears out its records', ->
+      store.clear()
+      expect(window.localStorage.getItem 'cats').toBe ''
+      expect(window.localStorage.getItem 'cats3').toBeUndefined()
+
+    it 'creates records', ->
+      model = id: 2, color: 'blue'
+      store.create model
+      expect(window.localStorage.getItem 'cats').toBe '3,2'
+      expect(JSON.parse(window.localStorage.getItem 'cats2')).toEqual id: 2, color: 'blue'
+
+    it 'generates an id when creating records with no id', ->
+      window.localStorage.clear()
+      store = new window.Store 'cats'
+      model = color: 'calico', idAttribute: 'id', set: (attribute, value) -> this[attribute] = value
+      store.create model
+      expect(model.id).not.toBeUndefined()
+      expect(window.localStorage.getItem('cats')).toBe model.id
+
+    it 'updates records', ->
+      store.update id: 3, color: 'green'
+      expect(JSON.parse(window.localStorage.getItem 'cats3')).toEqual id: 3, color: 'green'
+
+    it 'destroys records', ->
+      store.destroy id: 3
+      expect(window.localStorage.getItem 'cats').toBe ''
+      expect(window.localStorage.getItem 'cats3').toBeUndefined()

   describe 'offline', ->
     xit 'marks records dirty and clean', ->
     xit 'marks records destroyed', ->
     xit 'reports if it hasDirtyOrDestroyed records', ->
-
-  describe 'persistence', ->
-    xit 'fetches records by id with find', ->
-    xit 'fetches all records with findAll', ->
-    xit 'clears out its records', ->
-    xit 'creates records', ->
-    xit 'updates records', ->
-    xit 'destroys records', ->


up:fragment:offline: Test that event.retry() takes an object with overriding render options

--- spec/unpoly/fragment_spec.js.coffee
+++ spec/unpoly/fragment_spec.js.coffee
@@ -938,6 +938,31 @@
               expect(listener.calls.count()).toBe(1)

               expect('.target').toHaveText('new text')
+
+          it 'allows up:fragment:offline listeners to override render options when retrying', asyncSpec (next) ->
+            listener = jasmine.createSpy('onOffline callback')
+
+            fixture('.target', text: 'old text')
+            fixture('.override-target', text: 'old text')
+
+            up.render('.target', url: '/other-path', onOffline: listener)
+
+            next ->
+              expect(jasmine.Ajax.requests.count()).toBe(1)
+
+              expect(listener).not.toHaveBeenCalled()
+
+              jasmine.lastRequest().responseError()
+
+            next ->
+              expect('.target').toHaveText('old text')
+              expect(listener.calls.count()).toBe(1)
+              event = listener.calls.argsFor(0)[0]
+              event.retry(target: '.override-target')
+
+            next ->
+              expect(jasmine.Ajax.requests.count()).toBe(2)
+              expect(jasmine.lastRequest().requestHeaders['X-Up-Target']).toEqual('.override-target')

           it 'does not call an { onFinished } handler', asyncSpec (next) ->
             listener = jasmine.createSpy('onFinished callback')