Use RootView.eachEditor to attach snippet commands

Previously the 'editor:attached' was used which would not
fire for editors already attached and so any editors
opened before the snippets package was loaded would not
have snippets enabled.

--- src/packages/snippets/lib/snippets.coffee
+++ src/packages/snippets/lib/snippets.coffee
@@ -12,7 +12,8 @@
   activate: ->
     window.snippets = this
     @loadAll()
-    rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor)
+    rootView.eachEditor (editor) =>
+      @enableSnippetsInEditor(editor) if editor.attached

   deactivate: ->
     @loadSnippetsTask?.abort()


Cleaner handling of incoming post data

Cleaner handling of incoming post data

--- src/scripts/hubot-jenkins-notifier.coffee
+++ src/scripts/hubot-jenkins-notifier.coffee
@@ -70,6 +70,26 @@
       return false
     console.log "this should not happen"

+  dataMethodJSONParse: (req,data) ->
+    return false if typeof req.body != 'object'
+
+    # Remove __proto__ key from keys (newer node)
+    ret = Object.keys(req.body).filter (val) ->
+      val != '__proto__'
+
+    # if there is only one key remaining then process taht
+    try
+      if ret.length == 1
+        return JSON.parse ret[0]
+    catch err
+      return false
+
+    return false
+
+  dataMethodRaw: (req) ->
+    return false if typeof req.body != 'object'
+    return req.body
+
   process: (req,res) ->
     query = querystring.parse(url.parse(req.url).query)

@@ -82,23 +102,19 @@
     envelope.notstrat = 'FS' if query.always_notify #legacy
     envelope.user.type = query.type if query.type

-    try
-      # Newer versions of express/hubot already process posts that have Content-Type application/json
-      for key of req.body
-        # breaks in 0.11
-        if key == '__proto__'
-          continue
-        data = JSON.parse key
-        break
-    catch err
-      @error err, req.body
-      try
-        data = req.body
-      catch error
-        @error err, req.body
-        return
+    data = null

-    if typeof data.build != 'object'
+    filterChecker = (item, callback) ->
+      return if data
+
+      ret = item(req)
+      if (ret && ret.build)
+        data = ret
+        return true
+
+    [@dataMethodJSONParse, @dataMethodRaw].forEach(filterChecker)
+
+    if !data || typeof data.build != 'object'
       @error new Error("Unable to process data"), req.body
       return
     fullurl = encodeURI(data.build.full_url || data.build.url)


Fix ShareJsUpdateManager tests

Fix ShareJsUpdateManager tests

--- services/document-updater/test/unit/coffee/ShareJsUpdateManager/ShareJsUpdateManagerTests.coffee
+++ services/document-updater/test/unit/coffee/ShareJsUpdateManager/ShareJsUpdateManagerTests.coffee
@@ -17,12 +17,16 @@
 				"./ShareJsDB" : @ShareJsDB = { mockDB: true }
 				"redis-sharelatex" : createClient: () => @rclient = auth:->
 				"logger-sharelatex": @logger = { log: sinon.stub() }
+				"./WebRedisManager": @WebRedisManager = {}
 			globals:
 				clearTimeout: @clearTimeout = sinon.stub()

 	describe "applyUpdate", ->
 		beforeEach ->
+			@lines = ["one", "two"]
 			@version = 34
+			@update = {p: 4, t: "foo"}
+			@updatedDocLines = ["onefoo", "two"]
 			@model =
 				applyOp: sinon.stub().callsArg(2)
 				getSnapshot: sinon.stub()
@@ -31,20 +35,19 @@
 			@ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(@model)
 			@ShareJsUpdateManager._listenForOps = sinon.stub()
 			@ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1)
-			@update = {p: 4, t: "foo"}
-			@updatedDocLines = ["one", "two"]

 		describe "successfully", ->
 			beforeEach (done) ->
 				@model.getSnapshot.callsArgWith(1, null, {snapshot: @updatedDocLines.join("\n"), v: @version})
 				@model.db.appliedOps["#{@project_id}:#{@doc_id}"] = @appliedOps = ["mock-ops"]
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, (err, docLines, version, appliedOps) =>
+				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version, appliedOps) =>
 					@callback(err, docLines, version, appliedOps)
 					done()

 			it "should create a new ShareJs model", ->
 				@ShareJsUpdateManager.getNewShareJsModel
-					.called.should.equal true
+					.calledWith(@project_id, @doc_id, @lines, @version)
+					.should.equal true

 			it "should listen for ops on the model", ->
 				@ShareJsUpdateManager._listenForOps
@@ -69,7 +72,7 @@
 				@error = new Error("Something went wrong")
 				@ShareJsUpdateManager._sendError = sinon.stub()
 				@model.applyOp = sinon.stub().callsArgWith(2, @error)
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, (err, docLines, version) =>
+				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
 					@callback(err, docLines, version)
 					done()

@@ -86,7 +89,7 @@
 				@error = new Error("Something went wrong")
 				@ShareJsUpdateManager._sendError = sinon.stub()
 				@model.getSnapshot.callsArgWith(1, @error)
-				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, (err, docLines, version) =>
+				@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
 					@callback(err, docLines, version)
 					done()

@@ -114,22 +117,22 @@
 				@opData =
 					op: {t: "foo", p: 1}
 					meta: source: "bar"
-				@rclient.publish = sinon.stub()
+				@WebRedisManager.sendData = sinon.stub()
 				@callback("#{@project_id}:#{@doc_id}", @opData)

 			it "should publish the op to redis", ->
-				@rclient.publish
-					.calledWith("applied-ops", JSON.stringify(project_id: @project_id, doc_id: @doc_id, op: @opData))
+				@WebRedisManager.sendData
+					.calledWith({project_id: @project_id, doc_id: @doc_id, op: @opData})
 					.should.equal true

 	describe "_sendError", ->
 		beforeEach ->
 			@error_text = "Something went wrong"
-			@rclient.publish = sinon.stub()
+			@WebRedisManager.sendData = sinon.stub()
 			@ShareJsUpdateManager._sendError(@project_id, @doc_id, new Error(@error_text))

 		it "should publish the error to the redis stream", ->
-			@rclient.publish
-				.calledWith("applied-ops", JSON.stringify(project_id: @project_id, doc_id: @doc_id, error: @error_text))
+			@WebRedisManager.sendData
+				.calledWith({project_id: @project_id, doc_id: @doc_id, error: @error_text})
 				.should.equal true


FIX: check for blank containers

FIX: check for blank containers

--- app/assets/javascripts/media_lib/inserter_factory.js.coffee
+++ app/assets/javascripts/media_lib/inserter_factory.js.coffee
@@ -49,11 +49,12 @@
     $uploader.find('.js-uploader-input').trigger 'change'

     $playerContainer = $uploader.find('.js-player-container')
-    $player = $('<div>')
-    $playerContainer.html $player
-    $player.nmdVideoPlayerJw setup:
-      file: @model.static_url
-      height: 30
+    if $playerContainer.length
+      $player = $('<div>')
+      $playerContainer.html $player
+      $player.nmdVideoPlayerJw setup:
+        file: @model.static_url
+        height: 30

 # Стратегия вставки видео-модели
 class MediaLib.VideoInserter extends MediaLib.BaseInserter
@@ -61,20 +62,22 @@
     $uploader.find('.js-uploader-input').val @model.static_name
     $uploader.find('.js-uploader-input').trigger 'change'

-    $player = $('<div>')
-    $uploader.find('.js-uploader-video').html $player
-    $player.nmdVideoPlayerJw setup:
-      file: @model.static_url
-      image: @model.player_image
-      aspectratio: '16:9'
-      events:
-        onError: ->
-          if @model.player_video_fallback
-            $player.nmdVideoPlayerJw setup:
-              aspectratio: '16:9'
-              file: model.player_video_fallback
-              image: model.player_image
-              autostart: true
+    $uploaderVideo = $uploader.find('.js-uploader-video')
+    if $uploaderVideo.length
+      $player = $('<div>')
+      $uploaderVideo.html $player
+      $player.nmdVideoPlayerJw setup:
+        file: @model.static_url
+        image: @model.player_image
+        aspectratio: '16:9'
+        events:
+          onError: ->
+            if @model.player_video_fallback
+              $player.nmdVideoPlayerJw setup:
+                aspectratio: '16:9'
+                file: model.player_video_fallback
+                image: model.player_image
+                autostart: true

 # Стратегия вставки одного изображения
 class MediaLib.SingleImageInserter extends MediaLib.BaseInserter


Activate game dev Spawnable translations in the UI

--- app/views/play/level/tome/SpellPaletteThangEntryView.coffee
+++ app/views/play/level/tome/SpellPaletteThangEntryView.coffee
@@ -29,13 +29,15 @@
       example = options.doc.example?[options.language]
     else
       example = "\# usage code \ngame.spawnXY(\"#{options.buildableName}\", 21, 20)"
+    description = utils.i18n(options.doc, 'description')
+    translatedName = utils.i18n(options.doc, 'name')
     @doc =
       name: options.buildableName
       initialHTML: popoverTemplate _: _, marked: marked, doc:
-        #shortName: @thang.get('name')
         shortName: options.doc.name
+        translatedShortName: if translatedName isnt options.doc.name then translatedName else undefined
         type: "spawnable"
-        description: "![#{@thang.get('name')}](#{@thang.getPortraitURL()}) #{options.doc.description}"
+        description: "![#{@thang.get('name')}](#{@thang.getPortraitURL()}) #{description}"
         example: example
       example: example


move shiftSchedule contents into isServer blocks

--- both/methods/shift/shiftSchedule.coffee
+++ both/methods/shift/shiftSchedule.coffee
@@ -9,101 +9,101 @@
 			if message?
 				check message, String

-		for team in shift.teams when team._id == teamId
-			Meteor.call 'sendCancelTeam', shiftId, teamId, message, ->
-				if message == 'missingParticipant'
-					for pendingUser in team.pending
-						pendingUser.checked = false
-						pendingUser.informed = false
-
-						Shifts.update _id: shiftId, 'teams._id': teamId,
-							$pull: 'teams.$.pending': _id: pendingUser._id
-							$addToSet: 'teams.$.declined': pendingUser
+			for team in shift.teams when team._id == teamId
+				Meteor.call 'sendCancelTeam', shiftId, teamId, message, ->
+					if message == 'missingParticipant'
+						for pendingUser in team.pending
+							pendingUser.checked = false
+							pendingUser.informed = false
+
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$pull: 'teams.$.pending': _id: pendingUser._id
+								$addToSet: 'teams.$.declined': pendingUser
+
+						for participant in team.participants
+							participant.checked = true
+							participant.informed = false
+
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$pull: 'teams.$.participants': _id: participant._id
+								$addToSet: 'teams.$.pending': participant
+
+						Meteor.call 'openTeam', shiftId, teamId
+
+					else
+						for participant in team.participants.concat(team.pending)
+							participant.thisTeamleader = false
+
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$pull:
+									'teams.$.participants': _id: participant._id
+									'teams.$.pending': _id: participant._id
+								$addToSet: 'teams.$.declined': participant
+
+						Meteor.call 'closeTeam', shiftId, teamId
+
+	acceptRequest: (shiftId, teamId, userId) ->
+		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
+		user = Meteor.users.findOne userId, fields: _id: 1
+
+		if Meteor.isServer
+			check userId, isExistingUser
+			check { shiftId: shiftId, teamId: teamId }, isExistingShiftAndTeam
+			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
+			check { tagId: shift.tagId, userId: userId }, isTagParticipant
+
+			for team in shift.teams
+				if team._id == teamId
+					hasTeamleader = false
+					isTeamleader = false

 					for participant in team.participants
