Przeglądaj źródła

Merge branch 'master' into pr-expandable-text-area-fixes

Paulo Reis 9 lat temu
rodzic
commit
6ef8d2d46d
83 zmienionych plików z 782 dodań i 273 usunięć
  1. 3 3
      services/web/Gruntfile.coffee
  2. 31 26
      services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
  3. 1 1
      services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
  4. 2 2
      services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
  5. 4 4
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  6. 3 3
      services/web/app/coffee/Features/StaticPages/HomeController.coffee
  7. 11 10
      services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
  8. 1 1
      services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
  9. 2 2
      services/web/app/coffee/infrastructure/Modules.coffee
  10. 1 1
      services/web/app/coffee/infrastructure/Server.coffee
  11. 1 1
      services/web/app/coffee/models/User.coffee
  12. 2 2
      services/web/app/views/admin/index.pug
  13. 0 0
      services/web/app/views/admin/register.pug
  14. 0 0
      services/web/app/views/beta_program/opt_in.pug
  15. 0 0
      services/web/app/views/blog/blog_holder.pug
  16. 5 5
      services/web/app/views/contact-us-modal.pug
  17. 0 0
      services/web/app/views/general/404.pug
  18. 0 0
      services/web/app/views/general/500.pug
  19. 0 0
      services/web/app/views/general/closed.pug
  20. 3 1
      services/web/app/views/layout.pug
  21. 3 3
      services/web/app/views/layout/footer.pug
  22. 1 1
      services/web/app/views/layout/navbar.pug
  23. 0 0
      services/web/app/views/project/editor.pug
  24. 0 0
      services/web/app/views/project/editor/binary-file.pug
  25. 1 1
      services/web/app/views/project/editor/chat.pug
  26. 3 3
      services/web/app/views/project/editor/editor.pug
  27. 0 0
      services/web/app/views/project/editor/feature-onboarding.pug
  28. 6 6
      services/web/app/views/project/editor/file-tree.pug
  29. 3 3
      services/web/app/views/project/editor/header.pug
  30. 0 0
      services/web/app/views/project/editor/history.pug
  31. 0 0
      services/web/app/views/project/editor/hotkeys.pug
  32. 3 3
      services/web/app/views/project/editor/left-menu.pug
  33. 7 7
      services/web/app/views/project/editor/pdf.pug
  34. 0 0
      services/web/app/views/project/editor/publish-template.pug
  35. 3 2
      services/web/app/views/project/editor/review-panel.pug
  36. 3 3
      services/web/app/views/project/editor/share.pug
  37. 0 0
      services/web/app/views/project/invite/not-valid.pug
  38. 3 3
      services/web/app/views/project/invite/show.pug
  39. 0 0
      services/web/app/views/project/list.pug
  40. 0 0
      services/web/app/views/project/list/empty-project-list.pug
  41. 0 0
      services/web/app/views/project/list/modals.pug
  42. 0 0
      services/web/app/views/project/list/notifications.pug
  43. 4 4
      services/web/app/views/project/list/project-list.pug
  44. 0 0
      services/web/app/views/project/list/side-bar.pug
  45. 6 6
      services/web/app/views/referal/bonus.pug
  46. 2 2
      services/web/app/views/restore.pug
  47. 1 1
      services/web/app/views/scribtex-modal.pug
  48. 2 2
      services/web/app/views/sentry.pug
  49. 0 0
      services/web/app/views/subscriptions/custom_account.pug
  50. 10 10
      services/web/app/views/subscriptions/dashboard.pug
  51. 0 0
      services/web/app/views/subscriptions/edit-billing-details.pug
  52. 0 0
      services/web/app/views/subscriptions/group/invite.pug
  53. 0 0
      services/web/app/views/subscriptions/group/successful_join.pug
  54. 0 0
      services/web/app/views/subscriptions/group_admin.pug
  55. 1 1
      services/web/app/views/subscriptions/new.pug
  56. 0 0
      services/web/app/views/subscriptions/plans.pug
  57. 0 0
      services/web/app/views/subscriptions/successful_subscription.pug
  58. 1 1
      services/web/app/views/subscriptions/upgradeToAnnual.pug
  59. 0 0
      services/web/app/views/tests.pug
  60. 0 0
      services/web/app/views/translations/translation_message.pug
  61. 0 0
      services/web/app/views/university/case_study.pug
  62. 0 0
      services/web/app/views/university/university_holder.pug
  63. 1 1
      services/web/app/views/user/activate.pug
  64. 1 1
      services/web/app/views/user/login.pug
  65. 0 0
      services/web/app/views/user/passwordReset.pug
  66. 0 0
      services/web/app/views/user/register.pug
  67. 0 0
      services/web/app/views/user/restricted.pug
  68. 0 0
      services/web/app/views/user/sessions.pug
  69. 0 0
      services/web/app/views/user/setPassword.pug
  70. 1 1
      services/web/app/views/user/settings.pug
  71. 0 0
      services/web/app/views/view_templates/bonus_templates.pug
  72. 8 0
      services/web/modules/.gitignore
  73. 471 57
      services/web/npm-shrinkwrap.json
  74. 2 1
      services/web/package.json
  75. 3 0
      services/web/public/coffee/ide/editor/Document.coffee
  76. 2 1
      services/web/public/coffee/ide/editor/EditorManager.coffee
  77. 2 2
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  78. 35 20
      services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
  79. 9 3
      services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
  80. 5 0
      services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee
  81. 5 0
      services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee
  82. 75 56
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
  83. 29 6
      services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee

+ 3 - 3
services/web/Gruntfile.coffee

@@ -230,11 +230,11 @@ module.exports = (grunt) ->
 
 
 		sed:
 		sed:
 			version:
 			version:
-				path: "app/views/sentry.jade"
+				path: "app/views/sentry.pug"
 				pattern: '@@COMMIT@@',
 				pattern: '@@COMMIT@@',
 				replacement: '<%= commit %>',
 				replacement: '<%= commit %>',
 			release:
 			release:
-				path: "app/views/sentry.jade"
+				path: "app/views/sentry.pug"
 				pattern: "@@RELEASE@@"
 				pattern: "@@RELEASE@@"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 
 
@@ -397,5 +397,5 @@ module.exports = (grunt) ->
 
 
 	grunt.registerTask 'default', 'run'
 	grunt.registerTask 'default', 'run'
 
 
-	grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
+	grunt.registerTask 'version', "Write the version number into sentry.pug", ['git-rev-parse', 'sed']
 
 

+ 31 - 26
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -23,7 +23,7 @@ module.exports = CollaboratorsInviteController =
 				return next(err)
 				return next(err)
 			res.json({invites: invites})
 			res.json({invites: invites})
 
 
-	_checkShouldInviteEmail: (sendingUser, email, callback=(err, shouldAllowInvite)->) ->
+	_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
 		if Settings.restrictInvitesToExistingAccounts == true
 		if Settings.restrictInvitesToExistingAccounts == true
 			logger.log {email}, "checking if user exists with this email"
 			logger.log {email}, "checking if user exists with this email"
 			UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
 			UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
@@ -31,19 +31,20 @@ module.exports = CollaboratorsInviteController =
 				userExists = user? and user?._id?
 				userExists = user? and user?._id?
 				callback(null, userExists)
 				callback(null, userExists)
 		else
 		else
-			UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)->
-				if err?
-					return callback(err)
-				collabLimit = user?.features?.collaborators || 1
-				if collabLimit == -1 
-					collabLimit = 20
-				collabLimit = collabLimit * 10
-				opts = 
-					endpointName: "invite_to_project"
-					timeInterval: 60 * 30
-					subjectName: sendingUser._id
-					throttle: collabLimit
-				rateLimiter.addCount opts, callback
+			callback(null, true)
+	
+	_checkRateLimit: (user_id, callback = (error) ->) ->
+		LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
+			return callback(err) if err?
+			if collabLimit == -1 
+				collabLimit = 20
+			collabLimit = collabLimit * 10
+			opts = 
+				endpointName: "invite-to-project-by-user-id"
+				timeInterval: 60 * 30
+				subjectName: user_id
+				throttle: collabLimit
+			rateLimiter.addCount opts, callback
 
 
 	inviteToProject: (req, res, next) ->
 	inviteToProject: (req, res, next) ->
 		projectId = req.params.Project_id
 		projectId = req.params.Project_id
@@ -64,20 +65,24 @@ module.exports = CollaboratorsInviteController =
 			if !email? or email == ""
 			if !email? or email == ""
 				logger.log {projectId, email, sendingUserId}, "invalid email address"
 				logger.log {projectId, email, sendingUserId}, "invalid email address"
 				return res.sendStatus(400)
 				return res.sendStatus(400)
-			CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)->
-				if err?
-					logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
-					return next(err)
-				if !shouldAllowInvite
-					logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
-					return res.json {invite: null, error: 'cannot_invite_non_user'}
-				CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+			CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
+				return next(error) if error?
+				if !underRateLimit
+					return res.sendStatus(429)
+				CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
 					if err?
 					if err?
-						logger.err {projectId, email, sendingUserId}, "error creating project invite"
+						logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
 						return next(err)
 						return next(err)
-					logger.log {projectId, email, sendingUserId}, "invite created"
-					EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
-					return res.json {invite: invite}
+					if !shouldAllowInvite
+						logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
+						return res.json {invite: null, error: 'cannot_invite_non_user'}
+					CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+						if err?
+							logger.err {projectId, email, sendingUserId}, "error creating project invite"
+							return next(err)
+						logger.log {projectId, email, sendingUserId}, "invite created"
+						EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
+						return res.json {invite: invite}
 
 
 	revokeInvite: (req, res, next) ->
 	revokeInvite: (req, res, next) ->
 		projectId = req.params.Project_id
 		projectId = req.params.Project_id

+ 1 - 1
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee

@@ -80,7 +80,7 @@ module.exports = CollaboratorsInviteHandler =
 				# Send email and notification in background
 				# Send email and notification in background
 				CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
 				CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
 					if err?
 					if err?
-						logger.err {projectId, email}, "error sending messages for invite"
+						logger.err {err, projectId, email}, "error sending messages for invite"
 				callback(null, invite)
 				callback(null, invite)
 
 
 
 

+ 2 - 2
services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee

