StatusBar: fix 'Find and replace' not passing 'replaceMode' flag

--- client/ide/views/statusbar/statusbarmenu.coffee
+++ client/ide/views/statusbar/statusbarmenu.coffee
@@ -17,7 +17,7 @@
         @destroy()

   getMenuItems: ->
-    isApple = utils.isApple()
+    isNavigatorApple = utils.isNavigatorApple()

     @syntaxSelector = new IDE.SyntaxSelectorMenuItem

@@ -29,7 +29,7 @@
       @syntaxSelector,
       [ 'Preview'            , 'Ctrl+Alt+P'  , 'previewFile' ],
       [ 'Find...'            , 'Meta+F'      , 'showFindReplaceView' ],
-      [ 'Find and replace...', 'Meta+Shift+F', 'showFindReplaceView' ],
+      [ 'Find and replace...', 'Meta+Shift+F', 'showFindReplaceViewWithReplaceMode' ],
       [ 'Find in files...'   , 'Ctrl+Alt+F'  , 'showContentSearch' ],
       [ 'Jump to file...'    , 'Ctrl+Alt+O'  , 'showFileFinder' ],
       [ 'Go to line...'      , 'Meta+G'      , 'goToLine' ]
@@ -54,7 +54,7 @@
       if item is @syntaxSelector
         items.customView = @syntaxSelector
         continue
-      if isApple
+      if isNavigatorApple
         for k, v of macKeysUnicodeMapping
           item[1] = item[1].replace(k, v)
         # by tradition osx is not displaying + for shortcuts


Removed global test variable from accordion

--- app/assets/javascripts/lib/components/accordion.js.coffee
+++ app/assets/javascripts/lib/components/accordion.js.coffee
@@ -40,7 +40,6 @@

     constructor : (args) ->
       config = $.extend config, args
-      window.x = config
       @parent = $(config.parent)
       @panels = @parent.find('.js-accordion-panel')


Checking allowing a refetch option, adding promises

--- app/assets/javascripts/sprangular/services/catalog.coffee
+++ app/assets/javascripts/sprangular/services/catalog.coffee
@@ -13,12 +13,17 @@
     productsByTaxon: (path, page=1) ->
       @getPaged(page, taxon: path)

-    taxonomies: ->
+    taxonomies: (refetch=true)->
+      q = $q.defer()
       _service = @
-      $http.get("/api/taxonomies")
-        .then (response) ->
-          _service.fetched.taxonomies = response.data
-          response.data
+      if !refetch and _service.fetched.taxonomies isnt null
+        q.resolve _service.fetched.taxonomies
+      else
+        $http.get("/api/taxonomies")
+          .then (response) ->
+            _service.fetched.taxonomies = response.data
+            q.resolve response.data
+      return q.promise

     taxonsByName: (name) ->
       $http.get("spree/api/taxonomies?q[name_eq]=#{name}")
@@ -35,14 +40,14 @@

     getPaged: (page=1, params={}) ->
       $http.get("/api/products", ignoreLoadingIndicator: params.ignoreLoadingIndicator, params: {per_page: @pageSize, page: page, "q[name_or_description_cont]": params.search, "q[taxons_permalink_eq]": params.taxon})
-           .then (response) ->
-             data = response.data
-             list = Sprangular.extend(data.products || [], Sprangular.Product)
-             list.isLastPage = (data.count < service.pageSize) || (page == data.pages)
-             list.totalCount = data.total_count
-             list.totalPages = data.pages
-             list.page = data.current_page
-             Status.cacheProducts(list)
-             list
+        .then (response) ->
+          data = response.data
+          list = Sprangular.extend(data.products || [], Sprangular.Product)
+          list.isLastPage = (data.count < service.pageSize) || (page == data.pages)
+          list.totalCount = data.total_count
+          list.totalPages = data.pages
+          list.page = data.current_page
+          Status.cacheProducts(list)
+          list

   service


Updates catalog service to match new AMS response

--- app/assets/javascripts/sprangular/services/catalog.coffee
+++ app/assets/javascripts/sprangular/services/catalog.coffee
@@ -13,7 +13,7 @@
     taxonomies: ->
       $http.get("/api/taxonomies", {cache: true})
         .then (response) ->
-          response.data
+          response.data.taxonomies

     taxonsByName: (name) ->
       @taxonomies().then (response)->


Removes empty params from query string

Removes empty params from query string

--- app/assets/javascripts/sprangular/services/catalog.coffee
+++ app/assets/javascripts/sprangular/services/catalog.coffee
@@ -49,6 +49,9 @@
         price_max:    params.price?.max
         sorting:      params.sorting

+      for key, value of queryParams
+        delete queryParams[key] unless value
+
       $http.get("/api/products?#{$.param(queryParams)}", ignoreLoadingIndicator: params.ignoreLoadingIndicator)
         .then (response) ->
           data = response.data


Use textarea for description

Use textarea for description

--- client/skr/screens/time-tracking/EditEntry.cjsx
+++ client/skr/screens/time-tracking/EditEntry.cjsx
@@ -37,11 +37,10 @@
             </BS.Row><BS.Row>
                 <LC.DateTime {...props} step={15} name="end_at"/>
             </BS.Row><BS.Row>
-                <LC.Input sm={12}
+                <LC.TextArea sm={12}
+                    selectOnFocus editOnly
                     model={@entry}
-                    ref='textarea'
                     onEnter={@onSave}
-                    selectOnFocus editOnly
                     name="description"
                     type="textarea"
                     label='Description'


High or low key priority

High or low key priority

--- src/scripts/Keyboard.coffee
+++ src/scripts/Keyboard.coffee
@@ -14,7 +14,16 @@
         $(document).keydown -> self.keydown(event)
         $(document).keyup   -> self.keyup(event)
         @scanner = [0,0,0,0,0,0,0,0,0,0,0,0]
+        @lowPriority = false

+
+    setKeyPriority: (priority) ->
+        if (priority == "high")
+            @lowPriority = false
+        else if (priority == "low")
+            @lowPriority = true
+        else
+            throw "Invalid key priority"

     # Handler for a keydown event
     keydown: (event) ->
@@ -34,9 +43,15 @@

     # Gets the active note that should be playing, low note priority.
     getActiveNote: ->
-        for scan, i in @scanner
-            if scan == 1
-                return i
+        if (@lowPriority)
+            for scan, i in @scanner
+                if scan == 1
+                    return i
+        else
+            for scan, i in @scanner.slice(0).reverse()
+                if scan == 1
+                    return 12-i
+
         return -1

     # Converts a note from a keyboard (US layout) to a note in the octave


Issue #216 Websocket vulnerability

Issue #216 Websocket vulnerability

--- src/server/websocket.coffee
+++ src/server/websocket.coffee
@@ -11,7 +11,13 @@
   wrapper.send = (response) ->
     conn.send JSON.stringify response if wrapper.ready()
   wrapper.ready = -> conn.readyState is 1
-  conn.on 'message', (data) -> wrapper.emit 'message', JSON.parse data
+  conn.on 'message', (data) ->
+    try
+      parsed = JSON.parse data
+      wrapper.emit 'message', parsed
+    catch error
+      console.log "Received data parsing error #{error}"
+
   wrapper.headers = conn.upgradeReq.headers
   # TODO - I don't think this is the right way to get the address
   wrapper.address = conn._socket.server._connectionKey?


Throw errors instead of just logging them

--- src/sharedcollection.coffee
+++ src/sharedcollection.coffee
@@ -141,7 +141,7 @@

     if not @_syncDoc.snapshot[@collectionId][model.id]
-      log "ERROR: snapshot has no this model #{ model.id }"
+      throw new Error  "ERROR: snapshot has no model with id #{ model.id }"

     if operations.length isnt 0
       @_syncDoc.submitOp operations, @handleError(model)
@@ -211,8 +211,7 @@

     model = @get modelId
     if not model
-      log "ERROR: Remote asked to remove non existing model #{ modelId }"
-      return
+      throw new Error "Remote asked to remove non existing model #{ modelId }"

     log "RECEIVE REMOVE #{ model.id }: #{ JSON.stringify modelId }"

@@ -231,8 +230,7 @@

     model = @get modelId
     if not model
-      log "ERROR: Remote asked to update non existing model: #{ model.id } #{ modelId }"
-      return
+      throw new Error "Remote asked to update non existing model: #{ model.id } #{ modelId }"

     log "RECEIVE CHANGE #{ model.id }: #{ attrName }: #{ attrValue }"


label on click state change, slight changes to  the query stuff

--- components/inputs/KDInputViewWithPreview.coffee
+++ components/inputs/KDInputViewWithPreview.coffee
@@ -31,6 +31,10 @@
       title         : "Preview"
       tooltip       :
         title       : "Show/hide the Text Preview Box"
+      click         :=>
+        @previewOnOffSwitch.setValue not @previewOnOffSwitch.getValue()
+        no
+
     @previewOnOffSwitch = new KDOnOffSwitch
       label         : @previewOnOffLabel
       size          : "tiny"
@@ -107,12 +111,14 @@
           modal = new KDModalView
             title       : "Please enter your content here."
             cssClass    : "modal-fullscreen"
-            width       : $(window).width()-110
-            height      : $(window).height()-120
+            width       : window.innerWidth-100
+            height      : window.innerHeight-100
+            # width       : $(window).width()-100
+            # height      : $(window).height()-100
             view        : @textContainer
             position:
-              top       : 30
-              left      : 50
+              top       : 35
+              left      : 35
             overlay     : yes
             buttons     :
               Cancel    :
@@ -128,25 +134,30 @@
                   @generatePreview()
                   modal.destroy()

-          modal.$(".kdmodal-content").height modal.$(".kdmodal-inner").height()-modal.$(".kdmodal-buttons").height()-modal.$(".kdmodal-title").height() # minus the margin, border pixels too..
-          modal.$(".fullscreen-data").height modal.$(".kdmodal-content").height()-30-23+10
-          modal.$(".input_preview").height   modal.$(".kdmodal-content").height()-0-21+10
-          modal.$(".input_preview").css maxHeight:  modal.$(".kdmodal-content").height()-0-21+10
-          modal.$(".input_preview div.preview_content").css maxHeight:  modal.$(".kdmodal-content").height()-0-21
-          contentWidth = modal.$(".kdmodal-content").width()-40
-          halfWidth  = contentWidth / 2
-
-          @text.on "PreviewHidden", =>
-            modal.$(".fullscreen-data").width contentWidth #-(modal.$("div.preview_switch").width()+20)-10
-            # modal.$(".input_preview").width (modal.$("div.preview_switch").width()+20)
-
-          @text.on "PreviewShown", =>
+
+          @utils.wait =>
+
+            modal.$(".kdmodal-content").height modal.$(".kdmodal-inner").height()-modal.$(".kdmodal-buttons").height()-modal.$(".kdmodal-title").height()+10 # minus the margin, border pixels too..
+            modal.$(".fullscreen-data").height modal.$(".kdmodal-content").height()-30-23+10
+            modal.$(".input_preview").height   modal.$(".kdmodal-content").height()-0-21+10
+            modal.$(".input_preview").css maxHeight:  modal.$(".kdmodal-content").height()-0-21+10
+            modal.$(".input_preview div.preview_content").css maxHeight:  modal.$(".kdmodal-content").height()-0-21
+
+            contentWidth = modal.$(".kdmodal-content").width()-40
+            halfWidth  = contentWidth / 2
+
+            @text.on "PreviewHidden", =>
+              modal.$(".fullscreen-data").width contentWidth #-(modal.$("div.preview_switch").width()+20)-10
+              # modal.$(".input_preview").width (modal.$("div.preview_switch").width()+20)
+
+            @text.on "PreviewShown", =>
+              modal.$(".fullscreen-data").width contentWidth-halfWidth-5
+              # modal.$(".input_preview").width halfWidth-5
+
             modal.$(".fullscreen-data").width contentWidth-halfWidth-5
-            # modal.$(".input_preview").width halfWidth-5
-
-          modal.$(".fullscreen-data").width contentWidth-halfWidth-5
-          modal.$(".input_preview").width halfWidth-5
-
+            modal.$(".input_preview").width halfWidth-5
+
+      # @addSubView @fullScreenBtn
       @previewOnOffContainer.addSubView @fullScreenBtn

     @previewOnOffContainer.addSubView @previewOnOffLabel


Make number of columns to display be responsive

--- app/assets/javascripts/components/list_detail/main.js.coffee
+++ app/assets/javascripts/components/list_detail/main.js.coffee
@@ -41,6 +41,7 @@
   _renderBody: ->
     React.createElement View.ListDetail.ItemsContainer,
       items: @state.items
+      numberOfColumns: @_getNumberOfColumnsToDisplay()

   _renderNotification: ->
     React.createElement View.Notification,
@@ -79,3 +80,12 @@

   _handleClickAddItemBtn: (event) ->
     ReactAction.ListDetailAction.showItemFormFor({})
+
+  _getNumberOfColumnsToDisplay: ->
+    width = $(window).width()
+    if width <= 414
+      1
+    else if width < 600
+      2
+    else
+      3


titleにSPを追加