-						participant.checked = true
-						participant.informed = false
-
-						Shifts.update _id: shiftId, 'teams._id': teamId,
-							$pull: 'teams.$.participants': _id: participant._id
-							$addToSet: 'teams.$.pending': participant
-
-					Meteor.call 'openTeam', shiftId, teamId
-
-				else
-					for participant in team.participants.concat(team.pending)
-						participant.thisTeamleader = false
+						if participant.thisTeamleader
+							hasTeamleader = true
+						if participant._id == userId
+							isTeamleader = (participant.teamleader || participant.substituteTeamleader)
+
+					for acceptedUser in team.pending.concat(team.declined) when acceptedUser._id == userId
+						acceptedUser.thisTeamleader = (!hasTeamleader && isTeamleader)
+						acceptedUser.checked = false

 						Shifts.update _id: shiftId, 'teams._id': teamId,
 							$pull:
-								'teams.$.participants': _id: participant._id
-								'teams.$.pending': _id: participant._id
-							$addToSet: 'teams.$.declined': participant
+								'teams.$.pending': _id: userId
+								'teams.$.declined': _id: userId
+							$addToSet: 'teams.$.participants': acceptedUser
+						break

 					Meteor.call 'closeTeam', shiftId, teamId
-
-	acceptRequest: (shiftId, teamId, userId) ->
-		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
-		user = Meteor.users.findOne userId, fields: _id: 1
-
-		if Meteor.isServer
-			check userId, isExistingUser
-			check { shiftId: shiftId, teamId: teamId }, isExistingShiftAndTeam
-			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
-			check { tagId: shift.tagId, userId: userId }, isTagParticipant
-
-		for team in shift.teams
-			if team._id == teamId
-				hasTeamleader = false
-				isTeamleader = false
-
-				for participant in team.participants
-					if participant.thisTeamleader
-						hasTeamleader = true
-					if participant._id == userId
-						isTeamleader = (participant.teamleader || participant.substituteTeamleader)
-
-				for acceptedUser in team.pending.concat(team.declined) when acceptedUser._id == userId
-					acceptedUser.thisTeamleader = (!hasTeamleader && isTeamleader)
-					acceptedUser.checked = false
-
-					Shifts.update _id: shiftId, 'teams._id': teamId,
-						$pull:
-							'teams.$.pending': _id: userId
-							'teams.$.declined': _id: userId
-						$addToSet: 'teams.$.participants': acceptedUser
-					break
-
-				Meteor.call 'closeTeam', shiftId, teamId
-			else
-				for user in team.participants when user._id == userId
-					wholeTeamCancelled = false
-
-					if user.thisTeamleader
-						foundTeamleader = false
-
-						for participant in team.participants when participant.teamleader
-							foundTeamleader = true
-							Meteor.call 'setLeader', shiftId, team._id, participant._id
-							break
-
-						if !foundTeamleader
-							for participant in team.participants when participant.substituteTeamleader
+				else
+					for user in team.participants when user._id == userId
+						wholeTeamCancelled = false
+
+						if user.thisTeamleader
+							foundTeamleader = false
+
+							for participant in team.participants when participant.teamleader
 								foundTeamleader = true
 								Meteor.call 'setLeader', shiftId, team._id, participant._id
 								break

-						if !foundTeamleader
-							wholeTeamCancelled = true
-							Meteor.call 'cancelTeam', shiftId, team._id
-
-					if !wholeTeamCancelled
-						user.thisTeamleader = false
-
-						Shifts.update _id: shiftId, 'teams._id': team._id,
-							$pull: 'teams.$.participants': _id: userId
-							$addToSet: 'teams.$.declined': user
-					break
+							if !foundTeamleader
+								for participant in team.participants when participant.substituteTeamleader
+									foundTeamleader = true
+									Meteor.call 'setLeader', shiftId, team._id, participant._id
+									break
+
+							if !foundTeamleader
+								wholeTeamCancelled = true
+								Meteor.call 'cancelTeam', shiftId, team._id
+
+						if !wholeTeamCancelled
+							user.thisTeamleader = false
+
+							Shifts.update _id: shiftId, 'teams._id': team._id,
+								$pull: 'teams.$.participants': _id: userId
+								$addToSet: 'teams.$.declined': user
+						break

 	declineRequest: (shiftId, teamId, userId) ->
 		shift = Shifts.findOne shiftId, fields: teams: 1
@@ -111,13 +111,13 @@
 		if Meteor.isServer
 			check { shiftId: shiftId, teamId: teamId }, isExistingShiftAndTeam

-		for team in shift.teams when team._id == teamId
-			for pending in team.pending when pending._id == userId
-				user = pending
-
-		Shifts.update _id: shiftId, 'teams._id': teamId,
-			$pull: 'teams.$.pending': _id: userId
-			$addToSet: 'teams.$.declined': user
+			for team in shift.teams when team._id == teamId
+				for pending in team.pending when pending._id == userId
+					user = pending
+
+			Shifts.update _id: shiftId, 'teams._id': teamId,
+				$pull: 'teams.$.pending': _id: userId
+				$addToSet: 'teams.$.declined': user

 	declineParticipant: (shiftId, teamId, userId) ->
 		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
@@ -129,87 +129,87 @@
 			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
 			check { tagId: shift.tagId, userId: userId }, isTagParticipant

-		for team in shift.teams when team._id == teamId
-			if team.participants.length == team.min
-				Meteor.call 'cancelTeam', shiftId, teamId, 'missingParticipant'
-			else
-				wasTeamleader = false
-				hasTeamleader = false
-				participantData = {}
-				newTeamleaderData = {}
-
+			for team in shift.teams when team._id == teamId
+				if team.participants.length == team.min
+					Meteor.call 'cancelTeam', shiftId, teamId, 'missingParticipant'
+				else
+					wasTeamleader = false
+					hasTeamleader = false
+					participantData = {}
+					newTeamleaderData = {}
+
+					for participant in team.participants
+						if participant._id == userId
+							participantData = participant
+
+							if participant.thisTeamleader
+								wasTeamleader = true
+								participantData.thisTeamleader = false
+						else if participant.teamleader || participant.substituteTeamleader
+							hasTeamleader = true
+							newTeamleaderData = participant
+							newTeamleaderData.thisTeamleader = true
+
+					if wasTeamleader
+						if hasTeamleader
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$pull: 'teams.$.participants': _id: userId
+
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$pull: 'teams.$.participants': _id: newTeamleaderData._id
+
+							Shifts.update _id: shiftId, 'teams._id': teamId,
+								$addToSet:
+									'teams.$.declined': participantData
+									'teams.$.participants': newTeamleaderData
+						else
+							Meteor.call 'cancelTeam', shiftId, teamId
+					else
+						Shifts.update _id: shiftId, 'teams._id': teamId,
+							$pull: 'teams.$.participants': _id: userId
+							$addToSet: 'teams.$.declined': participantData
+
+					if participantData.informed and userId != Meteor.userId()
+						Meteor.call 'sendReversal', shiftId, teamId, userId
+
+	setLeader: (shiftId, teamId, userId) ->
+		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
+		user = Meteor.users.findOne userId, fields: _id: 1
+
+		if Meteor.isServer
+			check userId, isExistingUser
+			check { shiftId: shiftId, teamId: teamId }, isExistingShiftAndTeam
+			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
+			check { tagId: shift.tagId, userId: userId }, isTeamleader
+
+			for team in shift.teams when team._id == teamId
 				for participant in team.participants
 					if participant._id == userId
-						participantData = participant
-
-						if participant.thisTeamleader
-							wasTeamleader = true
-							participantData.thisTeamleader = false
-					else if participant.teamleader || participant.substituteTeamleader
-						hasTeamleader = true
-						newTeamleaderData = participant
-						newTeamleaderData.thisTeamleader = true
-
-				if wasTeamleader
-					if hasTeamleader
-						Shifts.update _id: shiftId, 'teams._id': teamId,
-							$pull: 'teams.$.participants': _id: userId
-
-						Shifts.update _id: shiftId, 'teams._id': teamId,
-							$pull: 'teams.$.participants': _id: newTeamleaderData._id
-
-						Shifts.update _id: shiftId, 'teams._id': teamId,
-							$addToSet:
-								'teams.$.declined': participantData
-								'teams.$.participants': newTeamleaderData
-					else
-						Meteor.call 'cancelTeam', shiftId, teamId
-				else
-					Shifts.update _id: shiftId, 'teams._id': teamId,
-						$pull: 'teams.$.participants': _id: userId
-						$addToSet: 'teams.$.declined': participantData
-
-				if participantData.informed and userId != Meteor.userId()
-					Meteor.call 'sendReversal', shiftId, teamId, userId
-
-	setLeader: (shiftId, teamId, userId) ->
-		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
-		user = Meteor.users.findOne userId, fields: _id: 1
-
-		if Meteor.isServer
-			check userId, isExistingUser
-			check { shiftId: shiftId, teamId: teamId }, isExistingShiftAndTeam
-			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
-			check { tagId: shift.tagId, userId: userId }, isTeamleader
-
-		for team in shift.teams when team._id == teamId
-			for participant in team.participants
-				if participant._id == userId
-					if !participant.thisTeamleader
-						if Roles.userIsInRole participant._id, Permissions.teamleader, shift.tagId
-							participant = participant
-							participant.thisTeamleader = true
-
-							Shifts.update _id: shiftId, 'teams._id': team._id,
-								$pull: 'teams.$.participants': _id: userId
-
-							Shifts.update _id: shiftId, 'teams._id': team._id,
-								$addToSet: 'teams.$.participants': participant
+						if !participant.thisTeamleader
+							if Roles.userIsInRole participant._id, Permissions.teamleader, shift.tagId
+								participant = participant
+								participant.thisTeamleader = true
+
+								Shifts.update _id: shiftId, 'teams._id': team._id,
+									$pull: 'teams.$.participants': _id: userId
+
+								Shifts.update _id: shiftId, 'teams._id': team._id,
+									$addToSet: 'teams.$.participants': participant
+							else
+								throw new Meteor.Error 500, TAPi18n.__('modal.shift.noTeamleader')
 						else
-							throw new Meteor.Error 500, TAPi18n.__('modal.shift.noTeamleader')
-					else
-						throw new Meteor.Error 500, TAPi18n.__('modal.shift.alreadyTeamleader')
-				else if participant.thisTeamleader
-					participant.thisTeamleader = false
-
-					Shifts.update _id: shiftId, 'teams._id': team._id,
-						$pull: 'teams.$.participants': _id: participant._id
-
-					Shifts.update _id: shiftId, 'teams._id': team._id,
-						$addToSet: 'teams.$.participants': participant
-
-			Meteor.call 'sendTeamUpdate', shiftId, teamId, 'leader'
-			break
+							throw new Meteor.Error 500, TAPi18n.__('modal.shift.alreadyTeamleader')
+					else if participant.thisTeamleader
+						participant.thisTeamleader = false
+
+						Shifts.update _id: shiftId, 'teams._id': team._id,
+							$pull: 'teams.$.participants': _id: participant._id
+
+						Shifts.update _id: shiftId, 'teams._id': team._id,
+							$addToSet: 'teams.$.participants': participant
+
+				Meteor.call 'sendTeamUpdate', shiftId, teamId, 'leader'
+				break

 	addParticipant: (shiftId, teamId, userId) ->
 		shift = Shifts.findOne shiftId, fields: teams: 1, tagId: 1, projectId: 1
@@ -225,30 +225,30 @@
 			check { projectId: shift.projectId, userId: Meteor.userId() }, isShiftAdmin
 			check { tagId: shift.tagId, userId: userId }, isTagParticipant

