SimplePlayer is now keyboard enabled

SimplePlayer is now keyboard enabled

--- scripts/coffee/lib/player/SimplePlayer.coffee
+++ scripts/coffee/lib/player/SimplePlayer.coffee
@@ -68,6 +68,10 @@

 			@timeControl.togglePlayState()

+		@kilid.on 'space', =>
+
+			@timeControl.togglePlayState()
+
 	_relayPlayPause: ->

 		dims = @display.currentDims
@@ -99,6 +103,12 @@

 			@display.toggle()

+		@kilid.on 'esc', =>
+
+			if @display.state is 'fullscreen'
+
+				@display.restore()
+
 	_relayFullscreenRestore: ->

 		dims = @display.currentDims
@@ -135,8 +145,23 @@

 			@timeControl.tick (e.absX + startingPos) / seekbarWidth * @timeControl.duration

+		@kilid.on 'right', =>
+
+			@timeControl.seekBy 5000
+
+		@kilid.on 'left', =>
+
+			@timeControl.seekBy -5000
+
+		@kilid.on 'alt+right', =>
+
+			@timeControl.seekBy 1000
+
+		@kilid.on 'alt+left', =>
+
+			@timeControl.seekBy -1000
+
 		do @_prepareSeeker
-

 	_relaySeekbar: ->


Implement a way to display a multi-lines log entry when filtering

--- lib/log-filter.coffee
+++ lib/log-filter.coffee
@@ -43,11 +43,27 @@

     useCompleteLogLines = true
     if useCompleteLogLines
-        @performLinesWithTimestampFilter()
-      linesArray = for line, i in buffer.getLines()
-        if !timestamp = @getLineTimestamp(i) then else i
-      @results.text = for logLine, i in linesArray
-        if regex.test(logLine) then else i
+      linesArray = []
+      linesIndexes = []
+      @performLinesWithTimestampFilter()
+      for line, i in @results.linesWithTimestamp
+        start = line
+        end = line
+        if i + 1 < @results.linesWithTimestamp.length
+          start = line
+          end = @results.linesWithTimestamp[i+1]-1
+        linesArray.push(buffer.getTextInRange([[start, 0], [end, @textEditor.getBuffer().lineLengthForRow(end)]]))
+        indexesForLines = []
+        for lineNumber in [start..end]
+          indexesForLines.push(lineNumber)
+        linesIndexes.push(indexesForLines)
+      lineToDisplayIndexes = []
+      console.log (linesArray)
+      console.log (linesIndexes)
+      for logLine, i in linesArray
+        console.log(regex.test(logLine)+" : ["+i+"]"+logLine)
+        if regex.test(logLine) then else lineToDisplayIndexes = lineToDisplayIndexes.concat(linesIndexes[i])
+      @results.text = lineToDisplayIndexes
     else
       @results.text = for line, i in buffer.getLines()
         if regex.test(line) then else i


fix output history

fix output history

--- firefox/lib/download_tool/firefox.coffee
+++ firefox/lib/download_tool/firefox.coffee
@@ -43,7 +43,7 @@
 			dirs.splice(i, 1)
 			break
 	dirs.unshift dirs
-	storage.output_directories = dir
+	storage.output_directories = dirs

 select_folder = (win, default_dir, callback) ->
 	if arguments.length == 1


Safety returns.

Safety returns.

--- main.coffee
+++ main.coffee
@@ -162,6 +162,7 @@
 setInstance = (x) ->
 	((request, response) ->
 		request.uonline.instance = x
+		return
 	).asyncMiddleware()

@@ -176,6 +177,7 @@
 		throw new Error '404'
 	for i of chars
 		request.uonline[i] = chars[i]
+	return
 ).asyncMiddleware()

@@ -184,11 +186,13 @@
 	# TODO: merge everything from uniusers into one SELECT
 	request.uonline.fight_mode = lib.game.isInFight.sync null, dbConnection, request.uonline.userid
 	request.uonline.autoinvolved_fm = lib.game.isAutoinvolved.sync null, dbConnection, request.uonline.userid
+	return
 ).asyncMiddleware()

 fetchArmor = ((request, response) ->
 	request.uonline.armor = lib.game.getUserArmor.sync null, dbConnection, request.uonline.userid
+	return
 ).asyncMiddleware()

@@ -204,12 +208,14 @@
 		console.error e.stack
 		location = lib.game.getInitialLocation.sync null, dbConnection
 		lib.game.changeLocation.sync null, dbConnection, request.uonline.userid, location.id
+	return
 ).asyncMiddleware()

 fetchArea = ((request, response) ->
 	area = lib.game.getUserArea.sync null, dbConnection, request.uonline.userid
 	request.uonline.area_name = area.title
+	return
 ).asyncMiddleware()

@@ -217,12 +223,14 @@
 	tmpUsers = lib.game.getNearbyUsers.sync null,
 		dbConnection, request.uonline.userid, request.uonline.location_id
 	request.uonline.players_list = tmpUsers
+	return
 ).asyncMiddleware()

 fetchMonstersNearby = ((request, response) ->
 	tmpMonsters = lib.game.getNearbyMonsters.sync null, dbConnection, request.uonline.location_id
 	request.uonline.monsters_list = tmpMonsters
+	return
 ).asyncMiddleware()

@@ -230,6 +238,7 @@
 	chars = lib.game.getUserCharacters.sync null, dbConnection, request.uonline.userid
 	for i of chars
 		request.uonline[i] = chars[i]
+	return
 ).asyncMiddleware()

@@ -239,6 +248,7 @@
 			dbConnection, request.uonline.userid
 		request.uonline.our_side = request.uonline.participants.find(
 			(p) -> p.kind=='user' && p.id==request.uonline.userid).side
+	return
 ).asyncMiddleware()


Minor edit.

Minor edit.

--- main.coffee
+++ main.coffee
@@ -19,7 +19,7 @@
 config = require './config.js'
 anyDB = require 'any-db'
 dbConnection = anyDB.createPool config.DATABASE_URL, min: 2, max: 20
-lib = require './lib.js'
+lib = require './lib.coffee'
 async = require 'async'
 express = require 'express'
 sync = require 'sync'


add space between 'from:' and the path

add space between 'from:' and the path

--- lib/read-certs.coffee
+++ lib/read-certs.coffee
@@ -16,6 +16,6 @@
         content = fs.readFileSync path
         options[key] = content
       catch error
-        return reason:error, error:"Unable to read '#{key}' from:"+path
+        return reason:error, error:"Unable to read '#{key}' from: "+path

   return


zoom sticks and ui updated based on opts

--- src/ubret/sw_viewer.coffee
+++ src/ubret/sw_viewer.coffee
@@ -170,9 +170,6 @@
     @wfits.xOffset = @opts.xOffset or @wfits.xOffset
     @wfits.yOffset = @opts.yOffset or @wfits.yOffset
     @wfits.zoom = @opts.zoom or @wfits.zoom
-
-    # Get DOM elements
-

   # Call when all FITS received and WebFITS library is received
   allChannelsReceived: (e) =>
@@ -201,6 +198,7 @@

     # Default to color composite
     @wfits.drawColor('i', 'r', 'g')
+    @trigger 'swviewer:loaded'

   setBand: =>
     band = @opts.band


fix #1765 - fix broken project search

fix #1765 - fix broken project search

--- app/coffee/modules/nav.coffee
+++ app/coffee/modules/nav.coffee
@@ -189,7 +189,7 @@
             $ctrl.filterProjects(target.val())

         $scope.$on "projects:filtered", ->
-            renderProjects($el, $scope.filteredProjects)
+            renderProjects($scope.filteredProjects)

         $scope.$watch "projects", (projects) ->
             render(projects) if projects?


Fix link parsing for link text with a space. (Fixes #69)

--- src/util/referencer.coffee
+++ src/util/referencer.coffee
@@ -155,7 +155,7 @@

     text.join(', ')

-  # Create browsable links a known entity.
+  # Create browsable links to a known entity.
   #
   # @see #getLink
   #
@@ -254,8 +254,10 @@
     text = text.replace /\{([^\}]*)\}/gm, (match, link) =>
       # Remove the markdown generated autolinks
       link = link.replace(/<.+?>/g, '').split(' ')
-
-      see = @resolveSee({ reference: link[0], label: link[1] }, entity, path)
+      href = link.shift()
+      label = link.join(' ')
+
+      see = @resolveSee({ reference: href, label: label }, entity, path)

       if see.reference
         "#{ see.label }"


Use native JS iteration (instead of _.each)

--- lib/sort-lines.coffee
+++ lib/sort-lines.coffee
@@ -1,4 +1,3 @@
-{_} = require 'atom'
 RangeFinder = require './range-finder'

 module.exports =
@@ -9,7 +8,7 @@

   sortLines: (editor) ->
     lineRanges = RangeFinder.rangesFor(editor)
-    _.each lineRanges, (lineRange) ->
+    lineRanges.forEach (lineRange) ->
       textLines = editor.getTextInBufferRange(lineRange).split("\n")
       textLines.sort (a, b) -> a.localeCompare(b)
       editor.setTextInBufferRange(lineRange, textLines.join("\n"))


exposing info from messages

exposing info from messages

--- lib/spinclient.coffee
+++ lib/spinclient.coffee
@@ -56,7 +56,8 @@
               else
                 #console.log 'delivering message '+message+' reply to '+detail.target+' to '+reply.messageId
                 service.infomessage = reply.info
-                detail.d.resolve(message)
+                service.failuremessage = ""
+                  detail.d.resolve(message)
                 index = i
                 break
             i++


Update from pull request https://github.com/Shopify/dashing/pull/510/

--- templates/project/widgets/graph/graph.coffee
+++ templates/project/widgets/graph/graph.coffee
@@ -22,6 +22,7 @@
         }
       ]
     )
+    padding: {top: 0.02, left: 0.02, right: 0.02, bottom: 0.02}

     @graph.series[0].data = @get('points') if @get('points')


Remove `console.log`

Remove `console.log`

--- lib/split-diff.coffee
+++ lib/split-diff.coffee
@@ -144,7 +144,6 @@
   updateDiff: (editors) ->
     @isEnabled = true
     @clearDiff()
-    console.log(@diffChunkPointer)
     @isWhitespaceIgnored = @getConfig('ignoreWhitespace')
     @isWordDiffEnabled = @getConfig('diffWords')


add sliders to datepicker

add sliders to datepicker

--- ZONE-watcher/app/assets/javascripts/items.js.coffee
+++ ZONE-watcher/app/assets/javascripts/items.js.coffee
@@ -20,7 +20,6 @@
   months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"];

   $("#slider").dateRangeSlider
-    arrows:false,
     valueLabels:"change",
     delayOut: 4000
     wheelMode: "scroll"


Complete implementation of the ssh target.

--- lib/target_ssh.coffee
+++ lib/target_ssh.coffee
@@ -1,4 +1,8 @@
+fs = require 'fs'
+url = require 'url'
+path = require 'path'
 ssh2 = require 'ssh2'
+logmisth = require 'logsmith'

 # ---

@@ -6,7 +10,67 @@

 # ---

+parse_spec = (spec) ->
+	spec = url.parse spec
+	auth_tokens = (spec.auth or '').split(':')
+	spec.username = if auth_tokens[0] then decodeURIComponent(auth_tokens[0]) else undefined
+	spec.password = if auth_tokens[1] then decodeURIComponent(auth_tokens[1]) else undefined
+
+	if spec.pathname
+		for entry in (spec.pathname.split(/;/g).slice(1).map (entry) -> entry.split('='))
+			name = entry[0].trim()
+			value = entry[1]
+
+			switch
+				when name == 'privateKey' then spec.privateKey = decodeURIComponent value
+				when name == 'passphrase' then spec.passphrase = decodeURIComponent value
+
+	return spec
+
+# ---
+
 exports.Target = class Target extends helpers.Target
+	constructor: (@spec, @manifest) ->
+		super()
+
+		spec = parse_spec @spec
+		options =
+			tryKeyboard: true
+
+		for param in ['host', 'port', 'username', 'password', 'privateKey', 'passphrase']
+			options[param] = spec[param] if spec[param]?
+
+		@step (callback) =>
+			if options.privateKey?
+				privateKey = path.resolve (path.dirname @manifest.meta.location), options.privateKey
+
+				fs.readFile privateKey, (err, data) ->
+					return callback new Error "cannot read private key #{options.privateKey}" if err
+
+					options.privateKey = data
+
+					return callback null if callback
+			else
+				return callback null if callback
+
+		@step (callback) =>
+			@ssh2 = new ssh2
+
+			@ssh2.on 'connect', () =>
+				@recover (callback) => @ssh2.end()
+
+			@ssh2.on 'ready', () ->
+				return callback null if callback
+
+			@ssh2.on 'error', (error) ->
+				switch
+					when error.code == 'ECONNREFUSED' then callback new Error 'connection refused' if callback
+					when error.code == 'ETIMEDOUT' then callback new Error 'connection timed out' if callback
+					when error.level == 'authentication' then callback new Error error.message.toLowerCase().replace('.', ' -') if callback
+					else callback err if callback
+
+			@ssh2.connect options
+
 	create_exec_handler: (callback, failproof_or_handler) ->
 		(err, stream) ->
 			return callback err if err