titleにSPを追加

--- app/assets/javascripts/components/welcomes/top_panel.js.cjsx
+++ app/assets/javascripts/components/welcomes/top_panel.js.cjsx
@@ -3,7 +3,7 @@
     super
     @state =
       current_user: UserStore.get()
-      title: '☆12参考表(地力表)支援サイト'
+      title: 'SP☆12参考表(地力表)支援サイト'
       one: '掲示板で議論された地力表を自分のランプで反映'
       two: '地力値を用いて楽曲の難しさを数値化'
       three: 'ランプの遷移をグラフで可視化'
@@ -20,7 +20,7 @@

   mobileContent: ->
     @setState
-      title: '☆12参考表'
+      title: 'SP☆12参考表'
       one: '掲示板で議論された地力表を反映'
       two: '地力値を用いた楽曲難易度'
       three: 'ランプ遷移をグラフで可視化'


Simplified isEditable logic

Simplified isEditable logic

--- app/assets/javascripts/controllers/task_controller.js.coffee
+++ app/assets/javascripts/controllers/task_controller.js.coffee
@@ -2,10 +2,10 @@
   needs: ['application']
   onClose: 'closeOverlay'
   isLoading: false
-  isPaperEditDisabled: Ember.computed.not('paper.editable')
   isMetadata: Ember.computed.alias('isMetadataTask')
-  isMetadataAndPaperEditDisabled: Ember.computed.and('isPaperEditDisabled', 'isMetadata')
-  isUserEditable: Ember.computed.not('isMetadataAndPaperEditDisabled')
+  isUserEditable: Ember.computed 'paper.editable', 'isMetadata', ->
+    @get('paper.editable') || !@get('isMetadata')
+
   isCurrentUserAdmin: Ember.computed.alias 'controllers.application.currentUser.admin'
   isEditable: Ember.computed.or('isUserEditable', 'isCurrentUserAdmin')


OX-336 save waypoints only if you press Space

--- app/assets/javascripts/oxalis/controller/controller3d.coffee
+++ app/assets/javascripts/oxalis/controller/controller3d.coffee
@@ -62,24 +62,7 @@
   render : (forceUpdate, event) ->

     matrix = @cam.getMatrix()
-
     @model.binary.arbitraryPing(matrix)
-
-    unless @lastNodeMatrix?
-      @lastNodeMatrix = matrix
-
-    lastNodeMatrix = @lastNodeMatrix
-
-    vector = [
-      lastNodeMatrix[12] - matrix[12]
-      lastNodeMatrix[13] - matrix[13]
-      lastNodeMatrix[14] - matrix[14]
-    ]
-    vectorLength = V3.length(vector)
-
-    if vectorLength > 10
-      @setWaypoint()
-      @lastNodeMatrix = matrix

     @model.route.rendered()

@@ -125,7 +108,9 @@
       "s" : => @cam.move [0, @model.user.moveValue, 0]
       "a" : => @cam.move [-@model.user.moveValue, 0, 0]
       "d" : => @cam.move [@model.user.moveValue, 0, 0]
-      "space" : =>  @cam.move [0, 0, @model.user.moveValue]
+      "space" : =>
+        @cam.move [0, 0, @model.user.moveValue]
+        @moved()
       "shift + space" : => @cam.move [0, 0, -@model.user.moveValue]

       #Rotate in distance
@@ -237,4 +222,26 @@
   setActiveNode : (nodeId, centered, mergeTree) ->

     @model.route.setActiveNode(nodeId, mergeTree)
-    @cam.setPos @model.route.getActiveNodePos()  +    @cam.setPos @model.route.getActiveNodePos()
+
+
+  moved : =>
+
+    matrix = @cam.getMatrix()
+
+    unless @lastNodeMatrix?
+      @lastNodeMatrix = matrix
+      @setWaypoint()
+
+    lastNodeMatrix = @lastNodeMatrix
+
+    vector = [
+      lastNodeMatrix[12] - matrix[12]
+      lastNodeMatrix[13] - matrix[13]
+      lastNodeMatrix[14] - matrix[14]
+    ]
+    vectorLength = V3.length(vector)
+
+    if vectorLength > 10
+      @setWaypoint()
+      @lastNodeMatrix = matrix


Hide "Composed of:" element completely if there are no containing channels.

--- core/app/backbone/views/subchannels_view.coffee
+++ core/app/backbone/views/subchannels_view.coffee
@@ -7,7 +7,8 @@
     "click #more-button": "toggleMore"

   initialize: ->
-    @collection.bind "remove", @renderCollection, this
+    @collection.bind "add remove reset", @updateVisibility, @
+    @collection.bind "remove", @renderCollection, @

   toggleMore: ->
     button = @$("#more-button .label")
@@ -17,3 +18,6 @@
   appendHtml: (collectView, itemView) ->
     @$(".contained-channel-description").show()
     @$("ul").prepend itemView.el
+
+  updateVisibility: ->
+    @$el.toggleClass 'hide', @collection.length <= 0


update MainController to use Socket initially

--- app/templates/app/src/Main/controllers/MainController.coffee
+++ app/templates/app/src/Main/controllers/MainController.coffee
@@ -2,17 +2,23 @@

 class MainController

-    constructor: (@$scope, @$location, StorageService)-> @greet()
+    constructor: (@$scope, @$location, Config, StorageService, Socket)->
+        @greet()
+
+        stream = Socket.open '/comments', (stream)->
+            # close the stream when moving out of the main page.
+            $scope.$on '$locationChangeStart', -> Socket.close stream

     greet: -> @$scope.message = 'Welcome!'

 # inject controller dependencies
-MainController.$inject = ['$scope', '$location', 'StorageService']
+MainController.$inject = ['$scope', '$location', 'Config', 'StorageService', 'Socket']

 module.exports = (app)->

     require('src/Config/Config')(app)
     require('src/Storage/StorageService')(app)
+    require('src/Socket/Socket')(app)

     # bind controller to app
     app.controller 'MainController', MainController


Notify the adapter when a visit fails or completes

--- src/turbolinks/visit.coffee
+++ src/turbolinks/visit.coffee
@@ -6,16 +6,29 @@
       @previousLocation = Turbolinks.Location.box(previousLocation)
       @location = Turbolinks.Location.box(location)
       @adapter = @controller.adapter
+      @state = "initialized"

   start: ->
-    unless @started
-      @started = true
+    if @state is "initialized"
+      @state = "started"
       @adapter.visitStarted(this)

   cancel: ->
-    if @started and not @canceled
+    if @state is "started"
       @request?.cancel()
-      @canceled = true
+      @state = "canceled"
+
+  complete: ->
+    if @state is "started"
+      @state = "completed"
+      @adapter.visitCompleted?(this)
+      @resolve()
+
+  fail: ->
+    if @state is "started"
+      @state = "failed"
+      @adapter.visitFailed?(this)
+      @reject()

   then: ->
     @promise.then(arguments...)
@@ -43,18 +56,19 @@
       if @snapshotRestored = @controller.restoreSnapshotForLocationWithAction(@location, @action)
         @adapter.visitSnapshotRestored?(this)
       unless @shouldIssueRequest()
-        @resolve()
+        @complete()

   loadResponse: ->
     if @response?
       @saveSnapshot()
       if @request.failed
         @controller.loadErrorResponse(@response)
-        @reject()
+        @adapter.visitResponseLoaded?(this)
+        @fail()
       else
         @controller.loadResponse(@response)
-        @resolve()
-      @adapter.visitResponseLoaded?(this)
+        @adapter.visitResponseLoaded?(this)
+        @complete()

   # HTTP Request delegate

@@ -84,4 +98,4 @@
   saveSnapshot: ->
     unless @snapshotSaved
       @controller.saveSnapshotForLocation(@previousLocation)
-      @snapshotSaved = true+      @snapshotSaved = true


robust error handling (for a tiny part of tests)

--- test/integration/api.coffee
+++ test/integration/api.coffee
@@ -305,10 +305,12 @@
               , cb

           async.series functionArr, (err, res) ->
+            if err
+              callback err, res
             if res.length is 1
               callback err, res[0][0], res[0][1]
             else
-              callback res
+              callback err, res

         before (done) ->
           createDatasets 1,


peillis/helpapp#3455 more tests and a warning

--- test/test_controller.coffee
+++ test/test_controller.coffee
@@ -125,6 +125,28 @@
       controller = new doofinder.Controller client_mock, [widget_mock]
       controller.search 'silla'

+    ### IN DOC STATES THAT 'EVERY SEARCH PARAM' CAN BE USED IN SEARCH METHOD
+      # 'filters' are search params, but are not allowe
+      # either fix the doc or fix the source code
+    it 'extra search params can be added in search', (done)->
+      client_mock.search = (query, params, cb)->
+        params.should.have.keys 'query_counter', 'query', 'filters', 'page', 'rpp'
+        params.rpp.should.be.equal 23
+        params.filters.color.should.eql ['Rojo']
+        done()
+      controller = new doofinder.Controller client_mock, [widget_mock]
+      controller.search 'silla', rpp: 23, filters: color: ['Rojo']
+    ###
+
+    it 'extra search params can be added in constructor', (done)->
+      client_mock.search = (query, params)->
+        params.should.have.keys 'query_counter', 'query', 'filters', 'page', 'rpp'
+        params.rpp.should.be.equal 11
+        params.filters.color.should.eql ['Azul']
+        done()
+      controller = new doofinder.Controller client_mock, [widget_mock], {rpp: 11, filters: {color: ['Azul']}}
+      controller.search 'silla'
+
     it 'when adding terms filters, filters params change', (done) ->
       # we need to make a search first in order to "refresh" it with
       # applied filters
@@ -165,3 +187,24 @@
         done()
       # go!
       controller.refresh()
+
+    it 'when adding range filters, filters params change', (done) ->
+      controller = new doofinder.Controller client_mock, [widget_mock]
+      controller.search 'silla'
+      client_mock.search = (query, params, cb) ->
+        params.filters.price.should.be.eql gt: 10
+        done()
+      controller.addFilter 'price', gt: 10
+      controller.refresh()
+
+    it 'when removing range filters, filters params change', (done) ->
+      controller = new doofinder.Controller client_mock, [widget_mock]
+      controller.search 'silla'
+      controller.addFilter 'price', gt: 10
+      controller.refresh()
+      controller.removeFilter 'price', lte: 5
+      client_mock.search = (query, params, cb) ->
+        console.log params
+        params.filters.should.be.empty
+        done()
+      controller.refresh()


Re-ordering font tests, add ONLINE flag

Re-ordering font tests, add ONLINE flag

--- test/tests/UtilsTest.coffee
+++ test/tests/UtilsTest.coffee
@@ -272,7 +272,50 @@
 			# it "should return the right size with height constraint", ->
 			# 	Utils.textSize(text, style, {height: 100}).should.eql(width: 168, height: 100)

-	describe.skip "loadWebFontConfig", ->
+	describe "loadWebFontConfig", ->
+
+		it "should return a promise", (done) ->
+			promise = Utils.loadWebFontConfig
+				custom:
+					families: ["Test"]
+				timeout: 5
+			promise.catch ->
+				done()
+			return
+
+		it "should reject the promise if the font can't be loaded", (done) ->
+			promise = Utils.loadWebFontConfig
+				custom:
+					families: ["Test2"]
+				timeout: 5
+			promise.catch (error) ->
+				error.message.should.equal "Test2 failed to load"
+				done()
+			return
+
+		it "should return false if the font has already failed loading", (done) ->
+			promise = Utils.loadWebFontConfig
+				custom:
+					families: ["Test3"]
+				timeout: 5
+			promise.catch ->
+				result = Utils.loadWebFontConfig
+					custom:
+						families: ["Test3"]
+				result.should.equal false
+				done()
+			return
+
+		it "should support loading webfonts with WebFontConfig syntax", (done) ->
+			result = Utils.loadWebFontConfig
+				custom:
+					families: ['Random font']
+				timeout: 5
+			result.catch (e) ->
+				e.message.should.equal "Random font failed to load"
+				done()
+			return
+
 		describe "Real font loading tests", ->
 			before ->
 				# We skip the this test on CI, because I can't get the WebFont loading to work... :'(
@@ -298,6 +341,12 @@
 					done()
 				return

+		describe "Online font loading tests", ->
+			before ->
+				# We skip the this test on CI, because I can't get the WebFont loading to work... :'(
+				if mocha.env.CI or not mocha.env.ONLINE
+					@skip()
+
 			it "should not interfere with each other", (done) ->
 				Utils.loadWebFont("Raleway")
 				roboto = Utils.loadWebFontConfig
@@ -308,7 +357,7 @@
 				return

 			it "should cache loading of google fonts", (done) ->
-				@skip()
+				@timeout(5000)
 				droid = Utils.loadWebFontConfig
 					google:
 						families: ['Droid Sans']
@@ -330,58 +379,17 @@
 					done()
 				return