@@ -22,13 +22,13 @@ module.exports =
 		webRouter.post(
 		webRouter.post(
 			'/project/:Project_id/invite',
 			'/project/:Project_id/invite',
 			RateLimiterMiddlewear.rateLimit({
 			RateLimiterMiddlewear.rateLimit({
-				endpointName: "invite-to-project"
+				endpointName: "invite-to-project-by-project-id"
 				params: ["Project_id"]
 				params: ["Project_id"]
 				maxRequests: 100
 				maxRequests: 100
 				timeInterval: 60 * 10
 				timeInterval: 60 * 10
 			}),
 			}),
 			RateLimiterMiddlewear.rateLimit({
 			RateLimiterMiddlewear.rateLimit({
-				endpointName: "invite-to-project-ip"
+				endpointName: "invite-to-project-by-ip"
 				ipOnly:true
 				ipOnly:true
 				maxRequests: 100
 				maxRequests: 100
 				timeInterval: 60 * 10
 				timeInterval: 60 * 10

+ 4 - 4
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -97,7 +97,7 @@ Thank you
 
 
 
 
 templates.projectInvite =
 templates.projectInvite =
-	subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>"
+	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
 	layout: BaseWithHeaderEmailLayout
 	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	type:"notification"
 	plainTextTemplate: _.template """
 	plainTextTemplate: _.template """
@@ -111,16 +111,16 @@ Thank you
 """
 """
 	compiledTemplate: (opts) -> 
 	compiledTemplate: (opts) -> 
 		SingleCTAEmailBody({
 		SingleCTAEmailBody({
-			title: "#{ opts.project.name.slice(0, 40) } &ndash; shared by #{ opts.owner.email }"
+			title: "#{ opts.project.name } &ndash; shared by #{ opts.owner.email }"
 			greeting: "Hi,"
 			greeting: "Hi,"
-			message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name.slice(0, 40) }&rdquo; with you."
+			message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name }&rdquo; with you."
 			secondaryMessage: null
 			secondaryMessage: null
 			ctaText: "View project"
 			ctaText: "View project"
 			ctaURL: opts.inviteUrl
 			ctaURL: opts.inviteUrl
 			gmailGoToAction: 
 			gmailGoToAction: 
 				target: opts.inviteUrl
 				target: opts.inviteUrl
 				name: "View project"
 				name: "View project"
-				description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX"
+				description: "Join #{ opts.project.name } at ShareLaTeX"
 		})
 		})
 
 
 templates.completeJoinGroupAccount =
 templates.completeJoinGroupAccount =

+ 3 - 3
services/web/app/coffee/Features/StaticPages/HomeController.coffee

@@ -7,7 +7,7 @@ fs = require "fs"
 ErrorController = require "../Errors/ErrorController"
 ErrorController = require "../Errors/ErrorController"
 AuthenticationController = require('../Authentication/AuthenticationController')
 AuthenticationController = require('../Authentication/AuthenticationController')
 
 
-homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
+homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.pug")
 
 
 module.exports = HomeController =
 module.exports = HomeController =
 	index : (req,res)->
 	index : (req,res)->
@@ -28,10 +28,10 @@ module.exports = HomeController =
 
 
 	externalPage: (page, title) ->
 	externalPage: (page, title) ->
 		return (req, res, next = (error) ->) ->
 		return (req, res, next = (error) ->) ->
-			path = Path.resolve(__dirname + "/../../../views/external/#{page}.jade")
+			path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug")
 			fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
 			fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
 				if exists
 				if exists
-					res.render "external/#{page}.jade",
+					res.render "external/#{page}.pug",
 						title: title
 						title: title
 				else
 				else
 					ErrorController.notFound(req, res, next)
 					ErrorController.notFound(req, res, next)

+ 11 - 10
services/web/app/coffee/Features/Subscription/LimitationsManager.coffee

@@ -1,20 +1,25 @@
 logger = require("logger-sharelatex")
 logger = require("logger-sharelatex")
 Project = require("../../models/Project").Project
 Project = require("../../models/Project").Project
-User = require("../../models/User").User
+UserGetter = require("../User/UserGetter")
 SubscriptionLocator = require("./SubscriptionLocator")
 SubscriptionLocator = require("./SubscriptionLocator")
 Settings = require("settings-sharelatex")
 Settings = require("settings-sharelatex")
 CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
 CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
 CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
 CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
 
 
 module.exports =
 module.exports =
-
 	allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
 	allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
-		getOwnerOfProject project_id, (error, owner)->
+		Project.findById project_id, 'owner_ref', (error, project) =>
+			return callback(error) if error?
+			@allowedNumberOfCollaboratorsForUser project.owner_ref, callback
+	
+	allowedNumberOfCollaboratorsForUser: (user_id, callback) ->
+		UserGetter.getUser user_id, {features: 1}, (error, user) ->
 			return callback(error) if error?
 			return callback(error) if error?
-			if owner.features? and owner.features.collaborators?
-				callback null, owner.features.collaborators
+			if user.features? and user.features.collaborators?
+				callback null, user.features.collaborators
 			else
 			else
 				callback null, Settings.defaultPlanCode.collaborators
 				callback null, Settings.defaultPlanCode.collaborators
+		
 
 
 	canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
 	canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
 		@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
 		@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
@@ -63,8 +68,4 @@ module.exports =
 			logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
 			logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
 			callback(err, limitReached, subscription)
 			callback(err, limitReached, subscription)
 
 
-getOwnerOfProject = (project_id, callback)->
-	Project.findById project_id, 'owner_ref', (error, project) ->
-		return callback(error) if error?
-		User.findById project.owner_ref, (error, owner) ->
-			callback(error, owner)
+getOwnerIdOfProject = (project_id, callback)->

+ 1 - 1
services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee

@@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
 logger = require('logger-sharelatex')
 logger = require('logger-sharelatex')
 Settings = require('settings-sharelatex')
 Settings = require('settings-sharelatex')
 FileTypeManager = require('../Uploads/FileTypeManager')
 FileTypeManager = require('../Uploads/FileTypeManager')
-uuid = require('node-uuid')
+uuid = require('uuid')
 fs = require('fs')
 fs = require('fs')
 
 
 module.exports =
 module.exports =

+ 2 - 2
services/web/app/coffee/infrastructure/Modules.coffee

@@ -1,6 +1,6 @@
 fs   = require "fs"
 fs   = require "fs"
 Path = require "path"
 Path = require "path"
-jade = require "jade"
+pug = require "pug"
 async = require "async"
 async = require "async"
 
 
 MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
 MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
@@ -29,7 +29,7 @@ module.exports = Modules =
 		for module in @modules
 		for module in @modules
 			for view, partial of module.viewIncludes or {}
 			for view, partial of module.viewIncludes or {}
 				@viewIncludes[view] ||= []
 				@viewIncludes[view] ||= []
-				@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
+				@viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
 			
 			
 	moduleIncludes: (view, locals) ->
 	moduleIncludes: (view, locals) ->
 		compiledPartials = Modules.viewIncludes[view] or []
 		compiledPartials = Modules.viewIncludes[view] or []

+ 1 - 1
services/web/app/coffee/infrastructure/Server.coffee

@@ -61,7 +61,7 @@ if Settings.behindProxy
 
 
 webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
 webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
 app.set 'views', __dirname + '/../../views'
 app.set 'views', __dirname + '/../../views'
-app.set 'view engine', 'jade'
+app.set 'view engine', 'pug'
 Modules.loadViewIncludes app
 Modules.loadViewIncludes app
 
 
 
 

+ 1 - 1
services/web/app/coffee/models/User.coffee

@@ -2,7 +2,7 @@ Project = require('./Project').Project
 Settings = require 'settings-sharelatex'
 Settings = require 'settings-sharelatex'
 _ = require('underscore')
 _ = require('underscore')
 mongoose = require('mongoose')
 mongoose = require('mongoose')
-uuid = require('node-uuid')
+uuid = require('uuid')
 Schema = mongoose.Schema
 Schema = mongoose.Schema
 ObjectId = Schema.ObjectId
 ObjectId = Schema.ObjectId
 
 

+ 2 - 2
services/web/app/views/admin/index.jade → services/web/app/views/admin/index.pug

@@ -28,10 +28,10 @@ block content
 							tab(heading="Open Sockets")
 							tab(heading="Open Sockets")
 								.row-spaced
 								.row-spaced
 									ul
 									ul
-										-each agents, url in openSockets
+										each agents, url in openSockets
 											li #{url} - total : #{agents.length}
 											li #{url} - total : #{agents.length}
 												ul
 												ul
-													-each agent in agents
+													each agent in agents
 														li #{agent}
 														li #{agent}
 
 
 							tab(heading="Close Editor")
 							tab(heading="Close Editor")

+ 0 - 0
services/web/app/views/admin/register.jade → services/web/app/views/admin/register.pug


+ 0 - 0
services/web/app/views/beta_program/opt_in.jade → services/web/app/views/beta_program/opt_in.pug


+ 0 - 0
services/web/app/views/blog/blog_holder.jade → services/web/app/views/blog/blog_holder.pug


+ 5 - 5
services/web/app/views/contact-us-modal.jade → services/web/app/views/contact-us-modal.pug

@@ -24,10 +24,10 @@ script(type='text/ng-template', id='supportModalTemplate')
 						a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
 						a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
 							span(ng-bind-html="suggestion.name")
 							span(ng-bind-html="suggestion.name")
 							i.fa.fa-angle-right
 							i.fa.fa-angle-right
-			label.desc(ng-show="'#{getUserEmail()}'.length < 1")
+			label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
 				| #{translate("email")}
 				| #{translate("email")}
-			.form-group(ng-show="'#{getUserEmail()}'.length < 1")
-				input.field.text.medium.span8.form-control(ng-model="form.email",  ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
+			.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
+				input.field.text.medium.span8.form-control(ng-model="form.email",  ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
 			label#title12.desc
 			label#title12.desc
 				| #{translate("project_url")} (#{translate("optional")})
 				| #{translate("project_url")} (#{translate("optional")})
 			.form-group
 			.form-group
@@ -37,6 +37,6 @@ script(type='text/ng-template', id='supportModalTemplate')
 			.form-group
 			.form-group
 				textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
 				textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
 			.form-group.text-center
 			.form-group.text-center
-				input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}')
+				input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
 		span(ng-show="sent")
 		span(ng-show="sent")
-			p #{translate("request_sent_thank_you")}
+			p #{translate("request_sent_thank_you")}

+ 0 - 0
services/web/app/views/general/404.jade → services/web/app/views/general/404.pug


+ 0 - 0
services/web/app/views/general/500.jade → services/web/app/views/general/500.pug


+ 0 - 0
services/web/app/views/general/closed.jade → services/web/app/views/general/closed.pug


+ 3 - 1
services/web/app/views/layout.jade → services/web/app/views/layout.pug

@@ -21,6 +21,8 @@ html(itemscope, itemtype='http://schema.org/Product')
 		link(rel="icon", href="/favicon.ico")
 		link(rel="icon", href="/favicon.ico")
 		link(rel='stylesheet', href=buildCssPath('/style.css'))
 		link(rel='stylesheet', href=buildCssPath('/style.css'))
 
 
+		block _headLinks
+
 		if settings.i18n.subdomainLang
 		if settings.i18n.subdomainLang
 			each subdomainDetails in settings.i18n.subdomainLang
 			each subdomainDetails in settings.i18n.subdomainLang
 				if !subdomainDetails.hide
 				if !subdomainDetails.hide
@@ -30,7 +32,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
 		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
 		
 		
 		-if (typeof(meta) == "undefined")
 		-if (typeof(meta) == "undefined")
-			meta(itemprop="description", name="description", content='#{translate("site_description")}')
+			meta(itemprop="description", name="description", content=translate("site_description"))
 		-else
 		-else
 			meta(itemprop="description", name="description" , content=meta)
 			meta(itemprop="description", name="description" , content=meta)
 		
 		

+ 3 - 3
services/web/app/views/layout/footer.jade → services/web/app/views/layout/footer.pug

@@ -13,9 +13,9 @@ footer.site-footer
 							data-toggle="dropdown",
 							data-toggle="dropdown",
 							aria-haspopup="true",
 							aria-haspopup="true",
 							aria-expanded="false",
 							aria-expanded="false",
-							tooltip="#{translate('language')}"
+							tooltip=translate('language')
 						)
 						)
-							figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
+							figure(class="sprite-icon sprite-icon-lang sprite-icon-"+currentLngCode)
 
 
 						ul.dropdown-menu(role="menu")
 						ul.dropdown-menu(role="menu")
 							li.dropdown-header #{translate("language")}
 							li.dropdown-header #{translate("language")}
@@ -23,7 +23,7 @@ footer.site-footer
 								if !subdomainDetails.hide
 								if !subdomainDetails.hide
 									li.lngOption
 									li.lngOption
 										a.menu-indent(href=subdomainDetails.url+currentUrl)
 										a.menu-indent(href=subdomainDetails.url+currentUrl)
-											figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
+											figure(class="sprite-icon sprite-icon-lang sprite-icon-"+subdomainDetails.lngCode)
 											|  #{translate(subdomainDetails.lngCode)}
 											|  #{translate(subdomainDetails.lngCode)}
 										//- img(src="/img/flags/24/.png")
 										//- img(src="/img/flags/24/.png")
 				each item in nav.left_footer
 				each item in nav.left_footer

+ 1 - 1
services/web/app/views/layout/navbar.jade → services/web/app/views/layout/navbar.pug

@@ -4,7 +4,7 @@ nav.navbar.navbar-default
 			button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
 			button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
 				i.fa.fa-bars
 				i.fa.fa-bars
 			if settings.nav.custom_logo
 			if settings.nav.custom_logo
-				a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand
+				a(href='/', style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
 			else if (nav.title)
 			else if (nav.title)
 				a(href='/').navbar-title #{nav.title}
 				a(href='/').navbar-title #{nav.title}
 			else
 			else

+ 0 - 0
services/web/app/views/project/editor.jade → services/web/app/views/project/editor.pug


+ 0 - 0
services/web/app/views/project/editor/binary-file.jade → services/web/app/views/project/editor/binary-file.pug


+ 1 - 1
services/web/app/views/project/editor/chat.jade → services/web/app/views/project/editor/chat.pug

@@ -50,7 +50,7 @@ aside.chat(
 
 
 	.new-message
 	.new-message
 		textarea(
 		textarea(
-			placeholder="#{translate('your_message')}...",
+			placeholder=translate('your_message')+"...",
 			on-enter="sendMessage()",
 			on-enter="sendMessage()",
 			ng-model="newMessageContent",
 			ng-model="newMessageContent",
 			ng-click="resetUnreadMessages()"
 			ng-click="resetUnreadMessages()"

+ 3 - 3
services/web/app/views/project/editor/editor.jade → services/web/app/views/project/editor/editor.pug

@@ -70,7 +70,7 @@ div.full-size(
 		ng-controller="PdfSynctexController"
 		ng-controller="PdfSynctexController"
 	)
 	)
 		a.btn.btn-default.btn-xs(
 		a.btn.btn-default.btn-xs(
-			tooltip="#{translate('go_to_code_location_in_pdf')}"
+			tooltip=translate('go_to_code_location_in_pdf')
 			tooltip-placement="right"
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			tooltip-append-to-body="true"
 			ng-click="syncToPdf()"
 			ng-click="syncToPdf()"
@@ -78,7 +78,7 @@ div.full-size(
 			i.fa.fa-long-arrow-right
 			i.fa.fa-long-arrow-right
 		br
 		br
 		a.btn.btn-default.btn-xs(
 		a.btn.btn-default.btn-xs(
-			tooltip-html="'#{translate('go_to_pdf_location_in_code')}'"
+			tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
 			tooltip-placement="right"
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			tooltip-append-to-body="true"
 			ng-click="syncToCode()"
 			ng-click="syncToCode()"
@@ -90,4 +90,4 @@ div.full-size(
 	ng-show="ui.view == 'pdf'"
 	ng-show="ui.view == 'pdf'"
 )
 )
 	include ./pdf
 	include ./pdf
-	
+	

+ 0 - 0
services/web/app/views/project/editor/feature-onboarding.jade → services/web/app/views/project/editor/feature-onboarding.pug


+ 6 - 6
services/web/app/views/project/editor/file-tree.jade → services/web/app/views/project/editor/file-tree.pug

@@ -3,21 +3,21 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
 		a(
 		a(
 			href,
 			href,
 			ng-click="openNewDocModal()",
 			ng-click="openNewDocModal()",
-			tooltip-html="'#{translate('new_file').replace(' ', '<br>')}'",
+			tooltip-html="'"+translate('new_file').replace(' ', '<br>')+"'",
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-file
 			i.fa.fa-file
 		a(
 		a(
 			href,
 			href,
 			ng-click="openNewFolderModal()",
 			ng-click="openNewFolderModal()",
-			tooltip-html="'#{translate('new_folder').replace(' ', '<br>')}'",
+			tooltip-html="'"+translate('new_folder').replace(' ', '<br>')+"'",
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-folder
 			i.fa.fa-folder
 		a(
 		a(
 			href,
 			href,
 			ng-click="openUploadFileModal()",
 			ng-click="openUploadFileModal()",
-			tooltip="#{translate('upload')}",
+			tooltip=translate('upload'),
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-upload
 			i.fa.fa-upload
@@ -26,7 +26,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
 			a(
 			a(
 				href,
 				href,
 				ng-click="startRenamingSelected()",
 				ng-click="startRenamingSelected()",
-				tooltip="#{translate('rename')}",
+				tooltip=translate('rename'),
 				tooltip-placement="bottom",
 				tooltip-placement="bottom",
 				ng-show="multiSelectedCount == 0"
 				ng-show="multiSelectedCount == 0"
 			)
 			)
@@ -34,7 +34,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
 			a(
 			a(
 				href,
 				href,
 				ng-click="openDeleteModalForSelected()",
 				ng-click="openDeleteModalForSelected()",
-				tooltip="#{translate('delete')}",
+				tooltip=translate('delete'),
 				tooltip-placement="bottom",
 				tooltip-placement="bottom",
 				tooltip-append-to-body="true"
 				tooltip-append-to-body="true"
 			)
 			)
@@ -431,4 +431,4 @@ script(type='text/ng-template', id='invalidFileNameModalTemplate')
 	.modal-footer
 	.modal-footer
 		button.btn.btn-default(
 		button.btn.btn-default(
 			ng-click="$close()"
 			ng-click="$close()"
-		) #{translate('ok')}
+		) #{translate('ok')}

+ 3 - 3
services/web/app/views/project/editor/header.jade → services/web/app/views/project/editor/header.pug

@@ -45,7 +45,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
 			ng-if="permissions.admin",
 			ng-if="permissions.admin",
 			href='#',
 			href='#',
 			tooltip-placement="bottom",
 			tooltip-placement="bottom",
-			tooltip="#{translate('rename')}",
+			tooltip=translate('rename'),
 			tooltip-append-to-body="true",
 			tooltip-append-to-body="true",
 			ng-click="startRenaming()",
 			ng-click="startRenaming()",
 			ng-show="!state.renaming"
 			ng-show="!state.renaming"
@@ -71,7 +71,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
 			span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
 			span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
 				span.online-user.online-user-multi(
 				span.online-user.online-user-multi(
 					dropdown-toggle,
 					dropdown-toggle,
-					tooltip="#{translate('connected_users')}",
+					tooltip=translate('connected_users'),
 					tooltip-placement="left"
 					tooltip-placement="left"
 				)
 				)
 					strong {{ onlineUsersArray.length }}
 					strong {{ onlineUsersArray.length }}
@@ -121,4 +121,4 @@ header.toolbar.toolbar-header.toolbar-with-labels(
 			span.label.label-info(
 			span.label.label-info(
 				ng-show="unreadMessages > 0"
 				ng-show="unreadMessages > 0"
 			) {{ unreadMessages }}
 			) {{ unreadMessages }}
-			p.toolbar-label #{translate("chat")}
+			p.toolbar-label #{translate("chat")}

+ 0 - 0
services/web/app/views/project/editor/history.jade → services/web/app/views/project/editor/history.pug


+ 0 - 0
services/web/app/views/project/editor/hotkeys.jade → services/web/app/views/project/editor/hotkeys.pug


+ 3 - 3
services/web/app/views/project/editor/left-menu.jade → services/web/app/views/project/editor/left-menu.pug

@@ -24,7 +24,7 @@ aside#left-menu.full-size(
 				| PDF
 				| PDF
 			div.link-disabled(
 			div.link-disabled(
 				ng-if="!pdf.url"
 				ng-if="!pdf.url"
-				tooltip="#{translate('please_compile_pdf_before_download')}"
+				tooltip=translate('please_compile_pdf_before_download')
 				tooltip-placement="bottom"
 				tooltip-placement="bottom"
 			)
 			)
 				i.fa.fa-file-pdf-o.fa-2x
 				i.fa.fa-file-pdf-o.fa-2x
@@ -47,7 +47,7 @@ aside#left-menu.full-size(
 				a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
 				a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
 					i.fa.fa-fw.fa-eye
 					i.fa.fa-fw.fa-eye
 					span &nbsp;&nbsp; #{translate("word_count")}
 					span &nbsp;&nbsp; #{translate("word_count")}
-				a.link-disabled(href, ng-if="!pdf.url" , tooltip="#{translate('please_compile_pdf_before_word_count')}")
+				a.link-disabled(href, ng-if="!pdf.url" , tooltip=translate('please_compile_pdf_before_word_count'))
 					i.fa.fa-fw.fa-eye
 					i.fa.fa-fw.fa-eye
 					span.link-disabled &nbsp;&nbsp; #{translate("word_count")}	
 					span.link-disabled &nbsp;&nbsp; #{translate("word_count")}	
 
 
@@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
 				span &nbsp; #{translate("loading")}...  
 				span &nbsp; #{translate("loading")}...  
 			div.pdf-disabled(
 			div.pdf-disabled(
 				ng-if="!pdf.url"
 				ng-if="!pdf.url"
-				tooltip="#{translate('please_compile_pdf_before_word_count')}"
+				tooltip=translate('please_compile_pdf_before_word_count')
 				tooltip-placement="bottom"
 				tooltip-placement="bottom"
 			)
 			)
 		div(ng-if="!status.loading")
 		div(ng-if="!status.loading")

+ 7 - 7
services/web/app/views/project/editor/pdf.jade → services/web/app/views/project/editor/pdf.pug

@@ -2,7 +2,7 @@ div.full-size.pdf(ng-controller="PdfController")
 	.toolbar.toolbar-tall
 	.toolbar.toolbar-tall
 		.btn-group(
 		.btn-group(
 			dropdown,
 			dropdown,
-			tooltip-html="'#{translate('recompile_pdf')} <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
+			tooltip-html="'"+translate('recompile_pdf')+" <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
 			tooltip-class="keyboard-tooltip"
 			tooltip-class="keyboard-tooltip"
 			tooltip-popup-delay="500"
 			tooltip-popup-delay="500"
 			tooltip-append-to-body="true"
 			tooltip-append-to-body="true"
@@ -53,7 +53,7 @@ div.full-size.pdf(ng-controller="PdfController")
 			href
 			href
 			ng-click="stop()"
 			ng-click="stop()"
 			ng-show="pdf.compiling",
 			ng-show="pdf.compiling",
-			tooltip="#{translate('stop_compile')}"
+			tooltip=translate('stop_compile')
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-stop()
 			i.fa.fa-stop()
@@ -61,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
 			href
 			href
 			ng-click="toggleLogs()"
 			ng-click="toggleLogs()"
 			ng-class="{ 'active': shouldShowLogs == true }"
 			ng-class="{ 'active': shouldShowLogs == true }"
-			tooltip="#{translate('logs_and_output_files')}"
+			tooltip=translate('logs_and_output_files')
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-file-text-o
 			i.fa.fa-file-text-o
@@ -77,7 +77,7 @@ div.full-size.pdf(ng-controller="PdfController")
 			ng-href="{{pdf.downloadUrl || pdf.url}}"
 			ng-href="{{pdf.downloadUrl || pdf.url}}"
 			target="_blank"
 			target="_blank"
 			ng-if="pdf.url"
 			ng-if="pdf.url"
-			tooltip="#{translate('download_pdf')}"
+			tooltip=translate('download_pdf')
 			tooltip-placement="bottom"
 			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-download
 			i.fa.fa-download
@@ -87,7 +87,7 @@ div.full-size.pdf(ng-controller="PdfController")
 				href,
 				href,
 				ng-click="switchToFlatLayout()"
 				ng-click="switchToFlatLayout()"
 				ng-show="ui.pdfLayout == 'sideBySide'"
 				ng-show="ui.pdfLayout == 'sideBySide'"
-				tooltip="#{translate('full_screen')}"
+				tooltip=translate('full_screen')
 				tooltip-placement="bottom"
 				tooltip-placement="bottom"
 				tooltip-append-to-body="true"
 				tooltip-append-to-body="true"
 			)
 			)
@@ -96,7 +96,7 @@ div.full-size.pdf(ng-controller="PdfController")
 				href,
 				href,
 				ng-click="switchToSideBySideLayout()"
 				ng-click="switchToSideBySideLayout()"
 				ng-show="ui.pdfLayout == 'flat'"
 				ng-show="ui.pdfLayout == 'flat'"
-				tooltip="#{translate('split_screen')}"
+				tooltip=translate('split_screen')
 				tooltip-placement="bottom"
 				tooltip-placement="bottom"
 				tooltip-append-to-body="true"
 				tooltip-append-to-body="true"
 			)
 			)
@@ -233,7 +233,7 @@ div.full-size.pdf(ng-controller="PdfController")
 			.files-dropdown-container
 			.files-dropdown-container
 				a.btn.btn-default.btn-sm(
 				a.btn.btn-default.btn-sm(
 					href,
 					href,
-					tooltip="#{translate('clear_cached_files')}",
+					tooltip=translate('clear_cached_files'),
 					tooltip-placement="top",
 					tooltip-placement="top",
 					tooltip-append-to-body="true",
 					tooltip-append-to-body="true",
 					ng-click="openClearCacheModal()"
 					ng-click="openClearCacheModal()"

+ 0 - 0
services/web/app/views/project/editor/publish-template.jade → services/web/app/views/project/editor/publish-template.pug


+ 3 - 2
services/web/app/views/project/editor/review-panel.jade → services/web/app/views/project/editor/review-panel.pug

@@ -41,7 +41,6 @@
 		.rp-entry-list-inner
 		.rp-entry-list-inner
 			.rp-entry-wrapper(
 			.rp-entry-wrapper(
 				ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
 				ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
-				ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
 			)
 			)
 				div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
 				div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
 					change-entry(
 					change-entry(
@@ -50,6 +49,7 @@
 						on-reject="rejectChange(entry_id);"
 						on-reject="rejectChange(entry_id);"
 						on-accept="acceptChange(entry_id);"
 						on-accept="acceptChange(entry_id);"
 						on-indicator-click="toggleReviewPanel();"
 						on-indicator-click="toggleReviewPanel();"
+						on-body-click="gotoEntry(editor.open_doc_id, entry)"
 						permissions="permissions"
 						permissions="permissions"
 					)
 					)
 
 
@@ -62,6 +62,7 @@
 						on-indicator-click="toggleReviewPanel();"
 						on-indicator-click="toggleReviewPanel();"
 						on-save-edit="saveEdit(entry.thread_id, comment)"
 						on-save-edit="saveEdit(entry.thread_id, comment)"
 						on-delete="deleteComment(entry.thread_id, comment)"
 						on-delete="deleteComment(entry.thread_id, comment)"
+						on-body-click="gotoEntry(editor.open_doc_id, entry)"
 						permissions="permissions"
 						permissions="permissions"
 						ng-if="!reviewPanel.loadingThreads"
 						ng-if="!reviewPanel.loadingThreads"
 					)
 					)
@@ -191,7 +192,7 @@ script(type='text/ng-template', id='commentEntryTemplate')
 
 
 			.rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
 			.rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
 				| No comments
 				| No comments
-			div
+			.rp-comment-loaded
 				.rp-comment(
 				.rp-comment(
 					ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
 					ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
 				)
 				)

+ 3 - 3
services/web/app/views/project/editor/share.jade → services/web/app/views/project/editor/share.pug

@@ -38,7 +38,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 				.col-xs-1
 				.col-xs-1
 					a(
 					a(
 						href
 						href
-						tooltip="#{translate('remove_collaborator')}"
+						tooltip=translate('remove_collaborator')
 						tooltip-placement="bottom"
 						tooltip-placement="bottom"
 						ng-click="removeMember(member)"
 						ng-click="removeMember(member)"
 					)
 					)
@@ -55,7 +55,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 				.col-xs-1
 				.col-xs-1
 					a(
 					a(
 						href
 						href
-						tooltip="#{translate('revoke_invite')}"
+						tooltip=translate('revoke_invite')
 						tooltip-placement="bottom"
 						tooltip-placement="bottom"
 						ng-click="revokeInvite(invite)"
 						ng-click="revokeInvite(invite)"
 					)
 					)
@@ -66,7 +66,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 					.form-group
 					.form-group
 						tags-input(
 						tags-input(
 							template="shareTagTemplate"
 							template="shareTagTemplate"
-							placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}"
+							placeholder=settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'
 							ng-model="inputs.contacts"
 							ng-model="inputs.contacts"
 							focus-on="open"
 							focus-on="open"
 							display-property="display"
 							display-property="display"

+ 0 - 0
services/web/app/views/project/invite/not-valid.jade → services/web/app/views/project/invite/not-valid.pug


+ 3 - 3
services/web/app/views/project/invite/show.jade → services/web/app/views/project/invite/show.pug

@@ -20,12 +20,12 @@ block content
 								form.form(
 								form.form(
 									name="acceptForm",
 									name="acceptForm",
 									method="POST",
 									method="POST",
-									action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
+									action="/project/"+invite.projectId+"/invite/token/"+invite.token+"/accept"
 								)
 								)
 									input(name='_csrf', type='hidden', value=csrfToken)
 									input(name='_csrf', type='hidden', value=csrfToken)
-									input(name='token', type='hidden', value="#{invite.token}")
+									input(name='token', type='hidden', value=invite.token)
 									.form-group.text-center
 									.form-group.text-center
 										button.btn.btn-lg.btn-primary(type="submit")
 										button.btn.btn-lg.btn-primary(type="submit")
 											| #{translate("join_project")}
 											| #{translate("join_project")}
 									.form-group.text-center
 									.form-group.text-center
-							
+							

+ 0 - 0
services/web/app/views/project/list.jade → services/web/app/views/project/list.pug


+ 0 - 0
services/web/app/views/project/list/empty-project-list.jade → services/web/app/views/project/list/empty-project-list.pug


+ 0 - 0
services/web/app/views/project/list/modals.jade → services/web/app/views/project/list/modals.pug


+ 0 - 0
services/web/app/views/project/list/notifications.jade → services/web/app/views/project/list/notifications.pug


+ 4 - 4
services/web/app/views/project/list/project-list.jade → services/web/app/views/project/list/project-list.pug

@@ -6,7 +6,7 @@
 		form.project-search.form-horizontal(role="form")
 		form.project-search.form-horizontal(role="form")
 			.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
 			.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
 				input.form-control.col-md-7.col-xs-12(
 				input.form-control.col-md-7.col-xs-12(
-					placeholder="#{translate('search_projects')}…",
+					placeholder=translate('search_projects')+"…",
 					autofocus='autofocus',
 					autofocus='autofocus',
 					ng-model="searchText.value",
 					ng-model="searchText.value",
 					focus-on='search:clear',
 					focus-on='search:clear',
@@ -25,7 +25,7 @@
 				.btn-group(ng-hide="selectedProjects.length < 1")
 				.btn-group(ng-hide="selectedProjects.length < 1")
 					a.btn.btn-default(
 					a.btn.btn-default(
 						href,
 						href,
-						tooltip="#{translate('download')}",
+						tooltip=translate('download'),
 						tooltip-placement="bottom",
 						tooltip-placement="bottom",
 						tooltip-append-to-body="true",
 						tooltip-append-to-body="true",
 						ng-click="downloadSelectedProjects()"
 						ng-click="downloadSelectedProjects()"
@@ -33,7 +33,7 @@
 						i.fa.fa-cloud-download
 						i.fa.fa-cloud-download
 					a.btn.btn-default(
 					a.btn.btn-default(
 						href,
 						href,
-						tooltip="#{translate('delete')}",
+						tooltip=translate('delete'),
 						tooltip-placement="bottom",
 						tooltip-placement="bottom",
 						tooltip-append-to-body="true",
 						tooltip-append-to-body="true",
 						ng-click="openArchiveProjectsModal()"
 						ng-click="openArchiveProjectsModal()"
@@ -45,7 +45,7 @@
 						href,
 						href,
 						data-toggle="dropdown",
 						data-toggle="dropdown",
 						dropdown-toggle,
 						dropdown-toggle,
-						tooltip="#{translate('add_to_folders')}",
+						tooltip=translate('add_to_folders'),
 						tooltip-append-to-body="true",
 						tooltip-append-to-body="true",
 						tooltip-placement="bottom"
 						tooltip-placement="bottom"
 					)
 					)

+ 0 - 0
services/web/app/views/project/list/side-bar.jade → services/web/app/views/project/list/side-bar.pug


+ 6 - 6
services/web/app/views/referal/bonus.jade → services/web/app/views/referal/bonus.pug

@@ -24,7 +24,7 @@ block content
 							.row
 							.row
 								.col-md-8.col-md-offset-2.bonus-banner
 								.col-md-8.col-md-offset-2.bonus-banner
 									.title
 									.title
-										a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url=#{encodeURIComponent(buildReferalUrl("t"))}&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
+										a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url='+encodeURIComponent(buildReferalUrl("t"))+'&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
 									
 									
 							.row
 							.row
 								.col-md-8.col-md-offset-2.bonus-banner
 								.col-md-8.col-md-offset-2.bonus-banner
@@ -34,12 +34,12 @@ block content
 							.row
 							.row
 								.col-md-8.col-md-offset-2.bonus-banner
 								.col-md-8.col-md-offset-2.bonus-banner
 									.title
 									.title
-										a(href="https://plus.google.com/share?url=#{encodeURIComponent(buildReferalUrl('gp'))}", onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
+										a(href="https://plus.google.com/share?url="+encodeURIComponent(buildReferalUrl('gp')), onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
 									
 									
 							.row
 							.row
 								.col-md-8.col-md-offset-2.bonus-banner
 								.col-md-8.col-md-offset-2.bonus-banner
 									.title
 									.title
-										a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. #{encodeURIComponent(buildReferalUrl("e"))}', title='Share by Email').email #{translate("email_us_to_your_friends")}
+										a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. '+encodeURIComponent(buildReferalUrl("e")), title='Share by Email').email #{translate("email_us_to_your_friends")}
 
 
 							.row
 							.row
 								.col-md-8.col-md-offset-2.bonus-banner
 								.col-md-8.col-md-offset-2.bonus-banner
@@ -58,9 +58,9 @@ block content
 								.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
 								.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
 									- for (var i = 0; i <= 10; i++) {
 									- for (var i = 0; i <= 10; i++) {
 										- if (refered_user_count == i)
 										- if (refered_user_count == i)
-											.number(style="left: #{i}0%").active #{i}
+											.number(style="left: "+i+"0%").active #{i}
 										- else
 										- else
-											.number(style="left: #{i}0%") #{i}
+											.number(style="left: "+i+"0%") #{i}
 									- }
 									- }
 
 
 							.row.ab-bonus
 							.row.ab-bonus
@@ -68,7 +68,7 @@ block content
 									.progress
 									.progress
 										- if (refered_user_count == 0)
 										- if (refered_user_count == 0)
 											div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
 											div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
-										.progress-bar.progress-bar-info(style="width: #{refered_user_count}0%")
+										.progress-bar.progress-bar-info(style="width: "+refered_user_count+"0%")
 
 
 							.row.ab-bonus
 							.row.ab-bonus
 								.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")
 								.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")

+ 2 - 2
services/web/app/views/restore.jade → services/web/app/views/restore.pug

@@ -19,11 +19,11 @@ block content
 
 
 					.row-fluid
 					.row-fluid
 						table.table
 						table.table
-							-each project in projects
+							each project in projects
 								tr
 								tr
 									- project_id = project._id.toString()
 									- project_id = project._id.toString()
 									td(width="50%") #{project.name}
 									td(width="50%") #{project.name}
 									td(width="25%")
 									td(width="25%")
-										a.btn(href="/project/#{project_id}/zip") Download latest version as Zip
+										a.btn(href="/project/"+project_id+"/zip") Download latest version as Zip
 
 
 		include general/small-footer
 		include general/small-footer

+ 1 - 1
services/web/app/views/scribtex-modal.jade → services/web/app/views/scribtex-modal.pug

@@ -5,4 +5,4 @@ script(type='text/ng-template', id='scribtexModalTemplate')
 		p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks.
 		p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks.
 		p(style="text-align: center") You can find the page you were looking for here:
 		p(style="text-align: center") You can find the page you were looking for here:
 		p(style="text-align: center")
 		p(style="text-align: center")
-			a(href="https://scribtex.sharelatex.com#{scribtexPath}", style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}
+			a(href="https://scribtex.sharelatex.com"+scribtexPath, style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}

+ 2 - 2
services/web/app/views/sentry.jade → services/web/app/views/sentry.pug

@@ -1,8 +1,8 @@
 - if (typeof(sentrySrc) != "undefined")
 - if (typeof(sentrySrc) != "undefined")
 	- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
 	- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
-		script(src="#{sentrySrc}")
+		script(src=sentrySrc)
 	- else
 	- else
-		script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
+		script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
 - if (typeof(sentrySrc) != "undefined")
 - if (typeof(sentrySrc) != "undefined")
 	script(type="text/javascript").
 	script(type="text/javascript").
 		if (typeof(Raven) != "undefined" && Raven.config) {
 		if (typeof(Raven) != "undefined" && Raven.config) {

+ 0 - 0
services/web/app/views/subscriptions/custom_account.jade → services/web/app/views/subscriptions/custom_account.pug


+ 10 - 10
services/web/app/views/subscriptions/dashboard.jade → services/web/app/views/subscriptions/dashboard.pug

@@ -12,7 +12,7 @@ block scripts
 
 
 mixin printPlan(plan)
 mixin printPlan(plan)
 	-if (!plan.hideFromUsers)
 	-if (!plan.hideFromUsers)
-		tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
+		tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan), ng-show="shouldShowPlan(plan.planCode)")
 			td
 			td
 				strong #{plan.name}
 				strong #{plan.name}
 			td {{refreshPrice(plan.planCode)}}
 			td {{refreshPrice(plan.planCode)}}
@@ -22,18 +22,18 @@ mixin printPlan(plan)
 					| {{prices[plan.planCode]}} / #{translate("month")}
 					| {{prices[plan.planCode]}} / #{translate("month")}
 			td
 			td
 				-if (subscription.state == "free-trial")
 				-if (subscription.state == "free-trial")
-					a(href="/user/subscription/new?planCode=#{plan.planCode}").btn.btn-success #{translate("subscribe_to_this_plan")}
+					a(href="/user/subscription/new?planCode="+plan.planCode).btn.btn-success #{translate("subscribe_to_this_plan")}
 				-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
 				-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
 					button.btn.disabled #{translate("your_plan")}
 					button.btn.disabled #{translate("your_plan")}
 				-else
 				-else
 					form
 					form
-						input(type="hidden", ng-model="plan_code", name="plan_code", value="#{plan.planCode}")
+						input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
 						input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
 						input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
 
 
 
 
 mixin printPlans(plans)
 mixin printPlans(plans)
-	-each plan in plans
-		mixin printPlan(plan)
+	each plan in plans
+		+printPlan(plan)
 
 
 block content
 block content
 	.content.content-alt(ng-cloak)
 	.content.content-alt(ng-cloak)
@@ -46,7 +46,7 @@ block content
 							| &nbsp;
 							| &nbsp;
 							| #{translate("your_billing_details_were_saved")}
 							| #{translate("your_billing_details_were_saved")}
 					.card(ng-if="view == 'overview'")
 					.card(ng-if="view == 'overview'")
-						.page-header(x-current-plan="#{subscription.planCode}")
+						.page-header(x-current-plan=subscription.planCode)
 							h1 #{translate("your_subscription")} 
 							h1 #{translate("your_subscription")} 
 
 
 						- if (subscription && user._id+'' == subscription.admin_id+'')
 						- if (subscription && user._id+'' == subscription.admin_id+'')
@@ -97,9 +97,9 @@ block content
 										th !{translate("name")}
 										th !{translate("name")}
 										th !{translate("price")}
 										th !{translate("price")}
 										th
 										th
-									mixin printPlans(plans.studentAccounts)
-									mixin printPlans(plans.individualMonthlyPlans)
-									mixin printPlans(plans.individualAnnualPlans)
+									+printPlans(plans.studentAccounts)
+									+printPlans(plans.individualMonthlyPlans)
+									+printPlans(plans.individualAnnualPlans)
 							
 							
 
 
 						each groupSubscription in groupSubscriptions
 						each groupSubscription in groupSubscriptions
@@ -107,7 +107,7 @@ block content
 								div 
 								div 
 									p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
 									p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
 									span 
 									span 
-										button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
+										button.btn.btn-danger(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
 
 
 						-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
 						-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
 							div
 							div

+ 0 - 0
services/web/app/views/subscriptions/edit-billing-details.jade → services/web/app/views/subscriptions/edit-billing-details.pug


+ 0 - 0
services/web/app/views/subscriptions/group/invite.jade → services/web/app/views/subscriptions/group/invite.pug


+ 0 - 0
services/web/app/views/subscriptions/group/successful_join.jade → services/web/app/views/subscriptions/group/successful_join.pug


+ 0 - 0
services/web/app/views/subscriptions/group_admin.jade → services/web/app/views/subscriptions/group_admin.pug


+ 1 - 1
services/web/app/views/subscriptions/new.jade → services/web/app/views/subscriptions/new.pug

@@ -164,7 +164,7 @@ block content
 													ng-change="updateCountry()"
 													ng-change="updateCountry()"
 													required
 													required
 												)
 												)
-													mixin countries_options()
+													+countries_options()
 												span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
 												span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
 
 
 											if (showVatField)
 											if (showVatField)

+ 0 - 0
services/web/app/views/subscriptions/plans.jade → services/web/app/views/subscriptions/plans.pug


+ 0 - 0
services/web/app/views/subscriptions/successful_subscription.jade → services/web/app/views/subscriptions/successful_subscription.pug


+ 1 - 1
services/web/app/views/subscriptions/upgradeToAnnual.jade → services/web/app/views/subscriptions/upgradeToAnnual.pug

@@ -6,7 +6,7 @@ block content
 		.container(ng-controller="AnnualUpgradeController")
 		.container(ng-controller="AnnualUpgradeController")
 			.row(ng-cloak)
 			.row(ng-cloak)
 				.col-md-6.col-md-offset-3
 				.col-md-6.col-md-offset-3
-					.card(ng-init="planName = #{JSON.stringify(planName)}")
+					.card(ng-init="planName = "+JSON.stringify(planName))
 						.page-header
 						.page-header
 							h1.text-centered #{translate("move_to_annual_billing")}
 							h1.text-centered #{translate("move_to_annual_billing")}
 						div(ng-hide="upgradeComplete")
 						div(ng-hide="upgradeComplete")

+ 0 - 0
services/web/app/views/tests.jade → services/web/app/views/tests.pug


+ 0 - 0
services/web/app/views/translations/translation_message.jade → services/web/app/views/translations/translation_message.pug


+ 0 - 0
services/web/app/views/university/case_study.jade → services/web/app/views/university/case_study.pug


+ 0 - 0
services/web/app/views/university/university_holder.jade → services/web/app/views/university/university_holder.pug


+ 1 - 1
services/web/app/views/user/activate.jade → services/web/app/views/user/activate.pug

@@ -36,7 +36,7 @@ block content
 									placeholder="email@example.com"
 									placeholder="email@example.com"
 									required,
 									required,
 									ng-model="email",
 									ng-model="email",
-									ng-init="email = #{JSON.stringify(email)}",
+									ng-init="email = "+JSON.stringify(email),
 									ng-model-options="{ updateOn: 'blur' }",
 									ng-model-options="{ updateOn: 'blur' }",
 									disabled
 									disabled
 								)
 								)

+ 1 - 1
services/web/app/views/user/login.jade → services/web/app/views/user/login.pug

@@ -19,7 +19,7 @@ block content
 									placeholder='email@example.com',
 									placeholder='email@example.com',
 									ng-model="email",
 									ng-model="email",
 									ng-model-options="{ updateOn: 'blur' }",
 									ng-model-options="{ updateOn: 'blur' }",
-									ng-init="email = #{JSON.stringify(email)}",
+									ng-init="email = "+JSON.stringify(email),
 									focus="true"
 									focus="true"
 								)
 								)
 								span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
 								span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")

+ 0 - 0
services/web/app/views/user/passwordReset.jade → services/web/app/views/user/passwordReset.pug


+ 0 - 0
services/web/app/views/user/register.jade → services/web/app/views/user/register.pug


+ 0 - 0
services/web/app/views/user/restricted.jade → services/web/app/views/user/restricted.pug


+ 0 - 0
services/web/app/views/user/sessions.jade → services/web/app/views/user/sessions.pug


+ 0 - 0
services/web/app/views/user/setPassword.jade → services/web/app/views/user/setPassword.pug


+ 1 - 1
services/web/app/views/user/settings.jade → services/web/app/views/user/settings.pug

@@ -28,7 +28,7 @@ block content
 														placeholder="email@example.com"
 														placeholder="email@example.com"
 														required,
 														required,
 														ng-model="email",
 														ng-model="email",
-														ng-init="email = #{JSON.stringify(user.email)}",
+														ng-init="email = "+JSON.stringify(user.email),
 														ng-model-options="{ updateOn: 'blur' }"
 														ng-model-options="{ updateOn: 'blur' }"
 													)
 													)
 													span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
 													span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")

+ 0 - 0
services/web/app/views/view_templates/bonus_templates.jade → services/web/app/views/view_templates/bonus_templates.pug


+ 8 - 0
services/web/modules/.gitignore

@@ -2,3 +2,11 @@
 */test/unit/js
 */test/unit/js
 */index.js
 */index.js
 ldap
 ldap
+admin-panel
+groovehq
+launchpad
+learn-wiki
+references-search
+sharelatex-saml
+templates
+tpr-webmodule

+ 471 - 57
services/web/npm-shrinkwrap.json

@@ -20,7 +20,8 @@
             },
             },
             "isarray": {
             "isarray": {
               "version": "0.0.1",
               "version": "0.0.1",
-              "from": "isarray@0.0.1"
+              "from": "isarray@0.0.1",
+              "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
             },
             },
             "string_decoder": {
             "string_decoder": {
               "version": "0.10.31",
               "version": "0.10.31",
@@ -100,7 +101,7 @@
             },
             },
             "glob": {
             "glob": {
               "version": "3.2.11",
               "version": "3.2.11",
-              "from": "glob@~3.2.6",
+              "from": "glob@~3.2.9",
               "dependencies": {
               "dependencies": {
                 "inherits": {
                 "inherits": {
                   "version": "2.0.3",
                   "version": "2.0.3",
@@ -124,7 +125,7 @@
             },
             },
             "minimatch": {
             "minimatch": {
               "version": "0.2.14",
               "version": "0.2.14",
-              "from": "minimatch@~0.2.12",
+              "from": "minimatch@~0.2.9",
               "dependencies": {
               "dependencies": {
                 "lru-cache": {
                 "lru-cache": {
                   "version": "2.7.3",
                   "version": "2.7.3",
@@ -256,7 +257,8 @@
             },
             },
             "setprototypeof": {
             "setprototypeof": {
               "version": "1.0.2",
               "version": "1.0.2",
-              "from": "setprototypeof@1.0.2"
+              "from": "setprototypeof@1.0.2",
+              "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz"
             },
             },
             "statuses": {
             "statuses": {
               "version": "1.3.1",
               "version": "1.3.1",
@@ -266,7 +268,8 @@
         },
         },
         "iconv-lite": {
         "iconv-lite": {
           "version": "0.4.15",
           "version": "0.4.15",
-          "from": "iconv-lite@0.4.15"
+          "from": "iconv-lite@0.4.15",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz"
         },
         },
         "on-finished": {
         "on-finished": {
           "version": "2.3.0",
           "version": "2.3.0",
@@ -280,7 +283,8 @@
         },
         },
         "qs": {
         "qs": {
           "version": "6.2.1",
           "version": "6.2.1",
-          "from": "qs@6.2.1"
+          "from": "qs@6.2.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz"
         },
         },
         "raw-body": {
         "raw-body": {
           "version": "2.2.0",
           "version": "2.2.0",
@@ -341,11 +345,12 @@
               "from": "double-ended-queue@^2.1.0-0"
               "from": "double-ended-queue@^2.1.0-0"
             },
             },
             "redis-commands": {
             "redis-commands": {
-              "version": "1.3.0",
-              "from": "redis-commands@^1.2.0"
+              "version": "1.3.1",
+              "from": "redis-commands@^1.2.0",
+              "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz"
             },
             },
             "redis-parser": {
             "redis-parser": {
-              "version": "2.3.0",
+              "version": "2.4.0",
               "from": "redis-parser@^2.0.0"
               "from": "redis-parser@^2.0.0"
             }
             }
           }
           }
@@ -446,15 +451,17 @@
             },
             },
             "rndm": {
             "rndm": {
               "version": "1.2.0",
               "version": "1.2.0",
-              "from": "rndm@1.2.0"
+              "from": "rndm@1.2.0",
+              "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz"
             },
             },
             "tsscmp": {
             "tsscmp": {
               "version": "1.0.5",
               "version": "1.0.5",
-              "from": "tsscmp@1.0.5"
+              "from": "tsscmp@1.0.5",
+              "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz"
             },
             },
             "uid-safe": {
             "uid-safe": {
               "version": "2.1.3",
               "version": "2.1.3",
-              "from": "uid-safe@2.1.3",
+              "from": "uid-safe@~2.1.3",
               "dependencies": {
               "dependencies": {
                 "random-bytes": {
                 "random-bytes": {
                   "version": "1.0.0",
                   "version": "1.0.0",
@@ -466,7 +473,7 @@
         },
         },
         "http-errors": {
         "http-errors": {
           "version": "1.5.1",
           "version": "1.5.1",
-          "from": "http-errors@~1.5.1",
+          "from": "http-errors@~1.5.0",
           "dependencies": {
           "dependencies": {
             "inherits": {
             "inherits": {
               "version": "2.0.3",
               "version": "2.0.3",
@@ -474,7 +481,8 @@
             },
             },
             "setprototypeof": {
             "setprototypeof": {
               "version": "1.0.2",
               "version": "1.0.2",
-              "from": "setprototypeof@1.0.2"
+              "from": "setprototypeof@1.0.2",
+              "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz"
             },
             },
             "statuses": {
             "statuses": {
               "version": "1.3.1",
               "version": "1.3.1",
@@ -491,6 +499,7 @@
     "express": {
     "express": {
       "version": "4.13.0",
       "version": "4.13.0",
       "from": "express@4.13.0",
       "from": "express@4.13.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz",
       "dependencies": {
       "dependencies": {
         "accepts": {
         "accepts": {
           "version": "1.2.13",
           "version": "1.2.13",
@@ -696,7 +705,7 @@
         },
         },
         "type-is": {
         "type-is": {
           "version": "1.6.14",
           "version": "1.6.14",
-          "from": "type-is@~1.6.3",
+          "from": "type-is@~1.6.14",
           "dependencies": {
           "dependencies": {
             "media-typer": {
             "media-typer": {
               "version": "0.3.0",
               "version": "0.3.0",
@@ -738,11 +747,12 @@
         },
         },
         "crc": {
         "crc": {
           "version": "3.4.4",
           "version": "3.4.4",
-          "from": "crc@3.4.4"
+          "from": "crc@3.4.4",
+          "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz"
         },
         },
         "debug": {
         "debug": {
           "version": "2.6.0",
           "version": "2.6.0",
-          "from": "debug@^2.2.0",
+          "from": "debug@*",
           "dependencies": {
           "dependencies": {
             "ms": {
             "ms": {
               "version": "0.7.2",
               "version": "0.7.2",
@@ -760,7 +770,7 @@
         },
         },
         "parseurl": {
         "parseurl": {
           "version": "1.3.1",
           "version": "1.3.1",
-          "from": "parseurl@~1.3.1"
+          "from": "parseurl@~1.3.0"
         },
         },
         "uid-safe": {
         "uid-safe": {
           "version": "2.1.3",
           "version": "2.1.3",
@@ -800,7 +810,8 @@
         },
         },
         "dateformat": {
         "dateformat": {
           "version": "1.0.2-1.2.3",
           "version": "1.0.2-1.2.3",
-          "from": "dateformat@1.0.2-1.2.3"
+          "from": "dateformat@1.0.2-1.2.3",
+          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
         },
         },
         "eventemitter2": {
         "eventemitter2": {
           "version": "0.4.14",
           "version": "0.4.14",
@@ -990,7 +1001,7 @@
         },
         },
         "debug": {
         "debug": {
           "version": "2.6.0",
           "version": "2.6.0",
-          "from": "debug@^2.2.0",
+          "from": "debug@*",
           "dependencies": {
           "dependencies": {
             "ms": {
             "ms": {
               "version": "0.7.2",
               "version": "0.7.2",
@@ -1007,8 +1018,9 @@
           "from": "flexbuffer@0.0.6"
           "from": "flexbuffer@0.0.6"
         },
         },
         "redis-commands": {
         "redis-commands": {
-          "version": "1.3.0",
-          "from": "redis-commands@^1.2.0"
+          "version": "1.3.1",
+          "from": "redis-commands@^1.2.0",
+          "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz"
         },
         },
         "redis-parser": {
         "redis-parser": {
           "version": "1.3.0",
           "version": "1.3.0",
@@ -1160,7 +1172,8 @@
                     },
                     },
                     "window-size": {
                     "window-size": {
                       "version": "0.1.0",
                       "version": "0.1.0",
-                      "from": "window-size@0.1.0"
+                      "from": "window-size@0.1.0",
+                      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
                     },
                     },
                     "wordwrap": {
                     "wordwrap": {
                       "version": "0.0.2",
                       "version": "0.0.2",
@@ -1178,7 +1191,7 @@
           "dependencies": {
           "dependencies": {
             "uglify-js": {
             "uglify-js": {
               "version": "2.4.24",
               "version": "2.4.24",
-              "from": "uglify-js@~2.4.0",
+              "from": "uglify-js@~2.4.12",
               "dependencies": {
               "dependencies": {
                 "async": {
                 "async": {
                   "version": "0.2.10",
                   "version": "0.2.10",
@@ -1212,7 +1225,8 @@
                     },
                     },
                     "window-size": {
                     "window-size": {
                       "version": "0.1.0",
                       "version": "0.1.0",
-                      "from": "window-size@0.1.0"
+                      "from": "window-size@0.1.0",
+                      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
                     },
                     },
                     "wordwrap": {
                     "wordwrap": {
                       "version": "0.0.2",
                       "version": "0.0.2",
@@ -1388,7 +1402,7 @@
           "dependencies": {
           "dependencies": {
             "core-util-is": {
             "core-util-is": {
               "version": "1.0.2",
               "version": "1.0.2",
-              "from": "core-util-is@1.0.2"
+              "from": "core-util-is@~1.0.0"
             },
             },
             "extsprintf": {
             "extsprintf": {
               "version": "1.3.0",
               "version": "1.3.0",
@@ -1402,7 +1416,7 @@
           "dependencies": {
           "dependencies": {
             "nan": {
             "nan": {
               "version": "2.5.1",
               "version": "2.5.1",
-              "from": "nan@^2.3.3"
+              "from": "nan@^2.0.8"
             }
             }
           }
           }
         }
         }
@@ -1519,7 +1533,8 @@
         },
         },
         "coffee-script": {
         "coffee-script": {
           "version": "1.4.0",
           "version": "1.4.0",
-          "from": "coffee-script@1.4.0"
+          "from": "coffee-script@1.4.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz"
         },
         },
         "raven": {
         "raven": {
           "version": "0.8.1",
           "version": "0.8.1",
@@ -1533,6 +1548,10 @@
               "version": "0.0.3",
               "version": "0.0.3",
               "from": "lsmod@~0.0.3"
               "from": "lsmod@~0.0.3"
             },
             },
+            "node-uuid": {
+              "version": "1.4.7",
+              "from": "node-uuid@~1.4.1"
+            },
             "stack-trace": {
             "stack-trace": {
               "version": "0.0.7",
               "version": "0.0.7",
               "from": "stack-trace@0.0.7"
               "from": "stack-trace@0.0.7"
@@ -1594,7 +1613,8 @@
       "dependencies": {
       "dependencies": {
         "coffee-script": {
         "coffee-script": {
           "version": "1.6.0",
           "version": "1.6.0",
-          "from": "coffee-script@1.6.0"
+          "from": "coffee-script@1.6.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
         }
         }
       }
       }
     },
     },
@@ -1608,7 +1628,8 @@
           "dependencies": {
           "dependencies": {
             "iconv-lite": {
             "iconv-lite": {
               "version": "0.4.15",
               "version": "0.4.15",
-              "from": "iconv-lite@~0.4.13"
+              "from": "iconv-lite@~0.4.13",
+              "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz"
             }
             }
           }
           }
         },
         },
@@ -1633,6 +1654,7 @@
         "jade": {
         "jade": {
           "version": "0.26.3",
           "version": "0.26.3",
           "from": "jade@0.26.3",
           "from": "jade@0.26.3",
+          "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
           "dependencies": {
           "dependencies": {
             "commander": {
             "commander": {
               "version": "0.6.1",
               "version": "0.6.1",
@@ -1660,7 +1682,7 @@
         },
         },
         "mkdirp": {
         "mkdirp": {
           "version": "0.3.5",
           "version": "0.3.5",
-          "from": "mkdirp@0.3.5"
+          "from": "mkdirp@~0.3.5"
         },
         },
         "glob": {
         "glob": {
           "version": "3.2.3",
           "version": "3.2.3",
@@ -1695,6 +1717,7 @@
     "mongojs": {
     "mongojs": {
       "version": "0.18.2",
       "version": "0.18.2",
       "from": "mongojs@0.18.2",
       "from": "mongojs@0.18.2",
+      "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz",
       "dependencies": {
       "dependencies": {
         "thunky": {
         "thunky": {
           "version": "0.1.0",
           "version": "0.1.0",
@@ -1702,7 +1725,7 @@
         },
         },
         "readable-stream": {
         "readable-stream": {
           "version": "1.1.14",
           "version": "1.1.14",
-          "from": "readable-stream@~1.1.9",
+          "from": "readable-stream@1.1.x",
           "dependencies": {
           "dependencies": {
             "core-util-is": {
             "core-util-is": {
               "version": "1.0.2",
               "version": "1.0.2",
@@ -1710,7 +1733,8 @@
             },
             },
             "isarray": {
             "isarray": {
               "version": "0.0.1",
               "version": "0.0.1",
-              "from": "isarray@0.0.1"
+              "from": "isarray@0.0.1",
+              "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
             },
             },
             "string_decoder": {
             "string_decoder": {
               "version": "0.10.31",
               "version": "0.10.31",
@@ -1725,6 +1749,7 @@
         "mongodb": {
         "mongodb": {
           "version": "1.4.32",
           "version": "1.4.32",
           "from": "mongodb@1.4.32",
           "from": "mongodb@1.4.32",
+          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz",
           "dependencies": {
           "dependencies": {
             "bson": {
             "bson": {
               "version": "0.2.22",
               "version": "0.2.22",
@@ -1739,6 +1764,7 @@
             "kerberos": {
             "kerberos": {
               "version": "0.0.9",
               "version": "0.0.9",
               "from": "kerberos@0.0.9",
               "from": "kerberos@0.0.9",
+              "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz",
               "dependencies": {
               "dependencies": {
                 "nan": {
                 "nan": {
                   "version": "1.6.2",
                   "version": "1.6.2",
@@ -1854,7 +1880,8 @@
                 },
                 },
                 "isarray": {
                 "isarray": {
                   "version": "0.0.1",
                   "version": "0.0.1",
-                  "from": "isarray@0.0.1"
+                  "from": "isarray@0.0.1",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
                 },
                 },
                 "string_decoder": {
                 "string_decoder": {
                   "version": "0.10.31",
                   "version": "0.10.31",
@@ -1942,7 +1969,8 @@
                 },
                 },
                 "isarray": {
                 "isarray": {
                   "version": "0.0.1",
                   "version": "0.0.1",
-                  "from": "isarray@0.0.1"
+                  "from": "isarray@0.0.1",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
                 },
                 },
                 "string_decoder": {
                 "string_decoder": {
                   "version": "0.10.31",
                   "version": "0.10.31",
@@ -1950,7 +1978,7 @@
                 },
                 },
                 "inherits": {
                 "inherits": {
                   "version": "2.0.3",
                   "version": "2.0.3",
-                  "from": "inherits@~2.0.1"
+                  "from": "inherits@2"
                 }
                 }
               }
               }
             }
             }
@@ -1986,10 +2014,6 @@
         }
         }
       }
       }
     },
     },
-    "node-uuid": {
-      "version": "1.4.1",
-      "from": "node-uuid@1.4.1"
-    },
     "nodemailer": {
     "nodemailer": {
       "version": "2.1.0",
       "version": "2.1.0",
       "from": "nodemailer@2.1.0",
       "from": "nodemailer@2.1.0",
@@ -2127,7 +2151,7 @@
       "from": "nodemailer-ses-transport@^1.3.0",
       "from": "nodemailer-ses-transport@^1.3.0",
       "dependencies": {
       "dependencies": {
         "aws-sdk": {
         "aws-sdk": {
-          "version": "2.7.28",
+          "version": "2.9.0",
           "from": "aws-sdk@^2.6.12",
           "from": "aws-sdk@^2.6.12",
           "dependencies": {
           "dependencies": {
             "buffer": {
             "buffer": {
@@ -2342,6 +2366,388 @@
         }
         }
       }
       }
     },
     },
+    "pug": {
+      "version": "2.0.0-beta9",
+      "from": "pug@^2.0.0-beta6",
+      "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.0-beta9.tgz",
+      "dependencies": {
+        "pug-code-gen": {
+          "version": "1.1.1",
+          "from": "pug-code-gen@^1.1.1",
+          "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-1.1.1.tgz",
+          "dependencies": {
+            "constantinople": {
+              "version": "3.1.0",
+              "from": "constantinople@^3.0.1",
+              "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.0.tgz",
+              "dependencies": {
+                "acorn": {
+                  "version": "3.3.0",
+                  "from": "acorn@^3.1.0",
+                  "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"
+                },
+                "is-expression": {
+                  "version": "2.1.0",
+                  "from": "is-expression@^2.0.1",
+                  "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-2.1.0.tgz",
+                  "dependencies": {
+                    "object-assign": {
+                      "version": "4.1.1",
+                      "from": "object-assign@^4.0.1"
+                    }
+                  }
+                }
+              }
+            },
+            "doctypes": {
+              "version": "1.1.0",
+              "from": "doctypes@^1.1.0",
+              "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz"
+            },
+            "js-stringify": {
+              "version": "1.0.2",
+              "from": "js-stringify@^1.0.1",
+              "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz"
+            },
+            "pug-attrs": {
+              "version": "2.0.2",
+              "from": "pug-attrs@^2.0.2",
+              "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.2.tgz"
+            },
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            },
+            "void-elements": {
+              "version": "2.0.1",
+              "from": "void-elements@^2.0.1",
+              "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz"
+            },
+            "with": {
+              "version": "5.1.1",
+              "from": "with@^5.0.0",
+              "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz",
+              "dependencies": {
+                "acorn": {
+                  "version": "3.3.0",
+                  "from": "acorn@^3.1.0",
+                  "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"
+                },
+                "acorn-globals": {
+                  "version": "3.0.0",
+                  "from": "acorn-globals@^3.0.0",
+                  "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.0.0.tgz"
+                }
+              }
+            }
+          }
+        },
+        "pug-filters": {
+          "version": "2.1.0",
+          "from": "pug-filters@^2.1.0",
+          "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-2.1.0.tgz",
+          "dependencies": {
+            "constantinople": {
+              "version": "3.1.0",
+              "from": "constantinople@^3.0.1",
+              "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.0.tgz",
+              "dependencies": {
+                "acorn": {
+                  "version": "3.3.0",
+                  "from": "acorn@^3.1.0",
+                  "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz"
+                },
+                "is-expression": {
+                  "version": "2.1.0",
+                  "from": "is-expression@^2.0.1",
+                  "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-2.1.0.tgz",
+                  "dependencies": {
+                    "object-assign": {
+                      "version": "4.1.1",
+                      "from": "object-assign@^4.0.1"
+                    }
+                  }
+                }
+              }
+            },
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            },
+            "pug-walk": {
+              "version": "1.1.0",
+              "from": "pug-walk@^1.1.0",
+              "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz"
+            },
+            "jstransformer": {
+              "version": "1.0.0",
+              "from": "jstransformer@1.0.0",
+              "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
+              "dependencies": {
+                "is-promise": {
+                  "version": "2.1.0",
+                  "from": "is-promise@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz"
+                },
+                "promise": {
+                  "version": "7.1.1",
+                  "from": "promise@^7.0.1",
+                  "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz",
+                  "dependencies": {
+                    "asap": {
+                      "version": "2.0.5",
+                      "from": "asap@~2.0.3",
+                      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "resolve": {
+              "version": "1.2.0",
+              "from": "resolve@^1.1.6",
+              "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.2.0.tgz"
+            },
+            "uglify-js": {
+              "version": "2.7.5",
+              "from": "uglify-js@^2.6.1",
+              "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz",
+              "dependencies": {
+                "async": {
+                  "version": "0.2.10",
+                  "from": "async@~0.2.6"
+                },
+                "source-map": {
+                  "version": "0.5.6",
+                  "from": "source-map@~0.5.1"
+                },
+                "uglify-to-browserify": {
+                  "version": "1.0.2",
+                  "from": "uglify-to-browserify@~1.0.0"
+                },
+                "yargs": {
+                  "version": "3.10.0",
+                  "from": "yargs@~3.10.0",
+                  "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+                  "dependencies": {
+                    "camelcase": {
+                      "version": "1.2.1",
+                      "from": "camelcase@^1.0.2"
+                    },
+                    "cliui": {
+                      "version": "2.1.0",
+                      "from": "cliui@^2.1.0",
+                      "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+                      "dependencies": {
+                        "center-align": {
+                          "version": "0.1.3",
+                          "from": "center-align@^0.1.1",
+                          "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+                          "dependencies": {
+                            "align-text": {
+                              "version": "0.1.4",
+                              "from": "align-text@^0.1.1",
+                              "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+                              "dependencies": {
+                                "kind-of": {
+                                  "version": "3.1.0",
+                                  "from": "kind-of@^3.0.2",
+                                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.1.0.tgz",
+                                  "dependencies": {
+                                    "is-buffer": {
+                                      "version": "1.1.4",
+                                      "from": "is-buffer@^1.0.2",
+                                      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz"
+                                    }
+                                  }
+                                },
+                                "longest": {
+                                  "version": "1.0.1",
+                                  "from": "longest@^1.0.1",
+                                  "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+                                },
+                                "repeat-string": {
+                                  "version": "1.6.1",
+                                  "from": "repeat-string@^1.5.2",
+                                  "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
+                                }
+                              }
+                            },
+                            "lazy-cache": {
+                              "version": "1.0.4",
+                              "from": "lazy-cache@^1.0.3",
+                              "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz"
+                            }
+                          }
+                        },
+                        "right-align": {
+                          "version": "0.1.3",
+                          "from": "right-align@^0.1.1",
+                          "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+                          "dependencies": {
+                            "align-text": {
+                              "version": "0.1.4",
+                              "from": "align-text@^0.1.1",
+                              "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+                              "dependencies": {
+                                "kind-of": {
+                                  "version": "3.1.0",
+                                  "from": "kind-of@^3.0.2",
+                                  "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.1.0.tgz",
+                                  "dependencies": {
+                                    "is-buffer": {
+                                      "version": "1.1.4",
+                                      "from": "is-buffer@^1.0.2",
+                                      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz"
+                                    }
+                                  }
+                                },
+                                "longest": {
+                                  "version": "1.0.1",
+                                  "from": "longest@^1.0.1",
+                                  "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+                                },
+                                "repeat-string": {
+                                  "version": "1.6.1",
+                                  "from": "repeat-string@^1.5.2",
+                                  "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "wordwrap": {
+                          "version": "0.0.2",
+                          "from": "wordwrap@0.0.2"
+                        }
+                      }
+                    },
+                    "decamelize": {
+                      "version": "1.2.0",
+                      "from": "decamelize@^1.0.0"
+                    },
+                    "window-size": {
+                      "version": "0.1.0",
+                      "from": "window-size@0.1.0",
+                      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "pug-lexer": {
+          "version": "2.3.2",
+          "from": "pug-lexer@^2.3.1",
+          "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-2.3.2.tgz",
+          "dependencies": {
+            "character-parser": {
+              "version": "2.2.0",
+              "from": "character-parser@^2.1.1",
+              "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
+              "dependencies": {
+                "is-regex": {
+                  "version": "1.0.3",
+                  "from": "is-regex@^1.0.3",
+                  "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.3.tgz"
+                }
+              }
+            },
+            "is-expression": {
+              "version": "3.0.0",
+              "from": "is-expression@^3.0.0",
+              "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz",
+              "dependencies": {
+                "acorn": {
+                  "version": "4.0.4",
+                  "from": "acorn@~4.0.2",
+                  "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.4.tgz"
+                },
+                "object-assign": {
+                  "version": "4.1.1",
+                  "from": "object-assign@^4.0.1"
+                }
+              }
+            },
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            }
+          }
+        },
+        "pug-linker": {
+          "version": "2.0.1",
+          "from": "pug-linker@^2.0.1",
+          "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-2.0.1.tgz",
+          "dependencies": {
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            },
+            "pug-walk": {
+              "version": "1.1.0",
+              "from": "pug-walk@^1.1.0",
+              "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz"
+            }
+          }
+        },
+        "pug-load": {
+          "version": "2.0.4",
+          "from": "pug-load@^2.0.4",
+          "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.4.tgz",
+          "dependencies": {
+            "object-assign": {
+              "version": "4.1.1",
+              "from": "object-assign@^4.1.0"
+            },
+            "pug-walk": {
+              "version": "1.1.0",
+              "from": "pug-walk@^1.1.0",
+              "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz"
+            }
+          }
+        },
+        "pug-parser": {
+          "version": "2.0.2",
+          "from": "pug-parser@^2.0.2",
+          "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-2.0.2.tgz",
+          "dependencies": {
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            },
+            "token-stream": {
+              "version": "0.0.1",
+              "from": "token-stream@0.0.1",
+              "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz"
+            }
+          }
+        },
+        "pug-runtime": {
+          "version": "2.0.3",
+          "from": "pug-runtime@^2.0.3",
+          "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.3.tgz"
+        },
+        "pug-strip-comments": {
+          "version": "1.0.2",
+          "from": "pug-strip-comments@^1.0.2",
+          "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.2.tgz",
+          "dependencies": {
+            "pug-error": {
+              "version": "1.3.2",
+              "from": "pug-error@^1.3.2",
+              "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz"
+            }
+          }
+        }
+      }
+    },
     "redis": {
     "redis": {
       "version": "0.10.1",
       "version": "0.10.1",
       "from": "redis@0.10.1"
       "from": "redis@0.10.1"
@@ -2349,6 +2755,7 @@
     "redis-sharelatex": {
     "redis-sharelatex": {
       "version": "0.0.9",
       "version": "0.0.9",
       "from": "redis-sharelatex@0.0.9",
       "from": "redis-sharelatex@0.0.9",
+      "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz",
       "dependencies": {
       "dependencies": {
         "chai": {
         "chai": {
           "version": "1.9.1",
           "version": "1.9.1",
@@ -2360,7 +2767,7 @@
             },
             },
             "deep-eql": {
             "deep-eql": {
               "version": "0.1.3",
               "version": "0.1.3",
-              "from": "deep-eql@0.1.3",
+              "from": "deep-eql@^0.1.3",
               "dependencies": {
               "dependencies": {
                 "type-detect": {
                 "type-detect": {
                   "version": "0.1.1",
                   "version": "0.1.1",
@@ -2456,7 +2863,7 @@
                 },
                 },
                 "mkdirp": {
                 "mkdirp": {
                   "version": "0.5.1",
                   "version": "0.5.1",
-                  "from": "mkdirp@^0.5.0",
+                  "from": "mkdirp@~0.5.1",
                   "dependencies": {
                   "dependencies": {
                     "minimist": {
                     "minimist": {
                       "version": "0.0.8",
                       "version": "0.0.8",
@@ -2557,6 +2964,7 @@
             "jade": {
             "jade": {
               "version": "0.26.3",
               "version": "0.26.3",
               "from": "jade@0.26.3",
               "from": "jade@0.26.3",
+              "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
               "dependencies": {
               "dependencies": {
                 "commander": {
                 "commander": {
                   "version": "0.6.1",
                   "version": "0.6.1",
@@ -2584,7 +2992,7 @@
             },
             },
             "mkdirp": {
             "mkdirp": {
               "version": "0.3.5",
               "version": "0.3.5",
-              "from": "mkdirp@0.3.5"
+              "from": "mkdirp@~0.3.5"
             },
             },
             "glob": {
             "glob": {
               "version": "3.2.3",
               "version": "3.2.3",
@@ -2618,11 +3026,13 @@
         },
         },
         "redis": {
         "redis": {
           "version": "0.12.1",
           "version": "0.12.1",
-          "from": "redis@0.12.1"
+          "from": "redis@0.12.1",
+          "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz"
         },
         },
         "redis-sentinel": {
         "redis-sentinel": {
           "version": "0.1.1",
           "version": "0.1.1",
           "from": "redis-sentinel@0.1.1",
           "from": "redis-sentinel@0.1.1",
+          "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz",
           "dependencies": {
           "dependencies": {
             "redis": {
             "redis": {
               "version": "0.11.0",
               "version": "0.11.0",
@@ -2955,10 +3365,6 @@
         "tunnel-agent": {
         "tunnel-agent": {
           "version": "0.4.3",
           "version": "0.4.3",
           "from": "tunnel-agent@~0.4.1"
           "from": "tunnel-agent@~0.4.1"
-        },
-        "uuid": {
-          "version": "3.0.1",
-          "from": "uuid@^3.0.0"
         }
         }
       }
       }
     },
     },
@@ -3039,7 +3445,7 @@
         },
         },
         "depd": {
         "depd": {
           "version": "1.1.0",
           "version": "1.1.0",
-          "from": "depd@^1.1.0"
+          "from": "depd@~1.1.0"
         },
         },
         "dottie": {
         "dottie": {
           "version": "1.1.1",
           "version": "1.1.1",
@@ -3047,10 +3453,11 @@
         },
         },
         "generic-pool": {
         "generic-pool": {
           "version": "2.4.2",
           "version": "2.4.2",
-          "from": "generic-pool@2.4.2"
+          "from": "generic-pool@2.4.2",
+          "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz"
         },
         },
         "inflection": {
         "inflection": {
-          "version": "1.10.0",
+          "version": "1.12.0",
           "from": "inflection@^1.6.0"
           "from": "inflection@^1.6.0"
         },
         },
         "lodash": {
         "lodash": {
@@ -3127,7 +3534,8 @@
         },
         },
         "shimmer": {
         "shimmer": {
           "version": "1.1.0",
           "version": "1.1.0",
-          "from": "shimmer@1.1.0"
+          "from": "shimmer@1.1.0",
+          "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz"
         },
         },
         "terraformer-wkt-parser": {
         "terraformer-wkt-parser": {
           "version": "1.1.2",
           "version": "1.1.2",
@@ -3149,7 +3557,8 @@
         },
         },
         "wkx": {
         "wkx": {
           "version": "0.2.0",
           "version": "0.2.0",
-          "from": "wkx@0.2.0"
+          "from": "wkx@0.2.0",
+          "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz"
         }
         }
       }
       }
     },
     },
@@ -3160,7 +3569,8 @@
       "dependencies": {
       "dependencies": {
         "coffee-script": {
         "coffee-script": {
           "version": "1.6.0",
           "version": "1.6.0",
-          "from": "coffee-script@1.6.0"
+          "from": "coffee-script@1.6.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
         }
         }
       }
       }
     },
     },
@@ -3182,6 +3592,10 @@
       "version": "1.6.0",
       "version": "1.6.0",
       "from": "underscore@1.6.0"
       "from": "underscore@1.6.0"
     },
     },
+    "uuid": {
+      "version": "3.0.1",
+      "from": "uuid@^3.0.1"
+    },
     "v8-profiler": {
     "v8-profiler": {
       "version": "5.6.5",
       "version": "5.6.5",
       "from": "v8-profiler@^5.2.3",
       "from": "v8-profiler@^5.2.3",
@@ -3442,7 +3856,7 @@
                 },
                 },
                 "inherits": {
                 "inherits": {
                   "version": "2.0.3",
                   "version": "2.0.3",
-                  "from": "inherits@2"
+                  "from": "inherits@~2.0.0"
                 }
                 }
               }
               }
             },
             },
@@ -3484,7 +3898,7 @@
                     },
                     },
                     "minimatch": {
                     "minimatch": {
                       "version": "3.0.3",
                       "version": "3.0.3",
-                      "from": "minimatch@^3.0.2",
+                      "from": "minimatch@^3.0.0",
                       "dependencies": {
                       "dependencies": {
                         "brace-expansion": {
                         "brace-expansion": {
                           "version": "1.1.6",
                           "version": "1.1.6",

+ 2 - 1
services/web/package.json

@@ -42,7 +42,6 @@
     "mongojs": "0.18.2",
     "mongojs": "0.18.2",
     "mongoose": "4.1.0",
     "mongoose": "4.1.0",
     "multer": "^0.1.8",
     "multer": "^0.1.8",
-    "node-uuid": "1.4.1",
     "nodemailer": "2.1.0",
     "nodemailer": "2.1.0",
     "nodemailer-sendgrid-transport": "^0.2.0",
     "nodemailer-sendgrid-transport": "^0.2.0",
     "nodemailer-ses-transport": "^1.3.0",
     "nodemailer-ses-transport": "^1.3.0",
@@ -64,6 +63,8 @@
     "v8-profiler": "^5.2.3",
     "v8-profiler": "^5.2.3",
     "xml2js": "0.2.0",
     "xml2js": "0.2.0",
     "passport-saml": "^0.15.0",
     "passport-saml": "^0.15.0",
+    "pug": "^2.0.0-beta6",
+    "uuid": "^3.0.1",
     "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master"
     "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 3 - 0
services/web/public/coffee/ide/editor/Document.coffee

@@ -84,6 +84,9 @@ define [
 		setTrackingChanges: (track_changes) ->
 		setTrackingChanges: (track_changes) ->
 			@doc.track_changes = track_changes
 			@doc.track_changes = track_changes
 		
 		
+		getTrackingChanges: () ->
+			!!@doc.track_changes
+		
 		setTrackChangesIdSeeds: (id_seeds) ->
 		setTrackChangesIdSeeds: (id_seeds) ->
 			@doc.track_changes_id_seeds = id_seeds
 			@doc.track_changes_id_seeds = id_seeds
 
 

+ 2 - 1
services/web/public/coffee/ide/editor/EditorManager.coffee

@@ -162,8 +162,9 @@ define [
 				@_syncTimeout = null
 				@_syncTimeout = null
 
 
 			want = @$scope.editor.wantTrackChanges
 			want = @$scope.editor.wantTrackChanges
-			have = @$scope.editor.trackChanges
+			have = doc.getTrackingChanges()
 			if want == have
 			if want == have
+				@$scope.editor.trackChanges = want
 				return
 				return
 
 
 			do tryToggle = () =>
 			do tryToggle = () =>

+ 2 - 2
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -13,7 +13,7 @@ define [
 	EditSession = ace.require('ace/edit_session').EditSession
 	EditSession = ace.require('ace/edit_session').EditSession
 	ModeList = ace.require('ace/ext/modelist')
 	ModeList = ace.require('ace/ext/modelist')
 
 
-	# set the path for ace workers if using a CDN (from editor.jade)
+	# set the path for ace workers if using a CDN (from editor.pug)
 	if window.aceWorkerPath != ""
 	if window.aceWorkerPath != ""
 		syntaxValidationEnabled = true
 		syntaxValidationEnabled = true
 		ace.config.set('workerPath', "#{window.aceWorkerPath}")
 		ace.config.set('workerPath', "#{window.aceWorkerPath}")
@@ -279,7 +279,7 @@ define [
 
 
 					session.setUseWrapMode(true)
 					session.setUseWrapMode(true)
 					# use syntax validation only when explicitly set
 					# use syntax validation only when explicitly set
-					if scope.syntaxValidation? and syntaxValidationEnabled
+					if scope.syntaxValidation? and syntaxValidationEnabled and !scope.fileName.match(/\.bib$/)
 						session.setOption("useWorker", scope.syntaxValidation);
 						session.setOption("useWorker", scope.syntaxValidation);
 
 
 					# now attach session to editor
 					# now attach session to editor

+ 35 - 20
services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee

@@ -35,8 +35,8 @@ define [
 			@$scope.$on "comment:remove", (e, comment_id) =>
 			@$scope.$on "comment:remove", (e, comment_id) =>
 				@removeCommentId(comment_id)
 				@removeCommentId(comment_id)
 			
 			
-			@$scope.$on "comment:resolve_thread", (e, thread_id) =>
-				@resolveCommentByThreadId(thread_id)
+			@$scope.$on "comment:resolve_threads", (e, thread_ids) =>
+				@resolveCommentByThreadIds(thread_ids)
 			
 			
 			@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
 			@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
 				@unresolveCommentByThreadId(thread_id)
 				@unresolveCommentByThreadId(thread_id)
@@ -105,29 +105,45 @@ define [
 			# ace has updated
 			# ace has updated
 			@rangesTracker.on "insert:added", (change) =>
 			@rangesTracker.on "insert:added", (change) =>
 				sl_console.log "[insert:added]", change
 				sl_console.log "[insert:added]", change
-				setTimeout () => @_onInsertAdded(change)
+				setTimeout () =>
+					@_onInsertAdded(change)
+					@broadcastChange()
 			@rangesTracker.on "insert:removed", (change) =>
 			@rangesTracker.on "insert:removed", (change) =>
 				sl_console.log "[insert:removed]", change
 				sl_console.log "[insert:removed]", change
-				setTimeout () => @_onInsertRemoved(change)
+				setTimeout () =>
+					@_onInsertRemoved(change)
+					@broadcastChange()
 			@rangesTracker.on "delete:added", (change) =>
 			@rangesTracker.on "delete:added", (change) =>
 				sl_console.log "[delete:added]", change
 				sl_console.log "[delete:added]", change
-				setTimeout () => @_onDeleteAdded(change)
+				setTimeout () =>
+					@_onDeleteAdded(change)
+					@broadcastChange()
 			@rangesTracker.on "delete:removed", (change) =>
 			@rangesTracker.on "delete:removed", (change) =>
 				sl_console.log "[delete:removed]", change
 				sl_console.log "[delete:removed]", change
-				setTimeout () => @_onDeleteRemoved(change)
+				setTimeout () =>
+					@_onDeleteRemoved(change)
+					@broadcastChange()
 			@rangesTracker.on "changes:moved", (changes) =>
 			@rangesTracker.on "changes:moved", (changes) =>
 				sl_console.log "[changes:moved]", changes
 				sl_console.log "[changes:moved]", changes
-				setTimeout () => @_onChangesMoved(changes)
+				setTimeout () =>
+					@_onChangesMoved(changes)
+					@broadcastChange()
 
 
 			@rangesTracker.on "comment:added", (comment) =>
 			@rangesTracker.on "comment:added", (comment) =>
 				sl_console.log "[comment:added]", comment
 				sl_console.log "[comment:added]", comment
-				setTimeout () => @_onCommentAdded(comment)
+				setTimeout () =>
+					@_onCommentAdded(comment)
+					@broadcastChange()
 			@rangesTracker.on "comment:moved", (comment) =>
 			@rangesTracker.on "comment:moved", (comment) =>
 				sl_console.log "[comment:moved]", comment
 				sl_console.log "[comment:moved]", comment
-				setTimeout () => @_onCommentMoved(comment)
+				setTimeout () =>
+					@_onCommentMoved(comment)
+					@broadcastChange()
 			@rangesTracker.on "comment:removed", (comment) =>
 			@rangesTracker.on "comment:removed", (comment) =>
 				sl_console.log "[comment:removed]", comment
 				sl_console.log "[comment:removed]", comment
-				setTimeout () => @_onCommentRemoved(comment)
+				setTimeout () =>
+					@_onCommentRemoved(comment)
+					@broadcastChange()
 			
 			
 			@rangesTracker.on "clear", () =>
 			@rangesTracker.on "clear", () =>
 				@clearAnnotations()
 				@clearAnnotations()
@@ -150,6 +166,8 @@ define [
 
 
 			for comment in @rangesTracker.comments
 			for comment in @rangesTracker.comments
 				@_onCommentAdded(comment)
 				@_onCommentAdded(comment)
+			
+			@broadcastChange()
 
 
 		addComment: (offset, content, thread_id) ->
 		addComment: (offset, content, thread_id) ->
 			op = { c: content, p: offset, t: thread_id }
 			op = { c: content, p: offset, t: thread_id }
@@ -190,15 +208,20 @@ define [
 		removeCommentId: (comment_id) ->
 		removeCommentId: (comment_id) ->
 			@rangesTracker.removeCommentId(comment_id)
 			@rangesTracker.removeCommentId(comment_id)
 
 
-		resolveCommentByThreadId: (thread_id) ->
+		resolveCommentByThreadIds: (thread_ids) ->
+			resolve_ids = {}
+			for id in thread_ids
+				resolve_ids[id] = true
 			for comment in @rangesTracker?.comments or []
 			for comment in @rangesTracker?.comments or []
-				if comment.op.t == thread_id
+				if resolve_ids[comment.op.t]
 					@_onCommentRemoved(comment)
 					@_onCommentRemoved(comment)
+			@broadcastChange()
 			
 			
 		unresolveCommentByThreadId: (thread_id) ->
 		unresolveCommentByThreadId: (thread_id) ->
 			for comment in @rangesTracker?.comments or []
 			for comment in @rangesTracker?.comments or []
 				if comment.op.t == thread_id
 				if comment.op.t == thread_id
 					@_onCommentAdded(comment)
 					@_onCommentAdded(comment)
+			@broadcastChange()
 
 
 		checkMapping: () ->
 		checkMapping: () ->
 			# TODO: reintroduce this check
 			# TODO: reintroduce this check
@@ -303,7 +326,6 @@ define [
 			background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text"
 			background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text"
 			callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout")
 			callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout")
 			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
 			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
-			@broadcastChange()
 
 
 		_onDeleteAdded: (change) ->
 		_onDeleteAdded: (change) ->
 			position = @_shareJsOffsetToAcePosition(change.op.p)
 			position = @_shareJsOffsetToAcePosition(change.op.p)
@@ -318,7 +340,6 @@ define [
 
 
 			callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout")
 			callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout")
 			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
 			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
-			@broadcastChange()
 		
 		
 		_onInsertRemoved: (change) ->
 		_onInsertRemoved: (change) ->
 			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
 			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
@@ -326,7 +347,6 @@ define [
 			session = @editor.getSession()
 			session = @editor.getSession()
 			session.removeMarker background_marker_id
 			session.removeMarker background_marker_id
 			session.removeMarker callout_marker_id
 			session.removeMarker callout_marker_id
-			@broadcastChange()
 		
 		
 		_onDeleteRemoved: (change) ->
 		_onDeleteRemoved: (change) ->
 			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
 			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
@@ -334,7 +354,6 @@ define [
 			session = @editor.getSession()
 			session = @editor.getSession()
 			session.removeMarker background_marker_id
 			session.removeMarker background_marker_id
 			session.removeMarker callout_marker_id
 			session.removeMarker callout_marker_id
-			@broadcastChange()
 		
 		
 		_onCommentAdded: (comment) ->
 		_onCommentAdded: (comment) ->
 			if @rangesTracker.resolvedThreadIds[comment.op.t]
 			if @rangesTracker.resolvedThreadIds[comment.op.t]
@@ -350,7 +369,6 @@ define [
 				background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text"
 				background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text"
 				callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout")
 				callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout")
 				@changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id }
 				@changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id }
-			@broadcastChange()
 		
 		
 		_onCommentRemoved: (comment) ->
 		_onCommentRemoved: (comment) ->
 			if @changeIdToMarkerIdMap[comment.id]?
 			if @changeIdToMarkerIdMap[comment.id]?
@@ -360,7 +378,6 @@ define [
 				session = @editor.getSession()
 				session = @editor.getSession()
 				session.removeMarker background_marker_id
 				session.removeMarker background_marker_id
 				session.removeMarker callout_marker_id
 				session.removeMarker callout_marker_id
-			@broadcastChange()
 
 
 		_aceRangeToShareJs: (range) ->
 		_aceRangeToShareJs: (range) ->
 			lines = @editor.getSession().getDocument().getLines 0, range.row
 			lines = @editor.getSession().getDocument().getLines 0, range.row
@@ -385,14 +402,12 @@ define [
 					end = start
 					end = start
 				@_updateMarker(change.id, start, end)
 				@_updateMarker(change.id, start, end)
 			@editor.renderer.updateBackMarkers()
 			@editor.renderer.updateBackMarkers()
-			@broadcastChange()
 		
 		
 		_onCommentMoved: (comment) ->
 		_onCommentMoved: (comment) ->
 			start = @_shareJsOffsetToAcePosition(comment.op.p)
 			start = @_shareJsOffsetToAcePosition(comment.op.p)
 			end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
 			end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
 			@_updateMarker(comment.id, start, end)
 			@_updateMarker(comment.id, start, end)
 			@editor.renderer.updateBackMarkers()
 			@editor.renderer.updateBackMarkers()
-			@broadcastChange()
 	
 	
 		_updateMarker: (change_id, start, end) ->
 		_updateMarker: (change_id, start, end) ->
 			return if !@changeIdToMarkerIdMap[change_id]?
 			return if !@changeIdToMarkerIdMap[change_id]?

+ 9 - 3
services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee

@@ -70,6 +70,10 @@ define [
 		ide.socket.on "reopen-thread", (thread_id) ->
 		ide.socket.on "reopen-thread", (thread_id) ->
 			_onCommentReopened(thread_id)
 			_onCommentReopened(thread_id)
 		
 		
+		ide.socket.on "delete-thread", (thread_id) ->
+			_onThreadDeleted(thread_id)
+			$scope.$apply () ->
+		
 		ide.socket.on "edit-message", (thread_id, message_id, content) ->
 		ide.socket.on "edit-message", (thread_id, message_id, content) ->
 			_onCommentEdited(thread_id, message_id, content)
 			_onCommentEdited(thread_id, message_id, content)
 			$scope.$apply () ->
 			$scope.$apply () ->
@@ -226,8 +230,10 @@ define [
 				delete delete_changes[comment.id]
 				delete delete_changes[comment.id]
 				if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
 				if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
 					new_comment = resolvedComments[comment.id] ?= {}
 					new_comment = resolvedComments[comment.id] ?= {}
+					delete entries[comment.id]
 				else
 				else
 					new_comment = entries[comment.id] ?= {}
 					new_comment = entries[comment.id] ?= {}
+					delete resolvedComments[comment.id]
 				new_entry = {
 				new_entry = {
 					type: "comment"
 					type: "comment"
 					thread_id: comment.op.t
 					thread_id: comment.op.t
@@ -360,7 +366,7 @@ define [
 			thread.resolved_by_user = formatUser(user)
 			thread.resolved_by_user = formatUser(user)
 			thread.resolved_at = new Date()
 			thread.resolved_at = new Date()
 			$scope.reviewPanel.resolvedThreadIds[thread_id] = true
 			$scope.reviewPanel.resolvedThreadIds[thread_id] = true
-			$scope.$broadcast "comment:resolve_thread", thread_id
+			$scope.$broadcast "comment:resolve_threads", [thread_id]
 		
 		
 		_onCommentReopened = (thread_id) ->
 		_onCommentReopened = (thread_id) ->
 			thread = getThread(thread_id)
 			thread = getThread(thread_id)
@@ -374,6 +380,7 @@ define [
 		_onThreadDeleted = (thread_id) ->
 		_onThreadDeleted = (thread_id) ->
 			delete $scope.reviewPanel.resolvedThreadIds[thread_id]
 			delete $scope.reviewPanel.resolvedThreadIds[thread_id]
 			delete $scope.reviewPanel.commentThreads[thread_id]
 			delete $scope.reviewPanel.commentThreads[thread_id]
+			$scope.$broadcast "comment:remove", thread_id
 		
 		
 		_onCommentEdited = (thread_id, comment_id, content) ->
 		_onCommentEdited = (thread_id, comment_id, content) ->
 			thread = getThread(thread_id)
 			thread = getThread(thread_id)
@@ -398,7 +405,6 @@ define [
 					'X-CSRF-Token': window.csrfToken
 					'X-CSRF-Token': window.csrfToken
 				}
 				}
 			})
 			})
-			$scope.$broadcast "comment:remove", entry_id
 			event_tracking.sendMB "rp-comment-delete"
 			event_tracking.sendMB "rp-comment-delete"
 		
 		
 		$scope.saveEdit = (thread_id, comment) ->
 		$scope.saveEdit = (thread_id, comment) ->
@@ -481,9 +487,9 @@ define [
 						for comment in thread.messages
 						for comment in thread.messages
 							formatComment(comment)
 							formatComment(comment)
 						if thread.resolved_by_user?
 						if thread.resolved_by_user?
-							$scope.$broadcast "comment:resolve_thread", thread_id
 							thread.resolved_by_user = formatUser(thread.resolved_by_user)
 							thread.resolved_by_user = formatUser(thread.resolved_by_user)
 							$scope.reviewPanel.resolvedThreadIds[thread_id] = true
 							$scope.reviewPanel.resolvedThreadIds[thread_id] = true
+							$scope.$broadcast "comment:resolve_threads", [thread_id]
 					$scope.reviewPanel.commentThreads = threads
 					$scope.reviewPanel.commentThreads = threads
 					$timeout () ->
 					$timeout () ->
 						$scope.$broadcast "review-panel:layout"
 						$scope.$broadcast "review-panel:layout"

+ 5 - 0
services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee

@@ -11,11 +11,16 @@ define [
 			onAccept: "&"
 			onAccept: "&"
 			onReject: "&"
 			onReject: "&"
 			onIndicatorClick: "&"
 			onIndicatorClick: "&"
+			onBodyClick: "&"
 		link: (scope, element, attrs) ->
 		link: (scope, element, attrs) ->
 			scope.contentLimit = 40
 			scope.contentLimit = 40
 			scope.isCollapsed = true
 			scope.isCollapsed = true
 			scope.needsCollapsing = false
 			scope.needsCollapsing = false
 
 
+			element.on "click", (e) ->
+				if $(e.target).is('.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i')
+					scope.onBodyClick()
+
 			scope.toggleCollapse = () ->
 			scope.toggleCollapse = () ->
 				scope.isCollapsed = !scope.isCollapsed
 				scope.isCollapsed = !scope.isCollapsed
 				$timeout () ->
 				$timeout () ->

+ 5 - 0
services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee

@@ -13,10 +13,15 @@ define [
 			onIndicatorClick: "&"
 			onIndicatorClick: "&"
 			onSaveEdit: "&"
 			onSaveEdit: "&"
 			onDelete: "&"
 			onDelete: "&"
+			onBodyClick: "&"
 		link: (scope, element, attrs) ->
 		link: (scope, element, attrs) ->
 			scope.state =
 			scope.state =
 				animating: false
 				animating: false
 
 
+			element.on "click", (e) ->
+				if $(e.target).is('.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata')
+					scope.onBodyClick()
+
 			scope.handleCommentReplyKeyPress = (ev) ->
 			scope.handleCommentReplyKeyPress = (ev) ->
 				if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
 				if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
 					ev.preventDefault()
 					ev.preventDefault()

+ 75 - 56
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee

@@ -114,7 +114,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when all goes well', ->
 		describe 'when all goes well', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
 
@@ -128,7 +129,7 @@ describe "CollaboratorsInviteController", ->
 
 
 			it 'should have called _checkShouldInviteEmail', ->
 			it 'should have called _checkShouldInviteEmail', ->
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
-				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
+				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
 
 
 			it 'should have called inviteToProject', ->
 			it 'should have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@@ -141,7 +142,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when the user is not allowed to add more collaborators', ->
 		describe 'when the user is not allowed to add more collaborators', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
 
@@ -159,7 +161,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when canAddXCollaborators produces an error', ->
 		describe 'when canAddXCollaborators produces an error', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@err = new Error('woops')
 				@err = new Error('woops')
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
@@ -178,7 +181,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when inviteToProject produces an error', ->
 		describe 'when inviteToProject produces an error', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@err = new Error('woops')
 				@err = new Error('woops')
 				@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
 				@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
@@ -193,7 +197,7 @@ describe "CollaboratorsInviteController", ->
 
 
 			it 'should have called _checkShouldInviteEmail', ->
 			it 'should have called _checkShouldInviteEmail', ->
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
-				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
+				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
 
 
 			it 'should have called inviteToProject', ->
 			it 'should have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@@ -202,7 +206,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when _checkShouldInviteEmail disallows the invite', ->
 		describe 'when _checkShouldInviteEmail disallows the invite', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, false)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, false)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
 
@@ -212,7 +217,7 @@ describe "CollaboratorsInviteController", ->
 
 
 			it 'should have called _checkShouldInviteEmail', ->
 			it 'should have called _checkShouldInviteEmail', ->
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
-				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
+				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
 
 
 			it 'should not have called inviteToProject', ->
 			it 'should not have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
@@ -220,7 +225,8 @@ describe "CollaboratorsInviteController", ->
 		describe 'when _checkShouldInviteEmail produces an error', ->
 		describe 'when _checkShouldInviteEmail produces an error', ->
 
 
 			beforeEach ->
 			beforeEach ->
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, new Error('woops'))
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, new Error('woops'))
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
 
@@ -230,7 +236,7 @@ describe "CollaboratorsInviteController", ->
 
 
 			it 'should have called _checkShouldInviteEmail', ->
 			it 'should have called _checkShouldInviteEmail', ->
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
 				@CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
-				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true
+				@CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
 
 
 			it 'should not have called inviteToProject', ->
 			it 'should not have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
@@ -240,7 +246,8 @@ describe "CollaboratorsInviteController", ->
 			beforeEach ->
 			beforeEach ->
 				@req.session.user = {_id: 'abc', email: 'me@example.com'}
 				@req.session.user = {_id: 'abc', email: 'me@example.com'}
 				@req.body.email = 'me@example.com'
 				@req.body.email = 'me@example.com'
-				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
 
@@ -261,6 +268,22 @@ describe "CollaboratorsInviteController", ->
 			it 'should not have called emitToRoom', ->
 			it 'should not have called emitToRoom', ->
 				@EditorRealTimeController.emitToRoom.callCount.should.equal 0
 				@EditorRealTimeController.emitToRoom.callCount.should.equal 0
 
 
+		describe 'when _checkRateLimit returns false', ->
+
+			beforeEach ->
+				@CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+				@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, false)
+				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+			it 'should send a 429 response', ->
+				@res.sendStatus.calledWith(429).should.equal true
+
+			it 'should not call inviteToProject', ->
+				@CollaboratorsInviteHandler.inviteToProject.called.should.equal false
+
+			it 'should not call emitToRoom', ->
+				@EditorRealTimeController.emitToRoom.called.should.equal false
 
 
 	describe "viewInvite", ->
 	describe "viewInvite", ->
 
 
@@ -679,13 +702,12 @@ describe "CollaboratorsInviteController", ->
 		beforeEach ->
 		beforeEach ->
 			@email = 'user@example.com'
 			@email = 'user@example.com'
 
 
-
 		describe 'when we should be restricting to existing accounts', ->
 		describe 'when we should be restricting to existing accounts', ->
 
 
 			beforeEach ->
 			beforeEach ->
 				@settings.restrictInvitesToExistingAccounts = true
 				@settings.restrictInvitesToExistingAccounts = true
 				@call = (callback) =>
 				@call = (callback) =>
-					@CollaboratorsInviteController._checkShouldInviteEmail {}, @email, callback
+					@CollaboratorsInviteController._checkShouldInviteEmail @email, callback
 
 
 			describe 'when user account is present', ->
 			describe 'when user account is present', ->
 
 
@@ -730,46 +752,43 @@ describe "CollaboratorsInviteController", ->
 						expect(shouldAllow).to.equal undefined
 						expect(shouldAllow).to.equal undefined
 						done()
 						done()
 
 
-		describe 'when we should not be restricting on only registered users but do rate limit', ->
-
-			beforeEach ->
-				@settings.restrictInvitesToExistingAccounts = false
-				@sendingUser = 
-					_id:"32312313"
-					features:
-						collaborators:17.8
-				@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @sendingUser)
-
-			it 'should callback with `true` when rate limit under', (done) ->
-				@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
-				@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
-					@RateLimiter.addCount.called.should.equal true
-					result.should.equal true
-					done()
-
-			it 'should callback with `false` when rate limit hit', (done) ->
-				@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
-				@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
-					@RateLimiter.addCount.called.should.equal true
-					result.should.equal false
-					done()
-		
-			it 'should call rate limiter with 10x the collaborators', (done) ->
-				@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
-				@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
-					@RateLimiter.addCount.args[0][0].throttle.should.equal(178)
-					done()
-
-			it 'should call rate limiter with 200 when collaborators is -1', (done) ->
-				@sendingUser.features.collaborators = -1
-				@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
-				@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
-					@RateLimiter.addCount.args[0][0].throttle.should.equal(200)
-					done()
-
-			it 'should call rate limiter with 10 when user has no collaborators set', (done) ->
-				delete @sendingUser.features
-				@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
-				@CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=>
-					@RateLimiter.addCount.args[0][0].throttle.should.equal(10)
-					done()
+	describe '_checkRateLimit', ->
+		beforeEach ->
+			@settings.restrictInvitesToExistingAccounts = false
+			@sendingUserId = "32312313"
+			@LimitationsManager.allowedNumberOfCollaboratorsForUser = sinon.stub()
+			@LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, 17)
+
+		it 'should callback with `true` when rate limit under', (done) ->
+			@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+			@CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+				@RateLimiter.addCount.called.should.equal true
+				result.should.equal true
+				done()
+
+		it 'should callback with `false` when rate limit hit', (done) ->
+			@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
+			@CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+				@RateLimiter.addCount.called.should.equal true
+				result.should.equal false
+				done()
+	
+		it 'should call rate limiter with 10x the collaborators', (done) ->
+			@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+			@CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+				@RateLimiter.addCount.args[0][0].throttle.should.equal(170)
+				done()
+
+		it 'should call rate limiter with 200 when collaborators is -1', (done) ->
+			@LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, -1)
+			@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+			@CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+				@RateLimiter.addCount.args[0][0].throttle.should.equal(200)
+				done()
+
+		it 'should call rate limiter with 10 when user has no collaborators set', (done) ->
+			@LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null)
+			@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+			@CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+				@RateLimiter.addCount.args[0][0].throttle.should.equal(10)
+				done()

+ 29 - 6
services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee

@@ -6,17 +6,17 @@ Settings = require("settings-sharelatex")
 
 
 describe "LimitationsManager", ->
 describe "LimitationsManager", ->
 	beforeEach ->
 	beforeEach ->
-		@project = { _id: "project-id" }
-		@user = { _id: "user-id", features:{} }
+		@project = { _id: @project_id = "project-id" }
+		@user = { _id: @user_id = "user-id", features:{} }
 		@Project =
 		@Project =
 			findById: (project_id, fields, callback) =>
 			findById: (project_id, fields, callback) =>
 				if project_id == @project_id
 				if project_id == @project_id
 					callback null, @project
 					callback null, @project
 				else
 				else
 					callback null, null
 					callback null, null
-		@User =
-			findById: (user_id, callback) =>
-				if user_id == @user.id
+		@UserGetter =
+			getUser: (user_id, filter, callback) =>
+				if user_id == @user_id
 					callback null, @user
 					callback null, @user
 				else
 				else
 					callback null, null
 					callback null, null
@@ -26,7 +26,7 @@ describe "LimitationsManager", ->
 
 
 		@LimitationsManager = SandboxedModule.require modulePath, requires:
 		@LimitationsManager = SandboxedModule.require modulePath, requires:
 			'../../models/Project' : Project: @Project
 			'../../models/Project' : Project: @Project
-			'../../models/User' : User: @User
+			'../User/UserGetter' : @UserGetter
 			'./SubscriptionLocator':@SubscriptionLocator
 			'./SubscriptionLocator':@SubscriptionLocator
 			'settings-sharelatex' : @Settings = {}
 			'settings-sharelatex' : @Settings = {}
 			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
 			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
@@ -37,6 +37,7 @@ describe "LimitationsManager", ->
 		describe "when the project is owned by a user without a subscription", ->
 		describe "when the project is owned by a user without a subscription", ->
 			beforeEach ->
 			beforeEach ->
 				@Settings.defaultPlanCode = collaborators: 23
 				@Settings.defaultPlanCode = collaborators: 23
+				@project.owner_ref = @user_id
 				delete @user.features
 				delete @user.features
 				@callback = sinon.stub()
 				@callback = sinon.stub()
 				@LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback)
 				@LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback)
@@ -46,6 +47,7 @@ describe "LimitationsManager", ->
 
 
 		describe "when the project is owned by a user with a subscription", ->
 		describe "when the project is owned by a user with a subscription", ->
 			beforeEach ->
 			beforeEach ->
+				@project.owner_ref = @user_id
 				@user.features =
 				@user.features =
 					collaborators: 21
 					collaborators: 21
 				@callback = sinon.stub()
 				@callback = sinon.stub()
@@ -53,6 +55,27 @@ describe "LimitationsManager", ->
 
 
 			it "should return the number of collaborators the user is allowed", ->
 			it "should return the number of collaborators the user is allowed", ->
 				@callback.calledWith(null, @user.features.collaborators).should.equal true
 				@callback.calledWith(null, @user.features.collaborators).should.equal true
+	
+	describe "allowedNumberOfCollaboratorsForUser", ->
+		describe "when the user has no features", ->
+			beforeEach ->
+				@Settings.defaultPlanCode = collaborators: 23
+				delete @user.features
+				@callback = sinon.stub()
+				@LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback)
+
+			it "should return the default number", ->
+				@callback.calledWith(null, @Settings.defaultPlanCode.collaborators).should.equal true
+
+		describe "when the user has features", ->
+			beforeEach ->
+				@user.features =
+					collaborators: 21
+				@callback = sinon.stub()
+				@LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback)
+
+			it "should return the number of collaborators the user is allowed", ->
+				@callback.calledWith(null, @user.features.collaborators).should.equal true
 
 
 	describe "canAddXCollaborators", ->
 	describe "canAddXCollaborators", ->
 		beforeEach ->
 		beforeEach ->