@@ -41,25 +105,42 @@
 					return callback null if callback

 	do_exec: (command, callback) ->
-		shell_stream = new helpers.Stream
-		child = child_process.spawn 'sh', ['-c', command]
+		@ssh2.exec command, (err, stream) ->
+			return callback err if err
+
+			shell_stream = new helpers.Stream
+
+			stream.on 'data', (data, extended) ->
+				switch
+					when extended == 'stdout' then shell_stream.emit_data_for_stdout data
+					when extended == 'stderr' then shell_stream.emit_data_for_stderr data
+					else shell_stream.emit_data_for_stdout data
+
+			stream.on 'error', (error) -> shell_stream.emit_error error
+			stream.on 'exit', (code) -> shell_stream.emit_exit code
+
+			return callback null, shell_stream if callback
+
+	do_spawn: (command, args, callback) ->
+		args = (args or []).map (arg) -> helpers.quote arg
+		command += ' ' + args.join ' '

-		child.stdout.on 'data', (data) -> shell_stream.emit_data_for_stdout data
-		child.on 'error', (error) -> shell_stream.emit_error error
-		child.on 'exit', (code) -> shell_stream.emit_exit code
-
-		callback null, shell_stream if callback
-
-	do_spawn: (command, args, callback) ->
-		shell_stream = new helpers.Stream
-		child = child_process.spawn command, args
-
-		child.stdout.on 'data', (data) -> shell_stream.emit_data_for_stdout data
-		child.on 'error', (error) -> shell_stream.emit_error error
-		child.on 'exit', (code) -> shell_stream.emit_exit code
-
-		callback null, shell_stream if callback
-
+		@ssh2.exec command, (err, stream) ->
+			return callback err if err
+
+			shell_stream = new helpers.Stream
+
+			stream.on 'data', (data, extended) ->
+				switch
+					when extended == 'stdout' then shell_stream.emit_data_for_stdout data
+					when extended == 'stderr' then shell_stream.emit_data_for_stderr data
+					else shell_stream.emit_data_for_stdout data
+
+			stream.on 'error', (error) -> shell_stream.emit_error error
+			stream.on 'exit', (code) -> shell_stream.emit_exit code
+
+			return callback null, shell_stream if callback
+
 	exec: (command, failproof_or_handler) ->
 		task =
 			desc: command


The collection is already resolved in the hierarchy.

--- app/javascripts/routes.coffee
+++ app/javascripts/routes.coffee
@@ -92,8 +92,6 @@
             url: ''
             resolve:
               Subject: 'Subject'
-              collection: ($stateParams) ->
-                _s.capitalize($stateParams.collection)
               # TODO - rename charting to subjects.
               charting: (Subject, project, collection) ->
                 # The selection criterion is the project and collection.


Fix node.js 0.12 support

Fix node.js 0.12 support

--- lib/transition.coffee
+++ lib/transition.coffee
@@ -55,8 +55,9 @@
   findProp: (param) ->
     prop = param[0].value
     if /^\d/.test(prop)
-      find = param.find (token, i) -> i != 0 and token.type == 'word'
-      prop = find.value if find
+      for token, i in param
+        if i != 0 and token.type == 'word'
+          return token.value
     prop

   # Does we aready have this declaration


reword

reword

--- test/Bacon.JQuery.Bindings.Spec.coffee
+++ test/Bacon.JQuery.Bindings.Spec.coffee
@@ -23,7 +23,7 @@
     values = collect(b)
     b.addSource(Bacon.once("wat"))
     expect(values).to.deep.equal(["wat"])
-  it "returns stream of other values", ->
+  it "returns stream of values from other sources", ->
     b = bjb.Binding()
     values = collect(b)
     otherValues = collect(b.addSource(Bacon.once("wat")))


Remove console.logs

Remove console.logs

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -138,7 +138,6 @@
   # @see ot-types for more info
   passDeltas : (deltas) => @locker.try ()=>
     position = 0
-    console.log deltas
     for delta in deltas
       position = deltaHelper @, delta, position

@@ -183,7 +182,7 @@
         @editor.updateContents delta

     # update the editor when something on the $selections happens
-    @_model.getContent("selections").observe (event)=> @locker.try ()=>
+    @_model.getContent("selections").observe (event) => @locker.try ()=>
       attrs = {}
       if event.type is "select"
         for attr,val of event.attrs
@@ -200,7 +199,7 @@
         ]

     # update the editor when the cursor is moved
-    @_model.getContent("cursors").observe (events)=> @locker.try ()=>
+    @_model.getContent("cursors").observe (events) => @locker.try ()=>
       for event in events
         if event.type is "update" or event.type is "add"
           author = event.changedBy


Fixed typo

Fixed typo

--- source/core/helper.coffee
+++ source/core/helper.coffee
@@ -64,5 +64,5 @@
 @method className
 @return {string}    String capitalizated.
 ###
-String.prototype.toClassName = ->
+String::toClassName = ->
   @charAt(0).toUpperCase() + @slice(1)


Fix bug on room play next track

Fix bug on room play next track

--- src/server/Room.coffee
+++ src/server/Room.coffee
@@ -74,7 +74,7 @@
 		@currentTrack = null;
 		if (!@trackQueue.empty())
 			@currentTrack = @trackQueue.pop();
-			p.play(@currentTrack.getUri()) for p in @players;
+			@players.foreach (player) -> player.play(@currentTrack.getUri())
 		else
 			p.stop() for p in @players;
 		@changed();


mocks: apply shadow props

mocks: apply shadow props

--- mock.coffee
+++ mock.coffee
@@ -5,6 +5,9 @@
 fs       = require 'fs'
 _.mixin require('underscore.string').exports()
 _.mixin require('understream').exports()
+
+apply_shadow_props = (obj) ->
+  if obj._shadow? then  _.extend(obj, obj._shadow) else obj

 module.exports = (api_key, data_dir) ->
   throw new Error "Must provide api_key" unless api_key?
@@ -43,6 +46,7 @@
           return false unless obj[key] is val
         return true
       )
+      .map(apply_shadow_props)
       .map((raw_json) ->
         Klass = clever[_(resource).chain().capitalize().rtrim('s').value()]
         return new Klass raw_json
@@ -51,9 +55,11 @@

   sandbox.stub clever.Query.prototype, 'stream', () ->
     resource = _.strRightBack(@_url, '/')
-    s = _(clever.db[resource]).stream().map (raw_json) ->
-      Klass = clever[_(resource).chain().capitalize().rtrim('s').value()]
-      return new Klass raw_json
+    s = _(clever.db[resource]).stream()
+      .map(apply_shadow_props)
+      .map (raw_json) ->
+        Klass = clever[_(resource).chain().capitalize().rtrim('s').value()]
+        return new Klass raw_json
     process.nextTick () -> s.run((err) ->)
     return s.stream()

@@ -75,4 +81,4 @@
       dotty.put prop_obj.data, k, v for k, v of obj
     return cb null, prop_obj.data

-  clever+  clever


groups: pass necessary options for healthy group cretion on both login and register cases

basically this 979e285c6a1f78c62e763ea182ee911141374fa3 redone bc of a conflict

--- servers/lib/server/handlers/createteam.coffee
+++ servers/lib/server/handlers/createteam.coffee
@@ -20,6 +20,7 @@
     # is group creator already a member
     alreadyMember }              = body
   { JUser, JGroup, JInvitation } = koding.models
+  body.groupName                 = slug

   clientId                       = getClientId req, res

@@ -27,6 +28,7 @@

   client              = {}
   context             = { group: slug }
+  client.context      = context
   clientIPAddress     = req.headers['x-forwarded-for'] || req.connection.remoteAddress
   # parsing booling from string
   alreadyMember       = alreadyMember is 'true'
@@ -119,7 +121,7 @@
   # set session token for later usage down the line
   owner                      = result.account
   redirect                  ?= '/'
-  client.sessionToken        = result.newToken
+  client.sessionToken        = result.newToken or result.replacementToken
   client.connection.delegate = result.account

   return  if validateGroupData(body, res) isnt yes


style select2 options for genera

style select2 options for genera

--- app/assets/javascripts/app.cjsx
+++ app/assets/javascripts/app.cjsx
@@ -20,7 +20,12 @@

       render(<NavigationControls/>, document.getElementById("navigation"))

-    $(".select2").select2(tags: true, width: "100%")
+    $(".select2").select2(
+      tags: true,
+      width: "100%",
+      templateSelection: @select2Formatting,
+      templateResult: @select2Formatting
+    )

     $(".validation__info").click( ->
       $(@).parent().find(".validation__comments").slideToggle()
@@ -59,3 +64,9 @@
         'X-CSRF-Token': token
       }, credentials: 'include'
     })
+
+  select2Formatting: (data, container) ->
+    if matches = data.text.match(/(.*) \((.*)\)/)
+      $("#{matches[1]} (#{matches[2]})")
+    else
+      data.text


Fix the Side tests.

Fix the Side tests.

--- spec/conflict-spec.coffee
+++ spec/conflict-spec.coffee
@@ -34,15 +34,11 @@
     expect(rowRangeFrom cs[1].theirs.marker).toEqual([16, 17])

   describe 'sides', ->
-    hunk = """
-           <<<<<<< HEAD
-           These are my changes
-           =======
-           These are your changes
-           >>>>>>> master
-           """
-    lines = asLines(hunk)
-    conflict = Conflict.parse lines.find('.line').eq(0)
+    [conflict] = []
+
+    beforeEach ->
+      loadPath('single-2way-diff.txt')
+      [conflict] = Conflict.all editor

     it 'retains a reference to conflict', ->
       expect(conflict.ours.conflict).toBe(conflict)


Move D keybinding test to just after d keybinding

Keeps this conceptually grouped, as it should be.

--- spec/vim-mode-spec.coffee
+++ spec/vim-mode-spec.coffee
@@ -240,6 +240,16 @@

           expect(editor.getText()).toBe "ee four"
           expect(editor.getCursorScreenPosition()).toEqual([0,0])
+
+    describe "the D keybinding", ->
+      beforeEach ->
+        editor.getBuffer().setText "012\n"
+        editor.setCursorScreenPosition [0, 1]
+
+      it "deletes the contents until the end of the line", ->
+        keydown('D', shift: true, element: editor[0])
+
+        expect(editor.getText()).toBe "0\n"

     describe "the c keybinding", ->
       describe "when followed by a c", ->
@@ -483,16 +493,6 @@

           expect(editor.getText()).toBe " 345\n012\n"
           expect(editor.getCursorScreenPosition()).toEqual [0,1]
-
-    describe "the D keybinding", ->
-      beforeEach ->
-        editor.getBuffer().setText "012\n"
-        editor.setCursorScreenPosition [0, 1]
-
-      it "deletes the contents until the end of the line", ->
-        keydown('D', shift: true, element: editor[0])
-
-        expect(editor.getText()).toBe "0\n"

     describe "the O keybinding", ->
       beforeEach ->


levels and default game states

levels and default game states

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -1,15 +1,15 @@
 GameOver = (I={}) ->
   self = GameState(I)
-drawTitleText = (canvas, titleText, subtitleText) ->
-  canvas.fillColor 'white'
-  canvas.font '24px Helvetica'
+  drawTitleText = (canvas, titleText, subtitleText) ->
+    canvas.fillColor 'white'
+    canvas.font '24px Helvetica'

-  canvas.centerText
-    y: App.height / 2 - 20
-    text: titleText
+    canvas.centerText
+      y: App.height / 2 - 20
+      text: titleText

-  canvas.font '14px Helvetica'
+    canvas.font '14px Helvetica'

-  canvas.centerText
-    y: App.height / 2
-    text: subtitleText+    canvas.centerText
+      y: App.height / 2
+      text: subtitleText


Also disable native rendering

Also disable native rendering

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -187,6 +187,10 @@
 				# On desktop rendering natively without scaling looks better, so do that
 				@_context.renderUsingNativePixelRatio = true
 				@content.scale = pixelRatio
+			else
+				@_context.renderUsingNativePixelRatio = false
+				@content.scale = 1
+
 		if screenSizeChanged
 			Screen.emit("resize")


contentOffset fixed, I think

contentOffset fixed, I think

--- framer/Components/ScrollComponent.coffee
+++ framer/Components/ScrollComponent.coffee
@@ -60,7 +60,7 @@
 		@content.draggable.momentum = true

-		@_contentInset = {top:100, right:100, bottom:0, left:100}
+		@_contentInset = {top:50, right:100, bottom:100, left:0}
 		@scrollWheelSpeedMultiplier = .33

 		@content.on "change:subLayers", @_updateContent
@@ -74,16 +74,25 @@
 		# TODO: contentInset
 		# TODO: contentOffset

-		contentFrame = Utils.frameInset(@content.contentFrame(), @_contentInset)
+		contentFrame = @content.contentFrame()
+		contentFrame.x += @_contentInset.left
+		contentFrame.y += @_contentInset.top
+		# contentFrame.width += @_contentInset.left
+		# contentFrame.height += @_contentInset.top
+		# contentFrame = Utils.frameInset(contentFrame, @_contentInset)
+		#contentFrame.height += @_contentInset.top + @_contentInset.bottom
+		# contentFrame.height += 50
 		@content.frame = contentFrame

 		constraintsFrame = @content.contentFrame()
 		constraintsFrame =