-
-		it "should return a promise", (done) ->
-			promise = Utils.loadWebFontConfig
-				custom:
-					families: ["Test"]
-				timeout: 100
-			promise.catch ->
-				done()
-			return
-
-		it "should reject the promise if the font can't be loaded", (done) ->
-			promise = Utils.loadWebFontConfig
-				custom:
-					families: ["Test2"]
-				timeout: 100
-			promise.catch (error) ->
-				error.message.should.equal "Test2 failed to load"
-				done()
-			return
-
-		it "should return false if the font has already failed loading", (done) ->
-			promise = Utils.loadWebFontConfig
-				custom:
-					families: ["Test3"]
-				timeout: 100
-			promise.catch ->
-				result = Utils.loadWebFontConfig
-					custom:
-						families: ["Test3"]
-				result.should.equal false
-				done()
-			return
-
-		it "should support loading webfonts with WebFontConfig syntax", (done) ->
-			result = Utils.loadWebFontConfig
-				custom:
-					families: ['Random font']
-				timeout: 100
-			result.catch (e) ->
-				e.message.should.equal "Random font failed to load"
-				done()
-			return
-
-	describe.skip "isFontFamilyLoaded", ->
-		it "should not reset the result if it is loaded successfully", (done) ->
-			if mocha.env.CI
+	describe "isFontFamilyLoaded", ->
+		before: ->
+			if mocha.env.CI or not mocha.env.ONLINE
 				@skip()
 				return
+
+		it "should not reset the result if it is loaded successfully", (done) ->
 			p = Utils.loadWebFontConfig
 				custom:
 					families: ["Georgia"]
-				timeout: 100
+				timeout: 5
 			p.then ->
 				Utils.loadWebFont("Georgia")
 				promise = Utils.isFontFamilyLoaded("Georgia", 100)
@@ -393,7 +401,7 @@
 			p = Utils.loadWebFontConfig
 				custom:
 					families: ["Test4"]
-				timeout: 100
+				timeout: 5
 			p.catch ->
 				Utils.loadWebFont("Test4")
 				promise = Utils.isFontFamilyLoaded("Test4", 100)
@@ -401,7 +409,12 @@
 				done()
 			return

-	describe.skip "loadWebFont", ->
+	describe "Online loadWebFont", ->
+		before: ->
+			if not mocha.env.ONLINE
+				@skip()
+				return
+
 		it "loads fonts at different weights" , ->
 			raleway = Utils.loadWebFont("Raleway")
 			raleway200 = Utils.loadWebFont("Raleway", 200)


Test SessionDetail.

Test SessionDetail.

--- test/unit/routerSpec.coffee
+++ test/unit/routerSpec.coffee
@@ -3,12 +3,12 @@
     describe 'Unit Testing Router', ->
       # The mock Router service module.
       Router = null
-
+
       # The mock Angular $http service provider.
       $httpBackend = null
-
+
       $rootScope = null
-
+
       # The mock objects.
       mock =
         subject:
@@ -37,58 +37,42 @@
           ]
         session_detail:
           scan:
+            name: 'scan'
             intensity:
               intensities: [2.4]
           registrations: [
+            name: 'reg_test'
             intensity:
               intensities: [3.1]
           ]

       beforeEach ->
-        # Fake the service module.
+        # Fake the router service module.
         angular.mock.module('qiprofile.router')

-        inject ['Router', '$httpBackend', '$rootScope', (_Router_, _$httpBackend_, _$rootScope_) ->
-          Router = _Router_
-          $httpBackend = _$httpBackend_
-          $rootScope = _$rootScope_
-
-          # The mock subjects http call.
-          url = encodeURI('/api/subjects?where=' +
-                          '{"project":"QIN_Test","collection":"Breast","number":1}')
-          $httpBackend.whenGET(url).respond(JSON.stringify(_items: [mock.subject]))
-
-          # The mock subject-detail http call.
-          url = encodeURI('/api/subject-detail/a')
-          $httpBackend.whenGET(url).respond(JSON.stringify(mock.subject_detail))
-
-          # The mock session-detail http call.
-          url = encodeURI('/api/session-detail/b')
-          $httpBackend.whenGET(url).respond(JSON.stringify(mock.session_detail))
+        inject ['Router', '$httpBackend', '$rootScope',
+          (_Router_, _$httpBackend_, _$rootScope_) ->
+            Router = _Router_
+            $httpBackend = _$httpBackend_
+            $rootScope = _$rootScope_
+
+            # The mock subjects http call.
+            url = encodeURI('/api/subjects?where=' +
+                            '{"project":"QIN_Test","collection":"Breast","number":1}')
+            $httpBackend.whenGET(url).respond(JSON.stringify(_items: [mock.subject]))
+
+            # The mock subject-detail http call.
+            url = encodeURI('/api/subject-detail/a')
+            $httpBackend.whenGET(url).respond(JSON.stringify(mock.subject_detail))
+
+            # The mock session-detail http call.
+            url = encodeURI('/api/session-detail/b')
+            $httpBackend.whenGET(url).respond(JSON.stringify(mock.session_detail))
         ]

       afterEach ->
         $httpBackend.verifyNoOutstandingExpectation()
         $httpBackend.verifyNoOutstandingRequest()
-
-      describe 'Subject', ->
-        it 'should fetch the subject when the fetch flag is set', ->
-          # "Fetch" the subject.
-          params = _.extend({fetch: true}, _.omit(mock.subject, 'detail'))
-          Router.getSubject(params).then (actual) ->
-              expect(_.pairs(actual), "Subject with fetch incorrect")
-                .to.deep.have.members(_.pairs(mock.subject))
-          # Dispatch the backend request.
-          $httpBackend.flush()
-
-        it 'should not fetch the subject when the fetch flag is not set', ->
-          # The router ignores the detail parameter.
-          params = _.omit(mock.subject, 'detail')
-          Router.getSubject(params).then (actual) ->
-            expect(_.pairs(actual), "Subject without fetch incorrect")
-              .to.deep.have.members(_.pairs(params))
-          # Resolve the pending promise.
-          $rootScope.$apply()

       describe 'Subject Detail', ->
         # Validates the resolved subject.
@@ -111,25 +95,94 @@
           expect(mdl.delta_k_trans, "Delta Ktrans is not #{ mock_delta_k_trans }")
             .to.be.closeTo(mock_delta_k_trans, 0.0000001)

-        it 'should fetch the detail with a subject detail property', ->
+        it 'should fetch the detail with a detail property', ->
           subject = _.clone(mock.subject)
-          Router.getSubjectDetail(subject, {}).then ->
-            validate(subject)
-          # Dispatch the backend request.
-          $httpBackend.flush()
-
-        it 'should fetch the detail with a detail query parameter', ->
-          subject = _.omit(mock.subject, 'detail')
-          Router.getSubjectDetail(subject, detail: mock.subject.detail).then ->
+          Router.getSubjectDetail(subject).then ->
             validate(subject)
           # Dispatch the backend request.
           $httpBackend.flush()

-        it 'should fetch the detail without a detail property or parameter', ->
+        it 'should fetch the detail without a detail property', ->
           subject = _.omit(mock.subject, 'detail')
-          Router.getSubjectDetail(subject, {}).then ->
+          Router.getSubjectDetail(subject).then ->
             validate(subject)
           # Dispatch the backend request.
           $httpBackend.flush()

-      # TODO - add Session tests.
+      describe 'Session Detail', ->
+        mock_session = null
+
+        # Validates the resolved session.
+        validate = (session) ->
+          expect(session.scan, "Session scan is missing").to.exist
+          expect(session.scan.intensity, "Session scan is incorrect")
+            .to.deep.eql(mock.session_detail.scan.intensity)
+          expect(session.registrations.length,
+               "Session registrations count is incorrect")
+            .to.equal(1)
+          reg = session.registrations[0]
+          mock_reg = mock.session_detail.registrations[0]
+          expect(reg.intensity, "Session registration is incorrect")
+            .to.deep.eql(mock_reg.intensity)
+
+        beforeEach ->
+          mock_session = mock.subject_detail.sessions[0]
+          # The session subject reference is set in the routes
+          # session resolution.
+          mock_session.subject = mock.subject
+
+        it 'should fetch the detail with a session detail property', ->
+          session = _.clone(mock_session)
+          Router.getSessionDetail(session).then ->
+            validate(session)
+          # Dispatch the backend request.
+          $httpBackend.flush()
+
+        it 'should fetch the detail without a detail property', ->
+          session = _.omit(mock_session, 'detail')
+          session.subject = _.clone(mock.subject)
+          Router.getSessionDetail(session).then ->
+            validate(session)
+          # Dispatch the backend request.
+          $httpBackend.flush()
+
+      describe 'Image Container', ->
+        mock_session = null
+
+        # Validates the resolved container.
+        validate = (image) ->
+          expect(session.scan, "Session scan is missing").to.exist
+          expect(session.scan.intensity, "Session scan is incorrect")
+            .to.deep.eql(mock.session_detail.scan.intensity)
+          expect(session.registrations.length,
+               "Session registrations count is incorrect")
+            .to.equal(1)
+          reg = session.registrations[0]
+          mock_reg = mock.session_detail.registrations[0]
+          expect(reg.intensity, "Session registration is incorrect")
+            .to.deep.eql(mock_reg.intensity)
+
+        beforeEach ->
+          mock_session = mock.subject_detail.sessions[0]
+          # The session subject reference is set in the routes
+          # session resolution.
+          mock_session.subject = mock.subject
+
+        it 'should find the scan container in a fetched session', ->
+          session = _.clone(mock_session)
+          session.scan = mock.session_detail.scan
+          actual = Router.getImageContainer(session, session.scan.name)
+          expect(actual, 'Image container missing').to.exist
+          expect(actual, 'Image container incorrect')
+            .to.equal(mock.session_detail.scan)
+
+        it 'should fetch a session if necessary to obtain the scan container', ->
+          session = _.omit(mock_session, 'detail')
+          session.subject = _.clone(mock.subject)
+          Router.getImageContainer(session, mock.session_detail.scan.name).then (container) ->
+            expect(container, 'Image container missing').to.exist
+            expect(container.intensity, 'Image container intensity incorrect').
+              to.eql(mock.session_detail.scan.intensity)
+
+          # Dispatch the backend request.
+          $httpBackend.flush()


Whitespace

Whitespace

--- framer/LayerDraggable.coffee
+++ framer/LayerDraggable.coffee
@@ -31,15 +31,15 @@

 """

-    ┌──────┐                   │
-    │      │
-    │      │  ───────────────▶ │ ◀────▶
-    │      │
-    └──────┘                   │
-
-    ════════  ═════════════════ ═══════
-
-      Drag         Momentum      Bounce
+	┌──────┐                   │
+	│      │
+	│      │  ───────────────▶ │ ◀────▶
+	│      │
+	└──────┘                   │
+
+	════════  ═════════════════ ═══════
+
+		Drag         Momentum      Bounce

 """


Game server refactor

Game server refactor

--- gameserver/gameserver.coffee
+++ gameserver/gameserver.coffee
@@ -4,43 +4,45 @@
 util = require('util')
 _ = require('underscore')

-raceMatches = (p1, p2) ->
-    return p2.race in p1.params.opp_races
-
-leagueMatches = (p1, p2) ->
-    return p2.league in p1.params.opp_leagues
-
-seriesMatches = (p1, p2) ->
-    return _.intersection(p1.params.series, p2.params.series)
-
-mapMatches = (p1, p2) ->
-    return _.intersection(p1.params.maps, p2.params.maps)
-
-blocklistOk = (p1, p2) ->
-    user = p2.name + '.' + p2.char_code
-    return not p1.params.blocked_users[user]
-
 class Player
     constructor: (socket, request) ->
         _.extend(this, request)
         @socket = socket
         @id = socket.id

-    matches: (p2) =>
-        p1 = this
-        if p1.region isnt p2.region
-            return false
-        if not raceMatches(p1, p2) or not raceMatches(p2, p1)
-            return false
-        if not leagueMatches(p1, p2) or not leagueMatches(p2, p1)
-            return false
-        if not blocklistOk(p1, p2) or not blocklistOk(p2, p1)
-            return false
-        series = seriesMatches(p1, p2)
+    matches: (other) =>
+        if not this.regionMatches(other)
+            return false
+        if not this.raceMatches(other) or not other.raceMatches(this)
+            return false
+        if not this.leagueMatches(other) or not other.leagueMatches(this)
+            return false
+        if not this.blocklistOk(other) or not other.blocklistOk(this)
+            return false
+        series = this.seriesMatches(other)
         return false if series.length is 0
-        maps = mapMatches(p1, p2)
+        maps = this.mapMatches(other)
         return false if maps.length is 0
         return {series: series, maps: maps}
+
+    regionMatches: (other) ->
+        return @region is other.region
+
+    raceMatches: (other) ->
+        return other.race in @params.opp_races
+
+    leagueMatches: (other) ->
+        return other.league in @params.opp_leagues
+
+    seriesMatches: (other) ->
+        return _.intersection(@params.series, other.params.series)
+
+    mapMatches: (other) ->
+        return _.intersection(@params.maps, other.params.maps)
+
+    blocklistOk: (other) ->
+        user = other.name + '.' + other.char_code
+        return not @params.blocked_users[user]

     toJSON: =>
         id: @id
@@ -53,36 +55,75 @@
         params: @params

 class Lobby
-    constructor: (player) ->
-        @players = [player]
+    constructor: (p1, p2, match) ->
+        @players = [p1, p2]
+        p1.socket.emit('playerJoined', p2)
+        p2.socket.emit('playerJoined', p1)
+        match.random_map = match.maps[Math.floor(
+            Math.random()*match.maps.length)]
+        p.socket.emit('lobbyFinished', match) for p in @players

     removePlayer: (id) =>
-        @players = _.reject(@players, (p) -> p.id is id)
-        for player in @players
-            player.socket.emit('playerLeft', id)
-
-    matches: (other_lobby) =>
-        return false if this.finished() or other_lobby.finished()
-        return @players[0].matches(other_lobby.players[0])
-
-    match: (other_lobby) =>
-        match = this.matches(other_lobby)
-        return unless match
-        @players.push(other_lobby.players[0])
-        @players[0].socket.emit('playerJoined', @players[1])
-        @players[1].socket.emit('playerJoined', @players[0])
-        if this.finished()
-            match.random_map = match.maps[Math.floor(
-                Math.random()*match.maps.length)]
-            p.socket.emit('lobbyFinished', match) for p in @players
-
-    finished: =>
-        return @players.length is 2
-
+        to_return = _.find(@players, (p) -> p.id isnt id)
+        to_return.socket.emit('playerLeft', id)
+        return to_return
+
     sendChat: (id, text) =>
-        to_players = _.reject(@players, (p) -> p.id is id)
-        for p in to_players
-            p.socket.emit('chatReceived', {id: id, text: text})
+        to_player = _.reject(@players, (p) -> p.id is id)[0]
+        to_player.socket.emit('chatReceived', {id: id, text: text})
+
+class LobbyManagerGlobal
+    constructor: ->
+        @pending_players = []
+        @lobbies_by_id = {}
+
+    isPresent: (id) =>
+        return id of @lobbies_by_id or id in (p.id for p in @pending_players)
+
+    addPlayer: (player) =>
+        return if this.isPresent(player.id)
+        @pending_players.push(player)
+        players = {}
+        players[player.id] = player
+        return players
+
+    removePlayer: (id) =>
+        if id of @lobbies_by_id
+            lobby = @lobbies_by_id[id]
+            other_player = lobby.removePlayer(id)
+            @pending_players.push(other_player)
+            delete @lobbies_by_id[id]
+            delete @lobbies_by_id[other_player.id]
+        else
+            @pending_players = _.reject(@pending_players, (p) -> p.id is id)
+
+    matchmake: =>
+        new_pending = []
+        while @pending_players.length > 0
+            player = @pending_players.shift()
+            continue if player.id of @lobbies_by_id
+            matched = false
+            for other_player in @pending_players
+                match = player.matches(other_player)
+                if match
+                    lobby = new Lobby(player, other_player, match)
+                    @lobbies_by_id[player.id] = lobby
+                    @lobbies_by_id[other_player.id] = lobby
+                    matched = true
+                    break
+            new_pending.push(player) if not matched
+        @pending_players = new_pending
+
+    sendChat: (id, text) =>
+        return unless id of @lobbies_by_id
+        @lobbies_by_id[id].sendChat(id, text)
+LobbyManager = new LobbyManagerGlobal()
+setInterval(->
+    try
+        LobbyManager.matchmake()
+    catch err
+        console.log('Exception: ' + err)
+, 5000)

 class GlobalLobbyGlobal
     constructor: ->
@@ -118,74 +159,6 @@
             a.emit('playerLeft', id) for a in _.values(@anons)
 GlobalLobby = new GlobalLobbyGlobal()

-class LobbyManagerGlobal
-    constructor: ->
-        @pending_lobbies = []
-        @lobbies_by_id = {}
-
-    addPlayer: (player) =>
-        return if player.id of @lobbies_by_id
-        lobby = new Lobby(player)
-        @pending_lobbies.push(lobby)
-        @lobbies_by_id[player.id] = lobby
-        players = {}
-        players[player.id] = player
-        return players
-
-    removePlayer: (id) =>
-        return unless id of @lobbies_by_id
-        lobby = @lobbies_by_id[id]
-        if lobby.finished()
-            lobby.removePlayer(id)
-            @pending_lobbies.push(lobby)
-        else
-            @pending_lobbies = _.without(@pending_lobbies, lobby)
-        delete @lobbies_by_id[id]
-
-    matchmake: =>
-        new_pending = []
-        while @pending_lobbies.length > 0
-            lobby = @pending_lobbies.shift()
-            continue if lobby.finished()
-            merged = false
-            for other_lobby in @pending_lobbies
-                if other_lobby.match(lobby)
-                    for player in other_lobby.players
-                        @lobbies_by_id[player.id] = other_lobby
-                    merged = true
-                    break
-            new_pending.push(lobby) if not merged
-        @pending_lobbies = new_pending
-
-    sendChat: (id, text) =>
-        return unless id of @lobbies_by_id
-        @lobbies_by_id[id].sendChat(id, text)
-
-    getMatchingLobbies: (player) =>
-        fake_lobby = new Lobby(player)
-        return (l.players[0] for l in @pending_lobbies when l.matches(fake_lobby))
-
-    joinLobby: (player, cb) =>
-        if player.id of @lobbies_by_id
-            return cb('You are already in a lobby. Try reloading the page.')
-        new_lobby = new Lobby(player)
-        lobby_to_join = @lobbies_by_id[player.params.lobby_id]
-        if lobby_to_join? and lobby_to_join.matches(new_lobby)
-            cb()
-            lobby_to_join.match(new_lobby)
-            @lobbies_by_id[player.id] = lobby_to_join
-            @pending_lobbies = _.without(@pending_lobbies, lobby_to_join)
-        else
-            cb('Lobby unavailable, please try another.')
-
-LobbyManager = new LobbyManagerGlobal()
-setInterval(->
-    try
-        LobbyManager.matchmake()
-    catch err
-        console.log('Exception: ' + err)
-, 5000)
-
 class UserProfilesGlobal
     LEAGUES:
         'none': 'n'
@@ -297,36 +270,19 @@
                 cb(GlobalLobby.addPlayer(player))
             ))
     ))