-		for team in shift.teams when team._id == teamId
-			for notAcceptedUser in team.declined.concat(team.pending) when notAcceptedUser._id == userId
-				throw new Meteor.Error 500, TAPi18n.__('modal.addParticipant.alreadyRequested')
-
-			for acceptedUser in team.participants when acceptedUser._id == userId
-				throw new Meteor.Error 500, TAPi18n.__('modal.addParticipant.alreadyParticipating')
-			break
-
-		user =
-			_id: userId
-			name: user.profile.firstname + ' ' + user.profile.lastname
-			teamleader: Roles.userIsInRole userId, 'teamleader', shift.tagId
-			substituteTeamleader: Roles.userIsInRole userId, 'substituteTeamleader', shift.tagId
-			thisTeamleader: false
-			phone: user.profile.telefon
-			email: user.profile.email
-
-		for team in shift.teams when team._id != teamId
-			if userId in team.participants
-				Meteor.call 'declineParticipant', shiftId, team._id, userId
-			else if userId in team.pending
-				Shifts.update _id: shiftId, 'teams._id': team._id,
-					$pull: 'teams.$.pending': _id: userId
-					$addToSet: 'teams.$.declined': user
-
-		Shifts.update _id: shiftId, 'teams._id': teamId,
-			$addToSet: 'teams.$.participants': user
+			for team in shift.teams when team._id == teamId
+				for notAcceptedUser in team.declined.concat(team.pending) when notAcceptedUser._id == userId
+					throw new Meteor.Error 500, TAPi18n.__('modal.addParticipant.alreadyRequested')
+
+				for acceptedUser in team.participants when acceptedUser._id == userId
+					throw new Meteor.Error 500, TAPi18n.__('modal.addParticipant.alreadyParticipating')
+				break
+
+			user =
+				_id: userId
+				name: user.profile.firstname + ' ' + user.profile.lastname
+				teamleader: Roles.userIsInRole userId, 'teamleader', shift.tagId
+				substituteTeamleader: Roles.userIsInRole userId, 'substituteTeamleader', shift.tagId
+				thisTeamleader: false
+				phone: user.profile.telefon
+				email: user.profile.email
+
+			for team in shift.teams when team._id != teamId
+				if userId in team.participants
+					Meteor.call 'declineParticipant', shiftId, team._id, userId
+				else if userId in team.pending
+					Shifts.update _id: shiftId, 'teams._id': team._id,
+						$pull: 'teams.$.pending': _id: userId
+						$addToSet: 'teams.$.declined': user
+
+			Shifts.update _id: shiftId, 'teams._id': teamId,
+				$addToSet: 'teams.$.participants': user


Export exercise card mixin for use by QA view

--- src/components/task-plan/homework/exercises.cjsx
+++ src/components/task-plan/homework/exercises.cjsx
@@ -361,4 +361,4 @@
     </BS.Grid>

-module.exports = {AddExercises, ReviewExercises, ExerciseTable}
+module.exports = {AddExercises, ReviewExercises, ExerciseTable, ExerciseCardMixin}


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/point.coffee
+++ test/point.coffee
@@ -141,7 +141,7 @@

   test "#clamp", ->
     p = Point(10, 10)