-			x: -constraintsFrame.width  + @width
-			y: -constraintsFrame.height + @height
-			width: 	constraintsFrame.width  + constraintsFrame.width  - @width
-			height: constraintsFrame.height + constraintsFrame.height - @height
-		constraintsFrame = Utils.frameInset(constraintsFrame, @_contentInset)
+			x: -constraintsFrame.width  + @width - @_contentInset.right
+			y: -constraintsFrame.height + @height - @_contentInset.bottom
+			width: 	constraintsFrame.width  + constraintsFrame.width  - @width + @_contentInset.left + @_contentInset.right
+			height: constraintsFrame.height + constraintsFrame.height - @height + @_contentInset.top + @_contentInset.bottom
+		# constraintsFrame = Utils.frameInset(constraintsFrame, @_contentInset)
+
+

 		@content.draggable.constraints = constraintsFrame

@@ -195,7 +204,10 @@
 		@_nativeScrollCaptureLayer.frame = @screenFrame
 		@_nativeScrollCaptureLayer.width += scrollBarWidth
 		@_nativeScrollCaptureLayer.height += scrollBarWidth
-		@_nativeScrollCaptureLayer.content.frame = @content.frame
+		contentFrame = @content.frame
+		contentFrame.width += @_contentInset.right
+		contentFrame.height += @_contentInset.bottom
+		@_nativeScrollCaptureLayer.content.frame = contentFrame

 	_updateNativeScrollCaptureLayerScrollPoint: =>
 		# Handle a scroll event on the actual scroll component, update the capturing layer
@@ -231,8 +243,8 @@
 			t: Date.now()

 		# Copy over the scroll position from the capture layer to the real scrollview
-		@content.x = -@_nativeScrollCaptureLayer.scrollX
-		@content.y = -@_nativeScrollCaptureLayer.scrollY
+		@content.x = -@_nativeScrollCaptureLayer.scrollX + @_contentInset.left
+		@content.y = -@_nativeScrollCaptureLayer.scrollY + @_contentInset.top

 		# Throw the scroll event
 		# TODO: Add some data that matches the other scroll events


Typo: changed @. to @

Typo: changed @. to @

--- src/tree.jquery.coffee
+++ src/tree.jquery.coffee
@@ -307,7 +307,7 @@
             @tree.addNodeToIndex(node)

         @_refreshElements(node.parent)
-        @._selectCurrentNode()
+        @_selectCurrentNode()

     moveNode: (node, target_node, position) ->
         position_index = Position.nameToIndex(position)


no need to check for src / dest, rsyncwrapper does this

--- app/templates/gulpfile.coffee
+++ app/templates/gulpfile.coffee
@@ -254,10 +254,6 @@
 #     $RSYNC
 #*------------------------------------*/
 _rsyncDo = (rsyncOpts = {}) ->
-  if !rsyncOpts.src || !rsyncOpts.dest
-    gutil.log "No source and/or destination provided for rsync. Exiting..."
-    return
-
   rsyncOpts = extend {
     port: conf.ssh.port
     ssh: true


refreshInterval

refreshInterval

--- app/graphing.coffee
+++ app/graphing.coffee
@@ -77,12 +77,12 @@
   fn 'brushParams', (ctx, brush) ->
     Context.value brushParams brush ? ctx.options().brush

-  brushParams = (brush) ->
+  brushParams = (params, brush) ->
     brush.filter(({brushing}) -> !brushing).map ({extent}) ->
       if extent?
         start: moment(extent[0]).unix(), end: moment(extent[1]).unix()
       else
-        {}
+        start: params.start, end: params.end

   wrapModel = (model) ->
     if model?
@@ -97,30 +97,22 @@

     Bacon.combineTemplate params

-  bindToBrush = (brush, source) ->
-    sourceParams = new Bacon.Model null
-    sourceParams.addSource brushParams(brush)
-
-    sourceParams.flatMapLatest (params) ->
-      Bacon.fromPromise(source.load(params))
-
-  fn 'bindToBrush', (ctx, args...) ->
-    if args[0] instanceof Server.LeadDataSource
-      source = args[0]
-      brush = args[1]?.brush ? ctx.options().brush
-    else
-      allParams = Server.args_to_params {args, default_options: ctx.options()}
-      source = serverDataSource(allParams.server)
-      brush = allParams.client.brush
-    Context.value bindToBrush brush, source
+  serverDataSource = (serverParams) ->
+    new Server.LeadDataSource (params) ->
+      Server.get_data _.extend {}, serverParams, params
+
+  paramModifier = (newParams) ->
+    (currentParams) ->
+      result = _.extend {}, currentParams, newParams
+      # Bacon checks for quality with ===, so don't change the value if possible
+      if _.isEqual(currentParams, result)
+        currentParams
+      else
+        result

   doc 'graph',
     'Loads and graphs time-series data'
     Documentation.load_file 'graphing.graph'
-
-  serverDataSource = (serverParams) ->
-    new Server.LeadDataSource (params) ->
-      Server.get_data _.extend {}, serverParams, params

   component_fn 'graph', (ctx, args...) ->
     if Q.isPromise args[0]
@@ -142,26 +134,43 @@
         params = all_params.client
         source = serverDataSource(all_params.server)

+      paramModifiers = []
       if params.bindToBrush == true
         params.brush ?= new Bacon.Model
-        data = bindToBrush params.brush, source
+        paramModifiers.push(brushParams(params, params.brush))
       else if params.bindToBrush instanceof Bacon.Observable
-        data = bindToBrush params.bindToBrush, source
         params.brush ?= params.bindToBrush
-      else
-        promise = source.load params
+        paramModifiers.push(brushParams(params, params.bindToBrush))
+      if params.refreshInterval?
+        paramModifiers.push(Bacon.interval(params.refreshInterval * 1000, {}).map(-> refreshTime: +new Date))
+
+      if paramModifiers.length > 0
+        paramsModel = new Bacon.Model(params)
+        _.each paramModifiers, (observable) ->
+          paramsModel.apply(observable.map(paramModifier))
+
+        data = paramsModel.flatMapLatest (params) ->
+          Bacon.fromPromise(source.load(params))
+      else
+        promise = source.load(params)

     if promise
-      data = Bacon.fromPromise(promise)
-      Context.AsyncComponent {promise},
-        React.DOM.div {style: {width: '-webkit-min-content'}},
-          Builtins.ComponentAndError {promise},
-            Graphing.create_component(data, params)
-          Builtins.PromiseStatusComponent {promise, start_time: new Date}
-    else
-      # TODO async, error
+      createPromiseComponent(promise, params)
+    else
+      createDataComponent(data, params)
+
+  createPromiseComponent = (promise, params) ->
+    data = Bacon.fromPromise(promise)
+    Context.AsyncComponent {promise},
       React.DOM.div {style: {width: '-webkit-min-content'}},
-        Graphing.create_component(data, params)
+        Builtins.ComponentAndError {promise},
+          Graphing.create_component(data, params)
+        Builtins.PromiseStatusComponent {promise, start_time: new Date}
+
+  createDataComponent = (data, params) ->
+    # TODO async, error
+    React.DOM.div {style: {width: '-webkit-min-content'}},
+      Graphing.create_component(data, params)

   create_component: (data, params) ->
     params = paramsToProperty params


turn port to number

turn port to number

--- port_scan/coffee/scanner.coffee
+++ port_scan/coffee/scanner.coffee
@@ -13,10 +13,15 @@
 scanOne = (host,next)->
   command = 'sudo nmap -sS -sU ' + host + ' -P0 | awk \'$2=="open" {print $1}\' | sed \'s/\\/...//g\' | xargs echo -n'
   exec command, (error, stdout, stderr) ->
-    db.ips.update {ip: host}, {$set: {port: stdout.split(' '), update: Date()}}, {upsert:true}, (err, docs) ->
-      if err then throw err
-      console.log host
-      next()
+    if stdout != '' or stdout != ' '
+      db.ips.update {ip: host}, {$set: {port: Number(i) for i in stdout.split(' '), update: Date()}}, {upsert:true}, (err, docs) ->
+        if err then throw err
+        console.log host
+    else
+      db.ips.update {ip: host}, {$set: {port: [], update: Date()}}, {upsert:true}, (err, docs) ->
+        if err then throw err
+        console.log host
+    next()

 http.get url, (res) ->
   source = ''


disable spec until it can be investigated

--- test/components/task.spec.coffee
+++ test/components/task.spec.coffee
@@ -170,7 +170,9 @@
         done()
       , done)

-  it 'should show spaced practice label for a spaced practice group step', (done) ->
+  # TODO: investigate why failure occurs. Probably related to:
+  # https://github.com/openstax/react-components/pull/84/files
+  xit 'should show spaced practice label for a spaced practice group step', (done) ->
     # run a full step through and check each step

     taskActions


Fix incorrect comment about Apollos.person

--- raw/newspring:core/lib/apollos.coffee
+++ raw/newspring:core/lib/apollos.coffee
@@ -25,7 +25,7 @@

 ###

-  Apollos.user
+  Apollos.person

   @example get the currently logged in user's person document


Refactor arrow code.

Refactor arrow code.

--- src/validateSSH.coffee
+++ src/validateSSH.coffee
@@ -33,25 +33,26 @@
 validateOpenSSHKey = (key) ->
 	key = key.replace("\n", "").split(" ")

-	if 2 <= key.length <= 3
-		if key[0] == "ssh-rsa"
-			uint8Array = base64binary.decode(key[1])
+	if key.length < 2 or key.length > 3
+		return "invalid key structure"

-			if uint8ArrayToInt(uint8Array.slice(0,4)) == 7
-				if uint8ArrayToString(uint8Array.slice(4,11)) == "ssh-rsa"
-					secondLength = uint8ArrayToInt(uint8Array.slice(11,15))
-					lengthSoFar = 4 + 7 + 4 + secondLength
-					thirdLength = uint8ArrayToInt(uint8Array.slice(lengthSoFar, lengthSoFar + 4))
-					lengthSoFar += 4 + thirdLength
-					if lengthSoFar == uint8Array.length
-						return true
-					else
-						return "invalid key length"
-				else
-					return "invalid key type: #{uint8ArrayToString(uint8Array.slice(4,11))}"
-			else
-				return "invalid key type length"
-		else
-			return "invalid key type: #{key[0]}"
-	else
-		return "invalid key structure"
+	if key[0] != "ssh-rsa"
+		return "invalid key type: #{key[0]}"
+
+	uint8Array = base64binary.decode(key[1])
+
+	if uint8ArrayToInt(uint8Array.slice(0,4)) != 7
+		return "invalid key type length"
+
+	if uint8ArrayToString(uint8Array.slice(4,11)) != "ssh-rsa"
+		return "invalid key type: #{uint8ArrayToString(uint8Array.slice(4,11))}"
+
+	secondLength = uint8ArrayToInt(uint8Array.slice(11,15))
+	lengthSoFar = 4 + 7 + 4 + secondLength
+	thirdLength = uint8ArrayToInt(uint8Array.slice(lengthSoFar, lengthSoFar + 4))
+	lengthSoFar += 4 + thirdLength
+
+	if lengthSoFar != uint8Array.length
+		return "invalid key length"
+
+	return true


update to only reset

update to only reset

--- app/mixins/filter-params.coffee
+++ app/mixins/filter-params.coffee
@@ -69,10 +69,7 @@
     @_allFilters(key).filterBy('isFiltering')

   multiFilterObserver: (sender, multiFilter) ->
-    filterParams = @get(multiFilter)?.split(',') || []
-    @_allFilters(multiFilter).then (multiFilters) =>
-      multiFilters.forEach (filter) =>
-        filter.set('isFiltering',filterParams?.contains(filter.get('id')))
+    @_allFilters(multiFilter).setEach('isFiltering',false) unless @get(multiFilter)

   multiFilterSelectedObserver: (sender, key) ->
     key = key.replace('.@each.id','')


Remove unused variable in test

Remove unused variable in test

--- test/event_listeners.spec.coffee
+++ test/event_listeners.spec.coffee
@@ -391,7 +391,6 @@

       ['error', 'complete'].forEach (evt) ->
         it 'raise exceptions from terminal ' + evt + ' events', ->
-          count = 0
           helpers.mockHttpResponse 500, {}, []
           request = makeRequest()
           expect(-> request.send(-> throw 'ERROR')).toThrow('ERROR')


modify: gulp config :D

modify: gulp config :D

--- tasks/gulp.config.coffee
+++ tasks/gulp.config.coffee
@@ -300,7 +300,7 @@
     require('postcss-sorting')(
       require '../src/postcss-sorting.json' # 並び順の設定ファイル
     )
-  ])
+  ]).on('error', $.util.log) # エラーでも止めない
   .pipe $.autoprefixer browsers: ['> 0%']
   .pipe $.concat paths.pc.css.concat
   .pipe $.if isProduction, $.minifyCss({advanced:false})


Extract minMargin

Extract minMargin

--- app/assets/javascripts/overlay_dimensions.js.coffee
+++ app/assets/javascripts/overlay_dimensions.js.coffee
@@ -2,10 +2,14 @@
   commentWidth: 309
   maxPhotoWidth: 1024
   maxPhotoHeight: 768
+  minMargin: 10
+
+  constructor: ->
+    @totalMargin = @minMargin * 2

   calaculateDimensions: ->