-    socket.on('getUserProfile', (profile_url, cb) ->
+    socket.on('getUserProfile', handleErrors((profile_url, cb) ->
         UserProfiles.get(profile_url, cb)
-    )
-    socket.on('createLobby', (req, cb) ->
-        UserProfiles.get(req.profile_url, handleErrors((err, profile) ->
+    ))
+    socket.on('createLobby', handleErrors((req, cb) ->
+        UserProfiles.get(req.profile_url, (err, profile) ->
             return if err
             req.league = profile.league
             player = new Player(socket, req)
             players = LobbyManager.addPlayer(player)
             GlobalLobby.removeByID(player.id)
             cb(players)
-        ))
-    )
-    socket.on('listLobbies', (req, cb) ->
-        UserProfiles.get(req.profile_url, handleErrors((err, profile) ->
-            return if err
-            req.league = profile.league
-            player = new Player(socket, req)
-            lobbies = LobbyManager.getMatchingLobbies(player)
-            cb(lobbies)
-        ))
-    )
-    socket.on('joinLobby', (req, cb) ->
-        UserProfiles.get(req.profile_url, handleErrors((err, profile) ->
-            return if err
-            req.league = profile.league
-            player = new Player(socket, req)
-            LobbyManager.joinLobby(player, cb)
-        ))
-    )
+        )
+    ))
     socket.on('sendChat', handleErrors((text) ->
         if GlobalLobby.isPresent(socket.id)
             GlobalLobby.sendChat(socket.id, text)


Some form of working error ellipse code

Some form of working error ellipse code

--- attitude/display/assets/math.coffee
+++ attitude/display/assets/math.coffee
@@ -79,17 +79,16 @@
   # Get a single level of planar errors (or the
   # plane's nominal value) as a girdle
   n = opts.n or 100
+  upperHemisphere = opts.upperHemisphere or true
   sheet = opts.sheet or 'nominal'
   degrees = opts.degrees or false
   axes = identity unless axes?
   opts.traditionalLayout ?= true
-  upperHemisphere = opts.upperHemisphere or true

   step = 2*Math.PI/(n-1)
   angles = (i*step for i in [0...n])

-  v = axesCovariance.map Math.sqrt
-  s = (v[2]/i for i in v)
+  s = axesCovariance.map Math.sqrt
   axes = transpose(axes)

   sdot = (a,b)->
@@ -99,18 +98,20 @@
   c = if degrees then 180/Math.PI else 1

   c1 = 1
-  #if upperHemisphere
-    #c1 *= -1
-  # Flip upper and lower rings
-  #if axes[2][2] < 0
-  #  angles = angles.reverse()
+  if upperHemisphere
+    c1 *= -1

   stepFunc = (angle)->

-    #console.log Math.sin(angle), Math.cos(angle)
-    e = [Math.cos(angle)*s[0],
+    f = [Math.cos(angle)*s[0],
          Math.sin(angle)*s[1],
-         (Math.sin(angle)+Math.cos(angle))*s[2]*c1]
+         c1*s[2]]
+
+    e = [
+      -f[2]*Math.cos(angle)
+      -f[2]*Math.sin(angle)
+      norm([f[0],f[1]])
+    ]

     d = (sdot(e,i) for i in axes)
     r = norm(d)
@@ -128,7 +129,7 @@
       c*Math.atan2(y,x),
       c*Math.asin z/r]

-  angles.map(stepFunc)
+  return angles.map(stepFunc)

 combinedErrors = (sv, ax, opts={})->
   func = (type)->


create bottom scale, too

create bottom scale, too

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -15,9 +15,9 @@

     @dataLength = Math.min @data.x.length, @data.y.length

-    @data_orig = []
+    # @data_orig = []
     for xValue, idx in [@data.x...]
-      @data_orig[idx] = @data.x[idx]
+      # @data_orig[idx] = @data.x[idx]
       @data.x[idx] = xValue - @smallestX

     # normalize data
@@ -48,6 +48,7 @@
     for i in [0...@dataLength]
       if i % tickMinorInterval is 0 and i isnt 0
         tick_x = ((+i - xMin) / (xMax - xMin)) * @canvas.width
+        # top tickmarks
         @ctx.beginPath()
         @ctx.moveTo(tick_x,0)
         @ctx.lineTo(tick_x,tickMinorLength)
@@ -55,6 +56,14 @@
         @ctx.strokeStyle = tickColor
         @ctx.stroke()

+        # bottom tickmarks
+        @ctx.beginPath()
+        @ctx.moveTo(tick_x,@canvas.height)
+        @ctx.lineTo(tick_x,@canvas.height-tickMinorLength)
+        @ctx.lineWidth = tickWidth
+        @ctx.strokeStyle = tickColor
+        @ctx.stroke()
+
       if i % tickMajorInterval is 0 and i isnt 0
         tick_x = ((+i - xMin) / (xMax - xMin)) * @canvas.width
         @ctx.font = '10pt Arial'
@@ -62,10 +71,17 @@
         @ctx.fillStyle = textColor
         @ctx.fillText(i,tick_x,tickMajorLength+5)

+        tick_x = ((+i - xMin) / (xMax - xMin)) * @canvas.width
+        @ctx.font = '10pt Arial'
+        @ctx.textAlign = 'center'
+        @ctx.fillStyle = textColor
+        @ctx.fillText( i, tick_x, @canvas.height - tickMajorLength+5 )
+
     for i in [0...@dataLength]
       x = ((+@data.x[i] - xMin) / (xMax - xMin)) * @canvas.width
       # y = ((+@data.y[i] - @largestY) / (@smallestY - @largestY)) * @canvas.height
       y = +@data.y[i] * @canvas.height# - @canvas.height
+      y=-y+@canvas.height
       @ctx.fillStyle = "#fff"
       @ctx.fillRect(x,y,2,2)

@@ -83,7 +99,6 @@
     y_norm = []
     for y, idx in [@data.y...]
       y_norm[idx] =  ( parseFloat(y) - @smallestY ) / ( @largestY - @smallestY )
-
     # update max/min values (still needed?)
     @smallestX = Math.min @data.x...
     @smallestY = Math.min @data.y...


get measuring tool working again (and fancy zoom animations)

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -17,14 +17,13 @@
     @data_raw.x = @data.x.slice(0)
     @data_raw.y = @data.y.slice(0)

+    # apply normalization, outlier removal, etc.
+    @processLightcurve()

     # initialize zoom parameters
     @zoomRanges = [@largestX, 10, 2]
     @zoomLevel = 0
     @graphCenter = 5
-
-    # apply normalization, outlier removal, etc.
-    @processLightcurve()

   disableMarking: ->
     # console.log 'CANVAS GRAPH: disableMarking()'
@@ -56,33 +55,36 @@
     xClick = e.pageX - e.target.getBoundingClientRect().left - window.scrollX
     yClick = e.pageY - e.target.getBoundingClientRect().top - window.scrollY

-    # @plotPoints(@sliderValue, @sliderValue+@zoomRanges[@zoomLevel])
-
-    # if xClick < @leftPadding
-    #   # draw triangle
-    #   w = 10
-    #   s = 2*w*Math.tan(@leftPadding)
+    # @plotPoints()
+    # @zoomToCenter(@graphCenter)
+    console.log "PLOT POINTS [#{@sliderValue},#{@sliderValue+@zoomRanges[@zoomLevel]}]"
+    @plotPoints(@sliderValue, @sliderValue+@zoomRanges[@zoomLevel])
+
+    if xClick < @leftPadding
+      # draw triangle
+      w = 10
+      s = 2*w*Math.tan(@leftPadding)

-    #   @ctx.beginPath()
-    #   @ctx.moveTo(w,yClick)
-    #   @ctx.lineTo(0,yClick+s)
-    #   @ctx.lineTo(0,yClick-s)
-    #   @ctx.fillStyle = '#FC4542'
-    #   @ctx.fill()
-
-    #   @ctx.beginPath()
-    #   @ctx.moveTo(w,yClick)
-    #   @ctx.lineWidth = 1
-    #   @ctx.strokeStyle = 'rgba(252,69,66,0.9)'
-    #   @ctx.moveTo( 60, yClick )
-    #   @ctx.lineTo( @canvas.width, yClick )
-    #   @ctx.moveTo( 0, yClick )
-    #   @ctx.lineTo( 10, yClick )
-    #   @ctx.stroke()
-
-    #   @ctx.font = '10pt Arial'
-    #   @ctx.textAlign = 'left'
-    #   @ctx.fillText( @toDataYCoord((-yClick+@canvas.height)).toFixed(4), 15, yClick+5 ) # don't forget to flip y-axis values
+      @ctx.beginPath()
+      @ctx.moveTo(w,yClick)
+      @ctx.lineTo(0,yClick+s)
+      @ctx.lineTo(0,yClick-s)
+      @ctx.fillStyle = '#FC4542'
+      @ctx.fill()
+
+      @ctx.beginPath()
+      @ctx.moveTo(w,yClick)
+      @ctx.lineWidth = 1
+      @ctx.strokeStyle = 'rgba(252,69,66,0.9)'
+      @ctx.moveTo( 60, yClick )
+      @ctx.lineTo( @canvas.width, yClick )
+      @ctx.moveTo( 0, yClick )
+      @ctx.lineTo( 10, yClick )
+      @ctx.stroke()
+
+      @ctx.font = '10pt Arial'
+      @ctx.textAlign = 'left'
+      @ctx.fillText( @toDataYCoord((-yClick+@canvas.height)).toFixed(4), 15, yClick+5 ) # don't forget to flip y-axis values

   processLightcurve: (removeOutliers=true) ->

@@ -303,7 +305,6 @@
     ), 30
     return