-    p.clamp()
+    p2p.clamp(5)

   test ".centroid", ->
     centroid = Point.centroid(


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/point.coffee
+++ test/point.coffee
@@ -141,7 +141,9 @@

   test "#clamp", ->
     p = Point(10, 10)
-    p2p.clamp(5)
+    p2 = p.clamp(5)
+
+    e

   test ".centroid", ->
     centroid = Point.centroid(


post is a JSON string

post is a JSON string

--- src/matteruser.coffee
+++ src/matteruser.coffee
@@ -118,24 +118,23 @@

     message: (msg) =>
         @robot.logger.debug msg
-        data = msg.data
-        return if data.post.user_id == @self.id # Ignore our own output
-        @robot.logger.debug 'From: ' + data.post.user_id + ', To: ' + @self.id
+        mmPost = JSON.parse msg.data.post
+        return if mmPost.user_id == @self.id # Ignore our own output
+        @robot.logger.debug 'From: ' + mmPost.user_id + ', To: ' + @self.id

-        mmChannel = @client.getChannelByID data.post.channel_id if data.post.channel_id
-        mmUser = @client.getUserByID data.post.user_id
-        mmPost = JSON.parse data.post
+        mmChannel = @client.getChannelByID mmPost.channel_id if mmPost.channel_id
+        mmUser = @client.getUserByID mmPost.user_id

         @robot.logger.debug 'Received message from '+mmUser.username+': ' + mmPost.message
-        user = @robot.brain.userForId data.post.user_id
-        user.room = data.post.channel_id
+        user = @robot.brain.userForId mmPost.user_id
+        user.room = mmPost.channel_id

         text = mmPost.message
         if msg.data.channel_type == 'D' and !///^#{@robot.name} ///i.test(text) # Direct message
           text = "#{@robot.name} #{text}"
-          user.mm.dm_channel_id = data.post.channel_id
+          user.mm.dm_channel_id = mmPost.channel_id

-        @receive new TextMessage user, text, data.post.id
+        @receive new TextMessage user, text, mmPost.id
         @robot.logger.debug "Message sent to hubot brain."
         return true


Guard against no constructor

Guard against no constructor

--- lib/tab-view.coffee
+++ lib/tab-view.coffee
@@ -112,7 +112,7 @@
       delete @itemTitle.dataset.name
       delete @itemTitle.dataset.path

-    if itemClass = @item.constructor.name
+    if itemClass = @item.constructor?.name
       @dataset.type = itemClass
     else
       delete @dataset.type


add comment to UTF encoding technique

add comment to UTF encoding technique

--- src/element.coffee
+++ src/element.coffee
@@ -59,6 +59,13 @@
     null

 encodeUTF = (string) ->
+  # encoding UTF is required to make WebDriver see it properly,
+  # but it comes back out (e.g., `element.get('text')`)
+  # without the need for decoding it
+  #
+  # this is "a quick way to implement a UTF-8 encoder/decoder,
+  # by leveraging the UTF-8 processing in URIComponent handling"
+  # http://stackoverflow.com/questions/619323/decodeuricomponent-vs-unescape-what-is-wrong-with-unescape/619428#619428
   unescape(encodeURIComponent(string))

 module.exports = class Element


Return this on element operations.

Return this on element operations.

--- src/element.coffee
+++ src/element.coffee
@@ -56,6 +56,7 @@
         return this if @isStatic()
         velocity = @getVelocity().add(impulse)
         @setVelocity(velocity)
+        return this

     # Apply the given force to the element for the given number of
     # milliseconds.
@@ -71,6 +72,7 @@
         # Update the element's state.
         @setPosition(position)
         @setVelocity(velocity)
+        return this

     # Apply the given forces to the element on the next step of the simulation.
     addForces : (forces...) ->
@@ -84,6 +86,7 @@
         resultant = @forces.reduce (t, s) -> t.add(s)
         @applyForce(resultant, milliseconds)
         @forces = []
+        return this

     # Return true if this element intersects with the other
     # element, false otherwise.


the whole task plan store shouldnt be reset.. rather the specific plan should be reset on edit cancel

--- src/components/task-plan/plan-mixin.coffee
+++ src/components/task-plan/plan-mixin.coffee
@@ -89,7 +89,7 @@

   reset: ->
     {id, courseId} = @props
-    TaskPlanActions.reset(id)
+    TaskPlanActions.resetPlan(id)
     @goBackToCalendar()

   # TODO move to helper type thing.


Type ctor word-anchoring

Type ctor word-anchoring

--- src/haskell.coffee
+++ src/haskell.coffee
@@ -69,7 +69,7 @@
       |(?:(?!deriving)(?:[\w()'→⇒\[\],]|->|=>)+\s*)+ #anything goes!
       )
       ///
-    ctor: concat /({className})\s+/,
+    ctor: concat /\b({className})\s+/,
       listMaybe('ctorArgs',/{ctorArgs}/,/\s+/)
     typeDecl: /.+?/
     indentChar: /[ \t]/


:art:

:art:

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -116,7 +116,7 @@

 exports.keystrokeForKeyboardEvent = (event, dvorakQwertyWorkaroundEnabled) ->
   keyIdentifier = event.keyIdentifier
-  if process.platform is 'linux' or process.platform is 'win32'
+  if process.platform in ['linux', 'win32']
     keyIdentifier = translateKeyIdentifierForWindowsAndLinuxChromiumBug(keyIdentifier)

   unless KeyboardEventModifiers.has(keyIdentifier)


Implement text input replay using KeyboardLayout.getCurrentKeymap()

Signed-off-by: Nathan Sobo <2e8aa918660411855c6d44d5bb2da677aa033255@github.com>

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -87,10 +87,18 @@
   keystroke = normalizeKeystroke("^#{keystroke}") if event.type is 'keyup'
   keystroke

-exports.characterForKeyboardEvent = (event, dvorakQwertyWorkaroundEnabled) ->
-  unless event.ctrlKey or event.altKey or event.metaKey
-    if key = keyForKeyboardEvent(event, dvorakQwertyWorkaroundEnabled)
-      key if key.length is 1
+exports.characterForKeyboardEvent = (event, allowModifiers = false) ->
+  unless event.ctrlKey or event.metaKey
+    if characters = KeyboardLayout.getCurrentKeymap()[event.code]
+      if event.shiftKey
+        if event.altKey
+          return characters.withShiftAltGr
+        else
+          return characters.withShift
+      else if event.altKey
+        return characters.withAltGr
+      else
+        return characters.unmodified

 exports.calculateSpecificity = calculateSpecificity


Use Path instead of PATH env var on Windows

--- src/install.coffee
+++ src/install.coffee
@@ -59,10 +59,18 @@

     # Make sure node-gyp is always on the PATH
     localModuleBins = path.resolve(__dirname, '..', 'node_modules', '.bin')
-    if env.PATH
-      env.PATH += "#{path.delimiter}#{localModuleBins}"
+    if env.Path
+      env.Path += "#{path.delimiter}#{localModuleBins}"
     else
-      env.PATH = localModuleBins
+      env.Path = localModuleBins
+
+  addNodeBinToEnv: (env) ->
+    nodeBinFolder = path.resolve(__dirname, '..', 'bin')
+    pathKey = if config.isWin32() then 'Path' else 'PATH'
+    if env[pathKey]
+      env[pathKey] = "#{nodeBinFolder}#{path.delimiter}#{env[pathKey]}"
+    else
+      env[pathKey]= nodeBinFolder

   installModule: (options, pack, modulePath, callback) ->
     installArgs = ['--userconfig', config.getUserConfigPath(), 'install']
@@ -77,12 +85,7 @@

     env = _.extend({}, process.env, HOME: @atomNodeDirectory)
     @updateWindowsEnv(env) if config.isWin32()
-
-    nodeBinFolder = path.resolve(__dirname, '..', 'bin')
-    if env.PATH
-      env.PATH = "#{nodeBinFolder}#{path.delimiter}#{env.PATH}"
-    else
-      env.PATH = nodeBinFolder
+    @addNodeBinToEnv(env)
     installOptions = {env}

     installGlobally = options.installGlobally ? true
@@ -140,7 +143,7 @@

     env = _.extend({}, process.env, HOME: @atomNodeDirectory)
     @updateWindowsEnv(env) if config.isWin32()
-
+    @addNodeBinToEnv(env)
     installOptions = {env}
     installOptions.cwd = options.cwd if options.cwd


moved request creation logic to foreach loop

--- servers/lib/server/handlers/getteam.test.coffee
+++ servers/lib/server/handlers/getteam.test.coffee
@@ -41,22 +41,19 @@

   it 'should send HTTP 404 if group does not exist using any method', (done) ->

-    addRequestToQueue = (queue, method, requestParams) ->
-      requestParams.method = method
+    queue                 = []
+    methods               = ['post', 'get', 'put', 'patch']
+    groupSlug             = generateRandomString()
+
+    methods.forEach (method) ->
+      getTeamRequestParams  = generateGetTeamRequestParams { method, groupSlug }
+
       queue.push ->
-        request requestParams, (err, res, body) ->
+        request getTeamRequestParams, (err, res, body) ->
           expect(err)             .to.not.exist
           expect(res.statusCode)  .to.be.equal 404
           expect(body)            .to.be.equal 'no group found'
           queue.next()
-
-    queue                 = []
-    methods               = ['post', 'get', 'put', 'patch']
-    groupSlug             = generateRandomString()
-    getTeamRequestParams  = generateGetTeamRequestParams { groupSlug }
-
-    for method in methods
-      addRequestToQueue queue, method, getTeamRequestParams

     queue.push -> done()

@@ -65,18 +62,9 @@

   it 'should send HTTP 200 if slug is valid using any method', (done) ->

-    addRequestToQueue = (queue, method, requestParams) ->
-      requestParams.method = method
-      queue.push ->
-        request requestParams, (err, res, body) ->
-          expect(err)             .to.not.exist
-          expect(res.statusCode)  .to.be.equal 200
-          queue.next()
-
     queue                     = []
     methods                   = ['post', 'get', 'put', 'patch']
     groupSlug                 = generateRandomString()
-    getTeamRequestParams      = generateGetTeamRequestParams { groupSlug }

     createTeamRequestParams   = generateCreateTeamRequestParams
       body    :
@@ -88,8 +76,14 @@
         expect(res.statusCode)  .to.be.equal 200
         queue.next()

-    for method in methods
-      addRequestToQueue queue, method, getTeamRequestParams
+    methods.forEach (method) ->
+      getTeamRequestParams = generateGetTeamRequestParams { method, groupSlug }
+
+      queue.push ->
+        request getTeamRequestParams, (err, res, body) ->
+          expect(err)             .to.not.exist
+          expect(res.statusCode)  .to.be.equal 200
+          queue.next()

     queue.push -> done()


TestHelper: to allow groupSlug passed as for existence groups

--- workers/social/testhelper/index.coffee
+++ workers/social/testhelper/index.coffee
@@ -81,10 +81,10 @@

   [opts, callback] = [callback, opts]  unless callback
   opts ?= {}
-  { context, userFormData, createGroup } = opts
+  { context, userFormData, createGroup, groupSlug } = opts

   # if createGroup parameter is truthy generate random group slug
-  groupSlug = if createGroup
+  groupSlug ?= if createGroup
   then generateRandomString()
   else 'koding'


Auth function test.

Auth function test.

--- test/tests.coffee
+++ test/tests.coffee
@@ -178,6 +178,16 @@
           chatServer = new ChatService { port : port }, null, state
           socket1 = ioClient.connect url1, makeParams()
           socket1.on 'loginRejected', ->
+            done()
+
+        it 'should execute auth hook', (done) ->
+          reason = 'some reason'
+          auth = (socket, cb) ->
+            cb( new Error reason )
+          chatServer = new ChatService { port : port }, { auth : auth }, state
+          socket1 = ioClient.connect url1, makeParams()
+          socket1.on 'error', (e) ->
+            expect(e).deep.equal(reason)
             done()

         it 'should support multiple sockets per user', (done) ->


library: Add default .py handler for msgflo-python

--- src/library.coffee
+++ src/library.coffee
@@ -6,11 +6,11 @@

 common = require './common'

-# TODO: also add msgflo-python
 defaultHandlers =
   ".yml":     "msgflo-register-foreign --role #ROLE #FILENAME"
   ".js":      "msgflo-nodejs --name #ROLE #FILENAME"
   ".coffee":  "msgflo-nodejs --name #ROLE #FILENAME"
+  ".py":  "msgflo-python #FILENAME #ROLE"
   ".json":    "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"
   ".fbp":     "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS"


Make library contain 'abstract' component definitions without queues, ids, etc

--- src/library.coffee
+++ src/library.coffee
@@ -102,6 +102,20 @@

   return config

+# Remove instance-specific data like role and extra from library data
+cleanComponentDefinition = (discovered) ->
+  return discovered unless discovered?.definition
+  # Start by cloning the definition
+  component = JSON.parse JSON.stringify discovered
+  return component unless component?.definition
+  delete component.definition.extra
+  delete component.definition.id
+  for port in component.definition.inports
+    delete port.queue
+  for port in component.definition.outports
+    delete port.queue
+  return component
+
 class Library extends EventEmitter
   constructor: (options) ->
     options.config = JSON.parse(fs.readFileSync options.configfile, 'utf-8') if options.configfile
@@ -129,20 +143,18 @@
         @components[name] = null
         names.push name if names.indexOf(name) is -1
         continue
+      discovered = cleanComponentDefinition comp
       existing = @getComponent name
       unless existing?.definition
         # added
-        @components[name] = comp
+        @components[name] = discovered
         names.push name if names.indexOf(name) is -1
         continue
-      # update
-      for k, v of comp.definition
-        if existing.definition[k]
-          # Check if value has changed
-          continue if JSON.stringify(existing.definition[k]) is JSON.stringify(v)
-        existing.definition[k] = v
-        continue if k in ['extra', 'id']
+      unless JSON.stringify(existing.definition) is JSON.stringify(discovered.definition)
+        # updated
+        @components[name] = discovered
         names.push name if names.indexOf(name) is -1
+        continue

     # Skip sending components-changed if nothing changed
     return unless names.length


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -1,6 +1,6 @@
 ###*
 The Metered module provides a simple drop-in
-meter ui to track arbitrary numeric attributes
+meter ui to track arbitrary numeric attributes.

 player = Core


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -130,7 +130,7 @@
     borderColor - Color of the meter's border
     borderWidth - Width of the meter's border
     color - Color of the meter's inner rectangle
-    nameColor - Color of the property name
+    nameColor - Color of the property name displayed above
   ###
   meter: (options={}) ->
     Object.reverseMerge options,


Prevent errors in iframe gadgets with dist dir

--- src/preview.coffee
+++ src/preview.coffee
@@ -24,6 +24,7 @@

   async.map dirs, linkManifestDir.bind(this, app), (err, manifests) ->
     if err then return callback err
+
     async.map dirs, maybeLinkLegacyDir.bind(this, app), (err) ->
       if err then return callback err

@@ -63,7 +64,12 @@
 # TODO deprecate along with legacy gadgets
 maybeLinkLegacyDir = (app, dir, callback) ->
   dir = path.join dir, 'dist'
-  unless fs.existsSync dir then return callback()
+  legacyManifestPath = path.join dir, 'manifest.json'
+
+  # Don't bother if there isn't a manifest there
+  # because it could be a modern gadget's build dir.
+  unless fs.existsSync legacyManifestPath
+    return callback()

   manifest.readManifest dir, (err, man) ->
     if err then return callback(err)


Trim output

Trim output

--- src/browser/auto-updater-win32.coffee
+++ src/browser/auto-updater-win32.coffee
@@ -39,7 +39,7 @@

       try
         # Last line of output is the JSON details about the release
-        [json] = stdout.split('\n').reverse()
+        [json] = stdout.trim().split('\n').reverse()
         update = JSON.parse(json)?.releasesToApply?.pop?()
       catch error
         error.stdout = stdout


fixed issue with x-axis labels not being hidden properly

--- src/viz/types/helpers/graph/includes/plot.coffee
+++ src/viz/types/helpers/graph/includes/plot.coffee
@@ -188,7 +188,6 @@
   else
     xMaxWidth  = vars.x.scale.viz(xValues[1]) - vars.x.scale.viz(xValues[0])
     xMaxWidth  = Math.abs xMaxWidth
-    xMaxWidth -= vars.labels.padding * 2

   if xAxisWidth > xMaxWidth and xText.join("").indexOf(" ") > 0
     vars.x.ticks.wrap = true
@@ -217,10 +216,12 @@
     xAxisWidth          = d3.max xSizes, (d) -> d.height
     vars.x.ticks.rotate = -90
   else
-    vars.x.ticks.hidden    = true
-    vars.x.ticks.rotate    = 0
-    xAxisWidth             = 0
-    xAxisHeight            = 0
+    xAxisWidth  = 0
+    xAxisHeight = 0
+
+  unless xAxisWidth and xAxisHeight
+    vars.x.ticks.hidden = true
+    vars.x.ticks.rotate = 0

   xAxisHeight = Math.ceil xAxisHeight
   xAxisWidth  = Math.ceil xAxisWidth


✅ Add specs confirming bug in current behavior

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -743,7 +743,7 @@
     describe "when match.snippet is used", ->
       beforeEach ->
         spyOn(provider, 'getSuggestions').andCallFake ({prefix}) ->
-          list = ['method(${1:something})', 'method2(${1:something})', 'method3(${1:something})']
+          list = ['method(${1:something})', 'method2(${1:something})', 'method3(${1:something})', 'namespace\\\\method4(${1:something})']
           ({snippet, replacementPrefix: prefix} for snippet in list)

       describe "when the snippets package is enabled", ->
@@ -760,7 +760,7 @@
             expect(wordElement.querySelector('.snippet-completion').textContent).toBe 'something'

             wordElements = editorView.querySelectorAll('.autocomplete-plus span.word')
-            expect(wordElements).toHaveLength 3
+            expect(wordElements).toHaveLength 4

         it "accepts the snippet when autocomplete-plus:confirm is triggered", ->
           triggerAutocompletion(editor, true, 'm')
@@ -770,6 +770,24 @@
             atom.commands.dispatch(suggestionListView, 'autocomplete-plus:confirm')
             expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
             expect(editor.getSelectedText()).toBe 'something'
+
+        it "unescapes \\ in list to match snippet behavior", ->
+          triggerAutocompletion(editor, true, 'm')
+
+          runs ->
+            # Value in list
+            wordElements = editorView.querySelectorAll('.autocomplete-plus span.word')
+            expect(wordElements).toHaveLength 4
+            expect(wordElements[3].textContent).toBe 'namespace\\method4(something)'
+
+            # Select last item
+            atom.commands.dispatch(editorView, 'core:move-up')
+
+            # Value in editor
+            suggestionListView = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list')
+            atom.commands.dispatch(suggestionListView, 'autocomplete-plus:confirm')
+            expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
+            expect(editor.getText()).toBe 'namespace\\method4(something)'

     describe "when the matched prefix is highlighted", ->
       it 'highlights the prefix of the word in the suggestion list', ->


Fix document update set operator field construction

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1583,7 +1583,7 @@

         oldEmail = @getAt 'email'

-        @update { $set: { email }, sanitizedEmail }, (err, res) =>
+        @update { $set: { email, sanitizedEmail } }, (err, res) =>
           return callback err  if err

           account.profile.hash = getHash email


JUser: Fix signature for unregister

JUser: Fix signature for unregister

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -92,7 +92,7 @@
         setSSHKeys              : (signature [Object], Function)
         getSSHKeys              : (signature Function)
         authenticateWithOauth   : (signature Object, Function)
-        unregister              : (signature Boolean, Function)
+        unregister              : (signature String, Function)
         finishRegistration      : (signature Object, Function)

     schema          :
@@ -296,7 +296,7 @@

   @login = secure (client, credentials, callback)->
     {username, password} = credentials
-    {sessionToken: clientId, connection} = client
+    {sessionToken: clientId, connection} = client

     constructor = this
     JSession.one {clientId}, (err, session)->
@@ -709,11 +709,11 @@
           queue.next()
       =>
         JPasswordRecovery = require '../passwordrecovery'
-
+
         passwordOptions =
           email         : user.email
           verb          : unless username? then 'Register2'
-
+
         JPasswordRecovery.create client, passwordOptions, (err)->
           queue.next()
       ->
@@ -821,7 +821,7 @@
             user.changeEmail account, options, callback
             if account.status is 'registered'
               # don't send an email when guests change their emails, which we
-              # need to allow for the pricing workflow.
+              # need to allow for the pricing workflow.
               sendChangeEmail user.email, "email"

   @emailAvailable = (email, callback)->


user: allow registration without username

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -626,8 +626,7 @@
   @removeFromGuestsGroup = (account, callback) ->
     JGroup.one { slug: 'guests' }, (err, guestsGroup) ->
       return callback err  if err?
-      unless guestsGroup?
-        return callback createKodingError "Guests group not found!"
+      return callback message: "Guests group not found!"  unless guestsGroup?
       guestsGroup.removeMember account, callback

   @convert = secure (client, userFormData, callback) ->
@@ -646,6 +645,7 @@

     newToken  = null
     invite    = null
+    user      = null

     queue = [
       =>
@@ -655,31 +655,41 @@
       =>
         # password is autogenerated before here
         # user should reset his/her password
-        @update { username: oldUsername }, { $set: { 'passwordStatus' : 'needs set' }}, (err, res)=>
+        operation = { $set: passwordStatus: 'needs set' }
+        @update { username: oldUsername }, operation, (err, res)=>
           return callback err  if err
           queue.next()
       =>
-        options = { account, oldUsername, email }
-        @changeEmailByUsername options, (err) =>
-          return callback err  if err?
+        if username? and email?
+          options = { account, oldUsername, email, username }
+          @changeEmailByUsername options, (err) =>
+            return callback err  if err?
+            queue.next()
+        else process.nextTick -> queue.next()
+      ->
+        account.fetchUser (err, user_) ->
+          return callback err  if err
+          user = user_
           queue.next()
       =>
         @persistOauthInfo oldUsername, client.sessionToken, (err)=>
           return callback err  if err
           queue.next()
       =>
-        options = { account, username, clientId, isRegistration: yes }
-        @changeUsernameByAccount options, (err, newToken_) =>
-          return callback err  if err?
-          newToken = newToken_
-          queue.next()
+        if username?
+          options = { account, username, clientId, isRegistration: yes }
+          @changeUsernameByAccount options, (err, newToken_) =>
+            return callback err  if err?
+            newToken = newToken_
+            queue.next()
+        else process.nextTick -> queue.next()
       =>
-        @verifyEnrollmentEligibility {email, inviteCode}, (err, isEligible, invite_) =>
+        @verifyEnrollmentEligibility {email: user.email, inviteCode}, (err, isEligible, invite_) =>
           return callback err  if err
           invite = invite_
           queue.next()
       =>
-        @addToGroups account, invite, email, (err) =>
+        @addToGroups account, invite, user.email, (err) =>
           return callback err  if err?
           queue.next()
       =>
@@ -696,7 +706,7 @@
           queue.next()
       =>
         JPasswordRecovery = require '../passwordrecovery'
-        JPasswordRecovery.create client, {email}, (err)->
+        JPasswordRecovery.create client, {email: user.email}, (err)->
           queue.next()
       ->
         JAccount.emit "AccountRegistered", account, referrer
@@ -767,7 +777,10 @@
           @fetchUser client, (err,user)->
             account = client.connection.delegate
             user.changeEmail account, options, callback
-            sendChangeEmail user.email, "email"
+            if account.status is 'registered'
+              # don't send an email when guests change their emails, which we
+              # need to allow for the pricing workflow.
+              sendChangeEmail user.email, "email"

   @emailAvailable = (email, callback)->
     @count {email}, (err, count)->


rename confirmDevAccount to confirmAccount

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1414,7 +1414,7 @@
     if KONFIG.autoConfirmAccounts or group isnt 'koding'
       user.confirmEmail (err) ->
         console.warn err  if err?
-        queue.next()
+        return callback err

     else
       _options =
@@ -1527,10 +1527,9 @@
         queue.next()

       ->
-        # Auto confirm accounts for development environment
-        # This config should be no for production! ~ GG
-        group = client.context.group
-        confirmAccountIfNeeded { user, email, username, group }, (err, pin_) ->
+        # Auto confirm accounts for development environment or Teams ~ GG
+        options = { group : client.context.group, user, email, username }
+        confirmAccountIfNeeded options, (err, pin_) ->
           return callback err  if err
           pin = pin_
           queue.next()


Project sessions loaded in ClassroomView

--- app/views/courses/ClassroomView.coffee
+++ app/views/courses/ClassroomView.coffee
@@ -49,7 +49,7 @@
     @sessions = new CocoCollection([], { model: LevelSession })
     for courseInstance in @courseInstances.models
       sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })
-      @supermodel.loadCollection(sessions, 'sessions')
+      @supermodel.loadCollection(sessions, 'sessions', { data: { project: ['level', 'playtime', 'creator', 'changed', 'state.complete'].join(' ') } })
       courseInstance.sessions = sessions
       sessions.courseInstance = courseInstance
       courseInstance.sessionsByUser = {}


Callback project controller delete with err obj

--- controllers/projects.coffee
+++ controllers/projects.coffee
@@ -75,7 +75,7 @@
     t.on 'begin', =>
       TimeEntriesController.deleteForProject key, ()->
         start()
-    t.on 'error', console.log
+    t.on 'error', callback
     t.on 'commit', ->
       callback()
     t.on 'rollback', ->


trying to add multiple character support

--- web/scripts/codeland.coffee
+++ web/scripts/codeland.coffee
@@ -151,6 +151,31 @@
     game : {
         startpos : [1, 1]
         targetpos : [3, 5]
+        characters: {
+            protagonist: {
+                dir: 1
+                index: 0
+            }
+            guard: {
+                index: 2
+                x: 3
+                y: 0
+                dir: 2
+                ai: {
+                    move: (gamestate) =>
+                        [newx, newy] = gameState.computeStepInDirection(
+                            @dir, @x, @y)
+                        hitEvent = gamestate.checkEvent(@x, @y)
+                        if !hitEvent
+                            @x = newx
+                            @y = newy
+                            gamestate.gameVisual.gridMove @index, 1
+                        else
+                            @dir = @dir ^ 2
+                        return
+                }
+            }
+        }
     }
     visual : {
         gameType: "grid",
@@ -166,6 +191,7 @@
         },
         characters: {
             protagonist: {
+                dir: 1,
                 imgSet: 0,
                 # x: 0,
                 # y: 0,
@@ -175,9 +201,17 @@
                 ySize: 26
             }
             gflag: {
+                dir: 0,
                 imgSet: 1,
                 x: 5,
                 y: 5,
+                xOff: 2,
+                yOff: 2,
+                xSize: 26,
+                ySize: 26
+            }
+            guard: {
+                imgSet: 1,
                 xOff: 2,
                 yOff: 2,
                 xSize: 26,


Added map icon to Game Selection screen

Added map icon to Game Selection screen

--- web/scripts/codeland.coffee
+++ web/scripts/codeland.coffee
@@ -35,12 +35,18 @@
     descriptions = root.getGameDescriptions()
     mapDiv = $(root.UIcont)
     mapDiv.empty()
+
     gameSequence = root.getGameSequence()
     sel = new gameSelector(mapDiv, false)
     addGameToMap = (game) ->
         # console.log "Game: #{game}"
         sel.buildDiv(game, descriptions[game], player.games[game], root.canPlay(game), codeland)
     addGameToMap game for game in gameSequence
+    tmp1 = document.getElementById("gameSelection")
+
+    $('Choose your Java Game
').prependTo tmp1
+    $('').prependTo tmp1
+
     $('#gameSelection').animate {
         scrollTop: root.gameSelectionScrollPosition
     }, 0


Environments: Added existence check before trying to draw connection between rule and domain.

--- client/Environments/views/scene/environmentsceneview.coffee
+++ client/Environments/views/scene/environmentsceneview.coffee
@@ -135,7 +135,8 @@
       domainDia = domainsByDia[restriction.domainName]
       for filterId in restriction.filters
         ruleDia = rulesById[filterId]
-        @connect {dia : domainDia, joint : 'left'}, {dia : ruleDia, joint : 'right' }, yes
+        if domainDia and ruleDia
+          @connect {dia : domainDia, joint : 'left'}, {dia : ruleDia, joint : 'right' }, yes

   createApproveModal:(items, action)->
     return unless KD.isLoggedIn()


Client: Activity: Updated channelParticipants getter to remove current user from participant list

--- client/activity/lib/flux/getters.coffee
+++ client/activity/lib/flux/getters.coffee
@@ -80,7 +80,10 @@
   (channelIds, users) ->
     channelIds.map (participantIds) ->
       participantIds.reduce (result, id) ->
-        if users.has id
+
+        return result  if id is whoami()._id
+
+        if users.has(id)
         then result.set id, users.get id
         else result
       , immutable.Map()


Test for invalid response

Test for invalid response

--- test/unit/execute-transaction-test.coffee
+++ test/unit/execute-transaction-test.coffee
@@ -34,14 +34,14 @@

   beforeEach () ->
     sinon.stub cliUtilsStub, 'exit'
-    #sinon.stub cliUtilsStub, 'log'
-    #sinon.stub cliUtilsStub, 'error'
+    sinon.stub cliUtilsStub, 'log'
+    sinon.stub cliUtilsStub, 'error'
     nock.disableNetConnect()

   afterEach () ->
     cliUtilsStub.exit.restore()
-    #cliUtilsStub.log.restore()
-    #cliUtilsStub.error.restore()
+    cliUtilsStub.log.restore()
+    cliUtilsStub.error.restore()
     nock.enableNetConnect()
     nock.cleanAll()

@@ -49,7 +49,7 @@
   data = {}
   server = {}

-  describe.only 'backend responds as it should', () ->
+  describe 'backend responds as it should', () ->
     beforeEach () ->
       server = nock('http://localhost:3000').
         post('/machines', {"type":"bulldozer","name":"willy"}).
@@ -67,7 +67,26 @@
       executeTransaction transaction, () ->
         assert.notOk cliUtilsStub.exit.called
         done()
+
+  describe 'backend responds with non valid response', () ->
+    beforeEach () ->
+      server = nock('http://localhost:3000').
+        post('/machines', {"type":"bulldozer","name":"willy"}).
+        reply transaction['response']['status'],
+          'Foo bar',
+          {'Content-Type': 'text/plain'}

+
+    it 'should perform the request', (done) ->
+      executeTransaction transaction, () ->
+        assert.ok server.isDone()
+        done()
+
+    it 'should exit with status 1', (done) ->
+      executeTransaction transaction, () ->
+        assert.ok cliUtilsStub.exit.calledWith(1)
+        done()
+
   describe 'when dry run', () ->
     before () ->
       transaction['configuration']['dryRun'] = true


Adding tests for data.loadProjectStats

Adding tests for data.loadProjectStats

--- test/unit/services/dataServiceSpec.coffee
+++ test/unit/services/dataServiceSpec.coffee
@@ -19,6 +19,7 @@
             {codename: 'view_task'}
             {codename: 'edit_task'}
         ])
+        httpBackend.whenGET('http://localhost:8000/api/v1/projects/1/stats').respond(200, { test: "test" })

     describe '$data', ->
         afterEach ->
@@ -36,14 +37,24 @@
         it 'should allow to load the userstories of the current project', inject ($data, $rootScope) ->
             scope = $rootScope.$new()
             $rootScope.projectId = 1
+
+            signalEmitted = false
+            $rootScope.$on "userstories:loaded", ->
+                signalEmitted = true
+
             httpBackend.expectGET('http://localhost:8000/api/v1/userstories?project=1')
             $data.loadUserStories(scope)
             httpBackend.flush()
             expect(_.map(scope.userstories, (us) -> us.getAttrs())).to.be.deep.equal([{order: 1}, {order: 3}])
+            expect(signalEmitted).to.be.true

         it 'should allow to load the unassigned userstories of the current project', inject ($data, $rootScope) ->
             scope = $rootScope.$new()
             $rootScope.projectId = 1
+            signalEmitted = false
+            $rootScope.$on "userstories:loaded", ->
+                signalEmitted = true
+
             httpBackend.expectGET('http://localhost:8000/api/v1/userstories?milestone=null&project=1')
             $data.loadUnassignedUserStories(scope)
             httpBackend.flush()
@@ -51,6 +62,21 @@
                 {project: 1, order: 1, milestone: null}
                 {project: 1, order: 3, milestone: null}
             ])
+            expect(signalEmitted).to.be.true
+
+        it 'should allow to load the current project stats', inject ($data, $rootScope) ->
+            scope = $rootScope.$new()
+            scope.projectId = 1
+            signalEmitted = false
+            $rootScope.$on "project_stats:loaded", ->
+                signalEmitted = true
+
+            httpBackend.expectGET('http://localhost:8000/api/v1/projects/1/stats')
+            $data.loadProjectStats(scope)
+            httpBackend.flush()
+
+            expect(scope.projectStats.getAttrs()).to.be.deep.equal({test: "test"})
+            expect(signalEmitted).to.be.true

         it 'should allow to load the list of permissions', inject ($data, $rootScope) ->
             httpBackend.expectGET('http://localhost:8000/api/v1/permissions')


reduce loop

reduce loop

--- assets/scripts/controllers/task.coffee
+++ assets/scripts/controllers/task.coffee
@@ -62,10 +62,10 @@
         flushWorkers(id,jobInfo.workers[id])

     if jobInfo.socket
-      attachList = (w for w,x of jobInfo.workers when x.visible)
-      jobInfo.socket.emit 'attach',from: attachList if attachList.length > 0
-      detachList = (w for w,x of jobInfo.workers when not x.visible)
-      jobInfo.socket.emit 'detach',from: detachList if detachList.length > 0
+      if w.visible
+        jobInfo.socket.emit 'attach',from: [from]
+      else
+        jobInfo.socket.emit 'detach',from: [from]

   url = urlPrase window.location.href,true


ENHANCE: Add extra code upload options

ENHANCE: Add extra code upload options

--- src/app/common/file-uploader/file-uploader.coffee
+++ src/app/common/file-uploader/file-uploader.coffee
@@ -66,8 +66,8 @@
         name:       'CSV'
       code:
         extensions: ['pas', 'cpp', 'c', 'cs', 'h', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css',
-                    'scss', 'yaml', 'yml', 'xml', 'json', 'ts',
-                    'vb', 'sql', 'txt']
+                    'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'pres', 'tex',
+                    'vb', 'sql', 'txt', 'md']
         icon:       'fa-file-code-o'
         name:       'code'
       image:


kontrolprocess: result is not used anymore

--- node_modules_koding/koding-worker/main.coffee
+++ node_modules_koding/koding-worker/main.coffee
@@ -49,7 +49,6 @@
       message   :
         command : options.command
         option  : options.option
-        result  : "result"
       uuid      : options.uuid
       hostname  : @getHostName()
       timestamp : @isoDateString()


artist names as backup title for show for SEO

--- models/partner_show.coffee
+++ models/partner_show.coffee
@@ -39,24 +39,16 @@

   toPageTitle: ->
     _.compact([
-      @get 'name' || ''
+      @title()
       @get('partner')?.name || ''
       "Artsy"
     ]).join(' | ').replace('–', '-')

   # past / current / upcomming show featuring works by {artists} on view at {gallery name} {location} {dates}
   toPageDescription: ->
-    artists = []
-    if @get('artists')
-      artists =
-        _.compact(for artist in @get('artists')
-          artist.name
-        )
-
-    if artists.length > 1
-      artistText = "featuring works by #{artists[0...(artists.length - 1)].join(', ')} and #{artists[artists.length - 1]}"
-    else if artists.length == 1
-      artistText = "featuring works by #{artists[0]}"
+    artistText = @formatArtistText()
+    if artistText
+      artistText = "featuring works by #{artistText}"

     info = _.compact([
       'at'
@@ -71,6 +63,19 @@
       artistText
       info
     ]).join(' ').replace('–', '-')
+
+  formatArtistText: ->
+    artists = []
+    if @get('artists')
+      artists =
+        _.compact(for artist in @get('artists')
+          artist.name
+        )
+
+    if artists.length > 1
+      artistText = "#{artists[0...(artists.length - 1)].join(', ')} and #{artists[artists.length - 1]}"
+    else if artists.length == 1
+      artistText = "#{artists[0]}"

   #
   # Get the poster image url of the show (e.g. used in the shows tab in
@@ -106,7 +111,10 @@
     return @imageUrl('medium_rectangle') if @hasImage('medium_rectangle')

   title: ->
-    @get 'name'
+    if @get('name')?.length
+      @get 'name'
+    else
+      @formatArtistText()

   shareTitle: ->
     if @has('fair_location')
@@ -215,7 +223,7 @@
     compactObject {
       "@context" : "http://schema.org"
       "@type" : "Event"
-      name : @get('name')
+      name : @title()
       image: @metaImage()?.imageUrl()
       url: "#{sd.APP_URL}#{@href()}"
       startDate: new Date(@get('start_at')).toISOString()


bix BO bug

bix BO bug

--- app/assets/javascripts/backbone/models/election.js.coffee
+++ app/assets/javascripts/backbone/models/election.js.coffee
@@ -118,14 +118,15 @@
         type: 'PUT'
         dataType: 'json'
         contentType: 'application/json'
-        data: JSON.stringify(_.reduce(
+        data: JSON.stringify({election: _.reduce(
           model.attributes
           (memo, val, key) ->
             memo[key] = val if _.include(modelData, key)
             memo
           {}
-        ))
+        )})
         url: @url()
+      debugger
       $.ajax(_.extend(params, options))
     else
       Backbone.sync(method, model, options)


Moved angularjs code that stores important values (current feed etc) in the root scope, from functions in the controller to separate service files.

--- app/assets/javascripts/modules/ng-FeedbunchCtrl.js.coffee
+++ app/assets/javascripts/modules/ng-FeedbunchCtrl.js.coffee
@@ -22,7 +22,7 @@
   #--------------------------------------------

   $scope.show_start_page = ->
-    unset_current_feed()
+    currentFeedSvc.unset()
     $scope.loading_entries = false

   #--------------------------------------------
@@ -38,20 +38,20 @@

   $scope.unsubscribe = ->
     # Delete feed model from the scope
-    index = $scope.feeds.indexOf $rootScope.current_feed
+    index = $scope.feeds.indexOf currentFeedSvc.get()
     $scope.feeds.splice index, 1 if index != -1

     # Before deleting from the global scope, save some data we'll need later
-    path = "/feeds/#{$rootScope.current_feed.id}.json"
-    unread_entries = $rootScope.current_feed.unread_entries
-    folder_id = $rootScope.current_feed.folder_id
+    path = "/feeds/#{currentFeedSvc.get().id}.json"
+    unread_entries = currentFeedSvc.get().unread_entries
+    folder_id = currentFeedSvc.get().folder_id

     # Update folders
     find_folder('all').unread_entries -= unread_entries
-    feed_removed_from_folder $rootScope.current_feed, folder_id
+    feed_removed_from_folder currentFeedSvc.get(), folder_id

     # Tell the model that no feed is currently selected.
-    unset_current_feed()
+    currentFeedSvc.unset()

     $http.delete(path)
     .error ->
@@ -70,7 +70,7 @@
     $("#subscribe-feed-popup").modal 'hide'

     if $scope.subscription_url
-      unset_current_feed()
+      currentFeedSvc.unset()
       $scope.loading_entries = true

       $http.post('/feeds.json', feed:{url: $scope.subscription_url})
@@ -101,11 +101,11 @@
   #--------------------------------------------

   $scope.remove_from_folder = ->
-    folder_id = $rootScope.current_feed.folder_id
-    $rootScope.current_feed.folder_id = 'none'
-    feed_removed_from_folder $rootScope.current_feed, folder_id
-
-    $http.put('/folders/none.json', folder: {feed_id: $rootScope.current_feed.id})
+    folder_id = currentFeedSvc.get().folder_id
+    currentFeedSvc.get().folder_id = 'none'
+    feed_removed_from_folder currentFeedSvc.get(), folder_id
+
+    $http.put('/folders/none.json', folder: {feed_id: currentFeedSvc.get().id})
     .error ->
       # Show alert
       $rootScope.error_managing_folders = true
@@ -119,12 +119,12 @@
   #--------------------------------------------

   $scope.move_to_folder = (folder)->
-    old_folder_id = $rootScope.current_feed.folder_id
-    $rootScope.current_feed.folder_id = folder.id
-    feed_removed_from_folder $rootScope.current_feed, old_folder_id
-    folder.unread_entries += $rootScope.current_feed.unread_entries
-
-    $http.put("/folders/#{folder.id}.json", folder: {feed_id: $rootScope.current_feed.id})
+    old_folder_id = currentFeedSvc.get().folder_id
+    currentFeedSvc.get().folder_id = folder.id
+    feed_removed_from_folder currentFeedSvc.get(), old_folder_id
+    folder.unread_entries += currentFeedSvc.get().unread_entries
+
+    $http.put("/folders/#{folder.id}.json", folder: {feed_id: currentFeedSvc.get().id})
     .error ->
       # Show alert
       $rootScope.error_managing_folders = true
@@ -141,12 +141,12 @@
     $("#new-folder-popup").modal 'hide'

     if $scope.new_folder_title
-      $http.post("/folders.json", folder: {feed_id: $rootScope.current_feed.id, title: $scope.new_folder_title})
+      $http.post("/folders.json", folder: {feed_id: currentFeedSvc.get().id, title: $scope.new_folder_title})
       .success (data)->
         $scope.folders.push data
-        old_folder_id = $rootScope.current_feed.folder_id
-        $rootScope.current_feed.folder_id = data.id
-        feed_removed_from_folder $rootScope.current_feed, old_folder_id
+        old_folder_id = currentFeedSvc.get().folder_id
+        currentFeedSvc.get().folder_id = data.id
+        feed_removed_from_folder currentFeedSvc.get(), old_folder_id
       .error (data, status)->
         if status == 304
           # Show alert
@@ -169,7 +169,7 @@
   #--------------------------------------------

   $scope.read_feed = (feed)->
-    set_current_feed feed
+    currentFeedSvc.set feed
     load_feed feed, false

   #--------------------------------------------
@@ -177,7 +177,7 @@
   #--------------------------------------------

   $scope.read_folder = (folder)->
-    set_current_folder folder
+    currentFolderSvc.set folder
     $scope.loading_entries = true

     $http.get("/folders/#{folder.id}.json")
@@ -204,22 +204,22 @@
   #--------------------------------------------

   $scope.read_all_entries = ->
-    unset_open_entry()
-    load_feed $rootScope.current_feed, true
+    openEntrySvc.unset()
+    load_feed currentFeedSvc.get(), true

   #--------------------------------------------
   # Refresh a feed and load its unread entries
   #--------------------------------------------

   $scope.refresh_feed = ->
-    unset_open_entry()
+    openEntrySvc.unset()
     $scope.loading_entries = true

-    $http.put("/feeds/#{$rootScope.current_feed.id}.json")
+    $http.put("/feeds/#{currentFeedSvc.get().id}.json")
     .success (data)->
       $scope.loading_entries = false
       $scope.entries = data["entries"]
-      $rootScope.current_feed.unread_entries = data["unread_entries"]
+      currentFeedSvc.get().unread_entries = data["unread_entries"]
     .error ->
       $scope.loading_entries = false
       if status == 404
@@ -241,11 +241,11 @@
   #--------------------------------------------

   $scope.read_entry = (entry)->
-    if $rootScope.open_entry == entry
+    if openEntrySvc.get() == entry
       # User is closing the open entry, do nothing
-      unset_open_entry()
+      openEntrySvc.unset()
     else
-      set_open_entry entry
+      openEntrySvc.set entry
       if !entry.read
         # User is opening an unread entry, mark it as read
         change_entries_state [entry], true
@@ -255,11 +255,11 @@
   #--------------------------------------------

   $scope.open_folder = (folder)->
-    if $rootScope.current_open_folder == folder
+    if openFolderSvc.get() == folder
       # User is closing the open folder
-      unset_open_folder()
+      openFolderSvc.unset()
     else
-      set_open_folder folder
+      openFolderSvc.set folder

   #--------------------------------------------
   # Mark all entries as read
@@ -273,8 +273,8 @@
   #--------------------------------------------

   $scope.unread_entry = ->
-    if $rootScope.open_entry.read
-      change_entries_state [$rootScope.open_entry], false
+    if openEntrySvc.get().read
+      change_entries_state [openEntrySvc.get()], false

   #--------------------------------------------
@@ -290,7 +290,7 @@
       $scope.entries = data["entries"]
       feed.unread_entries = data["unread_entries"]
     .error (data,status)->
-      unset_current_feed()
+      currentFeedSvc.unset()
       $scope.loading_entries = false
       if status == 404
         $rootScope.error_no_entries = true
@@ -346,12 +346,12 @@
   #--------------------------------------------

   update_unread_count = (entries, increment)->
-    if $rootScope.current_feed
+    if currentFeedSvc.get()
       # if current_feed has value, all entries belong to the same feed which simplifies things
       if increment
-        $rootScope.current_feed.unread_entries += entries.length
-      else
-        $rootScope.current_feed.unread_entries -= entries.length
+        currentFeedSvc.get().unread_entries += entries.length
+      else
+        currentFeedSvc.get().unread_entries -= entries.length
     else
       # if current_feed has null value, each entry can belong to a different feed
       # we process each entry individually
@@ -405,68 +405,6 @@
         folder.unread_entries -= feed.unread_entries

   #--------------------------------------------
-  # Store the currently selected feed in the global scope
-  #--------------------------------------------
-
-  set_current_feed = (feed)->
-    unset_current_folder()
-    unset_open_entry()
-    $rootScope.current_feed = feed
-
-  #--------------------------------------------
-  # Unset the currently selected feed in the global scope
-  #--------------------------------------------
-
-  unset_current_feed = ->
-    unset_open_entry()
-    $rootScope.current_feed = null
-
-  #--------------------------------------------
-  # Store the currently selected folder in the global scope
-  #--------------------------------------------
-
-  set_current_folder = (folder)->
-    unset_current_feed()
-    unset_open_entry()
-    $rootScope.current_folder = folder
-
-  #--------------------------------------------
-  # Unset the currently selected folder in the global scope
-  #--------------------------------------------
-
-  unset_current_folder = ->
-    unset_open_entry()
-    $rootScope.current_folder = null
-
-  #--------------------------------------------
-  # Store the currently open entry in the global scope
-  #--------------------------------------------
-
-  set_open_entry = (entry)->
-    $rootScope.open_entry = entry
-
-  #--------------------------------------------
-  # Unset the currently open entry in the global scope
-  #--------------------------------------------
-
-  unset_open_entry = ->
-    $rootScope.open_entry = null
-
-  #--------------------------------------------
-  # Store the currently open folder in the global scope
-  #--------------------------------------------
-
-  set_open_folder = (folder)->
-    $rootScope.current_open_folder = folder
-
-  #--------------------------------------------
-  # Unset the currently open folder in the global scope
-  #--------------------------------------------
-
-  unset_open_folder = ->
-    $rootScope.current_open_folder = null
-
-  #--------------------------------------------
   # Function to filter feeds in a given folder
   #--------------------------------------------


Fix js indentation

Fix js indentation

--- app/assets/javascripts/tutor_report/chart/chart.js.coffee
+++ app/assets/javascripts/tutor_report/chart/chart.js.coffee
@@ -366,10 +366,9 @@

     node = svg.selectAll('.node')
               .data(bubble.nodes(root)
-                          .filter((d) ->
-                            !d.children
-                          )
-              )
+                .filter((d) ->
+                  !d.children
+                ))
               .enter()
               .append('g')
               .attr('class', 'node')


Fix comment spacing

Fix comment spacing

--- lib/actions/os.coffee
+++ lib/actions/os.coffee
@@ -142,7 +142,7 @@
 			if os.platform() is 'win32' and error? and (error.code is 'EPERM' or error.code is 'EACCES')
 				windosu = require('windosu')

-				# Need to escape everypath to avoid errors
+				# Need to escape every path to avoid errors
 				resinWritePath = "\"#{path.join(__dirname, '..', '..', 'bin', 'resin-write')}\""
 				windosu.exec("node #{resinWritePath} \"#{params.image}\" \"#{params.device}\"")
 			else


Fix if user has no services.

Fix if user has no services.

--- packages/retronator-accounts/user/user.coffee
+++ packages/retronator-accounts/user/user.coffee
@@ -54,7 +54,7 @@
         enabledServices = _.intersection _.keys(user.services), availableServices

         # Add password only if it has really been set (since the password key can also have just a reset token object).
-        enabledServices.push 'password' if user.services.password?.bcrypt
+        enabledServices.push 'password' if user.services?.password?.bcrypt

         [user._id, enabledServices]
       ]