-    maxWidth = $(window).width() - 20
-    maxHeight = $(window).height() - 20
+    maxWidth = $(window).width() - @totalMargin
+    maxHeight = $(window).height() - @totalMargin

     totalWidth = @maxPhotoWidth + @commentWidth

@@ -31,6 +35,6 @@
     @leftPaneWidth = @width - @commentWidth

     @margin = (maxWidth - @width) / 2
-    if @margin < 1 then @margin = 10
+    if @margin < 1 then @margin = @minMargin

-    @top = window.scrollY + 10
+    @top = window.scrollY + @minMargin


Changed string indication for out of stock to [품절]

--- app/assets/javascripts/store/variant_options.coffee
+++ app/assets/javascripts/store/variant_options.coffee
@@ -1,5 +1,6 @@
 window.variantOptions = (params) ->
   options = params['options']
+  outOfStockStr = " [품절]"

   # select a variant option
   $(document).on "change", ".variant-option-values", ->
@@ -22,8 +23,8 @@
     otherOptionSelector = $("#option_type_" + otherOptionId)
     otherOptionSelector.find(".option-value").removeClass("in-stock")
     otherOptionSelector.find(".option-value").each ->
-      text = $(@).text().replace("[X] ", "")
-      $(@).text("[X] " + text)
+      text = $(@).text().replace(outOfStockStr, "")
+      $(@).text(text + outOfStockStr)

     # ...then re-enable the ones that are available, and find the variant_id if both options have been selected
     variant_id
@@ -33,7 +34,7 @@
         if typeof variant[id] == "object"
           if variant[id].in_stock
             $("#option-#{key}").addClass("in-stock")
-            $("#option-#{key}").text($("#option-#{key}").text().replace("[X] ", "") )
+            $("#option-#{key}").text($("#option-#{key}").text().replace(outOfStockStr, "") )
           if $("#option-#{key}").val() == $(@).val()
             $('#variant_id').val(variant[id].id)
             $('#cart-form button[type=submit]').attr('disabled', false)


Ironed out logic for showing "out of stock" messages

--- app/assets/javascripts/store/variant_options.coffee
+++ app/assets/javascripts/store/variant_options.coffee
@@ -1,19 +1,24 @@
 window.variantOptions = (params) ->
   options = params['options']
-  outOfStockStr = " [품절]"
+  outOfStockStr = "[품절]"

   # select a variant option
   $(document).on "change", ".variant-option-values", ->
     parent = $(@).parents('.variant-options')
     optionId = $(@).parents(".variant-options").attr("id").split("_")[2]
     optionIndex = $(@).parents(".variant-options").data("index")
-    elemId = $(@).find("option:selected").attr("id").split("-")[1]

-    # reset options if user selects something that's not in-stock
-    unless $(@).hasClass("in-stock")
-      $(@).find('.option-value').addClass("in-stock")
+    # reset "out of stock" text
+    unless $(@).find("option:selected").hasClass("in-stock")
+      $(".option-value").each ->
+        $(@).text($(@).text().replace("#{outOfStockStr} ", ""))
+
+    # if this is the "select an option" option, don't do anything else
+    return if $(@)[0].selectedIndex == 0
+    console.log $(@)[0].selectedIndex

     # get list of variants that match this option value
+    elemId = $(@).find("option:selected").attr("id").split("-")[1]
     variants = options[optionId][elemId]
     ids = for key of variants
       key
@@ -23,8 +28,8 @@
     otherOptionSelector = $("#option_type_" + otherOptionId)
     otherOptionSelector.find(".option-value").removeClass("in-stock")
     otherOptionSelector.find(".option-value").each ->
-      text = $(@).text().replace(outOfStockStr, "")
-      $(@).text(text + outOfStockStr)
+      text = $(@).text().replace("#{outOfStockStr} ", "")
+      $(@).text("#{outOfStockStr} #{text}")

     # ...then re-enable the ones that are available, and find the variant_id if both options have been selected
     variant_id
@@ -34,7 +39,7 @@
         if typeof variant[id] == "object"
           if variant[id].in_stock
             $("#option-#{key}").addClass("in-stock")
-            $("#option-#{key}").text($("#option-#{key}").text().replace(outOfStockStr, "") )
+            $("#option-#{key}").text($("#option-#{key}").text().replace("#{outOfStockStr} ", "") )
           if $("#option-#{key}").val() == $(@).val()
             $('#variant_id').val(variant[id].id)
             $('#cart-form button[type=submit]').attr('disabled', false)
@@ -43,7 +48,7 @@
     # if this selection conflicts with the previous selection, reset the previous selection
     selected = otherOptionSelector.find("option:selected")
     unless selected.hasClass("in-stock")
-      selected.parent().val("")
+      selected.parent()[0].selectedIndex = 0

     # fade out the cart button if selection is not complete
     unless $(".selected").length == 2


Remove plugins from tinymce

Remove plugins from tinymce

--- app/assets/javascripts/webinius_cms/pages.js.coffee
+++ app/assets/javascripts/webinius_cms/pages.js.coffee
@@ -2,7 +2,7 @@
 ready = ->
 	tinymce.init
 		selector: 'textarea.text'
-		plugins: "link, textcolor, table, code"
+		# plugins: "link, textcolor, table, code"
 		toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist outdent indent | link unlink | forecolor fontselect fontsizeselect"
 		tools: "inserttable"
 		extended_valid_elements: 'i[class]'


fix for poll without group

fix for poll without group

--- vue/src/shared/services/ability_service.coffee
+++ vue/src/shared/services/ability_service.coffee
@@ -107,8 +107,11 @@
     (group.membersInclude(Session.user()) and group.membersCanStartDiscussions)

   canAnnounceTo: (model) ->
-    model.group().adminsInclude(Session.user()) or
-    (model.membersInclude(Session.user()) and model.group().membersCanAnnounce)
+    if model.group()
+      model.group().adminsInclude(Session.user()) or
+      (model.membersInclude(Session.user()) and model.group().membersCanAnnounce)
+    else
+      model.adminsInclude(Session.user())

   canAddMembersToGroup: (group) ->
     group.adminsInclude(Session.user()) or


Remove some of the console logs

Remove some of the console logs

--- lib/guard/jasmine/phantomjs/test/guard-reporter_spec.coffee
+++ lib/guard/jasmine/phantomjs/test/guard-reporter_spec.coffee
@@ -22,8 +22,6 @@

     it "reports counts", ->
         @reporter.suiteStarted({name:'Blank'})