-
   zoomToCenter: (center) ->
     console.log 'zoomToCenter, CENTER = ', center
     # classifier.el.find('#graph').addClass('is-zooming')
@@ -340,7 +341,6 @@
         @plotPoints(boundL,boundR)
         @rescaleMarks(boundL,boundR)
     ), 30
-

   zoomInTo: (wMin, wMax) ->
     classifier.el.find('#graph').addClass('is-zooming')


Fix area chart / tooltip defect

Fix area chart / tooltip defect

--- app/lib/layouts/area.coffee
+++ app/lib/layouts/area.coffee
@@ -73,42 +73,39 @@
         else scaleY = y.scale()

         area = d3.svg.area()
-        .x((d) ->  x.scale()(d.x))
-        .y0((d) ->  scaleY(d.y0 + d.y))
-        .y1((d) ->  scaleY(d.y0))
+          .x((d) ->  x.scale()(d.x))
+          .y0((d) ->  scaleY(d.y0 + d.y))
+          .y1((d) ->  scaleY(d.y0))

         layers = layers
-        .data(layoutNew, (d) -> d.key)
+          .data(layoutNew, (d) -> d.key)

         if layoutOld.length is 0
           layers.enter()
-          .append('path').attr('class', 'area')
-          .style('fill', (d, i) ->
-            color.scale()(d.key)).style('opacity', 0)
+            .append('path').attr('class', 'area')
+            .style('fill', (d, i) -> color.scale()(d.key)).style('opacity', 0)
+            .style('pointer-events', 'none')
         else
           layers.enter()
-          #.append('g').attr('class', 'layer')
-          .append('path').attr('class', 'area')
-          .attr('d', (d) ->
-            if addedPred[d.key] then getLayerByKey(addedPred[d.key], layoutOld).path else area(d.layer.map((p) ->
-              {x: p.x, y: 0, y0: 0}))
-          )
+            .append('path').attr('class', 'area')
+            .attr('d', (d) ->
+              if addedPred[d.key] then getLayerByKey(addedPred[d.key], layoutOld).path else area(d.layer.map((p) ->
+                {x: p.x, y: 0, y0: 0}))
+            )
           .style('fill', (d, i) ->
             color.scale()(d.key))
           .style('pointer-events', 'none')

         layers.transition().duration(options.duration)
-        .attr('d', (d) ->
-          null
-          area(d.layer))
-        .style('opacity', 1)
+          .attr('d', (d) -> area(d.layer))
+          .style('opacity', 1)

         layers.exit().transition().duration(options.duration)
           .attr('d', (d) ->
             succ = deletedSucc[d.key]
             if succ then area(getLayerByKey(succ, layoutNew).layer.map((p) -> {x: p.x, y: 0, y0: p.y0})) else area(layoutNew[layoutNew.length - 1].layer.map((p) -> {x: p.x, y: 0, y0: p.y0 + p.y}))
           )
-        .remove()
+          .remove()

       attrs.$observe 'area', (val) ->


Remove add student button

Remove add student button

--- src/components/course-settings/period-roster.cjsx
+++ src/components/course-settings/period-roster.cjsx
@@ -6,7 +6,6 @@
 {RosterStore, RosterActions} = require '../../flux/roster'
 ChangePeriodLink  = require './change-period'
 DeleteStudentLink = require './delete-student'
-AddStudentButton  = require './add-student'

 module.exports = React.createClass
   displayName: 'PeriodRoster'
@@ -28,11 +27,6 @@
   render: ->
     students = RosterStore.getActiveStudentsForPeriod(@props.courseId, @props.period.id)
     <div className="period">
-      <BS.Row>
-        <BS.Col sm=2>
-          <AddStudentButton {...@props} />
-        </BS.Col>
-       </BS.Row>
       <BS.Table striped bordered condensed hover className="roster">
         <thead>
           <tr>


"Travis CI (Part 4)"

"Travis CI (Part 4)"

--- spec/linter-opencl-spec.coffee
+++ spec/linter-opencl-spec.coffee
@@ -8,7 +8,7 @@
     waitsForPromise ->
       atom.config.set('linter-opencl.pythonPath', '/usr/bin/python')
       atom.config.set('linter-opencl.openCL.platformIndex', 0)
-      atom.config.set('linter-opencl.hybridGraphics.enable', true)
+      atom.config.set('linter-opencl.hybridGraphics.enable', false)
       atom.config.set('linter-opencl.hybridGraphics.offloadingPath', '/usr/bin/optirun')
       atom.config.set('linter-opencl.debug', false)
       atom.packages.activatePackage('language-opencl')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -22,7 +22,7 @@

   engine.each (obj, index) ->
     if index is 0
-      obj1
+      e

 test "#find", ->
   engine = Engine()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -17,6 +17,8 @@
     class: "Awesome"
     x: 5
     y: 10
+
+  obj4

   indexSum = 0


add hour spec for time formatter

add hour spec for time formatter

--- spec/timeFormatter.spec.coffee
+++ spec/timeFormatter.spec.coffee
@@ -9,8 +9,8 @@
     noTime = { milliseconds: 0, seconds: 0, minutes: 0, hours: 0, days: 0, months: 0, years: 0 }
     moment.duration noTime

-  describe 'is given invalid time', ->
-    it 'throw', ->
+  describe 'given invalid time', ->
+    it 'throws', ->
       expect(timeFormatter).to.throw

   describe 'given a number of milliseconds', ->
@@ -22,12 +22,23 @@
       expect(timeFormatter(fiveMinutes)).to.eq '5:00'

   describe 'given a object with time properties', ->
-    {fiveMinutes} = {}
-    beforeEach ->
-      fiveMinutes = minutes: 5
+    describe 'in minutes', ->
+      {fiveMinutes} = {}

-    it 'returns a valid respresentation', ->
-      expect(timeFormatter(fiveMinutes)).to.eq '5:00'
+      beforeEach ->
+        fiveMinutes = minutes: 5
+
+      it 'returns a valid respresentation', ->
+        expect(timeFormatter(fiveMinutes)).to.eq '5:00'
+
+    describe 'in hours', ->
+      {fiveHours} = {}
+
+      beforeEach ->
+        fiveHours = hours: 5
+
+      it 'returns a valid respresentation', ->
+        expect(timeFormatter(fiveHours)).to.eq '5:00:00'

   describe 'given a duration', ->
     {fiveMinutes} = {}


fix(onboarding): Request `/status/` to avoid 302

--- internal_packages/onboarding/lib/page-router-store.coffee
+++ internal_packages/onboarding/lib/page-router-store.coffee
@@ -71,7 +71,7 @@
     @trigger()

     TokenAuthAPI.request
-      path: "/status"
+      path: "/status/"
       returnsModel: false
       timeout: 10000
       success: (json) =>


test(mixins): add test for HasNestedCollection mixin failures

--- specs/units/mixins.spec.coffee
+++ specs/units/mixins.spec.coffee
@@ -99,3 +99,8 @@
     ].forEach (method) ->
       specify "the globalizable", ->
         expect(method, spectacular.global[method]).to exist
+
+describe spectacular.HasNestedCollection, ->
+  context 'when called without options', ->
+    it -> should throwAnError().with 'foo'
+    it -> should throwAnError().with 'foo', {}


activity: remove most active threads from thread sidebar

--- client/activity/lib/components/threadsidebar/index.coffee
+++ client/activity/lib/components/threadsidebar/index.coffee
@@ -22,11 +22,6 @@
           channelThread={@props.channelThread}
           participants={@props.channelParticipants} />
       </ThreadSidebarContentBox>
-      <ThreadSidebarContentBox className="secondary" title="Most Active Threads" titleLink="/Channels/Public/summary">
-        <ChannelMessagesList
-          channelThread={@props.channelThread}
-          messages={@props.popularMessages} />
-      </ThreadSidebarContentBox>
     </div>


reactivity: code cleanup

reactivity: code cleanup

--- client/activity/lib/flux/chatinput/actions/command.coffee
+++ client/activity/lib/flux/chatinput/actions/command.coffee
@@ -98,8 +98,8 @@

   switch name
     when '/invite'
-      params = (cleanUsername param for param in params)
-      channelActions.addParticipantsByNames channelId, params
+      usernames = (cleanUsername param for param in params)
+      channelActions.addParticipantsByNames channelId, usernames
       return yes

   return no