taking out logging call

taking out logging call

--- subtree/bokehjs/static/coffee/tools.coffee
+++ subtree/bokehjs/static/coffee/tools.coffee
@@ -347,7 +347,6 @@
     for own k,v of datasource_selections
       selected = _.intersection.apply(_, v)
       datasources[k].set('selected', selected)
-      console.log('selected', selected)
       datasources[k].save()
     return null


display reading review before last non-core step

--- tutor/src/components/task/crumb-mixin.cjsx
+++ tutor/src/components/task/crumb-mixin.cjsx
@@ -99,7 +99,7 @@

     # insert spacer panel/crumb for reading task that have spaced practices or personalized problems
     if task.type is 'reading'
-      notCore = _.find crumbs, (crumb) ->
+      notCore = _.last _.filter crumbs, (crumb) ->
         (crumb.type is 'step') and not TaskStepStore.isCore(crumb.data.id)

       if notCore?


do not need a new array there

do not need a new array there

--- client/landing/app/Applications/Environments.kdapplication/views/FirewallMapperView.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/FirewallMapperView.coffee
@@ -186,8 +186,10 @@
     newAction = @actionButton.getOptions().title.toLowerCase()
     futureAction = if newAction is 'deny' then 'allow' else 'deny'

+    console.log data
+
     KD.remote.api.JDomain.one {domainName:data.domainName}, (err, domain)=>
