payment: add some control over the new "plans" view

--- client/app/Applications/Dashboard.kdapplication/AppController.coffee
+++ client/app/Applications/Dashboard.kdapplication/AppController.coffee
@@ -136,18 +136,43 @@

   productViewAdded: (pane, view) ->

-    { JPaymentProduct } = KD.remote.api
+    { JPaymentProduct, JPaymentPlan } = KD.remote.api

     groupsController = KD.getSingleton 'groupsController'

     group = groupsController.getCurrentGroup()

-    group.fetchProducts (err, products) ->
-      view.setProducts products
+    reloadProducts = ->
+      group.fetchProducts (err, products) -> view.setProducts products
+
+    reloadPlans = -> # noop for now

     view.on 'ProductCreateRequested', (productData) ->
       JPaymentProduct.create productData, (err) ->
-        console.log { err }
+        return if KD.showError err
+
+        reloadProducts()
+
+    view.on 'ProductDeleteRequested', (code) ->
+      JPaymentProduct.removeByCode code, (err) ->
+        return if KD.showError err
+
+        reloadProducts()
+
+    view.on 'PlanCreateRequested', (planData) ->
+      JPaymentPlan.create planData, (err) ->
+        return if KD.showError err
+
+        reloadPlans()
+
+    view.on 'PlanDeleteRequested', (code) ->
+      JPaymentPlan.removeByCode code, (err) ->
+        return if KD.showError err
+
+        reloadPlans()
+
+    reloadProducts()
+    reloadPlans()

   showPaymentInfoModal: ->
     modal = @createPaymentInfoModal()


Use log instead console.log

Use log instead console.log

--- client/landing/app/Applications/WebTerm.kdapplication/AppView.coffee
+++ client/landing/app/Applications/WebTerm.kdapplication/AppView.coffee
@@ -63,7 +63,7 @@
       @appStorage.setValue 'visualBell', false if not @appStorage.getValue('visualBell')?
       @appStorage.setValue 'scrollback', 1000 if not @appStorage.getValue('scrollback')?
       @updateSettings()
-
+
       KD.getSingleton("vmController").run
         kiteName : 'os',
         method   : 'webterm.connect',
@@ -84,7 +84,7 @@
           @terminal.server = remote
           @setKeyView()
           @emit "WebTermConnected", remote
-          console.log remote.session
+          log "Remote session:", remote.session

   destroy: ->
     super


Adds unique ids to log messages for later retrival from the queue, matches related entries together

--- javascripts/lib/logger.coffee
+++ javascripts/lib/logger.coffee
@@ -1,16 +1,17 @@
 ttm.define "logger", ['lib/class_mixer'], (class_mixer)->
   class LogEntry
     initialize: ((@level, @args)->)
-    createStrMsg: ->
+    createStrMsg: (index)->
       message = ''
       for arg in @args
         arg = JSON.stringify(arg) unless typeof arg is 'string'
         message += "#{arg} "
-      "#{@level}: #{message}"
+      "#{@levelAndEntryIndexString()} #{message}"

-    createArrMsg: ->
+    createArrMsg: (index)->
       ret = []
-      current_str = "#{@level}: "
+      current_str = @levelAndEntryIndexString()
+
       for arg in @args
         if typeof arg is 'string'
           current_str += " #{arg}"
@@ -20,7 +21,11 @@
           ret.push arg
       ret.push current_str unless current_str.length == 0
       ret
-    display: (force_string)->
+
+    levelAndEntryIndexString: ()->
+      "#{@level} (##{@index}):"
+
+    display: (force_string, @index)->
       if force_string
         [@createStrMsg()]
       else
@@ -38,7 +43,6 @@
       @log_entry_types = ['error','warn','info','debug', 'log', 'instrumented']

       @log_entry_display_types = @typesForLevel(opts.log_level || 'production')
-
       @console_log = opts.console_log
       @stringify_objects = opts.stringify_objects
       @entries = []
@@ -50,9 +54,10 @@
     add: (type, args = []) ->
       entry = LogEntry.build(type, args)
       @entries.push entry
+      entry_index = @entries.length - 1

       if @log_entry_display_types.indexOf(type) != -1
-        @console_log.apply(null, entry.display(@stringify_objects))
+        @console_log.apply(null, entry.display(@stringify_objects, entry_index))

     typesForLevel: (level)->
       switch level
@@ -70,13 +75,21 @@
     instrument: (opts)->
       __logger = @
       ->
+        id = __logger.getUniqueId()
         arr = Array.prototype.slice.call(arguments)
         arr.unshift(@)
-        arr.unshift "#{opts.name} call: "
+        arr.unshift "#{opts.name} call (id #{id}): "
         __logger.add('instrumented', arr)
         retval = opts.fn.apply(@, arguments)
-        __logger.add('instrumented', ["#{opts.name} return: ", retval])
+        __logger.add('instrumented', ["#{opts.name} return (id #{id}): ", retval])
         retval
+
+    logMethodCall: (name, object, method, args)->
+      id = @getUniqueId()
+      @info("method call (id #{id}): ", name, object, method, args)
+      ret = object[method].apply(object, args)
+      @info("method call return (id #{id}): ", name, object, method, args, ret)
+      ret

   class_mixer Logger


Cleaner controller

Cleaner controller

--- core/app/backbone/controllers/conversations_controller.coffee
+++ core/app/backbone/controllers/conversations_controller.coffee
@@ -3,16 +3,13 @@
 class window.ConversationsController
   startAction: ->
     app.closeAllContentRegions()
-    @main = new TabbedMainRegionLayout();
+    @main ?= new TabbedMainRegionLayout();
     app.mainRegion.show(@main)
-
-    return if @alreadyLoaded
-    @conversations = new Conversations()
-    @alreadyLoaded = true

   showConversations: (with_startup=true)->
     @startAction() if with_startup

+    @conversations ?= new Conversations()
     @main.showTitle "Conversations"
     @main.contentRegion.show(
       new ConversationsView collection: @conversations, loading: true


Removed unused option

Removed unused option

--- core/app/backbone/views/evidence/ndp_add_evidence_view.coffee
+++ core/app/backbone/views/evidence/ndp_add_evidence_view.coffee
@@ -67,4 +67,3 @@
       collection: @collection.oneSidedEvidenceCollection(type)
       fact_id: @options.fact_id
       type: type
-      ndp: true


fake activity: added fake activity feed fetcher methods

--- client/landing/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppController.coffee
@@ -151,7 +151,67 @@

       eventSuffix = "#{@getFeedFilter()}_#{@getActivityFilter()}"

-      if @getFeedFilter() is "Public"
+      group = KD.getSingleton('groupsController').getCurrentGroup().slug
+      {roles} = KD.config
+
+      @on "StartFetchingFakeActivityComment", (activityId, commentId)=>
+        return if commentId > 2
+        timeoutValue = parseInt(Math.random()*100000,10)
+        setTimeout =>
+            eventName = "activity_#{activityId}_C#{commentId}_fetch"
+            @off "#{eventName}_succcess"
+
+            # if fetching comments fails, then shut down the event
+            @once "#{eventName}_fail", ()=>
+              @off "activity_#{activityId}_fetch_success"
+
+            # if fetching success, add them to activity feed
+            @once eventName, (activities)=>
+              @listActivities activities, callback, true
+              @emit "StartFetchingFakeActivityComment", activityId, commentId+1
+            @getFakes(activityId, commentId)
+
+          , timeoutValue
+
+      @on "StartFetchingFakeActivity", (activityId=0)=>
+        return if activityId > 7
+        @isLoading = true
+        timeoutValue = parseInt(Math.random()*10000,10)
+
+        eventName = "activity_#{activityId}_fetch"
+        @off "#{eventName}_succcess"
+
+        setTimeout =>
+            @once "#{eventName}_fail", ()=>
+              @off "#{eventName}_succcess"
+              @emit "StartFetchingFakeActivity", activityId+1
+
+            @once "#{eventName}_success", (activities)=>
+              @listActivities activities, (sanitizedCache)=>
+                callback sanitizedCache
+                @emit "StartFetchingFakeActivityComment", activityId, 1
+
+              @emit "StartFetchingFakeActivity", activityId+1
+            #fetch fake activity
+            @getFakes(activityId)
+
+          , timeoutValue
+
+
+      if group == "koding" and  "guest" in roles
+        # this is a hack, find a better way to hide inner navigation
+        $('.common-inner-nav').hide()
+        eventName = "activity_fetch"
+        @once "#{eventName}_fail", ()->
+          console.log "ERR : static main page activities will not work"
+
+        @once "#{eventName}_success", (activities)=>
+          activities.overview.reverse()  if activities.overview
+          @listActivities activities, callback
+          @emit "StartFetchingFakeActivity", 1
+        @getFakes()
+
+      else if @getFeedFilter() is "Public"
         @once "publicFeedFetched_#{eventSuffix}", (cache)=>
           reset()
           @extractCacheTimeStamps cache
@@ -173,6 +233,28 @@
     if isReady
     then fetch()
     else groupsController.once 'groupChanged', fetch
+
+  listActivities:(activities, callback, update=false)->
+    @sanitizeCache activities, (err, sanitizedCache)=>
+      @extractCacheTimeStamps sanitizedCache
+      @listController.listActivitiesFromCache sanitizedCache, 0 , {type : "slideDown", duration : 100}, update
+      callback sanitizedCache
+
+  getFakes:(activityId=0, commentId=0, likeId=0)->
+    activityName = "activity"
+    activityName += "_#{activityId}" unless activityId is 0
+    activityName += "_C#{commentId}" unless commentId is 0
+    activityName += "_L#{commentId}" unless likeId is 0
+
+    $.ajax({
+      type: "GET"
+      url: "js/activity/#{activityName}.json"
+      dataType: "json"
+      cache: "false"
+    }).done (json) =>
+      @emit "#{activityName}_fetch_success", json
+    .fail ()=>
+      @emit "#{activityName}_fetch_fail"

   fetchPublicActivities:(options = {})->
     {CStatusActivity} = KD.remote.api


[DONE] prompt file rename on file creation

--- client/landing/app/MainApp/filetree/controllers/findertreecontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/findertreecontroller.coffee
@@ -289,8 +289,10 @@
     FSItem.create path, type, (err, file)=>
       @notify null, null, err if err
       @refreshFolder @nodes[parentPath], =>
-        @selectNode @nodes[file.path]
         @notify "#{type} created!", "success"
+        node = @nodes[file.path]
+        @selectNode node
+        @showRenameDialog node

   moveFiles:(nodesToBeMoved, targetNodeView, callback)->


[Filetree] Change timeout to 10000

[Filetree] Change timeout to 10000

--- client/landing/app/MainApp/filetree/controllers/findertreecontroller.coffee
+++ client/landing/app/MainApp/filetree/controllers/findertreecontroller.coffee
@@ -158,7 +158,7 @@
         @hideNotification()
       else
         failCallback err
-    , failCallback), no
+    , failCallback, 10000), no

   collapseFolder:(nodeView, callback, silence=no)->


edison.coffee: Set initial versioning (v1)

Signed-off-by: Florin Sarbu <3a837a37a2ac9072a088641ec330e0e5d5b86f4e@resin.io>