Fixed boot order options bug (Fix #726) Unchecking a boot option should not uncheck all the options below it.

--- app/modules/vm/index.coffee
+++ app/modules/vm/index.coffee
@@ -271,7 +271,10 @@
         return
       $scope.savingBootOrder = true
       paramString = ''
-      forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
+      forEach(bootParams, (boot) ->
+        boot.v && paramString += boot.e
+        return
+      )
       return xo.vm.setBootOrder {vm: id, order: paramString}
       .finally () ->
         $scope.savingBootOrder = false


Tabs: avoid removePane to throw if pane is not passed

--- client/ide/lib/views/tabview/ideapplicationtabview.coffee
+++ client/ide/lib/views/tabview/ideapplicationtabview.coffee
@@ -20,6 +20,9 @@
   removePane_: KDTabView::removePane

   removePane: (pane, shouldDetach, quiet = no) ->
+
+    return  unless pane
+
     {aceView} = pane.getOptions()

     if quiet or not aceView or not aceView.ace.isContentChanged()


Added expiry date feature to introduction tooltips.

--- client/landing/app/CommonViews/introductiontooltip.coffee
+++ client/landing/app/CommonViews/introductiontooltip.coffee
@@ -29,6 +29,7 @@

     super options, data

+    @currentTimestamp = Date.now()
     @shouldAddOverlay = no
     @visibleTooltips  = []

@@ -52,7 +53,7 @@

   createInstance: (parentView, data) ->
     assets = @getAssets parentView, data
-    return unless assets
+    return if not assets or @isExpired assets.data.expiryDate

     {parentView, tooltipView} = assets
     tooltip = new IntroductionTooltip {
@@ -67,6 +68,9 @@
       @close parentView.getOptions().introId
       parentView.tooltip.destroy()
       @visibleTooltips.splice @visibleTooltips.indexOf(tooltip), 1
+
+  isExpired: (expiryDate) ->
+    return new Date(expiryDate).getTime() < @currentTimestamp

   getAssets: (parentView, data) ->
     return no if not @introSnippets


Refactor: Once again renew the forecast-dataset transformation logic due-to amendments to dataset returned by REST-API

--- app/routes/dashboard.coffee
+++ app/routes/dashboard.coffee
@@ -5403,7 +5403,7 @@
         # Weather data aquisition procedure/configuration
         # For Simulation mode >> true
         # For RESTful-Resource mode >> false
-        simulationMode = false # Default: false
+        simulationMode = false# Default: false

         # City of interest >> Tampere >> 634963
         tampere_LocationId = 634963
@@ -5535,7 +5535,7 @@
         # Hourly Weather Forecast Info
         # -----------------------------
         # --- Get hourly weather forcast info, max 11 hours in the future ---
-        tampere_HourlyWeatherForecasts_Max11Hours = data.forecasts[0].forecast[1..11]
+        tampere_HourlyWeatherForecasts_Max11Hours = data.forecasts[0].forecast[2..12]
         for forecast in tampere_HourlyWeatherForecasts_Max11Hours
             forecastOnTheHour = {}
             forecastOnTheHour.geoid = forecast.geoid


Update pointers validation tests

Update pointers validation tests

--- test/testJsonPointer.coffee
+++ test/testJsonPointer.coffee
@@ -58,19 +58,21 @@
     describe "validates pointer and", () ->
       target =
         foo: "bar"
+        baz: [1, 2, 3]
+        "-": "valid"
       targetAsString = JSON.stringify target
       getTestFunction = (pointer) ->
         () -> jsonPointer.get targetAsString, pointer

       it "throws an error if pointer is not valid", () ->
-        invalidPointers = ["a", "/01"]
+        invalidPointers = ["a", "/baz/01", "/baz/-", "-"]
         testFunctions = invalidPointers.map getTestFunction

         expect(f).to.throw Error for f in testFunctions

       it "does not throw an error if pointer is valid", () ->
-        validPointers = ["", "/", "//", "/a", "/0", "/10", "/a/0", "/1/a"]
+        validPointers = ["", "/", "//", "/a", "/0", "/10", "/a/0", "/1/a", "/-"]
         testFunctions = validPointers.map getTestFunction

         expect(f).to.not.throw Error for f in testFunctions


Add the call to removeListeners for the generator

--- lib/generators/utensils/utensil/templates/behavior.coffee
+++ lib/generators/utensils/utensil/templates/behavior.coffee
@@ -18,6 +18,7 @@
 # PUBLIC #

   dispose: ->
+    @removeListeners()

 # PROTECTED #


Cleanup

Cleanup

--- test/tests/animation.coffee
+++ test/tests/animation.coffee
@@ -37,8 +37,6 @@
 				curve: "linear"

 			animation.start()
-
-			console.log "animation.curveValues", animation.curveValues

 			animation.totalTime.should.equal animation.time
 			animation.curveValues.length.should.equal \


createAppsIcons to createAllAppIcons

createAppsIcons to createAllAppIcons

--- client/app/Applications/StartTab.kdapplication/AppView.coffee
+++ client/app/Applications/StartTab.kdapplication/AppView.coffee
@@ -154,7 +154,7 @@
             delegate : @
           , manifest

-      @createAppsIcons apps
+      @createAllAppIcons apps
       @createGetMoreAppsButton()

       @hideLoader()
@@ -180,7 +180,7 @@
     {removedApps, newApps, existingApps, force} = changes
     return @decorateApps()  if force or existingApps.length is 0
     @removeAppIcon app  for app in removedApps
-    @createAppsIcons @appsController.getManifests()  if newApps.length > 0
+    @createAllAppIcons @appsController.getManifests()  if newApps.length > 0

   createAppIcon:(app, appData, bulk=no)->

@@ -198,7 +198,7 @@
     # To make sure its always the last icon
     @createGetMoreAppsButton()  unless bulk

-  createAppsIcons:(apps)->
+  createAllAppIcons:(apps)->
     for app, appData of apps
       do (app, appData)=>
         @createAppIcon app, appData, yes


Client: Activity: Added sidebarModalChannels, searchProp and onThresholdAction props to sidebarModalList component

--- client/app/lib/components/sidebarmessagessection/index.coffee
+++ client/app/lib/components/sidebarmessagessection/index.coffee
@@ -10,8 +10,9 @@
 module.exports = class SidebarMessagesSection extends React.Component

   @defaultProps =
+    selectedId : null
     threads    : immutable.Map()
-    selectedId : null
+    sidebarChannelsSection: immutable.Map()

   constructor: (props) ->
@@ -41,6 +42,9 @@
       <SidebarModalList
         title='Other Messages:'
         threads={@props.threads}
+        onThresholdAction='loadFollowedPrivateChannels'
+        searchProp='purpose'
+        sidebarModalChannels={@props.sidebarModalChannels}
         itemComponent={PrivateChannelListItem}/>
     </Modal>


Transform fust jump buttons activated

Transform fust jump buttons activated

--- lib/foundry-xray/app/assets/javascripts/application.js.coffee
+++ lib/foundry-xray/app/assets/javascripts/application.js.coffee
@@ -27,6 +27,8 @@
     @sliderApply = $('#timeline button.ok')
     @sliderNext  = $('#timeline button.next')
     @sliderPrev  = $('#timeline button.prev')
+    @sliderFNext = $('#timeline button.fnext')
+    @sliderFPrev = $('#timeline button.fprev')
     @transforms  = $('#transforms')

@@ -112,3 +114,11 @@

     @sliderPrev.click => @draw @currentStep-1
     @sliderNext.click => @draw @currentStep+1
+
+    @sliderFNext.click =>
+      step = @input.transforms.find (x) => x.id > @currentStep
+      @draw step.id if step
+
+    @sliderFPrev.click =>
+      step = @input.transforms.findAll((x) => x.id < @currentStep).last()
+      @draw step.id if step


cleanup

cleanup

--- apps/sitemaps/routes.coffee
+++ apps/sitemaps/routes.coffee
@@ -8,7 +8,7 @@
 { API_URL, POSITRON_URL, FUSION_URL, APP_URL } = require('sharify').data
 artsyXapp = require 'artsy-xapp'
 PAGE_SIZE = 100
-FUSION_PAGE_SIZE = 1000
+FUSION_PAGE_SIZE = 5000

 epoch = -> moment('2010 9 1', 'YYYY MM DD')
 buckets = _.times moment().diff(epoch(), 'months'), (i) ->
@@ -139,7 +139,7 @@
 @bingjson = (req, res, next) ->
   res.set('Content-Disposition': 'attachment').write('[')
   getArtworkBuckets (err, buckets) ->
-    async.mapSeries buckets.reverse()[0..10], (bucket, callback) ->
+    async.mapSeries buckets.reverse(), (bucket, callback) ->
       iterator = (page, callback) ->
         request
           .get("#{FUSION_URL}/api/v1/artworks")
@@ -155,7 +155,9 @@
             streamResults sres.body.results, res
             callback()
       async.timesSeries(bucket.pages + 1, iterator, callback)
-    , -> res.write ']'
+    , ->
+      res.write('{}]')
+      res.end()

 streamResults = (results, res) ->
   results.forEach (artwork) ->
@@ -202,4 +204,4 @@
         "height": dimensions.height
         "width": dimensions.width
       }
-    res.write JSON.stringify json
+    res.write JSON.stringify(json) + ','


FIX: Pie chart will show only relevant tasks by the target grade

Previously considering all tasks, even if those tasks were outside of the
target grade.

--- src/app/visualisations/partials/directives/student-task-status-pie-chart.coffee
+++ src/app/visualisations/partials/directives/student-task-status-pie-chart.coffee
@@ -11,10 +11,10 @@
     updateData = ->
       $scope.data = []
       _.each taskService.statusLabels, (label, key) ->
-        count =  _.where($scope.project.tasks, {status: key}).length
+        count = projectService.tasksByStatus($scope.project, key).length
         $scope.data.push { key: label, y: count, status_key: key }

-    $scope.$watch 'project.burndown_chart_data', updateData, true
+    $scope.$watch 'project.burndown_chart_data', updateData

     [$scope.options, $scope.config] = Visualisation 'pieChart', {
       color: (d, i) ->


Moved actual drawing into LightCurve.drawData method.

--- assets/js/lightcurve.coffee
+++ assets/js/lightcurve.coffee
@@ -27,21 +27,8 @@
                 d3.scale.linear().range([0, @height]).nice()
             ]

-
-    selector = '.lightcurve'
-    lc = new LightCurve(selector)
-    width = lc.width
-    height = lc.height
-    svg = lc.getSvg()
-    [xScale, yScale] = lc.getScales()
-    csvUrl = $(selector).data 'csvSource'
-    d3.csv(
-        csvUrl,
-        ((d) ->
-            jd: +d.jd
-            magnitude: +d.magnitude
-        ),
-        ((error, data) ->
+        drawData: (svg, data) ->
+            [xScale, yScale] = @getScales()
             xScale.domain d3.extent data, (d) -> d.jd
             yScale.domain d3.extent data, (d) -> d.magnitude
             xAxis = d3.svg.axis().scale(xScale).orient('bottom').ticks(10)
@@ -49,12 +36,12 @@
             svg.append('g')
                 .attr
                     class: 'x axis'
-                    transform: "translate(0,#{height})"
-                .call(xAxis.tickSize(-height, 0, 0))
+                    transform: "translate(0,#{@height})"
+                .call(xAxis.tickSize(-@height, 0, 0))
             svg.append('g')
                 .attr
                     class: 'y axis'
-                .call(yAxis.tickSize(-width, 0, 0))
+                .call(yAxis.tickSize(-@width, 0, 0))
             svg.selectAll('circle')
                 .data(data)
                 .enter()
@@ -64,5 +51,19 @@
                     cy: (d) -> yScale d.magnitude
                     fill: 'red'
                     r: '2'
-            )
+
+
+    selector = '.lightcurve'
+    lc = new LightCurve(selector)
+    svg = lc.getSvg()
+    csvUrl = $(selector).data 'csvSource'
+    d3.csv(
+        csvUrl,
+        ((d) ->
+            jd: +d.jd
+            magnitude: +d.magnitude
+        ),
+        ((error, data) ->
+            lc.drawData svg, data
         )
+    )


need to check if continue is enabled before allowing on continue to be called, esp with enter key nav

--- src/components/exercise/step-card.cjsx
+++ src/components/exercise/step-card.cjsx
@@ -105,7 +105,9 @@
     @props.onChangeAnswerAttempt?(answer)

   onContinue: ->
-    {panel, canReview, onNextStep, onStepCompleted, onContinue} = @props
+    {panel, canReview, onNextStep, onStepCompleted, onContinue, isContinueEnabled} = @props
+
+    return unless isContinueEnabled and @isContinueEnabled(@props, @state)

     if onContinue?
       onContinue(@state)


home/tryonkoding: switch disable to readonly

--- client/home/lib/utilities/components/tryonkoding/view.coffee
+++ client/home/lib/utilities/components/tryonkoding/view.coffee
@@ -6,23 +6,23 @@
 module.exports = class TryOnKodingView extends React.Component

   renderGuideButton: ->
-
+

       VIEW GUIDE

-
-
+
+
   renderTryOnKodingButton: ->
-
+

-
+
   renderButtons: ->

       {@renderGuideButton()} {@renderTryOnKodingButton()}

-
-
+
+
   render: ->

@@ -32,7 +32,7 @@
         “Try On Koding” Button
         Visiting users will have access to all team stack scripts

-          <textarea ref='textarea' spellCheck={no} disabled={yes} value={@props.value}>
+          <textarea ref='textarea' spellCheck={no} readonly value={@props.value}>
           </textarea>

         {@renderButtons()}
@@ -41,10 +41,9 @@

 Primary = ({ className }) ->
-
+

     Enable “Try On Koding” Button
     Allow access to team stack catalogue for visitors

-
-  +


Fix migration 36 (assets)

Fix migration 36 (assets)

--- server/startup/migrations/v36.coffee
+++ server/startup/migrations/v36.coffee
@@ -25,3 +25,27 @@

 		RocketChat.models.Settings.remove _id: 'Layout_Login_Header'
+
+		files = RocketChat.__migration_assets_files = new Mongo.Collection('assets.files')
+		chunks = RocketChat.__migration_assets_chunks = new Mongo.Collection('assets.chunks')
+		list = {
+			'favicon.ico': 'favicon_ico'
+			'favicon.svg': 'favicon'
+			'favicon_64.png': 'favicon_64'
+			'favicon_96.png': 'favicon_96'
+			'favicon_128.png': 'favicon_128'
+			'favicon_192.png': 'favicon_192'
+			'favicon_256.png': 'favicon_256'
+		}
+
+		for from, to of list
+			newFile = files.findOne({_id: to})
+			if not newFile?
+				oldFile = files.findOne({_id: from})
+				if oldFile?
+					oldFile._id = to
+					oldFile.filename = to
+					files.insert(oldFile)
+					files.remove({_id: from})
+					chunks.update({files_id: from}, {$set: {files_id: to}}, {multi: true})
+


Improve method broadcastAuth

Improve method broadcastAuth

--- server/stream/streamBroadcast.coffee
+++ server/stream/streamBroadcast.coffee
@@ -54,7 +54,7 @@
 			check remoteId, String

 			@unblock()
-			if InstanceStatus.id() is selfId and InstanceStatus.getCollection().findOne({_id: remoteId})?
+			if selfId is InstanceStatus.id() and remoteId isnt InstanceStatus.id() and InstanceStatus.getCollection().findOne({_id: remoteId})?
 				@connection.broadcastAuth = true

 			return @connection.broadcastAuth is true


FIX: Ensure home has the notification query parameter

--- src/app/home/states/home/home.coffee
+++ src/app/home/states/home/home.coffee
@@ -2,7 +2,7 @@

 .config((headerServiceProvider) ->
   homeStateData =
-    url: "/home"
+    url: "/home?notifications"
     views:
       main:
         controller: "HomeCtrl"
@@ -13,7 +13,7 @@
   headerServiceProvider.state 'home', homeStateData
 )

-.controller("HomeCtrl", ($scope, $state, $timeout, User, Unit, ExternalName, headerService, currentUser, unitService, projectService, $rootScope, analyticsService, dateService) ->
+.controller("HomeCtrl", ($scope, $state, $timeout, User, Unit, ExternalName, headerService, currentUser, unitService, projectService, $rootScope, analyticsService, dateService, UserNotificationSettingsModal) ->
   analyticsService.event 'Home', 'Viewed Home page'

   # Get the confugurable, external name of Doubtfire
@@ -92,4 +92,7 @@

   $scope.currentUser = currentUser

+  if $state.params?.notifications?
+    UserNotificationSettingsModal.show currentUser?.profile
+
 )


fixed bug loading user-properties

fixed bug loading user-properties

--- src/art/engine/file/v1_loader.coffee
+++ src/art/engine/file/v1_loader.coffee
@@ -6,7 +6,7 @@
 Elements = require '../elements'
 Model = require './model'

-{Binary, inspect, BaseObject, Promise, log, mergeInto} = Foundation
+{Binary, inspect, BaseObject, Promise, log, mergeInto, lowerCamelCase, merge} = Foundation
 {point, rect, matrix} = Atomic
 {EncodedImage} = Binary

@@ -210,7 +210,7 @@

     @decodeLayout object, tag, parent

-
+    userProperties = {}
     for k, v of tag.attributes
       switch k
         when "name" then object.name = v
@@ -218,7 +218,7 @@
         when "art_engine_version" then # ignored
         when "kimi_editor_version" then # ignored
         when "composite_mode" then @setCompositeMode object, v
-        when "drop_in_enabled" then object.userProperties.drop_in_enabled = true
+        when "drop_in_enabled" then userProperties.dropInEnabled = true
         when "lock_mode" then # ignored

         when "handle", "w_val", "h_val", "x_val", "y_val", "x_layout_mode", "y_layout_mode", "stack_mode" then # handled elsewhere
@@ -227,8 +227,10 @@
           switch tagKey
             when "art_file:bitmaps", "art_bitmap:bitmap_id"
             else
-              object.userProperties[k] = v.toString()
+              userProperties[lowerCamelCase k] = v.toString()
               @log "WARNING: unknown tag:attribute: #{tagKey} (ignored)"
+
+    object.userProperties = userProperties

     if children = tag.tags.children
       @populateChildrenFromTag object, children


Tidy up docblock in deploy.coffee

Tidy up docblock in deploy.coffee

--- web/coffee/deploy.coffee
+++ web/coffee/deploy.coffee
@@ -1,13 +1,6 @@
-#
-# Project: Pontoon
-# Author: Saul Johnson
-# Notes: Client-side script for the deployments page.
-#
-
-#
 # Uses an AJAX call to deploy a website.
 #
-# @param hash  the hash (unique ID) of the project to deploy
+# @param [String] hash the hash (unique ID) of the project to deploy
 #
 window.deploy = (hash) ->
   waitingDialog.show 'Deploying site', {}


If we're not in standalone, create table on run

This simply calls `createComponent: DG.TableView` with no additional
arguments, to create a default table, on case creation.

Note that calling this multiple times while a table is open has
no effect.

[#112600817]

--- src/code/models/codap-connect.coffee
+++ src/code/models/codap-connect.coffee
@@ -3,6 +3,7 @@
 CodapActions    = require '../actions/codap-actions'
 SimulationStore = require '../stores/simulation-store'
 TimeUnits       = require '../utils/time-units'
+
 module.exports = class CodapConnect

   name: 'Building Models Tool'
@@ -10,6 +11,8 @@
   codapPhone: null

   currentCaseID: null
+
+  standaloneMode: false

   stepsInCurrentCase: 0

@@ -101,6 +104,8 @@
         @currentCaseID = result.caseID
         @stepsInCurrentCase = 0
         @_flushQueue()
+        if not @standaloneMode
+          @createTable()
       else
         log.info "CODAP returned an error on 'openCase'"

@@ -165,6 +170,14 @@
         yAttributeName: yAttributeName,
         size: { width: 242, height: 221 },
         position: 'bottom'
+        log: false
+      }
+
+  createTable: (yAttributeName)->
+    @codapPhone.call
+      action: 'createComponent'
+      args: {
+        type: 'DG.TableView',
         log: false
       }

@@ -192,6 +205,7 @@

       when 'standaloneUndoModeAvailable'
         log.info 'Received standaloneUndoModeAvailable request from CODAP.'
+        @standaloneMode = true
         @graphStore.setCodapStandaloneMode true

       when 'undoAction'


Fix svg graphs to look more like the real output

[#112564481][#113164471]

--- src/code/views/svg-graph-view.coffee
+++ src/code/views/svg-graph-view.coffee
@@ -20,8 +20,16 @@
   # TODO: This is a hack. We parse the formula.
   # We want to know because it informs how we scale the
   # graph axis
-  isExponential: ->
-    @props.formula.indexOf("^") > -1 or @props.formula.indexOf("1/in")  > -1
+  getYScale: (xrange, yrange) ->
+    if @props.formula.indexOf("^") > -1
+      yrange
+    else if (@props.formula.indexOf("log")  > -1) and (@props.formula.indexOf("maxIn -") is -1)
+      yrange * 3
+    else
+      xrange
+
+  startAt1: ->
+    @props.formula.indexOf("log")  > -1

   invertPoint: (point) ->
     {x: point.x, y: @props.height - point.y}
@@ -42,13 +50,13 @@
     "M #{data}"

   getPathPoints: ->
-    rangex = 20
-    x0 = if @isExponential() then 1 else 0
+    rangex = 60
+    x0 = if @startAt1() then 1 else 0
     data = _.range(x0,rangex)
     miny = Infinity
     maxy = -Infinity
     data = _.map data, (x) =>
-      scope = {in: x, out: 0, maxIn: rangex}
+      scope = {in: x, out: 0, maxIn: rangex, maxOut: rangex}
       try
         y = math.eval @props.formula, scope
         if y < miny then miny = y
@@ -61,15 +69,12 @@

     # if we aren't doing exponential graphing, then keep
     # then use the range of x for axis scaling
-    scaley = if @isExponential() then rangey else rangex
+    scaley = @getYScale(rangex, rangey)
     data = _.map data, (d) ->
       {x,y} = d
-      y = (y - miny)
       x = x / rangex
       y = y / scaley
       {x: x, y: y}
-    console.log(@props.formula)
-    console.log(data)
     data

   renderXLabel: ->
@@ -98,8 +103,8 @@
   render: ->
     (div {className: 'svgGraphView'},
       (svg {width: @props.width, height: @props.height},
+        @renderAxisLines()
         @renderLineData()
-        @renderAxisLines()
         @renderXLabel()
         @renderYLabel()
       )


don't divide by zero when computing bucket properties based on target

--- src/node_modules/bucket-model/bucket-model.coffee
+++ src/node_modules/bucket-model/bucket-model.coffee
@@ -58,11 +58,15 @@
         percentageFunded:
           deps: ['contributionTotal', 'target']
           get: ->
+            if not @target.gt(0)
+              return new Decimal(0)
             @contributionTotal.div(@target)

         myContributionPercentage:
           deps: ['myContribution', 'target']
           get: ->
+            if not @target.gt(0)
+              return new Decimal(0)
             @myContribution.amount.div(@target)

         groupContributionPercentage:


Revert "removing the tree view core:close test, because cmd-w no longer closes the sidebar."

This reverts commit ab144a6d312765a37aba0bc4bd9dd9d6bc3839f1.

--- src/packages/tree-view/spec/tree-view-spec.coffee
+++ src/packages/tree-view/spec/tree-view-spec.coffee
@@ -193,6 +193,19 @@
       expect(treeView).toBeVisible()
       expect(treeView).not.toMatchSelector(':focus')
       expect(rootView.getActiveEditor().isFocused).toBeTruthy()
+
+  describe "when core:close is triggered on the tree view", ->
+    it "detaches the TreeView, focuses the RootView and does not bubble the core:close event", ->
+      treeView.attach()
+      treeView.focus()
+      rootViewCloseHandler = jasmine.createSpy('rootViewCloseHandler')
+      rootView.on 'core:close', rootViewCloseHandler
+      spyOn(rootView, 'focus')
+
+      treeView.trigger('core:close')
+      expect(rootView.focus).toHaveBeenCalled()
+      expect(rootViewCloseHandler).not.toHaveBeenCalled()
+      expect(treeView.hasParent()).toBeFalsy()

   describe "when a directory's disclosure arrow is clicked", ->
     it "expands / collapses the associated directory", ->


setup colors for comments #347

setup colors for comments #347

--- app/assets/javascripts/oxalis/view/skeletontracing/abstract_tree_renderer.coffee
+++ app/assets/javascripts/oxalis/view/skeletontracing/abstract_tree_renderer.coffee
@@ -34,13 +34,18 @@
     @canvas[0].height = height

-  drawTree : (tree, @activeNodeId) ->
+  drawTree : (tree, @activeNodeId, @comments) ->
+
     # clear Background
     @ctx.clearRect(0, 0, @canvas.width(), @canvas.height())
+
+    # apply color scheme
     if app.oxalis.view.theme == Constants.THEME_BRIGHT
       @vgColor = "black"
+      @commentColor = "red"
     else
       @vgColor = "white"
+      @commentColor = "blue"

     unless tree?
       return


fix(analytics): Add Mixpanel `alias`, don't store @id locally

--- src/flux/stores/analytics-store.coffee
+++ src/flux/stores/analytics-store.coffee
@@ -46,18 +46,17 @@
     fileDownloaded: -> {}

   track: (action, data={}) ->
-    return unless @id
-    @analytics.track(action, _.extend(data, namespaceId: @id))
+    @analytics.track(action, _.extend(data, namespaceId: NamespaceStore.current()?.id))

   identify: ->
-    @id = NamespaceStore.current?()?.id?
-    return unless @id
-    me = NamespaceStore.current().me()
-    @analytics.people.set @id,
-      "$email": me.email
-      "$first_name": me.firstName()
-      "$last_name": me.lastName()
-      "namespaceId": me.namespaceId
+    namespace = NamespaceStore.current()
+    if namespace
+      @analytics.alias("distinct_id", namespace.id)
+      @analytics.people.set namespace.id,
+        "$email": namespace.me().email
+        "$first_name": namespace.me().firstName()
+        "$last_name": namespace.me().lastName()
+        "namespaceId": namespace.id

   _listenToActions: ->
     _.each @actionsToTrack(), (callback, action) =>


fix custom date filter

fix custom date filter

--- @client/opinion_views.coffee
+++ @client/opinion_views.coffee
@@ -408,12 +408,15 @@

 date_option_changed = (activated) ->
-  date_toggle_state = fetch 'date_toggle_state'
+  date_toggle_state = fetch 'opinion-date-filter'
+  tz_offset = new Date().getTimezoneOffset() * 60 * 1000
+
   pass = (u, opinion, proposal) ->
     date = new Date(opinion.updated_at).getTime()
     now = Date.now()

     earliest = latest = null
+
     if activated.key != 'custom'
       clear_custom_date()

@@ -428,10 +431,8 @@
       if date_toggle_state.start
         earliest = date_toggle_state.start + tz_offset

-
       if date_toggle_state.end
         latest = date_toggle_state.end + tz_offset
-

     (earliest == null || earliest <= date) && (latest == null || latest >= date)

@@ -440,6 +441,7 @@
     salience: (u, opinion, proposal) -> if pass(u, opinion, proposal) then 1 else .1
     weight:   (u, opinion, proposal) -> if pass(u, opinion, proposal) then 1 else .1
     name: activated.key
+

   activate_opinion_date_filter view

@@ -462,7 +464,7 @@
   ]

 clear_custom_date = ->
-  date_toggle_state = fetch 'date_toggle_state'
+  date_toggle_state = fetch 'opinion-date-filter'
   date_toggle_state.start = null
   date_toggle_state.end = null
   save date_toggle_state
@@ -726,7 +728,7 @@
                 className: 'custom_view_triangle'
                 style:
                   left: triangle_left
-                  bottom: if browser.is_mobile then -27 else -26
+                  bottom: if browser.is_mobile then -27 else -25
                 dangerouslySetInnerHTML: __html: """<svg width="25px" height="13px" viewBox="0 0 25 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Artboard" transform="translate(-1086.000000, -586.000000)" fill="#FFFFFF" stroke="#979797"><polyline id="Path" points="1087 599 1098.5 586 1110 599"></polyline></g></g></svg>"""

@@ -1130,10 +1132,12 @@
           for weight in get_weights()
             do (weight) ->
               LI
+                key: weight.key
                 style:
                   marginRight: 8
                   marginBottom: 1
                   display: 'inline-block'
+                'data-key': weight.key

                 BUTTON
                   'data-tooltip': if !browser.is_mobile then weight.label
@@ -1201,6 +1205,7 @@

           else
             filter_str = "#{to_date_str(date_toggle_state.start)} - #{to_date_str(date_toggle_state.end)}"
+
         else
           filter_str = default_date_options().find((o) -> o.key == date_toggle_state.active).label


Show all tags on QA view

Show all tags on QA view

--- src/components/qa/exercise-card.cjsx
+++ src/components/qa/exercise-card.cjsx
@@ -21,7 +21,11 @@
   render: ->
     return null if _.every( ExerciseStore.poolTypes(@props.exercise), (pt) => @props.ignoredPoolTypes[pt] )
     editUrl = @props.exercise.url.replace(/@\d+/, '@draft')
-    <ExerciseCard {...@props} header={@renderHeader()} displayFeedback>
+    <ExerciseCard {...@props}
+      header={@renderHeader()}
+      displayAllTags
+      displayFeedback
+    >
       edit
     </ExerciseCard>


minor updates

minor updates

--- server.coffee
+++ server.coffee
@@ -12,6 +12,8 @@
 gitty   = require "gitty"

 GitHubApi = require "github"
+ghRepos   = new GitHubApi({version: "3.0.0"}).repos
+

 redis = require "redis"
 rc    = redis.createClient()
@@ -87,7 +89,7 @@
     #     return
     gitty.commit __dirname + "/repos/#{name}", "initial", (err, data) ->
       gitty.remote.add __dirname + "/repos/#{name}", "origin", "https://github.com/communities/#{name}.git", (err, data) ->
-        console.log "origin was add", err, data
+        console.log "origin was added", err, data
         gitty.push __dirname + "/repos/#{name}", "origin", "master", (err, data) ->
           if err
             callback err
@@ -181,9 +183,8 @@
     if repos.length > 0
       callback undefined, repos
     else
-      gh = new GitHubApi version: "3.0.0"
-      gh.repos.getFromOrg {org: "communities"}, (err, repos) ->
-        repos = _.filter repos, (repo) -> repo.name != 'gitcommunities.com'
+      ghRepos.getFromOrg {org: "communities"}, (err, repos) ->
+        repos = _.filter repos, (repo) -> repo.name != "gitcommunities.com"
         communitiesFetchFuncs = []
         _.each repos, (repo) ->
           communitiesFetchFuncs.push async.apply getCommunity, repo.name
@@ -205,8 +206,7 @@

 getCommunity = (community, callback) ->
-  gh = new GitHubApi version: "3.0.0"
-  gh.repos.get {user: "communities", repo: community}, (err, repo) ->
+  ghRepos.get {user: "communities", repo: community}, (err, repo) ->
     if err
       callback err
       return
@@ -227,8 +227,7 @@
       callback undefined, repo

 getTopics = (community, callback) ->
-  gh = new GitHubApi version: "3.0.0"
-  gh.repos.getBranches {user: "communities", repo: community}, (err, branches) ->
+  ghRepos.getBranches {user: "communities", repo: community}, (err, branches) ->
     if err
       callback err
       return
@@ -338,9 +337,6 @@
       return
     res.json resp

-
-
-
 app.get "/", (req, res) ->
   params =
     user: req.user or {}


fix errors after update bookshelf to 0.13.3

--- server.coffee
+++ server.coffee
@@ -22,7 +22,7 @@

 Message = bookshelf.Model.extend({
   tableName: 'messages'
-  hasTimestamps: ['created_at']
+  hasTimestamps: true
   user: ->
     this.belongsTo(User, 'user_id')
 })


Exposer Fiber.

Exposer Fiber.

--- server.coffee
+++ server.coffee
@@ -3,6 +3,7 @@
 do -> # To not pollute the namespace
   require = __meteor_bootstrap__.require

+  fibers = require 'fibers'
   future = require 'fibers/future'

   blocking = (obj, fun) ->
@@ -13,3 +14,5 @@
     wrapped = future.wrap fun
     () ->
       wrapped.apply(obj, arguments).wait()
+
+  blocking.Fiber = fibers


Timestamps in audio/video control packet is no longer used

--- server.coffee
+++ server.coffee
@@ -260,8 +260,6 @@

 videoFrames = 0
 audioFrames = 0
-lastVideoStatsTime = null
-lastAudioStatsTime = null

 onReceiveBuffer = (buf) ->
   packetType = buf[0]
@@ -278,34 +276,16 @@
   return

 onReceiveVideoControlBuffer = (buf) ->
-  time  = buf[1] * 0x0100000000000000  # this loses some precision
-  time += buf[2] * 0x01000000000000
-  time += buf[3] * 0x010000000000
-  time += buf[4] * 0x0100000000
-  time += buf[5] * 0x01000000
-  time += buf[6] * 0x010000
-  time += buf[7] * 0x0100
-  time += buf[8]
-  lastVideoStatsTime = new Date().getTime()
+  console.log "video start"
   timeForVideoRTPZero = Date.now()
   timeForAudioRTPZero = timeForVideoRTPZero
-  console.log "video start"
   spropParameterSets = ''
   rtmpServer.startStream timeForVideoRTPZero

 onReceiveAudioControlBuffer = (buf) ->
-  time  = buf[1] * 0x0100000000000000  # this loses some precision
-  time += buf[2] * 0x01000000000000
-  time += buf[3] * 0x010000000000
-  time += buf[4] * 0x0100000000
-  time += buf[5] * 0x01000000
-  time += buf[6] * 0x010000
-  time += buf[7] * 0x0100
-  time += buf[8]
-  lastAudioStatsTime = new Date().getTime()
+  console.log "audio start"
   timeForAudioRTPZero = Date.now()
   timeForVideoRTPZero = timeForAudioRTPZero
-  console.log "audio start"
   rtmpServer.startStream timeForAudioRTPZero

 onReceiveVideoDataBuffer = (buf) ->


Improved: logging

Improved: logging

--- server.coffee
+++ server.coffee
@@ -5,4 +5,4 @@

 americano.start name: 'pfm', port: port, ->
     init ->
-        console.log "initialization done"+        console.log "=> Server intialized!"


Use volatile

Use volatile

--- server.coffee
+++ server.coffee
@@ -34,8 +34,8 @@
     cmds = packetParse packet
   catch e
     # TODO: catch only parse errors
-    websocket.emit "parseError",
-      error: "Failed to parse whole packet: #{ e.message }"
+    websocket.sockets.volatile.emit "parseError",
+      error: e.message
       address: rinfo.address

     # Failed to parse the packet. We cannot continue from here at all.
@@ -60,14 +60,8 @@
     results.cmds.push cmd

   manager.commitAll()
-  console.log "sending", cmds
-  websocket.sockets.emit "cmds", results
+  websocket.sockets.volatile.emit "cmds", results

-
-
-# websocket.on "cmds", (cmds) ->
-#   for cmd in cmds
-#     console.log "WEB", cmd


added unit tests

added unit tests

--- services/document-updater/test/unit/coffee/RedisManager/RedisManagerTests.coffee
+++ services/document-updater/test/unit/coffee/RedisManager/RedisManagerTests.coffee
@@ -59,7 +59,12 @@
 				@rclient.get
 					.calledWith("DocVersion:#{@doc_id}")
 					.should.equal true
-
+
+			it 'should get the hash', ->
+				@rclient.get
+					.calledWith("DocHash:#{@doc_id}")
+					.should.equal true
+
 			it "should get the ranges", ->
 				@rclient.get
 					.calledWith("Ranges:#{@doc_id}")
@@ -69,6 +74,26 @@
 				@callback
 					.calledWith(null, @lines, @version, @ranges)
 					.should.equal true
+
+			it 'should not log any errors', ->
+				@logger.error.calledWith()
+					.should.equal false
+
+		describe "with a corrupted document", ->
+			beforeEach ->
+				@badHash = "INVALID-HASH-VALUE"
+				@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges])
+				@RedisManager.getDoc @project_id, @doc_id, @callback
+
+			it 'should log a hash error', ->
+				@logger.error.calledWith()
+					.should.equal true
+
+			it 'should return the document', ->
+				@callback
+					.calledWith(null, @lines, @version, @ranges)
+					.should.equal true
+

 		describe "getDoc with an invalid project id", ->
 			beforeEach ->
@@ -177,6 +202,7 @@
 			@lines = ["one", "two", "three"]
 			@ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }]
 			@version = 42