-      domain.updateRuleBehavior
+      domain.updateProxyRule
         ruleName   : data.ruleName
         behaviorInfo :
           enabled : "yes"


Remove debug log.

Remove debug log.

--- src/coffee/utils/product-utils.coffee
+++ src/coffee/utils/product-utils.coffee
@@ -225,9 +225,7 @@
     actions

   actionForSku = (variantDiff, variant) ->
-    console.log "variantDiff %j", variantDiff
     if _.has variantDiff, 'sku'
-      console.log "HAJO %j", variantDiff
       action =
         action: 'setSKU'
         variantId: variant.id


Klient: keep active terminal sessions in klient storage [completes #91551672]

--- client/app/lib/kite/kites/klient.coffee
+++ client/app/lib/kite/kites/klient.coffee
@@ -166,36 +166,32 @@
     @tell 'storage.delete', {key}

-  getLocalStorage = ->
-
-    return kd.singletons.localStorageController.storage 'Klient', '1.0'
-
-  setActiveSessions = (sessions)->
-
-    getLocalStorage().setValue 'activeSessions', sessions
+  setActiveSessions: (sessions)->
+    @storageSet 'activeSessions', sessions

   getActiveSessions: ->

-    return (getLocalStorage().getValue 'activeSessions') ? []
+    @storageGet 'activeSessions'
+    .then (sessions)-> sessions or []
+    .catch          -> []

   syncSessionsWithLocalStorage: ->

-    setActiveSessions @getActiveSessions().filter (session)=>
-      session in @terminalSessions
+    @getActiveSessions().then (sessions) =>
+      @setActiveSessions sessions.filter (session) =>
+        session in @terminalSessions

   addToActiveSessions: (session)->

-    activeSessions = @getActiveSessions()
-    activeSessions.push session  unless session in activeSessions
-
-    setActiveSessions activeSessions
+    @getActiveSessions().then (activeSessions) =>
+      activeSessions.push session  unless session in activeSessions
+      @setActiveSessions activeSessions

   removeFromActiveSessions: (session)->

-    activeSessions = @getActiveSessions().filter (old)-> session isnt old
-
-    setActiveSessions activeSessions
+    @getActiveSessions().then (sessions) =>
+      @setActiveSessions sessions.filter (old)-> session isnt old


Clean up

Clean up

--- client/lib/helpers/boxAPIHelpers.coffee
+++ client/lib/helpers/boxAPIHelpers.coffee
@@ -1,26 +1,8 @@
-###
-Template.boxAPIresponse.helpers({
-  boxResponse: function () {
-
-    Meteor.setTimeout(function() {
-      Session.set('boxResponse', undefined);
-    }, 7000);
-
-    return Session.get('boxResponse');
-  }
-});
-###
-
 Template.templates.events(
   'click #copyTemplate': (evt, tpl) ->
     sourceFolder = Session.get("folderToCopy")
     targetFolderName = $('input[name="newFolderName"]').val()
-    ###
-    tokenExpiration = Meteor.user().services.box.expiresAt
-    if Date.now() - tokenExpiration > 0
-      console.log("Token Expired")
-      Meteor.call("refreshToken")
-    ###
+
     console.log("Preparing to copy Box folder (" + sourceFolder + ") as " + targetFolderName + "...")
     Session.set("templateStatus","copy")
     Meteor.call('copyTemplate', sourceFolder, targetFolderName, (error, result) ->


Remove expected block comment from test

Remove expected block comment from test

--- test/src/system/installation_process_test.coffee
+++ test/src/system/installation_process_test.coffee
@@ -11,7 +11,7 @@

 editorTest "sets value attribute", (done) ->
   defer ->
-    equal getEditorElement().getAttribute("value"), "
Hello world
"
+    equal getEditorElement().getAttribute("value"), "
Hello world
"
     done()

 editorModule "Installation process", template: "editor_json"


Generate taskset_id when id is TEST

Generate taskset_id when id is TEST

--- loadHIT.coffee
+++ loadHIT.coffee
@@ -117,6 +117,11 @@
     contribution: null
   )

+  # If taskset Id is "TEST", generate a unique one.
+  if obj.opts.taskset_id is "TEST"
+    obj.opts.taskset_id ==  "TEST" + Math.floor(Math.random()*Math.pow(10,10))
+    console.log "Generating unique taskset_id: " + obj.opts.taskset_id
+
   obj.taskset =
     _id: obj.opts.taskset_id
     lock: if obj.opts.lock then true else false


Correct toolbar_location default to be the same default as python

--- bokehjs/src/coffee/api/plotting.coffee
+++ bokehjs/src/coffee/api/plotting.coffee
@@ -391,7 +391,7 @@
 color = (r, g, b) -> sprintf("#%02x%02x%02x", r, g, b)

 gridplot = (children, options={}) ->
-  toolbar_location = if _.isUndefined(options.toolbar_location) then 'left' else options.toolbar_location
+  toolbar_location = if _.isUndefined(options.toolbar_location) then 'right' else options.toolbar_location
   sizing_mode = if _.isUndefined(options.sizing_mode) then 'fixed' else options.sizing_mode

   tools = []


Simplified debug logging function, that doesn't require a callback function.

--- lib/connection.coffee
+++ lib/connection.coffee
@@ -40,9 +40,7 @@
       @logPacket('Received', packet);

       preloginPayload = new PreloginPayload(packet.data())
-      @debug((log) ->
-        log(preloginPayload.toString('  '))
-      )
+      @debug(preloginPayload.toString('  '))

       @packetBuffer = new Buffer(0)

@@ -73,9 +71,7 @@
     packet.last(true)

     @sendPacket(packet)
-    @debug((log) ->
-      log(preloginPayload.toString('  '))
-    )
+    @debug(preloginPayload.toString('  '))
     #@state = STATE.SENT_PRELOGIN

   sendPacket: (packet) =>
@@ -83,20 +79,15 @@
     @connection.write(packet.buffer)

   logPacket: (text, packet) ->
-    @debug((log) ->
-      log(text + ' packet')
-
-      log(packet.headerToString('  '))
-      log('')
-      log(packet.dataToString('  '))
-      log('')
-    )
+    @debug(text + ' packet')
+    @debug(packet.headerToString('  '))
+    @debug('')
+    @debug(packet.dataToString('  '))
+    @debug('')

-  debug: (debugFunction) =>
+  debug: (text) ->
     if @listeners('debug').length > 0
-      debugFunction((text) =>
-        if !@closed
-          @emit('debug', text)
-      )
+      if !@closed
+        @emit('debug', text)

 module.exports = Connection


Fix problems with broadcast methods

Fix problems with broadcast methods

--- lib/controller.coffee
+++ lib/controller.coffee
@@ -570,15 +570,17 @@

     event = {changeType: changeType, event: event, colour: @markerColour, eventType: 'broadcast-event'}
     @events.push(event)
-    @eval() if not exclusively
-
-  broadcastCmdPeriod: (exclusively) -> #broadcast command+. exclusively or not
+    if not exclusively
+      @eval()
+
+  broadcastCmdPeriod: (exclusively) -> #broadcast command+period exclusively or not
     changeType = 'cmd-period'
     event  = {}

     event = {changeType: changeType, event: event, colour: @markerColour, eventType: 'broadcast-event'}
     @events.push(event)
-    @activeRepl?.cmdPeriod() if not exclusively
+    if not exclusively
+      @activeRepl?.cmdPeriod()

   pairEval: (data) -> #pair evaluation
     if data.event.newRange then newRange = Range.fromObject(data.event.newRange)
@@ -595,7 +597,7 @@
         atom.workspace.addModalPanel(item: noticeView, visible: true)
         @evalWithRepl(newExpression, @currentPath(), newRange)
       when 'cmd-period'
-        noticeView = new AlertView "Your "+buddyColor+" buddy evaluated: Command + Period"
+        noticeView = new AlertView "Your "+buddyColour+" buddy evaluated: Command + Period"
         atom.workspace.addModalPanel(item: noticeView, visible: true)
         @activeRepl?.cmdPeriod()
       else


Add back code to show docstore tab after it is disabled once

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -168,6 +168,7 @@
   filterTabComponent: (capability, defaultComponent) ->
     # allow the save elsewhere button to hide the document provider tab in save
     if capability is 'save' and @disableForNextSave
+      @disableForNextSave = false
       null
     else
       defaultComponent


Replace with shorthand call to this.working.find

--- src/coffee/cilantro/models/context/manager.coffee
+++ src/coffee/cilantro/models/context/manager.coffee
@@ -128,7 +128,7 @@
         # triggers a sync with a server.
         apply: (ident, options) ->
             ident = ident.identity?() or ident
-            attrs = @working.find(ident).toJSON()
+            attrs = @find(ident).toJSON()
             node = @upstream.define(ident)
             node.set(attrs, options)
             @save(node)
@@ -136,7 +136,7 @@
         # Reverts a working tree node to it's upstream state if one exists.
         revert: (ident, options) ->
             ident = ident.identity?() or ident
-            if not (wn = @working.find(ident))
+            if not (wn = @find(ident))
                 return
             if (un = @upstream.find(ident))
                 wn.set(un.toJSON())
@@ -174,7 +174,7 @@
             ident = ident.identity?() or ident
             if not (u = @upstream.find(ident))
                 return false
-             return not _.isEqual(u.toJSON(), @working.find(ident).toJSON())
+             return not _.isEqual(u.toJSON(), @find(ident).toJSON())

         # Check if the node for a given ident is active and enabled. If
         # `enabled` is not defined, it is assumed to be true hence the


Change 'context:required' event argument to be an array of objects

This fixes the discrepancy with 'context:invalid'.

Fix #421

--- src/coffee/cilantro/models/context/manager.coffee
+++ src/coffee/cilantro/models/context/manager.coffee
@@ -59,7 +59,7 @@

         # Triggers the context invalid event given a list of invalid nodes
         _triggerRequired: (required) ->
-            c.trigger(c.CONTEXT_REQUIRED, required)
+            c.trigger(c.CONTEXT_REQUIRED, (concept: id, reason: 'required' for id in required))

         # Performs a check in the working tree for required nodes.
         _checkRequired: ->


fix doc for cronevents library

fix doc for cronevents library

--- lib/cronevents.coffee
+++ lib/cronevents.coffee
@@ -1,13 +1,5 @@
 # Description:
-#   requests Phabricator Conduit api
-#
-# Dependencies:
-#
-# Configuration:
-#  PHABRICATOR_URL
-#  PHABRICATOR_API_KEY
-#  PHABRICATOR_BOT_PHID
-#  PHABRICATOR_TRUSTED_USERS
+#   cron events library
 #
 # Author:
 #   mose


Builder: on error give fullpath rather than just the filename

--- Builder.coffee
+++ Builder.coffee
@@ -222,7 +222,7 @@
           if file.sourcePath in @blackList
             @blackList.splice (@blackList.indexOf file.sourcePath), 1
           else if not checkFileCase file.sourcePath
-            log.error "File name case is wrong: #{ includePath }"
+            log.error "File name case is wrong: #{ file.sourcePath }"
             process.exit 1

           switch file.extension


treesame commit of origin/stable/2021-10-27

Treesame-Commit-Id: a6e5d46bce187762d2028f784be15d6524753484

--- ui/shared/handlebars-helpers/index.coffee
+++ ui/shared/handlebars-helpers/index.coffee
@@ -198,10 +198,11 @@

   # convert an event date and time to a string using the given date and time format specifiers
   tEventToString : (date = '', i18n_date_format = 'short', i18n_time_format = 'tiny') ->
-    I18nObj.t 'time.event',
-      defaultValue: '%{date} at %{time}',
-      date: I18nObj.l "date.formats.#{i18n_date_format}", date
-      time: I18nObj.l "time.formats.#{i18n_time_format}", date
+    if (date)
+      I18nObj.t 'time.event',
+        defaultValue: '%{date} at %{time}',
+        date: I18nObj.l "date.formats.#{i18n_date_format}", date
+        time: I18nObj.l "time.formats.#{i18n_time_format}", date

   # formats a date as a string, using the given i18n format string
   strftime : (date = '', fmtstr) ->


Make agda-mode:info static, don't spawn a new Agda process

--- lib/executable.coffee
+++ lib/executable.coffee
@@ -12,6 +12,7 @@
     # instance wired the agda-mode executable
     agdaProcessWired: false
     agdaProcess: null
+    agdaVersion: null

     constructor: (@core) ->

@@ -34,7 +35,9 @@
             agdaProcess.on 'error', (error) =>
                 reject new InvalidExecutablePathError error, path
             agdaProcess.stdout.once 'data', (data) =>
-                if /^Agda/.test data.toString()
+                result = data.toString().match /^Agda version (.*)\n$/
+                if result
+                    @agdaVersion = result[1]
                     atom.config.set 'agda-mode.executablePath', path
                     resolve path
                 else
@@ -165,23 +168,15 @@
         @agdaProcessWired = false

     info: =>
-        @getExecutablePath().then (path) =>
-            args = @getProgramArgs().join(' ')
-            child = exec "#{path} #{args} -V", (error, stdout, stderr) =>
-                if error
-                    @core.panel.setContent "Error", "#{error}", 'error'
-                else
-                    result = stdout.toString().match /^Agda version (.*)\n$/
-                    args = @getProgramArgs()
-                    args.unshift '--interaction'
-                    if result
-                        @core.panel.setContent "Info", [
-                            "Agda version: #{result[1]}"
-                            "Agda executable path: #{path}"
-                            "Agda executable arguments: #{args.join(' ')}"
-                        ]
-                    else
-                        @core.panel.setContent "Error", ["unable to parse agda version message #{stdout.toString()}"], 'error'
+        path = atom.config.get('agda-mode.executablePath')
+        args = @getProgramArgs()
+        args.unshift '--interaction'
+
+        @core.panel.setContent "Info", [
+            "Agda version: #{@agdaVersion}"
+            "Agda executable path: #{path}"
+            "Agda executable arguments: #{args.join(' ')}"
+        ]

     compile: =>
         backend = atom.config.get 'agda-mode.backend'


fix typo

fix typo

--- lib/file-icons.coffee
+++ lib/file-icons.coffee
@@ -28,4 +28,4 @@
     @clearElement(el)
     el.className = el.className + " " + className;
   clearElement: (el) ->
-    el.className = el.className.replace(/\sfiletype-icon-[\w]+/, '');
+    el.className = el.className.replace(/\sfile-icon-[\w]+/, '');


Lint code

Lint code

--- picksel.coffee
+++ picksel.coffee
@@ -104,7 +104,7 @@
         + id \
         + '\''
     else
-      url = body.hits[0][resolutions[resolution]];
+      url = body.hits[0][resolutions[resolution]]
       log.info 'Downloading file from ' \
         + url
       file = fs.createWriteStream destination


delegate unclear use of Array#slice(0)

delegate unclear use of Array#slice(0)

--- app/js/jasmine-given.coffee
+++ app/js/jasmine-given.coffee
@@ -181,7 +181,7 @@

   class Waterfall
     constructor: (functions = [], finalCallback) ->
-      @functions = functions.slice(0)
+      @functions = cloneArray(functions)
       @finalCallback = finalCallback

       @asyncCount = 0
@@ -208,6 +208,8 @@
         func()
         @flow()

+  cloneArray = (a) -> a.slice(0)
+
   beforeEach ->
     if jasmine.addMatchers?
       jasmine.addMatchers(jasmine.matcherWrapper.wrap(jasmine._given.matchers))


Fix full_height_header.coffee

Fix full_height_header.coffee

--- app/assets/javascripts/martian_components/components/utils/full_height_header.coffee
+++ app/assets/javascripts/martian_components/components/utils/full_height_header.coffee
@@ -21,5 +21,5 @@

   compute: =>
     return false if @lastWidth == window.innerWidth
-    @inner.height(window.innerHeight - @offset)
+    @inner.css('height', window.innerHeight - @offset)
     @lastWidth = window.innerWidth


add another callback for socket closure

add another callback for socket closure

--- vendor/assets/javascripts/myxi/connection.coffee
+++ vendor/assets/javascripts/myxi/connection.coffee
@@ -43,6 +43,7 @@
     @websocket.onclose = (event)=>
       if @connected
         @_runCallbacks('SocketDisconnected')
+      @_runCallbacks('SocketClosed')
       @connected = false
       @authenticated = false
       @_markAllSubscriptionsAsUnsubscribed()


Sort out rows and columns in browse view


Add JS helper to toggle boolean icons

Add JS helper to toggle boolean icons

--- app/assets/javascripts/easy_app_bootstrap.js.coffee
+++ app/assets/javascripts/easy_app_bootstrap.js.coffee
@@ -31,6 +31,20 @@

 root.highlight = (element) ->
   $(element).effect('highlight', { color: '#B0D6D6' }, 1500)
+
+
+root.toggleBooleanIcon = (element) ->
+  icon = $(element)
+  if icon.hasClass('fa-dot-circle-o')
+    icon.removeClass('fa-dot-circle-o')
+    icon.removeClass('fa-important')
+    icon.addClass('fa-check')
+    icon.addClass('fa-success')
+  else
+    icon.removeClass('fa-check')
+    icon.removeClass('fa-success')
+    icon.addClass('fa-dot-circle-o')
+    icon.addClass('fa-important')

 root.toggleCheckboxes = (element, selector) ->


Whitespace

Whitespace

--- app/assets/javascripts/player/player_view.js.coffee
+++ app/assets/javascripts/player/player_view.js.coffee
@@ -29,6 +29,7 @@
   appendChildViews: ->
     @$el.append(@rendererView.$el)
     @rendererView.afterInsertedToDom()
+
     @$el.append(@hudView.$el)

   onStartPromptClick: ->


small review feedback refactor

small review feedback refactor

--- app/assets/javascripts/ui_components/toolbar.coffee
+++ app/assets/javascripts/ui_components/toolbar.coffee
@@ -1,10 +1,5 @@
 $ ->
-  # all checked checkboxes display as active
-  $(".toolbar .checkbox input[type='checkbox']:checked").parents('.checkbox').addClass('active')
+  $('.toolbar .checkbox input').change ->
+    $(this).parents('.checkbox').toggleClass('active', this.checked)

-  # checking/unchecking checkbox displays as active/inactive
-  $(".toolbar .checkbox input[type='checkbox']").change ->
-    if @checked
-      $(this).parents('.checkbox').addClass('active')
-    else
-      $(this).parents('.checkbox').removeClass('active')
+  $('.toolbar .checkbox input').trigger('change')