--- edison.coffee
+++ edison.coffee
@@ -40,6 +40,7 @@
 '''

 module.exports =
+	version: 1
 	slug: 'intel-edison'
 	aliases: [ 'edison' ]
 	name: 'Intel Edison'


[track] added analytics file to include file

--- client/landing/includes.coffee
+++ client/landing/includes.coffee
@@ -699,6 +699,7 @@
   "app/MainApp/monitor_status.coffee",
   "app/MainApp/rollbar.coffee",
   "app/MainApp/mixpanel.coffee",
+  "app/MainApp/analytic.coffee",

   # --- Styles ---
   "css/style.css",


Delayed raw rez a little more

Delayed raw rez a little more

--- client/terrain/streamer.coffee
+++ client/terrain/streamer.coffee
@@ -173,7 +173,7 @@
   resetRawRez: ->
     @rawRez = null
     clearTimeout(@rawRezTimer)
-    @rawRezTimer = setTimeout((=> @loadRawRez()), 2300)
+    @rawRezTimer = setTimeout((=> @loadRawRez()), 3000)
   loadRawRez: ->
     @rawRez = new TerrainRawRez @bounds, @pxWidth, => @update()
     @loadExtended(1.0, 1.0, {shading: true})


Revert ":penguin: Use ctrl-f3 for use-selection-as-find-pattern"

This reverts commit e62ab33dd3787fb52e3626f4304da0d72977b3a7.

Go back to using ctrl-e see #202 for more details.

--- keymaps/find-and-replace.cson
+++ keymaps/find-and-replace.cson
@@ -24,7 +24,8 @@
 '.platform-linux .editor':
   'f3': 'find-and-replace:find-next'
   'shift-f3': 'find-and-replace:find-previous'
-  'ctrl-f3': 'find-and-replace:use-selection-as-find-pattern'
+
+  'ctrl-e': 'find-and-replace:use-selection-as-find-pattern'

 '.platform-darwin .find-and-replace':
   'cmd-enter': 'find-and-replace:replace-all'


implemented repo selection by label

implemented repo selection by label

Signed-off-by: Dominik Richter <b7323add517e187efe0283b76511b7ed762bfe13@gmail.com>

--- coffee/controllers/main.coffee
+++ coffee/controllers/main.coffee
@@ -24,6 +24,7 @@
     $scope.packageSelected = {}
     $scope.packageFetching = {}
     $scope.repoSelected = null
+    $scope.repoSelectedLabel = null
     $scope.newRepos = []
     $scope.editingRepo = {}

@@ -87,8 +88,12 @@
           # if we got a result and want to continue
           # then try fetching the next page
           if not stop_when(data)
-            o = _.merge( (opts.query || {}),
-              {'stop_when':stop_when, 'page':page + 1, 'limit': limit })
+            o = {
+              'stop_when':stop_when,
+              'page':page + 1,
+              'limit': limit,
+              'query': (opts.query || {})
+            }
             fetchAllPaginated(field, action, o)
         # error handling
         , () ->
@@ -115,6 +120,8 @@
       # If a repo was selected, filter by it
       if $scope.repoSelected?
         query = { 'outdated':true, 'repo': $scope.repoSelected.id }
+      else if $scope.repoSelectedLabel?
+        query = { 'outdated':true, 'repolabel': $scope.repoSelectedLabel }
       else
         query = {}

@@ -336,10 +343,24 @@
       if m? and m.id?
         console.log("repo #{m.name} (#{m.id}) selected")
         $scope.repoSelected = m
+        # deselect repo selection by Label
+        $scope.repoSelectedLabel = null
       else
         console.log("repo deselected")
         $scope.repoSelected = null
+        $scope.repoSelectedLabel = null
       # update the list of packages with the selected repo
+      $scope.fetchPackages()
+
+    $scope.selectRepoLabel = (label)->
+      if label? and label isnt ''
+        console.log("selecting repo label: #{label}")
+        $scope.repoSelectedLabel = label
+        # deselect repo selection by ID
+        $scope.repoSelected = null
+      # update the list of packages with the selected repo
+      console.log "-- fetch packages label: #{$scope.repoSelectedLabel}"
+      console.log "-- fetch packages id   : #{$scope.repoSelected}"
       $scope.fetchPackages()

     $scope.isPackageOutdated = (p)->


Check for, and deny anonymous api_keys

Check for, and deny anonymous api_keys

--- routes/api/v2/authentication.coffee
+++ routes/api/v2/authentication.coffee
@@ -8,8 +8,10 @@
       if (key = @params.query?['api-key']) or (key = @request.headers?['api-key'])
         if (apiKey = APIKeys.findOne key: key)
           user = Meteor.users.findOne(apiKey.owner)
-          # @authenticatedUSer = user
-          @next()
+          if user
+            @next()
+          else
+            lib.endWithError @response, 401, "Not authenticated"
         else if(key in keys())
           user = "deploy"
           @next()


try reconnecting to campfire on a per room basis when streams close

--- src/adapters/campfire.coffee
+++ src/adapters/campfire.coffee
@@ -55,6 +55,10 @@
           bot.Room(roomId).join (err, callback) ->
             bot.Room(roomId).listen()

+    bot.on "reconnect", (roomId) =>
+      bot.Room(roomId).join (err, callback) ->
+        bot.Room(roomId).listen()
+
     @bot = bot

 exports.use = (robot) ->
@@ -159,8 +163,10 @@
                   @robot.logger.error "Campfire error: #{err}"

         response.on "end", =>
-          @robot.logger.error "Streaming connection closed. :("
-          process.exit(1)
+          @robot.logger.error "Streaming connection closed for room #{id}. :("
+          setTimeout (->
+            self.emit "reconnect", id
+          ), 5000

         response.on "error", (err) =>
           @robot.logger.error "Campfire response error: #{err}"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/camera.coffee
+++ test/camera.coffee
@@ -20,6 +20,6 @@

   camera = Camera()

-  camera.trigger 'overlay',
+  camera.trigger 'overlay', canvas, [object]
 # Clear out the module
 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color2.coffee
+++ test/color2.coffee
@@ -8,14 +8,14 @@
   equal color.b, 0
   equal color.a, 1

-test "accepts a single array of length 3 in the constructor", ->
+test "accepts a single array of length 3", ->
   color = Color2([255, 1, 2])

   equal color.r, 255
   equal color.g, 1
   equal color.b, 2

-test "accepts a single array of length 4 in the constructor", ->
+test "accepts a single array of length 4", ->
   color = Color2([255, 4, 3, 0.8])

   equal color.r, 255
@@ -23,4 +23,12 @@
   equal color.b, 3
   equal color.a, 0.8

+test "accepts an array as the first argument and an alpha value as the second", ->
+  color = Color2([100, 5, 3], 0.4)
+
+  equal color.r, 100
+  equal color.g, 5
+  equal color.b, 3
+  equal color.a, 0.4
+
 module()


fix spec that tests title

fix spec that tests title

--- test/components/navbar/server-error-monitoring.spec.coffee
+++ test/components/navbar/server-error-monitoring.spec.coffee
@@ -7,7 +7,7 @@
 ErrorMonitor = require '../../../src/components/navbar/server-error-monitoring'

-describe 'Server Error Monitoring', ->
+describe 'Error display messages', ->

   beforeEach ->
     sinon.spy(Dialog, 'show')
@@ -15,19 +15,19 @@
     Dialog.show.restore()

   it 'renders error dialog', ->
-    AppActions.setServerError(500, 'an error happens',
-      {url: '/test', displayError: true})
     Testing.renderComponent( ErrorMonitor ).then ({dom}) ->
       expect(Dialog.show).to.have.been.calledWith(sinon.match(
         title: 'Server Error'
       ))
+    AppActions.setServerError(500, 'an error happens',
+      {url: '/test', opts: {displayError: true}})

   it 'renders a no_exercises message', ->
+    Testing.renderComponent( ErrorMonitor ).then ({dom}) ->
+      expect(Dialog.show).to.have.been.calledWith(sinon.match(
+        title: 'No exercises are available'
+      ))
     AppActions.setServerError(422,
       '{"status":422,"errors":[{"code":"no_exercises","message":' +
       '"No exercises were found to build the Practice Widget.","data":null}]}'
-      , {url: '/test', displayError: true})
-    Testing.renderComponent( ErrorMonitor ).then ({dom}) ->
-      expect(Dialog.show).to.have.been.calledWith(sinon.match(
-        title: 'Unable to practice topic'
-      ))
+      , {url: '/test', opts: {displayError: true}})


fix autocomplete and specs

fix autocomplete and specs

--- src/autocomplete-view.coffee
+++ src/autocomplete-view.coffee
@@ -106,7 +106,6 @@

   setPosition: ->
     { left, top } = @editor.pixelPositionForScreenPosition(@originalCursorPosition)
-
     height = @outerHeight()
     potentialTop = top + @editor.lineHeight
     potentialBottom = potentialTop - @editor.scrollTop() + height


Matrix copy constructor

Matrix copy constructor

--- test/matrix.coffee
+++ test/matrix.coffee
@@ -15,6 +15,8 @@
     equalEnough(m1.tx, m2.tx, TOLERANCE)
     equalEnough(m1.ty, m2.ty, TOLERANCE)

+  t
+
   test "Matrix() (Identity)", ->
     matrix = Matrix()


fix scope for pdf page load error handler

--- services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
@@ -229,7 +229,7 @@
 						pageState.complete = false
 						delete pageState.renderTask
 						@removeCompletedJob pagenum
-				.catch (error) ->
+				.catch (error) =>
 					# page load error
 					$timeout.cancel(timer)
 					@clearIndicator page


fix browse endpoint

fix browse endpoint

--- apps/browse/client.coffee
+++ apps/browse/client.coffee
@@ -5,6 +5,7 @@
 FilterArtworks = require '../../collections/filter_artworks.coffee'
 FilterView = require '../../components/filter2/view.coffee'
 FilterRouter = require '../../components/filter2/router/index.coffee'
+{ FILTER_ROOT } = require('sharify').data

 module.exports.init = ->
   queryParams = qs.parse(location.search.replace(/^\?/, ''))
@@ -18,7 +19,7 @@

   router = new FilterRouter
     params: params
-    urlRoot: 'browse'
+    urlRoot: FILTER_ROOT

   collection.fetch
     data: params.toJSON()


Sometimes bower list can take a long time during tests.

Let's update the timeout to 15s.

--- test/runner.coffee
+++ test/runner.coffee
@@ -20,7 +20,7 @@
     ui: "bdd"
     reporter: "spec"
     slow: 6000
-    timeout: 10000
+    timeout: 15000

   runner.files = all_specs()


Reset undo state when switching documents

--- services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
@@ -14,6 +14,7 @@
 			@nextUpdateIsRemote = false

 			@editor.on "changeSession", (e) =>
+				@reset()
 				e.session.setUndoManager(@)

 		showUndoConflictWarning: () ->
@@ -26,10 +27,16 @@
 			, 4000

 		reset: () ->
+			@firstUpdate = true
 			@undoStack = []
 			@redoStack = []

 		execute: (options) ->
+			if @firstUpdate
+				# The first update we receive is Ace setting the document, which we should
+				# ignore
+				@firstUpdate = false
+				return
 			aceDeltaSets = options.args[0]
 			@session = options.args[1]
 			return if !aceDeltaSets?


Add in extra sentry logging for undefined start in aceDelta


refactor out renderReplyButton.

refactor out renderReplyButton.

Reply buttons very same-y

--- resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
+++ resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
@@ -92,34 +92,19 @@
                   osu.trans('beatmaps.discussions.resolved')
           div className: "#{bn}__actions-group",
             if @canResolve() && !@props.discussion.resolved
-              div className: "#{bn}__action",
-                el BigButton,
-                  text: osu.trans('common.buttons.reply_resolve')
-                  icon: if @state.posting then 'ellipsis-h' else 'reply'
-                  props:
-                    'data-action': 'resolve'
-                    disabled: !@validPost() || @state.posting?
-                    onClick: @throttledPost
+              @renderReplyButton
+                text: osu.trans('common.buttons.reply_resolve')
+                extraProps:
+                  'data-action': 'resolve'

             if @canResolve() && @props.discussion.resolved
-              div className: "#{bn}__action",
-                el BigButton,
-                  text: osu.trans('common.buttons.reply_reopen')
-                  icon: if @state.posting then 'ellipsis-h' else 'reply'
-                  props:
-                    'data-action': 'reopen'
-                    disabled: !@validPost() || @state.posting?
-                    onClick: @throttledPost
-
-            div className: "#{bn}__action",
-              el BigButton,
-                text: osu.trans('common.buttons.reply')
-                # wobbles if using spinner
-                icon: if @state.posting then 'ellipsis-h' else 'reply'
-                props:
-                  disabled: !@validPost() || @state.posting?
-                  onClick: @throttledPost
-
+              @renderReplyButton
+                text: osu.trans('common.buttons.reply_reopen')
+                extraProps:
+                  'data-action': 'reopen'
+
+            @renderReplyButton
+              text: osu.trans('common.buttons.reply')

   renderPlaceholder: =>
     [text, icon] =
@@ -138,6 +123,20 @@
           onClick: @editStart

+  renderReplyButton: ({ text, extraProps = {} }) =>
+    props = _.extend
+      disabled: !@validPost() || @state.posting?
+      onClick: @throttledPost,
+      extraProps
+
+    div className: "#{bn}__action",
+      el BigButton,
+        text: text
+        # wobbles if using spinner
+        icon: if @state.posting then 'ellipsis-h' else 'reply'
+        props: props
+
+
   canResolve: =>
     @props.discussion.can_be_resolved && @props.discussion.current_user_attributes.can_resolve


fix test

fix test

--- exercises/bob/bob.spec.coffee
+++ exercises/bob/bob.spec.coffee
@@ -14,11 +14,11 @@
     expect(result).toEqual 'Sure.'

   xit 'talking forcefully', ->
-    result = bob.hey 'Let's go make out behind the gym!'
+    result = bob.hey 'Let\'s go make out behind the gym!'
     expect(result).toEqual 'Whatever.'

   xit 'using acronyms in regular speech', ->
-    result = bob.hey 'It's OK if you don't want to go to the DMV.'
+    result = bob.hey 'It\'s OK if you don\'t want to go to the DMV.'
     expect(result).toEqual 'Whatever.'

   xit 'forceful questions', ->


Added canceable to android loading indicator.

--- Framework/Views/Controls/LoadingIndicatorView_Framework.coffee
+++ Framework/Views/Controls/LoadingIndicatorView_Framework.coffee
@@ -24,11 +24,10 @@
       @add(@loadingView)
     else if Ti.Platform.osname == "android"
       @activityIndicator = Ti.UI.createActivityIndicator({
-        top: 200
-        message: "Loading..."
-        width: Ti.UI.SIZE
-        height: Ti.UI.SIZE
+        message: "Loading Properties..."
+        width: Ti.UI.SIZE, height: Ti.UI.SIZE
         color: "#333"
+        cancelable: true
         font: { fontSize: 14, fontWeight: 'bold' }
       })
       @add(@activityIndicator)


Rationalize things a bit

Rationalize things a bit

--- src/coffee/directives.coffee
+++ src/coffee/directives.coffee
@@ -222,9 +222,8 @@
     scope:
       tool:     '='
       step:     '='
-      fullSize: '='
       state:    '='
-      onToggle: '&'
+      toggle:   '&'
       hasItems: '&'
       has:      '&'
       wants:    '&'
@@ -232,8 +231,8 @@
     template: """

+           ng-click="toggle()"
+           ng-class="{'fa-compress': state.expanded, 'fa-expand': !state.expanded}">
         {{tool.heading}}

     """


Input

Input

- disable autocomplete on shadow elements

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -456,7 +456,7 @@
 		if @__contentSize
 			return

-		@__contentSize = $element("textarea", "cui-input-shadow", tabindex: "-1")
+		@__contentSize = $element("textarea", "cui-input-shadow", tabindex: "-1", autocomplete: "off")

 		@__contentSize.appendTo(document.body)
 		@__contentSize0 = @__contentSize[0]
@@ -784,6 +784,7 @@
 			@__shadow = $element("input", "cui-input-shadow", type: "text")

 		@__shadow.prop("tabindex", "-1")
+		@__shadow.prop("autocomplete", "off")
 		@__shadow.appendTo(document.body)
 		@__shadow0 = @__shadow[0]


CUI: table: add zebra option

CUI: table: add zebra option

--- src/elements/Table/Table.coffee
+++ src/elements/Table/Table.coffee
@@ -24,6 +24,10 @@

 		if @_bordered
 			cls.push("cui-table--bordered")
+
+		if @_zebra
+			cls.push("cui-table--zebra")
+

 		@__table = CUI.dom.table(cls.join(" "))

@@ -70,6 +74,9 @@
 				default: true
 				check: Boolean
 			bordered:
+				default: false
+				check: Boolean
+			zebra:
 				default: false
 				check: Boolean
 			columns:


fix scrolling on open in cc

fix scrolling on open in cc

--- src/concept-coach/index.cjsx
+++ src/concept-coach/index.cjsx
@@ -46,9 +46,11 @@

     @component

-  handleOpened: (eventData, scrollTo = window.scrollTo, body = document.body) ->
+  handleOpened: (eventData, scrollTo, body = document.body) ->
+    scrollTo ?= _.partial(window.scrollTo, 0)
     {top} = eventData.coach.el.getBoundingClientRect()
-    scrollTo(0, top)
+    top +=  window.scrollY
+    scrollTo(top)
     body.classList.add('cc-opened')

   handleClosed: (eventData, body = document.body) ->


Make dump() log to the browser console as well

--- src/ext/modules/routines.coffee
+++ src/ext/modules/routines.coffee
@@ -975,7 +975,9 @@
       .fail onFail

   dumpFuture = (result, go) ->
-    go null, render_ (result or {}), -> Flow.ObjectBrowser 'dump', result
+    debug result
+    go null, render_ (result or {}), ->
+      Flow.ObjectBrowser 'dump', result

   dump = (f) ->
     if f?.isFuture


typo. include webpack

typo. include webpack

--- webpack.config.coffee
+++ webpack.config.coffee
@@ -1,3 +1,4 @@
+webpack = require 'webpack'
 ExtractTextPlugin = require 'extract-text-webpack-plugin'

 isProduction = process.env.NODE_ENV is 'production'


Don't include more build files

Don't include more build files

--- build/tasks/build-task.coffee
+++ build/tasks/build-task.coffee
@@ -52,6 +52,9 @@
       path.join('jasmine-node', 'node_modules', 'gaze')
       path.join('jasmine-node', 'spec')
       path.join('nan', 'nan.h')
+      path.join('build', 'binding.Makefile')
+      path.join('build', 'config.gypi')
+      path.join('build', 'gyp-mac-tool')
       path.join('build', 'Release', 'obj.target')
       path.join('build', 'Release', 'obj')
       path.join('build', 'Release', '.deps')


DOM merge functionality correct.

DOM merge functionality correct.

--- js/binding/src/binding.coffee
+++ js/binding/src/binding.coffee
@@ -6,16 +6,20 @@

 ((($) ->
 	$.fn.merge = (doms) ->
-		doms.each (i, dom) =>
+		doms.filter("[id]").each (i, dom) =>
 			dom = $(dom)
-			child = this.children("#" + dom.attr("id"))
+			id = dom.attr("id")
+			child = this.children("#" + id)
 			if child.length is 0
-				this.append(dom.remove())
+				this.append(dom)
 			else
 				child.merge(dom.children())
-				# dom.children removed, replace kids
-				this.children(":not([id])").remove()
-				this.append(dom.children().remove())
+			null
+		rest = doms.filter(":not([id])")
+		#[id]s removed, replace kids
+		if rest.length
+			this.children(":not([id])").remove()
+			this.append(rest)
 		this
 ).call(this, jQuery))

@@ -92,9 +96,9 @@

 		view: (root = settings.paths.root, theme = settings.paths.theme, entity = "_default_entity", property = "?", view = "view") ->
 			_property = find.property(root, theme, entity, property)
-			_view = _property.find("." + view)
+			_view = _property.find("#" + view)
 			if _view.length isnt 1
-				_view = _property.find(".view");
+				_view = _property.find("#view");
 			_view.clone()
 	})


Add player sprites to the js-compiler

Add player sprites to the js-compiler

--- src/js/compilers/js/main.coffee
+++ src/js/compilers/js/main.coffee
@@ -23,6 +23,33 @@

 		colors.forEach (color) ->
 			push "#{compileColor color},"
+			return
+
+		push "}"
+
+		lines.join '\n'
+
+
+	compilePlayerSprite = ({ name, data }) ->
+		colorData = for line in data
+			for color in line
+				"colors['#{color}']"
+
+		"""
+			'#{name}': {
+				data: [#{colorData}],
+			}
+		"""
+
+
+	compilePlayerSprites = (playerSprites) ->
+		lines = []
+		push = lines.push.bind lines
+
+		push "var player = {"
+
+		playerSprites.forEach (playerSprite) ->
+			push "#{compilePlayerSprite playerSprite},"
 			return

 		push "}"
@@ -104,9 +131,11 @@

 		"""
 			#{compileColors spec.colors}
+			#{compilePlayerSprites spec.player}
 			#{compileObjects spec.objects}

-			var sprites = {}
+			var objectSprites = {}
+			var playerSprites = {}

 			var params = {
 				scale: #{params['scale'][0]},


engine#add

engine#add

--- src/engine_game_state.coffee
+++ src/engine_game_state.coffee
@@ -32,8 +32,7 @@
     I.currentState.trigger "draw", canvas
     I.currentState.trigger "overlay", canvas

-  # We must always return self as the last line
-  return
+
     # Just pass through to the current state
     add: (className, entityData) ->
       # Allow optional add "Class", data form


Link to CC help if the current course isn't Tutor

Or unless the user has any Tutor courses

--- src/flux/current-user.coffee
+++ src/flux/current-user.coffee
@@ -147,7 +147,12 @@

     getHelpLink: (courseId) ->
       course = CourseStore.get(courseId)
-      if course?.is_concept_coach then CONCEPT_COACH_HELP else TUTOR_HELP
+      if course
+        if course.is_concept_coach then CONCEPT_COACH_HELP else TUTOR_HELP
+      else
+        courses = CourseStore.allCourses()
+        # link to TUTOR_HELP if they have any tutor courses
+        if _.findWhere(courses, is_concept_coach: false) then TUTOR_HELP else CONCEPT_COACH_HELP

     # if menu routes are being retrieved, then getCourseRole should store
     # what courseId is being viewed.


fix: unset the css properties set on the tree view

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -59,6 +59,10 @@
       @applyMinimizedWidth()

   deactivate: ->
+    if treeViewEl = atom.views.getView treeView
+      treeViewEl.style.transitionDelay = ''
+      treeViewEl.style.width = ''
+      treeViewEl.parentNode.style.width = ''
     @subs.dispose()

   show: (noDelay) ->


fixed issue where  was bound to window

fixed issue where  was bound to window

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -37,10 +37,10 @@
       minimum: 1

   activate: ->
-    @unfoldSpeedSub = atom.config.observe 'autohide-tree-view.unfoldSpeed', @applyUnfoldSpeed
-    @hideDelaySub = atom.config.observe 'autohide-tree-view.hideDelay', @applyHideDelay
-    @minWidthSub = atom.config.observe 'autohide-tree-view.minimizedWidth', @applyMinWidth
-    @treeViewShowOnRightSideSub = atom.config.observe 'tree-view.showOnRightSide', @applyTreeViewSide
+    @unfoldSpeedSub = atom.config.observe 'autohide-tree-view.unfoldSpeed', @applyUnfoldSpeed.bind @
+    @hideDelaySub = atom.config.observe 'autohide-tree-view.hideDelay', @applyHideDelay.bind @
+    @minWidthSub = atom.config.observe 'autohide-tree-view.minimizedWidth', @applyMinWidth.bind @
+    @treeViewShowOnRightSideSub = atom.config.observe 'tree-view.showOnRightSide', @applyTreeViewSide.bind @

   deactivate: ->
     @unfoldSpeedSub.dispose()


model_postgres: fix SQL INTERVAL syntax

model_postgres: fix SQL INTERVAL syntax

--- src/local/model_postgres.coffee
+++ src/local/model_postgres.coffee
@@ -668,7 +668,7 @@
                             COUNT(user) AS count
                      FROM subscriptions
                      WHERE node LIKE $2
-                       AND updated >= CURRENT_TIMESTAMP - INTERVAL $1
+                       AND updated >= CURRENT_TIMESTAMP - $1 :: INTERVAL
                      GROUP BY node
                      ORDER BY count DESC
                      LIMIT $3"""
@@ -679,7 +679,7 @@
                             COUNT(xml) AS count
                      FROM items
                      WHERE node LIKE $2
-                       AND updated >= CURRENT_TIMESTAMP - INTERVAL $1
+                       AND updated >= CURRENT_TIMESTAMP - $1 :: INTERVAL
                      GROUP BY node
                      ORDER BY count DESC
                      LIMIT $3"""


tmp commit

tmp commit

--- js/core.coffee
+++ js/core.coffee
@@ -6,8 +6,15 @@
 ###
 richisCore =
 	init: ->
+		# loading data from the server
+		@loadSections()
+		# prevent default all document
 		$(document).on "click" , "a" , (e)-> e.preventDefault()
-		@loadSections()
+		# main navegation
+		@navElements = $("#navigation")
+		@navElements.bind "click" , @navBehavior
+	navBehavior: (e) ->
+		console.log @
 	richistoken: -> Math.random().toString(36).substr(2)
 	loadSections: ->
 		$.ajax(
@@ -19,12 +26,7 @@
 		).done (data) =>
 			@initSections data
 	initSections: (@response = false)->
-		console.log @response
-		@navElements = $("#navigation")
-		@navElements.on "click" , "a" , (e)->
-			e.preventDefault()
-			console.log @
-			$(@).unbind()
+		console.log @response
 ###
 <div id="container">
 <section>


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -74,6 +74,7 @@
   Math.round(this)

 ###*
+
 ###
 Number::circularPoints = (n, block) ->
   n.times (i) ->


Only add trailing slash when needed

Only add trailing slash when needed

--- src/package-converter.coffee
+++ src/package-converter.coffee
@@ -33,7 +33,7 @@
     {protocol} = url.parse(@sourcePath)
     if protocol is 'http:' or protocol is 'https:'
       tempPath = temp.mkdirSync('atom-bundle-')
-      request("#{@sourcePath}/archive/master.tar.gz")
+      request(@getDownloadUrl())
         .pipe(zlib.createGunzip())
         .pipe(tar.Extract(path: tempPath))
         .on 'error', (error) -> callback(error)
@@ -42,6 +42,11 @@
           @copyDirectories(sourcePath, callback)
     else
       @copyDirectories(@sourcePath, callback)
+
+  getDownloadUrl: ->
+    downloadUrl = @sourcePath
+    downloadUrl += '/' unless downloadUrl[downloadUrl.length - 1] is '/'
+    downloadUrl += 'archive/master.tar.gz'

   copyDirectories: (sourcePath, callback) ->
     for directoryName in @directoryNames


fixed sourcemaps when compress is true

fixed sourcemaps when compress is true

--- lib/coffee-autocompile.coffee
+++ lib/coffee-autocompile.coffee
@@ -32,18 +32,19 @@

     console.log renderer

+    filePath = path.resolve path.dirname(params.file), params.out
     if params.sourcemap
       {js, v3SourceMap} = renderer
-      js += "\n\n//# sourceMappingURL=#{params.out}.map"
+      js += "\n//# sourceMappingURL=#{filePath}.map"
+      if params.compress
+        {code: js, map: v3SourceMap} = uglify.minify js, fromString: true, inSourceMap: JSON.parse(v3SourceMap), outSourceMap: "#{filePath}.map"
     else
       js = renderer
-    if params.uglify
-      js = uglify.minify(js, fromString: true).code
+      if params.compress
+        js = uglify.minify(js, fromString: true).code

-    filePath = path.resolve path.dirname(params.file), params.out
     @writeFile filePath, js
-    if v3SourceMap?
-      @writeFile "#{filePath}.map", v3SourceMap
+    @writeFile "#{filePath}.map", v3SourceMap if v3SourceMap

   getParams: (filePath, fileContent) ->
     serialized = @getSerializedParams fileContent
@@ -82,6 +83,7 @@
         atom.notifications.addSuccess "#{filePath} created"

   parseBoolean: (value) ->
-    value is 'true' or value is 'yes' or value is 1
+    (value is 'true' or value is 'yes' or value is 1) and
+      value isnt 'false' and value isnt 'no' and value isnt 0

 module.exports = new StylusAutocompile()


Prepare for language manager

Prepare for language manager

--- app/main.coffee
+++ app/main.coffee
@@ -10,7 +10,20 @@
   domain: 'milkywayproject.org'

 t7e = require 't7e'
-t7e.load require './lib/en-us'
+enUs = require './lib/en-us'
+
+t7e.load enUs
+
+# LanguageManager = require 'zooniverse/lib/language-manager'
+# languageManager = new LanguageManager
+#   translations:
+#     en: label: 'English', strings: enUs
+#     fr: label: 'Français', strings: './translations/fr.json'
+#     pl: label: 'Polski', strings: './translations/pl.json'
+
+# languageManager.on 'change-language', (e, code, languageStrings) ->
+#   t7e.load languageStrings
+#   t7e.refresh()

 SiteHeader = require './controllers/site-header'
 siteHeader = new SiteHeader


remove test

remove test

--- websockets/websockets.coffee
+++ websockets/websockets.coffee
@@ -25,8 +25,6 @@
             if socket.user.teamId
                 @teams[socket.user.teamId] = {} if not @teams[socket.user.teamId]
                 @teams[socket.user.teamId][socket.id] = socket
-
-            @sendToTeam id: 1, 'message', Object.keys(@users[socket.user.id])

             socket.on 'disconnect', =>
                 delete @users[socket.user.id][socket.id]


Projection point

Projection point

--- lib/assets/javascripts/socmap_adf/modules/gmap/views/context_menu.js.coffee
+++ lib/assets/javascripts/socmap_adf/modules/gmap/views/context_menu.js.coffee
@@ -9,6 +9,9 @@
     @gElement = options.gElement
     @map = options.mapModel
     @eventBus.on "adf.hideContextMenu", @hide
+    @overlay = new google.maps.OverlayView()
+    @overlay.draw = () ->
+    @overlay.setMap(@map.getGMap())

   render: () ->
     newElement = @make("div", {"class": "map_context_menu", "style" : "display:none;position:absolute;z-index:10;"} )
@@ -29,10 +32,8 @@
   onRightClicked: (e) =>
     @eventBus.trigger "adf.hideContextMenu"
     @hide()
-    overlay = new google.maps.OverlayView()
-    overlay.draw = () ->
-    overlay.setMap(@map.getGMap())
-    point = overlay.getProjection().fromLatLngToDivPixel(e.latLng)
+
+    point = @overlay.getProjection().fromLatLngToContainerPixel(e.latLng)
     @position = point
     @latLng = e.latLng
     @show()


Update number.coffee

Update number.coffee

If 'current' happens to equal 'last' then print no arrow.

--- widgets/number/number.coffee
+++ widgets/number/number.coffee
@@ -13,7 +13,11 @@

   @accessor 'arrow', ->
     if @get('last')
-      if parseInt(@get('current')) > parseInt(@get('last')) then 'icon-arrow-up' else 'icon-arrow-down'
+      if parseInt(@get('current')) > parseInt(@get('last'))
+        'icon-arrow-up'
+      else if parseInt(@get('current')) < parseInt(@get('last'))
+        'icon-arrow-down'
+      else ''

   onData: (data) ->
     if data.status


stopped make-server and about from being sent to the server when the client types them

--- src/remote_connection.coffee
+++ src/remote_connection.coffee
@@ -154,7 +154,7 @@

   broadcastUserInput: (userInput) ->
     userInput.on 'command', (event) =>
-      unless event.name in ['network-info', 'join-server']
+      unless event.name in ['network-info', 'join-server', 'make-server', 'about']
         @_broadcast 'user_input', event

   broadcastSocketData: (socket, server) ->


add Spline dashed line support

add Spline dashed line support

--- src/renderer/Renderer.coffee
+++ src/renderer/Renderer.coffee
@@ -381,7 +381,12 @@
 						shape.vertices[i].x,
 						shape.vertices[i].y
 					)
-			@context.stroke()
+			if shape.dashStyle and @context.setLineDash?
+				@context.setLineDash shape.dashStyle
+				@context.stroke()
+				@context.setLineDash [] # clear dashStyle
+			else
+				@context.stroke()
 		@


FIX: Fix scoping issue in staff task list

--- src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.coffee
+++ src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.coffee
@@ -32,7 +32,7 @@
       taskDefinition: null
     }, $scope.filters)
     # Sets the new filteredTasks variable
-    applyFilters = ->
+    $scope.applyFilters = ->
       filteredTasks = $filter('tasksOfTaskDefinition')($scope.tasks, $scope.filters.taskDefinition)
       filteredTasks = $filter('tasksInTutorials')(filteredTasks, $scope.filters.tutorials)
       filteredTasks = $filter('tasksWithStudentName')(filteredTasks, $scope.filters.studentName)
@@ -43,7 +43,7 @@
         $scope.setSelectedTask(null)
       if filteredTasks && filteredTasks.length && !$scope.taskData.selectedTask
         $scope.setSelectedTask(filteredTasks[0])
-
+
     # Let's call having a source of tasksForDefinition plus having a task definition
     # auto-selected with the search options open task def mode -- i.e., the mode
     # for selecting tasks by task definitions
@@ -77,7 +77,7 @@
       else
         $scope.filters.tutorials = [$scope.unit.tutorialFromId(tutorialId)]
       $scope.filters.tutorials = _.map($scope.filters.tutorials, 'id')
-      applyFilters()
+      $scope.applyFilters()
     # Set initial tutorial scope
     $scope.tutorialIdChanged()
     # Task definition options
@@ -90,7 +90,7 @@
       taskDef = $scope.unit.taskDef(taskDefId) if taskDefId?
       $scope.filters.taskDefinition = taskDef
       refreshData() if $scope.isTaskDefMode
-      applyFilters()
+      $scope.applyFilters()
     # Set new taskDefinitionIdSelected if task def mode and taskDefAbbr set
     setTaskDefFromTaskKey = (taskKey) ->
       # Only applicable in taskDefMode
@@ -100,7 +100,7 @@
       $scope.filters.taskDefinition = taskDef
     setTaskDefFromTaskKey($scope.taskData.taskKey)
     # Student Name options
-    $scope.studentNameChanged = applyFilters
+    $scope.studentNameChanged = $scope.applyFilters
     # Finds a task (or null) given its task key
     findTaskForTaskKey = (key) -> _.find($scope.tasks, (t) -> t.hasTaskKey(key))
     # Initially not watching the task key
@@ -110,13 +110,13 @@
       # Tasks for feedback or tasks for task, depending on the data source
       $scope.taskData.source.query { id: $scope.unit.id, task_def_id: $scope.filters.taskDefinitionIdSelected },
         (response) ->
-          $scope.tasks = $scope.unit.incorporateTasks(response, applyFilters)
+          $scope.tasks = $scope.unit.incorporateTasks(response, $scope.applyFilters)
           # If loading via task definitions, fill
           if $scope.isTaskDefMode
             unstartedTasks = $scope.unit.fillWithUnStartedTasks($scope.tasks, $scope.filters.taskDefinitionIdSelected)
             _.extend($scope.tasks, unstartedTasks)
           # Apply initial filters
-          applyFilters()
+          $scope.applyFilters()
           # Load initial set task, either the one provided (by the URL)
           # then load actual task in now or the first task that applies
           # to the given set of filters.


Restore the original 'pager sup' format for nagios alerts

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -645,15 +645,15 @@
      #   HOSTSTATE: 'UP' },

     summary = if inc.trigger_summary_data
+              if inc.trigger_summary_data.pd_nagios_object == 'service'
+                 "#{inc.trigger_summary_data.HOSTNAME}/#{inc.trigger_summary_data.SERVICEDESC}"
+              else if inc.trigger_summary_data.pd_nagios_object == 'host'
+                 "#{inc.trigger_summary_data.HOSTNAME}/#{inc.trigger_summary_data.HOSTSTATE}"
               # email services
-              if inc.trigger_summary_data.subject
+              else if inc.trigger_summary_data.subject
                 inc.trigger_summary_data.subject
               else if inc.trigger_summary_data.description
                 inc.trigger_summary_data.description
-              else if inc.trigger_summary_data.pd_nagios_object == 'service'
-                 "#{inc.trigger_summary_data.HOSTNAME}/#{inc.trigger_summary_data.SERVICEDESC}"
-              else if inc.trigger_summary_data.pd_nagios_object == 'host'
-                 "#{inc.trigger_summary_data.HOSTNAME}/#{inc.trigger_summary_data.HOSTSTATE}"
               else
                 ""
             else


Fixed multiline code snippet

Fixed multiline code snippet

--- lib/written/app/assets/javascripts/written/parsers/block/code.coffee
+++ lib/written/app/assets/javascripts/written/parsers/block/code.coffee
@@ -23,7 +23,7 @@
     @node

   toHTMLString: (node) =>
-    code = node.toString().split('\n').slice(1, -1)
+    code = node.toString().split('\n').slice(1, -1).join('\n')
     "
#{code}
"

 Written.Parsers.Block.register Code, /((~{3})([a-z]+)?)(.+)?/i


fixed double push state if link is current url

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -1,7 +1,7 @@
 @historyCache = []

 visit = (url) ->
-  if browserSupportsPushState?
+  if browserSupportsPushState? and document.location.href != url
     @historyCache.push({url:document.location.href,body:document.body,title:document.title})
     reflectNewUrl url
     fetchReplacement url


Close doc after doc.write

Close doc after doc.write

doc.close does nothing in coffee-script.

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -134,7 +134,7 @@
     doc = document.implementation.createHTMLDocument ''
     doc.open 'replace'
     doc.write html
-    doc.close
+    doc.close()
     doc

   if window.DOMParser


Fix race condition causing some apps to be rendered more than once

--- lib/assets/javascripts/views/apps.js.coffee
+++ lib/assets/javascripts/views/apps.js.coffee
@@ -12,15 +12,21 @@

     @collection = TentAdmin.Collections.Apps.find(entity: TentAdmin.config.meta.content.entity) || new TentAdmin.Collections.Apps

+    _render_set_immediate = null
+
     @collection.on 'reset', =>
-      setImmediate =>
+      clearImmediate(_render_set_immediate)
+
+      _render_set_immediate = setImmediate =>
         # defer until model.auth is set
         # (see fetchSuccess() method of apps collection)

         @render()

     @collection.on 'append', (models) =>
-      setImmediate =>
+      clearImmediate(_render_set_immediate)
+
+      _render_set_immediate = setImmediate =>
         # defer until model.auth is set
         # (see fetchSuccess() method of apps collection)


Torrent plugin stuff.

Torrent plugin stuff.

--- src/Plugins/Torrents/Hadouken.Plugins.Torrents/UI/js/plugin.coffee
+++ src/Plugins/Torrents/Hadouken.Plugins.Torrents/UI/js/plugin.coffee
@@ -1,5 +1,8 @@
 class Plugin
-  load: -> console.log('load')
+  pageManager: Hadouken.UI.PageManager.getInstance()
+
+  load: =>
+    console.log @pageManager.current

   unload: -> console.log('unload')


Update hubble.coffee

Update hubble.coffee

Get a case sensitive file system :-P

--- hubble.coffee
+++ hubble.coffee
@@ -1,2 +1,2 @@
 #!/usr/bin/env ./node_modules/coffee-script/bin/coffee
-hubble = require './src/Index.coffee'
+hubble = require './src/index.coffee'


Add miss out API: each_battle_water

Add miss out API: each_battle_water

--- index.cjsx
+++ index.cjsx
@@ -200,6 +200,7 @@
            '/kcsapi/api_req_combined_battle/ld_airbattle', \
            '/kcsapi/api_req_combined_battle/ec_battle', \
            '/kcsapi/api_req_combined_battle/each_battle', \
+           '/kcsapi/api_req_combined_battle/each_battle_water', \
            '/kcsapi/api_req_battle_midnight/sp_midnight', \
            '/kcsapi/api_req_combined_battle/sp_midnight'
         drop.enemyShips = []


Check for null and return 404 if subscriber does not exist

--- lib/api.coffee
+++ lib/api.coffee
@@ -83,6 +83,11 @@
                 return

         req.subscriber.getSubscriptions (subs) ->
+            if not subs?
+                logger.error "No subscriber #{req.subscriber.id}"
+                res.send 404
+                return
+
             tasks = []

             for sub in subs


match shortcuts with no filters

match shortcuts with no filters

--- lib/bot.coffee
+++ lib/bot.coffee
@@ -132,7 +132,7 @@
     get(context, message)
   else
     shortCommands = _.sortBy(_.values(customCommands), (c) -> -c.name.length)
-    matchedCommand = shortCommands.filter((c) -> message.text.toLowerCase().indexOf("#{c.name} ") == 0)?[0]
+    matchedCommand = shortCommands.filter((c) -> message.text.toLowerCase().indexOf(c.name) == 0)?[0]
     if matchedCommand

       query = message.text[matchedCommand.name.length..].trim()


:lipstick:

:lipstick:

--- lib/atom-ternjs-client.coffee
+++ lib/atom-ternjs-client.coffee
@@ -16,7 +16,6 @@
     origins: false
     caseInsensitive: false
     documentation: false
-

   constructor: ->
     @registerEvents()


cast to Number

cast to Number

--- lib/atom-ternjs-config.coffee
+++ lib/atom-ternjs-config.coffee
@@ -145,7 +145,7 @@
     newConfig = {}
     for key in Object.keys(@config.ecmaVersions)
       if @config.ecmaVersions[key]
-        newConfig.ecmaVersion = key[key.length - 1]
+        newConfig.ecmaVersion = Number(key[key.length - 1])
         break
     if !_.isEmpty(@config.libs)
       newConfig.libs = []


daily

daily

--- lib/cli/generators/nim.coffee
+++ lib/cli/generators/nim.coffee
@@ -36,10 +36,9 @@
     #if flags.indexOf('-u') or flags.indexOf('--unity')

     s0 = [] # Components.nim
-    s1 = [] # Entity.nim
-    se = [] # header
-    sv = [] # variables
-    sc = [] # Entity constructor
+    se = [] # Entity.nim
+    st = [] # Pool type variables
+    sc = [] # Pool constructor
     s2 = [] # Match.nim
     sm = [] # variables
     s3 = [] # World.nim
@@ -48,61 +47,69 @@
     s0.push "##"
     s0.push "## Entitas Generated Components for #{config.namespace}"
     s0.push "##"
-    s0.push "## do not edit this file"
-    s0.push "##"
+    s0.push "## ** do not edit this file **"
+    s0.push "##"
+    s0.push "import queues"
     s0.push "import bosco/ECS"
     s0.push "import bosco/Sprite"
-    s0.push ""
+    s0.push "const POOL_SIZE : int = #{config.alloc.components}"
+    s0.push ""
+    s0.push "##"
+    s0.push "## Define Components"
+    s0.push "##"
+    s0.push "type"

     se.push "##"
     se.push "## Entitas Generated Entity Extensions for #{config.namespace}"
     se.push "##"
-    se.push "## do not edit this file"
+    se.push "## ** do not edit this file **"
     se.push "##"
     se.push "import queues"
     se.push "import bosco/ECS"
     se.push "import bosco/Sprite"
     se.push "import ComponentEx"
-    se.push "const POOL_SIZE : int = #{config.alloc.components}"
     se.push ""
-    se.push ""
+    se.push "##"
+    se.push "## Extend Entity"
+    se.push "##"

     s2.push "##"
     s2.push "## Entitas Generated Match Extensions for #{config.namespace}"
     s2.push "##"
-    s2.push "## do not edit this file"
+    s2.push "## ** do not edit this file **"
     s2.push "##"
     s2.push "import bosco/ECS"
     s2.push "import ComponentEx"
-    s2.push "type TMatch = ref object of RootObj"
+    s2.push "##"
+    s2.push "## Define a Match for each Component"
+    s2.push "##"
+    s2.push "type MatchObj = ref object of RootObj"

     s3.push "##"
     s3.push "## Entitas Generated World Extensions for #{config.namespace}"
     s3.push "##"
-    s3.push "## do not edit this file"
+    s3.push "## ** do not edit this file **"
     s3.push "##"
     s3.push "import bosco/ECS"
     s3.push "import ComponentEx"
     s3.push "import MatchEx"
     s3.push "import EntityEx"
     s3.push ""
-    s3.push ""
+    s3.push "##"
+    s3.push "## Extend World"
+    s3.push "##"

     ###
      * Components Enum
     ###
-    s0.push ""
-    s0.push "type"
-    s0.push "  Component* = enum"
+    s0.push "  Component* {.pure.} = enum"
     for Name, properties of config.components
       s0.push "    #{Name}"
     s0.push ""
-    s0.push ""

     ###
      * Components Type Definitions
     ###
-    s0.push ""
     for Name, properties of config.components
       name = Name[0].toLowerCase()+Name[1...]
       s0.push "  #{Name}Component* = ref object of IComponent"
@@ -117,91 +124,90 @@
     ###
      * Extend Entity with components
     ###
-    s1.push ""
+    se.push ""
     for Name, properties of config.components
       name = Name[0].toLowerCase()+Name[1...]
       switch properties
         when false
-          sv.push "  ## @type {#{config.namespace}.#{Name}Component}"
-          sv.push "  #{name}Component* : #{Name}Component"
+          st.push "    #{name}Component* : #{Name}Component"
           sc.push ""
-          sc.push "Pool.#{name}Component = #{Name}Component()"
-          s1.push "## @type {boolean} "
-          s1.push "proc is#{Name}*(this : Entity) : bool ="
-          s1.push "  this.hasComponent(int(Component.#{Name}))"
-          s1.push "proc `is#{Name}=`*(this : Entity, value : bool) ="
-          s1.push "  if value != this.is#{Name}:"
-          s1.push "    if value:"
-          s1.push "      discard this.addComponent(int(Component.#{Name}), Pool.#{name}Component)"
-          s1.push "    else:"
-          s1.push "      discard this.removeComponent(int(Component.#{Name}))"
-          s1.push ""
-          s1.push "##"
-          s1.push "## @param {boolean} value"
-          s1.push "## @returns {bosco.Entity}"
-          s1.push "##"
-          s1.push "proc set#{Name}*(this : Entity, value : bool) : Entity ="
-          s1.push "  this.is#{Name} = value"
-          s1.push "  return this"
-          s1.push ""
+          sc.push "  result.#{name}Component = #{Name}Component()"
+          se.push "## @type {boolean} "
+          se.push "proc is#{Name}*(this : Entity) : bool ="
+          se.push "  this.hasComponent(int(Component.#{Name}))"
+          se.push "proc `is#{Name}=`*(this : Entity, value : bool) ="
+          se.push "  if value != this.is#{Name}:"
+          se.push "    if value:"
+          se.push "      discard this.addComponent(int(Component.#{Name}), Pool.#{name}Component)"
+          se.push "    else:"
+          se.push "      discard this.removeComponent(int(Component.#{Name}))"
+          se.push ""
+          se.push "##"
+          se.push "## @param {boolean} value"
+          se.push "## @returns {bosco.Entity}"
+          se.push "##"
+          se.push "proc set#{Name}*(this : Entity, value : bool) : Entity ="
+          se.push "  this.is#{Name} = value"
+          se.push "  return this"
+          se.push ""

         else
-          sv.push "  ## @type {bosco.utils.Bag}"
-          sv.push "  #{name}Component* : Queue[#{Name}Component]"
-          s1.push ""
+          st.push "    #{name}Component* : Queue[#{Name}Component]"
+          se.push ""

           sc.push ""
-          sc.push "Pool.#{name}Component = initQueue[#{Name}Component]()"
-          sc.push "for i in 1..POOL_SIZE:Pool.#{name}Component.add(#{Name}Component())"
+          sc.push "  result.#{name}Component = initQueue[#{Name}Component]()"
+          sc.push "  for i in 1..POOL_SIZE:"
+          sc.push "    result.#{name}Component.add(#{Name}Component())"

-          s1.push "proc clear#{Name}Component*(this : Entity) ="
-          s1.push "  Pool.#{name}Component = initQueue[#{Name}Component]()"
-          s1.push ""
-          s1.push "## @type {#{config.namespace}.#{Name}Component} "
-          s1.push "proc #{name}*(this : Entity) : #{Name}Component ="
-          s1.push "  (#{Name}Component)this.getComponent(int(Component.#{Name}))"
-          s1.push ""
-          s1.push "## @type {boolean} "
-          s1.push "proc has#{Name}*(this : Entity) : bool ="
-          s1.push "  this.hasComponent(int(Component.#{Name}))"
-          s1.push ""
-          s1.push "##"
-          for p in properties
-            s1.push "## @param {#{p.split(':')[1]}} #{p.split(':')[0]}"
-          s1.push "## @returns {bosco.Entity}"
-          s1.push "##"
-          s1.push "proc add#{Name}*(this : Entity, #{properties.join(', ')}) : Entity ="
-          s1.push "  var component = if Pool.#{name}Component.len > 0 : Pool.#{name}Component.dequeue() else: #{Name}Component()"
-          for p in properties
-            s1.push "  component.#{p.split(':')[0]} = #{p.split(':')[0]}"
-          s1.push "  discard this.addComponent(int(Component.#{Name}), component)"
-          s1.push "  return this"
-          s1.push ""
-          s1.push "##"
-          for p in properties
-            s1.push "## @param {#{p.split(':')[1]}} #{p.split(':')[0]}"
-          s1.push "## @returns {bosco.Entity}"
-          s1.push "##"
-          s1.push "proc replace#{Name}*(this : Entity, #{properties.join(', ')}) : Entity ="
-          s1.push "  var previousComponent = if this.has#{Name} : this.#{name} else: nil"
-          s1.push "  var component = if Pool.#{name}Component.len > 0 : Pool.#{name}Component.dequeue() else: #{Name}Component()"
-          for p in properties
-            s1.push "  component.#{p.split(':')[0]} = #{p.split(':')[0]}"
-          s1.push "  discard this.replaceComponent(int(Component.#{Name}), component)"
-          s1.push "  if previousComponent != nil:"
-          s1.push "    Pool.#{name}Component.enqueue(previousComponent)"
-          s1.push ""
-          s1.push "  return this"
-          s1.push ""
-          s1.push "##"
-          s1.push "## @returns {bosco.Entity}"
-          s1.push "##"
-          s1.push "proc remove#{Name}*(this : Entity) : Entity ="
-          s1.push "  var component = this.#{name}"
-          s1.push "  discard this.removeComponent(int(Component.#{Name}))"
-          s1.push "  Pool.#{name}Component.enqueue(component)"
-          s1.push "  return this"
-          s1.push ""
+          se.push "proc clear#{Name}Component*(this : Entity) ="
+          se.push "  Pool.#{name}Component = initQueue[#{Name}Component]()"
+          se.push ""
+          se.push "## @type {#{config.namespace}.#{Name}Component} "
+          se.push "proc #{name}*(this : Entity) : #{Name}Component ="
+          se.push "  (#{Name}Component)this.getComponent(int(Component.#{Name}))"
+          se.push ""
+          se.push "## @type {boolean} "
+          se.push "proc has#{Name}*(this : Entity) : bool ="
+          se.push "  this.hasComponent(int(Component.#{Name}))"
+          se.push ""
+          se.push "##"
+          for p in properties
+            se.push "## @param {#{p.split(':')[1]}} #{p.split(':')[0]}"
+          se.push "## @returns {bosco.Entity}"
+          se.push "##"
+          se.push "proc add#{Name}*(this : Entity, #{properties.join(', ')}) : Entity ="
+          se.push "  var component = if Pool.#{name}Component.len > 0 : Pool.#{name}Component.dequeue() else: #{Name}Component()"
+          for p in properties
+            se.push "  component.#{p.split(':')[0]} = #{p.split(':')[0]}"
+          se.push "  discard this.addComponent(int(Component.#{Name}), component)"
+          se.push "  return this"
+          se.push ""
+          se.push "##"
+          for p in properties
+            se.push "## @param {#{p.split(':')[1]}} #{p.split(':')[0]}"
+          se.push "## @returns {bosco.Entity}"
+          se.push "##"
+          se.push "proc replace#{Name}*(this : Entity, #{properties.join(', ')}) : Entity ="
+          se.push "  var previousComponent = if this.has#{Name} : this.#{name} else: nil"
+          se.push "  var component = if Pool.#{name}Component.len > 0 : Pool.#{name}Component.dequeue() else: #{Name}Component()"
+          for p in properties
+            se.push "  component.#{p.split(':')[0]} = #{p.split(':')[0]}"
+          se.push "  discard this.replaceComponent(int(Component.#{Name}), component)"
+          se.push "  if previousComponent != nil:"
+          se.push "    Pool.#{name}Component.enqueue(previousComponent)"
+          se.push ""
+          se.push "  return this"
+          se.push ""
+          se.push "##"
+          se.push "## @returns {bosco.Entity}"
+          se.push "##"
+          se.push "proc remove#{Name}*(this : Entity) : Entity ="
+          se.push "  var component = this.#{name}"
+          se.push "  discard this.removeComponent(int(Component.#{Name}))"
+          se.push "  Pool.#{name}Component.enqueue(component)"
+          se.push "  return this"
+          se.push ""

     ###
@@ -209,12 +215,10 @@
     ###
     for Name, properties of config.components
       name = Name[0].toLowerCase()+Name[1...];
-      s2.push "  ## @type {bosco.Match} "
       s2.push "  match#{Name} : Matcher"

       sm.push ""
-      sm.push "## @type {bosco.Matcher} "
-      sm.push "proc #{Name}*(this : TMatch) : Matcher ="
+      sm.push "proc #{Name}*(this : MatchObj) : Matcher ="
       sm.push "  if this.match#{Name} == nil:"
       sm.push "    this.match#{Name} = MatchAllOf(@[int(Component.#{Name})])"
       sm.push "  return this.match#{Name}"
@@ -291,14 +295,24 @@

-    se.push "type TPool = ref object of RootObj"
-    se.push sv.join('\n')
-    se.push "var Pool* = TPool()"
-    se.push sc.join('\n')
-    se.push s1.join('\n')
-
+    s0.push "  ##"
+    s0.push "  ## Component Pool"
+    s0.push "  ##"
+    s0.push "  PoolObj = ref object of RootObj"
+    s0.push st.join('\n')
+    s0.push ""
+    s0.push "##"
+    s0.push "## constructor for a new Component Pool"
+    s0.push "##"
+    s0.push "proc newPoolObj() : PoolObj ="
+    s0.push "  new(result)"
+    s0.push sc.join('\n')
+    s0.push ""
+    s0.push "var Pool* = PoolObj()"
+
     s2.push sm.join('\n')
-    s2.push "var Match* = TMatch()"
+    s2.push ""
+    s2.push "var Match* = MatchObj()"

     mkdirp.sync path.join(process.cwd(), 'gen/')

@@ -307,8 +321,8 @@
     s2.push('\n')
     s3.push('\n')

-    fs.writeFileSync(path.join(process.cwd(), "src/ComponentEx.nim"), s0.join('\n'))
-    fs.writeFileSync(path.join(process.cwd(), "src/EntityEx.nim"), se.join('\n'))
-    fs.writeFileSync(path.join(process.cwd(), "src/MatchEx.nim"), s2.join('\n'))
-    fs.writeFileSync(path.join(process.cwd(), "src/WorldEx.nim"), s3.join('\n'))
-
+    fs.writeFileSync(path.join(process.cwd(), "src/gen/ComponentEx.nim"), s0.join('\n'))
+    fs.writeFileSync(path.join(process.cwd(), "src/gen/EntityEx.nim"), se.join('\n'))
+    fs.writeFileSync(path.join(process.cwd(), "src/gen/MatchEx.nim"), s2.join('\n'))
+    fs.writeFileSync(path.join(process.cwd(), "src/gen/WorldEx.nim"), s3.join('\n'))
+


no need to reuse the node.  just create a new identifier

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -274,10 +274,7 @@
                                         init: null
                                 }],
                                 kind: "let"
-                        n.left.type = syntax.Identifier
-                        n.left.name = n.left.declarations[0].id.name
-                        delete n.left.declarations
-                        delete n.left.kind
+                        n.left = create_identifier n.left.declarations[0].id.name

                 n.right = @visit n.right
                 n.body = @visit n.body


maybe fix https://github.com/sagemathinc/smc/issues/1327 -- will re-open if not: "sometimes -- maybe 1 in 50 -- when switching to a codemirror editor tab, the cursor and scroll positions aren't restored properly"

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -1504,7 +1504,7 @@
     restore_cursor_position: () =>
         for i, cm of [@codemirror, @codemirror1]
             if cm?
-                pos = @local_storage("cursor#{i}")
+                pos = @local_storage("cursor#{cm.name}")
                 if pos?
                     cm.setCursor(pos)
                     #console.log("#{@filename}: setting view #{cm.name} to cursor pos -- #{misc.to_json(pos)}")
@@ -1587,8 +1587,14 @@
             # the flex layout with column direction is broken on Safari.
             @element.find(".salvus-editor-codemirror-input-container-layout-#{@_layout}").make_height_defined()

+        refresh = (cm) =>
+            return if not cm?
+            cm.refresh()
+            # See https://github.com/sagemathinc/smc/issues/1327#issuecomment-265488872
+            setTimeout((=>cm.refresh()), 1)
+
         for cm in @codemirrors()
-            cm?.refresh()
+            refresh(cm)

         @emit('show')


get rid of the flicker when dragging and letting go of bar in codemirror split

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -929,7 +929,6 @@
                 p   = layout1_bar.offset().top + layout1_bar.height()/2
                 @_layout1_split_pos = (p - top) / ht
                 @local_storage("layout1_split_pos", @_layout1_split_pos)
-                layout1_bar.css(top:0)
                 # redraw, which uses split info
                 @show()

@@ -949,7 +948,6 @@
                 p     = layout2_bar.offset().left
                 @_layout2_split_pos = (p - left) / width
                 @local_storage("layout2_split_pos", @_layout2_split_pos)
-                layout2_bar.css('left', 0)   # otherwise gets set to some crazy value...
                 # redraw, which uses split info
                 @show()

@@ -1437,6 +1435,11 @@
         # We set only the default size of the *first* div -- everything else expands accordingly.
         elt = @element.find(".salvus-editor-codemirror-input-container-layout-#{@_layout}").show()

+        if @_layout == 1
+            @element.find(".salvus-editor-resize-bar-layout-1").css(top:0)
+        else if @_layout == 2
+            @element.find(".salvus-editor-resize-bar-layout-2").css(left:0)
+
         c = elt.find(".salvus-editor-codemirror-input-box")
         if @_layout == 0
             c.css('flex', 1)   # use the full vertical height


fix https://github.com/sagemathinc/smc/issues/991 -- "editor search button does nothing"

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -1862,6 +1862,7 @@
             CodeMirror.commands.defaultTab(editor)

     init_edit_buttons: () =>
+
         that = @
         for name in ['search', 'next', 'prev', 'replace', 'undo', 'redo', 'autoindent',
                      'shift-left', 'shift-right', 'split-view','increase-font', 'decrease-font', 'goto-line', 'print' ]
@@ -1888,35 +1889,41 @@
                     CodeMirror.commands.findNext(cm)
                 else
                     CodeMirror.commands.goPageDown(cm)
+                    cm.focus()
             when 'prev'
                 if cm._searchState?.query
                     CodeMirror.commands.findPrev(cm)
                 else
                     CodeMirror.commands.goPageUp(cm)
+                    cm.focus()
             when 'replace'
                 CodeMirror.commands.replace(cm)
             when 'undo'
                 cm.undo()
+                cm.focus()
             when 'redo'
                 cm.redo()
+                cm.focus()
             when 'split-view'
                 @toggle_split_view(cm)
             when 'autoindent'
                 CodeMirror.commands.indentAuto(cm)
             when 'shift-left'
                 cm.unindent_selection()
+                cm.focus()
             when 'shift-right'
                 @press_tab_key(cm)
+                cm.focus()
             when 'increase-font'
                 @change_font_size(cm, +1)
+                cm.focus()
             when 'decrease-font'
                 @change_font_size(cm, -1)
+                cm.focus()
             when 'goto-line'
                 @goto_line(cm)
             when 'print'
                 @print()
-        if name != 'goto-line'
-            cm.focus()

     restore_font_size: () =>


with example

with example

--- lib/esl.coffee
+++ lib/esl.coffee
@@ -279,10 +279,26 @@
 # Examples:
 ###

-client =  createClient()
-client.on 'esl_auth_request', (req,res) ->
-    res.auth "ClueCon", () ->
-      res.send 'event json HEARTBEAT'
-client.connect(8021, '127.0.0.1')
+  esl = require 'esl'
+
+  # Open connection, send arbitrary API command, disconnect.
+  fs_command = (cmd,cb) ->
+    client = esl.createClient()
+    client.on 'esl_auth_request', (req,res) ->
+      res.auth 'ClueCon', (req,res) ->
+        res.api cmd, (req,res) ->
+          res.exit ->
+            client.end()
+    if cb?
+      client.on 'close', cb
+    client.connect(8021, '127.0.0.1')
+
+  # Example
+  fs_command "reloadxml"
+
+  # Note: Use
+  #    res.send 'event json HEARTBEAT'
+  # to start receiving event notifications.
+
 ###


Fix stale listing problem

Fix stale listing problem

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -242,7 +242,7 @@
         opts = defaults opts,
             exclusions : undefined # Array<String> of sub-trees' root paths to omit
         # WARNING: Do not change the store except in a callback below.
-        block = "_fetch_directory_tree_#{project_id}"
+        block = "_fetch_directory_tree_#{project_id}_#{opts.exclusions?.toString()}"
         if @[block]
             return
         @[block] = true


webapp/sagews: discarding empty strings when deserializing cell output -- related to #1012

--- src/smc-webapp/sagews.coffee
+++ src/smc-webapp/sagews.coffee
@@ -2415,11 +2415,15 @@

     output: =>
         v = []
-        for x in @raw_output().slice(38).split(MARKERS.output)
-            try
-                v.push(misc.from_json(x))
-            catch
-                console.warn("unable to read json message in worksheet: #{x}")
+        raw = @raw_output()
+        if not raw?  # might return undefined, see above
+            return v
+        for x in raw.slice(38).split(MARKERS.output)
+            if x?.length > 0 # empty strings cause json deserialization problems (i.e. that warning below)
+                try
+                    v.push(misc.from_json(x))
+                catch
+                    console.warn("unable to read json message in worksheet: #{x}")
         return v

     _get_output: () =>


Fixing up option parsing

Fixing up option parsing

--- cli.coffee
+++ cli.coffee
@@ -29,8 +29,9 @@
 cli.parse
   'delete':   ['d', 'Delete all records', 'boolean']
   'port':     ['p', 'TCP port to check', 'number', 80]
-  'servers':  ['s', 'whitespace-delimited list of servers'
-                    +'(which may use brace expansion)', 'string']
+  'servers':  ['s',
+    'whitespace-delimited list of servers (which may use brace expansion)',
+    'string']
   'watch':    ['w', 'Monitor for changes after first check', 'boolean']

 cli.main (args, options) ->


Fixed crash when function tried to inspect itself

--- lib/syn.coffee
+++ lib/syn.coffee
@@ -2,19 +2,26 @@

 convert = (input) ->
   r = []
-  switch true
-    when input instanceof Array
-      r.push (input.map (x) -> convert x).join ' '
-      r.unshift '('
-      r.push ')'
-    when typeof input is 'string'
-      r.push '"' + input + '"'
-    when input instanceof Object
-      object_mapper input, (x) -> r.push "\"#{x}\":#{convert input[x]}"
-      r.unshift '{'
-      r.push '}'
-    else
-      r.push input
+  if input instanceof Array
+    r.push (input.map (x) -> convert x).join ' '
+    r.unshift '('
+    r.push ')'
+  else if typeof input is 'string'
+    r.push '"' + input + '"'
+  else if typeof input is 'function'
+    r.push '(lambda ...)'
+  else if input instanceof Object
+    a = []
+    object_mapper input, (x) -> a.push "\"#{x}\":#{convert input[x]}"
+    r.push a.join ' '
+    r.unshift '{'
+    r.push '}'
+  else if input is null
+    r.push 'null'
+  else if input is undefined
+    r.push 'undefined'
+  else
+    r.push input
   r.join ''

 verify = (input) ->


JSON.parse body when content-type is application/json

--- lib/zen.coffee
+++ lib/zen.coffee
@@ -100,7 +100,10 @@
                   request.connection.destroy()
               request.on "end", ->
                 if body isnt ""
-                  parameters[key] = value for key, value of querystring.parse body
+                  if request.headers["content-type"] is CONST.MIME.json
+                    parameters[key] = value for key, value of JSON.parse body
+                  else
+                    parameters[key] = value for key, value of querystring.parse body
                 request.parameters = __cast parameters
                 endpoint.callback request, response
             else


fix random index function

fix random index function

--- lingsim.coffee
+++ lingsim.coffee
@@ -8,7 +8,7 @@

   # random index of a list
   randomIndex: (list) =>
-    @randomRange 0, list.length - 1
+    Math.floor @randomRange 0, list.length

   # random element of list
   sample: (list) =>


Fixed interrupt enabled flag handling.

Fixed interrupt enabled flag handling.

--- src/CPU.coffee
+++ src/CPU.coffee
@@ -120,7 +120,7 @@
                 when Interrupt.Reset then handleReset()
             @tick()
             @tick() # To make totally 7 cycles together with interrupt handler.
-            requestedInterrupt = false
+            requestedInterrupt = null

     isRequestedInterruptDisabled: ->
         @requestedInterrupt == Interrupt.IRQ and @interruptDisable is on


Fix: create mentions cursors in profile when not found

--- assets/javascripts/models/background_mentions_cursor.js.coffee
+++ assets/javascripts/models/background_mentions_cursor.js.coffee
@@ -14,7 +14,8 @@

       error: (res, xhr) =>
         if xhr.status == 404
-          @resetProfileCursor()
+          for type in TentStatus.config.post_types
+            @resetProfileCursor(type, res)

   resetProfileCursor: (type, cursor) =>
     unless TentStatus.background_mentions_unread_count


consistency: order classes like in other case

--- js-library/app/js/jail_iframe/classes/notification.coffee
+++ js-library/app/js/jail_iframe/classes/notification.coffee
@@ -6,7 +6,7 @@
 FactlinkJailRoot.showLoadedNotification = ->
   showNotification
     message: 'Factlink is loaded!'
-    type_classes: 'fl-message-success fl-message-icon-time'
+    type_classes: 'fl-message-icon-time fl-message-success'

 in_screen_time = 3000
 removal_delay = 1000 # Should be larger than notification_transition_time


escape underscore in markdown of how-to

escape underscore in markdown of how-to

--- app/pages/lab/how-to-page.cjsx
+++ app/pages/lab/how-to-page.cjsx
@@ -260,7 +260,7 @@

       *The condensed answer:*

-      A manifest is a file that tells our software how to combine the images you have into units of data (subjects) to be classified. The manifest also allows you to link your classifications back to the rest of your data. A manifest is formatted as a CSV file with 1 line per subject, with a unique identifier and the names of images to be associated with a subject on each row (with additional information often included in other fields as well). There is an example in the "Kitteh" zip file.
+      A manifest is a file that tells our software how to combine the images you have into units of data (subjects) to be classified. The manifest also allows you to link your classifications back to the rest of your data. A manifest is formatted as a CSV file with 1 line per subject, with a unique identifier and the names of images to be associated with a subject on each row (with additional information often included in other fields as well). There is an example in the ["Kitteh" zip file](https://data.zooniverse.org/tutorial/kitteh_zoo.zip).

       *The full answer:*

@@ -273,11 +273,11 @@
           2,8300920648_d4a21bba59_z,Flickr,https://www.flickr.com/photos/aigle_dore/8300920648,Moyan Brenn,Creative Commons - share adapt attribute,grandfather kitteh has ear hair. a lot of it
           3,6713782851_82fc8c73e5_z.jpg,Flickr,https://www.flickr.com/photos/hellie55/6713782851,hehaden,Creative Commons - share adapt attribute,juvenile kittehs practice break-in at the catnip factory

-      The first line of the file is a header line that specifies the name of each of the manifest fields. In this case, our manifest has 7 fields (or columns), called "subject_id", "image_name", "origin", "link", "attribution", "license" and “#secret_description”. They are separated by commas: this is what's known as a "comma separated values" file, or CSV file.
-
-      After the first line, each row of the file contains information about 1 subject. The first field, corresponding with the "subject_id" header, is a unique number that identifies the subject. The second field, which aligns with the "image_name" header, contains the name of the image that's associated with that subject. These 2 fields are critically important: the image name is obviously important, and a unique identifier is important for matching your classifications to the rest of your data.
-
-      All the other fields are optional, but in general having more information in the manifest is better. Most projects include additional information in the manifest that helps them match the classifications and subjects to the other data they need for their research. The additional information in the manifest can also be made available to volunteers as they classify (some very keen volunteers find this extremely useful). Any fields with names that begin with “#” or “//” will *not* be shown to volunteers, such as the “#secret_description” field in Kitteh Zoo. These hidden fields will still be returned to you in the classification file, so you can use these to include information helpful to your research without worrying about whether it might affect the classifications themselves. Information in fields that *don’t* begin with either “#” or “//” will be accessible to volunteers.
+      The first line of the file is a header line that specifies the name of each of the manifest fields. In this case, our manifest has 7 fields (or columns), called "subject\_id", "image\_name", "origin", "link", "attribution", "license" and “#secret\_description”. They are separated by commas: this is what's known as a "comma separated values" file, or CSV file.
+
+      After the first line, each row of the file contains information about 1 subject. The first field, corresponding with the "subject\_id" header, is a unique number that identifies the subject. The second field, which aligns with the "image\_name" header, contains the name of the image that's associated with that subject. These 2 fields are critically important: the image name is obviously important, and a unique identifier is important for matching your classifications to the rest of your data.
+
+      All the other fields are optional, but in general having more information in the manifest is better. Most projects include additional information in the manifest that helps them match the classifications and subjects to the other data they need for their research. The additional information in the manifest can also be made available to volunteers as they classify (some very keen volunteers find this extremely useful). Any fields with names that begin with “#” or “//” will *not* be shown to volunteers, such as the “#secret\_description” field in Kitteh Zoo. These hidden fields will still be returned to you in the classification file, so you can use these to include information helpful to your research without worrying about whether it might affect the classifications themselves. Information in fields that *don’t* begin with either “#” or “//” will be accessible to volunteers.

       For now, let’s assume you’re just including the bare minimum of information, like:


cleanup regex

cleanup regex

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -274,8 +274,8 @@
   _findFilesInMetadata: (metadata) ->
     filesInMetadata = []
     for key, value of metadata
-      extensions = if isAdmin() then '\\.\\D\\w{2,4}' else "(?:#{'\\'.concat VALID_SUBJECT_EXTENSIONS.join '|\\'})"
-      filesInValue = value.match? ///([^#{INVALID_FILENAME_CHARS.join '\\'}]+#{extensions})///gi
+      extensions = if isAdmin() then '\\.\\D\\w{2,4}' else "(?:#{'\\'+VALID_SUBJECT_EXTENSIONS.join '|\\'})"
+      filesInValue = value.match? ///([^#{'\\'+INVALID_FILENAME_CHARS.join '\\'}]+#{extensions})///gi
       if filesInValue?
         filesInMetadata.push filesInValue...
     filesInMetadata


added missing </a>

added missing </a>

added missing </a>

--- app/pages/lab/subject-set.cjsx
+++ app/pages/lab/subject-set.cjsx
@@ -180,7 +180,7 @@

         {@subjectLimitMessage(@props.project.subjects_count, @props.user)} Please  contact us to request changes to your allowance.

-
We strongly recommend uploading subjects in batches of 500 - 1,000 at a time. When uploading large numbers of subjects, we recommend using our  Panoptes command line interface or our  Panoptes Client package for Python  rather than the web portal.

+
We strongly recommend uploading subjects in batches of 500 - 1,000 at a time. When uploading large numbers of subjects, we recommend using our  Panoptes command line interface  or our  Panoptes Client package for Python  rather than the web portal.

       <form onSubmit={@handleSubmit}>


:art: cleanup

:art: cleanup

--- lib/move-selected-text.coffee
+++ lib/move-selected-text.coffee
@@ -27,12 +27,28 @@
 class MoveSelectedText extends TransformString
   @commandScope: 'atom-text-editor.vim-mode-plus.visual-mode'

+  initialize: ->
+    stateByEditor.set(@editor, newState()) unless stateByEditor.has(@editor)
+    unless disposableByEditor.has(@editor)
+      disposable = @vimState.modeManager.onDidDeactivateMode ({mode}) =>
+        if mode is 'visual'
+          stateByEditor.delete(@editor)
+
+      disposableByEditor.set @editor, @editor.onDidDestroy =>
+        stateByEditor.delete(@editor)
+        disposable.dispose()
+
+  getSelectedTexts: ->
+    @editor.getSelections().map((selection) -> selection.getText()).join("\n")
+
+  isOverwrite: ->
+    atom.config.get('vim-mode-plus-move-selected-text.overwrite')
+
   execute: ->
     @withUndoJoin =>
       selections = @editor.getSelectionsOrderedByBufferPosition()
       topSelection = selections[0]
       selections.reverse() if @direction is 'down'
-      @initOverwrittenArea()
       @editor.transact =>
         @countTimes =>
           return if (not @isLinewise()) and (not @isMovable(topSelection))
@@ -43,40 +59,24 @@
             @extendMovingArea(selection)
             @mutate(selection) if @isMovable(selection)

-  initOverwrittenArea: ->
-    state = stateByEditor.get(@editor)
-    unless state.overwritten?
-      if @isLinewise()
-        overwrittenArea = @editor.getSelections().map (selection) ->
-          _.multiplyString("\n", swrap(selection).getRowCount()-1)
-      else
-        overwrittenArea = @editor.getSelections().map (selection) ->
-          _.multiplyString(' ', selection.getBufferRange().getExtent().column)
-      state.overwritten = overwrittenArea
-
-  getSelectedTexts: ->
-    @editor.getSelections().map((selection) -> selection.getText()).join("\n")
-
-  isOverwrite: ->
-    atom.config.get('vim-mode-plus-move-selected-text.overwrite')
+  getInitialOverwrittenArea: ->
+    selections = @editor.getSelections()
+    if @isLinewise()
+      selections.map (selection) ->
+        _.multiplyString("\n", swrap(selection).getRowCount()-1)
+    else
+      selections.map (selection) ->
+        _.multiplyString(' ', selection.getBufferRange().getExtent().column)

   withUndoJoin: (fn) ->
-    stateByEditor.set(@editor, newState()) unless stateByEditor.has(@editor)
-
-    unless disposableByEditor.has(@editor)
-      disposable = @vimState.modeManager.onDidDeactivateMode ({mode}) =>
-        if mode is 'visual'
-          stateByEditor.delete(@editor)
-
-      disposableByEditor.set @editor, @editor.onDidDestroy =>
-        stateByEditor.delete(@editor)
-        disposable.dispose()
-
     state = stateByEditor.get(@editor)
     isSequential = state.selectedTexts is @getSelectedTexts()
     unless isSequential
       state.checkpoint = @editor.createCheckpoint()
       state.overwritten = null
+
+    unless state.overwritten?
+      state.overwritten = @getInitialOverwrittenArea()

     fn()


Set default type if undefined for Type mixin.

--- app/scripts/mixins/Type.coffee
+++ app/scripts/mixins/Type.coffee
@@ -9,6 +9,8 @@

     typeClass: ( ->
         type = @get 'type'
+        type = 'default' if not type?
+
         pref = @get 'classTypePrefix'
         "#{pref}-#{type}"
     ).property('type').cacheable()


fix crash on close

fix crash on close

--- app/scripts/robot/radio.coffee
+++ app/scripts/robot/radio.coffee
@@ -41,7 +41,7 @@
     radioInit = (radioAddr = "0013A20040A580C4", portPath = "/dev/ttyUSB0") ->
       _radioAddr = radioAddr

-      _ndl3Radio.close() if _ndl3Radio
+      _ndl3Radio.close() if _ndl3Radio?
       radio = requireNode('kyleradio')
       _ndl3Radio = new radio.Radio()

@@ -84,7 +84,6 @@
       close: ->
         if _init
           _ndl3Radio.close() if _ndl3Radio?
-          _serialPort.close() if _serialPort?
           _ndl3Radio = undefined
           _serialPort = undefined
           _portPath = ''


Caught a bug where running wouldn't save the options first

--- lib/custom-option-view.coffee
+++ lib/custom-option-view.coffee
@@ -27,4 +27,5 @@
   save: ->
     atom.workspaceView.trigger "script:save-options"
   run: ->
+    atom.workspaceView.trigger "script:save-options"
     atom.workspaceView.trigger "script:run"


Removing commented log calls

Removing commented log calls

--- src/polvo/core/optimizer.coffee
+++ src/polvo/core/optimizer.coffee
@@ -148,7 +148,6 @@

   missing = {}
   reorder: (files, cycling = false) ->
-    # log "Module.reorder"

     # if cycling is true or @missing is null, initializes empty array
     # for holding missing dependencies
@@ -161,10 +160,6 @@
     for file, i in files

       # if theres no dependencies, go to next file
-      # console.log '...'
-      # console.log file.id
-      # console.log file.dependencies
-      # console.log file.baseclasses
       continue if !file.dependencies.length && !file.baseclasses.length

       # otherwise loop thourgh all file dependencies


Fixed bug #39

Fixed bug #39

--- ng-modules/travelScreen/travelScreen.coffee
+++ ng-modules/travelScreen/travelScreen.coffee
@@ -80,13 +80,14 @@
     };
 });

-app.controller("travelScreenController", ['$scope', 'data', function($scope, data){
+app.controller("travelScreenController", ['$scope', 'data', '$interval', function($scope, data, $interval){
     var vm = this;
     vm.data = data;
     // TODO: do these need to be set after $(document).ready()?
     vm.canvasElement = document.getElementById("travelCanvas");
     vm.ctx = vm.canvasElement.getContext("2d");
     vm.shipImg = document.getElementById("player-ship");
+    var  timeoutId;

     vm.init = function(){
         vm.tiles = [new Tile(0, document.getElementById("bg-earth"))];
@@ -100,18 +101,24 @@

     vm.startTravel = function(){
         vm.travelling = true;
-        setTimeout(vm.go, TRAVEL_SPEED);
+        timeoutId = $interval(vm.go, TRAVEL_SPEED);
     }

     vm.go = function(){
-        if (vm.travelling){
-            vm.travel()
-            setTimeout(vm.go, TRAVEL_SPEED);
-        }  // else stop going
+        if (vm.travelling) vm.travel();
+        else if (timeoutId != undefined) vm.stopTravel();
+
+    }
+
+    // PRIVATE FUNCTION
+    var cancelInterval = function() {
+        var result = $interval.cancel(timeoutId);
+        if (result == true) timeoutId = undefined;
     }

     vm.stopTravel = function(){
         vm.travelling = false;
+        cancelInterval();
     }
     $scope.$on('switchToModule', function(switchingTo){
         vm.stopTravel();


status bar show last notification time

status bar show last notification time

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -1,3 +1,5 @@
+moment = require 'moment'
+
 module.exports =
 class StatusBarManager
   count: 0
@@ -16,11 +18,11 @@

     @element = document.createElement('a')
     @element.classList.add('notifications-count', 'inline-block')
-    atom.tooltips.add(@element, title: "Notifications")
+    @tooltip = atom.tooltips.add(@element, title: 'Notifications')
     span = document.createElement('span')
     span.appendChild(@number)
     @element.appendChild(span)
-    @element.addEventListener 'click', => atom.commands.dispatch(@element, "notifications-plus:toggle-log")
+    @element.addEventListener 'click', => atom.commands.dispatch(@element, 'notifications-plus:toggle-log')

     lastNotification = null
     for notification in atom.notifications.getNotifications()
@@ -37,8 +39,13 @@
   destroy: ->
     @tile.destroy()
     @tile = null
+    @tooltip.dispose()
+    @tooltip = null

   addNotification: (notification) ->
-    @element.setAttribute("last-type", notification.getType())
+    date = moment(notification.timestamp).format('LT')
+    @tooltip.dispose()
+    @tooltip = atom.tooltips.add(@element, title: "Last Notification #{date}")
+    @element.setAttribute('last-type', notification.getType())
     @number.textContent = ++@count
-    @number.classList.add("new-notification")
+    @number.classList.add('new-notification')


further fixes

further fixes

the previous version could leave the wrong classes on the status bar div

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -1,14 +1,20 @@
 {Disposable, CompositeDisposable} = require 'event-kit'

-# Mode names can be combined with submode names with the `.` character.
-# The status bar will display the most specific matching entry.
+# Mode names are combined with submode names with the `.` character.
+# If a mode has submodes, they need to be enumerated here.
 ContentsByMode =
-  'insert':           ["status-bar-vim-mode-insert",  "Insert"]
-  'insert.replace':   ["status-bar-vim-mode-insert",  "Replace"]
-  'command':          ["status-bar-vim-mode-command", "Command"]
-  'visual':           ["status-bar-vim-mode-visual",  "Visual"]
-  'visual.linewise':  ["status-bar-vim-mode-visual",  "Visual Line"]
-  'visual.blockwise': ["status-bar-vim-mode-visual",  "Visual Block"]
+  'insert':               ["status-bar-vim-mode-insert",  "Insert"]
+  'insert.replace':       ["status-bar-vim-mode-insert",  "Replace"]
+  'command':              ["status-bar-vim-mode-command", "Command"]
+  'visual':               ["status-bar-vim-mode-visual",  "Visual"]
+  'visual.characterwise': ["status-bar-vim-mode-visual",  "Visual"]
+  'visual.linewise':      ["status-bar-vim-mode-visual",  "Visual Line"]
+  'visual.blockwise':     ["status-bar-vim-mode-visual",  "Visual Block"]
+
+ClassesByMode = []
+
+for mode, [klass, html] of ContentsByMode
+  ClassesByMode.push(klass) unless klass in ClassesByMode

 module.exports =
 class StatusBarManager
@@ -20,16 +26,16 @@
   initialize: (@statusBar) ->

   update: (currentMode, currentSubmode) ->
-    currentFullMode = currentMode + "." + currentSubmode if currentSubmode?
-    for mode, [klass, html] of ContentsByMode
-      if mode is currentMode or mode is currentFullMode
-        @element.classList.add(klass)
-        @element.innerHTML = html
+    currentMode = currentMode + "." + currentSubmode if currentSubmode?
+    return unless currentMode of ContentsByMode

-        # if the submode is matched, it's the most specific matching entry, so stop looking
-        break if mode is currentFullMode
-      else
-        @element.classList.remove(klass)
+    # remove all the classes from @element before adding the right class
+    for klass in ClassesByMode
+      @element.classList.remove(klass)
+
+    [klass, html] = ContentsByMode[currentMode]
+    @element.classList.add(klass)
+    @element.innerHTML = html

   # Private


scale screen size

scale screen size

--- app/utils/connector-rfb.coffee
+++ app/utils/connector-rfb.coffee
@@ -25,6 +25,10 @@
       'onUpdateState': updateState
       'onXvpInit': xvpInit
     @rfb.connect ENV.deviceFarmHost, ENV.deviceFarmPort, '1234', "#{ENV.deviceFarmPath}?token=#{@deviceToken}"
+    if @rfb.get_display
+      display = @rfb.get_display
+      scaleRatio = display.autoscale screen.w, screen.h, true
+      @rfb.get_mouse().set_scale scaleRatio

   disconnect: ->
     @rfb.disconnect()


Only terminate worker after the worker completes work

--- lib/load-snippets-task.coffee
+++ lib/load-snippets-task.coffee
@@ -13,7 +13,7 @@

   loadNextPackageSnippets: ->
     unless @packages.length
-      @terminate()
+      @done()
       @snippets.loaded = true
       return


Strip git+ prefix from repository URL

Strip git+ prefix from repository URL

--- lib/notification-issue.coffee
+++ lib/notification-issue.coffee
@@ -156,8 +156,7 @@
           repo = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json')))?.repository
           repoUrl = repo?.url ? repo

-    repoUrl = repoUrl?.replace(/\.git$/, '')
-    repoUrl
+    repoUrl?.replace(/\.git$/, '').replace(/^git\+/, '')

   getPackageNameFromFilePath: (filePath) ->
     return unless filePath


Index manager now checks and does not allow creating duplicate indexes.

--- community/server/src/main/coffeescript/neo4j/webadmin/models/IndexManager.coffee
+++ community/server/src/main/coffeescript/neo4j/webadmin/models/IndexManager.coffee
@@ -33,12 +33,14 @@

     createNodeIndex: (opts) =>
       name = opts.name
+      if @_hasNodeIndex name then return
       @server.index.createNodeIndex(name).then (index) =>
         @get("nodeIndexes").push index
         @trigger "change"

     createRelationshipIndex : (opts) =>
       name = opts.name
+      if @_hasRelationshipIndex name then return
       @server.index.createRelationshipIndex(name).then (index) =>
         @get("relationshipIndexes").push index
         @trigger "change"
@@ -55,6 +57,16 @@
         @set "relationshipIndexes" : @_removeIndexFromList( @get("relationshipIndexes"), name )
         @trigger "change"

+    _hasRelationshipIndex : (name) =>
+      for idx in @get "relationshipIndexes"
+        if idx.name == name then return true
+      return false
+
+    _hasnNodeIndex : (name) =>
+      for idx in @get "nodeIndexes"
+        if idx.name == name then return true
+      return false
+
     _removeIndexFromList : (idxs, name) ->
       for i in [idxs.length-1..0]
         if idxs[i].name == name


Horizontal centering

Horizontal centering

--- circles.coffee
+++ circles.coffee
@@ -34,7 +34,7 @@
       new Circle i * lambda / 2, (i % 2) is 0

   draw: (distance) ->
-    @wrapper.attr('transform', "translate(0 #{(SVG_HEIGHT - distance) / 2})")
+    @wrapper.attr('transform', "translate(#{SVG_WIDTH / 2} #{(SVG_HEIGHT - distance) / 2})")
     @cg[1].attr('transform', "translate(0 #{distance})")
     for group in @cg
       circles = group.selectAll('circle').data(@circles)