+			@hash = crypto.createHash('sha1').update(JSON.stringify(@lines)).digest('hex')
 			@ranges = { comments: "mock", entries: "mock" }

 			@rclient.exec = sinon.stub().callsArg(0)
@@ -199,6 +225,11 @@
 			it "should set the version", ->
 				@rclient.set
 					.calledWith("DocVersion:#{@doc_id}", @version)
+					.should.equal true
+
+			it "should set the hash", ->
+				@rclient.set
+					.calledWith("DocHash:#{@doc_id}", @hash)
 					.should.equal true

 			it "should set the ranges", ->
@@ -275,6 +306,7 @@
 			@rclient.exec.yields()
 			@lines = ["one", "two", "three"]
 			@version = 42
+			@hash = crypto.createHash('sha1').update(JSON.stringify(@lines)).digest('hex')
 			@ranges = { comments: "mock", entries: "mock" }

 		describe "with non-empty ranges", ->
@@ -289,6 +321,11 @@
 			it "should set the version", ->
 				@rclient.set
 					.calledWith("DocVersion:#{@doc_id}", @version)
+					.should.equal true
+
+			it "should set the hash", ->
+				@rclient.set
+					.calledWith("DocHash:#{@doc_id}", @hash)
 					.should.equal true

 			it "should set the ranges", ->