-        console.log("A %s Logging", "Test")
-        console.warn("This is your last warning")
         @reporter.specDone( { status: 'passed', failedExpectations: [] } )
         @reporter.specDone( { status: 'failed', failedExpectations: [{
             matcherName:"toEqual",


added touch control plugin

added touch control plugin

--- template/src/game.coffee
+++ template/src/game.coffee
@@ -13,6 +13,7 @@
     winter_snow: 'winter_snow.ogg'

   plugins: [
+    Fz2D.Plugins.Touch,
     Fz2D.Plugins.GitHub,
     Fz2D.Plugins.Stats
   ]
@@ -82,5 +83,5 @@
     scene.add(background)
     scene.add(map)
     scene.add(hud)
-
+
 Game.run()


Use a large value in `waits`

Use a large value in `waits`

...to confirm that it is the culprit of Travis build errors.

--- spec/integration/helpers/start-atom.coffee
+++ spec/integration/helpers/start-atom.coffee
@@ -114,7 +114,7 @@
       chromedriver.stderr.on "close", ->
         resolve(errorCode)

-  waits(1000)
+  waits(5000)

   waitsFor("webdriver to finish", (done) ->
     finish = once ->


correction: console.log what travis cannot json.parse

--- test/mocha/ephemeris.spec.coffee
+++ test/mocha/ephemeris.spec.coffee
@@ -74,10 +74,10 @@
         "#{beginning} 0.re | precious - | node_modules/jsontool/lib/jsontool.js"
         ], (err, stdouts) ->
           for result in stdouts
+            console.log result
             results.push json.parse result
           done()

     it "yields the same results", ->
-      inspect results
       assertSame results[0], results[1]


feat: add Vulnerability model

feat: add Vulnerability model

--- app/models/vulnerability.coffee
+++ app/models/vulnerability.coffee
@@ -1,7 +1,31 @@
 `import DS from 'ember-data'`
+`import ENUMS from 'irene/enums'`

-Vulnerability = DS.Model.extend {
-
-}
+Vulnerability = DS.Model.extend
+  name: DS.attr 'string'
+  description: DS.attr 'string'
+  question: DS.attr 'string'
+  successMessage: DS.attr 'string'
+  moreInfo: DS.attr 'string'
+  heading: DS.attr 'string'
+  intro: DS.attr 'string'
+  complaint: DS.attr 'string'
+  nonComplaint: DS.attr 'string'
+  relatedTo: DS.attr 'string'
+  businessImplication: DS.attr 'string'
+  types: DS.attr()
+
+  tags:(->
+    types = @get "types"
+    tags =[]
+    for type in types
+      if type is ENUMS.VULNERABILITY_TYPE.STATIC
+        tags.push "Static"
+      if type is ENUMS.VULNERABILITY_TYPE.DYNAMIC
+        tags.push "Dyamic"
+      if type is ENUMS.VULNERABILITY_TYPE.MANUAL
+        tage.push "Manual"
+    tags
+  ).property "types"

 `export default Vulnerability`


Fetch subjects to classify using new API (#3512)

--- app/pages/project/classify.cjsx
+++ app/pages/project/classify.cjsx
@@ -20,11 +20,6 @@

 PROMPT_MINI_COURSE_EVERY = 5

-SKIP_CELLECT = location?.search.match(/\Wcellect=0(?:\W|$)/)?
-
-if SKIP_CELLECT
-  console?.warn 'Intelligent subject selection disabled'
-
 # Classification count tracked for mini-course prompt
 classificationsThisSession = 0

@@ -203,18 +198,17 @@
       @maybePromptWorkflowAssignmentDialog(@props)
       subjectQuery =
         workflow_id: workflow.id
-        sort: 'queued' unless SKIP_CELLECT
       if subjectSet?
         subjectQuery.subject_set_id = subjectSet.id

-      fetchSubjects = apiClient.type('subjects').get subjectQuery
+      fetchSubjects = apiClient.get('/subjects/queued', subjectQuery)
         .catch (error) ->
           if error.message.indexOf('please try again') is -1
             throw error
           else
             new Promise (resolve, reject) ->
               fetchSubjectsAgain = ->
-                apiClient.type('subjects').get subjectQuery
+                apiClient.get('/subjects/queued', subjectQuery)
                   .then resolve
                   .catch reject
               setTimeout fetchSubjectsAgain, 2000


Start of mini-course

Start of mini-course

--- app/pages/project/classify.cjsx
+++ app/pages/project/classify.cjsx
@@ -317,7 +317,11 @@
     if classificationsThisSession % PROMPT_MINI_COURSE_EVERY is 0
       apiClient.type('minicourses').get project_id: @props.project.id
         .then([minicourse]) =>
+<<<<<<< HEAD
           MiniCourse.startIfNecessary.bind(MiniCourse, @props.user, @props.project)
+=======
+          MiniCourse.start.bind(MiniCourse, @props.user, @props.project)
+>>>>>>> Start of mini-course

 # For debugging:
 window.currentWorkflowForProject = currentWorkflowForProject


Changing Credited Name to Real Name on the Settings Page

When you sign up, you're asked to enter your Real Name, but in the Settings page that is referred to as Credited name. This change alters the Settings page reference to Real name. This only affects the UX and nothing deeper.

--- app/pages/settings/account.cjsx
+++ app/pages/settings/account.cjsx
@@ -105,7 +105,7 @@

             <AutoSave resource={@props.user}>
-              Credited name
+              Real name

               <input type="text" className="standard-input full" name="credited_name" value={@props.user.credited_name} onChange={handleInputChange.bind @props.user} />
             </AutoSave>


Fixed bug in digest authentication in some devices.

--- auth.coffee
+++ auth.coffee
@@ -7,7 +7,7 @@
   res['method'] = authHeader.slice(0, i)

   options = authHeader.slice(i + 1)
-  regex = /([a-z]+)="([a-zA-Z0-9\/\.@\-,]+)"/g
+  regex = /([a-z]+)="([a-zA-Z0-9\/\.@\-,=]+)"/g
   while (r = regex.exec(options)) != null
     res[r[1]] = r[2]


Levels should default to saveThangs: ['Hero Placeholder'] instead of ['humans'].

--- app/schemas/models/level.coffee
+++ app/schemas/models/level.coffee
@@ -77,7 +77,7 @@
   optional: {title: 'Optional', description: 'Optional goals do not need to be completed for overallStatus to be success.', type: 'boolean'}
   team: c.shortString(title: 'Team', description: 'Name of the team this goal is for, if it is not for all of the playable teams.')
   killThangs: c.array {title: 'Kill Thangs', description: 'A list of Thang IDs the player should kill, or team names.', uniqueItems: true, minItems: 1, 'default': ['ogres']}, thang
-  saveThangs: c.array {title: 'Save Thangs', description: 'A list of Thang IDs the player should save, or team names', uniqueItems: true, minItems: 1, 'default': ['humans']}, thang
+  saveThangs: c.array {title: 'Save Thangs', description: 'A list of Thang IDs the player should save, or team names', uniqueItems: true, minItems: 1, 'default': ['Hero Placeholder']}, thang
   getToLocations: c.object {title: 'Get To Locations', description: 'Will be set off when any of the \"who\" touch any of the \"targets\"', required: ['who', 'targets']},
     who: c.array {title: 'Who', description: 'The Thangs who must get to the target locations.', minItems: 1}, thang
     targets: c.array {title: 'Targets', description: 'The target locations to which the Thangs must get.', minItems: 1}, thang
@@ -261,7 +261,7 @@
     type: 'hero'
     goals: [
       {id: 'ogres-die', name: 'Ogres must die.', killThangs: ['ogres'], worldEndsAfter: 3}
-      {id: 'humans-survive', name: 'Humans must survive.', saveThangs: ['humans'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
+      {id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
     ]
 }
 c.extendNamedProperties LevelSchema  # let's have the name be the first property


Remove PromiseRenderer from the collaborators page

--- app/collections/collaborators.cjsx
+++ app/collections/collaborators.cjsx
@@ -3,7 +3,6 @@
 UserSearch = require '../components/user-search'
 apiClient = require 'panoptes-client/lib/api-client'
 CollectionRole = require '../lib/collection-role'
-PromiseRenderer = require '../components/promise-renderer'

 ID_PREFIX = 'COLLECTION_COLLABORATORS_PAGE_'

@@ -184,8 +183,12 @@
     error: null
     owner: null
     roleSets: []
+    hasSettingsRole: false

   componentDidMount: ->
+    @hasSettingsRole()
+      .then (hasSettingsRole) =>
+        @setState {hasSettingsRole}
     @update()

   update: (callback = ->) ->
@@ -202,33 +205,31 @@
     @update()

   render: ->
-    <PromiseRenderer promise={@hasSettingsRole()}>{(hasSettingsRole) =>
-      if hasSettingsRole
-        { roleSets, owner } = @state
-
-

-          {if @state.error?
-
{@state.error.toString()}
}
-
-          {if roleSets.length is 1
-
None yet, add some with the form below.
}
-
-          {if owner and roleSets.length > 1
-

-              {for roleSet in roleSets
-                continue if owner.id is roleSet.links.owner.id
-                <RoleRow key={roleSet.id} roleSet={roleSet} onRemove={@handleCollaboratorChange} />}
-
}
-
-

-

-

-
-
Add a collaborator

-          <RoleCreator collection={@props.collection} onAdd={@handleCollaboratorChange} />
-

-      else
-

-
Not allowed to edit this collection

-

-    }</PromiseRenderer>
+    if @state.hasSettingsRole
+      { roleSets, owner } = @state
+
+

+        {if @state.error?
+
{@state.error.toString()}
}
+
+        {if roleSets.length is 1
+
None yet, add some with the form below.
}
+
+        {if owner and roleSets.length > 1
+

+            {for roleSet in roleSets
+              continue if owner.id is roleSet.links.owner.id
+              <RoleRow key={roleSet.id} roleSet={roleSet} onRemove={@handleCollaboratorChange} />}
+
}
+
+

+

+

+
+
Add a collaborator

+        <RoleCreator collection={@props.collection} onAdd={@handleCollaboratorChange} />
+

+    else
+

+
Not allowed to edit this collection

+


Don't try to update struct elements.

Don't try to update struct elements.

--- create-form.coffee
+++ create-form.coffee
@@ -23,12 +23,15 @@
          parent

 hook_field = (n, field, elm) ->
-  elm.change (x) ->
-               v = ($ x.target).val()
-               if field.attr
-                 n.attr (field_name field), v
-               else
-                 n.text v
+  if field.type is "struct"
+    elm
+  else
+    elm.change (x) ->
+                 v = ($ x.target).val()
+                 if field.attr
+                   n.attr (field_name field), v
+                 else
+                   n.text v

 create_form_field = (parent, field) ->
   name = field.name or field.type


Replace 'images' with 'sprites'

Replace 'images' with 'sprites'

--- app/scripts/scenery/City.coffee
+++ app/scripts/scenery/City.coffee
@@ -40,8 +40,7 @@
     @addBackground(300, y, c2, s)

 generator.defineElement 'waterHorizon', ->
-  h = Crafty.e('2D, WebGL, ImageWithEffects, SunBlock, Horizon')
-    .image('images/water-horizon.png')
+  h = Crafty.e('2D, WebGL, waterHorizon, SunBlock, Horizon')
     .attr(z: -600)
     .colorDesaturation(Game.backgroundColor)
     .saturationGradient(1.0, .0)
@@ -127,15 +126,15 @@
   #col3 = '#66667D'
   #@addBackground(0, @level.visibleHeight - 140, Crafty.e('2D, WebGL, Color').color(col3).attr(z: -400, w: 297, h: 83), .37)

-  bg = Crafty.e('2D, WebGL, ImageWithEffects, Collision, SunBlock, Horizon')
-    .image('images/city-layer2.png').attr(z: -505)
+  bg = Crafty.e('2D, WebGL, cityLayer2, Collision, SunBlock, Horizon')
+    .attr(z: -505)
     .collision([4, 29, 72, 29, 72, 118, 4, 118])
     .colorDesaturation(Game.backgroundColor)
     .saturationGradient(.6, .6)
   @addBackground(0, @level.visibleHeight - 57 - 240, bg, .37)

-  e = Crafty.e('2D, WebGL, ImageWithEffects, Collision, SunBlock, Horizon')
-    .image('images/city.png').attr(z: -305)
+  e = Crafty.e('2D, WebGL, city, Collision, SunBlock, Horizon')
+    .attr(z: -305)
     .colorDesaturation(Game.backgroundColor)
     .saturationGradient(.4, .4)
   e.collision([35, 155, 35, 0, 130, 0, 130, 155])
@@ -148,8 +147,8 @@
   @addBackground(400, @level.visibleHeight - 290, c, .5)

 generator.defineElement 'city-bridge', ->
-  bg = Crafty.e('2D, WebGL, ImageWithEffects, Collision, SunBlock, Horizon')
-    .image('images/city-layer2.png').attr(z: -505)
+  bg = Crafty.e('2D, WebGL, cityLayer2, Collision, SunBlock, Horizon')
+    .attr(z: -505)
     .collision([4, 29, 72, 29, 72, 118, 4, 118])
     .colorDesaturation(Game.backgroundColor)
     .saturationGradient(.6, .6)
@@ -165,8 +164,8 @@
   @addBackground(0, @level.visibleHeight - 290, e, .5)

 generator.defineElement 'cityStart', ->
-  e = Crafty.e('2D, WebGL, ImageWithEffects, Collision, SunBlock, Horizon')
-    .image('images/city-start.png').attr(z: -305)
+  e = Crafty.e('2D, WebGL, cityStart, Collision, SunBlock, Horizon')
+    .attr(z: -305)
     .colorDesaturation(Game.backgroundColor)
     .saturationGradient(.4, .4)
   e.collision([220, 155, 220, 20, 270, 20, 270, 0, 330, 0, 330, 155])
@@ -295,8 +294,12 @@
     y: 0

   assets: ->
-    images: ['water-horizon.png']
-    sprites:
+    sprites:
+      'water-horizon.png':
+        tile: 201
+        tileh: 155
+        map:
+          waterHorizon: [0, 0]
       'water.png':
         tile: 400
         tileh: 105
@@ -349,8 +352,12 @@
     y: 0

   assets: ->
-    images: ['water-horizon.png']
-    sprites:
+    sprites:
+      'water-horizon.png':
+        tile: 201
+        tileh: 155
+        map:
+          waterHorizon: [0, 0]
       'water.png':
         tile: 400
         tileh: 105
@@ -385,7 +392,13 @@
   autoNext: 'Bay'

   assets: ->
-    images: ['city-start.png']
+    sprites:
+      'city-start.png':
+        tile: 400
+        tileh: 270
+        paddingY: 1
+        map:
+          cityStart: [0, 0]

   generate: ->
     super
@@ -401,8 +414,12 @@
     y: 0

   assets: ->
-    images: ['water-horizon.png', 'city.png', 'city-layer2.png']
-    sprites:
+    sprites:
+      'water-horizon.png':
+        tile: 201
+        tileh: 155
+        map:
+          waterHorizon: [0, 0]
       'water.png':
         tile: 400
         tileh: 105
@@ -421,6 +438,17 @@
         map:
           coastStart: [0, 0]
           coast: [0, 1]
+      'city.png':
+        tile: 402
+        tileh: 270
+        paddingY: 1
+        map:
+          city: [0, 0]
+      'city-layer2.png':
+        tile: 297
+        tileh: 220
+        map:
+          cityLayer2: [0, 0]

   generate: ->
     super
@@ -485,7 +513,23 @@
     y: 0

   assets: ->
-    images: ['water-horizon.png', 'city.png', 'city-layer2.png']
+    sprites:
+      'city-layer2.png':
+        tile: 297
+        tileh: 220
+        map:
+          cityLayer2: [0, 0]
+      'city.png':
+        tile: 402
+        tileh: 270
+        paddingY: 1
+        map:
+          city: [0, 0]
+      'water-horizon.png':
+        tile: 201
+        tileh: 155
+        map:
+          waterHorizon: [0, 0]

   generate: ->
     super
@@ -508,7 +552,18 @@
   autoNext: 'Skyline2'

   assets: ->
-    images: ['city.png', 'city-layer2.png']
+    sprites:
+      'city-layer2.png':
+        tile: 297
+        tileh: 220
+        map:
+          cityLayer2: [0, 0]
+      'city.png':
+        tile: 402
+        tileh: 270
+        paddingY: 1
+        map:
+          city: [0, 0]

   generate: ->
     super
@@ -530,7 +585,23 @@
     y: 0

   assets: ->
-    images: ['water-horizon.png', 'city.png', 'city-layer2.png']
+    sprites:
+      'city-layer2.png':
+        tile: 297
+        tileh: 220
+        map:
+          cityLayer2: [0, 0]
+      'city.png':
+        tile: 402
+        tileh: 270
+        paddingY: 1
+        map:
+          city: [0, 0]
+      'water-horizon.png':
+        tile: 201
+        tileh: 155
+        map:
+          waterHorizon: [0, 0]

   generate: ->
     super


Fix bounce effect

Fix bounce effect

--- app/scripts/scenes/Intro.coffee
+++ app/scripts/scenes/Intro.coffee
@@ -21,7 +21,7 @@
       @one('TweenEnd', ->
         @tween({ x: 150 }, 2000)
       )
-    , 2000, -1
+    , 4000, -1

   Crafty.e('2D, DOM, Text, Tween, Delay')
     .attr(x: 200, y: 290, w: 750)


Added Scale to Translation Calculation

Added Scale to Translation Calculation

--- js/camera.coffee
+++ js/camera.coffee
@@ -26,8 +26,8 @@
       @scale *= 1 - @ZOOM_SPEED * deltaTime if Keyboard.isDown 90

     _updatePosition: (deltaTime) ->
-      cos = @PAN_SPEED * deltaTime * Math.cos(@rotation)
-      sin = @PAN_SPEED * deltaTime * Math.sin(@rotation)
+      cos = @PAN_SPEED * deltaTime * Math.cos(@rotation) / @scale
+      sin = @PAN_SPEED * deltaTime * Math.sin(@rotation) / @scale

       @position = @position.add new Vector(cos, -sin) if Keyboard.isDown 65
       @position = @position.add new Vector(sin, cos) if Keyboard.isDown 87
@@ -43,8 +43,6 @@
       canvas.width = width
       canvas.height = height

-      context.setTransform 1, 0, 0, 1, 0, 0
-      context.clearRect 0, 0, width, height
       context.translate width/2, height/2
       context.rotate @rotation
       context.scale @scale, @scale


Add env to template context in paginator plugin

Closes #141

--- examples/blog/plugins/paginator.coffee
+++ examples/blog/plugins/paginator.coffee
@@ -43,7 +43,7 @@
         return callback new Error "unknown paginator template '#{ options.template }'"

       # setup the template context
-      ctx = {contents, @articles, @prevPage, @nextPage}
+      ctx = {env, contents, @articles, @prevPage, @nextPage}

       # extend the template context with the enviroment locals
       env.utils.extend ctx, locals


fix reply generation

fix reply generation

--- scripts/imaginaria.coffee
+++ scripts/imaginaria.coffee
@@ -28,7 +28,7 @@
           feed.init()
           count = msg.match[1] || 5
           items = feed.getItems(0, count)
-          msg.send item.getTitle() + " [" + item.getCategories() + "] : " + item.getPermalink() +  for item in items
+          msg.send item.getTitle() + " [" + item.getCategories() + "] : " + item.getPermalink() for item in items
         catch e
           console.log(e)
           msg.send "Something's gone awry"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/approachable.coffee
+++ test/approachable.coffee
@@ -13,11 +13,6 @@
   5.times ->
     obj.update()

-  equals obj.I.active, true, "object is active until duration is exceeded"
-
-  6.times ->
-    obj.update()
-
-  equals obj.I.active, false, "object is inactive after duration"
+  equals obj.I.cooldowns.bullet.value, 95, "bullet cooldown should decrease by 5"

 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -73,7 +73,7 @@
   player.update()

   equals player.I.velocity.x, Point(-1, -1).norm().x * player.I.speed
-  equals player.I.velocity.y, -Math.sqrt(2)
+  equals player.I.velocity.y, Point(-1, -1).norm().y * player.I.speed

 test "should correctly track facing direction", ->
   ok player.I.facing.equal(Point(1, 0))


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -1,6 +1,6 @@
 module "Controllable"

-test "should move player left wh", ->
+test "should move player left while ke", ->
   one = 1

   # Test for equality of two objects


Set zooTalk projectToken

Set zooTalk projectToken

--- app/talk/index.cjsx
+++ app/talk/index.cjsx
@@ -19,8 +19,12 @@
     sugarClient.subscribeTo @props.section or 'zooniverse'
     @logTalkView()

+  componentDidMount: ->
+    @context.geordi?.remember projectToken: 'zooTalk'
+
   componentWillUnmount: ->
     sugarClient.unsubscribeFrom @props.section or 'zooniverse'
+    @context.geordi?.forget ['projectToken']

   render: ->
     logClick = @context.geordi?.makeHandler? 'breadcrumb'


globalize data and avg

globalize data and avg

--- atmospheric/002-accumulated-heat/accumulator.coffee
+++ atmospheric/002-accumulated-heat/accumulator.coffee
@@ -53,6 +53,9 @@
                 avg[cur] = data[cur] / lday

         [128, 128 * 128, avg]
+    exports.data = data
+    exports.avg = avg
+

     exports


Check if model is defined before cloning its attributes

Previously, if the context(@model) was empty then the `attrs = c._.clone(...)` line would fail with an error involving the attempt to access the attributes property of an undefined object. This code checks if the model is defined before cloning and updating the count and returns the empty object if model is not defined.

--- src/coffee/cilantro/ui/context/actions.coffee
+++ src/coffee/cilantro/ui/context/actions.coffee
@@ -20,9 +20,11 @@
             'change:count': 'renderCount'

         serializeData: ->
-            attrs = c._.clone(@model.attributes)
-            delete attrs.json
-            attrs.count = c.utils.prettyNumber(attrs.count)
+            attrs = {}
+            if @model?
+                attrs = c._.clone(@model.attributes)
+                delete attrs.json
+                attrs.count = c.utils.prettyNumber(attrs.count)
             return attrs

         renderCount: (model, value, options) ->


Add owned projects to the user admin page

--- app/pages/admin/user-settings.cjsx
+++ app/pages/admin/user-settings.cjsx
@@ -5,6 +5,37 @@
 apiClient = require 'panoptes-client/lib/api-client'
 AutoSave = require '../../components/auto-save'
 handleInputChange = require '../../lib/handle-input-change'
+{Link} = require 'react-router'
+
+UserProjects = React.createClass
+  displayName: 'UserProjects'
+
+  getInitialState: ->
+    projects: []
+
+  componentDidMount: ->
+    @updateProjects @props.user.login
+
+  componentWillReceiveProps: (newProps) ->
+    @updateProjects newProps.user.login if newProps.user isnt @props.user
+
+  updateProjects: (login) ->
+    apiClient.type 'projects'
+      .get
+        owner: login
+        cards: true
+      .then (projects) =>
+        @setState {projects}
+
+  render: ->
+

+
Projects

+

+        {@state.projects.map (project) ->
+
<Link to="/projects/#{project.slug}">{project.display_name}</Link>

+        }
+

+

 UserSettings = React.createClass
   displayName: "User Settings"
@@ -111,6 +142,9 @@
           if editUser == @props.user

Can't edit your own account

           else if editUser?
-            <UserSettings editUser={editUser} />
+

+              <UserSettings editUser={editUser} />
+              <UserProjects user={editUser} />
+

         }</PromiseRenderer>}


made steam be the default system if no system is present

--- lib/js/integrations/slack_integration.coffee
+++ lib/js/integrations/slack_integration.coffee
@@ -55,9 +55,13 @@
     if commandAction is VALID_COMMANDS.ADD
       return @sendMessage("You already have #{MAX_USERS}. Please upgrade to premium to add more :kappa:", channel) if NUM_USERS_ADDED is MAX_USERS

-      system = commandArr[2]?.toLowerCase()
-      newUser = commandArr[3]
-
+      if commandArr.length == 3
+        system = 'steam'
+        newUser = commandArr[2]
+      else
+        system = commandArr[2]?.toLowerCase()
+        newUser = commandArr[3]
+
       return @sendMessage("Adding a user must be of format `add <system> <username>`", channel) if not newUser

       if system is 'steam'


Fix end of game

Fix end of game

--- app/scripts/lazerscripts/stage1end.coffee
+++ app/scripts/lazerscripts/stage1end.coffee
@@ -1,3 +1,5 @@
+require('src/scripts/lazerscripts/lunch/presentation_leave_screen')
+
 class Game.Scripts.Stage1End extends Game.LazerScript

   assets: ->


pom.xml and standalone.xml specific settigns

--- atom/atom.symlink/init.coffee
+++ atom/atom.symlink/init.coffee
@@ -4,3 +4,7 @@
   editor = editorView.getEditor()
   if path.extname(editor.getPath()) is '.md'
     editor.setSoftWrap(true)
+
+  if /standalone.*\.xml/.test(path.basename(editor.getPath())) || path.basename(editor.getPath()) is 'pom.xml'
+    editor.setSoftWrap(false)
+    editor.setTabLength(4)


Fix outcomes report data loading

Fix outcomes report data loading

--- app/views/admin/OutcomesReportView.coffee
+++ app/views/admin/OutcomesReportView.coffee
@@ -35,8 +35,7 @@
   template: require('templates/admin/outcomes-report-view')()
   data: ->
     accountManager: me.toJSON()
-    teacherEmail: 'robin+teacher@codecombat.com' # TODO: Don't hardcode this. And add get-by-email endpoint.
-    # teacherEmail: '568e7f2552eea226005180b3' # TODO: Don't hardcode this. And add get-by-email endpoint.
+    teacherEmail: ''
     teacher: null
     teacherFullName: null
     accountManagerFullName: null
@@ -46,13 +45,15 @@
     startDate: null
     classrooms: null
     courses: null
-    sessions: []
     courseInstances: []
+    dataReady: false
     isClassroomSelected: {}
     isCourseSelected: {}
     endDate: moment(new Date()).format('MMM D, YYYY')
     insightsMarkdown: ""
   computed:
+    sessions: ->
+      _.flatten(@classrooms.map (c) -> c.sessions)
     studentIDs: ->
       _.uniq _.flatten _.pluck(@classrooms, 'members')
     indexedSessions: ->
@@ -73,10 +74,7 @@
       @courses.filter (c) => @isCourseSelected[c._id]
     insightsHtml: ->
       marked(@insightsMarkdown, sanitize: false)
-    dataReady: ->
-      return (_.all([@classrooms, @courses, @courseInstances]) and _.all(@classrooms?.map (c) -> c.sessions))
     courseCompletion: ->
-      console.log [@classrooms, @courses, @courseInstances, @classrooms?.map (c) -> c.sessions]
       return if not @dataReady
       classroomsWithSessions = new Classrooms(@classrooms)
       classroomsWithSessions.forEach (classroom) =>
@@ -118,7 +116,6 @@
       counts = @courses.map (course) =>
         courseID = course._id ? course
         instancesOfThisCourse = _.where(@courseInstances, {courseID})
-        console.log _.union.apply(null, instancesOfThisCourse.map((i) -> i.members)).length
         {
           _id: courseID
           count: _.union.apply(null, instancesOfThisCourse.map((i) -> i.members)).length
@@ -148,18 +145,27 @@
       for course in courses
         course.newConcepts = _.difference(course.concepts, alreadyCoveredConcepts)
         alreadyCoveredConcepts = _.union(course.concepts, alreadyCoveredConcepts)
-        console.log course.campaignID, course.newConcepts
     accountManager: ->
       @accountManagerFullName = "#{@accountManager?.firstName} #{@accountManager?.lastName}"
+    dataReady: ->
+      console.log "Data ready!", @dataReady

   methods:
     submitEmail: (e) ->
-      $.ajax
-        type: 'GET',
-        url: '/db/user'
-        data: {email: @teacherEmail}
-        success: @fetchCompleteUser
-        error: (data) => noty text: 'Failed to find user by that email', type: 'error'
+      @fetchData().then =>
+        @dataReady = true
+
+    fetchByEmail: ->
+      new Promise (accept, reject) =>
+        reject("No email provided!") if _.isEmpty(@teacherEmail)
+        $.ajax
+          type: 'GET',
+          url: '/db/user'
+          data: {email: @teacherEmail}
+          success: (data) => accept(data)
+          error: (data) =>
+            noty text: 'Failed to find user by that email', type: 'error'
+            reject(data)

     displayReport: (e) ->
       @fetchLinesOfCode().then @finishDisplayReport
@@ -192,66 +198,82 @@
       window.currentView = undefined
       application.router.openView(resultView)

+    fetchData: ->
+      @fetchByEmail().then (trimTeacher) =>
+        @fetchCompleteUser(trimTeacher).then (teacher) =>
+          @teacher = teacher
+          Promise.all([
+            @fetchTrialRequest(teacher).then (trialRequests) =>
+              @trialRequest = trialRequests[0]
+            @fetchClassrooms(teacher).then (classrooms) =>
+              @classrooms = classrooms
+              Promise.all @classrooms.map (classroom) =>
+                @fetchStudentSessions(classroom).then (sessions) =>
+                  Vue.set classroom, 'sessions', sessions
+            Promise.all([
+              @fetchCourseInstances(teacher).then (courseInstances) =>
+                @courseInstances = courseInstances
+              @fetchCourses().then (courses) =>
+                @courses = courses
+            ]).then ([courseInstances, courses]) =>
+              courseIDs = _.uniq courseInstances.map (courseInstance) =>
+                courseInstance.courseID
+              indexedCourses = _.indexBy(courses, '_id')
+              @courses = utils.sortCourses(courseIDs.map (courseID) =>
+                indexedCourses[courseID]
+              )
+          ])

     fetchCompleteUser: (data) ->
-      user = new User(data)
-      console.log data
-      user.fetch()
-      user.once 'sync', (fullData) =>
-        @teacher = fullData.toJSON()
-        console.log @teacher
-        @fetchTrialRequest()
-        @fetchClassrooms()
-        @fetchCourses()
-
-    fetchTrialRequest: ->
-      trialRequests = new TrialRequests()
-      trialRequests.fetchByApplicant(@teacher._id)
-      trialRequests.once 'sync', =>
-        if trialRequests.length is 0
-          noty text: "WARNING: No trial request found for that user!", type: 'error'
-        @trialRequest = trialRequests.models[0]?.toJSON()
-
-    fetchClassrooms: ->
-      classrooms = new Classrooms()
-      classrooms.fetchByOwner(@teacher._id)
-      classrooms.once 'sync', =>
-        @classrooms = classrooms.toJSON()
-        @fetchStudentSessions()
-        @fetchCourseInstances()
+      new Promise (accept, reject) ->
+        user = new User(data)
+        user.fetch().then (data) ->
+          return accept(data)
+        , (error) ->
+          return reject(error) if error
+
+    fetchTrialRequest: (teacher) ->
+      new Promise (accept, reject) ->
+        trialRequests = new TrialRequests()
+        trialRequests.fetchByApplicant(teacher._id).then (data) ->
+          if data.length is 0
+            noty text: "WARNING: No trial request found for that user!", type: 'error'
+            return reject("WARNING: No trial request found for that user!")
+          return accept(data)
+        , (error) ->
+          return reject(error) if error
+
+    fetchClassrooms: (teacher) ->
+      new Promise (accept, reject) ->
+        classrooms = new Classrooms()
+        classrooms.fetchByOwner(teacher._id).then (data) ->
+          accept(data)
+        , (error) ->
+          reject(error)
+
+    fetchCourseInstances: (teacher) ->
+      new Promise (accept, reject) ->
+        courseInstances = new CourseInstances()
+        courseInstances.fetchByOwner(teacher._id).then (data) ->
+          accept(data)
+        , (error) ->
+          reject(error)

     fetchCourses: ->
-      courseInstances = new CourseInstances()
-      courseInstances.fetchByOwner(@teacher._id)
-      courseInstances.once 'sync', =>
+      new Promise (accept, reject) ->
         courses = new Courses()
-        courses.fetch()
-        courses.once 'sync', =>
-          courseIDs = _.uniq courseInstances.map (courseInstance) =>
-            courseInstance.get('courseID')
-          Vue.set @$data, 'courses', utils.sortCourses(courseIDs.map (courseID) => courses.get(courseID).toJSON())
-
-    fetchStudentSessions: ->
-      @classrooms.forEach (classroom) =>
-        console.log "Fetching sessions for", classroom
+        courses.fetch().then (data) ->
+          accept(data)
+        , (error) ->
+          reject(error)
+
+    fetchStudentSessions: (classroom) ->
+      new Promise (accept, reject) ->
         sessions = new LevelSessions()
         jqxhrs = sessions.fetchForAllClassroomMembers(new Classroom(classroom))
-        $.when(jqxhrs...).done =>
-          console.log sessions
-          Vue.set(classroom, 'sessions', sessions.toJSON())
-          sessions.forEach (session) =>
-            # if not _.detect(@$data.sessions, { _id: session.id })
-            @$data.sessions.push(session.toJSON())
-
-    fetchCourseInstances: ->
-      @classrooms.forEach (classroom) =>
-        courseInstances = new CourseInstances()
-        courseInstances.fetchForClassroom(classroom._id)
-        courseInstances.once 'sync', (courseInstances) =>
-          courseInstances.forEach (courseInstance) =>
-            if not _.contains(@$data.courseInstances, { _id: courseInstance.id })
-              @$data.courseInstances.push(courseInstance.toJSON())
-
+        Promise.all(jqxhrs.map (jqxhr) -> new Promise(jqxhr.then)).then (responses) ->
+          return accept(_.union.apply(_, responses))
+
     fetchLinesOfCode: ->
       $.ajax
         type: 'GET',


Revert "Fix regression in legend location"

--- bokehjs/src/coffee/models/annotations/legend.coffee
+++ bokehjs/src/coffee/models/annotations/legend.coffee
@@ -88,12 +88,8 @@
           y = (v_range.end + v_range.start)/2 + legend_height/2
     else if isArray(location) and location.length == 2
       [x, y] = location   # left, bottom wrt panel
-      if panel.side?
-        x += h_range.start
-        y += v_range.end
-      else
-        x += h_range.start
-        y += v_range.start
+      x += h_range.start
+      y += v_range.start + legend_height

     x = @plot_view.canvas.vx_to_sx(x)
     y = @plot_view.canvas.vy_to_sy(y)


adds onFinished callback to each

adds onFinished callback to each

--- drivers/javascript/src/cursor.coffee
+++ drivers/javascript/src/cursor.coffee
@@ -6,14 +6,15 @@
     hasNext: -> throw "Abstract Method"
     next: -> throw "Abstract Method"

-    each: ar (cb) ->
+    each: varar 1, 2, (cb, onFinished) ->
         n = =>
-            @next (err, row) =>
-                cb(err, row)
-                if @hasNext()
+            if @hasNext()
+                @next (err, row) =>
+                    cb(err, row)
                     n()
-        if @hasNext()
-            n()
+            else if onFinished?
+                onFinished()
+        n()

     toArray: ar (cb) ->
         arr = []


Submit as much data as possible with each event

--- apps/apply/client/analytics.coffee
+++ apps/apply/client/analytics.coffee
@@ -4,23 +4,23 @@
 module.exports = (router) ->
   { state, form } = router

-  track.funnel 'Landed on partner application form', mode: state.get('mode')
+  track.funnel 'Landed on partner application form', state.attributes

   router.on 'route', (route) ->
-    track.funnel 'Changed partner application route', route: route
+    track.funnel 'Changed partner application route', _.extend(route: route, state.attributes)

   state.on 'change:mode', (state, mode) ->
-    track.funnel 'Partner application mode changed', mode: mode
+    track.funnel 'Partner application mode changed', state.attributes

   state.on 'change:step', (state, step) ->
-    track.funnel 'Partner application step changed', step: step
+    track.funnel 'Partner application step changed', state.attributes

   state.on 'change:state', (state, value) ->
-    track.funnel 'Submitted partner application form', state: value
+    track.funnel 'Submitted partner application form', state.attributes
     snowplowStruct 'partner_application', 'submit', null, 'partner_application', null, _.extend({}, state.pick('mode'), form.pick('name', 'first_name', 'last_name'))

   $(document).on 'click', '.paf-submit', ->
-    track.funnel 'Clicked submit button on partner application form'
+    track.funnel 'Clicked submit button on partner application form', state.attributes

   $(document).on 'click', '.paf-next', ->
-    track.funnel 'Clicked next button on partner application form'
+    track.funnel 'Clicked next button on partner application form', state.attributes


removing test rooms

removing test rooms

--- server/startup/migrations/xrun.coffee
+++ server/startup/migrations/xrun.coffee
@@ -1,3 +1,3 @@
-# Meteor.startup ->
-# 	Meteor.defer ->
-# 		Migrations.migrateTo 'latest'+Meteor.startup ->
+	Meteor.defer ->
+		Migrations.migrateTo 'latest'


use $compile/replace body instead of re-bootstrapping angular (was casuing all sorts of problems before with $locationProvider/$digest

--- lib/assets/javascripts/angular-turbolinks.js.coffee
+++ lib/assets/javascripts/angular-turbolinks.js.coffee
@@ -1,4 +1,4 @@
-angular.module('ngTurbolinks', []).run(($location, $rootScope, $http, $q)->
+angular.module('ngTurbolinks', []).run(($location, $rootScope, $http, $q, $compile)->

   loadedAssets = null
   createDocument = null
@@ -96,7 +96,7 @@

   changePage = (title, body, csrfToken, runScripts) ->
     document.title = title
-    document.documentElement.replaceChild body, document.body
+    angular.element("body").html($compile("
"+body.innerHTML+"
")($rootScope))
     CSRFToken.update csrfToken if csrfToken?
     executeScriptTags() if runScripts
     currentState = window.history.state


Performance tuning

Performance tuning

--- src/diary/site/static/coffee/diary.app.coffee
+++ src/diary/site/static/coffee/diary.app.coffee
@@ -46,14 +46,14 @@
     .when "/diary/:diary_id?",
       controller: "diaryController"
       template: """

-

+

{{post.title}}

             <time datetime="{{post.date}}">{{post.date|date:'fullDate'}}</time>

-

+

@@ -130,7 +130,7 @@
   restrict: "A"
   replace: true
   template: """

-

+

       {{diary.title}}

"""


Made it so that modal doesn't show if you've signed the contract

--- app/views/employers_view.coffee
+++ app/views/employers_view.coffee
@@ -23,7 +23,7 @@

   checkForEmployerSignupHash: =>
-    if window.location.hash is "#employerSignupLoggingIn"
+    if window.location.hash is "#employerSignupLoggingIn" and not ("employer" in me.get("permissions"))
       @openModalView application.router.getView("modal/employer_signup","_modal")
       window.location.hash = ""
   afterRender: ->


fix error handling

fix error handling

--- extension/chrome/js/src/content.coffee
+++ extension/chrome/js/src/content.coffee
@@ -629,11 +629,7 @@
 					action: 	'notification'
 					icon: 		"images/tie48x48.png"
 					title:		__msg 'notificationScheduleErrorTitle'
-					message:	__msg 'notificationScheduleError', ''+error
-
-				# we get 'InvalidScheduleTime' and we need to add 'notificationScheduleError' to it
-				message = __msg "notificationScheduleError#{error}"
-				notification.message = message if message
+					message:	if 'code' of error then (__msg "notificationScheduleError#{error.code}") else (__msg 'notificationScheduleError', '' + new String error)

 				chrome.extension.sendMessage notification


Fix typo in GlobalIframeSaver related to save indicator

[#90778062]


teamwork: restore `WebTermView` methods deleted in merge conflict

--- client/Terminal/webtermview.coffee
+++ client/Terminal/webtermview.coffee
@@ -247,6 +247,15 @@
   keyUp: (event) ->
     @terminal.keyUp event

+  _windowDidResize: (event) ->
+    @terminal.windowDidResize()
+
+  getAdvancedSettingsMenuItems: ->
+    settings     :
+      type       : 'customView'
+      view       : new WebtermSettingsView
+        delegate : this
+
   listenFullscreen: (event)->
     requestFullscreen = (event.metaKey or event.ctrlKey) and event.keyCode is 13
     if requestFullscreen


Reduced complexity of function

Reduced complexity of function

--- attitude/display/assets/test-plane.coffee
+++ attitude/display/assets/test-plane.coffee
@@ -21,36 +21,27 @@

 s = data.singularValues.map Math.sqrt
 axes = transpose(data.axes)
-#console.log cov
-
-cov2 = [0,0,s[2]]

 sdot = (a,b)->
   zipped = (a[i]*b[i] for i in [0..a.length])
   d3.sum zipped

-sheets =
-  upper: (d,i)->d+cov2[i]
-  lower: (d,i)->d-cov2[i]
+stepFunc = (angle)->
+  e = [Math.cos(angle)*s[0],
+       Math.sin(angle)*s[1]]

-stepFunc = (angle)->
-  a = [Math.cos(angle),Math.sin(angle)]
-  e = a.map (d,i)->d*s[i]
-
-  func = sheets[data.sheet]
-  if func?
-    e = e.map func
+  if data.sheet == 'upper'
+    e[2] = s[2]
+  else if data.sheet == 'lower'
+    e[2] = -s[2]

   d = (sdot(e,i) for i in axes)

-  x = -d[2]
-  y = d[0]
-  z = d[1]
-  sq = (a)->Math.pow(a,2)
-  r = Math.sqrt(sq(x)+sq(y)+sq(z))
-  lat = Math.asin(z/r)
-  lon = Math.atan2(y,x)
-  return [lon,lat]
+  sq = (a)->a*a
+  r = Math.sqrt d3.sum d.map(sq)
+  return [
+    Math.atan2(d[0],-d[2]),
+    Math.asin d[1]/r]

 val = angles.map(stepFunc)


Alternative implementation of related publish.

--- server/lib/publish.coffee
+++ server/lib/publish.coffee
@@ -25,53 +25,82 @@
         # using currentDocuments variable that we have a consistent view of the
         # documents we published
         currentDocuments = {}
-        handleDocuments = null
-        collectionName = null
+        relatedPublish = null

         publishDocuments = (relatedDocuments) ->
+          assert relatedDocuments.length
+
           initializing = true
-          initializedDocuments = []
+          initializedDocuments = {}

-          oldHandleDocuments = handleDocuments
-          newCursor = publishFunction relatedDocuments...
-          unless newCursor
-            handleDocuments = null
+          oldRelatedPublish = relatedPublish
+
+          relatedPublish = publish._recreate()
+          relatedPublish.personId = publish.personId
+          relatedPublish.related = publish.related
+
+          relatedPublishAdded = relatedPublish.added
+          relatedPublishChanged = relatedPublish.changed
+          relatedPublishRemoved = relatedPublish.removed
+
+          _.extend relatedPublish,
+            added: (collectionName, id, fields) ->
+              currentDocuments[collectionName] ?= {}
+              initializedDocuments[collectionName] ?= []
+
+              initializedDocuments[collectionName].push id if initializing
+
+              return if currentDocuments[collectionName][id]
+              currentDocuments[collectionName][id] = true
+
+              relatedPublishAdded.call @, collectionName, id, fields
+
+            changed: (collection, id, fields) ->
+              currentDocuments[collectionName] ?= {}
+              initializedDocuments[collectionName] ?= []
+
+              return if not currentDocuments[collectionName][id]
+
+              relatedPublishChanged.call @, collectionName, id, fields
+
+            removed: (collection, id) ->
+              currentDocuments[collectionName] ?= {}
+              initializedDocuments[collectionName] ?= []
+
+              return if not currentDocuments[collectionName][id]
+              delete currentDocuments[collectionName][id]
+
+              relatedPublishRemoved.call @, collectionName, id
+
+            ready: -> # Noop
+
+            stop: (onlyRelated) ->
+              @_deactivate()
+              publish.stop() unless onlyRelated
+
+          if Package['audit-argument-checks']
+            relatedPublish._handler = (args...) ->
+              # Related parameters are trusted
+              check arg, Match.Any for arg in args
+              publishFunction args...
           else
-            collectionName = newCursor._cursorDescription.collectionName unless collectionName
-            assert.equal newCursor._cursorDescription.collectionName, collectionName
-
-            handleDocuments = newCursor.observeChanges
-              added: (id, fields) ->
-                initializedDocuments.push id if initializing
-
-                return if currentDocuments[id]
-                currentDocuments[id] = true
-
-                publish.added collectionName, id, fields
-
-              changed: (id, fields) ->
-                return if not currentDocuments[id]
-
-                publish.changed collectionName, id, fields
-
-              removed: (id) ->
-                return if not currentDocuments[id]
-                delete currentDocuments[id]
-
-                publish.removed collectionName, id
+            relatedPublish._handler = publishFunction
+          relatedPublish._params = relatedDocuments
+          relatedPublish._runHandler()

           initializing = false

-          # We stop the handle after we established the new handle,
+          # We call stop after we established the new related publish,
           # so that any possible changes hapenning in the meantime
-          # were still processed by the old handle
-          oldHandleDocuments.stop() if oldHandleDocuments
-          oldHandleDocuments = null
+          # were still processed by the old related publish
+          oldRelatedPublish.stop true if oldRelatedPublish
+          oldRelatedPublish = null

           # And then we remove those which are not published anymore
-          for id in _.difference _.keys(currentDocuments), initializedDocuments
-            delete currentDocuments[id]
-            publish.removed collectionName, id
+          for collectionName, documents of currentDocuments
+            for id in _.difference _.keys(documents), initializedDocuments[collectionName]
+              delete documents[id]
+              publish.removed collectionName, id

         currentRelatedDocuments = []
         handleRelatedDocuments = []
@@ -121,7 +150,7 @@
           for handle, i in handleRelatedDocuments
             handle.stop() if handle
             handleRelatedDocuments[i] = null
-          handleDocuments.stop() if handleDocuments
-          handleDocuments = null
+          relatedPublish.stop true if relatedPublish
+          relatedPublish = null

       func.apply publish, args


Meat-axe the stuff we aren't using

Meat-axe the stuff we aren't using

--- assets/front.coffee
+++ assets/front.coffee
@@ -1,225 +1,6 @@
-window.log = ->
-	log.history = log.history or []
-	log.history.push arguments
-	console.log Array::slice.call(arguments)  if @console
-
-
+# Is this needed?
 soundManager.setup
 	url: '/static/flash/'
-
-TIMING_INTERVAL = 30000 # ms between checking server ping
-NUM_TRACKS = 5
-
-#   Empirical.
-OFFSET = 5
-BUFFERED = OFFSET
-
-MIN_LISTENERS = 30
-
-DONE_TRACKS_LIMIT = 8
-MAGIC_REGEX = /(\s*-*\s*((\[|\(|\*|~)[^\)\]]*(mp3|dl|description|free|download|comment|out now|clip|bonus|preview|teaser|in store|follow me|follow on|prod|full|snip|exclusive|beatport|original mix)+[^\)\]]*(\]|\)|\*|~)|((OUT NOW( ON \w*)?|free|download|preview|follow me|follow on|teaser|in store|mp3|dl|description|full|snip|exclusive|beatport|original mix).*$))\s*|\[(.*?)\])/i
-
-comma = (x) ->
-	if x? then x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') else x
-
-window.ping = 0
-window.serverTime = -> (+new Date) - window.ping
-
-class Frame
-	constructor: (init, is_new) ->
-		for k, v of init
-			this[k] = v
-		@image = null
-		@div = null
-		@id = "track_#{@tracks[0].metadata.id}"
-		@new = if is_new then 'new' else ''
-		# hack for development... don't wanna do this
-		while document.getElementById(@id)
-			@id += "_"
-
-		@parseMetaData()
-
-	parseMetaData: ->
-		matches = @tracks[0].metadata.title.match(/(.*?) by (.*)/i)
-		if matches?
-			[_, @title, @artist] = matches
-			matches = @artist.match(/(.*?)\s+-\s+(.*)/i)
-			[_, @artist, other] = matches if matches?
-		else
-			matches = @tracks[0].metadata.title.match(/(.*?)\s*-\s+(.*)/i)
-			if matches?
-				[_, @artist, @title] = matches
-			else
-				@title = @tracks[0].metadata.title
-				@artist = @tracks[0].metadata.artist
-		console.log(matches)
-		console.log(@artist)
-		console.log(@title)
-
-		matches = @title.match(/([^\[\]]*?)( - ([^\[\]\(\)]*)|(\[.*\]))/i)
-		if matches?
-			[_, @title, _, other] = matches
-
-		#   Try to remove "Free Download," "Follow Me" and the like
-		_title = @title.replace MAGIC_REGEX, ""
-		@title = _title if _title.length > 0
-
-		_artist = @artist.replace MAGIC_REGEX, ""
-		@artist = _artist if _artist.length > 0
-
-		if @title[0] == '"' and @title[@title.length - 1] == '"'
-			@title = @title[1...@title.length - 1].trim()
-
-		#@img = if @tracks[0].metadata.artwork_url?
-		#         @tracks[0].metadata.artwork_url
-		#       else
-		#         @tracks[0].metadata.user.avatar_url
-
-
-		# Buttons
-		@buttons = true
-		@nid = @tracks[0].metadata.id
-
-
-		@url = @tracks[0].metadata.permalink_url
-
-
-	html: (first) ->
-		first = false if not first?
-		_new = @new
-		@new = ''
-		"""
-

-
-

-				#{@title}
-				#{@artist}
-

-
-			#{if @stats then "
-
NOTHING

-			" else ""}
-

-		"""
-
-	played: ->
-		(@time + @duration + BUFFERED) < (window.serverTime() / 1000)
-
-	playing: ->
-		((@time + BUFFERED) < (window.serverTime() / 1000)) and not @played()
-
-	intendedParent: ->
-		document.getElementById( if @played() then "done" else "tracks" )
-
-	render: ->
-		return if @action != "Playback"
-		return @relayout() if @div?
-
-		parent = @intendedParent()
-		$(parent).prepend @html(true)
-		id = @id
-		setTimeout((-> $("##{id}").removeClass 'new hidden'), 100)
-		@div = document.getElementById @id
-
-	relayout: ->
-		@div = document.getElementById(@id) if not @div.parentNode?
-		newparent = document.getElementById( if @played() then "done" else "tracks" )
-		if @div.parentNode != newparent
-			neighbour = @div.parentNode.children[@div.parentNode.children.length - 2]
-			$(@div).addClass("ending")
-			$(neighbour).addClass("next")
-
-			div = @div
-			html = @html()
-			setTimeout ->
-				$(neighbour).removeClass("next")
-				div.parentNode.removeChild div
-				newparent.innerHTML = html + newparent.innerHTML
-				setTimeout ->
-					$(".hidden", newparent).removeClass('hidden')
-					if newparent.children.length > DONE_TRACKS_LIMIT
-						end = newparent.children[newparent.children.length - 1]
-						$(end).addClass('hidden')
-						setTimeout ->
-							newparent.removeChild end
-						, 1000
-				, 100
-			, 1400
-
-
-class Titular
-	char: "\u25b6"
-	constructor: ->
-		@title = document.title
-		@__title = ""
-		@rotation = 0
-		@drawloop()
-
-	drawloop: ->
-		@draw()
-		me = this
-		return if @stop?
-		setTimeout((-> me.drawloop()), 400)
-
-	draw: (playing) ->
-		if not playing?
-			s = window.soundManager.sounds.ui360Sound0
-			playing = (s? and s.playState == 1 and not s.paused)
-		document.title = @title
-
-	rot: (title) ->
-		if title != @__title
-			@rotation = 0
-			@__title = title
-		if @rotation == @__title.length
-			@rotation = 0
-		r = @__title[@rotation...@__title.length] + " " + @__title[0...@rotation]
-		@rotation += 1
-		r
-
-window.replace_h2 = (tag, text) ->
-	return if tag.html() == text
-	tag.css 'opacity', 0
-	setTimeout ->
-		tag.html(text)
-		tag.css 'opacity', 1
-	, 500
-
-
-format_uptime = (seconds) ->
-	hours = Math.round(seconds / 3600)
-	return "#{hours} hour#{if hours == 1 then '' else 's'}" if hours < 24
-	days = Math.round(seconds / 86400)
-	return "#{days} day#{if days == 1 then '' else 's'}" if days < 7
-	weeks = Math.round(seconds / 604800)
-	return "#{weeks} week#{if weeks == 1 then '' else 's'}" if weeks < 4
-	months = Math.round(seconds / 2419200)
-	return "#{months} month#{if months == 1 then '' else 's'}" if months < 12
-	years = Math.round(seconds / 29030400)
-	return "#{years} year#{if years == 1 then '' else 's'}"
-
-window.rotate_h2 = ->
-	tag = $('h2')
-	window.__original_h2 = tag.html() if not window.__original_h2?
-	setInterval ->
-		toggle = tag.data('toggle')
-		if toggle?
-			switch toggle
-				when 0
-					window.replace_h2 tag, window.__original_h2
-				when 1
-					if window._listeners? and window._listeners > MIN_LISTENERS
-						window.replace_h2 tag, "#{window._listeners} listeners"
-				when 2
-					if window._started_at?
-						uptime = ((+new Date) / 1000) - window._started_at
-						if uptime > 86400
-							window.replace_h2 tag, "up for #{format_uptime uptime}"
-		else
-			toggle = 0
-		toggle = (toggle + 1) % 3
-		tag.data('toggle', toggle)
-	, 5000

 $(document).ready ->

@@ -232,17 +13,7 @@
 				window.threeSixtyPlayer.handleClick {target: $('a.sm2_link')[0]}

 	getTrackInfo = ->
-		req_time = new Date/1000
 		$.getJSON "http://localhost:8889/status.json", (info) ->
-			#console.log("getTrackInfo")
-			resp_time = new Date/1000
-			# We started the request some time ago, and now it's completed. Assume that
-			# the server did its part of the work mid-way between those two time points,
-			# and assume further that the client clock and server clock tick at the same
-			# rate. This means assuming that there's no mess from clock drift, relativity,
-			# time offset changes (eg DST), or clock changes. If there are, stuff will go
-			# out of sync until the next status query, after which they'll be corrected.
-			window.ping = (resp_time + req_time) / 2 - info.ts
 			tag = 0
 			console.log("req " + req_time + " resp " + resp_time + " ts " + info.ts)
 			for track in info.tracks
@@ -293,43 +64,4 @@
 				else
 					break
 	setTimeout getTrackInfo, 1000
-	setInterval getTrackInfo, TIMING_INTERVAL
-
-	getTrackDetails = ->
-		s = io.connect ":8193/info.websocket"
-		s.on 'message', (data) ->
-			if typeof data is "string"
-				data = JSON.parse(data)
-				if data.segment.send_next_track
-					#console.log("Send next track!")
-					#console.log(Date())
-					#console.log(data.segment.prev_track)
-					#console.log(data.segment.next_track)
-					# Temporarily tagging the artist so we know it came from send_next_track
-					document.getElementById('artist').innerHTML = "[snt] " + data.segment.next_track.artist
-					document.getElementById('artist_next').innerHTML = "Up next: (unknown)"
-					length = data.segment.next_track.length
-					minutes = Math.floor(length/60)
-					seconds = Math.floor(length%60)
-					if seconds < 10
-						seconds = "0" + seconds
-					document.getElementById('length').innerHTML = minutes + ":" + seconds
-					return
-				if data.segment.tracks[0].metadata.id != window._track_id
-					window._track_id = data.segment.tracks[0].metadata.id
-					# TODO Be safe against embedded HTML tags
-					document.getElementById('artist').innerHTML = window._next_artist
-					window._next_artist = data.segment.tracks[0].metadata.artist
-					document.getElementById('artist_next').innerHTML = "Up next: " + window._next_artist
-				#console.log("GetTrackDets Data " + window._count_TrackDeets + ": ")
-				#console.log(data.segment.tracks[0].metadata.artist)
-				#console.log(data.segment.tracks[0].metadata.id)
-				window._count_TrackDeets++
-			if data.listener_count?
-				window._listeners = data.listener_count
-		window._socket = s
-
-	# setTimeout getTrackDetails, 1000 # Not in use currently
-	window._count_getJSON = 1
-	if not window._count_TrackDeets?
-		window._count_TrackDeets = 1
+	setInterval getTrackInfo, 30000


Show as many next-artist and next-track entries as possible

--- assets/front.coffee
+++ assets/front.coffee
@@ -235,7 +235,7 @@
 		$.getJSON "all.json", (segments) ->
 			console.log("getTrackInfo")
 			console.log(segments)
-			curtrack = nexttrack = 0 # Assume we'll never actually get a track ID of zero
+			trackids = []
 			for segment in segments
 				if segment.tracks?
 					id = segment.tracks[0].metadata.id
@@ -243,24 +243,29 @@
 					# code. I am not a CoffeeScript programmer, and it shows. The
 					# idea here (and you're *most* welcome to edit the code to better
 					# express that) is to have the first new track ID go into
-					# curtrack, and the next new track ID go into nexttrack; any track
-					# with the same ID as either of them will be ignored.
+					# artist0, the next new track ID go into artist1, etc; any track
+					# with the same ID as a previously-seen track will be ignored.
 					# Signed: Chris Angelico (Rosuav).
-					if id != curtrack and id != nexttrack
-						if curtrack != 0
-							nexttrack = id
-							tag = "_next"
-						else
-							curtrack = id
-							tag = ""
+					isnew = 1
+					for t in trackids
+						if t == id
+							isnew = 0
+					if isnew
+						trackids.push(id)
+						tag = trackids.length
 						length = segment.tracks[0].metadata.length
 						minutes = Math.floor(length/60)
 						seconds = Math.floor(length%60)
 						if seconds < 10
 							seconds = "0" + seconds
-						document.getElementById('artist'+tag).innerHTML = segment.tracks[0].metadata.artist
-						document.getElementById('length'+tag).innerHTML = minutes + ":" + seconds
 						console.log("Recording artist"+tag+" as "+segment.tracks[0].metadata.artist)
+						artist = document.getElementById('artist'+tag)
+						if artist
+							# Eventually we'll run out of objects to stash info into - that's fine.
+							# Assume that every artistN has corresponding other stash-targets lengthN
+							# and (eventually) titleN.
+							artist.innerHTML = segment.tracks[0].metadata.artist
+							document.getElementById('length'+tag).innerHTML = minutes + ":" + seconds
 				# console.log(segment.tracks[0].metadata.artist)
 				# console.log(segment.tracks[0].metadata.id)
 	setTimeout getTrackInfo, 1000


Alternative snippet for remove.

Alternative snippet for remove.

--- BST.coffee
+++ BST.coffee
@@ -110,6 +110,12 @@
 			node.rightChild = successor.rightChild
 			# successor is now detached

+			# Alt version:
+			# successor = node.leftChild or node.rightChild
+			# if node is @root then @root = successor
+			# successor.parent = node.parent
+			# node.parent?[_leftOrRightChild node] = successor
+
 _leftOrRightChild = (node) ->
 	# No need to check if parent exist. It's never used this way.
 	if node is node.parent.leftChild then "leftChild" else "rightChild"