@@ -336,6 +373,11 @@
 			@rclient.del
 				.calledWith("DocVersion:#{@doc_id}")
 				.should.equal true
+
+		it "should delete the hash", ->
+			@rclient.del
+				.calledWith("DocHash:#{@doc_id}")
+				.should.equal true

 		it "should delete the project_id for the doc", ->
 			@rclient.del


Update HumanReadableLogsRules.coffee

Update HumanReadableLogsRules.coffee

--- services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -153,6 +153,36 @@
 			You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\ before it.
 		"""
 	,
+		regexToMatch: /LaTeX Error: There's no line here to end/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_There%27s_no_line_here_to_end"
+		humanReadableHint: """
+			You have used a \\\ or \\newline command where LaTeX was not expecting one. Make sure that you only use line breaks after blocks of text, and be careful using linebreaks inside lists and other environments.
+		"""
+	,
+		regexToMatch: /LaTeX Error: \\verb ended by end of line/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cverb_ended_by_end_of_line"
+		humanReadableHint: """
+			You have used a \\verb command incorrectly. Try replacling the \\verb command with \begin{verbatim}\u2026\end{verbatim}.
+		"""
+	,
+		regexToMatch: /Illegal unit of measure (pt inserted)/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FIllegal%20unit%20of%20measure%20(pt%20inserted)"
+		humanReadableHint: """
+			You have written a length, but have not specified the appropriate units (pt, mm, cm etc.). If you have not written a length, check that you have not witten a linebreak \\\ followed by square brackets [\u2026] anywhere.
+		"""
+	,
+		regexToMatch: /Extra \\right/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_%5Cright"
+		humanReadableHint: """
+			You have written a \\right command without a corresponding \\left command. Check that all \\left and \\right commands balance everywhere.
+		"""
+	,
+		regexToMatch: /Missing \\begin{document}/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Missing%20%5Cbegin%20document"
+		humanReadableHint: """
+			No \\begin{document} command was found. Make sure you have included \\begin{document} in your preamble, and that your main document is set correctly.
+		"""
+	,
 		ruleId: "hint_mismatched_environment2"
 		types: ['environment']
 		cascadesFrom: ['environment']


Add more warning in react rendering when lines are undefined

--- lib/minimap-editor-view.coffee
+++ lib/minimap-editor-view.coffee
@@ -271,10 +271,13 @@
             html = linesComponent.buildLineHTML(line, screenRow)
             @dummyNode.innerHTML = html
             lineElement = @dummyNode.childNodes[0]
+            unless lineElement?
+              console.warn "Unexpected undefined line element at screen row #{screenRow}"
+              continue
             classes = @lineClasses[row+1]
-            lineElement?.classList.add(classes...) if classes?
-            lineElement?.style.cssText=""
             lineElement.className = 'line'
+            lineElement.classList.add(classes...) if classes?
+            lineElement.style.cssText=""
             @lines[0].insertBefore(lineElement, currentLine)
             row++
       else